@aws-amplify/api-graphql 4.7.2-graphql-data-ka.518fb2c.0 → 4.7.2-publish-event-websocket.65d9752.0

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.
@@ -23,7 +23,7 @@ import {
23
23
  CONNECTION_INIT_TIMEOUT,
24
24
  CONNECTION_STATE_CHANGE,
25
25
  DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT,
26
- DEFAULT_KEEP_ALIVE_HEARTBEAT_TIMEOUT,
26
+ DEFAULT_KEEP_ALIVE_TIMEOUT,
27
27
  MAX_DELAY_MS,
28
28
  MESSAGE_TYPES,
29
29
  NON_RETRYABLE_CODES,
@@ -83,8 +83,9 @@ export abstract class AWSWebSocketProvider {
83
83
 
84
84
  protected awsRealTimeSocket?: WebSocket;
85
85
  private socketStatus: SOCKET_STATUS = SOCKET_STATUS.CLOSED;
86
- private kaTimestamp?: number;
87
- private keepAliveHeartbeatIntervalId?: ReturnType<typeof setTimeout>;
86
+ private keepAliveTimeoutId?: ReturnType<typeof setTimeout>;
87
+ private keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT;
88
+ private keepAliveAlertTimeoutId?: ReturnType<typeof setTimeout>;
88
89
  private promiseArray: { res(): void; rej(reason?: any): void }[] = [];
89
90
  private connectionState: ConnectionState | undefined;
90
91
  private readonly connectionStateMonitor = new ConnectionStateMonitor();
@@ -118,7 +119,6 @@ export abstract class AWSWebSocketProvider {
118
119
  return new Promise<void>((resolve, reject) => {
119
120
  if (this.awsRealTimeSocket) {
120
121
  this.awsRealTimeSocket.onclose = (_: CloseEvent) => {
121
- this._closeSocket();
122
122
  this.subscriptionObserverMap = new Map();
123
123
  this.awsRealTimeSocket = undefined;
124
124
  resolve();
@@ -171,7 +171,7 @@ export abstract class AWSWebSocketProvider {
171
171
  this.logger.debug(
172
172
  `${CONTROL_MSG.REALTIME_SUBSCRIPTION_INIT_ERROR}: ${err}`,
173
173
  );
174
- this._closeSocket();
174
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
175
175
  })
176
176
  .finally(() => {
177
177
  subscriptionStartInProgress = false;
@@ -255,6 +255,11 @@ export abstract class AWSWebSocketProvider {
255
255
 
256
256
  return new Promise((resolve, reject) => {
257
257
  if (this.awsRealTimeSocket) {
258
+ const timeoutId = setTimeout(() => {
259
+ cleanup();
260
+ reject(new Error('Publish operation timed out'));
261
+ }, 30000); // 30 seconds timeout
262
+
258
263
  const publishListener = (event: MessageEvent) => {
259
264
  const data = JSON.parse(event.data);
260
265
  if (data.id === subscriptionId && data.type === 'publish_success') {
@@ -263,22 +268,47 @@ export abstract class AWSWebSocketProvider {
263
268
  'message',
264
269
  publishListener,
265
270
  );
266
-
271
+ cleanup();
267
272
  resolve();
268
273
  }
269
274
 
270
275
  if (data.erroredEvents && data.erroredEvents.length > 0) {
271
276
  // TODO: handle errors
277
+ const errors = data.erroredEvents.map(
278
+ (errorEvent: any) => errorEvent.error,
279
+ );
280
+ cleanup();
281
+ reject(new Error(`Publish errors: ${errors.join(', ')}`));
272
282
  }
273
283
  };
274
- this.awsRealTimeSocket.addEventListener('message', publishListener);
275
- this.awsRealTimeSocket.addEventListener('close', () => {
284
+
285
+ const errorListener = (error: Event) => {
286
+ cleanup();
287
+ reject(new Error(`WebSocket error: ${error}`));
288
+ };
289
+
290
+ const closeListener = () => {
291
+ cleanup();
276
292
  reject(new Error('WebSocket is closed'));
277
- });
278
- //
279
- // this.awsRealTimeSocket.addEventListener('error', publishListener);
293
+ };
294
+
295
+ const cleanup = () => {
296
+ clearTimeout(timeoutId);
297
+ this.awsRealTimeSocket?.removeEventListener(
298
+ 'message',
299
+ publishListener,
300
+ );
301
+ this.awsRealTimeSocket?.removeEventListener('error', errorListener);
302
+ this.awsRealTimeSocket?.removeEventListener('close', closeListener);
303
+ };
304
+
305
+ this.awsRealTimeSocket.addEventListener('message', publishListener);
306
+ this.awsRealTimeSocket.addEventListener('error', errorListener);
307
+ this.awsRealTimeSocket.addEventListener('close', closeListener);
280
308
 
281
309
  this.awsRealTimeSocket.send(serializedSubscriptionMessage);
310
+ } else {
311
+ reject(new Error('WebSocket is not connected'));
282
312
  }
283
313
  });
284
314
  }
@@ -435,7 +465,7 @@ export abstract class AWSWebSocketProvider {
435
465
  this.logger.debug({ err });
436
466
  const message = String(err.message ?? '');
437
467
  // Resolving to give the state observer time to propogate the update
438
- this._closeSocket();
468
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
439
469
 
440
470
  // Capture the error only when the network didn't cause disruption
441
471
  if (
@@ -544,7 +574,12 @@ export abstract class AWSWebSocketProvider {
544
574
  setTimeout(this._closeSocketIfRequired.bind(this), 1000);
545
575
  } else {
546
576
  this.logger.debug('closing WebSocket...');
547
-
577
+ if (this.keepAliveTimeoutId) {
578
+ clearTimeout(this.keepAliveTimeoutId);
579
+ }
580
+ if (this.keepAliveAlertTimeoutId) {
581
+ clearTimeout(this.keepAliveAlertTimeoutId);
582
+ }
548
583
  const tempSocket = this.awsRealTimeSocket;
549
584
  // Cleaning callbacks to avoid race condition, socket still exists
550
585
  tempSocket.onclose = null;
@@ -552,7 +587,7 @@ export abstract class AWSWebSocketProvider {
552
587
  tempSocket.close(1000);
553
588
  this.awsRealTimeSocket = undefined;
554
589
  this.socketStatus = SOCKET_STATUS.CLOSED;
555
- this._closeSocket();
590
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
556
591
  }
557
592
  }
558
593
 
@@ -572,43 +607,13 @@ export abstract class AWSWebSocketProvider {
572
607
  errorType: string;
573
608
  };
574
609
 
575
- private maintainKeepAlive() {
576
- this.kaTimestamp = Date.now();
577
- }
578
-
579
- private keepAliveHeartbeat(connectionTimeoutMs: number) {
580
- const currentTime = Date.now();
581
-
582
- // Check for missed KA message
583
- if (
584
- this.kaTimestamp &&
585
- currentTime - this.kaTimestamp > DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT
586
- ) {
587
- this.connectionStateMonitor.record(CONNECTION_CHANGE.KEEP_ALIVE_MISSED);
588
- } else {
589
- this.connectionStateMonitor.record(CONNECTION_CHANGE.KEEP_ALIVE);
590
- }
591
-
592
- // Recognize we are disconnected if we haven't seen messages in the keep alive timeout period
593
- if (
594
- this.kaTimestamp &&
595
- currentTime - this.kaTimestamp > connectionTimeoutMs
596
- ) {
597
- this._errorDisconnect(CONTROL_MSG.TIMEOUT_DISCONNECT);
598
- }
599
- }
600
-
601
610
  private _handleIncomingSubscriptionMessage(message: MessageEvent) {
602
611
  if (typeof message.data !== 'string') {
603
612
  return;
604
613
  }
605
614
 
606
615
  const [isData, data] = this._handleSubscriptionData(message);
607
- if (isData) {
608
- this.maintainKeepAlive();
609
-
610
- return;
611
- }
616
+ if (isData) return;
612
617
 
613
618
  const { type, id, payload } = data;
614
619
 
@@ -657,7 +662,16 @@ export abstract class AWSWebSocketProvider {
657
662
  }
658
663
 
659
664
  if (type === MESSAGE_TYPES.GQL_CONNECTION_KEEP_ALIVE) {
660
- this.maintainKeepAlive();
665
+ if (this.keepAliveTimeoutId) clearTimeout(this.keepAliveTimeoutId);
666
+ if (this.keepAliveAlertTimeoutId)
667
+ clearTimeout(this.keepAliveAlertTimeoutId);
668
+ this.keepAliveTimeoutId = setTimeout(() => {
669
+ this._errorDisconnect(CONTROL_MSG.TIMEOUT_DISCONNECT);
670
+ }, this.keepAliveTimeout);
671
+ this.keepAliveAlertTimeoutId = setTimeout(() => {
672
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.KEEP_ALIVE_MISSED);
673
+ }, DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT);
674
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.KEEP_ALIVE);
661
675
 
662
676
  return;
663
677
  }
@@ -702,21 +716,13 @@ export abstract class AWSWebSocketProvider {
702
716
  this.logger.debug(`Disconnect error: ${msg}`);
703
717
 
704
718
  if (this.awsRealTimeSocket) {
705
- this._closeSocket();
719
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
706
720
  this.awsRealTimeSocket.close();
707
721
  }
708
722
 
709
723
  this.socketStatus = SOCKET_STATUS.CLOSED;
710
724
  }
711
725
 
712
- private _closeSocket() {
713
- if (this.keepAliveHeartbeatIntervalId) {
714
- clearInterval(this.keepAliveHeartbeatIntervalId);
715
- this.keepAliveHeartbeatIntervalId = undefined;
716
- }
717
- this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
718
- }
719
-
720
726
  private _timeoutStartSubscriptionAck(subscriptionId: string) {
721
727
  const subscriptionObserver =
722
728
  this.subscriptionObserverMap.get(subscriptionId);
@@ -732,7 +738,7 @@ export abstract class AWSWebSocketProvider {
732
738
  subscriptionState: SUBSCRIPTION_STATUS.FAILED,
733
739
  });
734
740
 
735
- this._closeSocket();
741
+ this.connectionStateMonitor.record(CONNECTION_CHANGE.CLOSED);
736
742
  this.logger.debug(
737
743
  'timeoutStartSubscription',
738
744
  JSON.stringify({ query, variables }),
@@ -844,7 +850,6 @@ export abstract class AWSWebSocketProvider {
844
850
  this.logger.debug(`WebSocket connection error`);
845
851
  };
846
852
  newSocket.onclose = () => {
847
- this._closeSocket();
848
853
  reject(new Error('Connection handshake error'));
849
854
  };
850
855
  newSocket.onopen = () => {
@@ -874,7 +879,6 @@ export abstract class AWSWebSocketProvider {
874
879
 
875
880
  this.awsRealTimeSocket.onclose = event => {
876
881
  this.logger.debug(`WebSocket closed ${event.reason}`);
877
- this._closeSocket();
878
882
  reject(new Error(JSON.stringify(event)));
879
883
  };
880
884
 
@@ -938,11 +942,7 @@ export abstract class AWSWebSocketProvider {
938
942
  return;
939
943
  }
940
944
 
941
- // Setup a keep alive heartbeat for this connection
942
- this.keepAliveHeartbeatIntervalId = setInterval(() => {
943
- this.keepAliveHeartbeat(connectionTimeoutMs);
944
- }, DEFAULT_KEEP_ALIVE_HEARTBEAT_TIMEOUT);
945
-
945
+ this.keepAliveTimeout = connectionTimeoutMs;
946
946
  this.awsRealTimeSocket.onmessage =
947
947
  this._handleIncomingSubscriptionMessage.bind(this);
948
948
 
@@ -953,7 +953,6 @@ export abstract class AWSWebSocketProvider {
953
953
 
954
954
  this.awsRealTimeSocket.onclose = event => {
955
955
  this.logger.debug(`WebSocket closed ${event.reason}`);
956
- this._closeSocket();
957
956
  this._errorDisconnect(CONTROL_MSG.CONNECTION_CLOSED);
958
957
  };
959
958
  }
@@ -128,11 +128,6 @@ export const START_ACK_TIMEOUT = 15000;
128
128
  */
129
129
  export const DEFAULT_KEEP_ALIVE_TIMEOUT = 5 * 60 * 1000;
130
130
 
131
- /**
132
- * Default Time in milleseconds between monitoring checks of keep alive status
133
- */
134
- export const DEFAULT_KEEP_ALIVE_HEARTBEAT_TIMEOUT = 5 * 1000;
135
-
136
131
  /**
137
132
  * Default Time in milleseconds to alert for missed GQL_CONNECTION_KEEP_ALIVE message
138
133
  */
@@ -70,7 +70,7 @@ async function connect(
70
70
  };
71
71
 
72
72
  // WS publish is not enabled in the service yet. It will be a follow up feature
73
- const _pub = async (
73
+ const pub = async (
74
74
  event: DocumentType,
75
75
  pubOptions?: EventsOptions,
76
76
  ): Promise<any> => {
@@ -94,7 +94,7 @@ async function connect(
94
94
  return {
95
95
  subscribe: sub,
96
96
  close,
97
- // publish: pub,
97
+ publish: pub,
98
98
  };
99
99
  }
100
100
 
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { Subscription } from 'rxjs';
5
5
  import type { GraphQLAuthMode } from '@aws-amplify/core/internals/utils';
6
+ import { DocumentType } from '@aws-amplify/core/internals/utils';
6
7
 
7
8
  export interface SubscriptionObserver<T> {
8
9
  next(value: T): void;
@@ -51,6 +52,15 @@ export interface EventsChannel {
51
52
  *
52
53
  */
53
54
  close(): void;
55
+ /**
56
+ *
57
+ * @param event
58
+ * @param pubOptions
59
+ *
60
+ * @example
61
+ * await channel.publish({ some: "data" });
62
+ */
63
+ publish(event: DocumentType, pubOptions?: EventsOptions): Promise<any>;
54
64
  }
55
65
 
56
66
  export type ResolvedGraphQLAuthModes = Exclude<GraphQLAuthMode, 'identityPool'>;