homey-api 3.17.5 → 3.17.6
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.
|
@@ -6,6 +6,16 @@ const Util = require('../../Util');
|
|
|
6
6
|
const HomeyAPIError = require('../HomeyAPIError');
|
|
7
7
|
|
|
8
8
|
const SOCKET_SESSION_NOT_READY_CODE = 'ERR_SOCKET_SESSION_NOT_READY';
|
|
9
|
+
const SERVER_DISCONNECT_REASON = 'io server disconnect';
|
|
10
|
+
const NAMESPACE_STABILITY_TIMEOUT = 25;
|
|
11
|
+
const PHASES = {
|
|
12
|
+
IDLE: 'idle',
|
|
13
|
+
CONNECTING: 'connecting',
|
|
14
|
+
HANDSHAKING: 'handshaking',
|
|
15
|
+
READY: 'ready',
|
|
16
|
+
DISCONNECTING: 'disconnecting',
|
|
17
|
+
DESTROYED: 'destroyed',
|
|
18
|
+
};
|
|
9
19
|
|
|
10
20
|
class SocketSession extends EventEmitter {
|
|
11
21
|
static NOT_READY_CODE = SOCKET_SESSION_NOT_READY_CODE;
|
|
@@ -13,8 +23,11 @@ class SocketSession extends EventEmitter {
|
|
|
13
23
|
constructor(homey) {
|
|
14
24
|
super();
|
|
15
25
|
|
|
26
|
+
// The root socket owns transport connectivity. Each ready cycle then
|
|
27
|
+
// handshakes a disposable Homey namespace socket, while higher layers
|
|
28
|
+
// restore URI subscriptions against the current namespace revision.
|
|
16
29
|
this.homey = homey;
|
|
17
|
-
this.__phase =
|
|
30
|
+
this.__phase = PHASES.IDLE;
|
|
18
31
|
this.__socket = null;
|
|
19
32
|
this.__homeySocket = null;
|
|
20
33
|
this.__boundHomeySocket = null;
|
|
@@ -67,7 +80,7 @@ class SocketSession extends EventEmitter {
|
|
|
67
80
|
const abortController = new AbortController();
|
|
68
81
|
this.__readyAbortController = abortController;
|
|
69
82
|
|
|
70
|
-
const readyPromise =
|
|
83
|
+
const readyPromise = (async () => {
|
|
71
84
|
if (this.__destroyed) {
|
|
72
85
|
throw this.__createAbortError('Socket session destroyed.');
|
|
73
86
|
}
|
|
@@ -76,7 +89,7 @@ class SocketSession extends EventEmitter {
|
|
|
76
89
|
throw this.__createAbortError(abortController.signal.reason);
|
|
77
90
|
}
|
|
78
91
|
|
|
79
|
-
this.__setPhase(
|
|
92
|
+
this.__setPhase(PHASES.CONNECTING);
|
|
80
93
|
|
|
81
94
|
const baseUrl = await this.homey.baseUrl;
|
|
82
95
|
|
|
@@ -90,12 +103,12 @@ class SocketSession extends EventEmitter {
|
|
|
90
103
|
|
|
91
104
|
await this.__waitForSocketConnect(abortController.signal);
|
|
92
105
|
|
|
93
|
-
this.__setPhase(
|
|
106
|
+
this.__setPhase(PHASES.HANDSHAKING);
|
|
94
107
|
const namespace = await this.__handshakeClient(abortController.signal);
|
|
95
108
|
await this.__connectHomeySocket(namespace, abortController.signal);
|
|
96
109
|
|
|
97
110
|
this.__readyRevision += 1;
|
|
98
|
-
this.__setPhase(
|
|
111
|
+
this.__setPhase(PHASES.READY);
|
|
99
112
|
|
|
100
113
|
const readyState = {
|
|
101
114
|
...this.__getReadyState(),
|
|
@@ -105,7 +118,7 @@ class SocketSession extends EventEmitter {
|
|
|
105
118
|
this.emit('ready', readyState);
|
|
106
119
|
|
|
107
120
|
return readyState;
|
|
108
|
-
});
|
|
121
|
+
})();
|
|
109
122
|
|
|
110
123
|
this.__readyPromise = readyPromise;
|
|
111
124
|
|
|
@@ -118,7 +131,7 @@ class SocketSession extends EventEmitter {
|
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
if (!this.isConnected() && !this.__destroyed && !this.__closing) {
|
|
121
|
-
this.__setPhase(
|
|
134
|
+
this.__setPhase(PHASES.IDLE);
|
|
122
135
|
}
|
|
123
136
|
})
|
|
124
137
|
.finally(() => {
|
|
@@ -187,7 +200,7 @@ class SocketSession extends EventEmitter {
|
|
|
187
200
|
this.__closing = true;
|
|
188
201
|
this.__cancelReadyCycle('Socket session disconnected.');
|
|
189
202
|
const shouldEmitDisconnect = this.isConnected();
|
|
190
|
-
this.__setPhase(
|
|
203
|
+
this.__setPhase(PHASES.DISCONNECTING);
|
|
191
204
|
|
|
192
205
|
const homeySocket = this.__homeySocket;
|
|
193
206
|
if (homeySocket) {
|
|
@@ -222,7 +235,7 @@ class SocketSession extends EventEmitter {
|
|
|
222
235
|
}
|
|
223
236
|
|
|
224
237
|
this.__closing = false;
|
|
225
|
-
this.__setPhase(
|
|
238
|
+
this.__setPhase(PHASES.IDLE);
|
|
226
239
|
}
|
|
227
240
|
|
|
228
241
|
destroy() {
|
|
@@ -246,7 +259,7 @@ class SocketSession extends EventEmitter {
|
|
|
246
259
|
this.__socket = null;
|
|
247
260
|
}
|
|
248
261
|
|
|
249
|
-
this.__setPhase(
|
|
262
|
+
this.__setPhase(PHASES.DESTROYED);
|
|
250
263
|
this.removeAllListeners();
|
|
251
264
|
}
|
|
252
265
|
|
|
@@ -326,14 +339,29 @@ class SocketSession extends EventEmitter {
|
|
|
326
339
|
}
|
|
327
340
|
|
|
328
341
|
__handleSessionDisconnect(reason) {
|
|
329
|
-
const shouldEmitDisconnect = this.__phase ===
|
|
342
|
+
const shouldEmitDisconnect = this.__phase === PHASES.READY || this.isConnected();
|
|
330
343
|
|
|
331
344
|
this.__cancelReadyCycle('Socket disconnected.');
|
|
332
|
-
this.__setPhase(this.__closing ?
|
|
345
|
+
this.__setPhase(this.__closing ? PHASES.DISCONNECTING : PHASES.IDLE);
|
|
333
346
|
|
|
334
347
|
if (shouldEmitDisconnect) {
|
|
335
348
|
this.emit('disconnect', reason);
|
|
336
349
|
}
|
|
350
|
+
|
|
351
|
+
if (this.__homeySocket && !this.__homeySocket.connected) {
|
|
352
|
+
this.__disposeStaleHomeySocket();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (
|
|
356
|
+
reason === SERVER_DISCONNECT_REASON
|
|
357
|
+
&& this.homey.__reconnect
|
|
358
|
+
&& !this.__destroyed
|
|
359
|
+
&& !this.__closing
|
|
360
|
+
&& this.__socket
|
|
361
|
+
&& !this.__socket.connected
|
|
362
|
+
) {
|
|
363
|
+
this.__socket.connect();
|
|
364
|
+
}
|
|
337
365
|
}
|
|
338
366
|
|
|
339
367
|
async __waitForSocketConnect(signal) {
|
|
@@ -427,6 +455,11 @@ class SocketSession extends EventEmitter {
|
|
|
427
455
|
this.__homeySocket.open();
|
|
428
456
|
},
|
|
429
457
|
});
|
|
458
|
+
// Homey can accept the /api namespace and then tear it down immediately
|
|
459
|
+
// during socket registration. Wait briefly to confirm /api stays connected
|
|
460
|
+
// before treating the session as ready, so that failed cycles reject
|
|
461
|
+
// connect()/reconnect instead of reporting a false success.
|
|
462
|
+
await this.__waitForNamespaceStability(this.__homeySocket, signal);
|
|
430
463
|
|
|
431
464
|
return this.__homeySocket;
|
|
432
465
|
}
|
|
@@ -578,6 +611,67 @@ class SocketSession extends EventEmitter {
|
|
|
578
611
|
}
|
|
579
612
|
}
|
|
580
613
|
|
|
614
|
+
// Wait one short grace window after /api connects so the session is only
|
|
615
|
+
// reported as ready once the namespace has stayed connected briefly.
|
|
616
|
+
async __waitForNamespaceStability(homeySocket, signal) {
|
|
617
|
+
await new Promise((resolve, reject) => {
|
|
618
|
+
const onAbort = () => {
|
|
619
|
+
cleanup();
|
|
620
|
+
reject(this.__createAbortError(signal.reason));
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const onDisconnect = (reason) => {
|
|
624
|
+
cleanup();
|
|
625
|
+
reject(this.__createNotReadyError(reason));
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const timer = setTimeout(() => {
|
|
629
|
+
cleanup();
|
|
630
|
+
|
|
631
|
+
if (homeySocket.connected) {
|
|
632
|
+
resolve();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
reject(this.__createNotReadyError());
|
|
637
|
+
}, NAMESPACE_STABILITY_TIMEOUT);
|
|
638
|
+
|
|
639
|
+
const cleanup = () => {
|
|
640
|
+
clearTimeout(timer);
|
|
641
|
+
homeySocket.removeListener('disconnect', onDisconnect);
|
|
642
|
+
|
|
643
|
+
if (signal) {
|
|
644
|
+
signal.removeEventListener('abort', onAbort);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
homeySocket.once('disconnect', onDisconnect);
|
|
649
|
+
|
|
650
|
+
if (signal) {
|
|
651
|
+
if (signal.aborted) {
|
|
652
|
+
cleanup();
|
|
653
|
+
reject(this.__createAbortError(signal.reason));
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
__disposeStaleHomeySocket() {
|
|
663
|
+
const homeySocket = this.__homeySocket;
|
|
664
|
+
if (!homeySocket) return;
|
|
665
|
+
|
|
666
|
+
this.__homeySocket = null;
|
|
667
|
+
this.__unbindHomeySocket();
|
|
668
|
+
|
|
669
|
+
// Disconnect the stale namespace socket so it cannot auto-open on the next
|
|
670
|
+
// transport connect before ensureConnected() installs its connect/error
|
|
671
|
+
// listeners for the new reconnect attempt.
|
|
672
|
+
homeySocket.disconnect();
|
|
673
|
+
}
|
|
674
|
+
|
|
581
675
|
__createAbortError(message) {
|
|
582
676
|
const err = new Error(message || 'Socket session aborted.');
|
|
583
677
|
err.name = 'AbortError';
|
|
@@ -6,6 +6,10 @@ class SubscriptionRegistry {
|
|
|
6
6
|
constructor({ homey, session }) {
|
|
7
7
|
this.homey = homey;
|
|
8
8
|
this.session = session;
|
|
9
|
+
// Each URI entry represents one wire-level subscription for the current
|
|
10
|
+
// session revision. Multiple local consumers can share that single server
|
|
11
|
+
// subscription, and needsReconnect means the consumer state still exists
|
|
12
|
+
// locally while the server-side subscription must be restored.
|
|
9
13
|
this.__entries = new Map();
|
|
10
14
|
this.__destroyed = false;
|
|
11
15
|
|
|
@@ -127,7 +131,7 @@ class SubscriptionRegistry {
|
|
|
127
131
|
return entry.subscribePromise;
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
const subscribePromise =
|
|
134
|
+
const subscribePromise = (async () => {
|
|
131
135
|
const { homeySocket, revision } = await this.session.ensureConnected();
|
|
132
136
|
|
|
133
137
|
if (entry.removed || entry.consumers.size === 0) {
|
|
@@ -151,7 +155,7 @@ class SubscriptionRegistry {
|
|
|
151
155
|
entry.subscribedRevision = revision;
|
|
152
156
|
entry.pendingRevision = 0;
|
|
153
157
|
entry.hasConnected = true;
|
|
154
|
-
});
|
|
158
|
+
})();
|
|
155
159
|
|
|
156
160
|
entry.subscribePromise = subscribePromise;
|
|
157
161
|
|
|
@@ -232,10 +236,9 @@ class SubscriptionRegistry {
|
|
|
232
236
|
return;
|
|
233
237
|
}
|
|
234
238
|
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
// current namespace socket can be rebound safely.
|
|
239
|
+
// The transport can reconnect before the current Homey namespace is ready.
|
|
240
|
+
// Listen to both reconnect and ready so entries resume promptly, while
|
|
241
|
+
// ready is the point where the active namespace socket can be rebound.
|
|
239
242
|
for (const entry of this.__entries.values()) {
|
|
240
243
|
if (entry.consumers.size === 0 || !entry.needsReconnect) {
|
|
241
244
|
continue;
|
|
@@ -271,8 +274,8 @@ class SubscriptionRegistry {
|
|
|
271
274
|
return entry.reconnectPromise;
|
|
272
275
|
}
|
|
273
276
|
|
|
274
|
-
const reconnectPromise =
|
|
275
|
-
|
|
277
|
+
const reconnectPromise = (async () => {
|
|
278
|
+
try {
|
|
276
279
|
await this.__ensureEntrySubscribed(entry);
|
|
277
280
|
|
|
278
281
|
if (
|
|
@@ -285,22 +288,23 @@ class SubscriptionRegistry {
|
|
|
285
288
|
|
|
286
289
|
entry.needsReconnect = false;
|
|
287
290
|
this.__notifyConsumers(entry, 'onReconnect');
|
|
288
|
-
})
|
|
289
|
-
.catch((err) => {
|
|
291
|
+
} catch (err) {
|
|
290
292
|
if (entry.removed || entry.consumers.size === 0) {
|
|
291
293
|
return;
|
|
292
294
|
}
|
|
293
295
|
|
|
294
296
|
this.__notifyConsumers(entry, 'onReconnectError', err);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (entry.reconnectPromise === reconnectPromise) {
|
|
298
|
-
entry.reconnectPromise = null;
|
|
299
|
-
}
|
|
300
|
-
});
|
|
297
|
+
}
|
|
298
|
+
})();
|
|
301
299
|
|
|
302
300
|
entry.reconnectPromise = reconnectPromise;
|
|
303
301
|
|
|
302
|
+
reconnectPromise.finally(() => {
|
|
303
|
+
if (entry.reconnectPromise === reconnectPromise) {
|
|
304
|
+
entry.reconnectPromise = null;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
304
308
|
return reconnectPromise;
|
|
305
309
|
}
|
|
306
310
|
|