@aws-amplify/pubsub 4.4.9 → 4.4.10-next.20

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 (102) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/aws-amplify-pubsub.js +1260 -2214
  3. package/dist/aws-amplify-pubsub.js.map +1 -1
  4. package/dist/aws-amplify-pubsub.min.js +3 -3
  5. package/dist/aws-amplify-pubsub.min.js.map +1 -1
  6. package/lib/Providers/AWSAppSyncProvider.js +20 -102
  7. package/lib/Providers/AWSAppSyncProvider.js.map +1 -1
  8. package/lib/Providers/AWSAppSyncRealTimeProvider/index.d.ts +10 -2
  9. package/lib/Providers/AWSAppSyncRealTimeProvider/index.js +166 -208
  10. package/lib/Providers/AWSAppSyncRealTimeProvider/index.js.map +1 -1
  11. package/lib/Providers/AWSIotProvider.js +4 -52
  12. package/lib/Providers/AWSIotProvider.js.map +1 -1
  13. package/lib/Providers/MqttOverWSProvider.d.ts +5 -1
  14. package/lib/Providers/MqttOverWSProvider.js +172 -187
  15. package/lib/Providers/MqttOverWSProvider.js.map +1 -1
  16. package/lib/Providers/PubSubProvider.d.ts +1 -1
  17. package/lib/Providers/PubSubProvider.js +3 -13
  18. package/lib/Providers/PubSubProvider.js.map +1 -1
  19. package/lib/Providers/{AWSAppSyncRealTimeProvider/constants.d.ts → constants.d.ts} +9 -0
  20. package/lib/Providers/{AWSAppSyncRealTimeProvider/constants.js → constants.js} +9 -0
  21. package/lib/Providers/constants.js.map +1 -0
  22. package/lib/Providers/index.js +6 -8
  23. package/lib/Providers/index.js.map +1 -1
  24. package/lib/PubSub.js +6 -44
  25. package/lib/PubSub.js.map +1 -1
  26. package/lib/index.d.ts +3 -10
  27. package/lib/index.js +5 -14
  28. package/lib/index.js.map +1 -1
  29. package/lib/types/Provider.d.ts +10 -1
  30. package/lib/types/PubSub.d.ts +22 -8
  31. package/lib/types/PubSub.js +56 -0
  32. package/lib/types/PubSub.js.map +1 -1
  33. package/lib/types/index.d.ts +0 -17
  34. package/lib/types/index.js +2 -36
  35. package/lib/types/index.js.map +1 -1
  36. package/lib/utils/ConnectionStateMonitor.d.ts +2 -1
  37. package/lib/utils/ConnectionStateMonitor.js +25 -30
  38. package/lib/utils/ConnectionStateMonitor.js.map +1 -1
  39. package/lib/utils/ReachabilityMonitor/index.d.ts +3 -0
  40. package/lib/utils/ReachabilityMonitor/index.js +5 -0
  41. package/lib/utils/ReachabilityMonitor/index.js.map +1 -0
  42. package/lib/utils/ReachabilityMonitor/index.native.d.ts +3 -0
  43. package/lib/utils/ReachabilityMonitor/index.native.js +9 -0
  44. package/lib/utils/ReachabilityMonitor/index.native.js.map +1 -0
  45. package/lib/utils/ReconnectionMonitor.d.ts +29 -0
  46. package/lib/utils/ReconnectionMonitor.js +73 -0
  47. package/lib/utils/ReconnectionMonitor.js.map +1 -0
  48. package/lib-esm/Providers/AWSAppSyncProvider.js +1 -80
  49. package/lib-esm/Providers/AWSAppSyncProvider.js.map +1 -1
  50. package/lib-esm/Providers/AWSAppSyncRealTimeProvider/index.d.ts +10 -2
  51. package/lib-esm/Providers/AWSAppSyncRealTimeProvider/index.js +130 -162
  52. package/lib-esm/Providers/AWSAppSyncRealTimeProvider/index.js.map +1 -1
  53. package/lib-esm/Providers/AWSIotProvider.js +1 -49
  54. package/lib-esm/Providers/AWSIotProvider.js.map +1 -1
  55. package/lib-esm/Providers/MqttOverWSProvider.d.ts +5 -1
  56. package/lib-esm/Providers/MqttOverWSProvider.js +154 -159
  57. package/lib-esm/Providers/MqttOverWSProvider.js.map +1 -1
  58. package/lib-esm/Providers/PubSubProvider.d.ts +1 -1
  59. package/lib-esm/Providers/PubSubProvider.js +1 -11
  60. package/lib-esm/Providers/PubSubProvider.js.map +1 -1
  61. package/lib-esm/Providers/{AWSAppSyncRealTimeProvider/constants.d.ts → constants.d.ts} +9 -0
  62. package/lib-esm/Providers/{AWSAppSyncRealTimeProvider/constants.js → constants.js} +9 -0
  63. package/lib-esm/Providers/constants.js.map +1 -0
  64. package/lib-esm/PubSub.js +1 -36
  65. package/lib-esm/PubSub.js.map +1 -1
  66. package/lib-esm/index.d.ts +3 -10
  67. package/lib-esm/index.js +3 -11
  68. package/lib-esm/index.js.map +1 -1
  69. package/lib-esm/types/Provider.d.ts +10 -1
  70. package/lib-esm/types/PubSub.d.ts +22 -8
  71. package/lib-esm/types/PubSub.js +56 -0
  72. package/lib-esm/types/PubSub.js.map +1 -1
  73. package/lib-esm/types/index.d.ts +0 -17
  74. package/lib-esm/types/index.js +1 -36
  75. package/lib-esm/types/index.js.map +1 -1
  76. package/lib-esm/utils/ConnectionStateMonitor.d.ts +2 -1
  77. package/lib-esm/utils/ConnectionStateMonitor.js +14 -16
  78. package/lib-esm/utils/ConnectionStateMonitor.js.map +1 -1
  79. package/lib-esm/utils/ReachabilityMonitor/index.d.ts +3 -0
  80. package/lib-esm/utils/ReachabilityMonitor/index.js +3 -0
  81. package/lib-esm/utils/ReachabilityMonitor/index.js.map +1 -0
  82. package/lib-esm/utils/ReachabilityMonitor/index.native.d.ts +3 -0
  83. package/lib-esm/utils/ReachabilityMonitor/index.native.js +6 -0
  84. package/lib-esm/utils/ReachabilityMonitor/index.native.js.map +1 -0
  85. package/lib-esm/utils/ReconnectionMonitor.d.ts +29 -0
  86. package/lib-esm/utils/ReconnectionMonitor.js +71 -0
  87. package/lib-esm/utils/ReconnectionMonitor.js.map +1 -0
  88. package/package.json +6 -5
  89. package/src/Providers/AWSAppSyncRealTimeProvider/index.ts +154 -90
  90. package/src/Providers/MqttOverWSProvider.ts +120 -53
  91. package/src/Providers/PubSubProvider.ts +1 -1
  92. package/src/Providers/{AWSAppSyncRealTimeProvider/constants.ts → constants.ts} +12 -0
  93. package/src/index.ts +3 -10
  94. package/src/types/Provider.ts +13 -1
  95. package/src/types/PubSub.ts +47 -8
  96. package/src/types/index.ts +0 -43
  97. package/src/utils/ConnectionStateMonitor.ts +20 -5
  98. package/src/utils/ReachabilityMonitor/index.native.ts +5 -0
  99. package/src/utils/ReachabilityMonitor/index.ts +3 -0
  100. package/src/utils/ReconnectionMonitor.ts +74 -0
  101. package/lib/Providers/AWSAppSyncRealTimeProvider/constants.js.map +0 -1
  102. package/lib-esm/Providers/AWSAppSyncRealTimeProvider/constants.js.map +0 -1
@@ -15,7 +15,7 @@ import { GraphQLError } from 'graphql';
15
15
  import * as url from 'url';
16
16
  import { v4 as uuid } from 'uuid';
17
17
  import { Buffer } from 'buffer';
18
- import { ProviderOptions } from '../../types';
18
+ import { ProviderOptions } from '../../types/Provider';
19
19
  import {
20
20
  Logger,
21
21
  Credentials,
@@ -26,11 +26,12 @@ import {
26
26
  jitteredExponentialRetry,
27
27
  NonRetryableError,
28
28
  ICredentials,
29
+ isNonRetryableError,
29
30
  } from '@aws-amplify/core';
30
31
  import Cache from '@aws-amplify/cache';
31
32
  import Auth, { GRAPHQL_AUTH_MODE } from '@aws-amplify/auth';
32
33
  import { AbstractPubSubProvider } from '../PubSubProvider';
33
- import { CONNECTION_STATE_CHANGE, CONTROL_MSG } from '../../index';
34
+ import { CONTROL_MSG, ConnectionState } from '../../types/PubSub';
34
35
 
35
36
  import {
36
37
  AMPLIFY_SYMBOL,
@@ -44,11 +45,16 @@ import {
44
45
  SOCKET_STATUS,
45
46
  START_ACK_TIMEOUT,
46
47
  SUBSCRIPTION_STATUS,
47
- } from './constants';
48
+ CONNECTION_STATE_CHANGE,
49
+ } from '../constants';
48
50
  import {
49
51
  ConnectionStateMonitor,
50
52
  CONNECTION_CHANGE,
51
53
  } from '../../utils/ConnectionStateMonitor';
54
+ import {
55
+ ReconnectEvent,
56
+ ReconnectionMonitor,
57
+ } from '../../utils/ReconnectionMonitor';
52
58
 
53
59
  const logger = new Logger('AWSAppSyncRealTimeProvider');
54
60
 
@@ -98,23 +104,63 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider {
98
104
  private keepAliveAlertTimeoutId?: ReturnType<typeof setTimeout>;
99
105
  private subscriptionObserverMap: Map<string, ObserverQuery> = new Map();
100
106
  private promiseArray: Array<{ res: Function; rej: Function }> = [];
107
+ private connectionState: ConnectionState;
101
108
  private readonly connectionStateMonitor = new ConnectionStateMonitor();
109
+ private readonly reconnectionMonitor = new ReconnectionMonitor();
110
+ private connectionStateMonitorSubscription: ZenObservable.Subscription;
102
111
 
103
112
  constructor(options: ProviderOptions = {}) {
104
113
  super(options);
105
114
  // Monitor the connection state and pass changes along to Hub
106
- this.connectionStateMonitor.connectionStateObservable.subscribe(
107
- ConnectionState => {
108
- dispatchApiEvent(
109
- CONNECTION_STATE_CHANGE,
110
- {
111
- provider: this,
112
- connectionState: ConnectionState,
113
- },
114
- `Connection state is ${ConnectionState}`
115
- );
116
- }
117
- );
115
+ this.connectionStateMonitorSubscription =
116
+ this.connectionStateMonitor.connectionStateObservable.subscribe(
117
+ connectionState => {
118
+ dispatchApiEvent(
119
+ CONNECTION_STATE_CHANGE,
120
+ {
121
+ provider: this,
122
+ connectionState,
123
+ },
124
+ `Connection state is ${connectionState}`
125
+ );
126
+ this.connectionState = connectionState;
127
+
128
+ // Trigger START_RECONNECT when the connection is disrupted
129
+ if (connectionState === ConnectionState.ConnectionDisrupted) {
130
+ this.reconnectionMonitor.record(ReconnectEvent.START_RECONNECT);
131
+ }
132
+
133
+ // Trigger HALT_RECONNECT to halt reconnection attempts when the state is anything other than
134
+ // ConnectionDisrupted or Connecting
135
+ if (
136
+ [
137
+ ConnectionState.Connected,
138
+ ConnectionState.ConnectedPendingDisconnect,
139
+ ConnectionState.ConnectedPendingKeepAlive,
140
+ ConnectionState.ConnectedPendingNetwork,
141
+ ConnectionState.ConnectedPendingNetwork,
142
+ ConnectionState.ConnectionDisruptedPendingNetwork,
143
+ ConnectionState.Disconnected,
144
+ ].includes(connectionState)
145
+ ) {
146
+ this.reconnectionMonitor.record(ReconnectEvent.HALT_RECONNECT);
147
+ }
148
+ }
149
+ );
150
+ }
151
+
152
+ /**
153
+ * Mark the socket closed and release all active listeners
154
+ */
155
+ close() {
156
+ // Mark the socket closed both in status and the connection monitor
157
+ this.socketStatus = SOCKET_STATUS.CLOSED;
158
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.CONNECTION_FAILED);
159
+
160
+ // Turn off the subscription monitor Hub publishing
161
+ this.connectionStateMonitorSubscription.unsubscribe();
162
+ // Complete all reconnect observers
163
+ this.reconnectionMonitor.close();
118
164
  }
119
165
 
120
166
  getNewWebSocket(url, protocol) {
@@ -157,26 +203,44 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider {
157
203
  });
158
204
  observer.complete();
159
205
  } else {
206
+ let subscriptionStartActive = false;
160
207
  const subscriptionId = uuid();
161
- this._startSubscriptionWithAWSAppSyncRealTime({
162
- options,
163
- observer,
164
- subscriptionId,
165
- }).catch<any>(err => {
166
- observer.error({
167
- errors: [
168
- {
169
- ...new GraphQLError(
208
+ const startSubscription = () => {
209
+ if (!subscriptionStartActive) {
210
+ subscriptionStartActive = true;
211
+ const startSubscriptionPromise =
212
+ this._startSubscriptionWithAWSAppSyncRealTime({
213
+ options,
214
+ observer,
215
+ subscriptionId,
216
+ }).catch<any>(err => {
217
+ logger.debug(
170
218
  `${CONTROL_MSG.REALTIME_SUBSCRIPTION_INIT_ERROR}: ${err}`
171
- ),
172
- },
173
- ],
174
- });
175
- this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
176
- observer.complete();
219
+ );
220
+
221
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
222
+ });
223
+ startSubscriptionPromise.finally(() => {
224
+ subscriptionStartActive = false;
225
+ });
226
+ }
227
+ };
228
+
229
+ let reconnectSubscription: ZenObservable.Subscription;
230
+
231
+ // Add an observable to the reconnection list to manage reconnection for this subscription
232
+ reconnectSubscription = new Observable(observer => {
233
+ this.reconnectionMonitor.addObserver(observer);
234
+ }).subscribe(() => {
235
+ startSubscription();
177
236
  });
178
237
 
238
+ startSubscription();
239
+
179
240
  return async () => {
241
+ // Cleanup reconnection subscription
242
+ reconnectSubscription?.unsubscribe();
243
+
180
244
  // Cleanup after unsubscribing or observer.complete was called after _startSubscriptionWithAWSAppSyncRealTime
181
245
  try {
182
246
  // Waiting that subscription has been connected before trying to unsubscribe
@@ -287,24 +351,7 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider {
287
351
  additionalHeaders,
288
352
  });
289
353
  } catch (err) {
290
- logger.debug({ err });
291
- const message = err['message'] ?? '';
292
- this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
293
- observer.error({
294
- errors: [
295
- {
296
- ...new GraphQLError(`${CONTROL_MSG.CONNECTION_FAILED}: ${message}`),
297
- },
298
- ],
299
- });
300
- observer.complete();
301
- const { subscriptionFailedCallback } =
302
- this.subscriptionObserverMap.get(subscriptionId) || {};
303
-
304
- // Notify concurrent unsubscription
305
- if (typeof subscriptionFailedCallback === 'function') {
306
- subscriptionFailedCallback();
307
- }
354
+ this._logStartSubscriptionError(subscriptionId, observer, err);
308
355
  return;
309
356
  }
310
357
 
@@ -332,6 +379,44 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider {
332
379
  }
333
380
  }
334
381
 
382
+ // Log logic for start subscription failures
383
+ private _logStartSubscriptionError(subscriptionId, observer, err) {
384
+ logger.debug({ err });
385
+ const message = err['message'] ?? '';
386
+ // Resolving to give the state observer time to propogate the update
387
+ Promise.resolve(
388
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED)
389
+ );
390
+
391
+ // Capture the error only when the network didn't cause disruption
392
+ if (
393
+ this.connectionState !== ConnectionState.ConnectionDisruptedPendingNetwork
394
+ ) {
395
+ // When the error is non-retriable, error out the observable
396
+ if (isNonRetryableError(err)) {
397
+ observer.error({
398
+ errors: [
399
+ {
400
+ ...new GraphQLError(
401
+ `${CONTROL_MSG.CONNECTION_FAILED}: ${message}`
402
+ ),
403
+ },
404
+ ],
405
+ });
406
+ } else {
407
+ logger.debug(`${CONTROL_MSG.CONNECTION_FAILED}: ${message}`);
408
+ }
409
+
410
+ const { subscriptionFailedCallback } =
411
+ this.subscriptionObserverMap.get(subscriptionId) || {};
412
+
413
+ // Notify concurrent unsubscription
414
+ if (typeof subscriptionFailedCallback === 'function') {
415
+ subscriptionFailedCallback();
416
+ }
417
+ }
418
+ }
419
+
335
420
  // Waiting that subscription has been connected before trying to unsubscribe
336
421
  private async _waitForSubscriptionToBeConnected(subscriptionId: string) {
337
422
  const subscriptionObserver =
@@ -504,6 +589,10 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider {
504
589
  subscriptionState,
505
590
  });
506
591
 
592
+ logger.debug(
593
+ `${CONTROL_MSG.CONNECTION_FAILED}: ${JSON.stringify(payload)}`
594
+ );
595
+
507
596
  observer.error({
508
597
  errors: [
509
598
  {
@@ -513,9 +602,9 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider {
513
602
  },
514
603
  ],
515
604
  });
605
+
516
606
  if (startAckTimeoutId) clearTimeout(startAckTimeoutId);
517
607
 
518
- observer.complete();
519
608
  if (typeof subscriptionFailedCallback === 'function') {
520
609
  subscriptionFailedCallback();
521
610
  }
@@ -525,14 +614,7 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider {
525
614
 
526
615
  private _errorDisconnect(msg: string) {
527
616
  logger.debug(`Disconnect error: ${msg}`);
528
- this.subscriptionObserverMap.forEach(({ observer }) => {
529
- if (observer && !observer.closed) {
530
- observer.error({
531
- errors: [{ ...new GraphQLError(msg) }],
532
- });
533
- }
534
- });
535
- this.subscriptionObserverMap.clear();
617
+
536
618
  if (this.awsRealTimeSocket) {
537
619
  this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
538
620
  this.awsRealTimeSocket.close();
@@ -556,22 +638,7 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider {
556
638
  subscriptionState: SUBSCRIPTION_STATUS.FAILED,
557
639
  });
558
640
 
559
- if (observer && !observer.closed) {
560
- observer.error({
561
- errors: [
562
- {
563
- ...new GraphQLError(
564
- `Subscription timeout ${JSON.stringify({
565
- query,
566
- variables,
567
- })}`
568
- ),
569
- },
570
- ],
571
- });
572
- // Cleanup will be automatically executed
573
- observer.complete();
574
- }
641
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
575
642
  logger.debug(
576
643
  'timeoutStartSubscription',
577
644
  JSON.stringify({ query, variables })
@@ -640,6 +707,7 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider {
640
707
  this.socketStatus = SOCKET_STATUS.READY;
641
708
  this.promiseArray = [];
642
709
  } catch (err) {
710
+ logger.debug('Connection exited with', err);
643
711
  this.promiseArray.forEach(({ rej }) => rej(err));
644
712
  this.promiseArray = [];
645
713
  if (
@@ -676,9 +744,6 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider {
676
744
  logger.debug(`WebSocket connection error`);
677
745
  };
678
746
  newSocket.onclose = () => {
679
- this.connectionStateMonitor.record(
680
- CONNECTION_CHANGE.CONNECTION_FAILED
681
- );
682
747
  rej(new Error('Connection handshake error'));
683
748
  };
684
749
  newSocket.onopen = () => {
@@ -687,7 +752,6 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider {
687
752
  };
688
753
  });
689
754
  })();
690
-
691
755
  // Step 2: wait for ack from AWS AppSyncReaTime after sending init
692
756
  await (() => {
693
757
  return new Promise((res, rej) => {
@@ -747,20 +811,20 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider {
747
811
  };
748
812
  this.awsRealTimeSocket.send(JSON.stringify(gqlInit));
749
813
 
750
- setTimeout(checkAckOk.bind(this, ackOk), CONNECTION_INIT_TIMEOUT);
751
- }
814
+ const checkAckOk = (ackOk: boolean) => {
815
+ if (!ackOk) {
816
+ this.connectionStateMonitor.record(
817
+ CONNECTION_CHANGE.CONNECTION_FAILED
818
+ );
819
+ rej(
820
+ new Error(
821
+ `Connection timeout: ack from AWSAppSyncRealTime was not received after ${CONNECTION_INIT_TIMEOUT} ms`
822
+ )
823
+ );
824
+ }
825
+ };
752
826
 
753
- function checkAckOk(ackOk: boolean) {
754
- if (!ackOk) {
755
- this.connectionStateMonitor.record(
756
- CONNECTION_CHANGE.CONNECTION_FAILED
757
- );
758
- rej(
759
- new Error(
760
- `Connection timeout: ack from AWSRealTime was not received on ${CONNECTION_INIT_TIMEOUT} ms`
761
- )
762
- );
763
- }
827
+ setTimeout(() => checkAckOk(ackOk), CONNECTION_INIT_TIMEOUT);
764
828
  }
765
829
  });
766
830
  })();
@@ -12,11 +12,21 @@
12
12
  */
13
13
  import * as Paho from 'paho-mqtt';
14
14
  import { v4 as uuid } from 'uuid';
15
- import Observable from 'zen-observable-ts';
15
+ import Observable, { ZenObservable } from 'zen-observable-ts';
16
16
 
17
17
  import { AbstractPubSubProvider } from './PubSubProvider';
18
- import { ProviderOptions, SubscriptionObserver } from '../types';
19
- import { ConsoleLogger as Logger } from '@aws-amplify/core';
18
+ import { SubscriptionObserver, ConnectionState } from '../types/PubSub';
19
+ import { ProviderOptions } from '../types/Provider';
20
+ import { ConsoleLogger as Logger, Hub } from '@aws-amplify/core';
21
+ import {
22
+ ConnectionStateMonitor,
23
+ CONNECTION_CHANGE,
24
+ } from '../utils/ConnectionStateMonitor';
25
+ import {
26
+ ReconnectEvent,
27
+ ReconnectionMonitor,
28
+ } from '../utils/ReconnectionMonitor';
29
+ import { AMPLIFY_SYMBOL, CONNECTION_STATE_CHANGE } from './constants';
20
30
 
21
31
  const logger = new Logger('MqttOverWSProvider');
22
32
 
@@ -49,17 +59,15 @@ class ClientsQueue {
49
59
 
50
60
  async get(clientId: string, clientFactory?: (input: string) => Promise<any>) {
51
61
  const cachedPromise = this.promises.get(clientId);
52
- if (cachedPromise) {
53
- return cachedPromise;
54
- }
62
+ if (cachedPromise) return cachedPromise;
55
63
 
56
64
  if (clientFactory) {
57
65
  const newPromise = clientFactory(clientId);
58
-
59
66
  this.promises.set(clientId, newPromise);
60
-
67
+ newPromise.catch(v => this.promises.delete(clientId));
61
68
  return newPromise;
62
69
  }
70
+
63
71
  return undefined;
64
72
  }
65
73
 
@@ -72,13 +80,44 @@ class ClientsQueue {
72
80
  }
73
81
  }
74
82
 
83
+ const dispatchPubSubEvent = (event: string, data: any, message: string) => {
84
+ Hub.dispatch('pubsub', { event, data, message }, 'PubSub', AMPLIFY_SYMBOL);
85
+ };
86
+
75
87
  const topicSymbol = typeof Symbol !== 'undefined' ? Symbol('topic') : '@@topic';
76
88
 
77
89
  export class MqttOverWSProvider extends AbstractPubSubProvider {
78
90
  private _clientsQueue = new ClientsQueue();
91
+ private connectionState: ConnectionState;
92
+ private readonly connectionStateMonitor = new ConnectionStateMonitor();
93
+ private readonly reconnectionMonitor = new ReconnectionMonitor();
79
94
 
80
95
  constructor(options: MqttProviderOptions = {}) {
81
96
  super({ ...options, clientId: options.clientId || uuid() });
97
+
98
+ // Monitor the connection health state and pass changes along to Hub
99
+ this.connectionStateMonitor.connectionStateObservable.subscribe(
100
+ connectionStateChange => {
101
+ dispatchPubSubEvent(
102
+ CONNECTION_STATE_CHANGE,
103
+ {
104
+ provider: this,
105
+ connectionState: connectionStateChange,
106
+ },
107
+ `Connection state is ${connectionStateChange}`
108
+ );
109
+
110
+ this.connectionState = connectionStateChange;
111
+
112
+ // Trigger reconnection when the connection is disrupted
113
+ if (connectionStateChange === ConnectionState.ConnectionDisrupted) {
114
+ this.reconnectionMonitor.record(ReconnectEvent.START_RECONNECT);
115
+ } else if (connectionStateChange !== ConnectionState.Connecting) {
116
+ // Trigger connected to halt reconnection attempts
117
+ this.reconnectionMonitor.record(ReconnectEvent.HALT_RECONNECT);
118
+ }
119
+ }
120
+ );
82
121
  }
83
122
 
84
123
  protected get clientId() {
@@ -117,7 +156,6 @@ export class MqttOverWSProvider extends AbstractPubSubProvider {
117
156
  if (errorCode !== 0) {
118
157
  logger.warn(clientId, JSON.stringify({ errorCode, ...args }, null, 2));
119
158
 
120
- const topicsToDelete: string[] = [];
121
159
  if (!clientId) {
122
160
  return;
123
161
  }
@@ -125,33 +163,17 @@ export class MqttOverWSProvider extends AbstractPubSubProvider {
125
163
  if (!clientIdObservers) {
126
164
  return;
127
165
  }
128
- clientIdObservers.forEach(observer => {
129
- observer.error('Disconnected, error code: ' + errorCode);
130
- // removing observers for disconnected clientId
131
- this._topicObservers.forEach((observerForTopic, observerTopic) => {
132
- observerForTopic.delete(observer);
133
- if (observerForTopic.size === 0) {
134
- topicsToDelete.push(observerTopic);
135
- }
136
- });
137
- });
138
-
139
- // forgiving any trace of clientId
140
- this._clientIdObservers.delete(clientId);
141
-
142
- // Removing topics that are not listen by an observer
143
- topicsToDelete.forEach(topic => {
144
- this._topicObservers.delete(topic);
145
- });
166
+ this.disconnect(clientId);
146
167
  }
147
168
  }
148
169
 
149
170
  public async newClient({ url, clientId }: MqttProviderOptions): Promise<any> {
150
171
  logger.debug('Creating new MQTT client', clientId);
151
172
 
173
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.OPENING_CONNECTION);
152
174
  // @ts-ignore
153
175
  const client = new Paho.Client(url, clientId);
154
- // client.trace = (args) => logger.debug(clientId, JSON.stringify(args, null, 2));
176
+
155
177
  client.onMessageArrived = ({
156
178
  destinationName: topic,
157
179
  payloadString: msg,
@@ -168,17 +190,28 @@ export class MqttOverWSProvider extends AbstractPubSubProvider {
168
190
  errorCode: number;
169
191
  }) => {
170
192
  this.onDisconnect({ clientId, errorCode, ...args });
193
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
171
194
  };
172
195
 
173
- await new Promise((resolve, reject) => {
196
+ const connected = await new Promise((resolve, reject) => {
174
197
  client.connect({
175
198
  useSSL: this.isSSLEnabled,
176
199
  mqttVersion: 3,
177
- onSuccess: () => resolve(client),
178
- onFailure: reject,
200
+ onSuccess: () => resolve(true),
201
+ onFailure: x => {
202
+ if (clientId) this._clientsQueue.remove(clientId);
203
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
204
+ resolve(false);
205
+ },
179
206
  });
180
207
  });
181
208
 
209
+ if (connected) {
210
+ this.connectionStateMonitor.record(
211
+ CONNECTION_CHANGE.CONNECTION_ESTABLISHED
212
+ );
213
+ }
214
+
182
215
  return client;
183
216
  }
184
217
 
@@ -186,9 +219,19 @@ export class MqttOverWSProvider extends AbstractPubSubProvider {
186
219
  clientId: string,
187
220
  options: MqttProviderOptions = {}
188
221
  ): Promise<any> {
189
- return await this.clientsQueue.get(clientId, clientId =>
190
- this.newClient({ ...options, clientId })
191
- );
222
+ return await this.clientsQueue.get(clientId, async clientId => {
223
+ const client = await this.newClient({ ...options, clientId });
224
+
225
+ if (client) {
226
+ // Once connected, subscribe to all topics registered observers
227
+ this._topicObservers.forEach(
228
+ (_value: Set<SubscriptionObserver<any>>, key: string) => {
229
+ client.subscribe(key);
230
+ }
231
+ );
232
+ }
233
+ return client;
234
+ });
192
235
  }
193
236
 
194
237
  protected async disconnect(clientId: string): Promise<void> {
@@ -198,18 +241,25 @@ export class MqttOverWSProvider extends AbstractPubSubProvider {
198
241
  client.disconnect();
199
242
  }
200
243
  this.clientsQueue.remove(clientId);
244
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
201
245
  }
202
246
 
203
247
  async publish(topics: string[] | string, msg: any) {
204
248
  const targetTopics = ([] as string[]).concat(topics);
205
249
  const message = JSON.stringify(msg);
206
250
 
207
- const url = await this.endpoint;
208
-
209
- const client = await this.connect(this.clientId, { url });
210
-
211
- logger.debug('Publishing to topic(s)', targetTopics.join(','), message);
212
- targetTopics.forEach(topic => client.send(topic, message));
251
+ const client = await this.clientsQueue.get(this.clientId);
252
+
253
+ if (client) {
254
+ logger.debug('Publishing to topic(s)', targetTopics.join(','), message);
255
+ targetTopics.forEach(topic => client.send(topic, message));
256
+ } else {
257
+ logger.debug(
258
+ 'Publishing to topic(s) failed',
259
+ targetTopics.join(','),
260
+ message
261
+ );
262
+ }
213
263
  }
214
264
 
215
265
  protected _topicObservers: Map<string, Set<SubscriptionObserver<any>>> =
@@ -246,6 +296,7 @@ export class MqttOverWSProvider extends AbstractPubSubProvider {
246
296
  ): Observable<any> {
247
297
  const targetTopics = ([] as string[]).concat(topics);
248
298
  logger.debug('Subscribing to topic(s)', targetTopics.join(','));
299
+ let reconnectSubscription: ZenObservable.Subscription;
249
300
 
250
301
  return new Observable(observer => {
251
302
  targetTopics.forEach(topic => {
@@ -261,8 +312,6 @@ export class MqttOverWSProvider extends AbstractPubSubProvider {
261
312
  observersForTopic.add(observer);
262
313
  });
263
314
 
264
- // @ts-ignore
265
- let client: Paho.Client;
266
315
  const { clientId = this.clientId } = options;
267
316
 
268
317
  // this._clientIdObservers is used to close observers when client gets disconnected
@@ -274,26 +323,44 @@ export class MqttOverWSProvider extends AbstractPubSubProvider {
274
323
  this._clientIdObservers.set(clientId, observersForClientId);
275
324
 
276
325
  (async () => {
277
- const { url = await this.endpoint } = options;
326
+ const getClient = async () => {
327
+ try {
328
+ const { url = await this.endpoint } = options;
329
+ const client = await this.connect(clientId, { url });
330
+ if (client !== undefined) {
331
+ targetTopics.forEach(topic => {
332
+ client.subscribe(topic);
333
+ });
334
+ }
335
+ } catch (e) {
336
+ logger.debug('Error forming connection', e);
337
+ }
338
+ };
278
339
 
279
- try {
280
- client = await this.connect(clientId, { url });
281
- targetTopics.forEach(topic => {
282
- client.subscribe(topic);
283
- });
284
- } catch (e) {
285
- observer.error(e);
286
- }
340
+ // Establish the initial connection
341
+ await getClient();
342
+
343
+ // Add an observable to the reconnection list to manage reconnection for this subscription
344
+ reconnectSubscription = new Observable(observer => {
345
+ this.reconnectionMonitor.addObserver(observer);
346
+ }).subscribe(() => {
347
+ getClient();
348
+ });
287
349
  })();
288
350
 
289
- return () => {
290
- logger.debug('Unsubscribing from topic(s)', targetTopics.join(','));
351
+ return async () => {
352
+ const client = await this.clientsQueue.get(clientId);
353
+
354
+ reconnectSubscription?.unsubscribe();
291
355
 
292
356
  if (client) {
293
357
  this._clientIdObservers.get(clientId)?.delete(observer);
294
358
  // No more observers per client => client not needed anymore
295
359
  if (this._clientIdObservers.get(clientId)?.size === 0) {
296
360
  this.disconnect(clientId);
361
+ this.connectionStateMonitor.record(
362
+ CONNECTION_CHANGE.CLOSING_CONNECTION
363
+ );
297
364
  this._clientIdObservers.delete(clientId);
298
365
  }
299
366
 
@@ -11,7 +11,7 @@
11
11
  * and limitations under the License.
12
12
  */
13
13
  import Observable from 'zen-observable-ts';
14
- import { PubSubProvider, ProviderOptions } from '../types';
14
+ import { PubSubProvider, ProviderOptions } from '../types/Provider';
15
15
  import { ConsoleLogger as Logger } from '@aws-amplify/core';
16
16
 
17
17
  const logger = new Logger('AbstractPubSubProvider');
@@ -2,6 +2,8 @@ export const MAX_DELAY_MS = 5000;
2
2
 
3
3
  export const NON_RETRYABLE_CODES = [400, 401, 403];
4
4
 
5
+ export const CONNECTION_STATE_CHANGE = 'ConnectionStateChange';
6
+
5
7
  export enum MESSAGE_TYPES {
6
8
  /**
7
9
  * Client -> Server message.
@@ -98,3 +100,13 @@ export const DEFAULT_KEEP_ALIVE_TIMEOUT = 5 * 60 * 1000;
98
100
  * Default Time in milleseconds to alert for missed GQL_CONNECTION_KEEP_ALIVE message
99
101
  */
100
102
  export const DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT = 65 * 1000;
103
+
104
+ /**
105
+ * Default delay time in milleseconds between when reconnect is triggered vs when it is attempted
106
+ */
107
+ export const RECONNECT_DELAY = 5 * 1000;
108
+
109
+ /**
110
+ * Default interval time in milleseconds between when reconnect is re-attempted
111
+ */
112
+ export const RECONNECT_INTERVAL = 60 * 1000;