@phystack/hub-client 4.4.53 → 4.4.54

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 (108) hide show
  1. package/dist/index.d.ts +18 -25
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +252 -188
  4. package/dist/index.js.map +1 -1
  5. package/dist/peripheral-twin.d.ts +10 -9
  6. package/dist/peripheral-twin.d.ts.map +1 -1
  7. package/dist/peripheral-twin.js +24 -54
  8. package/dist/peripheral-twin.js.map +1 -1
  9. package/dist/services/phyhub-connection.service.d.ts +1 -0
  10. package/dist/services/phyhub-connection.service.d.ts.map +1 -1
  11. package/dist/services/phyhub-connection.service.js +17 -0
  12. package/dist/services/phyhub-connection.service.js.map +1 -1
  13. package/dist/services/phyhub-direct-connection.service.d.ts +21 -0
  14. package/dist/services/phyhub-direct-connection.service.d.ts.map +1 -0
  15. package/dist/services/phyhub-direct-connection.service.js +101 -0
  16. package/dist/services/phyhub-direct-connection.service.js.map +1 -0
  17. package/dist/services/webrtc/data-channel-handler.d.ts +45 -0
  18. package/dist/services/webrtc/data-channel-handler.d.ts.map +1 -0
  19. package/dist/services/webrtc/data-channel-handler.js +260 -0
  20. package/dist/services/webrtc/data-channel-handler.js.map +1 -0
  21. package/dist/services/webrtc/index.d.ts +8 -0
  22. package/dist/services/webrtc/index.d.ts.map +1 -0
  23. package/dist/services/webrtc/index.js +18 -0
  24. package/dist/services/webrtc/index.js.map +1 -0
  25. package/dist/services/webrtc/media-stream-handler.d.ts +57 -0
  26. package/dist/services/webrtc/media-stream-handler.d.ts.map +1 -0
  27. package/dist/services/webrtc/media-stream-handler.js +367 -0
  28. package/dist/services/webrtc/media-stream-handler.js.map +1 -0
  29. package/dist/services/webrtc/peer-connection-manager.d.ts +40 -0
  30. package/dist/services/webrtc/peer-connection-manager.d.ts.map +1 -0
  31. package/dist/services/webrtc/peer-connection-manager.js +335 -0
  32. package/dist/services/webrtc/peer-connection-manager.js.map +1 -0
  33. package/dist/services/webrtc/types.d.ts +133 -0
  34. package/dist/services/webrtc/types.d.ts.map +1 -0
  35. package/dist/services/webrtc/types.js +12 -0
  36. package/dist/services/webrtc/types.js.map +1 -0
  37. package/dist/services/webrtc/webrtc-globals.d.ts +4 -0
  38. package/dist/services/webrtc/webrtc-globals.d.ts.map +1 -0
  39. package/dist/services/webrtc/webrtc-globals.js +72 -0
  40. package/dist/services/webrtc/webrtc-globals.js.map +1 -0
  41. package/dist/services/webrtc/webrtc-manager.d.ts +35 -0
  42. package/dist/services/webrtc/webrtc-manager.d.ts.map +1 -0
  43. package/dist/services/webrtc/webrtc-manager.js +274 -0
  44. package/dist/services/webrtc/webrtc-manager.js.map +1 -0
  45. package/dist/test/communication-comprehensive-test.d.ts +8 -0
  46. package/dist/test/communication-comprehensive-test.d.ts.map +1 -0
  47. package/dist/test/communication-comprehensive-test.js +356 -0
  48. package/dist/test/communication-comprehensive-test.js.map +1 -0
  49. package/dist/test/webrtc-channel-names-test.d.ts +2 -0
  50. package/dist/test/webrtc-channel-names-test.d.ts.map +1 -0
  51. package/dist/test/webrtc-channel-names-test.js +177 -0
  52. package/dist/test/webrtc-channel-names-test.js.map +1 -0
  53. package/dist/test/webrtc-comprehensive-test.d.ts +2 -0
  54. package/dist/test/webrtc-comprehensive-test.d.ts.map +1 -0
  55. package/dist/test/webrtc-comprehensive-test.js +328 -0
  56. package/dist/test/webrtc-comprehensive-test.js.map +1 -0
  57. package/dist/test/webrtc-reconnect-test.d.ts +4 -0
  58. package/dist/test/webrtc-reconnect-test.d.ts.map +1 -0
  59. package/dist/test/webrtc-reconnect-test.js +244 -0
  60. package/dist/test/webrtc-reconnect-test.js.map +1 -0
  61. package/dist/test/webrtc-test-harness.d.ts +4 -0
  62. package/dist/test/webrtc-test-harness.d.ts.map +1 -0
  63. package/dist/test/webrtc-test-harness.js +169 -0
  64. package/dist/test/webrtc-test-harness.js.map +1 -0
  65. package/dist/twin-messaging.d.ts +23 -0
  66. package/dist/twin-messaging.d.ts.map +1 -0
  67. package/dist/twin-messaging.js +91 -0
  68. package/dist/twin-messaging.js.map +1 -0
  69. package/dist/types/twin.types.d.ts +36 -20
  70. package/dist/types/twin.types.d.ts.map +1 -1
  71. package/dist/types/twin.types.js.map +1 -1
  72. package/docs/webrtc-howto.md +398 -0
  73. package/docs/webrtc-test.md +330 -0
  74. package/package.json +3 -3
  75. package/scripts/webrtc-test.sh +401 -0
  76. package/src/index.ts +370 -259
  77. package/src/peripheral-twin.ts +44 -67
  78. package/src/services/phyhub-connection.service.ts +24 -0
  79. package/src/services/phyhub-direct-connection.service.ts +159 -0
  80. package/src/services/webrtc/data-channel-handler.ts +362 -0
  81. package/src/services/webrtc/index.ts +36 -0
  82. package/src/services/webrtc/media-stream-handler.ts +515 -0
  83. package/src/services/webrtc/peer-connection-manager.ts +463 -0
  84. package/src/services/webrtc/types.ts +270 -0
  85. package/src/services/webrtc/webrtc-globals.ts +108 -0
  86. package/src/services/webrtc/webrtc-manager.ts +490 -0
  87. package/src/test/communication-comprehensive-test.ts +533 -0
  88. package/src/test/webrtc-channel-names-test.ts +266 -0
  89. package/src/test/webrtc-comprehensive-test.ts +494 -0
  90. package/src/test/webrtc-reconnect-test.ts +345 -0
  91. package/src/test/webrtc-test-harness.ts +254 -0
  92. package/src/twin-messaging.ts +188 -0
  93. package/src/types/twin.types.ts +59 -20
  94. package/dist/services/webrtc/datachannel.d.ts +0 -10
  95. package/dist/services/webrtc/datachannel.d.ts.map +0 -1
  96. package/dist/services/webrtc/datachannel.js +0 -290
  97. package/dist/services/webrtc/datachannel.js.map +0 -1
  98. package/dist/services/webrtc/mediastream.d.ts +0 -10
  99. package/dist/services/webrtc/mediastream.d.ts.map +0 -1
  100. package/dist/services/webrtc/mediastream.js +0 -396
  101. package/dist/services/webrtc/mediastream.js.map +0 -1
  102. package/dist/services/webrtc/peer-connection-ice.d.ts +0 -32
  103. package/dist/services/webrtc/peer-connection-ice.d.ts.map +0 -1
  104. package/dist/services/webrtc/peer-connection-ice.js +0 -483
  105. package/dist/services/webrtc/peer-connection-ice.js.map +0 -1
  106. package/src/services/webrtc/datachannel.ts +0 -421
  107. package/src/services/webrtc/mediastream.ts +0 -602
  108. package/src/services/webrtc/peer-connection-ice.ts +0 -689
package/src/index.ts CHANGED
@@ -13,88 +13,20 @@ import {
13
13
  TwinMessageResult,
14
14
  TwinMessageResultStatus,
15
15
  } from './types/twin.types';
16
- import {
17
- PhygridDataChannel,
18
- PhygridMediaStream,
19
- WebRTCConnectionOptions,
20
- } from './types/webrtc.types';
21
16
  import { PhyHubConnection } from './services/phyhub-connection.service';
17
+ import { PhyHubDirectConnection } from './services/phyhub-direct-connection.service';
22
18
  import { SignalsService } from './services/signals.service';
23
19
  import { DataRequestTypeEnum, InitSignalPayload } from './types/signal.types';
24
20
  import {
25
- createWebRTCDataChannelConnection,
26
- WebRTCDataChannelResult,
27
- } from './services/webrtc/datachannel';
28
- import {
29
- createWebRTCMediaStreamConnection,
30
- WebRTCMediaStreamResult,
31
- } from './services/webrtc/mediastream';
21
+ WebRTCManager,
22
+ TwinMessagingInterface,
23
+ PhygridDataChannel,
24
+ PhygridMediaStream,
25
+ WebRTCManagerOptions,
26
+ MediaStreamOptions,
27
+ } from './services/webrtc';
32
28
  import { TwinRegistry } from './twin-registry';
33
- // import { createWebRTCMediaStreamConnection, WebRTCMediaStreamResult } from './helpers/webrtc-mediastream';
34
-
35
- export * from './types';
36
-
37
- // Define WebRTC types that might not be available in all environments
38
- declare global {
39
- // In browsers, these are standard WebRTC interfaces
40
- // In Node.js, we polyfill them from @roamhq/wrtc
41
- interface Window {
42
- RTCVideoSource?: any;
43
- }
44
-
45
- namespace NodeJS {
46
- interface Global {
47
- MediaStream?: any;
48
- RTCPeerConnection?: any;
49
- RTCSessionDescription?: any;
50
- RTCIceCandidate?: any;
51
- RTCVideoSource?: any;
52
- }
53
- }
54
-
55
- // Make RTCVideoSource available in function scope when used
56
- var RTCVideoSource: any;
57
- }
58
-
59
- (async function initializeWebRTCGlobals() {
60
- // Only run in Node environment
61
- if (typeof window !== 'undefined') return;
62
-
63
- try {
64
- // Dynamically import wrtc
65
- const wrtc = require('@roamhq/wrtc');
66
-
67
- // Set all WebRTC globals
68
- if (typeof global.MediaStream === 'undefined' && wrtc.MediaStream) {
69
- global.MediaStream = wrtc.MediaStream;
70
- console.log('Global MediaStream initialized');
71
- }
72
-
73
- if (typeof global.RTCPeerConnection === 'undefined' && wrtc.RTCPeerConnection) {
74
- global.RTCPeerConnection = wrtc.RTCPeerConnection;
75
- console.log('Global RTCPeerConnection initialized');
76
- }
77
-
78
- if (typeof global.RTCSessionDescription === 'undefined' && wrtc.RTCSessionDescription) {
79
- global.RTCSessionDescription = wrtc.RTCSessionDescription;
80
- console.log('Global RTCSessionDescription initialized');
81
- }
82
-
83
- if (typeof global.RTCIceCandidate === 'undefined' && wrtc.RTCIceCandidate) {
84
- global.RTCIceCandidate = wrtc.RTCIceCandidate;
85
- console.log('Global RTCIceCandidate initialized');
86
- }
87
-
88
- if (typeof (global as any).RTCVideoSource === 'undefined' && wrtc.nonstandard.RTCVideoSource) {
89
- (global as any).RTCVideoSource = wrtc.nonstandard.RTCVideoSource;
90
- console.log('Global RTCVideoSource initialized');
91
- }
92
-
93
- console.log('WebRTC globals successfully initialized');
94
- } catch (error) {
95
- console.error('Failed to initialize WebRTC globals:', error);
96
- }
97
- })().catch(err => console.error('Error in WebRTC globals initialization:', err));
29
+ import { createTwinMessaging } from './twin-messaging';
98
30
 
99
31
  export interface EventPayload<T = any> {
100
32
  twinId?: string;
@@ -122,6 +54,7 @@ export class PhyHubClient {
122
54
  private instances: Map<string, Instance> = new Map();
123
55
  private twinUpdateListeners: { [key: string]: Set<(twin: TwinResponse) => void> } = {};
124
56
  private twinInstancesRegistry: TwinRegistry | null = null;
57
+ private webrtcManager: WebRTCManager | null = null;
125
58
 
126
59
  private readonly EVENTS = {
127
60
  PING: 'ping',
@@ -210,6 +143,11 @@ export class PhyHubClient {
210
143
  }
211
144
  );
212
145
 
146
+ // In direct connection mode, fetch our device twin to get the actual twinId (ObjectId)
147
+ if (PhyHubDirectConnection.isEnabled()) {
148
+ await this.initializeDirectConnectionTwin();
149
+ }
150
+
213
151
  return this;
214
152
  } catch (err) {
215
153
  this.socket = null;
@@ -267,38 +205,50 @@ export class PhyHubClient {
267
205
  this.socketConnected = false;
268
206
  });
269
207
 
270
- this.socket.on(this.EVENTS.TWIN_MESSAGE, (payload: EventPayload, callback: (response: TwinMessageResult | null) => void) => {
271
- // console.log('Received TWIN_MESSAGE payload:', {
272
- // JSON: JSON.stringify(payload, null, 2),
273
- // });
208
+ this.socket.on(this.EVENTS.TWIN_MESSAGE, (payload: EventPayload, callback?: (response: TwinMessageResult | null) => void) => {
209
+ console.log('[TWIN_MESSAGE] Received:', {
210
+ twinId: payload.twinId,
211
+ sourceTwinId: payload.sourceTwinId,
212
+ dataType: payload.data?.type,
213
+ });
274
214
 
275
215
  let result: TwinMessageResult | null = null;
276
- if (payload.twinId && payload.data) {
277
- // console.log('Received TWIN_MESSAGE event:', {
278
- // hasListeners: !!this.twinMessageListeners[payload.twinId],
279
- // listenerCount: this.twinMessageListeners[payload.twinId]?.size
280
- // });
281
- const listeners = this.twinMessageListeners[payload.twinId];
216
+
217
+ if (payload.data) {
218
+ // Try to find listeners by sourceTwinId (messages FROM this twin)
219
+ // This is the correct lookup - we register for a peer's twin ID to receive messages FROM them
220
+ const sourceTwinId = payload.sourceTwinId;
221
+ const targetTwinId = payload.twinId;
222
+
223
+ // First try sourceTwinId (the sender) - this is what onTwinMessage registers for
224
+ let listeners = sourceTwinId ? this.twinMessageListeners[sourceTwinId] : undefined;
225
+
226
+ // Also check targetTwinId for backwards compatibility
227
+ if (!listeners && targetTwinId) {
228
+ listeners = this.twinMessageListeners[targetTwinId];
229
+ }
230
+
282
231
  if (listeners?.size) {
283
- // console.log(`Executing ${listeners.size} listeners for twin ${payload.twinId}`);
232
+ console.log(`[TWIN_MESSAGE] Executing ${listeners.size} listeners`);
284
233
  listeners.forEach(listener => {
285
- // console.log('Executing listener for twin:', listener);
286
234
  try {
287
235
  listener(payload);
288
236
  result = { status: TwinMessageResultStatus.Success, message: 'Action completed' };
289
237
  } catch (error) {
290
- console.error(`Error in twin message listener for ${payload.twinId}:`, error);
238
+ console.error(`[TWIN_MESSAGE] Error in listener:`, error);
291
239
  result = { status: TwinMessageResultStatus.Error, message: (error as Error)?.message || 'An error occurred' };
292
240
  }
293
241
  });
294
242
  } else {
295
- // even though there are no errors, still send acknowledgement to the socket server
296
- result = { status: TwinMessageResultStatus.Warning, message: `No listeners found for twin ${payload.twinId} on this socket` };
243
+ console.log(`[TWIN_MESSAGE] No listeners found for source=${sourceTwinId} or target=${targetTwinId}`);
244
+ result = { status: TwinMessageResultStatus.Warning, message: `No listeners found` };
297
245
  }
298
246
 
299
- callback(result);
247
+ if (callback) {
248
+ callback(result);
249
+ }
300
250
  } else {
301
- // console.log('Invalid twin message payload:', payload);
251
+ console.log('[TWIN_MESSAGE] Invalid payload (no data):', payload);
302
252
  }
303
253
  });
304
254
 
@@ -460,88 +410,109 @@ export class PhyHubClient {
460
410
  });
461
411
  }
462
412
 
463
- public getDataChannel = async (targetTwinId: string): Promise<PhygridDataChannel> => {
464
- const options: WebRTCConnectionOptions = {
465
- targetTwinId,
466
- sendTwinMessage: this.sendTwinMessage.bind(this),
467
- subscribeTwin: this.subscribeTwin.bind(this),
468
- onTwinMessage: this.onTwinMessage.bind(this),
469
- offTwinMessage: this.offTwinMessage.bind(this),
470
- useStun: true,
471
- isInitiator: true,
472
- channelPrefix: 'channel',
473
- mediaOptions: { isMediaConnection: false },
474
- };
475
-
476
- const result = (await createWebRTCDataChannelConnection(options)) as WebRTCDataChannelResult;
477
- return result.dataChannel;
413
+ /**
414
+ * Get a DataChannel connection to a target twin.
415
+ * Initiates a WebRTC DataChannel connection for peer-to-peer messaging.
416
+ * @param targetTwinId - The twin ID to connect to
417
+ * @param channelName - Optional channel name for multiple channels to same peer (default: 'default')
418
+ */
419
+ public getDataChannel = async (targetTwinId: string, channelName?: string): Promise<PhygridDataChannel> => {
420
+ const manager = await this.getWebRTCManager();
421
+ return manager.createDataChannel(targetTwinId, channelName);
478
422
  };
479
423
 
424
+ /**
425
+ * Listen for an incoming DataChannel from a specific twin.
426
+ * Call this when you expect another twin to initiate a connection.
427
+ * @param sourceTwinId - The twin ID that will initiate the connection
428
+ * @param callback - Called when the channel is established
429
+ * @param channelName - Optional channel name for multiple channels (default: 'default')
430
+ */
480
431
  public onDataChannel = async (
481
432
  sourceTwinId: string,
482
- callback: (dc: PhygridDataChannel) => void
483
- ) => {
484
- const options: WebRTCConnectionOptions = {
485
- targetTwinId: sourceTwinId,
486
- sendTwinMessage: this.sendTwinMessage.bind(this),
487
- subscribeTwin: this.subscribeTwin.bind(this),
488
- onTwinMessage: this.onTwinMessage.bind(this),
489
- offTwinMessage: this.offTwinMessage.bind(this),
490
- useStun: true,
491
- isInitiator: false,
492
- channelPrefix: 'channel',
493
- mediaOptions: { isMediaConnection: false },
494
- };
495
-
496
- const result = (await createWebRTCDataChannelConnection(options)) as WebRTCDataChannelResult;
497
- callback(result.dataChannel);
433
+ callback: (dc: PhygridDataChannel) => void,
434
+ channelName?: string
435
+ ): Promise<void> => {
436
+ const manager = await this.getWebRTCManager();
437
+ const channel = await manager.acceptDataChannel(sourceTwinId, channelName);
438
+ callback(channel);
498
439
  };
499
440
 
500
- public async getMediaStream(targetTwinId: string): Promise<{
441
+ /**
442
+ * Get a MediaStream connection to a target twin.
443
+ * Initiates a WebRTC MediaStream connection for peer-to-peer media.
444
+ * @param targetTwinId - The twin ID to connect to
445
+ * @param options - Optional MediaStream options (including channelName for multiple streams)
446
+ */
447
+ public async getMediaStream(
448
+ targetTwinId: string,
449
+ options?: MediaStreamOptions
450
+ ): Promise<{
501
451
  stream: PhygridMediaStream;
502
452
  close: () => void;
503
453
  }> {
504
- const options: WebRTCConnectionOptions = {
505
- targetTwinId,
506
- sendTwinMessage: this.sendTwinMessage.bind(this),
507
- subscribeTwin: this.subscribeTwin.bind(this),
508
- onTwinMessage: this.onTwinMessage.bind(this),
509
- offTwinMessage: this.offTwinMessage.bind(this),
510
- isInitiator: true,
511
- channelPrefix: 'media',
512
- mediaOptions: {
513
- isMediaConnection: true,
514
- mediaDirection: 'recvonly',
515
- },
516
- };
517
- const result: WebRTCMediaStreamResult = await createWebRTCMediaStreamConnection(options);
454
+ const manager = await this.getWebRTCManager();
455
+ const channelName = options?.channelName ?? 'default';
456
+ const stream = await manager.createMediaStream(targetTwinId, options, channelName);
518
457
  return {
519
- stream: result.mediaStream,
520
- close: result.close,
458
+ stream,
459
+ close: () => stream.close(),
521
460
  };
522
461
  }
523
462
 
463
+ /**
464
+ * Listen for an incoming MediaStream from a specific twin.
465
+ * Call this when you expect another twin to initiate a media connection.
466
+ * @param sourceTwinId - The twin ID that will initiate the connection
467
+ * @param callback - Callback when stream is received
468
+ * @param options - Optional MediaStream options (including channelName for multiple streams)
469
+ */
524
470
  public onMediaStream = async (
525
471
  sourceTwinId: string,
526
- callback: (stream: PhygridMediaStream) => void
472
+ callback: (stream: PhygridMediaStream) => void,
473
+ options?: MediaStreamOptions
527
474
  ): Promise<void> => {
528
- const options: WebRTCConnectionOptions = {
529
- targetTwinId: sourceTwinId,
530
- sendTwinMessage: this.sendTwinMessage.bind(this),
531
- subscribeTwin: this.subscribeTwin.bind(this),
532
- onTwinMessage: this.onTwinMessage.bind(this),
533
- offTwinMessage: this.offTwinMessage.bind(this),
534
- isInitiator: false,
535
- channelPrefix: 'media',
536
- mediaOptions: {
537
- isMediaConnection: true,
538
- },
539
- };
540
-
541
- const result: WebRTCMediaStreamResult = await createWebRTCMediaStreamConnection(options);
542
- callback(result.mediaStream);
475
+ const manager = await this.getWebRTCManager();
476
+ const channelName = options?.channelName ?? 'default';
477
+ const stream = await manager.acceptMediaStream(sourceTwinId, options, channelName);
478
+ callback(stream);
543
479
  };
544
480
 
481
+ /**
482
+ * Get the WebRTCManager for advanced control over WebRTC connections.
483
+ * Provides access to events, connection state, and more.
484
+ */
485
+ public async getWebRTCManager(options?: WebRTCManagerOptions): Promise<WebRTCManager> {
486
+ if (!this.webrtcManager) {
487
+ await this.assureSocketConnection();
488
+
489
+ // Create TwinMessagingInterface adapter
490
+ const twinMessaging: TwinMessagingInterface = {
491
+ sendMessage: async (targetTwinId: string, data: any) => {
492
+ await this.sendTwinMessage(targetTwinId, data);
493
+ },
494
+ subscribe: async (twinId: string) => {
495
+ await this.subscribeTwin(twinId);
496
+ },
497
+ onMessage: (twinId: string, callback: (msg: any) => void) => {
498
+ this.onTwinMessage(twinId, callback);
499
+ },
500
+ offMessage: (twinId: string, callback: (msg: any) => void) => {
501
+ this.offTwinMessage(twinId, callback);
502
+ },
503
+ getOwnTwinId: () => {
504
+ if (!this.instanceId) {
505
+ throw new Error('Instance ID not set');
506
+ }
507
+ return this.instanceId;
508
+ },
509
+ };
510
+
511
+ this.webrtcManager = new WebRTCManager(twinMessaging, options);
512
+ }
513
+ return this.webrtcManager;
514
+ }
515
+
545
516
  // TODO properties should automatically be updated when the edge twin is updated
546
517
  // We could do a refresh method and call that on reconnect like we handle the resubscribes for peripherals
547
518
  public async getInstance(): Promise<Instance> {
@@ -558,50 +529,14 @@ export class PhyHubClient {
558
529
  let instance: Instance | undefined;
559
530
 
560
531
  let instanceTwin = await this.getTwinById(this.instanceId);
561
- // console.log('getTwinById response', instanceTwin);
562
-
563
- const edgeInstanceTypePrefix = 'edgeInstance';
564
-
565
- const edgeTwinEmit = (type: string, payload: any) => {
566
- // console.log('edgeTwinEmit', type, payload);
567
- const { id: twinId } = instanceTwin;
568
- const edgeTwinMessagePayload = {
569
- type: `${edgeInstanceTypePrefix}:${type}`,
570
- sourceTwinId: twinId,
571
- sourceDeviceId: instanceTwin.deviceId,
572
- data: payload,
573
- };
574
- // console.log('edgeTwinEmit', edgeTwinMessagePayload);
575
- this.sendTwinMessage(twinId, edgeTwinMessagePayload);
576
- };
577
-
578
- const edgeTwinOn = (type: string, callback: (message: any) => void) => {
579
- const { id: twinId } = instanceTwin;
580
- const onEdgeTwinMessage = (payload: any) => {
581
- // console.log('onEdgeTwinMessage callback', payload);
582
- const messageType = payload.data?.type || payload.type;
583
- if (messageType === `${edgeInstanceTypePrefix}:${type}`) {
584
- // console.log('onEdgeTwinMessage', payload.data?.data || payload.data);
585
- callback(payload.data?.data || payload.data);
586
- } else {
587
- // console.log('onEdgeTwinMessage', 'ignoring', messageType);
588
- }
589
- };
590
- this.onTwinMessage(twinId, onEdgeTwinMessage);
591
- };
592
532
 
593
- const edgeTwinTo = (targetTwinId: string) => ({
594
- emit: (type: string, payload: any) => {
595
- // console.log('edgeTwinEmit', type, payload);
596
- const edgeTwinMessagePayload = {
597
- type: `${edgeInstanceTypePrefix}:${type}`,
598
- sourceTwinId: instanceTwin.id,
599
- sourceDeviceId: instanceTwin.deviceId,
600
- data: payload,
601
- };
602
- // console.log('edgeTwinEmit', edgeTwinMessagePayload);
603
- this.sendTwinMessage(targetTwinId, edgeTwinMessagePayload);
604
- },
533
+ // Create messaging methods using the shared factory
534
+ const messaging = createTwinMessaging({
535
+ sendEvent: (targetTwinId, payload) => this.sendEvent(targetTwinId, payload),
536
+ onTwinMessage: (twinId, callback) => this.onTwinMessage(twinId, callback),
537
+ twinId: instanceTwin.id,
538
+ deviceId: instanceTwin.deviceId,
539
+ typePrefix: 'edgeInstance',
605
540
  });
606
541
 
607
542
  const getPeripheralTwins = async (): Promise<PeripheralTwinResponse[]> => {
@@ -663,27 +598,26 @@ export class PhyHubClient {
663
598
  });
664
599
  };
665
600
 
666
- const getDataChannel = async () => {
667
- return await this.getDataChannel(instanceTwin.id);
601
+ const getDataChannel = async (channelName?: string) => {
602
+ return await this.getDataChannel(instanceTwin.id, channelName);
668
603
  };
669
604
 
670
- const onDataChannel = async (callback: (dc: PhygridDataChannel) => void) => {
671
- return await this.onDataChannel(instanceTwin.id, callback);
605
+ const onDataChannel = async (callback: (dc: PhygridDataChannel) => void, channelName?: string) => {
606
+ return await this.onDataChannel(instanceTwin.id, callback, channelName);
672
607
  };
673
608
 
674
- const getMediaStream = async () => {
609
+ const getMediaStream = async (channelName?: string) => {
675
610
  if (!instance) {
676
611
  throw new Error('Instance not initialized');
677
612
  }
678
- return await this.getMediaStream(instance.id);
613
+ return await this.getMediaStream(instance.id, { channelName });
679
614
  };
680
615
 
681
- const onMediaStream = async (callback: (stream: PhygridMediaStream) => void): Promise<void> => {
616
+ const onMediaStream = async (callback: (stream: PhygridMediaStream) => void, channelName?: string): Promise<void> => {
682
617
  if (!instance) {
683
618
  throw new Error('Instance not initialized');
684
619
  }
685
- await this.onMediaStream(instance.id, callback);
686
- // Return void to match the expected return type
620
+ await this.onMediaStream(instance.id, callback, { channelName });
687
621
  };
688
622
 
689
623
  const updateReported = async (properties: Record<string, any>) => {
@@ -697,9 +631,9 @@ export class PhyHubClient {
697
631
 
698
632
  instance = {
699
633
  ...instanceTwin,
700
- emit: edgeTwinEmit,
701
- on: edgeTwinOn,
702
- to: edgeTwinTo,
634
+ emit: messaging.emit,
635
+ on: messaging.on,
636
+ to: messaging.to,
703
637
  createPeripheralTwin,
704
638
  updateReported,
705
639
  getPeripheralTwins,
@@ -746,7 +680,7 @@ export class PhyHubClient {
746
680
  }
747
681
  if (currentWindow.parent === currentWindow) break;
748
682
  currentWindow = currentWindow.parent as Window & typeof globalThis;
749
- } catch (e) {
683
+ } catch {
750
684
  break;
751
685
  }
752
686
  }
@@ -833,11 +767,22 @@ export class PhyHubClient {
833
767
  return;
834
768
  }
835
769
 
836
- const event = this.instanceId;
837
770
  const callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
838
- const payload = { method, ...args[0] };
839
771
 
840
- const emitArgs = [event, payload];
772
+ let emitArgs: any[];
773
+
774
+ // In direct connection mode, emit directly to the event name
775
+ // In normal mode, emit to instanceId channel with method in payload (for phyos routing)
776
+ if (PhyHubDirectConnection.isEnabled()) {
777
+ // Direct mode: emit to event name directly
778
+ const payload = args[0] || {};
779
+ emitArgs = [method, payload];
780
+ } else {
781
+ // Normal mode: emit to instanceId channel with method in payload
782
+ const event = this.instanceId;
783
+ const payload = { method, ...args[0] };
784
+ emitArgs = [event, payload];
785
+ }
841
786
 
842
787
  if (callback) {
843
788
  emitArgs.push(callback);
@@ -914,27 +859,63 @@ export class PhyHubClient {
914
859
  return result;
915
860
  }
916
861
 
917
- public async sendTwinMessage(
862
+ /**
863
+ * Send a fire-and-forget event to a twin.
864
+ * This is the standard way to send events - no response is expected.
865
+ */
866
+ public sendEvent(targetTwinId: string, data: any): void {
867
+ // console.log('[sendEvent] Sending to:', targetTwinId, 'data type:', data?.type || 'unknown');
868
+
869
+ // For fire-and-forget, we need to get the instance synchronously if possible
870
+ // or queue the message. We'll use a simpler approach here.
871
+ const instanceId = this.instanceId;
872
+ if (!instanceId) {
873
+ console.error('[sendEvent] Instance ID not set, cannot send event');
874
+ return;
875
+ }
876
+
877
+ // Get deviceId from cached instance if available
878
+ const cachedInstance = this.instances.get(instanceId);
879
+ const deviceId = cachedInstance?.deviceId || this.lastDeviceStatusResponse?.deviceId;
880
+
881
+ if (!deviceId) {
882
+ console.error('[sendEvent] Device ID not available, cannot send event');
883
+ return;
884
+ }
885
+
886
+ const payload: EventPayload = {
887
+ twinId: targetTwinId,
888
+ sourceTwinId: instanceId,
889
+ sourceDeviceId: deviceId,
890
+ data,
891
+ };
892
+
893
+ // Fire-and-forget: just emit, no callback, no promise
894
+ this.emit(this.EVENTS.TWIN_MESSAGE, payload);
895
+ }
896
+
897
+ /**
898
+ * @deprecated Use instance.request() or instance.to(twinId).request() instead.
899
+ * This method uses socket.io acknowledgements which may not work in all scenarios.
900
+ * The instance API uses a more reliable emit/on pattern for request-response.
901
+ */
902
+ public async request(
918
903
  targetTwinId: string,
919
904
  data: any,
920
- callback?: (response: any) => void
921
- ) : Promise<TwinMessageResult | undefined> {
922
- // console.log('sendTwinMessage called with:', {
923
- // targetTwinId,
924
- // data,
925
- // hasCallback: !!callback
926
- // });
905
+ callback?: (response: TwinMessageResult) => void
906
+ ): Promise<TwinMessageResult | undefined> {
907
+ console.log('[request] Sending to:', targetTwinId, 'data type:', data?.type || 'unknown');
927
908
 
928
909
  const edgeInstance = await this.getInstance();
929
910
  if (!edgeInstance) {
930
- console.error('sendTwinMessage failed: Edge instance not found');
911
+ console.error('[request] Edge instance not found');
931
912
  throw new Error('Edge instance not found');
932
913
  }
933
914
 
934
915
  const { id: edgeTwinId, deviceId } = edgeInstance;
935
916
 
936
917
  if (!deviceId) {
937
- console.error('sendTwinMessage failed: Device ID not available');
918
+ console.error('[request] Device ID not available');
938
919
  throw new Error('Device ID not available - ensure device is connected');
939
920
  }
940
921
 
@@ -945,48 +926,92 @@ export class PhyHubClient {
945
926
  data,
946
927
  };
947
928
 
948
- // console.log('sendTwinMessage prepared payload:', JSON.stringify(payload, null, 2));
949
-
950
929
  if (callback) {
951
- // console.log('sendTwinMessage emitting with callback');
952
- this.emit(this.EVENTS.TWIN_MESSAGE, payload, (response: any) => {
953
- // console.log('sendTwinMessage callback received response:', response);
930
+ // Callback pattern: use socket.io acknowledgement
931
+ this.emit(this.EVENTS.TWIN_MESSAGE, payload, (response: TwinMessageResult) => {
954
932
  callback(response);
955
933
  });
956
- } else {
957
- // console.log('sendTwinMessage emitting with promise');
958
- return new Promise((resolve, reject) => {
934
+ return;
935
+ }
936
+
937
+ // Promise pattern: wait for response with timeout
938
+ return new Promise((resolve, reject) => {
939
+ const ACTION_TIMEOUT = 10 * 1000;
940
+ const timeoutId = setTimeout(() => {
941
+ if (this.socket) {
942
+ this.socket?.off(payload.data.type);
943
+ }
944
+ reject({
945
+ status: TwinMessageResultStatus.Error,
946
+ message: `Request timed out after ${ACTION_TIMEOUT}ms`
947
+ });
948
+ }, ACTION_TIMEOUT);
959
949
 
960
- const twinMessageTimeout = 10 * 1000;
961
- const twinMessageTimeoutId = setTimeout(() => {
962
- if (this.socket) {
950
+ if (this.socket) {
951
+ this.socket.on(payload.data.type, (response: TwinMessageResult) => {
952
+ if (response && response.status !== TwinMessageResultStatus.Warning) {
953
+ clearTimeout(timeoutId);
963
954
  this.socket?.off(payload.data.type);
955
+ resolve(response);
964
956
  }
957
+ });
958
+ }
965
959
 
966
- reject({ status: TwinMessageResultStatus.Error, message: `twinMessage failed to complete in specified time (${twinMessageTimeout} ms)`});
967
- }, twinMessageTimeout);
968
-
960
+ this.emit(this.EVENTS.TWIN_MESSAGE, payload, (response: TwinMessageResult) => {
961
+ // Socket.io acknowledgement received
962
+ clearTimeout(timeoutId);
969
963
  if (this.socket) {
970
- this.socket.on(payload.data.type, (response: TwinMessageResult) => {
971
- if (response && response.status !== TwinMessageResultStatus.Warning) {
972
- // clear timeout on receiving twinMessage result when response status is not warning
973
- clearTimeout(twinMessageTimeoutId);
974
-
975
- this.socket?.off(payload.data.type);
976
- resolve(response);
977
- }
978
- });
964
+ this.socket?.off(payload.data.type);
979
965
  }
966
+ resolve(response);
967
+ });
980
968
 
981
- this.emit(this.EVENTS.TWIN_MESSAGE, payload, (response: any) => {
982
- console.log('sendTwinMessage promise received response:', response);
983
- });
969
+ this.socket?.on('error', (error: any) => {
970
+ clearTimeout(timeoutId);
971
+ console.error('[request] Socket error:', error);
972
+ reject(error);
973
+ });
974
+ });
975
+ }
984
976
 
985
- this.socket?.on('error', (error: any) => {
986
- console.error('sendTwinMessage socket error:', error);
987
- reject(error);
988
- });
977
+ /**
978
+ * @deprecated Use sendEvent() for fire-and-forget events, or request() for request-response.
979
+ * This method is kept for backwards compatibility.
980
+ */
981
+ public async sendTwinMessage(
982
+ targetTwinId: string,
983
+ data: any,
984
+ callback?: (response: any) => void
985
+ ): Promise<void> {
986
+ console.log('[sendTwinMessage] Sending to:', targetTwinId, 'data type:', data?.type || 'unknown');
987
+
988
+ const edgeInstance = await this.getInstance();
989
+ if (!edgeInstance) {
990
+ console.error('sendTwinMessage failed: Edge instance not found');
991
+ throw new Error('Edge instance not found');
992
+ }
993
+
994
+ const { id: edgeTwinId, deviceId } = edgeInstance;
995
+
996
+ if (!deviceId) {
997
+ console.error('sendTwinMessage failed: Device ID not available');
998
+ throw new Error('Device ID not available - ensure device is connected');
999
+ }
1000
+
1001
+ const payload: EventPayload = {
1002
+ twinId: targetTwinId,
1003
+ sourceTwinId: edgeTwinId,
1004
+ sourceDeviceId: deviceId,
1005
+ data,
1006
+ };
1007
+
1008
+ if (callback) {
1009
+ this.emit(this.EVENTS.TWIN_MESSAGE, payload, (response: any) => {
1010
+ callback(response);
989
1011
  });
1012
+ } else {
1013
+ // Fire-and-forget for backwards compatibility (no timeout/rejection)
1014
+ this.emit(this.EVENTS.TWIN_MESSAGE, payload);
990
1015
  }
991
1016
  }
992
1017
 
@@ -1096,12 +1121,14 @@ export class PhyHubClient {
1096
1121
  }
1097
1122
 
1098
1123
  public async getTwinById(twinId: string): Promise<TwinResponse> {
1124
+ console.log(`[getTwinById] Fetching twin: ${twinId}`);
1099
1125
  const payload: EventPayload<{ twinId: string }> = {
1100
1126
  data: { twinId },
1101
1127
  };
1102
1128
 
1103
1129
  return new Promise((resolve, reject) => {
1104
1130
  this.emit(this.EVENTS.GET_TWIN_BY_ID, payload, (response: any) => {
1131
+ console.log(`[getTwinById] Response for ${twinId}:`, response);
1105
1132
  const { twin } = response;
1106
1133
  if (!twin) {
1107
1134
  reject(new Error(`Twin with id ${twinId} not found`));
@@ -1111,6 +1138,7 @@ export class PhyHubClient {
1111
1138
  });
1112
1139
 
1113
1140
  this.socket?.on('error', (error: any) => {
1141
+ console.error(`[getTwinById] Socket error for ${twinId}:`, error);
1114
1142
  reject(error);
1115
1143
  });
1116
1144
  });
@@ -1162,16 +1190,99 @@ export class PhyHubClient {
1162
1190
  });
1163
1191
  });
1164
1192
  }
1193
+
1194
+ /**
1195
+ * Initialize the device twin in direct connection mode.
1196
+ * Waits for deviceAuthenticated, then calls connectDevice to get all twins
1197
+ * for this device, including the device twin which provides the actual twinId.
1198
+ */
1199
+ private async initializeDirectConnectionTwin(): Promise<void> {
1200
+ console.log('[initializeDirectConnectionTwin] Waiting for authentication...');
1201
+
1202
+ // First wait for deviceAuthenticated event from server
1203
+ await new Promise<void>((resolve, reject) => {
1204
+ const timeout = setTimeout(() => {
1205
+ reject(new Error('Timeout waiting for deviceAuthenticated'));
1206
+ }, 10000);
1207
+
1208
+ this.socket?.once('deviceAuthenticated', (data: any) => {
1209
+ clearTimeout(timeout);
1210
+ console.log('[initializeDirectConnectionTwin] Authenticated:', data);
1211
+ resolve();
1212
+ });
1213
+ });
1214
+
1215
+ console.log('[initializeDirectConnectionTwin] Calling connectDevice...');
1216
+
1217
+ return new Promise((resolve, reject) => {
1218
+ const timeout = setTimeout(() => {
1219
+ reject(new Error('Timeout waiting for connectDevice response'));
1220
+ }, 10000);
1221
+
1222
+ // Use connectDevice to get all twins for this device
1223
+ this.socket?.emit(
1224
+ 'connectDevice',
1225
+ (response: any) => {
1226
+ clearTimeout(timeout);
1227
+ console.log('[initializeDirectConnectionTwin] connectDevice response:', JSON.stringify(response, null, 2));
1228
+
1229
+ if (response?.status === 'success' && response?.twins?.length > 0) {
1230
+ // Find the device twin from the array (type is "Device" with capital D)
1231
+ const deviceTwin = response.twins.find((t: any) => t.type === 'Device');
1232
+
1233
+ if (deviceTwin) {
1234
+ const actualTwinId = deviceTwin.id || deviceTwin._id;
1235
+ console.log(
1236
+ `[initializeDirectConnectionTwin] Got device twin. DeviceId: ${deviceTwin.deviceId}, TwinId: ${actualTwinId}`
1237
+ );
1238
+ this.instanceId = actualTwinId;
1239
+ resolve();
1240
+ } else {
1241
+ console.error('[initializeDirectConnectionTwin] No device twin found in twins array');
1242
+ reject(new Error('No device twin found for this device'));
1243
+ }
1244
+ } else if (response?.status === 'error') {
1245
+ const errorMessage = response?.message || 'Unknown error';
1246
+ console.error('[initializeDirectConnectionTwin] connectDevice failed:', errorMessage);
1247
+
1248
+ // Provide more helpful error messages
1249
+ if (errorMessage.includes('Failed to find device twin')) {
1250
+ console.error('\n=== DEVICE TWIN NOT FOUND ===');
1251
+ console.error('This device exists in the legacy database but has not been');
1252
+ console.error('properly migrated to PhyHub. The device twins need to be created.');
1253
+ console.error('');
1254
+ console.error('To fix this:');
1255
+ console.error('1. Reset isMigratedToPhyhub flag in legacy DB, OR');
1256
+ console.error('2. Use a different device that has been properly migrated');
1257
+ console.error('==============================\n');
1258
+ }
1259
+
1260
+ reject(new Error(errorMessage));
1261
+ } else {
1262
+ console.error('[initializeDirectConnectionTwin] Unexpected response:', response);
1263
+ reject(new Error('Failed to connect device'));
1264
+ }
1265
+ }
1266
+ );
1267
+ });
1268
+ }
1165
1269
  }
1166
1270
 
1167
1271
  export const connectPhyClient = (
1168
1272
  params: { instanceId?: string; moduleName?: string; dataResidency?: string } = {}
1169
1273
  ) => PhyHubClient.connect(params);
1170
1274
 
1171
- export type { EdgeTwinResponse, Instance, PeripheralInstance };
1275
+ export type { EdgeTwinResponse, Instance, PeripheralInstance, IPeripheralTwinInstance, TwinMessageResult };
1276
+ export { TwinMessageResultStatus };
1172
1277
 
1173
1278
  export default {
1174
1279
  connectPhyClient,
1175
1280
  };
1176
1281
 
1177
- export type { PhygridDataChannel, PhygridMediaStream };
1282
+ // Re-export WebRTC types for convenience
1283
+ export type { PhygridDataChannel, PhygridMediaStream, WebRTCManagerOptions, MediaStreamOptions };
1284
+ export { WebRTCManager } from './services/webrtc';
1285
+
1286
+ // Re-export twin messaging types for custom implementations
1287
+ export type { TwinMessagingContext, TwinMessagingMethods } from './twin-messaging';
1288
+ export { createTwinMessaging } from './twin-messaging';