mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 05:55:34 +09:00
fix(voice): dispatch sourceLifecycle.removed on unbind (#2407)
This commit is contained in:
1 parent
5bc1f21d46
commit
af4a52173c
2 files changed
+74
-4
No files matched your search
@@ -226,6 +226,71 @@ describe('VoiceEngineV2AppSourceLifecycleBridge', () => {
|
|||||||
bridge.dispose();
|
bridge.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('unbind dispatches sourceLifecycle.removed so the reducer drops the snapshot slot', () => {
|
||||||
|
const binding = createMockNativeBinding();
|
||||||
|
const dispatched: Array<VoiceEngineV2Event> = [];
|
||||||
|
const bridge = new VoiceEngineV2AppSourceLifecycleBridge({
|
||||||
|
dispatch: (event) => dispatched.push(event),
|
||||||
|
subscribe: binding.subscribe,
|
||||||
|
});
|
||||||
|
|
||||||
|
bridge.bind({captureId: 'cap-removed', sourceId: 'src-removed'});
|
||||||
|
dispatched.length = 0;
|
||||||
|
|
||||||
|
expect(bridge.unbind('cap-removed')).toBe(true);
|
||||||
|
|
||||||
|
expect(dispatched).toHaveLength(1);
|
||||||
|
expect(dispatched[0]?.type).toBe('sourceLifecycle.removed');
|
||||||
|
if (dispatched[0]?.type === 'sourceLifecycle.removed') {
|
||||||
|
expect(dispatched[0].sourceId).toBe('src-removed');
|
||||||
|
}
|
||||||
|
expect(bridge.unbind('cap-removed')).toBe(false);
|
||||||
|
bridge.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispose dispatches sourceLifecycle.removed for every remaining binding', () => {
|
||||||
|
const binding = createMockNativeBinding();
|
||||||
|
const dispatched: Array<VoiceEngineV2Event> = [];
|
||||||
|
const bridge = new VoiceEngineV2AppSourceLifecycleBridge({
|
||||||
|
dispatch: (event) => dispatched.push(event),
|
||||||
|
subscribe: binding.subscribe,
|
||||||
|
});
|
||||||
|
|
||||||
|
bridge.bind({captureId: 'cap-1', sourceId: 'src-1'});
|
||||||
|
bridge.bind({captureId: 'cap-2', sourceId: 'src-2'});
|
||||||
|
dispatched.length = 0;
|
||||||
|
|
||||||
|
bridge.dispose();
|
||||||
|
|
||||||
|
expect(dispatched.map((event) => event.type)).toEqual(['sourceLifecycle.removed', 'sourceLifecycle.removed']);
|
||||||
|
expect(dispatched.flatMap((event) => (event.type === 'sourceLifecycle.removed' ? [event.sourceId] : []))).toEqual([
|
||||||
|
'src-1',
|
||||||
|
'src-2',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispose is re-entrant safe and does not re-dispatch removals', () => {
|
||||||
|
const binding = createMockNativeBinding();
|
||||||
|
const dispatched: Array<VoiceEngineV2Event> = [];
|
||||||
|
let reentrant: VoiceEngineV2AppSourceLifecycleBridge | null = null;
|
||||||
|
const bridge = new VoiceEngineV2AppSourceLifecycleBridge({
|
||||||
|
dispatch: (event) => {
|
||||||
|
dispatched.push(event);
|
||||||
|
if (event.type === 'sourceLifecycle.removed') reentrant?.dispose();
|
||||||
|
},
|
||||||
|
subscribe: binding.subscribe,
|
||||||
|
});
|
||||||
|
|
||||||
|
bridge.bind({captureId: 'cap-reentrant', sourceId: 'src-reentrant'});
|
||||||
|
dispatched.length = 0;
|
||||||
|
reentrant = bridge;
|
||||||
|
|
||||||
|
bridge.dispose();
|
||||||
|
|
||||||
|
expect(dispatched.map((event) => event.type)).toEqual(['sourceLifecycle.removed']);
|
||||||
|
expect(binding.listenerCount()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('dispose unsubscribes from the native binding', () => {
|
it('dispose unsubscribes from the native binding', () => {
|
||||||
const binding = createMockNativeBinding();
|
const binding = createMockNativeBinding();
|
||||||
const bridge = new VoiceEngineV2AppSourceLifecycleBridge({
|
const bridge = new VoiceEngineV2AppSourceLifecycleBridge({
|
||||||
|
|||||||
@@ -172,12 +172,17 @@ export class VoiceEngineV2AppSourceLifecycleBridge {
|
|||||||
unbind(captureId: string): boolean {
|
unbind(captureId: string): boolean {
|
||||||
assert.ok(!this.disposed, 'bridge must not be disposed');
|
assert.ok(!this.disposed, 'bridge must not be disposed');
|
||||||
assertValidString('captureId', captureId, MAX_SOURCE_ID_LENGTH);
|
assertValidString('captureId', captureId, MAX_SOURCE_ID_LENGTH);
|
||||||
|
return this.removeBinding(captureId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeBinding(captureId: string): boolean {
|
||||||
const binding = this.bindingsByCapture.get(captureId);
|
const binding = this.bindingsByCapture.get(captureId);
|
||||||
if (!binding) return false;
|
if (!binding) return false;
|
||||||
this.bindingsByCapture.delete(captureId);
|
this.bindingsByCapture.delete(captureId);
|
||||||
this.closedCleanLastAtMs.delete(captureId);
|
this.closedCleanLastAtMs.delete(captureId);
|
||||||
const removed = this.registry.remove(binding.sourceId);
|
const removed = this.registry.remove(binding.sourceId);
|
||||||
assert.equal(removed, true, 'unbind must remove the registry entry');
|
assert.equal(removed, true, 'unbind must remove the registry entry');
|
||||||
|
this.dispatch({type: 'sourceLifecycle.removed', sourceId: binding.sourceId});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,15 +243,15 @@ export class VoiceEngineV2AppSourceLifecycleBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
const wasDisposed = this.disposed;
|
if (this.disposed) return;
|
||||||
if (wasDisposed) return;
|
|
||||||
this.disposed = true;
|
this.disposed = true;
|
||||||
|
for (const captureId of [...this.bindingsByCapture.keys()]) {
|
||||||
|
this.removeBinding(captureId);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
this.unsubscribe();
|
this.unsubscribe();
|
||||||
} catch {}
|
} catch {}
|
||||||
this.bindingsByCapture.clear();
|
|
||||||
this.closedCleanLastAtMs.clear();
|
this.closedCleanLastAtMs.clear();
|
||||||
assert.equal(this.disposed, true, 'disposed flag must be true after dispose');
|
|
||||||
assert.equal(this.bindingsByCapture.size, 0, 'bindings must be cleared after dispose');
|
assert.equal(this.bindingsByCapture.size, 0, 'bindings must be cleared after dispose');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user