@phystack/hub-client 4.4.29

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 (113) hide show
  1. package/.prettierignore +10 -0
  2. package/.prettierrc +10 -0
  3. package/CHANGELOG.md +1202 -0
  4. package/README-MEDIASTREAM.md +124 -0
  5. package/README.md +302 -0
  6. package/dist/constants/constants.d.ts +7 -0
  7. package/dist/constants/constants.d.ts.map +1 -0
  8. package/dist/constants/constants.js +10 -0
  9. package/dist/constants/constants.js.map +1 -0
  10. package/dist/helpers/browser.helper.d.ts +5 -0
  11. package/dist/helpers/browser.helper.d.ts.map +1 -0
  12. package/dist/helpers/browser.helper.js +19 -0
  13. package/dist/helpers/browser.helper.js.map +1 -0
  14. package/dist/helpers/cache.helper.d.ts +6 -0
  15. package/dist/helpers/cache.helper.d.ts.map +1 -0
  16. package/dist/helpers/cache.helper.js +46 -0
  17. package/dist/helpers/cache.helper.js.map +1 -0
  18. package/dist/helpers/date.helper.d.ts +4 -0
  19. package/dist/helpers/date.helper.d.ts.map +1 -0
  20. package/dist/helpers/date.helper.js +13 -0
  21. package/dist/helpers/date.helper.js.map +1 -0
  22. package/dist/helpers/session.helper.d.ts +13 -0
  23. package/dist/helpers/session.helper.d.ts.map +1 -0
  24. package/dist/helpers/session.helper.js +88 -0
  25. package/dist/helpers/session.helper.js.map +1 -0
  26. package/dist/helpers/shorten-look-ups.helper.d.ts +3 -0
  27. package/dist/helpers/shorten-look-ups.helper.d.ts.map +1 -0
  28. package/dist/helpers/shorten-look-ups.helper.js +80 -0
  29. package/dist/helpers/shorten-look-ups.helper.js.map +1 -0
  30. package/dist/helpers/signals-client.helper.d.ts +9 -0
  31. package/dist/helpers/signals-client.helper.d.ts.map +1 -0
  32. package/dist/helpers/signals-client.helper.js +44 -0
  33. package/dist/helpers/signals-client.helper.js.map +1 -0
  34. package/dist/helpers/signals.helper.d.ts +19 -0
  35. package/dist/helpers/signals.helper.d.ts.map +1 -0
  36. package/dist/helpers/signals.helper.js +48 -0
  37. package/dist/helpers/signals.helper.js.map +1 -0
  38. package/dist/helpers/wrtc/browser.d.ts +3 -0
  39. package/dist/helpers/wrtc/browser.d.ts.map +1 -0
  40. package/dist/helpers/wrtc/browser.js +11 -0
  41. package/dist/helpers/wrtc/browser.js.map +1 -0
  42. package/dist/helpers/wrtc/index.d.ts +5 -0
  43. package/dist/helpers/wrtc/index.d.ts.map +1 -0
  44. package/dist/helpers/wrtc/index.js +30 -0
  45. package/dist/helpers/wrtc/index.js.map +1 -0
  46. package/dist/helpers/wrtc/node.d.ts +3 -0
  47. package/dist/helpers/wrtc/node.d.ts.map +1 -0
  48. package/dist/helpers/wrtc/node.js +18 -0
  49. package/dist/helpers/wrtc/node.js.map +1 -0
  50. package/dist/index.d.ts +109 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +1013 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/services/online-status-subscription.service.d.ts +28 -0
  55. package/dist/services/online-status-subscription.service.d.ts.map +1 -0
  56. package/dist/services/online-status-subscription.service.js +96 -0
  57. package/dist/services/online-status-subscription.service.js.map +1 -0
  58. package/dist/services/phyhub-connection.service.d.ts +20 -0
  59. package/dist/services/phyhub-connection.service.d.ts.map +1 -0
  60. package/dist/services/phyhub-connection.service.js +176 -0
  61. package/dist/services/phyhub-connection.service.js.map +1 -0
  62. package/dist/services/signals.service.d.ts +97 -0
  63. package/dist/services/signals.service.d.ts.map +1 -0
  64. package/dist/services/signals.service.js +536 -0
  65. package/dist/services/signals.service.js.map +1 -0
  66. package/dist/services/webrtc/datachannel.d.ts +10 -0
  67. package/dist/services/webrtc/datachannel.d.ts.map +1 -0
  68. package/dist/services/webrtc/datachannel.js +290 -0
  69. package/dist/services/webrtc/datachannel.js.map +1 -0
  70. package/dist/services/webrtc/mediastream.d.ts +10 -0
  71. package/dist/services/webrtc/mediastream.d.ts.map +1 -0
  72. package/dist/services/webrtc/mediastream.js +396 -0
  73. package/dist/services/webrtc/mediastream.js.map +1 -0
  74. package/dist/services/webrtc/peer-connection-ice.d.ts +32 -0
  75. package/dist/services/webrtc/peer-connection-ice.d.ts.map +1 -0
  76. package/dist/services/webrtc/peer-connection-ice.js +483 -0
  77. package/dist/services/webrtc/peer-connection-ice.js.map +1 -0
  78. package/dist/types/signal.types.d.ts +354 -0
  79. package/dist/types/signal.types.d.ts.map +1 -0
  80. package/dist/types/signal.types.js +53 -0
  81. package/dist/types/signal.types.js.map +1 -0
  82. package/dist/types/twin.types.d.ts +705 -0
  83. package/dist/types/twin.types.d.ts.map +1 -0
  84. package/dist/types/twin.types.js +21 -0
  85. package/dist/types/twin.types.js.map +1 -0
  86. package/dist/types/webrtc.types.d.ts +41 -0
  87. package/dist/types/webrtc.types.d.ts.map +1 -0
  88. package/dist/types/webrtc.types.js +3 -0
  89. package/dist/types/webrtc.types.js.map +1 -0
  90. package/package.json +50 -0
  91. package/src/constants/constants.ts +12 -0
  92. package/src/helpers/browser.helper.ts +15 -0
  93. package/src/helpers/cache.helper.ts +52 -0
  94. package/src/helpers/date.helper.ts +8 -0
  95. package/src/helpers/session.helper.ts +96 -0
  96. package/src/helpers/shorten-look-ups.helper.ts +75 -0
  97. package/src/helpers/signals-client.helper.ts +54 -0
  98. package/src/helpers/signals.helper.ts +41 -0
  99. package/src/helpers/wrtc/browser.ts +9 -0
  100. package/src/helpers/wrtc/index.ts +32 -0
  101. package/src/helpers/wrtc/node.ts +16 -0
  102. package/src/index.ts +1429 -0
  103. package/src/services/online-status-subscription.service.ts +127 -0
  104. package/src/services/phyhub-connection.service.ts +213 -0
  105. package/src/services/signals.service.ts +783 -0
  106. package/src/services/webrtc/datachannel.ts +421 -0
  107. package/src/services/webrtc/mediastream.ts +602 -0
  108. package/src/services/webrtc/peer-connection-ice.ts +689 -0
  109. package/src/types/lodash.d.ts +3 -0
  110. package/src/types/signal.types.ts +382 -0
  111. package/src/types/twin.types.ts +803 -0
  112. package/src/types/webrtc.types.ts +48 -0
  113. package/tsconfig.json +45 -0
package/src/index.ts ADDED
@@ -0,0 +1,1429 @@
1
+ import { Socket, SocketOptions, ManagerOptions } from 'socket.io-client';
2
+ import {
3
+ TwinStatusEnum,
4
+ ScreenTwinReportedProperties,
5
+ TwinResponse,
6
+ DeviceStatus,
7
+ EdgeTwinResponse,
8
+ EdgeTwinDesiredPropertiesResponse,
9
+ PeripheralTwinResponse,
10
+ TwinTypeEnum,
11
+ Instance,
12
+ PeripheralInstance,
13
+ } from './types/twin.types';
14
+ import {
15
+ PhygridDataChannel,
16
+ PhygridMediaStream,
17
+ WebRTCConnectionOptions,
18
+ } from './types/webrtc.types';
19
+ import { PhyHubConnection } from './services/phyhub-connection.service';
20
+ import { SignalsService } from './services/signals.service';
21
+ import { DataRequestTypeEnum, InitSignalPayload } from './types/signal.types';
22
+ import {
23
+ createWebRTCDataChannelConnection,
24
+ WebRTCDataChannelResult,
25
+ } from './services/webrtc/datachannel';
26
+ import {
27
+ createWebRTCMediaStreamConnection,
28
+ WebRTCMediaStreamResult,
29
+ } from './services/webrtc/mediastream';
30
+ // import { createWebRTCMediaStreamConnection, WebRTCMediaStreamResult } from './helpers/webrtc-mediastream';
31
+
32
+ // Define WebRTC types that might not be available in all environments
33
+ declare global {
34
+ // In browsers, these are standard WebRTC interfaces
35
+ // In Node.js, we polyfill them from @roamhq/wrtc
36
+ interface Window {
37
+ RTCVideoSource?: any;
38
+ }
39
+
40
+ namespace NodeJS {
41
+ interface Global {
42
+ MediaStream?: any;
43
+ RTCPeerConnection?: any;
44
+ RTCSessionDescription?: any;
45
+ RTCIceCandidate?: any;
46
+ RTCVideoSource?: any;
47
+ }
48
+ }
49
+
50
+ // Make RTCVideoSource available in function scope when used
51
+ var RTCVideoSource: any;
52
+ }
53
+
54
+ (async function initializeWebRTCGlobals() {
55
+ // Only run in Node environment
56
+ if (typeof window !== 'undefined') return;
57
+
58
+ try {
59
+ // Dynamically import wrtc
60
+ const wrtc = require('@roamhq/wrtc');
61
+
62
+ // Set all WebRTC globals
63
+ if (typeof global.MediaStream === 'undefined' && wrtc.MediaStream) {
64
+ global.MediaStream = wrtc.MediaStream;
65
+ console.log('Global MediaStream initialized');
66
+ }
67
+
68
+ if (typeof global.RTCPeerConnection === 'undefined' && wrtc.RTCPeerConnection) {
69
+ global.RTCPeerConnection = wrtc.RTCPeerConnection;
70
+ console.log('Global RTCPeerConnection initialized');
71
+ }
72
+
73
+ if (typeof global.RTCSessionDescription === 'undefined' && wrtc.RTCSessionDescription) {
74
+ global.RTCSessionDescription = wrtc.RTCSessionDescription;
75
+ console.log('Global RTCSessionDescription initialized');
76
+ }
77
+
78
+ if (typeof global.RTCIceCandidate === 'undefined' && wrtc.RTCIceCandidate) {
79
+ global.RTCIceCandidate = wrtc.RTCIceCandidate;
80
+ console.log('Global RTCIceCandidate initialized');
81
+ }
82
+
83
+ if (typeof (global as any).RTCVideoSource === 'undefined' && wrtc.nonstandard.RTCVideoSource) {
84
+ (global as any).RTCVideoSource = wrtc.nonstandard.RTCVideoSource;
85
+ console.log('Global RTCVideoSource initialized');
86
+ }
87
+
88
+ console.log('WebRTC globals successfully initialized');
89
+ } catch (error) {
90
+ console.error('Failed to initialize WebRTC globals:', error);
91
+ }
92
+ })().catch(err => console.error('Error in WebRTC globals initialization:', err));
93
+
94
+ interface EventPayload<T = any> {
95
+ twinId?: string;
96
+ sourceTwinId?: string;
97
+ sourceDeviceId?: string;
98
+ deviceId?: string;
99
+ status?: TwinStatusEnum;
100
+ data?: T;
101
+ }
102
+
103
+ export type MyIoOptions = Partial<ManagerOptions & SocketOptions>;
104
+
105
+ export class PhyHubClient {
106
+ private static instance: PhyHubClient | null = null;
107
+ private instanceId: string | undefined = undefined;
108
+ private moduleName: string | undefined = undefined;
109
+ private socket: Socket | null = null;
110
+ private socketConnected = false;
111
+ private emitQueue: any[] = [];
112
+ private lastDeviceStatusCheck: number = 0;
113
+ private lastDeviceStatusResponse: DeviceStatus | undefined = undefined;
114
+ private signals: SignalsService | undefined;
115
+ private subscribedTwins: Set<string> = new Set();
116
+ private twinMessageListeners: { [key: string]: Set<(message: any) => void> } = {};
117
+ private instances: Map<string, Instance> = new Map();
118
+ private peripheralInstances: Map<string, PeripheralInstance> = new Map();
119
+ private twinUpdateListeners: { [key: string]: Set<(twin: TwinResponse) => void> } = {};
120
+
121
+ private readonly EVENTS = {
122
+ PING: 'ping',
123
+ PONG: 'pong',
124
+ CONNECT: 'connect',
125
+ RECONNECT: 'reconnect',
126
+ RECONNECT_ERROR: 'reconnect_error',
127
+ RECONNECT_FAILED: 'reconnect_failed',
128
+ DISCONNECT: 'disconnect',
129
+ REPORT_SCREEN_INSTANCE_PROPERTIES: 'setScreenInstanceReportedProperties',
130
+ GET_DEVICE_STATUS: 'getDeviceStatus',
131
+ GET_DEVICE_NETWORKS: 'getDeviceNetworks',
132
+ GET_DEVICE_INSTANCE: 'getDeviceInstance',
133
+ GET_INSTANCE: 'getInstance',
134
+ SIGNAL_EVENT: 'sendEventSignal',
135
+ SIGNAL_CHECKOUT: 'sendEventSignal',
136
+ SIGNAL_PURCHASE: 'sendEventSignal',
137
+ SIGNAL_SESSION: 'sendSessionSignal',
138
+ SIGNAL_CLIENT: 'sendClientSignal',
139
+ TWIN_MESSAGE: 'twinMessage',
140
+ TWIN_SUBSCRIBE: 'twinSubscribe',
141
+ TWIN_UNSUBSCRIBE: 'twinUnsubscribe',
142
+ GET_TWIN_BY_ID: 'getTwinById',
143
+ } as const;
144
+
145
+ constructor(params: { instanceId?: string; moduleName?: string; dataResidency?: string } = {}) {
146
+ this.instanceId = params.instanceId;
147
+ if (!this.instanceId) {
148
+ if (typeof window !== 'undefined') {
149
+ console.log('Running in browser, looking for instanceId in URL hash', window.location.hash);
150
+ this.instanceId =
151
+ new URLSearchParams(window.location.hash.slice(1)).get('instanceId') || undefined;
152
+ console.log('Found instanceId in URL hash', this.instanceId);
153
+ } else {
154
+ this.instanceId = process.env.TWIN_ID;
155
+ }
156
+ }
157
+ this.moduleName = params.moduleName;
158
+ }
159
+
160
+ private async initializeConnection(): Promise<this> {
161
+ if (this.socket && this.socketConnected) {
162
+ return this;
163
+ }
164
+
165
+ try {
166
+ console.log(
167
+ 'initializeConnection()',
168
+ JSON.stringify(
169
+ {
170
+ instanceId: this.instanceId,
171
+ moduleName: this.moduleName,
172
+ },
173
+ null,
174
+ 2
175
+ )
176
+ );
177
+ const phyHubConnection = PhyHubConnection.getInstance({
178
+ instanceId: this.instanceId,
179
+ moduleName: this.moduleName,
180
+ });
181
+
182
+ this.socket = await phyHubConnection.getPhyHubSocket();
183
+
184
+ // Wait for socket to be connected
185
+ await new Promise<void>((resolve, reject) => {
186
+ if (this.socket?.connected) {
187
+ this.socketConnected = true;
188
+ resolve();
189
+ } else {
190
+ this.socket?.once('connect', () => {
191
+ this.socketConnected = true;
192
+ resolve();
193
+ });
194
+ this.socket?.once('connect_error', error => {
195
+ reject(error);
196
+ });
197
+ }
198
+ });
199
+
200
+ // Now that socket is connected, set up the listeners
201
+ this.setupSocketListeners(
202
+ (value: this) => value,
203
+ (reason?: any) => {
204
+ throw reason;
205
+ }
206
+ );
207
+
208
+ return this;
209
+ } catch (err) {
210
+ this.socket = null;
211
+ this.socketConnected = false;
212
+ throw err;
213
+ }
214
+ }
215
+
216
+ private setupSocketListeners(
217
+ resolve: (value: this) => void,
218
+ _reject: (reason?: any) => void
219
+ ): void {
220
+ if (!this.socket) return;
221
+
222
+ this.socket.on(this.EVENTS.PONG, (data: any) => {
223
+ setTimeout(() => {
224
+ this.emit('ping', { count: data.data.count + 1 });
225
+ }, 30000);
226
+ });
227
+
228
+ this.socket.on(this.EVENTS.CONNECT, () => {
229
+ this.socketConnected = true;
230
+ this.emit(this.EVENTS.PING, { count: 1 });
231
+ this.processEmitQueue();
232
+ // Re-subscribe to twins after reconnection
233
+ this.subscribedTwins.forEach(twinId => {
234
+ this.subscribeTwin(twinId).catch(error => {
235
+ console.error(`Failed to re-subscribe to twin ${twinId}:`, error);
236
+ });
237
+ });
238
+ resolve(this);
239
+ });
240
+
241
+ this.socket.io.on(this.EVENTS.RECONNECT, () => {
242
+ this.socketConnected = true;
243
+ this.processEmitQueue();
244
+
245
+ // Re-subscribe to twins after reconnection
246
+ this.subscribedTwins.forEach(twinId => {
247
+ this.subscribeTwin(twinId).catch(error => {
248
+ console.error(`Failed to re-subscribe to twin ${twinId}:`, error);
249
+ });
250
+ });
251
+ });
252
+
253
+ this.socket.io.on(this.EVENTS.RECONNECT_ERROR, () => {
254
+ this.socketConnected = false;
255
+ });
256
+
257
+ this.socket.io.on(this.EVENTS.RECONNECT_FAILED, () => {
258
+ this.socketConnected = false;
259
+ });
260
+
261
+ this.socket.on(this.EVENTS.DISCONNECT, () => {
262
+ this.socketConnected = false;
263
+ });
264
+
265
+ this.socket.on(this.EVENTS.TWIN_MESSAGE, (payload: EventPayload) => {
266
+ // console.log('Received TWIN_MESSAGE payload:', {
267
+ // JSON: JSON.stringify(payload, null, 2),
268
+ // });
269
+
270
+ if (payload.twinId && payload.data) {
271
+ // console.log('Received TWIN_MESSAGE event:', {
272
+ // hasListeners: !!this.twinMessageListeners[payload.twinId],
273
+ // listenerCount: this.twinMessageListeners[payload.twinId]?.size
274
+ // });
275
+ const listeners = this.twinMessageListeners[payload.twinId];
276
+ if (listeners) {
277
+ // console.log(`Executing ${listeners.size} listeners for twin ${payload.twinId}`);
278
+ listeners.forEach(callback => {
279
+ // console.log('Executing listener for twin:', callback);
280
+ try {
281
+ callback(payload);
282
+ } catch (error) {
283
+ console.error(`Error in twin message listener for ${payload.twinId}:`, error);
284
+ }
285
+ });
286
+ } else {
287
+ // console.log(`No listeners found for twin ${payload.twinId}`, JSON.stringify(payload, null, 2));
288
+ }
289
+ } else {
290
+ // console.log('Invalid twin message payload:', payload);
291
+ }
292
+ });
293
+
294
+ // Add global listener for twin updates
295
+ this.socket.on('twinUpdated', (payload: EventPayload<TwinResponse>) => {
296
+ console.log('twinUpdated event received', JSON.stringify(payload, null, 2));
297
+ if (payload.data) {
298
+ const twinId = payload.twinId || payload.data.id;
299
+ if (twinId && this.twinUpdateListeners[twinId]) {
300
+ // Invoke all callbacks registered for this twin
301
+ this.twinUpdateListeners[twinId].forEach(cb => {
302
+ try {
303
+ cb(payload.data!);
304
+ console.log(
305
+ 'twinUpdateListeners[twinId].forEach() callback executed',
306
+ JSON.stringify(payload.data, null, 2)
307
+ );
308
+ } catch (error) {
309
+ console.error(`Error in twin update listener for ${twinId}:`, error);
310
+ }
311
+ });
312
+ }
313
+ }
314
+ });
315
+ }
316
+
317
+ private async assureSocketConnection(): Promise<void> {
318
+ if (!this.socket || !this.socketConnected) {
319
+ await this.initializeConnection();
320
+ }
321
+ if (!this.socket) {
322
+ throw new Error('Socket not initialized');
323
+ }
324
+ }
325
+
326
+ public static async connect(
327
+ params: {
328
+ instanceId?: string;
329
+ moduleName?: string;
330
+ dataResidency?: string;
331
+ } = {}
332
+ ): Promise<PhyHubClient> {
333
+ console.info(`connect(): Connecting to phyhub`);
334
+ // TODO handle deviceId and accessKey for screen instance scenarios
335
+
336
+ if (!PhyHubClient.instance) {
337
+ PhyHubClient.instance = new PhyHubClient(params);
338
+ await PhyHubClient.instance.initializeConnection();
339
+ console.info(`connect(): Connection to phyhub initialized`);
340
+ }
341
+ return PhyHubClient.instance;
342
+ }
343
+
344
+ public getSocket = () => {
345
+ return this.socket;
346
+ };
347
+
348
+ public initializeSignals = async (initParams?: InitSignalPayload) => {
349
+ let edgeTwin;
350
+ if (!initParams) {
351
+ await this.getDeviceStatus();
352
+ if (!this.lastDeviceStatusResponse) {
353
+ throw new Error('Failed to fetch device settings');
354
+ }
355
+
356
+ edgeTwin = (await this.getInstance()) as Instance;
357
+ if (!edgeTwin) {
358
+ throw new Error('Unable to determine app settings');
359
+ }
360
+ }
361
+
362
+ const initSignalsPayload: InitSignalPayload = {
363
+ deviceId:
364
+ initParams?.deviceId ?? edgeTwin?.deviceId ?? this.lastDeviceStatusResponse?.deviceId ?? '',
365
+ installationId:
366
+ initParams?.installationId ?? edgeTwin?.properties.desired.installationId ?? '',
367
+ spaceId: initParams?.spaceId ?? this.lastDeviceStatusResponse?.spaceId ?? '',
368
+ tenantId: initParams?.tenantId ?? this.lastDeviceStatusResponse?.tenantId ?? '',
369
+ appVersion: initParams?.appVersion ?? 'XXXXXXXXXXXXXXXXXXXXXXXX',
370
+ appId: initParams?.appId ?? 'XXXXXXXXXXXXXXXXXXXXXXXX',
371
+ environment: initParams?.environment ?? this.lastDeviceStatusResponse?.gridEnv ?? '',
372
+ dataResidency: (
373
+ initParams?.dataResidency ??
374
+ this.lastDeviceStatusResponse?.dataResidency ??
375
+ ''
376
+ ).toUpperCase(),
377
+ country: initParams?.country ?? 'SE',
378
+ installationVersion: initParams?.installationVersion ?? 'XXXXXXXXXXXXXXXXXXXXXXXX',
379
+ accessToken: initParams?.accessToken ?? this.lastDeviceStatusResponse?.accessKey,
380
+ clientUserAgent: initParams?.clientUserAgent ?? undefined,
381
+ ip: initParams?.ip ?? this.lastDeviceStatusResponse?.ip?.[0]?.ipv4 ?? 'N/A',
382
+ };
383
+
384
+ this.signals = new SignalsService(this, initSignalsPayload);
385
+ return this.signals;
386
+ };
387
+
388
+ public isConnected(): boolean {
389
+ return this.socketConnected;
390
+ }
391
+
392
+ public async sendSignal(type: DataRequestTypeEnum, data: Record<string, any>) {
393
+ let eventToEmit = undefined;
394
+ switch (type) {
395
+ case DataRequestTypeEnum.EVENT:
396
+ eventToEmit = this.EVENTS.SIGNAL_EVENT;
397
+ break;
398
+ case DataRequestTypeEnum.CHECKOUT:
399
+ eventToEmit = this.EVENTS.SIGNAL_CHECKOUT;
400
+ break;
401
+ case DataRequestTypeEnum.PURCHASE:
402
+ eventToEmit = this.EVENTS.SIGNAL_PURCHASE;
403
+ break;
404
+ case DataRequestTypeEnum.SESSION:
405
+ eventToEmit = this.EVENTS.SIGNAL_SESSION;
406
+ break;
407
+ case DataRequestTypeEnum.CLIENT:
408
+ eventToEmit = this.EVENTS.SIGNAL_CLIENT;
409
+ break;
410
+ default:
411
+ throw new Error(`Unsupported type ${type}`);
412
+ }
413
+
414
+ if (!eventToEmit) {
415
+ throw new Error(`Unsupported type ${type}`);
416
+ }
417
+
418
+ const payload: EventPayload<Record<string, any>> = {
419
+ data,
420
+ };
421
+
422
+ this.emit(eventToEmit, payload);
423
+ }
424
+
425
+ public async getDeviceStatus(): Promise<any> {
426
+ await this.assureSocketConnection();
427
+
428
+ const now = Date.now();
429
+ const tenSeconds = 10 * 1000;
430
+ if (now - this.lastDeviceStatusCheck < tenSeconds) {
431
+ // console.info('getDeviceStatus(): Skipping emit - returning last known status');
432
+ return Promise.resolve(this.lastDeviceStatusResponse);
433
+ }
434
+
435
+ return new Promise((resolve, reject) => {
436
+ this.lastDeviceStatusCheck = now;
437
+ this.emit(this.EVENTS.GET_DEVICE_STATUS, (response: any) => {
438
+ this.lastDeviceStatusResponse = response;
439
+ resolve(response);
440
+ });
441
+
442
+ this.socket?.on('error', (error: any) => {
443
+ reject(error);
444
+ });
445
+ });
446
+ }
447
+
448
+ public getDataChannel = async (targetTwinId: string): Promise<PhygridDataChannel> => {
449
+ const options: WebRTCConnectionOptions = {
450
+ targetTwinId,
451
+ sendTwinMessage: this.sendTwinMessage.bind(this),
452
+ subscribeTwin: this.subscribeTwin.bind(this),
453
+ onTwinMessage: this.onTwinMessage.bind(this),
454
+ offTwinMessage: this.offTwinMessage.bind(this),
455
+ useStun: true,
456
+ isInitiator: true,
457
+ channelPrefix: 'channel',
458
+ mediaOptions: { isMediaConnection: false },
459
+ };
460
+
461
+ const result = (await createWebRTCDataChannelConnection(options)) as WebRTCDataChannelResult;
462
+ return result.dataChannel;
463
+ };
464
+
465
+ public onDataChannel = async (
466
+ sourceTwinId: string,
467
+ callback: (dc: PhygridDataChannel) => void
468
+ ) => {
469
+ const options: WebRTCConnectionOptions = {
470
+ targetTwinId: sourceTwinId,
471
+ sendTwinMessage: this.sendTwinMessage.bind(this),
472
+ subscribeTwin: this.subscribeTwin.bind(this),
473
+ onTwinMessage: this.onTwinMessage.bind(this),
474
+ offTwinMessage: this.offTwinMessage.bind(this),
475
+ useStun: true,
476
+ isInitiator: false,
477
+ channelPrefix: 'channel',
478
+ mediaOptions: { isMediaConnection: false },
479
+ };
480
+
481
+ const result = (await createWebRTCDataChannelConnection(options)) as WebRTCDataChannelResult;
482
+ callback(result.dataChannel);
483
+ };
484
+
485
+ public async getMediaStream(targetTwinId: string): Promise<{
486
+ stream: PhygridMediaStream;
487
+ close: () => void;
488
+ }> {
489
+ const options: WebRTCConnectionOptions = {
490
+ targetTwinId,
491
+ sendTwinMessage: this.sendTwinMessage.bind(this),
492
+ subscribeTwin: this.subscribeTwin.bind(this),
493
+ onTwinMessage: this.onTwinMessage.bind(this),
494
+ offTwinMessage: this.offTwinMessage.bind(this),
495
+ isInitiator: true,
496
+ channelPrefix: 'media',
497
+ mediaOptions: {
498
+ isMediaConnection: true,
499
+ mediaDirection: 'recvonly',
500
+ },
501
+ };
502
+ const result: WebRTCMediaStreamResult = await createWebRTCMediaStreamConnection(options);
503
+ return {
504
+ stream: result.mediaStream,
505
+ close: result.close,
506
+ };
507
+ }
508
+
509
+ public onMediaStream = async (
510
+ sourceTwinId: string,
511
+ callback: (stream: PhygridMediaStream) => void
512
+ ): Promise<void> => {
513
+ const options: WebRTCConnectionOptions = {
514
+ targetTwinId: sourceTwinId,
515
+ sendTwinMessage: this.sendTwinMessage.bind(this),
516
+ subscribeTwin: this.subscribeTwin.bind(this),
517
+ onTwinMessage: this.onTwinMessage.bind(this),
518
+ offTwinMessage: this.offTwinMessage.bind(this),
519
+ isInitiator: false,
520
+ channelPrefix: 'media',
521
+ mediaOptions: {
522
+ isMediaConnection: true,
523
+ },
524
+ };
525
+
526
+ const result: WebRTCMediaStreamResult = await createWebRTCMediaStreamConnection(options);
527
+ callback(result.mediaStream);
528
+ };
529
+
530
+ // TODO properties should automatically be updated when the edge twin is updated
531
+ // We could do a refresh method and call that on reconnect like we handle the resubscribes for peripherals
532
+ public async getInstance(): Promise<Instance> {
533
+ // TODO support multiple instances of different twin ids
534
+ if (!this.instanceId) {
535
+ throw new Error('Instance ID not set');
536
+ }
537
+ if (this.instances.has(this.instanceId)) {
538
+ return this.instances.get(this.instanceId)!;
539
+ }
540
+
541
+ await this.assureSocketConnection();
542
+
543
+ let instance: Instance | undefined;
544
+
545
+ let instanceTwin = await this.getTwinById(this.instanceId);
546
+ // console.log('getTwinById response', instanceTwin);
547
+
548
+ const edgeInstanceTypePrefix = 'edgeInstance';
549
+
550
+ const edgeTwinEmit = (type: string, payload: any) => {
551
+ // console.log('edgeTwinEmit', type, payload);
552
+ const { id: twinId } = instanceTwin;
553
+ const edgeTwinMessagePayload = {
554
+ type: `${edgeInstanceTypePrefix}:${type}`,
555
+ sourceTwinId: twinId,
556
+ sourceDeviceId: instanceTwin.deviceId,
557
+ data: payload,
558
+ };
559
+ // console.log('edgeTwinEmit', edgeTwinMessagePayload);
560
+ this.sendTwinMessage(twinId, edgeTwinMessagePayload);
561
+ };
562
+
563
+ const edgeTwinOn = (type: string, callback: (message: any) => void) => {
564
+ const { id: twinId } = instanceTwin;
565
+ const onEdgeTwinMessage = (payload: any) => {
566
+ // console.log('onEdgeTwinMessage callback', payload);
567
+ const messageType = payload.data?.type || payload.type;
568
+ if (messageType === `${edgeInstanceTypePrefix}:${type}`) {
569
+ // console.log('onEdgeTwinMessage', payload.data?.data || payload.data);
570
+ callback(payload.data?.data || payload.data);
571
+ } else {
572
+ // console.log('onEdgeTwinMessage', 'ignoring', messageType);
573
+ }
574
+ };
575
+ this.onTwinMessage(twinId, onEdgeTwinMessage);
576
+ };
577
+
578
+ const edgeTwinTo = (targetTwinId: string) => ({
579
+ emit: (type: string, payload: any) => {
580
+ // console.log('edgeTwinEmit', type, payload);
581
+ const edgeTwinMessagePayload = {
582
+ type: `${edgeInstanceTypePrefix}:${type}`,
583
+ sourceTwinId: instanceTwin.id,
584
+ sourceDeviceId: instanceTwin.deviceId,
585
+ data: payload,
586
+ };
587
+ // console.log('edgeTwinEmit', edgeTwinMessagePayload);
588
+ this.sendTwinMessage(targetTwinId, edgeTwinMessagePayload);
589
+ },
590
+ });
591
+
592
+ const getPeripheralTwins = async (): Promise<PeripheralTwinResponse[]> => {
593
+ const payload: EventPayload = {
594
+ data: {
595
+ instanceId: instanceTwin.id,
596
+ },
597
+ };
598
+
599
+ return new Promise((resolve, reject) => {
600
+ this.emit('getPeripheralTwins', payload, (response: any) => {
601
+ const { twins } = response;
602
+ resolve(twins || []);
603
+ });
604
+
605
+ this.socket?.on('error', (error: any) => {
606
+ reject(error);
607
+ });
608
+ });
609
+ };
610
+
611
+ const createPeripheralTwin = async (
612
+ peripheralName: string,
613
+ hardwareId: string,
614
+ desiredProperties?: Record<string, any>,
615
+ descriptors?: Record<string, string>
616
+ ): Promise<PeripheralTwinResponse> => {
617
+ const existingTwins = (await getPeripheralTwins()) as PeripheralTwinResponse[];
618
+ const existingTwin = existingTwins.find(
619
+ (twin: PeripheralTwinResponse) => twin.properties.desired.hardwareId === hardwareId
620
+ );
621
+
622
+ if (existingTwin) {
623
+ throw new Error(`Peripheral twin with hardwareId ${hardwareId} already exists`);
624
+ }
625
+
626
+ const payload: EventPayload = {
627
+ data: {
628
+ deviceId: instanceTwin.deviceId,
629
+ tenantId: instanceTwin.tenantId,
630
+ instanceId: instanceTwin.id,
631
+ peripheralName,
632
+ hardwareId,
633
+ desiredProperties,
634
+ descriptors,
635
+ },
636
+ };
637
+
638
+ return new Promise((resolve, reject) => {
639
+ this.emit('createPeripheralTwin', payload, (response: any) => {
640
+ const { twin } = response;
641
+ if (!twin) throw new Error('Failed to create peripheral twin');
642
+ resolve(twin);
643
+ });
644
+
645
+ this.socket?.on('error', (error: any) => {
646
+ reject(error);
647
+ });
648
+ });
649
+ };
650
+
651
+ const getDataChannel = async () => {
652
+ return await this.getDataChannel(instanceTwin.id);
653
+ };
654
+
655
+ const onDataChannel = async (callback: (dc: PhygridDataChannel) => void) => {
656
+ return await this.onDataChannel(instanceTwin.id, callback);
657
+ };
658
+
659
+ const getMediaStream = async () => {
660
+ if (!instance) {
661
+ throw new Error('Instance not initialized');
662
+ }
663
+ return await this.getMediaStream(instance.id);
664
+ };
665
+
666
+ const onMediaStream = async (callback: (stream: PhygridMediaStream) => void): Promise<void> => {
667
+ if (!instance) {
668
+ throw new Error('Instance not initialized');
669
+ }
670
+ await this.onMediaStream(instance.id, callback);
671
+ // Return void to match the expected return type
672
+ };
673
+
674
+ const updateReported = async (properties: Record<string, any>) => {
675
+ const newReported = {
676
+ ...properties,
677
+ };
678
+ const result = await this.updateReportedProperties(instanceTwin.id, newReported);
679
+ Object.assign(instance!, result);
680
+ return result;
681
+ };
682
+
683
+ instance = {
684
+ ...instanceTwin,
685
+ emit: edgeTwinEmit,
686
+ on: edgeTwinOn,
687
+ to: edgeTwinTo,
688
+ createPeripheralTwin,
689
+ updateReported,
690
+ getPeripheralTwins,
691
+ getDataChannel,
692
+ onDataChannel,
693
+ getMediaStream,
694
+ onMediaStream,
695
+ };
696
+
697
+ this.instances.set(this.instanceId, instance);
698
+ return instance;
699
+ }
700
+
701
+ public async getEdgeInstance(): Promise<Instance> {
702
+ return this.getInstance();
703
+ }
704
+
705
+ public async getScreenInstance(): Promise<Instance> {
706
+ return this.getInstance();
707
+ }
708
+
709
+ // TODO properties should automatically be updated when the edge twin is updated
710
+ public async getPeripheralInstance(peripheralTwinId: string): Promise<PeripheralInstance> {
711
+ if (this.peripheralInstances.has(peripheralTwinId)) {
712
+ return this.peripheralInstances.get(peripheralTwinId)!;
713
+ }
714
+ // console.log(`Getting peripheral instance for twin ID: ${peripheralTwinId}`);
715
+ let peripheralInstance: PeripheralInstance | undefined;
716
+ const edgeInstance = await this.getInstance();
717
+ if (!edgeInstance) {
718
+ console.error('Edge instance not found');
719
+ throw new Error('Edge instance not found');
720
+ }
721
+
722
+ // console.log('getting peripheral twin', peripheralTwinId);
723
+
724
+ const peripheralTwin = await this.getTwinById(peripheralTwinId);
725
+ // console.log('peripheralTwinp', peripheralTwin);
726
+ if (!peripheralTwin) {
727
+ console.error(`Peripheral twin with id ${peripheralTwinId} not found`);
728
+ throw new Error(`Peripheral twin with id ${peripheralTwinId} not found`);
729
+ }
730
+
731
+ if (peripheralTwin.type !== TwinTypeEnum.Peripheral) {
732
+ throw new Error(`Twin ${peripheralTwinId} is not a peripheral twin`);
733
+ }
734
+
735
+ // Auto-subscribe if peripheral belongs to a different device
736
+ // if (peripheralTwin.deviceId !== edgeInstance.deviceId) {
737
+ // console.log('Auto-subscribing to peripheral twin', peripheralTwinId);
738
+ // const response =
739
+ await this.subscribeTwin(peripheralTwinId);
740
+ // console.log('subscribeTwin response', response);
741
+ // }
742
+
743
+ const instanceTypePrefix = 'peripheralInstance';
744
+
745
+ const peripheralTwinEmit = (type: string, payload: any) => {
746
+ // console.log(`Peripheral ${peripheralTwinId} emitting event:`, {
747
+ // type,
748
+ // payload
749
+ // });
750
+
751
+ const messagePayload = {
752
+ type: `${instanceTypePrefix}:${peripheralTwin.id}:${type}`,
753
+ sourceTwinId: peripheralTwin.id,
754
+ sourceDeviceId: edgeInstance.deviceId,
755
+ data: payload,
756
+ };
757
+
758
+ // console.log('Sending twin message:', JSON.stringify(messagePayload, null, 2));
759
+ this.sendTwinMessage(peripheralTwin.id, messagePayload);
760
+ };
761
+
762
+ const peripheralTwinOn = (type: string, callback: (message: any) => void) => {
763
+ console.log(`Setting up listener for peripheral ${peripheralTwinId} event: ${type}`);
764
+
765
+ const onMessage = (payload: any) => {
766
+ // console.log(`Received message for peripheral ${peripheralTwinId}:`, payload);
767
+ const messageType = payload.data?.type || payload.type;
768
+ if (messageType === `${instanceTypePrefix}:${peripheralTwin.id}:${type}`) {
769
+ // console.log('Processing matching message type');
770
+ callback(payload.data?.data || payload.data);
771
+ } else {
772
+ // console.log('Ignoring non-matching message type:', messageType);
773
+ }
774
+ };
775
+
776
+ this.onTwinMessage(peripheralTwin.id, onMessage);
777
+ };
778
+
779
+ const peripheralTwinTo = (targetTwinId: string) => ({
780
+ emit: (type: string, payload: any) => {
781
+ const messagePayload = {
782
+ type: `${instanceTypePrefix}:${peripheralTwin.id}:${type}`,
783
+ sourceTwinId: edgeInstance.id,
784
+ sourceDeviceId: edgeInstance.deviceId,
785
+ data: payload,
786
+ };
787
+ this.sendTwinMessage(targetTwinId, messagePayload);
788
+ },
789
+ });
790
+
791
+ const getDataChannel = async () => {
792
+ return await this.getDataChannel(peripheralTwin.id);
793
+ };
794
+
795
+ const onDataChannel = async (callback: (dc: PhygridDataChannel) => void) => {
796
+ return await this.onDataChannel(peripheralTwin.id, callback);
797
+ };
798
+
799
+ const getMediaStream = async () => {
800
+ if (!peripheralInstance) {
801
+ throw new Error('Peripheral instance not initialized');
802
+ }
803
+ return await this.getMediaStream(peripheralInstance.id);
804
+ };
805
+
806
+ const onMediaStream = async (callback: (stream: PhygridMediaStream) => void): Promise<void> => {
807
+ if (!peripheralInstance) {
808
+ throw new Error('Peripheral instance not initialized');
809
+ }
810
+ await this.onMediaStream(peripheralInstance.id, callback);
811
+ // Return void to match the expected return type
812
+ };
813
+
814
+ const updateReported = async (properties: Record<string, any>) => {
815
+ const newReported = {
816
+ ...properties,
817
+ };
818
+ const result = await this.updateReportedProperties(peripheralTwin.id, newReported);
819
+ // console.log('hub-client updateReported result', result);
820
+ Object.assign(peripheralInstance!, result);
821
+ return result;
822
+ };
823
+
824
+ const onUpdateReported = (callback: (reportedProperties: Record<string, any>) => void) => {
825
+ let previousProperties: Record<string, any> | undefined = undefined;
826
+
827
+ console.log(`[onUpdateReported] Registering handler for all reported properties`);
828
+
829
+ this.onTwinUpdate(peripheralTwin.id, (twin: TwinResponse) => {
830
+ console.log(`[onUpdateReported] Twin update received for ${peripheralTwin.id}`);
831
+
832
+ // Only process updates for this specific peripheral
833
+ if (twin.id !== peripheralTwin.id) {
834
+ console.log(`[onUpdateReported] Twin ID mismatch: ${twin.id} !== ${peripheralTwin.id}`);
835
+ return;
836
+ }
837
+
838
+ const reported = twin.properties.reported as Record<string, any>;
839
+
840
+ // Check if reported properties exist
841
+ if (reported) {
842
+ // If this is the first update, always trigger
843
+ if (previousProperties === undefined) {
844
+ console.log(`[onUpdateReported] First update - calling callback`);
845
+ previousProperties = JSON.parse(JSON.stringify(reported)); // Deep copy
846
+ callback(reported);
847
+ return;
848
+ }
849
+
850
+ const currentPropertiesStr = JSON.stringify(reported);
851
+ const previousPropertiesStr = JSON.stringify(previousProperties);
852
+
853
+ // Debug by printing parts of the strings - last 100 chars to avoid excessive logs
854
+ console.log(
855
+ `[onUpdateReported] Current (end): ...${currentPropertiesStr.substring(currentPropertiesStr.length - 100)}`
856
+ );
857
+ console.log(
858
+ `[onUpdateReported] Previous (end): ...${previousPropertiesStr.substring(previousPropertiesStr.length - 100)}`
859
+ );
860
+
861
+ // Additional check - compare string lengths first
862
+ if (currentPropertiesStr.length !== previousPropertiesStr.length) {
863
+ console.log(
864
+ `[onUpdateReported] String length changed: ${currentPropertiesStr.length} vs ${previousPropertiesStr.length}`
865
+ );
866
+ }
867
+
868
+ const hasChanged = currentPropertiesStr !== previousPropertiesStr;
869
+
870
+ console.log(
871
+ `[onUpdateReported] Properties comparison:`,
872
+ hasChanged ? 'CHANGED' : 'UNCHANGED'
873
+ );
874
+
875
+ if (hasChanged) {
876
+ previousProperties = JSON.parse(currentPropertiesStr); // Deep copy
877
+ console.log(`[onUpdateReported] Calling callback with updated reported properties`);
878
+ callback(reported);
879
+ } else {
880
+ // Force a callback call at least every 5 updates to ensure clients get updates
881
+ // This is a safety mechanism
882
+ console.log(`[onUpdateReported] No change detected in properties`);
883
+ }
884
+ } else {
885
+ console.log(`[onUpdateReported] No reported properties found`);
886
+ }
887
+ });
888
+ };
889
+
890
+ const onUpdateDesired = (callback: (desiredProperties: Record<string, any>) => void) => {
891
+ let previousProperties: Record<string, any> | undefined = undefined;
892
+
893
+ console.log(`[onUpdateDesired] Registering handler for all desired properties`);
894
+
895
+ this.onTwinUpdate(peripheralTwin.id, (twin: TwinResponse) => {
896
+ console.log(`[onUpdateDesired] Twin update received for ${peripheralTwin.id}`);
897
+
898
+ // Only process updates for this specific peripheral
899
+ if (twin.id !== peripheralTwin.id) {
900
+ console.log(`[onUpdateDesired] Twin ID mismatch: ${twin.id} !== ${peripheralTwin.id}`);
901
+ return;
902
+ }
903
+
904
+ const desired = twin.properties.desired as Record<string, any>;
905
+
906
+ // Check if desired properties exist
907
+ if (desired) {
908
+ // If this is the first update, always trigger
909
+ if (previousProperties === undefined) {
910
+ console.log(`[onUpdateDesired] First update - calling callback`);
911
+ previousProperties = JSON.parse(JSON.stringify(desired)); // Deep copy
912
+ callback(desired);
913
+ return;
914
+ }
915
+
916
+ const currentPropertiesStr = JSON.stringify(desired);
917
+ const previousPropertiesStr = JSON.stringify(previousProperties);
918
+
919
+ // Debug by printing parts of the strings - last 100 chars to avoid excessive logs
920
+ console.log(
921
+ `[onUpdateDesired] Current (end): ...${currentPropertiesStr.substring(currentPropertiesStr.length - 100)}`
922
+ );
923
+ console.log(
924
+ `[onUpdateDesired] Previous (end): ...${previousPropertiesStr.substring(previousPropertiesStr.length - 100)}`
925
+ );
926
+
927
+ // Additional check - compare string lengths first
928
+ if (currentPropertiesStr.length !== previousPropertiesStr.length) {
929
+ console.log(
930
+ `[onUpdateDesired] String length changed: ${currentPropertiesStr.length} vs ${previousPropertiesStr.length}`
931
+ );
932
+ }
933
+
934
+ const hasChanged = currentPropertiesStr !== previousPropertiesStr;
935
+
936
+ console.log(
937
+ `[onUpdateDesired] Properties comparison:`,
938
+ hasChanged ? 'CHANGED' : 'UNCHANGED'
939
+ );
940
+
941
+ if (hasChanged) {
942
+ previousProperties = JSON.parse(currentPropertiesStr); // Deep copy
943
+ console.log(`[onUpdateDesired] Calling callback with updated desired properties`);
944
+ callback(desired);
945
+ } else {
946
+ // Force a callback call at least every 5 updates to ensure clients get updates
947
+ // This is a safety mechanism
948
+ console.log(`[onUpdateDesired] No change detected in properties`);
949
+ }
950
+ } else {
951
+ console.log(`[onUpdateDesired] No desired properties found`);
952
+ }
953
+ });
954
+ };
955
+
956
+ const remove = async (): Promise<TwinResponse | void> => {
957
+ const payload: EventPayload = {
958
+ data: { twinId: peripheralTwin.id },
959
+ };
960
+
961
+ return new Promise((resolve, reject) => {
962
+ this.emit('deletePeripheralTwin', payload, (response: any) => {
963
+ const { twin } = response;
964
+ resolve(twin);
965
+ });
966
+
967
+ this.socket?.on('error', (error: any) => {
968
+ reject(error);
969
+ });
970
+ });
971
+ };
972
+
973
+ const updateDesired = async (properties: Record<string, any>): Promise<TwinResponse> => {
974
+ const payload: EventPayload = {
975
+ twinId: peripheralTwin.id,
976
+ data: properties,
977
+ };
978
+
979
+ return new Promise((resolve, reject) => {
980
+ this.emit('updatePeripheralTwinDesired', payload, (response: any) => {
981
+ const { twin } = response;
982
+ resolve(twin || {});
983
+ });
984
+
985
+ this.socket?.on('error', (error: any) => {
986
+ reject(error);
987
+ });
988
+ });
989
+ };
990
+
991
+ peripheralInstance = {
992
+ ...peripheralTwin,
993
+ emit: peripheralTwinEmit,
994
+ on: peripheralTwinOn,
995
+ to: peripheralTwinTo,
996
+ updateReported,
997
+ updateDesired,
998
+ onUpdateReported,
999
+ onUpdateDesired,
1000
+ remove,
1001
+ getDataChannel,
1002
+ onDataChannel,
1003
+ getMediaStream,
1004
+ onMediaStream,
1005
+ };
1006
+ this.peripheralInstances.set(peripheralTwinId, peripheralInstance);
1007
+ return peripheralInstance;
1008
+ }
1009
+
1010
+ private getGridApp() {
1011
+ let currentWindow: Window & typeof globalThis = window;
1012
+ while (currentWindow) {
1013
+ try {
1014
+ if (
1015
+ (currentWindow as any).gridapp &&
1016
+ typeof (currentWindow as any).gridapp.getSettings === 'function'
1017
+ ) {
1018
+ return (currentWindow as any).gridapp;
1019
+ }
1020
+ if (currentWindow.parent === currentWindow) break;
1021
+ currentWindow = currentWindow.parent as Window & typeof globalThis;
1022
+ } catch (e) {
1023
+ break;
1024
+ }
1025
+ }
1026
+ return null;
1027
+ }
1028
+
1029
+ public async getSettings(): Promise<Partial<EdgeTwinDesiredPropertiesResponse>> {
1030
+ if (typeof window !== 'undefined') {
1031
+ const gridapp = this.getGridApp();
1032
+ if (gridapp) {
1033
+ return gridapp.getSettings() as Partial<EdgeTwinDesiredPropertiesResponse>;
1034
+ }
1035
+ }
1036
+
1037
+ // Existing fallback logic
1038
+ const edgeInstance = await this.getInstance();
1039
+ if (!edgeInstance) {
1040
+ throw new Error('Edge instance not found');
1041
+ }
1042
+ return (edgeInstance.properties.desired as EdgeTwinDesiredPropertiesResponse).settings;
1043
+ }
1044
+
1045
+ public async getEdgeSettings(): Promise<Partial<EdgeTwinDesiredPropertiesResponse>> {
1046
+ return this.getSettings();
1047
+ }
1048
+
1049
+ public async getScreenSettings(): Promise<Partial<EdgeTwinDesiredPropertiesResponse>> {
1050
+ return this.getSettings();
1051
+ }
1052
+
1053
+ // todo: fix return type, remove any
1054
+ public async getDeviceInstance(): Promise<any> {
1055
+ await this.assureSocketConnection();
1056
+
1057
+ return new Promise((resolve, reject) => {
1058
+ this.emit(this.EVENTS.GET_DEVICE_INSTANCE, (response: any) => {
1059
+ resolve(response);
1060
+ });
1061
+
1062
+ this.socket?.on('error', (error: any) => {
1063
+ reject(error);
1064
+ });
1065
+ });
1066
+ }
1067
+
1068
+ // todo: fix return type, remove any
1069
+ public async getDeviceNetworks(): Promise<any> {
1070
+ await this.assureSocketConnection();
1071
+
1072
+ return new Promise((resolve, reject) => {
1073
+ this.emit(this.EVENTS.GET_DEVICE_NETWORKS, (response: any) => {
1074
+ resolve(response);
1075
+ });
1076
+
1077
+ this.socket?.on('error', (error: any) => {
1078
+ reject(error);
1079
+ });
1080
+ });
1081
+ }
1082
+
1083
+ // todo: fix return type, remove any
1084
+ public async setScreenInstanceReportedProperties(
1085
+ payload: EventPayload<ScreenTwinReportedProperties>
1086
+ ): Promise<any> {
1087
+ await this.assureSocketConnection();
1088
+
1089
+ console.info(`setScreenInstanceReportedProperties(): payload ${JSON.stringify(payload)}`);
1090
+
1091
+ return new Promise((resolve, reject) => {
1092
+ this.emit(this.EVENTS.REPORT_SCREEN_INSTANCE_PROPERTIES, payload, (response: any) => {
1093
+ resolve(response);
1094
+ });
1095
+
1096
+ this.socket?.on('error', (error: any) => {
1097
+ reject(error);
1098
+ });
1099
+ });
1100
+ }
1101
+
1102
+ // todo: fix return type, remove any for clarity
1103
+ public emit(method: string, ...args: any[]): void {
1104
+ if (!this.socket) {
1105
+ console.error('emit(): Socket not created');
1106
+ return;
1107
+ }
1108
+
1109
+ const event = this.instanceId;
1110
+ const callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
1111
+ const payload = { method, ...args[0] };
1112
+
1113
+ const emitArgs = [event, payload];
1114
+
1115
+ if (callback) {
1116
+ emitArgs.push(callback);
1117
+ }
1118
+
1119
+ if (!this.socketConnected) {
1120
+ console.info('emit(): Socket not connected, adding to emitQueue', emitArgs);
1121
+ this.emitQueue.push(emitArgs);
1122
+ return;
1123
+ }
1124
+
1125
+ // console.info('hub-client is connected, emit(): Emitting', emitArgs);
1126
+ // TODO: fix ts-ignore
1127
+ // @ts-ignore
1128
+ this.socket.emit(...emitArgs);
1129
+ }
1130
+
1131
+ // todo: fix return type, remove any
1132
+ private processEmitQueue(): void {
1133
+ if (!this.socket || !this.socketConnected) {
1134
+ return;
1135
+ }
1136
+
1137
+ while (this.emitQueue.length > 0) {
1138
+ const args = this.emitQueue.shift();
1139
+ if (args) {
1140
+ this.socket.emit.apply(this.socket, args);
1141
+ }
1142
+ }
1143
+ }
1144
+
1145
+ public onTwinUpdate(targetTwinId: string, callback: (twin: TwinResponse) => void) {
1146
+ console.log(`onTwinUpdate() called for twin ${targetTwinId}`);
1147
+ this.assureSocketConnection();
1148
+
1149
+ // Initialize the set of listeners for this twin if it doesn't exist
1150
+ if (!this.twinUpdateListeners[targetTwinId]) {
1151
+ this.twinUpdateListeners[targetTwinId] = new Set();
1152
+ }
1153
+
1154
+ // Add the callback to the set of listeners
1155
+ this.twinUpdateListeners[targetTwinId].add(callback);
1156
+ console.log(
1157
+ `Added twin update listener for ${targetTwinId}. Total listeners: ${this.twinUpdateListeners[targetTwinId].size}`
1158
+ );
1159
+
1160
+ // Make sure we're subscribed to this twin
1161
+ if (!this.subscribedTwins.has(targetTwinId)) {
1162
+ this.subscribeTwin(targetTwinId).catch(error => {
1163
+ console.error(`Failed to subscribe to twin ${targetTwinId} for updates:`, error);
1164
+ });
1165
+ }
1166
+ }
1167
+
1168
+ public offTwinUpdate(targetTwinId: string, callback: (twin: TwinResponse) => void): boolean {
1169
+ console.log(`Removing update listener for twin ${targetTwinId}`);
1170
+
1171
+ if (!this.twinUpdateListeners[targetTwinId]) {
1172
+ console.log(`No update listeners found for twin ${targetTwinId}`);
1173
+ return false;
1174
+ }
1175
+
1176
+ const result = this.twinUpdateListeners[targetTwinId].delete(callback);
1177
+
1178
+ if (this.twinUpdateListeners[targetTwinId].size === 0) {
1179
+ delete this.twinUpdateListeners[targetTwinId];
1180
+ console.log(`Removed last update listener for twin ${targetTwinId}, cleaning up`);
1181
+ } else {
1182
+ console.log(
1183
+ `Removed update listener. Remaining listeners: ${this.twinUpdateListeners[targetTwinId].size}`
1184
+ );
1185
+ }
1186
+
1187
+ return result;
1188
+ }
1189
+
1190
+ public async sendTwinMessage(
1191
+ targetTwinId: string,
1192
+ data: any,
1193
+ callback?: (response: any) => void
1194
+ ) {
1195
+ // console.log('sendTwinMessage called with:', {
1196
+ // targetTwinId,
1197
+ // data,
1198
+ // hasCallback: !!callback
1199
+ // });
1200
+
1201
+ const edgeInstance = await this.getInstance();
1202
+ if (!edgeInstance) {
1203
+ console.error('sendTwinMessage failed: Edge instance not found');
1204
+ throw new Error('Edge instance not found');
1205
+ }
1206
+
1207
+ const { id: edgeTwinId, deviceId } = edgeInstance;
1208
+
1209
+ if (!deviceId) {
1210
+ console.error('sendTwinMessage failed: Device ID not available');
1211
+ throw new Error('Device ID not available - ensure device is connected');
1212
+ }
1213
+
1214
+ const payload: EventPayload = {
1215
+ twinId: targetTwinId,
1216
+ sourceTwinId: edgeTwinId,
1217
+ sourceDeviceId: deviceId,
1218
+ data,
1219
+ };
1220
+
1221
+ // console.log('sendTwinMessage prepared payload:', JSON.stringify(payload, null, 2));
1222
+
1223
+ if (callback) {
1224
+ // console.log('sendTwinMessage emitting with callback');
1225
+ this.emit(this.EVENTS.TWIN_MESSAGE, payload, (response: any) => {
1226
+ // console.log('sendTwinMessage callback received response:', response);
1227
+ callback(response);
1228
+ });
1229
+ } else {
1230
+ // console.log('sendTwinMessage emitting with promise');
1231
+ return new Promise((resolve, reject) => {
1232
+ this.emit(this.EVENTS.TWIN_MESSAGE, payload, (response: any) => {
1233
+ // console.log('sendTwinMessage promise received response:', response);
1234
+ resolve(response);
1235
+ });
1236
+
1237
+ this.socket?.on('error', (error: any) => {
1238
+ console.error('sendTwinMessage socket error:', error);
1239
+ reject(error);
1240
+ });
1241
+ });
1242
+ }
1243
+ }
1244
+
1245
+ public async onTwinMessage(
1246
+ targetTwinId: string,
1247
+ callback: (message: any) => void
1248
+ ): Promise<void> {
1249
+ console.log('onTwinMessage called for twin:', {
1250
+ targetTwinId,
1251
+ existingListeners: this.twinMessageListeners[targetTwinId]?.size || 0,
1252
+ });
1253
+
1254
+ if (!this.twinMessageListeners[targetTwinId]) {
1255
+ // console.log(`Creating new listener set for twin ${targetTwinId}`);
1256
+ this.twinMessageListeners[targetTwinId] = new Set();
1257
+ }
1258
+
1259
+ this.twinMessageListeners[targetTwinId].add(callback);
1260
+ console.log(
1261
+ `Added new callback. Total listeners: ${this.twinMessageListeners[targetTwinId].size}`
1262
+ );
1263
+
1264
+ // console.log(`Adding callback to listeners for twin ${targetTwinId}`);
1265
+ this.twinMessageListeners[targetTwinId].add(callback);
1266
+
1267
+ // console.log('Current listener count:', {
1268
+ // targetTwinId,
1269
+ // listenerCount: this.twinMessageListeners[targetTwinId].size,
1270
+ // allListeners: Object.keys(this.twinMessageListeners).map(id => ({
1271
+ // twinId: id,
1272
+ // count: this.twinMessageListeners[id].size
1273
+ // }))
1274
+ // });
1275
+ }
1276
+
1277
+ public offTwinMessage(targetTwinId: string, callback: (message: any) => void): boolean {
1278
+ console.log(`Removing message listener for twin ${targetTwinId}`);
1279
+
1280
+ if (!this.twinMessageListeners[targetTwinId]) {
1281
+ console.log(`No listeners found for twin ${targetTwinId}`);
1282
+ return false;
1283
+ }
1284
+
1285
+ const result = this.twinMessageListeners[targetTwinId].delete(callback);
1286
+
1287
+ if (this.twinMessageListeners[targetTwinId].size === 0) {
1288
+ delete this.twinMessageListeners[targetTwinId];
1289
+ console.log(`Removed last listener for twin ${targetTwinId}, cleaning up`);
1290
+ } else {
1291
+ console.log(
1292
+ `Removed listener. Remaining listeners: ${this.twinMessageListeners[targetTwinId].size}`
1293
+ );
1294
+ }
1295
+
1296
+ return result;
1297
+ }
1298
+
1299
+ public removeMessageListener(twinId: string, callback: (message: any) => void): void {
1300
+ const listeners = this.twinMessageListeners[twinId];
1301
+ if (listeners) {
1302
+ listeners.delete(callback);
1303
+ if (listeners.size === 0) {
1304
+ delete this.twinMessageListeners[twinId];
1305
+ }
1306
+ }
1307
+ }
1308
+
1309
+ public async subscribeTwin(targetTwinId: string, callback?: (response: any) => void) {
1310
+ console.log(`[subscribeTwin] Starting subscription to twin ${targetTwinId}`);
1311
+ console.log(`[subscribeTwin] Current subscribed twins:`, Array.from(this.subscribedTwins));
1312
+ console.log(`[subscribeTwin] Current listeners:`, Object.keys(this.twinMessageListeners));
1313
+
1314
+ this.subscribedTwins.add(targetTwinId);
1315
+ const edgeInstance = await this.getInstance();
1316
+ if (!edgeInstance) {
1317
+ throw new Error('Edge instance not found');
1318
+ }
1319
+
1320
+ const { id: edgeTwinId, deviceId } = edgeInstance;
1321
+
1322
+ const payload: EventPayload = {
1323
+ twinId: targetTwinId,
1324
+ sourceTwinId: edgeTwinId,
1325
+ sourceDeviceId: deviceId,
1326
+ };
1327
+
1328
+ console.log(`[subscribeTwin] Emitting TWIN_SUBSCRIBE with payload:`, payload);
1329
+
1330
+ if (callback) {
1331
+ this.emit(this.EVENTS.TWIN_SUBSCRIBE, payload, (response: any) => {
1332
+ console.log(`[subscribeTwin] Subscription callback response:`, response);
1333
+ callback(response);
1334
+ });
1335
+ } else {
1336
+ return new Promise((resolve, reject) => {
1337
+ this.emit(this.EVENTS.TWIN_SUBSCRIBE, payload, (response: any) => {
1338
+ console.log(`[subscribeTwin] Subscription promise response:`, response);
1339
+ resolve(response);
1340
+ });
1341
+
1342
+ this.socket?.on('error', (error: any) => {
1343
+ console.error(`[subscribeTwin] Socket error:`, error);
1344
+ reject(error);
1345
+ });
1346
+ });
1347
+ }
1348
+ }
1349
+
1350
+ public async getTwinById(twinId: string): Promise<TwinResponse> {
1351
+ const payload: EventPayload<{ twinId: string }> = {
1352
+ data: { twinId },
1353
+ };
1354
+
1355
+ return new Promise((resolve, reject) => {
1356
+ this.emit(this.EVENTS.GET_TWIN_BY_ID, payload, (response: any) => {
1357
+ const { twin } = response;
1358
+ if (!twin) {
1359
+ reject(new Error(`Twin with id ${twinId} not found`));
1360
+ return;
1361
+ }
1362
+ resolve(twin);
1363
+ });
1364
+
1365
+ this.socket?.on('error', (error: any) => {
1366
+ reject(error);
1367
+ });
1368
+ });
1369
+ }
1370
+
1371
+ // private async updateTwinById(twinId: string, newTwin: Partial<TwinResponse>): Promise<TwinResponse> {
1372
+ // const payload: EventPayload = {
1373
+ // twinId,
1374
+ // data: newTwin
1375
+ // };
1376
+
1377
+ // return new Promise((resolve, reject) => {
1378
+ // this.emit('updateTwin', payload, (response: any) => {
1379
+ // const { twin } = response;
1380
+ // if (!twin) {
1381
+ // reject(new Error(`Failed to update twin ${twinId}`));
1382
+ // return;
1383
+ // }
1384
+ // resolve(twin);
1385
+ // });
1386
+
1387
+ // this.socket?.on('error', (error: any) => {
1388
+ // reject(error);
1389
+ // });
1390
+ // });
1391
+ // }
1392
+
1393
+ private async updateReportedProperties(
1394
+ twinId: string,
1395
+ reportedProperties: Record<string, any>
1396
+ ): Promise<TwinResponse> {
1397
+ const payload: EventPayload = {
1398
+ twinId,
1399
+ data: reportedProperties,
1400
+ };
1401
+
1402
+ return new Promise((resolve, reject) => {
1403
+ this.emit('reportPeripheralTwinProperties', payload, (response: any) => {
1404
+ const { twin } = response;
1405
+ if (!twin) {
1406
+ reject(new Error(`Failed to update reported properties for twin ${twinId}`));
1407
+ return;
1408
+ }
1409
+ resolve(twin);
1410
+ });
1411
+
1412
+ this.socket?.on('error', (error: any) => {
1413
+ reject(error);
1414
+ });
1415
+ });
1416
+ }
1417
+ }
1418
+
1419
+ export const connectPhyClient = (
1420
+ params: { instanceId?: string; moduleName?: string; dataResidency?: string } = {}
1421
+ ) => PhyHubClient.connect(params);
1422
+
1423
+ export type { EdgeTwinResponse, Instance, PeripheralInstance };
1424
+
1425
+ export default {
1426
+ connectPhyClient,
1427
+ };
1428
+
1429
+ export type { PhygridDataChannel, PhygridMediaStream };