openrtc 0.1.0

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.
Files changed (122) hide show
  1. package/README.md +106 -0
  2. package/dist/OpenRTC.d.ts +71 -0
  3. package/dist/OpenRTC.js +130 -0
  4. package/dist/api/MediaTransport.d.ts +7 -0
  5. package/dist/api/MediaTransport.js +262 -0
  6. package/dist/api/PlutoPeerConnection.d.ts +50 -0
  7. package/dist/api/PlutoPeerConnection.js +434 -0
  8. package/dist/api/PlutoWebSocket.d.ts +24 -0
  9. package/dist/api/PlutoWebSocket.js +113 -0
  10. package/dist/api/PlutoWebTransport.d.ts +29 -0
  11. package/dist/api/PlutoWebTransport.js +105 -0
  12. package/dist/auth/host.d.ts +73 -0
  13. package/dist/auth/host.js +513 -0
  14. package/dist/auth/index.d.ts +1 -0
  15. package/dist/auth/index.js +1 -0
  16. package/dist/auth/internal.d.ts +1 -0
  17. package/dist/auth/internal.js +1 -0
  18. package/dist/compat/index.d.ts +20 -0
  19. package/dist/compat/index.js +20 -0
  20. package/dist/core/Client.d.ts +352 -0
  21. package/dist/core/Client.js +3689 -0
  22. package/dist/core/Connection.d.ts +67 -0
  23. package/dist/core/Connection.js +404 -0
  24. package/dist/core/IEngineBridge.d.ts +18 -0
  25. package/dist/core/IEngineBridge.js +1 -0
  26. package/dist/core/ISignalingTransport.d.ts +13 -0
  27. package/dist/core/ISignalingTransport.js +1 -0
  28. package/dist/core/PeerLifecycleManager.d.ts +43 -0
  29. package/dist/core/PeerLifecycleManager.js +330 -0
  30. package/dist/core/PeerProjectionStore.d.ts +43 -0
  31. package/dist/core/PeerProjectionStore.js +327 -0
  32. package/dist/core/PlutoRTC.d.ts +13 -0
  33. package/dist/core/PlutoRTC.js +26 -0
  34. package/dist/core/Room.d.ts +47 -0
  35. package/dist/core/Room.js +376 -0
  36. package/dist/core/Signaling.d.ts +39 -0
  37. package/dist/core/Signaling.js +147 -0
  38. package/dist/core/bridges/BridgeCodecs.d.ts +19 -0
  39. package/dist/core/bridges/BridgeCodecs.js +317 -0
  40. package/dist/core/bridges/BrowserFirestoreRooms.d.ts +36 -0
  41. package/dist/core/bridges/BrowserFirestoreRooms.js +169 -0
  42. package/dist/core/bridges/BrowserFirestoreSignaling.d.ts +111 -0
  43. package/dist/core/bridges/BrowserFirestoreSignaling.js +1010 -0
  44. package/dist/core/bridges/NativeIpcBridge.d.ts +156 -0
  45. package/dist/core/bridges/NativeIpcBridge.js +1472 -0
  46. package/dist/core/bridges/RtdbPresence.d.ts +122 -0
  47. package/dist/core/bridges/RtdbPresence.js +201 -0
  48. package/dist/core/bridges/TauriBridge.d.ts +8 -0
  49. package/dist/core/bridges/TauriBridge.js +7 -0
  50. package/dist/core/bridges/WasmBridge.d.ts +233 -0
  51. package/dist/core/bridges/WasmBridge.js +1811 -0
  52. package/dist/core/config.d.ts +31 -0
  53. package/dist/core/config.js +138 -0
  54. package/dist/core/transports/ITransport.d.ts +12 -0
  55. package/dist/core/transports/ITransport.js +1 -0
  56. package/dist/core/transports/LocalDaemonBridge.d.ts +41 -0
  57. package/dist/core/transports/LocalDaemonBridge.js +227 -0
  58. package/dist/core/transports/TauriSignaling.d.ts +28 -0
  59. package/dist/core/transports/TauriSignaling.js +373 -0
  60. package/dist/core/transports/TransportFactory.d.ts +9 -0
  61. package/dist/core/transports/TransportFactory.js +59 -0
  62. package/dist/core/transports/WasmSignaling.d.ts +78 -0
  63. package/dist/core/transports/WasmSignaling.js +522 -0
  64. package/dist/core/transports/WebRTCTransport.d.ts +23 -0
  65. package/dist/core/transports/WebRTCTransport.js +132 -0
  66. package/dist/core/trust.d.ts +23 -0
  67. package/dist/core/trust.js +99 -0
  68. package/dist/core/types.d.ts +559 -0
  69. package/dist/core/types.js +19 -0
  70. package/dist/index.d.ts +3 -0
  71. package/dist/index.js +3 -0
  72. package/dist/moq/MoQMediaTransport.d.ts +6 -0
  73. package/dist/moq/MoQMediaTransport.js +106 -0
  74. package/dist/moq/MoQPublisher.d.ts +22 -0
  75. package/dist/moq/MoQPublisher.js +179 -0
  76. package/dist/moq/MoQSession.d.ts +35 -0
  77. package/dist/moq/MoQSession.js +166 -0
  78. package/dist/moq/MoQSubscriber.d.ts +19 -0
  79. package/dist/moq/MoQSubscriber.js +235 -0
  80. package/dist/moq/common.d.ts +47 -0
  81. package/dist/moq/common.js +304 -0
  82. package/dist/moq/messages.d.ts +105 -0
  83. package/dist/moq/messages.js +367 -0
  84. package/dist/presets/plutoHosted.d.ts +13 -0
  85. package/dist/presets/plutoHosted.js +18 -0
  86. package/dist/runtime/DelegatingRuntimeAdapter.d.ts +68 -0
  87. package/dist/runtime/DelegatingRuntimeAdapter.js +250 -0
  88. package/dist/runtime/IpcRuntimeAdapter.d.ts +9 -0
  89. package/dist/runtime/IpcRuntimeAdapter.js +13 -0
  90. package/dist/runtime/RuntimeClient.d.ts +257 -0
  91. package/dist/runtime/RuntimeClient.js +1484 -0
  92. package/dist/runtime/TauriRuntimeAdapter.d.ts +1 -0
  93. package/dist/runtime/TauriRuntimeAdapter.js +1 -0
  94. package/dist/runtime/WasmRuntimeAdapter.d.ts +6 -0
  95. package/dist/runtime/WasmRuntimeAdapter.js +11 -0
  96. package/dist/runtime/factories.d.ts +24 -0
  97. package/dist/runtime/factories.js +66 -0
  98. package/dist/runtime/index.d.ts +6 -0
  99. package/dist/runtime/index.js +5 -0
  100. package/dist/runtime/ipc.d.ts +8 -0
  101. package/dist/runtime/ipc.js +65 -0
  102. package/dist/runtime/ipcContract.d.ts +46 -0
  103. package/dist/runtime/ipcContract.js +1 -0
  104. package/dist/runtime/tauri.d.ts +16 -0
  105. package/dist/runtime/tauri.js +22 -0
  106. package/dist/runtime/types.d.ts +110 -0
  107. package/dist/runtime/types.js +1 -0
  108. package/dist/transports/TauriTransport.d.ts +17 -0
  109. package/dist/transports/TauriTransport.js +49 -0
  110. package/dist/transports/WebTransport.d.ts +23 -0
  111. package/dist/transports/WebTransport.js +71 -0
  112. package/dist/utils/debug.d.ts +4 -0
  113. package/dist/utils/debug.js +33 -0
  114. package/dist/utils/runtime.d.ts +1 -0
  115. package/dist/utils/runtime.js +10 -0
  116. package/package.json +97 -0
  117. package/wasm/README.md +64 -0
  118. package/wasm/openrtc.d.ts +284 -0
  119. package/wasm/openrtc.js +2132 -0
  120. package/wasm/openrtc_bg.wasm +0 -0
  121. package/wasm/openrtc_bg.wasm.d.ts +110 -0
  122. package/wasm/package.json +16 -0
@@ -0,0 +1,330 @@
1
+ function normalizeId(value) {
2
+ if (!value)
3
+ return null;
4
+ const trimmed = value.trim();
5
+ return trimmed ? trimmed.toLowerCase() : null;
6
+ }
7
+ function choosePeerId(input) {
8
+ return input.deviceId || input.deviceIdHint || input.peerId || null;
9
+ }
10
+ function sortedStrings(values) {
11
+ return [...values].sort();
12
+ }
13
+ function samePeerStateIgnoringLastSeen(left, right) {
14
+ if (!left) {
15
+ return false;
16
+ }
17
+ return left.peerId === right.peerId
18
+ && (left.deviceId ?? null) === (right.deviceId ?? null)
19
+ && (left.deviceIdHint ?? null) === (right.deviceIdHint ?? null)
20
+ && (left.nodeId ?? null) === (right.nodeId ?? null)
21
+ && (left.connectionId ?? null) === (right.connectionId ?? null)
22
+ && (left.ticket ?? null) === (right.ticket ?? null)
23
+ && !!left.online === !!right.online
24
+ && (left.deviceName ?? null) === (right.deviceName ?? null)
25
+ && (left.platformType ?? null) === (right.platformType ?? null)
26
+ && left.status === right.status
27
+ && left.health === right.health
28
+ && (left.transportState ?? null) === (right.transportState ?? null)
29
+ && (left.protocolState ?? null) === (right.protocolState ?? null)
30
+ && !!left.routable === !!right.routable
31
+ && (left.error ?? null) === (right.error ?? null)
32
+ && JSON.stringify(sortedStrings(left.connectionIds)) === JSON.stringify(sortedStrings(right.connectionIds))
33
+ && JSON.stringify(sortedStrings(left.scopes)) === JSON.stringify(sortedStrings(right.scopes));
34
+ }
35
+ export class PeerLifecycleManager {
36
+ constructor() {
37
+ this.peers = new Map();
38
+ this.aliases = new Map();
39
+ this.watchers = new Set();
40
+ }
41
+ resolveCanonicalPeerId(input) {
42
+ const candidates = [
43
+ input.deviceId,
44
+ input.deviceIdHint,
45
+ input.peerId,
46
+ // Transport metadata may locate an existing peer, but it must not
47
+ // become the canonical public peer identity.
48
+ input.connectionId,
49
+ ];
50
+ for (const candidate of candidates) {
51
+ const alias = normalizeId(candidate);
52
+ if (!alias)
53
+ continue;
54
+ const existing = this.aliases.get(alias);
55
+ if (existing)
56
+ return existing;
57
+ }
58
+ return choosePeerId(input);
59
+ }
60
+ setAlias(value, peerId) {
61
+ const alias = normalizeId(value);
62
+ if (!alias)
63
+ return;
64
+ this.aliases.set(alias, peerId);
65
+ }
66
+ snapshot(record) {
67
+ return {
68
+ peerId: record.peerId,
69
+ deviceId: record.deviceId,
70
+ deviceIdHint: record.deviceIdHint,
71
+ nodeId: record.nodeId,
72
+ connectionId: record.connectionId,
73
+ connectionIds: Array.from(record.connectionIds.values()),
74
+ ticket: record.ticket,
75
+ online: record.online,
76
+ deviceName: record.deviceName,
77
+ platformType: record.platformType,
78
+ status: record.status,
79
+ health: record.health,
80
+ transportState: record.transportState,
81
+ protocolState: record.protocolState,
82
+ routable: record.routable,
83
+ scopes: Array.from(record.scopes.values()),
84
+ lastSeenAt: record.lastSeenAt,
85
+ error: record.error,
86
+ };
87
+ }
88
+ emit() {
89
+ const peers = this.list();
90
+ this.watchers.forEach((watcher) => watcher(peers));
91
+ }
92
+ rekeyPeer(existingPeerId, nextPeerId) {
93
+ const existing = this.peers.get(existingPeerId);
94
+ if (!existing) {
95
+ throw new Error(`Cannot rekey missing peer ${existingPeerId}`);
96
+ }
97
+ if (existingPeerId === nextPeerId) {
98
+ return existing;
99
+ }
100
+ this.peers.delete(existingPeerId);
101
+ existing.peerId = nextPeerId;
102
+ this.peers.set(nextPeerId, existing);
103
+ for (const [alias, peerId] of this.aliases.entries()) {
104
+ if (peerId === existingPeerId) {
105
+ this.aliases.set(alias, nextPeerId);
106
+ }
107
+ }
108
+ return existing;
109
+ }
110
+ upsert(input) {
111
+ const canonicalPeerId = this.resolveCanonicalPeerId(input) ?? choosePeerId(input);
112
+ if (!canonicalPeerId) {
113
+ throw new Error('Peer lifecycle upsert requires a peer identifier.');
114
+ }
115
+ const preferredPeerId = choosePeerId(input) ?? canonicalPeerId;
116
+ let record = this.peers.get(canonicalPeerId);
117
+ const existed = !!record;
118
+ if (!record) {
119
+ record = {
120
+ peerId: canonicalPeerId,
121
+ connectionIds: new Set(),
122
+ status: input.status ?? 'disconnected',
123
+ health: input.health ?? 'unknown',
124
+ scopes: new Set(),
125
+ };
126
+ this.peers.set(canonicalPeerId, record);
127
+ }
128
+ const before = existed ? this.snapshot(record) : undefined;
129
+ if (preferredPeerId !== record.peerId) {
130
+ const nextPreferred = input.deviceId ||
131
+ record.deviceId ||
132
+ input.deviceIdHint ||
133
+ record.deviceIdHint ||
134
+ record.peerId;
135
+ if (nextPreferred && nextPreferred !== record.peerId) {
136
+ record = this.rekeyPeer(record.peerId, nextPreferred);
137
+ }
138
+ }
139
+ if (input.deviceId)
140
+ record.deviceId = input.deviceId;
141
+ if (input.deviceIdHint !== undefined)
142
+ record.deviceIdHint = input.deviceIdHint;
143
+ if (input.nodeId)
144
+ record.nodeId = input.nodeId;
145
+ if (input.connectionId) {
146
+ record.connectionId = input.connectionId;
147
+ record.connectionIds.add(input.connectionId);
148
+ }
149
+ if (input.ticket !== undefined)
150
+ record.ticket = input.ticket;
151
+ if (input.online !== undefined)
152
+ record.online = input.online;
153
+ if (input.deviceName !== undefined)
154
+ record.deviceName = input.deviceName;
155
+ if (input.platformType !== undefined)
156
+ record.platformType = input.platformType;
157
+ if (input.status) {
158
+ const shouldPreserveConnected = record.connectionIds.size > 0 &&
159
+ record.status === 'connected' &&
160
+ input.status === 'disconnected' &&
161
+ !input.connectionId &&
162
+ !input.transportState &&
163
+ !input.protocolState &&
164
+ !input.error;
165
+ if (!shouldPreserveConnected) {
166
+ record.status = input.status;
167
+ }
168
+ }
169
+ if (input.health)
170
+ record.health = input.health;
171
+ if (input.transportState !== undefined)
172
+ record.transportState = input.transportState;
173
+ if (input.protocolState !== undefined)
174
+ record.protocolState = input.protocolState;
175
+ if (input.routable !== undefined)
176
+ record.routable = input.routable;
177
+ if (input.error !== undefined)
178
+ record.error = input.error;
179
+ if (input.lastSeenAt !== undefined)
180
+ record.lastSeenAt = input.lastSeenAt;
181
+ if (input.scopes) {
182
+ for (const scope of input.scopes) {
183
+ record.scopes.add(scope);
184
+ }
185
+ }
186
+ this.setAlias(record.peerId, record.peerId);
187
+ this.setAlias(record.deviceId, record.peerId);
188
+ this.setAlias(record.deviceIdHint, record.peerId);
189
+ this.setAlias(record.nodeId, record.peerId);
190
+ this.setAlias(record.connectionId, record.peerId);
191
+ for (const connectionId of record.connectionIds) {
192
+ this.setAlias(connectionId, record.peerId);
193
+ }
194
+ const snapshot = this.snapshot(record);
195
+ if (!samePeerStateIgnoringLastSeen(before, snapshot)) {
196
+ this.emit();
197
+ }
198
+ return snapshot;
199
+ }
200
+ upsertDevice(device) {
201
+ return this.upsert({
202
+ deviceId: device.deviceId,
203
+ nodeId: device.nodeId,
204
+ ticket: device.ticket,
205
+ online: device.online,
206
+ deviceName: device.deviceName,
207
+ platformType: device.platformType,
208
+ status: device.online ? undefined : 'disconnected',
209
+ health: 'unknown',
210
+ lastSeenAt: typeof device.updatedAt === 'number'
211
+ ? device.updatedAt
212
+ : typeof device.lastSeenAt === 'number'
213
+ ? device.lastSeenAt
214
+ : Date.now(),
215
+ });
216
+ }
217
+ watch(callback) {
218
+ this.watchers.add(callback);
219
+ callback(this.list());
220
+ return () => {
221
+ this.watchers.delete(callback);
222
+ };
223
+ }
224
+ get(id) {
225
+ const alias = normalizeId(id);
226
+ const peerId = alias ? this.aliases.get(alias) : undefined;
227
+ const record = (peerId && this.peers.get(peerId)) || this.peers.get(id);
228
+ return record ? this.snapshot(record) : undefined;
229
+ }
230
+ list() {
231
+ return Array.from(this.peers.values())
232
+ .map((record) => this.snapshot(record))
233
+ .sort((a, b) => {
234
+ const aSeen = a.lastSeenAt ?? 0;
235
+ const bSeen = b.lastSeenAt ?? 0;
236
+ if (aSeen !== bSeen) {
237
+ return bSeen - aSeen;
238
+ }
239
+ return a.peerId.localeCompare(b.peerId);
240
+ });
241
+ }
242
+ listConnected() {
243
+ return this.list().filter((peer) => peer.status === 'connected');
244
+ }
245
+ addScope(id, scope = 'persistent') {
246
+ const record = this.resolveRecord(id);
247
+ if (!record)
248
+ return undefined;
249
+ const before = this.snapshot(record);
250
+ record.scopes.add(scope);
251
+ const snapshot = this.snapshot(record);
252
+ if (!samePeerStateIgnoringLastSeen(before, snapshot)) {
253
+ this.emit();
254
+ }
255
+ return snapshot;
256
+ }
257
+ releaseScope(id, scope) {
258
+ const record = this.resolveRecord(id);
259
+ if (!record)
260
+ return undefined;
261
+ const before = this.snapshot(record);
262
+ if (scope) {
263
+ record.scopes.delete(scope);
264
+ }
265
+ else {
266
+ record.scopes.clear();
267
+ }
268
+ const snapshot = this.snapshot(record);
269
+ if (!samePeerStateIgnoringLastSeen(before, snapshot)) {
270
+ this.emit();
271
+ }
272
+ return snapshot;
273
+ }
274
+ hasScope(id, scope) {
275
+ const record = this.resolveRecord(id);
276
+ return record?.scopes.has(scope) ?? false;
277
+ }
278
+ hasScopes(id) {
279
+ const record = this.resolveRecord(id);
280
+ return (record?.scopes.size ?? 0) > 0;
281
+ }
282
+ removeConnection(id) {
283
+ const record = this.resolveRecord(id);
284
+ if (!record)
285
+ return undefined;
286
+ const before = this.snapshot(record);
287
+ const connectionAlias = normalizeId(id);
288
+ if (connectionAlias) {
289
+ const directConnectionId = Array.from(record.connectionIds).find((candidate) => normalizeId(candidate) === connectionAlias);
290
+ if (directConnectionId) {
291
+ record.connectionIds.delete(directConnectionId);
292
+ }
293
+ }
294
+ if (record.connectionId && normalizeId(record.connectionId) === connectionAlias) {
295
+ record.connectionId = Array.from(record.connectionIds)[0];
296
+ }
297
+ if (record.connectionIds.size === 0 && record.status === 'connected') {
298
+ record.status = 'closed';
299
+ record.routable = false;
300
+ record.transportState = 'closed';
301
+ record.protocolState = 'closed';
302
+ }
303
+ const snapshot = this.snapshot(record);
304
+ if (!samePeerStateIgnoringLastSeen(before, snapshot)) {
305
+ this.emit();
306
+ }
307
+ return snapshot;
308
+ }
309
+ updateHealth(id, health, status) {
310
+ const record = this.resolveRecord(id);
311
+ if (!record)
312
+ return undefined;
313
+ const before = this.snapshot(record);
314
+ record.health = health;
315
+ if (status) {
316
+ record.status = status;
317
+ }
318
+ record.lastSeenAt = Date.now();
319
+ const snapshot = this.snapshot(record);
320
+ if (!samePeerStateIgnoringLastSeen(before, snapshot)) {
321
+ this.emit();
322
+ }
323
+ return snapshot;
324
+ }
325
+ resolveRecord(id) {
326
+ const alias = normalizeId(id);
327
+ const peerId = alias ? this.aliases.get(alias) : undefined;
328
+ return (peerId && this.peers.get(peerId)) || this.peers.get(id);
329
+ }
330
+ }
@@ -0,0 +1,43 @@
1
+ import type { Device, PeerHealth, PeerLifecycleStatus, PeerScope, PeerState } from './types';
2
+ export type PeerProjectionUpsert = {
3
+ peerId?: string;
4
+ deviceId?: string;
5
+ deviceIdHint?: string;
6
+ nodeId?: string;
7
+ connectionId?: string;
8
+ ticket?: string;
9
+ online?: boolean;
10
+ deviceName?: string;
11
+ platformType?: string;
12
+ status?: PeerLifecycleStatus;
13
+ health?: PeerHealth;
14
+ transportState?: string;
15
+ protocolState?: string;
16
+ routable?: boolean;
17
+ promotionEligible?: boolean;
18
+ scopes?: Iterable<PeerScope>;
19
+ error?: string;
20
+ lastSeenAt?: number;
21
+ };
22
+ export declare class PeerProjectionStore {
23
+ private readonly peers;
24
+ private readonly aliases;
25
+ private readonly watchers;
26
+ private resolveCanonicalPeerId;
27
+ private setAlias;
28
+ private snapshot;
29
+ private emit;
30
+ private rekeyPeer;
31
+ upsert(input: PeerProjectionUpsert): PeerState;
32
+ upsertDevice(device: Device): PeerState;
33
+ watch(callback: (peers: PeerState[]) => void): () => void;
34
+ get(id: string): PeerState | undefined;
35
+ list(): PeerState[];
36
+ listConnected(): PeerState[];
37
+ addScope(id: string, scope?: PeerScope): PeerState | undefined;
38
+ releaseScope(id: string, scope?: PeerScope): PeerState | undefined;
39
+ hasScopes(id: string): boolean;
40
+ removeConnection(id: string): PeerState | undefined;
41
+ updateHealth(id: string, health: PeerHealth, status?: PeerLifecycleStatus): PeerState | undefined;
42
+ private resolveRecord;
43
+ }
@@ -0,0 +1,327 @@
1
+ function normalizeId(value) {
2
+ if (!value)
3
+ return null;
4
+ const trimmed = value.trim();
5
+ return trimmed ? trimmed.toLowerCase() : null;
6
+ }
7
+ function choosePeerId(input) {
8
+ return (input.deviceId ||
9
+ input.deviceIdHint ||
10
+ input.nodeId ||
11
+ input.peerId ||
12
+ input.connectionId ||
13
+ null);
14
+ }
15
+ function sortedStrings(values) {
16
+ return [...values].sort();
17
+ }
18
+ function sameProjectionIgnoringLastSeen(left, right) {
19
+ if (!left) {
20
+ return false;
21
+ }
22
+ return left.peerId === right.peerId
23
+ && (left.deviceId ?? null) === (right.deviceId ?? null)
24
+ && (left.deviceIdHint ?? null) === (right.deviceIdHint ?? null)
25
+ && (left.nodeId ?? null) === (right.nodeId ?? null)
26
+ && (left.connectionId ?? null) === (right.connectionId ?? null)
27
+ && (left.ticket ?? null) === (right.ticket ?? null)
28
+ && !!left.online === !!right.online
29
+ && (left.deviceName ?? null) === (right.deviceName ?? null)
30
+ && (left.platformType ?? null) === (right.platformType ?? null)
31
+ && left.status === right.status
32
+ && left.health === right.health
33
+ && (left.transportState ?? null) === (right.transportState ?? null)
34
+ && (left.protocolState ?? null) === (right.protocolState ?? null)
35
+ && !!left.routable === !!right.routable
36
+ && (left.promotionEligible ?? true) === (right.promotionEligible ?? true)
37
+ && (left.error ?? null) === (right.error ?? null)
38
+ && JSON.stringify(sortedStrings(left.connectionIds)) === JSON.stringify(sortedStrings(right.connectionIds))
39
+ && JSON.stringify(sortedStrings(left.scopes)) === JSON.stringify(sortedStrings(right.scopes));
40
+ }
41
+ export class PeerProjectionStore {
42
+ constructor() {
43
+ this.peers = new Map();
44
+ this.aliases = new Map();
45
+ this.watchers = new Set();
46
+ }
47
+ resolveCanonicalPeerId(input) {
48
+ const candidates = [
49
+ input.deviceId,
50
+ input.deviceIdHint,
51
+ input.nodeId,
52
+ input.peerId,
53
+ input.connectionId,
54
+ ];
55
+ for (const candidate of candidates) {
56
+ const alias = normalizeId(candidate);
57
+ if (!alias)
58
+ continue;
59
+ const existing = this.aliases.get(alias);
60
+ if (existing)
61
+ return existing;
62
+ }
63
+ return choosePeerId(input);
64
+ }
65
+ setAlias(value, peerId) {
66
+ const alias = normalizeId(value);
67
+ if (!alias)
68
+ return;
69
+ this.aliases.set(alias, peerId);
70
+ }
71
+ snapshot(record) {
72
+ return {
73
+ peerId: record.peerId,
74
+ deviceId: record.deviceId,
75
+ deviceIdHint: record.deviceIdHint,
76
+ nodeId: record.nodeId,
77
+ connectionId: record.connectionId,
78
+ connectionIds: Array.from(record.connectionIds.values()),
79
+ ticket: record.ticket,
80
+ online: record.online,
81
+ deviceName: record.deviceName,
82
+ platformType: record.platformType,
83
+ status: record.status,
84
+ health: record.health,
85
+ transportState: record.transportState,
86
+ protocolState: record.protocolState,
87
+ routable: record.routable,
88
+ promotionEligible: record.promotionEligible,
89
+ scopes: Array.from(record.scopes.values()),
90
+ lastSeenAt: record.lastSeenAt,
91
+ error: record.error,
92
+ };
93
+ }
94
+ emit() {
95
+ const peers = this.list();
96
+ this.watchers.forEach((watcher) => watcher(peers));
97
+ }
98
+ rekeyPeer(existingPeerId, nextPeerId) {
99
+ const existing = this.peers.get(existingPeerId);
100
+ if (!existing) {
101
+ throw new Error(`Cannot rekey missing peer ${existingPeerId}`);
102
+ }
103
+ if (existingPeerId === nextPeerId) {
104
+ return existing;
105
+ }
106
+ this.peers.delete(existingPeerId);
107
+ existing.peerId = nextPeerId;
108
+ this.peers.set(nextPeerId, existing);
109
+ for (const [alias, peerId] of this.aliases.entries()) {
110
+ if (peerId === existingPeerId) {
111
+ this.aliases.set(alias, nextPeerId);
112
+ }
113
+ }
114
+ return existing;
115
+ }
116
+ upsert(input) {
117
+ const canonicalPeerId = this.resolveCanonicalPeerId(input) ?? choosePeerId(input);
118
+ if (!canonicalPeerId) {
119
+ throw new Error('Peer projection upsert requires at least one peer identifier.');
120
+ }
121
+ const preferredPeerId = choosePeerId(input) ?? canonicalPeerId;
122
+ let record = this.peers.get(canonicalPeerId);
123
+ const existed = !!record;
124
+ if (!record) {
125
+ record = {
126
+ peerId: canonicalPeerId,
127
+ connectionIds: new Set(),
128
+ status: input.status ?? 'disconnected',
129
+ health: input.health ?? 'unknown',
130
+ scopes: new Set(),
131
+ };
132
+ this.peers.set(canonicalPeerId, record);
133
+ }
134
+ const before = existed ? this.snapshot(record) : undefined;
135
+ if (preferredPeerId !== record.peerId) {
136
+ const nextPreferred = input.deviceId ||
137
+ record.deviceId ||
138
+ input.deviceIdHint ||
139
+ record.deviceIdHint ||
140
+ input.nodeId ||
141
+ record.nodeId ||
142
+ record.peerId;
143
+ if (nextPreferred && nextPreferred !== record.peerId) {
144
+ record = this.rekeyPeer(record.peerId, nextPreferred);
145
+ }
146
+ }
147
+ if (input.deviceId)
148
+ record.deviceId = input.deviceId;
149
+ if (input.deviceIdHint !== undefined)
150
+ record.deviceIdHint = input.deviceIdHint;
151
+ if (input.nodeId)
152
+ record.nodeId = input.nodeId;
153
+ if (input.connectionId) {
154
+ record.connectionId = input.connectionId;
155
+ record.connectionIds.add(input.connectionId);
156
+ }
157
+ if (input.ticket !== undefined)
158
+ record.ticket = input.ticket;
159
+ if (input.online !== undefined)
160
+ record.online = input.online;
161
+ if (input.deviceName !== undefined)
162
+ record.deviceName = input.deviceName;
163
+ if (input.platformType !== undefined)
164
+ record.platformType = input.platformType;
165
+ if (input.status) {
166
+ record.status = input.status;
167
+ }
168
+ if (input.health)
169
+ record.health = input.health;
170
+ if (input.transportState !== undefined)
171
+ record.transportState = input.transportState;
172
+ if (input.protocolState !== undefined)
173
+ record.protocolState = input.protocolState;
174
+ if (input.routable !== undefined)
175
+ record.routable = input.routable;
176
+ if (input.promotionEligible !== undefined)
177
+ record.promotionEligible = input.promotionEligible;
178
+ if (input.error !== undefined)
179
+ record.error = input.error;
180
+ if (input.lastSeenAt !== undefined)
181
+ record.lastSeenAt = input.lastSeenAt;
182
+ if (input.scopes) {
183
+ for (const scope of input.scopes) {
184
+ record.scopes.add(scope);
185
+ }
186
+ }
187
+ this.setAlias(record.peerId, record.peerId);
188
+ this.setAlias(record.deviceId, record.peerId);
189
+ this.setAlias(record.deviceIdHint, record.peerId);
190
+ this.setAlias(record.nodeId, record.peerId);
191
+ this.setAlias(record.connectionId, record.peerId);
192
+ for (const connectionId of record.connectionIds) {
193
+ this.setAlias(connectionId, record.peerId);
194
+ }
195
+ const snapshot = this.snapshot(record);
196
+ if (!sameProjectionIgnoringLastSeen(before, snapshot)) {
197
+ this.emit();
198
+ }
199
+ return snapshot;
200
+ }
201
+ upsertDevice(device) {
202
+ return this.upsert({
203
+ deviceId: device.deviceId,
204
+ nodeId: device.nodeId,
205
+ ticket: device.ticket,
206
+ online: device.online,
207
+ deviceName: device.deviceName,
208
+ platformType: device.platformType,
209
+ status: device.online ? undefined : 'disconnected',
210
+ health: 'unknown',
211
+ lastSeenAt: typeof device.updatedAt === 'number'
212
+ ? device.updatedAt
213
+ : typeof device.lastSeenAt === 'number'
214
+ ? device.lastSeenAt
215
+ : Date.now(),
216
+ });
217
+ }
218
+ watch(callback) {
219
+ this.watchers.add(callback);
220
+ callback(this.list());
221
+ return () => {
222
+ this.watchers.delete(callback);
223
+ };
224
+ }
225
+ get(id) {
226
+ const alias = normalizeId(id);
227
+ const peerId = alias ? this.aliases.get(alias) : undefined;
228
+ const record = (peerId && this.peers.get(peerId)) || this.peers.get(id);
229
+ return record ? this.snapshot(record) : undefined;
230
+ }
231
+ list() {
232
+ return Array.from(this.peers.values())
233
+ .map((record) => this.snapshot(record))
234
+ .sort((a, b) => {
235
+ const aSeen = a.lastSeenAt ?? 0;
236
+ const bSeen = b.lastSeenAt ?? 0;
237
+ if (aSeen !== bSeen) {
238
+ return bSeen - aSeen;
239
+ }
240
+ return a.peerId.localeCompare(b.peerId);
241
+ });
242
+ }
243
+ listConnected() {
244
+ return this.list().filter((peer) => peer.status === 'connected');
245
+ }
246
+ addScope(id, scope = 'persistent') {
247
+ const record = this.resolveRecord(id);
248
+ if (!record)
249
+ return undefined;
250
+ const before = this.snapshot(record);
251
+ record.scopes.add(scope);
252
+ const snapshot = this.snapshot(record);
253
+ if (!sameProjectionIgnoringLastSeen(before, snapshot)) {
254
+ this.emit();
255
+ }
256
+ return snapshot;
257
+ }
258
+ releaseScope(id, scope) {
259
+ const record = this.resolveRecord(id);
260
+ if (!record)
261
+ return undefined;
262
+ const before = this.snapshot(record);
263
+ if (scope) {
264
+ record.scopes.delete(scope);
265
+ }
266
+ else {
267
+ record.scopes.clear();
268
+ }
269
+ const snapshot = this.snapshot(record);
270
+ if (!sameProjectionIgnoringLastSeen(before, snapshot)) {
271
+ this.emit();
272
+ }
273
+ return snapshot;
274
+ }
275
+ hasScopes(id) {
276
+ const record = this.resolveRecord(id);
277
+ return (record?.scopes.size ?? 0) > 0;
278
+ }
279
+ removeConnection(id) {
280
+ const record = this.resolveRecord(id);
281
+ if (!record)
282
+ return undefined;
283
+ const before = this.snapshot(record);
284
+ const connectionAlias = normalizeId(id);
285
+ if (connectionAlias) {
286
+ const directConnectionId = Array.from(record.connectionIds).find((candidate) => normalizeId(candidate) === connectionAlias);
287
+ if (directConnectionId) {
288
+ record.connectionIds.delete(directConnectionId);
289
+ }
290
+ }
291
+ if (record.connectionId && normalizeId(record.connectionId) === connectionAlias) {
292
+ record.connectionId = Array.from(record.connectionIds)[0];
293
+ }
294
+ if (record.connectionIds.size === 0 && record.status === 'connected') {
295
+ record.status = 'closed';
296
+ record.routable = false;
297
+ record.transportState = 'closed';
298
+ record.protocolState = 'closed';
299
+ }
300
+ const snapshot = this.snapshot(record);
301
+ if (!sameProjectionIgnoringLastSeen(before, snapshot)) {
302
+ this.emit();
303
+ }
304
+ return snapshot;
305
+ }
306
+ updateHealth(id, health, status) {
307
+ const record = this.resolveRecord(id);
308
+ if (!record)
309
+ return undefined;
310
+ const before = this.snapshot(record);
311
+ record.health = health;
312
+ if (status) {
313
+ record.status = status;
314
+ }
315
+ record.lastSeenAt = Date.now();
316
+ const snapshot = this.snapshot(record);
317
+ if (!sameProjectionIgnoringLastSeen(before, snapshot)) {
318
+ this.emit();
319
+ }
320
+ return snapshot;
321
+ }
322
+ resolveRecord(id) {
323
+ const alias = normalizeId(id);
324
+ const peerId = alias ? this.aliases.get(alias) : undefined;
325
+ return (peerId && this.peers.get(peerId)) || this.peers.get(id);
326
+ }
327
+ }