mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +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();
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const binding = createMockNativeBinding();
|
||||
const bridge = new VoiceEngineV2AppSourceLifecycleBridge({
|
||||
|
||||
@@ -172,12 +172,17 @@ export class VoiceEngineV2AppSourceLifecycleBridge {
|
||||
unbind(captureId: string): boolean {
|
||||
assert.ok(!this.disposed, 'bridge must not be disposed');
|
||||
assertValidString('captureId', captureId, MAX_SOURCE_ID_LENGTH);
|
||||
return this.removeBinding(captureId);
|
||||
}
|
||||
|
||||
private removeBinding(captureId: string): boolean {
|
||||
const binding = this.bindingsByCapture.get(captureId);
|
||||
if (!binding) return false;
|
||||
this.bindingsByCapture.delete(captureId);
|
||||
this.closedCleanLastAtMs.delete(captureId);
|
||||
const removed = this.registry.remove(binding.sourceId);
|
||||
assert.equal(removed, true, 'unbind must remove the registry entry');
|
||||
this.dispatch({type: 'sourceLifecycle.removed', sourceId: binding.sourceId});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -238,15 +243,15 @@ export class VoiceEngineV2AppSourceLifecycleBridge {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
const wasDisposed = this.disposed;
|
||||
if (wasDisposed) return;
|
||||
if (this.disposed) return;
|
||||
this.disposed = true;
|
||||
for (const captureId of [...this.bindingsByCapture.keys()]) {
|
||||
this.removeBinding(captureId);
|
||||
}
|
||||
try {
|
||||
this.unsubscribe();
|
||||
} catch {}
|
||||
this.bindingsByCapture.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');
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user