mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-02 21:04:06 +03:00
feat(voice-engine): add a sourceLifecycle.removed event (#2374)
This commit is contained in:
@@ -847,6 +847,45 @@ describe('transitionVoiceEngineV2', () => {
|
||||
expect(selectVoiceEngineV2SourceLifecycle(snapshot, 'unknown')).toBeNull();
|
||||
});
|
||||
|
||||
it('sourceLifecycle.removed drops the lifecycle slot for that source only', () => {
|
||||
const snapshot = applyEvents(initialSnapshot(), [
|
||||
{
|
||||
type: 'sourceLifecycle.transitioned',
|
||||
sourceId: 'source-removed',
|
||||
kind: 'failed',
|
||||
since: 11n,
|
||||
attempts: 8,
|
||||
fault: 'captureDeviceLost',
|
||||
atMs: 11,
|
||||
},
|
||||
{
|
||||
type: 'sourceLifecycle.transitioned',
|
||||
sourceId: 'source-kept',
|
||||
kind: 'active',
|
||||
since: 12n,
|
||||
attempts: 0,
|
||||
fault: null,
|
||||
atMs: 12,
|
||||
},
|
||||
]);
|
||||
|
||||
const transition = transitionVoiceEngineV2(snapshot, {type: 'sourceLifecycle.removed', sourceId: 'source-removed'});
|
||||
|
||||
expect(transition.commands).toEqual([]);
|
||||
expect(selectVoiceEngineV2SourceLifecycle(transition.snapshot, 'source-removed')).toBeNull();
|
||||
expect(selectVoiceEngineV2SourceLifecycle(transition.snapshot, 'source-kept')?.kind).toBe('active');
|
||||
expect([...selectVoiceEngineV2FailedSourceIds(transition.snapshot)]).toEqual([]);
|
||||
});
|
||||
|
||||
it('sourceLifecycle.removed for an unknown source leaves the snapshot untouched', () => {
|
||||
const snapshot = initialSnapshot();
|
||||
|
||||
const transition = transitionVoiceEngineV2(snapshot, {type: 'sourceLifecycle.removed', sourceId: 'source-absent'});
|
||||
|
||||
expect(transition.commands).toEqual([]);
|
||||
expect(transition.snapshot).toBe(snapshot);
|
||||
});
|
||||
|
||||
it('selectVoiceEngineV2FailedSourceIds returns only failed lifecycle ids', () => {
|
||||
const snapshot = applyEvents(initialSnapshot(), [
|
||||
{
|
||||
|
||||
@@ -276,6 +276,7 @@ export function dispatchObservabilityEvent(
|
||||
assert.equal(typeof event.type, 'string', 'dispatchObservabilityEvent event.type must be a string');
|
||||
switch (event.type) {
|
||||
case 'sourceLifecycle.transitioned':
|
||||
case 'sourceLifecycle.removed':
|
||||
return transitionSourceLifecycles(snapshot, event);
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import type {VoiceEngineV2SourceLifecycleTransitionedEvent} from '../../protocol/events';
|
||||
import type {
|
||||
VoiceEngineV2SourceLifecycleRemovedEvent,
|
||||
VoiceEngineV2SourceLifecycleTransitionedEvent,
|
||||
} from '../../protocol/events';
|
||||
import type {VoiceEngineV2DiagnosticEntry} from '../../protocol/types';
|
||||
import type {SourceFault, SourceLifecycleState} from '../../source_isolation/SourceLifecycleState';
|
||||
import type {VoiceEngineV2Snapshot, VoiceEngineV2Transition} from '../state';
|
||||
@@ -9,7 +12,9 @@ import {allocateOperation, appendDiagnostic, queueCommand} from './_helpers';
|
||||
|
||||
const SOURCE_LIFECYCLE_DIAGNOSTICS_CODE = 'sourceFailed';
|
||||
|
||||
type VoiceEngineV2SourceLifecyclesEvent = VoiceEngineV2SourceLifecycleTransitionedEvent;
|
||||
type VoiceEngineV2SourceLifecyclesEvent =
|
||||
| VoiceEngineV2SourceLifecycleTransitionedEvent
|
||||
| VoiceEngineV2SourceLifecycleRemovedEvent;
|
||||
|
||||
function assertNeverLifecycleKind(kind: never): never {
|
||||
assert.fail(`unhandled source lifecycle kind: ${JSON.stringify(kind)}`);
|
||||
@@ -43,6 +48,12 @@ export function transitionSourceLifecycles(
|
||||
assert.ok(event != null, 'transitionSourceLifecycles event must not be null');
|
||||
assert.equal(typeof event.sourceId, 'string', 'sourceLifecycle.sourceId must be a string');
|
||||
assert.ok(event.sourceId.length > 0, 'sourceLifecycle.sourceId must not be empty');
|
||||
if (event.type === 'sourceLifecycle.removed') {
|
||||
if (!Object.hasOwn(snapshot.sourceLifecycles, event.sourceId)) return {snapshot, commands: []};
|
||||
const sourceLifecycles = {...snapshot.sourceLifecycles};
|
||||
delete sourceLifecycles[event.sourceId];
|
||||
return {snapshot: {...snapshot, sourceLifecycles}, commands: []};
|
||||
}
|
||||
const nextState = rebuildSourceLifecycleState(event);
|
||||
const base: VoiceEngineV2Snapshot = {
|
||||
...snapshot,
|
||||
|
||||
@@ -212,6 +212,7 @@ export type {
|
||||
VoiceEngineV2ScreenOptions,
|
||||
VoiceEngineV2ScreenPacing,
|
||||
VoiceEngineV2SendStats,
|
||||
VoiceEngineV2SourceLifecycleRemovedEvent,
|
||||
VoiceEngineV2SourceLifecycleTransitionedEvent,
|
||||
VoiceEngineV2SourceLifecycleTransitionKind,
|
||||
VoiceEngineV2Stats,
|
||||
|
||||
@@ -15,6 +15,11 @@ export interface VoiceEngineV2SourceLifecycleTransitionedEvent {
|
||||
atMs: number;
|
||||
}
|
||||
|
||||
export interface VoiceEngineV2SourceLifecycleRemovedEvent {
|
||||
type: 'sourceLifecycle.removed';
|
||||
sourceId: string;
|
||||
}
|
||||
|
||||
import type {
|
||||
VoiceEngineV2AudioControlsPatch,
|
||||
VoiceEngineV2CameraEncodingOptions,
|
||||
@@ -264,4 +269,5 @@ export type VoiceEngineV2Event =
|
||||
| {type: 'inboundVideo.trackUnsubscribed'; trackSid: string}
|
||||
| {type: 'inboundVideo.frameReceived'; frame: VoiceEngineV2InboundVideoFrame}
|
||||
| {type: 'inboundVideo.frameStats'; stats: VoiceEngineV2InboundVideoFrameStats}
|
||||
| VoiceEngineV2SourceLifecycleTransitionedEvent;
|
||||
| VoiceEngineV2SourceLifecycleTransitionedEvent
|
||||
| VoiceEngineV2SourceLifecycleRemovedEvent;
|
||||
|
||||
@@ -9,6 +9,7 @@ export type {VoiceEngineV2Command, VoiceEngineV2CommandType} from './commands';
|
||||
export {getVoiceEngineV2CommandResourceKey, getVoiceEngineV2CommandTypeResourceKey} from './commands';
|
||||
export type {
|
||||
VoiceEngineV2Event,
|
||||
VoiceEngineV2SourceLifecycleRemovedEvent,
|
||||
VoiceEngineV2SourceLifecycleTransitionedEvent,
|
||||
VoiceEngineV2SourceLifecycleTransitionKind,
|
||||
} from './events';
|
||||
|
||||
Reference in New Issue
Block a user