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,522 @@
1
+ import { getAuthHost } from '../../auth/host';
2
+ export class WasmSignalingBackend {
3
+ constructor(options) {
4
+ this.authListeners = [];
5
+ // To be injected/set once WASM boots
6
+ this.wasmClient = null;
7
+ this.missingWasmWarnings = new Set();
8
+ this.wasmReadyWarningDelayMs = 15000;
9
+ this.deviceListeners = new Map();
10
+ this.nextDeviceListenerId = 1;
11
+ this.deviceReader = null;
12
+ this.devicesById = new Map();
13
+ this.activeDeviceUserId = null;
14
+ this.deviceSubscriptionCancelled = false;
15
+ this.deviceSubscriptionTask = null;
16
+ this.authContext = null;
17
+ if (!options.tag)
18
+ throw new Error("Signaling requires a 'tag' option.");
19
+ this.tag = options.tag;
20
+ this.options = options;
21
+ const authHost = getAuthHost();
22
+ this.app = authHost.getApp();
23
+ this.auth = authHost.getAuth();
24
+ this.persistenceReady = Promise.resolve();
25
+ authHost.onAuthStateChanged((user) => {
26
+ if (this.authContext) {
27
+ return;
28
+ }
29
+ const nextUserId = user?.uid ?? null;
30
+ if (this.activeDeviceUserId && this.activeDeviceUserId !== nextUserId) {
31
+ void this.stopDeviceSubscription();
32
+ this.devicesById.clear();
33
+ }
34
+ this.authListeners.forEach(cb => cb(this.toRuntimeIdentity(user)));
35
+ });
36
+ }
37
+ getDeviceIdentityStorageKey() {
38
+ const suffix = this.options.storagePrefix ? this.options.storagePrefix.replace(/_$/, '') : 'default';
39
+ return `${WasmSignalingBackend.WEB_DEVICE_ID_STORAGE_PREFIX}_${suffix}`;
40
+ }
41
+ getNodeIdPersistenceMode() {
42
+ return this.options.nodeIdPersistence ?? 'persistent';
43
+ }
44
+ createRandomDeviceId() {
45
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
46
+ return crypto.randomUUID();
47
+ }
48
+ return `web-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
49
+ }
50
+ resolveWebLogicalDeviceId() {
51
+ const mode = this.getNodeIdPersistenceMode();
52
+ if (typeof localStorage === 'undefined') {
53
+ return this.createRandomDeviceId();
54
+ }
55
+ const key = this.getDeviceIdentityStorageKey();
56
+ const now = Date.now();
57
+ let stored = null;
58
+ try {
59
+ const raw = localStorage.getItem(key);
60
+ stored = raw ? JSON.parse(raw) : null;
61
+ }
62
+ catch {
63
+ stored = null;
64
+ }
65
+ const storedDeviceId = typeof stored?.deviceId === 'string' ? stored.deviceId.trim() : '';
66
+ const storedExpiry = typeof stored?.expiresAt === 'number' ? stored.expiresAt : undefined;
67
+ if (mode === 'persistent') {
68
+ const nextDeviceId = storedDeviceId || this.createRandomDeviceId();
69
+ localStorage.setItem(key, JSON.stringify({ deviceId: nextDeviceId }));
70
+ return nextDeviceId;
71
+ }
72
+ if (storedDeviceId && typeof storedExpiry === 'number' && storedExpiry > now) {
73
+ const refreshedExpiry = now + WasmSignalingBackend.EPHEMERAL_DEVICE_ID_TTL_MS;
74
+ localStorage.setItem(key, JSON.stringify({ deviceId: storedDeviceId, expiresAt: refreshedExpiry }));
75
+ return storedDeviceId;
76
+ }
77
+ const nextDeviceId = this.createRandomDeviceId();
78
+ const expiresAt = now + WasmSignalingBackend.EPHEMERAL_DEVICE_ID_TTL_MS;
79
+ localStorage.setItem(key, JSON.stringify({ deviceId: nextDeviceId, expiresAt }));
80
+ return nextDeviceId;
81
+ }
82
+ withWebPresenceMetadata(metadata, localDeviceId) {
83
+ if (!metadata || !metadata.trim()) {
84
+ return JSON.stringify({ deviceId: localDeviceId });
85
+ }
86
+ try {
87
+ const parsed = JSON.parse(metadata);
88
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
89
+ return JSON.stringify({
90
+ ...parsed,
91
+ deviceId: localDeviceId,
92
+ });
93
+ }
94
+ }
95
+ catch {
96
+ // fall through to opaque metadata wrapper
97
+ }
98
+ return JSON.stringify({
99
+ deviceId: localDeviceId,
100
+ metadata,
101
+ });
102
+ }
103
+ setAuthContext(context) {
104
+ this.authContext = context;
105
+ if (this.wasmClient && typeof this.wasmClient.set_auth_token === 'function') {
106
+ this.wasmClient.set_auth_token(context?.token ?? null);
107
+ }
108
+ this.authListeners.forEach((callback) => callback(this.currentUser));
109
+ }
110
+ toRuntimeIdentity(user) {
111
+ if (!user) {
112
+ return null;
113
+ }
114
+ return {
115
+ id: user.uid,
116
+ email: user.email,
117
+ displayName: user.displayName,
118
+ isAnonymous: user.isAnonymous,
119
+ getToken: async (forceRefresh = false) => {
120
+ try {
121
+ return await user.getIdToken(forceRefresh);
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ },
127
+ };
128
+ }
129
+ getCurrentUserId() {
130
+ if (this.authContext?.userId) {
131
+ return this.authContext.userId;
132
+ }
133
+ return this.auth.currentUser?.uid ?? null;
134
+ }
135
+ getFilteredDevices(excludeNodeId) {
136
+ const devices = Array.from(this.devicesById.values())
137
+ .filter((device) => this.matchesTag(device));
138
+ if (!excludeNodeId)
139
+ return devices;
140
+ return devices.filter((device) => device.ticket !== excludeNodeId && device.deviceId !== excludeNodeId);
141
+ }
142
+ matchesTag(device) {
143
+ return typeof device.tag === 'string' && device.tag === this.tag;
144
+ }
145
+ emitDeviceUpdateForListener(listener) {
146
+ listener.callback(this.getFilteredDevices(listener.excludeNodeId));
147
+ }
148
+ emitDeviceUpdate() {
149
+ for (const listener of this.deviceListeners.values()) {
150
+ this.emitDeviceUpdateForListener(listener);
151
+ }
152
+ }
153
+ applyDeviceEvents(events) {
154
+ for (const event of events) {
155
+ if (event.type === 'removed') {
156
+ this.devicesById.delete(event.deviceId);
157
+ continue;
158
+ }
159
+ const normalized = this.normalizeDevice(event.device);
160
+ if (!normalized.deviceId)
161
+ continue;
162
+ if (!this.matchesTag(normalized)) {
163
+ this.devicesById.delete(normalized.deviceId);
164
+ continue;
165
+ }
166
+ this.devicesById.set(normalized.deviceId, normalized);
167
+ }
168
+ }
169
+ async stopDeviceSubscription() {
170
+ this.deviceSubscriptionCancelled = true;
171
+ if (this.deviceReader) {
172
+ try {
173
+ await this.deviceReader.cancel();
174
+ }
175
+ catch {
176
+ // noop
177
+ }
178
+ this.deviceReader = null;
179
+ }
180
+ this.activeDeviceUserId = null;
181
+ this.deviceSubscriptionTask = null;
182
+ }
183
+ startDeviceSubscription(userId) {
184
+ if (this.deviceSubscriptionTask && this.activeDeviceUserId === userId) {
185
+ return;
186
+ }
187
+ this.deviceSubscriptionTask = (async () => {
188
+ if (this.activeDeviceUserId && this.activeDeviceUserId !== userId) {
189
+ await this.stopDeviceSubscription();
190
+ }
191
+ this.activeDeviceUserId = userId;
192
+ this.deviceSubscriptionCancelled = false;
193
+ try {
194
+ const startedAt = Date.now();
195
+ let wasmClient = null;
196
+ while (!this.deviceSubscriptionCancelled) {
197
+ wasmClient = await this.waitForWasmClient(2000);
198
+ if (wasmClient && typeof wasmClient.subscribe_devices === 'function') {
199
+ break;
200
+ }
201
+ if (Date.now() - startedAt >= this.wasmReadyWarningDelayMs) {
202
+ this.warnMissingWasmOnce('onDevicesChange');
203
+ }
204
+ await new Promise((resolve) => setTimeout(resolve, 250));
205
+ }
206
+ if (!wasmClient || this.deviceSubscriptionCancelled) {
207
+ this.emitDeviceUpdate();
208
+ return;
209
+ }
210
+ const initialDevices = await this.searchDevices();
211
+ this.devicesById.clear();
212
+ for (const device of initialDevices) {
213
+ this.devicesById.set(device.deviceId, device);
214
+ }
215
+ this.emitDeviceUpdate();
216
+ const stream = await this.subscribeDevices(userId);
217
+ this.deviceReader = stream.getReader();
218
+ while (!this.deviceSubscriptionCancelled) {
219
+ const { done, value } = await this.deviceReader.read();
220
+ if (done || !value)
221
+ break;
222
+ this.applyDeviceEvents(value);
223
+ this.emitDeviceUpdate();
224
+ }
225
+ }
226
+ catch (error) {
227
+ if (!this.deviceSubscriptionCancelled) {
228
+ console.warn('[WasmSignaling] onDevicesChange stream failed:', error);
229
+ }
230
+ }
231
+ finally {
232
+ this.deviceReader = null;
233
+ if (this.deviceSubscriptionCancelled || this.deviceListeners.size === 0) {
234
+ this.activeDeviceUserId = null;
235
+ this.deviceSubscriptionTask = null;
236
+ }
237
+ }
238
+ })();
239
+ }
240
+ setWasmClient(client) {
241
+ this.wasmClient = client;
242
+ if (this.authContext && typeof this.wasmClient?.set_auth_token === 'function') {
243
+ this.wasmClient.set_auth_token(this.authContext.token ?? null);
244
+ }
245
+ }
246
+ warnMissingWasmOnce(method) {
247
+ if (this.missingWasmWarnings.has(method)) {
248
+ return;
249
+ }
250
+ this.missingWasmWarnings.add(method);
251
+ console.warn(`[WasmSignaling] ${method} skipped: wasm client is not ready.`);
252
+ }
253
+ async waitForWasmClient(timeoutMs = 4000, pollMs = 50) {
254
+ if (this.wasmClient) {
255
+ return this.wasmClient;
256
+ }
257
+ const start = Date.now();
258
+ while (!this.wasmClient && Date.now() - start < timeoutMs) {
259
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
260
+ }
261
+ return this.wasmClient ?? null;
262
+ }
263
+ emptyStream() {
264
+ return new ReadableStream({
265
+ start(controller) {
266
+ controller.close();
267
+ },
268
+ });
269
+ }
270
+ normalizeDevice(raw, fallbackId) {
271
+ const deviceId = raw.deviceId || (fallbackId || '');
272
+ const nodeId = raw.nodeId || raw.ticket || '';
273
+ return {
274
+ deviceId,
275
+ deviceName: raw.deviceName || 'Unknown Device',
276
+ online: !!raw.online,
277
+ ticket: nodeId,
278
+ tag: typeof raw.tag === 'string' ? raw.tag : undefined,
279
+ nodeId,
280
+ platformType: typeof raw.platformType === 'string' ? raw.platformType : undefined,
281
+ capabilities: raw.capabilities || undefined,
282
+ sessionId: typeof raw.sessionId === 'string' ? raw.sessionId : undefined,
283
+ userId: typeof raw.userId === 'string' ? raw.userId : undefined,
284
+ lastSeenAt: raw.lastSeenAt,
285
+ expiresAt: raw.expiresAt,
286
+ createdAt: raw.createdAt,
287
+ updatedAt: raw.updatedAt,
288
+ metadata: typeof raw.metadata === 'string' ? raw.metadata : undefined,
289
+ };
290
+ }
291
+ get currentUser() {
292
+ if (this.authContext?.userId) {
293
+ const token = this.authContext.token ?? null;
294
+ return {
295
+ id: this.authContext.userId,
296
+ getToken: async () => token,
297
+ };
298
+ }
299
+ return this.toRuntimeIdentity(this.auth.currentUser);
300
+ }
301
+ onAuthChange(callback) {
302
+ this.authListeners.push(callback);
303
+ callback(this.currentUser);
304
+ return () => {
305
+ this.authListeners = this.authListeners.filter(cb => cb !== callback);
306
+ };
307
+ }
308
+ async checkForSSOToken() {
309
+ await getAuthHost().checkForSSOToken();
310
+ }
311
+ async waitForAuth() {
312
+ if (this.authContext?.userId) {
313
+ return;
314
+ }
315
+ await getAuthHost().waitForAuth();
316
+ }
317
+ async signInAnonymously() {
318
+ await getAuthHost().signInAnonymously();
319
+ }
320
+ async signOut() {
321
+ await this.auth.signOut();
322
+ await this.signInAnonymously();
323
+ }
324
+ async getTurnCredentials() {
325
+ if (!this.auth.currentUser)
326
+ return null;
327
+ // In the future this should come from Rust/Tauri IPC.
328
+ return null;
329
+ }
330
+ async updatePresence(localNodeId, ticketStr, isOnline = true, ttlMs = 300000, metadata) {
331
+ const userId = this.getCurrentUserId();
332
+ if (!userId)
333
+ return;
334
+ const localDeviceId = this.resolveWebLogicalDeviceId();
335
+ const effectiveTtlMs = this.getNodeIdPersistenceMode() === 'ephemeral'
336
+ ? WasmSignalingBackend.EPHEMERAL_DEVICE_ID_TTL_MS
337
+ : ttlMs;
338
+ const mergedMetadata = this.withWebPresenceMetadata(metadata, localDeviceId);
339
+ const deviceName = this.options.storagePrefix
340
+ ? `Web (${this.options.storagePrefix.replace(/_$/, '')})`
341
+ : 'Web';
342
+ const wasmClient = await this.waitForWasmClient();
343
+ if (wasmClient && typeof wasmClient.update_presence === 'function') {
344
+ try {
345
+ const ttlBigInt = BigInt(Math.max(0, Math.trunc(effectiveTtlMs)));
346
+ await wasmClient.update_presence(userId, deviceName, ticketStr, mergedMetadata, ttlBigInt);
347
+ }
348
+ catch (error) {
349
+ console.warn('[WasmSignaling] WASM update_presence failed:', error);
350
+ }
351
+ return;
352
+ }
353
+ this.warnMissingWasmOnce('updatePresence');
354
+ }
355
+ async setOffline(localNodeId) {
356
+ const userId = this.getCurrentUserId();
357
+ if (!userId)
358
+ return;
359
+ const wasmClient = await this.waitForWasmClient();
360
+ if (wasmClient && typeof wasmClient.set_offline === 'function') {
361
+ try {
362
+ await wasmClient.set_offline(userId);
363
+ }
364
+ catch (error) {
365
+ console.warn('[WasmSignaling] WASM set_offline failed:', error);
366
+ }
367
+ return;
368
+ }
369
+ this.warnMissingWasmOnce('setOffline');
370
+ }
371
+ async cleanupStaleDevices() {
372
+ // Rust daemon handles this automatically.
373
+ }
374
+ async sendMessage(targetId, payload, state, replyPayload) {
375
+ const wasmClient = await this.waitForWasmClient();
376
+ if (!wasmClient || typeof wasmClient.send_message !== 'function') {
377
+ this.warnMissingWasmOnce('sendMessage');
378
+ return '';
379
+ }
380
+ return wasmClient.send_message(targetId, payload, state, replyPayload);
381
+ }
382
+ async pollMessages(targetId) {
383
+ // Deprecated
384
+ return [];
385
+ }
386
+ async searchDevices(excludeNodeId) {
387
+ const userId = this.getCurrentUserId();
388
+ if (!userId)
389
+ return [];
390
+ const wasmClient = await this.waitForWasmClient(500);
391
+ if (wasmClient && typeof wasmClient.search_devices === 'function') {
392
+ const devices = await wasmClient.search_devices(userId);
393
+ const normalized = (Array.isArray(devices) ? devices : [])
394
+ .map((device) => this.normalizeDevice(device))
395
+ .filter((device) => this.matchesTag(device));
396
+ if (!excludeNodeId)
397
+ return normalized;
398
+ return normalized.filter((d) => d.ticket !== excludeNodeId && d.deviceId !== excludeNodeId);
399
+ }
400
+ return [];
401
+ }
402
+ onDevicesChange(callback, excludeNodeId) {
403
+ const userId = this.getCurrentUserId();
404
+ if (!userId)
405
+ return () => { };
406
+ const listenerId = this.nextDeviceListenerId++;
407
+ const listener = { callback, excludeNodeId };
408
+ this.deviceListeners.set(listenerId, listener);
409
+ this.emitDeviceUpdateForListener(listener);
410
+ this.startDeviceSubscription(userId);
411
+ return () => {
412
+ this.deviceListeners.delete(listenerId);
413
+ if (this.deviceListeners.size === 0) {
414
+ void this.stopDeviceSubscription();
415
+ }
416
+ };
417
+ }
418
+ async subscribeDevices(userId) {
419
+ const wasmClient = await this.waitForWasmClient();
420
+ if (!wasmClient || typeof wasmClient.subscribe_devices !== 'function') {
421
+ this.warnMissingWasmOnce('subscribeDevices');
422
+ return this.emptyStream();
423
+ }
424
+ return wasmClient.subscribe_devices(userId);
425
+ }
426
+ async createSession(session) {
427
+ const wasmClient = await this.waitForWasmClient();
428
+ if (!wasmClient || typeof wasmClient.create_session !== 'function') {
429
+ this.warnMissingWasmOnce('createSession');
430
+ return;
431
+ }
432
+ const userId = this.getCurrentUserId();
433
+ if (!userId && !session.initiator && !session.target) {
434
+ throw new Error('createSession requires user scope');
435
+ }
436
+ const scopedSession = {
437
+ ...session,
438
+ initiator: session.initiator ?? userId ?? undefined,
439
+ target: session.target ?? userId ?? undefined,
440
+ };
441
+ return wasmClient.create_session(JSON.stringify(scopedSession));
442
+ }
443
+ async updateSession(sessionId, updateData) {
444
+ const wasmClient = await this.waitForWasmClient();
445
+ if (!wasmClient || typeof wasmClient.update_session !== 'function') {
446
+ this.warnMissingWasmOnce('updateSession');
447
+ return;
448
+ }
449
+ const userId = this.getCurrentUserId();
450
+ if (!userId) {
451
+ throw new Error('updateSession requires authenticated user scope');
452
+ }
453
+ const scopedSessionId = `${userId}::${sessionId}`;
454
+ return wasmClient.update_session(scopedSessionId, JSON.stringify(updateData));
455
+ }
456
+ async subscribeSessions(localDeviceId) {
457
+ const wasmClient = await this.waitForWasmClient();
458
+ if (!wasmClient || typeof wasmClient.subscribe_sessions !== 'function') {
459
+ this.warnMissingWasmOnce('subscribeSessions');
460
+ return this.emptyStream();
461
+ }
462
+ const userId = this.getCurrentUserId();
463
+ if (!userId) {
464
+ throw new Error('subscribeSessions requires authenticated user scope');
465
+ }
466
+ const scopedKey = `${userId}::${localDeviceId}`;
467
+ return wasmClient.subscribe_sessions(scopedKey);
468
+ }
469
+ async advertiseLocalDaemon(params) {
470
+ const userId = this.getCurrentUserId();
471
+ if (!userId)
472
+ return;
473
+ const wasmClient = await this.waitForWasmClient();
474
+ if (!wasmClient || typeof wasmClient.advertise_local_daemon !== 'function') {
475
+ this.warnMissingWasmOnce('advertiseLocalDaemon');
476
+ return;
477
+ }
478
+ const ttlMs = params.ttlMs ?? 60000;
479
+ await wasmClient.advertise_local_daemon(userId, params.deviceId, params.host, params.localPort, params.localToken, ttlMs);
480
+ }
481
+ async findLocalDaemons() {
482
+ const userId = this.getCurrentUserId();
483
+ if (!userId)
484
+ return [];
485
+ const wasmClient = await this.waitForWasmClient(this.wasmReadyWarningDelayMs);
486
+ if (!wasmClient || typeof wasmClient.find_local_daemons !== 'function') {
487
+ this.warnMissingWasmOnce('findLocalDaemons');
488
+ return [];
489
+ }
490
+ const raw = await wasmClient.find_local_daemons(userId);
491
+ const now = Date.now();
492
+ const entries = (Array.isArray(raw) ? raw : [])
493
+ .map((entry) => {
494
+ const port = typeof entry.localPort === 'number'
495
+ ? entry.localPort
496
+ : typeof entry.localPort === 'string'
497
+ ? Number(entry.localPort)
498
+ : NaN;
499
+ const token = typeof entry.localToken === 'string' ? entry.localToken : '';
500
+ const expiresAt = typeof entry.expiresAt === 'number'
501
+ ? entry.expiresAt
502
+ : typeof entry.expiresAt === 'string'
503
+ ? Number(entry.expiresAt)
504
+ : 0;
505
+ if (!Number.isFinite(port) || port <= 0 || !token || expiresAt <= now) {
506
+ return null;
507
+ }
508
+ return {
509
+ deviceId: typeof entry.deviceId === 'string' ? entry.deviceId : '',
510
+ host: typeof entry.host === 'string' && entry.host.length > 0 ? entry.host : '127.0.0.1',
511
+ localPort: port,
512
+ localToken: token,
513
+ expiresAt,
514
+ updatedAt: typeof entry.updatedAt === 'number' ? entry.updatedAt : undefined,
515
+ };
516
+ })
517
+ .filter((entry) => !!entry);
518
+ return entries;
519
+ }
520
+ }
521
+ WasmSignalingBackend.WEB_DEVICE_ID_STORAGE_PREFIX = 'pluto_rtc_web_device_identity';
522
+ WasmSignalingBackend.EPHEMERAL_DEVICE_ID_TTL_MS = 60000;
@@ -0,0 +1,23 @@
1
+ import { ITransport } from './ITransport';
2
+ export declare class WebRTCTransport implements ITransport {
3
+ readonly name = "webrtc";
4
+ get isReady(): boolean;
5
+ private pc;
6
+ private dc;
7
+ private state;
8
+ private pendingCandidates;
9
+ private messageListeners;
10
+ private stateListeners;
11
+ private internalSignalSender;
12
+ getNativeTransport(): RTCPeerConnection | null;
13
+ setSignalSender(sender: (msg: any) => Promise<void>): void;
14
+ init(localNodeId: string, remoteNodeId: string, options: any): Promise<void>;
15
+ private setupDataChannel;
16
+ private setupPCHandlers;
17
+ handleSignalingMessage(msg: any): void;
18
+ send(data: Uint8Array): Promise<void>;
19
+ close(): void;
20
+ onMessage(callback: (data: Uint8Array) => void): void;
21
+ onStateChange(callback: (state: 'connecting' | 'connected' | 'failed' | 'disconnected') => void): void;
22
+ private setState;
23
+ }
@@ -0,0 +1,132 @@
1
+ export class WebRTCTransport {
2
+ constructor() {
3
+ this.name = 'webrtc';
4
+ this.pc = null;
5
+ this.dc = null;
6
+ this.state = 'disconnected';
7
+ this.pendingCandidates = [];
8
+ this.messageListeners = [];
9
+ this.stateListeners = [];
10
+ this.internalSignalSender = null;
11
+ }
12
+ get isReady() { return this.state === 'connected'; }
13
+ getNativeTransport() {
14
+ return this.pc;
15
+ }
16
+ setSignalSender(sender) {
17
+ this.internalSignalSender = sender;
18
+ }
19
+ async init(localNodeId, remoteNodeId, options) {
20
+ this.setState('connecting');
21
+ const config = options.rtcConfig || {};
22
+ try {
23
+ this.pc = new RTCPeerConnection(config);
24
+ }
25
+ catch (e) {
26
+ console.error("[WebRTCTransport] Constructor failed", e);
27
+ this.setState('failed');
28
+ return;
29
+ }
30
+ const isInitiator = localNodeId > remoteNodeId;
31
+ const dc = this.pc.createDataChannel("pluto-dc", { negotiated: true, id: 0 });
32
+ this.setupDataChannel(dc);
33
+ this.setupPCHandlers(isInitiator);
34
+ }
35
+ setupDataChannel(dc) {
36
+ this.dc = dc;
37
+ dc.onopen = () => console.log("[WebRTCTransport] DC OPEN");
38
+ dc.onmessage = (ev) => {
39
+ const data = typeof ev.data === 'string' ? new TextEncoder().encode(ev.data) : new Uint8Array(ev.data);
40
+ this.messageListeners.forEach(cb => cb(data));
41
+ };
42
+ }
43
+ setupPCHandlers(isInitiator) {
44
+ if (!this.pc)
45
+ return;
46
+ this.pc.onicecandidate = (ev) => {
47
+ if (ev.candidate && this.internalSignalSender) {
48
+ this.internalSignalSender({ transport: 'webrtc', type: 'candidate', candidate: ev.candidate });
49
+ }
50
+ };
51
+ this.pc.onnegotiationneeded = async () => {
52
+ if (!isInitiator)
53
+ return;
54
+ if (this.pc?.signalingState !== 'stable')
55
+ return;
56
+ try {
57
+ const offer = await this.pc.createOffer();
58
+ await this.pc.setLocalDescription(offer);
59
+ if (this.internalSignalSender) {
60
+ this.internalSignalSender({ transport: 'webrtc', type: 'sdp', sdp: offer });
61
+ }
62
+ }
63
+ catch (e) {
64
+ console.error("Offer failed", e);
65
+ }
66
+ };
67
+ this.pc.onconnectionstatechange = () => {
68
+ const state = this.pc?.connectionState;
69
+ if (state === 'connected') {
70
+ this.setState('connected');
71
+ }
72
+ else if (state === 'failed' || state === 'disconnected') {
73
+ this.setState('failed');
74
+ }
75
+ };
76
+ }
77
+ handleSignalingMessage(msg) {
78
+ if (!this.pc || msg.transport !== 'webrtc')
79
+ return;
80
+ if (msg.type === 'candidate') {
81
+ if (this.pc.remoteDescription) {
82
+ this.pc.addIceCandidate(msg.candidate).catch(e => console.error("AddICE failed", e));
83
+ }
84
+ else {
85
+ this.pendingCandidates.push(msg.candidate);
86
+ }
87
+ }
88
+ else if (msg.type === 'sdp') {
89
+ const sdp = msg.sdp;
90
+ this.pc.setRemoteDescription(sdp).then(async () => {
91
+ if (this.pendingCandidates.length) {
92
+ this.pendingCandidates.forEach(c => this.pc?.addIceCandidate(c));
93
+ this.pendingCandidates = [];
94
+ }
95
+ if (sdp.type === 'offer') {
96
+ const answer = await this.pc.createAnswer();
97
+ await this.pc.setLocalDescription(answer);
98
+ if (this.internalSignalSender) {
99
+ this.internalSignalSender({ transport: 'webrtc', type: 'sdp', sdp: answer });
100
+ }
101
+ }
102
+ }).catch(e => console.error("SDP Error", e));
103
+ }
104
+ }
105
+ async send(data) {
106
+ if (this.dc && this.dc.readyState === 'open') {
107
+ const payload = new Uint8Array(data);
108
+ this.dc.send(payload);
109
+ }
110
+ else {
111
+ throw new Error("WebRTC DataChannel not open");
112
+ }
113
+ }
114
+ close() {
115
+ this.pc?.close();
116
+ this.pc = null;
117
+ this.dc = null;
118
+ this.setState('disconnected');
119
+ }
120
+ onMessage(callback) {
121
+ this.messageListeners.push(callback);
122
+ }
123
+ onStateChange(callback) {
124
+ this.stateListeners.push(callback);
125
+ }
126
+ setState(state) {
127
+ if (this.state !== state) {
128
+ this.state = state;
129
+ this.stateListeners.forEach(cb => cb(state));
130
+ }
131
+ }
132
+ }