homey-api 3.17.4 → 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.
- package/lib/HomeyAPI/HomeyAPIV3/Item.js +38 -81
- package/lib/HomeyAPI/HomeyAPIV3/Manager.js +91 -181
- package/lib/HomeyAPI/HomeyAPIV3/RealtimeConsumer.js +140 -0
- package/lib/HomeyAPI/HomeyAPIV3/SocketSession.js +711 -0
- package/lib/HomeyAPI/HomeyAPIV3/SubscriptionRegistry.js +345 -0
- package/lib/HomeyAPI/HomeyAPIV3.js +55 -332
- package/package.json +1 -1
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Util = require('../../Util');
|
|
4
|
+
|
|
5
|
+
class SubscriptionRegistry {
|
|
6
|
+
constructor({ homey, session }) {
|
|
7
|
+
this.homey = homey;
|
|
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.
|
|
13
|
+
this.__entries = new Map();
|
|
14
|
+
this.__destroyed = false;
|
|
15
|
+
|
|
16
|
+
this.__onSessionDisconnect = this.__onSessionDisconnect.bind(this);
|
|
17
|
+
this.__onSessionReconnect = this.__onSessionReconnect.bind(this);
|
|
18
|
+
this.__onSessionReady = this.__onSessionReady.bind(this);
|
|
19
|
+
this.__onSessionReadyError = this.__onSessionReadyError.bind(this);
|
|
20
|
+
|
|
21
|
+
this.session.on('disconnect', this.__onSessionDisconnect);
|
|
22
|
+
this.session.on('reconnect', this.__onSessionReconnect);
|
|
23
|
+
this.session.on('ready', this.__onSessionReady);
|
|
24
|
+
this.session.on('ready_error', this.__onSessionReadyError);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async subscribe(
|
|
28
|
+
uri,
|
|
29
|
+
{
|
|
30
|
+
onConnect = () => {},
|
|
31
|
+
onReconnect = () => {},
|
|
32
|
+
onReconnectError = () => {},
|
|
33
|
+
onDisconnect = () => {},
|
|
34
|
+
onEvent = () => {},
|
|
35
|
+
}
|
|
36
|
+
) {
|
|
37
|
+
if (this.__destroyed) {
|
|
38
|
+
throw new Error('Subscription registry destroyed.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.homey.__debug('subscribe', uri);
|
|
42
|
+
|
|
43
|
+
const entry = this.__getEntry(uri);
|
|
44
|
+
const consumer = {
|
|
45
|
+
active: true,
|
|
46
|
+
handlers: {
|
|
47
|
+
onConnect,
|
|
48
|
+
onReconnect,
|
|
49
|
+
onReconnectError,
|
|
50
|
+
onDisconnect,
|
|
51
|
+
onEvent,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
entry.consumers.add(consumer);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await this.__ensureEntrySubscribed(entry);
|
|
59
|
+
|
|
60
|
+
if (consumer.active) {
|
|
61
|
+
consumer.handlers.onConnect();
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
entry.consumers.delete(consumer);
|
|
65
|
+
this.__deleteEntryIfUnused(entry);
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
unsubscribe: () => {
|
|
71
|
+
if (!consumer.active) return;
|
|
72
|
+
|
|
73
|
+
consumer.active = false;
|
|
74
|
+
entry.consumers.delete(consumer);
|
|
75
|
+
this.__deleteEntryIfUnused(entry);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
destroy() {
|
|
81
|
+
if (this.__destroyed) return;
|
|
82
|
+
|
|
83
|
+
this.__destroyed = true;
|
|
84
|
+
|
|
85
|
+
this.session.removeListener('disconnect', this.__onSessionDisconnect);
|
|
86
|
+
this.session.removeListener('reconnect', this.__onSessionReconnect);
|
|
87
|
+
this.session.removeListener('ready', this.__onSessionReady);
|
|
88
|
+
this.session.removeListener('ready_error', this.__onSessionReadyError);
|
|
89
|
+
|
|
90
|
+
for (const entry of this.__entries.values()) {
|
|
91
|
+
entry.consumers.clear();
|
|
92
|
+
this.__teardownEntry(entry);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.__entries.clear();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
__getEntry(uri) {
|
|
99
|
+
if (this.__entries.has(uri)) {
|
|
100
|
+
return this.__entries.get(uri);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const entry = {
|
|
104
|
+
uri,
|
|
105
|
+
consumers: new Set(),
|
|
106
|
+
eventSocket: null,
|
|
107
|
+
eventHandler: (event, data) => {
|
|
108
|
+
this.__notifyConsumers(entry, 'onEvent', event, data);
|
|
109
|
+
},
|
|
110
|
+
subscribePromise: null,
|
|
111
|
+
subscribedRevision: 0,
|
|
112
|
+
pendingRevision: 0,
|
|
113
|
+
needsReconnect: false,
|
|
114
|
+
hasConnected: false,
|
|
115
|
+
reconnectPromise: null,
|
|
116
|
+
removed: false,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
this.__entries.set(uri, entry);
|
|
120
|
+
return entry;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async __ensureEntrySubscribed(entry) {
|
|
124
|
+
if (entry.removed || entry.consumers.size === 0) return;
|
|
125
|
+
|
|
126
|
+
if (entry.subscribedRevision === this.session.revision && this.session.isConnected()) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (entry.subscribePromise) {
|
|
131
|
+
return entry.subscribePromise;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const subscribePromise = (async () => {
|
|
135
|
+
const { homeySocket, revision } = await this.session.ensureConnected();
|
|
136
|
+
|
|
137
|
+
if (entry.removed || entry.consumers.size === 0) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (entry.subscribedRevision === revision) {
|
|
142
|
+
this.__bindEventSocket(entry, homeySocket);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
entry.pendingRevision = revision;
|
|
147
|
+
this.__bindEventSocket(entry, homeySocket);
|
|
148
|
+
await this.__subscribeUri(entry, homeySocket);
|
|
149
|
+
|
|
150
|
+
if (entry.removed || entry.consumers.size === 0) {
|
|
151
|
+
homeySocket.emit('unsubscribe', entry.uri);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
entry.subscribedRevision = revision;
|
|
156
|
+
entry.pendingRevision = 0;
|
|
157
|
+
entry.hasConnected = true;
|
|
158
|
+
})();
|
|
159
|
+
|
|
160
|
+
entry.subscribePromise = subscribePromise;
|
|
161
|
+
|
|
162
|
+
subscribePromise
|
|
163
|
+
.catch(() => {})
|
|
164
|
+
.finally(() => {
|
|
165
|
+
if (entry.subscribePromise === subscribePromise) {
|
|
166
|
+
entry.subscribePromise = null;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return subscribePromise;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async __subscribeUri(entry, homeySocket) {
|
|
174
|
+
await Util.timeout(
|
|
175
|
+
new Promise((resolve, reject) => {
|
|
176
|
+
const onDisconnect = (reason) => {
|
|
177
|
+
cleanup();
|
|
178
|
+
reject(new Error(reason));
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const cleanup = () => {
|
|
182
|
+
homeySocket.removeListener('disconnect', onDisconnect);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
homeySocket.once('disconnect', onDisconnect);
|
|
186
|
+
this.homey.__debug('subscribing', entry.uri);
|
|
187
|
+
homeySocket.emit('subscribe', entry.uri, (err) => {
|
|
188
|
+
cleanup();
|
|
189
|
+
|
|
190
|
+
if (err) {
|
|
191
|
+
this.homey.__debug('Failed to subscribe', entry.uri, err);
|
|
192
|
+
reject(err);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.homey.__debug('subscribed', entry.uri);
|
|
197
|
+
resolve();
|
|
198
|
+
});
|
|
199
|
+
}),
|
|
200
|
+
this.homey.constructor.SUBSCRIBE_TIMEOUT,
|
|
201
|
+
`Failed to subscribe to ${entry.uri} (Timeout after ${this.homey.constructor.SUBSCRIBE_TIMEOUT}ms).`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
__bindEventSocket(entry, homeySocket) {
|
|
206
|
+
if (entry.eventSocket === homeySocket) return;
|
|
207
|
+
|
|
208
|
+
if (entry.eventSocket) {
|
|
209
|
+
entry.eventSocket.removeListener(entry.uri, entry.eventHandler);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
entry.eventSocket = homeySocket;
|
|
213
|
+
entry.eventSocket.on(entry.uri, entry.eventHandler);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
__onSessionDisconnect(reason) {
|
|
217
|
+
for (const entry of this.__entries.values()) {
|
|
218
|
+
if (!entry.hasConnected || entry.consumers.size === 0) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
entry.needsReconnect = true;
|
|
223
|
+
entry.subscribedRevision = 0;
|
|
224
|
+
|
|
225
|
+
if (entry.eventSocket) {
|
|
226
|
+
entry.eventSocket.removeListener(entry.uri, entry.eventHandler);
|
|
227
|
+
entry.eventSocket = null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.__notifyConsumers(entry, 'onDisconnect', reason);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
__onSessionReady({ revision, homeySocket, isReconnect }) {
|
|
235
|
+
if (!isReconnect) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
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.
|
|
242
|
+
for (const entry of this.__entries.values()) {
|
|
243
|
+
if (entry.consumers.size === 0 || !entry.needsReconnect) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.__bindEventSocket(entry, homeySocket);
|
|
248
|
+
this.__resumeEntry(entry, revision);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
__onSessionReconnect() {
|
|
253
|
+
for (const entry of this.__entries.values()) {
|
|
254
|
+
if (entry.consumers.size === 0 || !entry.needsReconnect) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.__resumeEntry(entry);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
__onSessionReadyError(err) {
|
|
263
|
+
for (const entry of this.__entries.values()) {
|
|
264
|
+
if (!entry.needsReconnect || entry.consumers.size === 0 || entry.reconnectPromise) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.__notifyConsumers(entry, 'onReconnectError', err);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
__resumeEntry(entry, revision = null) {
|
|
273
|
+
if (entry.reconnectPromise) {
|
|
274
|
+
return entry.reconnectPromise;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const reconnectPromise = (async () => {
|
|
278
|
+
try {
|
|
279
|
+
await this.__ensureEntrySubscribed(entry);
|
|
280
|
+
|
|
281
|
+
if (
|
|
282
|
+
entry.removed
|
|
283
|
+
|| entry.consumers.size === 0
|
|
284
|
+
|| (revision != null && entry.subscribedRevision !== revision)
|
|
285
|
+
) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
entry.needsReconnect = false;
|
|
290
|
+
this.__notifyConsumers(entry, 'onReconnect');
|
|
291
|
+
} catch (err) {
|
|
292
|
+
if (entry.removed || entry.consumers.size === 0) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this.__notifyConsumers(entry, 'onReconnectError', err);
|
|
297
|
+
}
|
|
298
|
+
})();
|
|
299
|
+
|
|
300
|
+
entry.reconnectPromise = reconnectPromise;
|
|
301
|
+
|
|
302
|
+
reconnectPromise.finally(() => {
|
|
303
|
+
if (entry.reconnectPromise === reconnectPromise) {
|
|
304
|
+
entry.reconnectPromise = null;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return reconnectPromise;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
__notifyConsumers(entry, event, ...args) {
|
|
312
|
+
for (const consumer of entry.consumers) {
|
|
313
|
+
if (!consumer.active) continue;
|
|
314
|
+
consumer.handlers[event](...args);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
__deleteEntryIfUnused(entry) {
|
|
319
|
+
if (entry.consumers.size > 0) return;
|
|
320
|
+
|
|
321
|
+
entry.removed = true;
|
|
322
|
+
this.__entries.delete(entry.uri);
|
|
323
|
+
|
|
324
|
+
if (entry.eventSocket) {
|
|
325
|
+
entry.eventSocket.emit('unsubscribe', entry.uri);
|
|
326
|
+
} else if (this.session.homeySocket) {
|
|
327
|
+
this.session.homeySocket.emit('unsubscribe', entry.uri);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
this.__teardownEntry(entry);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
__teardownEntry(entry) {
|
|
334
|
+
if (entry.eventSocket) {
|
|
335
|
+
entry.eventSocket.removeListener(entry.uri, entry.eventHandler);
|
|
336
|
+
entry.eventSocket = null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
entry.pendingRevision = 0;
|
|
340
|
+
entry.subscribedRevision = 0;
|
|
341
|
+
entry.reconnectPromise = null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = SubscriptionRegistry;
|