@livedesk/hub 0.1.41 → 0.1.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,109 +1,109 @@
1
- function normalizedText(value, maxLength = 160) {
2
- return String(value || '').trim().slice(0, maxLength);
3
- }
4
-
5
- export function normalizeRemoteAudioStreamBindings(payload = {}, deviceIds = []) {
6
- const bindings = new Map();
7
- const byDevice = payload?.streamIdsByDeviceId;
8
- if (byDevice && typeof byDevice === 'object' && !Array.isArray(byDevice)) {
9
- for (const deviceId of deviceIds) {
10
- const streamId = normalizedText(byDevice[deviceId], 128);
11
- if (streamId) {
12
- bindings.set(deviceId, streamId);
13
- }
14
- }
15
- }
16
- if (Array.isArray(payload?.streamBindings)) {
17
- for (const binding of payload.streamBindings) {
18
- const deviceId = normalizedText(binding?.deviceId);
19
- const streamId = normalizedText(binding?.streamId, 128);
20
- if (deviceIds.includes(deviceId) && streamId) {
21
- bindings.set(deviceId, streamId);
22
- }
23
- }
24
- }
25
- if (deviceIds.length === 1 && !bindings.has(deviceIds[0])) {
26
- const streamId = normalizedText(payload?.streamId, 128);
27
- if (streamId) {
28
- bindings.set(deviceIds[0], streamId);
29
- }
30
- }
31
- return bindings;
32
- }
33
-
34
- export function remoteAudioSubscriberOwns(client, deviceId, streamId = '') {
35
- const subscribedDeviceIds = client?.liveDeskAudioDeviceIds;
36
- if (!(subscribedDeviceIds instanceof Set)
37
- || subscribedDeviceIds.size === 0
38
- || !subscribedDeviceIds.has(deviceId)) {
39
- return !(subscribedDeviceIds instanceof Set) || subscribedDeviceIds.size === 0;
40
- }
41
- const expectedStreamId = normalizedText(streamId, 128);
42
- const boundStreamId = client?.liveDeskAudioStreamIdsByDeviceId instanceof Map
43
- ? normalizedText(client.liveDeskAudioStreamIdsByDeviceId.get(deviceId), 128)
44
- : '';
45
- // A legacy subscriber without an exact binding remains a wildcard owner.
46
- return !expectedStreamId || !boundStreamId || boundStreamId === expectedStreamId;
47
- }
48
-
49
- export function collectReleasedRemoteAudioOwners({
50
- previousDeviceIds,
51
- previousStreamIdsByDeviceId,
52
- nextDeviceIds,
53
- nextStreamIdsByDeviceId,
54
- fallbackOwners = []
55
- }) {
56
- const previousIds = previousDeviceIds instanceof Set ? previousDeviceIds : new Set();
57
- const nextIds = nextDeviceIds instanceof Set ? nextDeviceIds : new Set();
58
- if (previousIds.size === 0) {
59
- return fallbackOwners.map(owner => ({
60
- deviceId: normalizedText(owner?.deviceId),
61
- streamId: normalizedText(owner?.streamId, 128)
62
- })).filter(owner => owner.deviceId);
63
- }
64
- const owners = [];
65
- for (const deviceId of previousIds) {
66
- const previousStreamId = previousStreamIdsByDeviceId instanceof Map
67
- ? normalizedText(previousStreamIdsByDeviceId.get(deviceId), 128)
68
- : '';
69
- const nextStreamId = nextStreamIdsByDeviceId instanceof Map
70
- ? normalizedText(nextStreamIdsByDeviceId.get(deviceId), 128)
71
- : '';
72
- if (!nextIds.has(deviceId) || previousStreamId !== nextStreamId) {
73
- owners.push({ deviceId, streamId: previousStreamId });
74
- }
75
- }
76
- return owners;
77
- }
78
-
79
- export function observeRemoteAudioStopConfirmation({
80
- result,
81
- deviceId,
82
- streamId,
83
- reason,
84
- hasSubscriber,
85
- disconnect,
86
- warn = () => {}
87
- }) {
88
- if (!result?.stopPromise) {
89
- return false;
90
- }
91
- void Promise.resolve(result.stopPromise).then(confirmation => {
92
- if (confirmation?.captureStopConfirmed === true) {
93
- return;
94
- }
95
- if (!hasSubscriber(deviceId, streamId)) {
96
- warn(
97
- `audio capture stop was not confirmed device=${deviceId} reason=${reason}: `
98
- + `${confirmation?.error || 'audio-stop-unconfirmed'}`
99
- );
100
- disconnect(deviceId, 'audio-capture-stop-unconfirmed');
101
- }
102
- }).catch(error => {
103
- if (!hasSubscriber(deviceId, streamId)) {
104
- warn(`audio capture stop confirmation failed device=${deviceId}: ${error instanceof Error ? error.message : String(error)}`);
105
- disconnect(deviceId, 'audio-capture-stop-confirmation-failed');
106
- }
107
- });
108
- return true;
109
- }
1
+ function normalizedText(value, maxLength = 160) {
2
+ return String(value || '').trim().slice(0, maxLength);
3
+ }
4
+
5
+ export function normalizeRemoteAudioStreamBindings(payload = {}, deviceIds = []) {
6
+ const bindings = new Map();
7
+ const byDevice = payload?.streamIdsByDeviceId;
8
+ if (byDevice && typeof byDevice === 'object' && !Array.isArray(byDevice)) {
9
+ for (const deviceId of deviceIds) {
10
+ const streamId = normalizedText(byDevice[deviceId], 128);
11
+ if (streamId) {
12
+ bindings.set(deviceId, streamId);
13
+ }
14
+ }
15
+ }
16
+ if (Array.isArray(payload?.streamBindings)) {
17
+ for (const binding of payload.streamBindings) {
18
+ const deviceId = normalizedText(binding?.deviceId);
19
+ const streamId = normalizedText(binding?.streamId, 128);
20
+ if (deviceIds.includes(deviceId) && streamId) {
21
+ bindings.set(deviceId, streamId);
22
+ }
23
+ }
24
+ }
25
+ if (deviceIds.length === 1 && !bindings.has(deviceIds[0])) {
26
+ const streamId = normalizedText(payload?.streamId, 128);
27
+ if (streamId) {
28
+ bindings.set(deviceIds[0], streamId);
29
+ }
30
+ }
31
+ return bindings;
32
+ }
33
+
34
+ export function remoteAudioSubscriberOwns(client, deviceId, streamId = '') {
35
+ const subscribedDeviceIds = client?.liveDeskAudioDeviceIds;
36
+ if (!(subscribedDeviceIds instanceof Set)
37
+ || subscribedDeviceIds.size === 0
38
+ || !subscribedDeviceIds.has(deviceId)) {
39
+ return !(subscribedDeviceIds instanceof Set) || subscribedDeviceIds.size === 0;
40
+ }
41
+ const expectedStreamId = normalizedText(streamId, 128);
42
+ const boundStreamId = client?.liveDeskAudioStreamIdsByDeviceId instanceof Map
43
+ ? normalizedText(client.liveDeskAudioStreamIdsByDeviceId.get(deviceId), 128)
44
+ : '';
45
+ // A legacy subscriber without an exact binding remains a wildcard owner.
46
+ return !expectedStreamId || !boundStreamId || boundStreamId === expectedStreamId;
47
+ }
48
+
49
+ export function collectReleasedRemoteAudioOwners({
50
+ previousDeviceIds,
51
+ previousStreamIdsByDeviceId,
52
+ nextDeviceIds,
53
+ nextStreamIdsByDeviceId,
54
+ fallbackOwners = []
55
+ }) {
56
+ const previousIds = previousDeviceIds instanceof Set ? previousDeviceIds : new Set();
57
+ const nextIds = nextDeviceIds instanceof Set ? nextDeviceIds : new Set();
58
+ if (previousIds.size === 0) {
59
+ return fallbackOwners.map(owner => ({
60
+ deviceId: normalizedText(owner?.deviceId),
61
+ streamId: normalizedText(owner?.streamId, 128)
62
+ })).filter(owner => owner.deviceId);
63
+ }
64
+ const owners = [];
65
+ for (const deviceId of previousIds) {
66
+ const previousStreamId = previousStreamIdsByDeviceId instanceof Map
67
+ ? normalizedText(previousStreamIdsByDeviceId.get(deviceId), 128)
68
+ : '';
69
+ const nextStreamId = nextStreamIdsByDeviceId instanceof Map
70
+ ? normalizedText(nextStreamIdsByDeviceId.get(deviceId), 128)
71
+ : '';
72
+ if (!nextIds.has(deviceId) || previousStreamId !== nextStreamId) {
73
+ owners.push({ deviceId, streamId: previousStreamId });
74
+ }
75
+ }
76
+ return owners;
77
+ }
78
+
79
+ export function observeRemoteAudioStopConfirmation({
80
+ result,
81
+ deviceId,
82
+ streamId,
83
+ reason,
84
+ hasSubscriber,
85
+ disconnect,
86
+ warn = () => {}
87
+ }) {
88
+ if (!result?.stopPromise) {
89
+ return false;
90
+ }
91
+ void Promise.resolve(result.stopPromise).then(confirmation => {
92
+ if (confirmation?.captureStopConfirmed === true) {
93
+ return;
94
+ }
95
+ if (!hasSubscriber(deviceId, streamId)) {
96
+ warn(
97
+ `audio capture stop was not confirmed device=${deviceId} reason=${reason}: `
98
+ + `${confirmation?.error || 'audio-stop-unconfirmed'}`
99
+ );
100
+ disconnect(deviceId, 'audio-capture-stop-unconfirmed');
101
+ }
102
+ }).catch(error => {
103
+ if (!hasSubscriber(deviceId, streamId)) {
104
+ warn(`audio capture stop confirmation failed device=${deviceId}: ${error instanceof Error ? error.message : String(error)}`);
105
+ disconnect(deviceId, 'audio-capture-stop-confirmation-failed');
106
+ }
107
+ });
108
+ return true;
109
+ }
@@ -1,72 +1,72 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
-
4
- import {
5
- collectReleasedRemoteAudioOwners,
6
- normalizeRemoteAudioStreamBindings,
7
- observeRemoteAudioStopConfirmation,
8
- remoteAudioSubscriberOwns
9
- } from './remote-audio-subscription-contract.mjs';
10
-
11
- function deferred() {
12
- let resolve;
13
- const promise = new Promise(resolvePromise => {
14
- resolve = resolvePromise;
15
- });
16
- return { promise, resolve };
17
- }
18
-
19
- test('late old WebSocket cleanup keeps a differently bound replacement owner', () => {
20
- const previousDeviceIds = new Set(['device-a']);
21
- const previousBindings = new Map([['device-a', 'audio-old']]);
22
- const replacement = {
23
- liveDeskAudioDeviceIds: new Set(['device-a']),
24
- liveDeskAudioStreamIdsByDeviceId: new Map([['device-a', 'audio-new']])
25
- };
26
- const released = collectReleasedRemoteAudioOwners({
27
- previousDeviceIds,
28
- previousStreamIdsByDeviceId: previousBindings,
29
- nextDeviceIds: new Set(),
30
- nextStreamIdsByDeviceId: new Map()
31
- });
32
-
33
- assert.deepEqual(released, [{ deviceId: 'device-a', streamId: 'audio-old' }]);
34
- assert.equal(remoteAudioSubscriberOwns(replacement, 'device-a', 'audio-old'), false);
35
- assert.equal(remoteAudioSubscriberOwns(replacement, 'device-a', 'audio-new'), true);
36
- });
37
-
38
- test('subscription binding change releases only the exact old stream', () => {
39
- const nextBindings = normalizeRemoteAudioStreamBindings({
40
- streamIdsByDeviceId: { 'device-a': 'audio-new' }
41
- }, ['device-a']);
42
- assert.deepEqual(
43
- collectReleasedRemoteAudioOwners({
44
- previousDeviceIds: new Set(['device-a']),
45
- previousStreamIdsByDeviceId: new Map([['device-a', 'audio-old']]),
46
- nextDeviceIds: new Set(['device-a']),
47
- nextStreamIdsByDeviceId: nextBindings
48
- }),
49
- [{ deviceId: 'device-a', streamId: 'audio-old' }]
50
- );
51
- });
52
-
53
- test('deduplicated API-first stop failure still forces final Agent drain', async () => {
54
- const stop = deferred();
55
- const disconnects = [];
56
- const result = { stopPromise: stop.promise };
57
- assert.equal(observeRemoteAudioStopConfirmation({
58
- result,
59
- deviceId: 'device-a',
60
- streamId: 'audio-current',
61
- reason: 'audio-subscriber-closed',
62
- hasSubscriber: () => false,
63
- disconnect: (...args) => disconnects.push(args)
64
- }), true);
65
-
66
- stop.resolve({ captureStopConfirmed: false, error: 'native-audio-stop-failed' });
67
- await stop.promise;
68
- await Promise.resolve();
69
- assert.deepEqual(disconnects, [
70
- ['device-a', 'audio-capture-stop-unconfirmed']
71
- ]);
72
- });
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ collectReleasedRemoteAudioOwners,
6
+ normalizeRemoteAudioStreamBindings,
7
+ observeRemoteAudioStopConfirmation,
8
+ remoteAudioSubscriberOwns
9
+ } from './remote-audio-subscription-contract.mjs';
10
+
11
+ function deferred() {
12
+ let resolve;
13
+ const promise = new Promise(resolvePromise => {
14
+ resolve = resolvePromise;
15
+ });
16
+ return { promise, resolve };
17
+ }
18
+
19
+ test('late old WebSocket cleanup keeps a differently bound replacement owner', () => {
20
+ const previousDeviceIds = new Set(['device-a']);
21
+ const previousBindings = new Map([['device-a', 'audio-old']]);
22
+ const replacement = {
23
+ liveDeskAudioDeviceIds: new Set(['device-a']),
24
+ liveDeskAudioStreamIdsByDeviceId: new Map([['device-a', 'audio-new']])
25
+ };
26
+ const released = collectReleasedRemoteAudioOwners({
27
+ previousDeviceIds,
28
+ previousStreamIdsByDeviceId: previousBindings,
29
+ nextDeviceIds: new Set(),
30
+ nextStreamIdsByDeviceId: new Map()
31
+ });
32
+
33
+ assert.deepEqual(released, [{ deviceId: 'device-a', streamId: 'audio-old' }]);
34
+ assert.equal(remoteAudioSubscriberOwns(replacement, 'device-a', 'audio-old'), false);
35
+ assert.equal(remoteAudioSubscriberOwns(replacement, 'device-a', 'audio-new'), true);
36
+ });
37
+
38
+ test('subscription binding change releases only the exact old stream', () => {
39
+ const nextBindings = normalizeRemoteAudioStreamBindings({
40
+ streamIdsByDeviceId: { 'device-a': 'audio-new' }
41
+ }, ['device-a']);
42
+ assert.deepEqual(
43
+ collectReleasedRemoteAudioOwners({
44
+ previousDeviceIds: new Set(['device-a']),
45
+ previousStreamIdsByDeviceId: new Map([['device-a', 'audio-old']]),
46
+ nextDeviceIds: new Set(['device-a']),
47
+ nextStreamIdsByDeviceId: nextBindings
48
+ }),
49
+ [{ deviceId: 'device-a', streamId: 'audio-old' }]
50
+ );
51
+ });
52
+
53
+ test('deduplicated API-first stop failure still forces final Agent drain', async () => {
54
+ const stop = deferred();
55
+ const disconnects = [];
56
+ const result = { stopPromise: stop.promise };
57
+ assert.equal(observeRemoteAudioStopConfirmation({
58
+ result,
59
+ deviceId: 'device-a',
60
+ streamId: 'audio-current',
61
+ reason: 'audio-subscriber-closed',
62
+ hasSubscriber: () => false,
63
+ disconnect: (...args) => disconnects.push(args)
64
+ }), true);
65
+
66
+ stop.resolve({ captureStopConfirmed: false, error: 'native-audio-stop-failed' });
67
+ await stop.promise;
68
+ await Promise.resolve();
69
+ assert.deepEqual(disconnects, [
70
+ ['device-a', 'audio-capture-stop-unconfirmed']
71
+ ]);
72
+ });