homey-api 3.17.4 → 3.17.5

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.
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const EventEmitter = require('../../EventEmitter');
4
+ const RealtimeConsumer = require('./RealtimeConsumer');
4
5
 
5
6
  /**
6
7
  * A superclass for all CRUD Items.
@@ -61,6 +62,37 @@ class Item extends EventEmitter {
61
62
  writable: true,
62
63
  });
63
64
 
65
+ this.__realtimeConsumer = new RealtimeConsumer({
66
+ subscribe: (uri, handlers) => this.homey.subscribe(uri, handlers),
67
+ getUri: () => this.uri,
68
+ debug: (...props) => this.__debug(...props),
69
+ setConnected: (connected) => {
70
+ this.__connected = connected;
71
+ },
72
+ onConnect: () => {
73
+ this.onConnect();
74
+ },
75
+ onDisconnect: () => {
76
+ this.onDisconnect();
77
+ },
78
+ onReconnect: () => {
79
+ this.onReconnect();
80
+ },
81
+ onEvent: (event, data) => {
82
+ if (event === 'update') {
83
+ this.__update(this.constructor.transformGet({ ...data }));
84
+ return;
85
+ }
86
+
87
+ if (event === 'delete') {
88
+ this.__delete();
89
+ return;
90
+ }
91
+
92
+ this.emit(event, data);
93
+ },
94
+ });
95
+
64
96
  // Set Properties
65
97
  for (const [key, value] of Object.entries(properties)) {
66
98
  if (key === 'id') continue;
@@ -93,7 +125,7 @@ class Item extends EventEmitter {
93
125
  this.manager.__debug(`[${this.constructor.name}:${this.id}]`, ...props);
94
126
  }
95
127
 
96
- __update(properties) {
128
+ __update(properties, { emitEvent = true } = {}) {
97
129
  for (const [key, value] of Object.entries(properties)) {
98
130
  if (key === 'id') continue;
99
131
  this[key] = value;
@@ -101,7 +133,9 @@ class Item extends EventEmitter {
101
133
 
102
134
  this.__lastUpdated = new Date();
103
135
 
104
- this.emit('update', properties);
136
+ if (emitEvent) {
137
+ this.emit('update', properties);
138
+ }
105
139
  }
106
140
 
107
141
  __delete() {
@@ -113,91 +147,14 @@ class Item extends EventEmitter {
113
147
  * Connect to this item's Socket.io namespace.
114
148
  */
115
149
  async connect() {
116
- this.__debug('connect');
117
-
118
- // If disconnecting, await that first
119
- try {
120
- // Ensure all microtasks are done first. E.g. if disconnect is called in the same tick as
121
- // connect. This way the disconnect is always started first so we can await the disconnect
122
- // promise before we try to connect again.
123
- await Promise.resolve();
124
- await this.__disconnectPromise;
125
- // eslint-disable-next-line no-empty
126
- } catch (err) { }
127
-
128
- this.__connectPromise = Promise.resolve().then(async () => {
129
- if (!this.io) {
130
- this.io = this.homey.subscribe(this.uri, {
131
- onConnect: () => {
132
- this.__debug('onConnect');
133
- this.__connected = true;
134
-
135
- this.onConnect();
136
- },
137
- onDisconnect: () => {
138
- this.__debug('onDisconnect');
139
- this.__connected = false;
140
-
141
- this.onDisconnect();
142
- },
143
- onReconnect: () => {
144
- this.__debug('onDisconnect');
145
-
146
- this.onReconnect();
147
- },
148
- onEvent: (event, data) => {
149
- this.__debug('onEvent', event, data);
150
-
151
- this.emit(event, data);
152
- },
153
- });
154
- }
155
-
156
- await this.io;
157
- });
158
-
159
- // Delete the connecting Promise
160
- this.__connectPromise
161
- .catch(() => { })
162
- .finally(() => {
163
- delete this.__connectPromise;
164
- });
165
-
166
- await this.__connectPromise;
150
+ await this.__realtimeConsumer.connect();
167
151
  }
168
152
 
169
153
  /**
170
154
  * Discconnect from this item's Socket.io namespace.
171
155
  */
172
156
  async disconnect() {
173
- this.__debug('disconnect');
174
-
175
- // If connecting, await that first
176
- try {
177
- await this.__connectPromise;
178
- // eslint-disable-next-line no-empty
179
- } catch (err) { }
180
-
181
- this.__disconnectPromise = Promise.resolve().then(async () => {
182
- this.__connected = false;
183
-
184
- if (this.io) {
185
- this.io
186
- .then((io) => io.unsubscribe())
187
- .catch((err) => this.__debug('Error Disconnecting:', err));
188
- }
189
- });
190
-
191
- // Delete the disconnecting Promise
192
- this.__disconnectPromise
193
- .catch(() => { })
194
- .finally(() => {
195
- delete this.__disconnectPromise;
196
- // Delete this.io so connect can start new connections.
197
- delete this.io;
198
- });
199
-
200
- await this.__disconnectPromise;
157
+ await this.__realtimeConsumer.disconnect();
201
158
  }
202
159
 
203
160
  onConnect() {
@@ -2,8 +2,8 @@
2
2
 
3
3
  const EventEmitter = require('../../EventEmitter');
4
4
  const Util = require('../../Util');
5
- const HomeyAPIError = require('../HomeyAPIError');
6
5
  const Item = require('./Item');
6
+ const RealtimeConsumer = require('./RealtimeConsumer');
7
7
 
8
8
  /**
9
9
  * @class
@@ -87,6 +87,71 @@ class Manager extends EventEmitter {
87
87
  writable: false,
88
88
  });
89
89
 
90
+ this.__realtimeConsumer = new RealtimeConsumer({
91
+ subscribe: (uri, handlers) => this.homey.subscribe(uri, handlers),
92
+ getUri: () => this.uri,
93
+ debug: (...props) => this.__debug(...props),
94
+ setConnected: (connected) => {
95
+ this.__connected = connected;
96
+ },
97
+ onEvent: (event, data) => {
98
+ // Transform & add to cache if this is a CRUD event
99
+ if (event.endsWith('.create') || event.endsWith('.update') || event.endsWith('.delete')) {
100
+ const [itemId, operation] = event.split('.');
101
+ const itemName = this.itemNames[itemId];
102
+ const ItemClass = this.itemClasses[itemName];
103
+
104
+ switch (operation) {
105
+ case 'create': {
106
+ const props = ItemClass.transformGet(data);
107
+
108
+ const item = new ItemClass({
109
+ id: props.id,
110
+ homey: this.homey,
111
+ manager: this,
112
+ properties: props,
113
+ });
114
+ this.__cache[ItemClass.ID][props.id] = item;
115
+
116
+ this.emit(event, item);
117
+ return;
118
+ }
119
+ case 'update': {
120
+ const props = ItemClass.transformGet(data);
121
+
122
+ if (this.__cache[ItemClass.ID][props.id]) {
123
+ const item = this.__cache[ItemClass.ID][props.id];
124
+ item.__update(props);
125
+ this.emit(event, item);
126
+ return;
127
+ }
128
+
129
+ break;
130
+ }
131
+ case 'delete': {
132
+ const props = ItemClass.transformGet(data);
133
+
134
+ if (this.__cache[ItemClass.ID][props.id]) {
135
+ const item = this.__cache[ItemClass.ID][props.id];
136
+ item.__delete();
137
+ delete this.__cache[ItemClass.ID][item.id];
138
+ this.emit(event, {
139
+ id: item.id,
140
+ });
141
+ return;
142
+ }
143
+
144
+ break;
145
+ }
146
+ default:
147
+ break;
148
+ }
149
+ }
150
+
151
+ this.emit(event, data);
152
+ },
153
+ });
154
+
90
155
  // Create methods
91
156
  for (const [operationId, operation] of Object.entries(operations)) {
92
157
  Object.defineProperty(
@@ -244,6 +309,7 @@ class Manager extends EventEmitter {
244
309
 
245
310
  async __request({ $cache, $updateCache, $timeout, $socket, operationId, operation, path, body, headers, shouldRetry, ...args }) {
246
311
  let result;
312
+ let hasSocketResult = false;
247
313
  const benchmark = Util.benchmark();
248
314
 
249
315
  // If connected to Socket.io,
@@ -273,47 +339,24 @@ class Manager extends EventEmitter {
273
339
  // If Homey is connected to Socket.io,
274
340
  // send the API request to socket.io.
275
341
  // This is about ~2x faster than HTTP
276
- if (this.homey.isConnected() && $socket === true) {
277
- result = await Util.timeout(
278
- new Promise((resolve, reject) => {
279
- this.__debug(`IO ${operationId}`);
280
- this.homey.__homeySocket.emit(
281
- 'api',
282
- {
283
- args,
284
- operation: operationId,
285
- uri: this.uri,
286
- },
287
- (err, result) => {
288
- if (err != null) {
289
- if (typeof err === 'object') {
290
- err = new HomeyAPIError(
291
- {
292
- stack: err.stack,
293
- error: err.error,
294
- error_description: err.error_description,
295
- },
296
- err.statusCode || err.code || 500
297
- );
298
- } else if (typeof err === 'string') {
299
- err = new HomeyAPIError(
300
- {
301
- error: err,
302
- },
303
- 500
304
- );
305
- }
306
-
307
- return reject(err);
308
- }
342
+ if ($socket === true && this.homey.isConnected()) {
343
+ try {
344
+ this.__debug(`IO ${operationId}`);
345
+ result = await this.homey.__apiRequest({
346
+ uri: this.uri,
347
+ operation: operationId,
348
+ args,
349
+ timeout: $timeout,
350
+ });
351
+ hasSocketResult = true;
352
+ } catch (err) {
353
+ if (err.code !== 'ERR_SOCKET_SESSION_NOT_READY') {
354
+ throw err;
355
+ }
356
+ }
357
+ }
309
358
 
310
- return resolve(result);
311
- }
312
- );
313
- }),
314
- $timeout
315
- );
316
- } else {
359
+ if (!hasSocketResult) {
317
360
  // Get from HTTP
318
361
  result = await this.homey.call({
319
362
  $timeout,
@@ -396,7 +439,11 @@ class Manager extends EventEmitter {
396
439
 
397
440
  if (this.isConnected() && $updateCache === true && this.__cache[ItemClass.ID][props.id]) {
398
441
  item = this.__cache[ItemClass.ID][props.id];
399
- item.__update(props);
442
+ item.__update(props, {
443
+ // Local mutation results update the cached item immediately, but the
444
+ // realtime manager event is responsible for the public update event.
445
+ emitEvent: false,
446
+ });
400
447
  } else {
401
448
  item = new ItemClass({
402
449
  id: props.id,
@@ -462,119 +509,7 @@ class Manager extends EventEmitter {
462
509
  * @returns {Promise<void>}
463
510
  */
464
511
  async connect() {
465
- this.__debug('connect');
466
-
467
- // If disconnecting, await that first
468
- try {
469
- await this.__disconnectPromise;
470
- // eslint-disable-next-line no-empty
471
- } catch (err) { }
472
-
473
- if (this.__connectPromise) {
474
- await this.__connectPromise;
475
- return;
476
- }
477
-
478
- if (this.io) {
479
- await this.io;
480
- return;
481
- }
482
-
483
- this.__connectPromise = Promise.resolve().then(async () => {
484
- if (!this.io) {
485
- this.io = this.homey.subscribe(this.uri, {
486
- onConnect: () => {
487
- this.__debug('onConnect');
488
- this.__connected = true;
489
- },
490
- onDisconnect: reason => {
491
- this.__debug(`onDisconnect Reason:${reason}`);
492
- this.__connected = false;
493
-
494
- // Disable for now. We should probably only set the cache to invalid.
495
-
496
- // Clear CRUD Item cache
497
- // for (const itemId of Object.keys(this.__cache)) {
498
- // this.__cache[itemId] = {};
499
- // this.__cacheAllComplete[itemId] = false;
500
- // }
501
- },
502
- onReconnect: () => {
503
- this.__debug(`onReconnect`);
504
- this.__connected = true;
505
- },
506
- onEvent: (event, data) => {
507
- this.__debug('onEvent', event);
508
-
509
- // Transform & add to cache if this is a CRUD event
510
- if (event.endsWith('.create') || event.endsWith('.update') || event.endsWith('.delete')) {
511
- const [itemId, operation] = event.split('.');
512
- const itemName = this.itemNames[itemId];
513
- const ItemClass = this.itemClasses[itemName];
514
-
515
- switch (operation) {
516
- case 'create': {
517
- const props = ItemClass.transformGet(data);
518
-
519
- const item = new ItemClass({
520
- id: props.id,
521
- homey: this.homey,
522
- manager: this,
523
- properties: props,
524
- });
525
- this.__cache[ItemClass.ID][props.id] = item;
526
-
527
- return this.emit(event, item);
528
- }
529
- case 'update': {
530
- const props = ItemClass.transformGet(data);
531
-
532
- if (this.__cache[ItemClass.ID][props.id]) {
533
- const item = this.__cache[ItemClass.ID][props.id];
534
- item.__update(props);
535
- return this.emit(event, item);
536
- }
537
-
538
- break;
539
- }
540
- case 'delete': {
541
- const props = ItemClass.transformGet(data);
542
-
543
- if (this.__cache[ItemClass.ID][props.id]) {
544
- const item = this.__cache[ItemClass.ID][props.id];
545
- item.__delete();
546
- delete this.__cache[ItemClass.ID][item.id];
547
- return this.emit(event, {
548
- id: item.id,
549
- });
550
- }
551
-
552
- break;
553
- }
554
- default:
555
- break;
556
- }
557
- }
558
-
559
- // Fire event listeners
560
- this.emit(event, data);
561
- },
562
- });
563
- }
564
-
565
- await this.io;
566
- });
567
-
568
- // Delete the connecting Promise
569
- this.__connectPromise
570
- .catch(() => {
571
- delete this.io;
572
- })
573
- .finally(() => {
574
- delete this.__connectPromise;
575
- });
576
-
577
- await this.__connectPromise;
512
+ await this.__realtimeConsumer.connect();
578
513
  }
579
514
 
580
515
  /**
@@ -582,32 +517,7 @@ class Manager extends EventEmitter {
582
517
  * @returns {Promise<void>}
583
518
  */
584
519
  async disconnect() {
585
- this.__debug('disconnect');
586
-
587
- // If connecting, await that first
588
- try {
589
- await this.__connectPromise;
590
- // eslint-disable-next-line no-empty
591
- } catch (err) { }
592
-
593
- this.__disconnectPromise = Promise.resolve().then(async () => {
594
- this.__connected = false;
595
-
596
- if (this.io) {
597
- await this.io.then(io => io.unsubscribe()).catch(err => this.__debug('Error Disconnecting:', err));
598
-
599
- delete this.io;
600
- }
601
- });
602
-
603
- // Delete the disconnecting Promise
604
- this.__disconnectPromise
605
- .catch(() => { })
606
- .finally(() => {
607
- delete this.__disconnectPromise;
608
- });
609
-
610
- await this.__disconnectPromise;
520
+ await this.__realtimeConsumer.disconnect();
611
521
  }
612
522
 
613
523
  /**
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ class RealtimeConsumer {
4
+ constructor({
5
+ subscribe,
6
+ getUri,
7
+ debug,
8
+ onConnect = () => {},
9
+ onDisconnect = () => {},
10
+ onReconnect = () => {},
11
+ onReconnectError = () => {},
12
+ onEvent = () => {},
13
+ setConnected = () => {},
14
+ }) {
15
+ this.__subscribe = subscribe;
16
+ this.__getUri = getUri;
17
+ this.__debug = debug;
18
+ this.__onConnect = onConnect;
19
+ this.__onDisconnect = onDisconnect;
20
+ this.__onReconnect = onReconnect;
21
+ this.__onReconnectError = onReconnectError;
22
+ this.__onEvent = onEvent;
23
+ this.__setConnected = setConnected;
24
+
25
+ this.__subscriptionPromise = null;
26
+ this.__connectPromise = null;
27
+ this.__disconnectPromise = null;
28
+ }
29
+
30
+ async connect() {
31
+ this.__debug('connect');
32
+
33
+ try {
34
+ await Promise.resolve();
35
+ await this.__disconnectPromise;
36
+ // eslint-disable-next-line no-empty
37
+ } catch (err) {}
38
+
39
+ if (this.__connectPromise) {
40
+ await this.__connectPromise;
41
+ return;
42
+ }
43
+
44
+ if (this.__subscriptionPromise) {
45
+ await this.__subscriptionPromise;
46
+ return;
47
+ }
48
+
49
+ const connectPromise = Promise.resolve().then(async () => {
50
+ if (!this.__subscriptionPromise) {
51
+ this.__subscriptionPromise = Promise.resolve(
52
+ this.__subscribe(this.__getUri(), {
53
+ onConnect: () => {
54
+ this.__debug('onConnect');
55
+ this.__setConnected(true);
56
+ this.__onConnect();
57
+ },
58
+ onDisconnect: (reason) => {
59
+ this.__debug(`onDisconnect Reason:${reason}`);
60
+ this.__setConnected(false);
61
+ this.__onDisconnect(reason);
62
+ },
63
+ onReconnect: () => {
64
+ this.__debug('onReconnect');
65
+ this.__setConnected(true);
66
+ this.__onReconnect();
67
+ },
68
+ onReconnectError: (err) => {
69
+ this.__debug('onReconnectError', err.message);
70
+ this.__onReconnectError(err);
71
+ },
72
+ onEvent: (event, data) => {
73
+ this.__debug('onEvent', event, data);
74
+ this.__onEvent(event, data);
75
+ },
76
+ })
77
+ );
78
+ }
79
+
80
+ await this.__subscriptionPromise;
81
+ });
82
+
83
+ this.__connectPromise = connectPromise;
84
+
85
+ connectPromise
86
+ .catch(() => {
87
+ this.__subscriptionPromise = null;
88
+ })
89
+ .finally(() => {
90
+ if (this.__connectPromise === connectPromise) {
91
+ this.__connectPromise = null;
92
+ }
93
+ });
94
+
95
+ await connectPromise;
96
+ }
97
+
98
+ async disconnect() {
99
+ this.__debug('disconnect');
100
+
101
+ try {
102
+ await this.__connectPromise;
103
+ // eslint-disable-next-line no-empty
104
+ } catch (err) {}
105
+
106
+ if (this.__disconnectPromise) {
107
+ await this.__disconnectPromise;
108
+ return;
109
+ }
110
+
111
+ const disconnectPromise = Promise.resolve().then(async () => {
112
+ this.__setConnected(false);
113
+
114
+ // A failed connect clears __subscriptionPromise in connect(). In that case
115
+ // disconnect() becomes a safe no-op because there is no subscription handle
116
+ // left to unsubscribe.
117
+ if (this.__subscriptionPromise) {
118
+ await this.__subscriptionPromise
119
+ .then((subscription) => subscription.unsubscribe())
120
+ .catch((err) => this.__debug('Error Disconnecting:', err));
121
+ }
122
+ });
123
+
124
+ this.__disconnectPromise = disconnectPromise;
125
+
126
+ disconnectPromise
127
+ .catch(() => {})
128
+ .finally(() => {
129
+ if (this.__disconnectPromise === disconnectPromise) {
130
+ this.__disconnectPromise = null;
131
+ }
132
+
133
+ this.__subscriptionPromise = null;
134
+ });
135
+
136
+ await disconnectPromise;
137
+ }
138
+ }
139
+
140
+ module.exports = RealtimeConsumer;