@atlaskit/collab-provider 8.1.0 → 8.3.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/cjs/analytics/index.js +68 -20
  3. package/dist/cjs/analytics/performance.js +45 -49
  4. package/dist/cjs/analytics/ufo.js +33 -0
  5. package/dist/cjs/channel.js +138 -154
  6. package/dist/cjs/connectivity/network.js +53 -0
  7. package/dist/cjs/connectivity/reconnect-helper.js +48 -0
  8. package/dist/cjs/connectivity/singleton.js +15 -0
  9. package/dist/cjs/disconnected-reason-mapper.js +19 -4
  10. package/dist/cjs/emitter.js +3 -11
  11. package/dist/cjs/error-code-mapper.js +14 -12
  12. package/dist/cjs/feature-flags/__test__/index.unit.js +0 -1
  13. package/dist/cjs/feature-flags/index.js +5 -15
  14. package/dist/cjs/feature-flags/types.js +2 -0
  15. package/dist/cjs/helpers/const.js +7 -18
  16. package/dist/cjs/helpers/utils.js +0 -12
  17. package/dist/cjs/index.js +0 -1
  18. package/dist/cjs/provider/catchup.js +35 -43
  19. package/dist/cjs/provider/commit-step.js +66 -0
  20. package/dist/cjs/provider/index.js +533 -742
  21. package/dist/cjs/provider/telepointers.js +78 -0
  22. package/dist/cjs/socket-io-provider.js +2 -12
  23. package/dist/cjs/types.js +0 -1
  24. package/dist/cjs/version-wrapper.js +1 -3
  25. package/dist/cjs/version.json +1 -1
  26. package/dist/es2019/analytics/index.js +59 -11
  27. package/dist/es2019/analytics/performance.js +46 -46
  28. package/dist/es2019/analytics/ufo.js +22 -0
  29. package/dist/es2019/channel.js +128 -107
  30. package/dist/es2019/connectivity/network.js +34 -0
  31. package/dist/es2019/connectivity/reconnect-helper.js +29 -0
  32. package/dist/es2019/connectivity/singleton.js +7 -0
  33. package/dist/es2019/disconnected-reason-mapper.js +18 -3
  34. package/dist/es2019/emitter.js +3 -8
  35. package/dist/es2019/error-code-mapper.js +14 -7
  36. package/dist/es2019/feature-flags/index.js +2 -5
  37. package/dist/es2019/feature-flags/types.js +2 -0
  38. package/dist/es2019/helpers/const.js +6 -15
  39. package/dist/es2019/helpers/utils.js +0 -2
  40. package/dist/es2019/provider/catchup.js +32 -23
  41. package/dist/es2019/provider/commit-step.js +53 -0
  42. package/dist/es2019/provider/index.js +446 -616
  43. package/dist/es2019/provider/telepointers.js +65 -0
  44. package/dist/es2019/socket-io-provider.js +4 -2
  45. package/dist/es2019/types.js +1 -1
  46. package/dist/es2019/version-wrapper.js +1 -1
  47. package/dist/es2019/version.json +1 -1
  48. package/dist/esm/analytics/index.js +69 -15
  49. package/dist/esm/analytics/performance.js +46 -46
  50. package/dist/esm/analytics/ufo.js +25 -0
  51. package/dist/esm/channel.js +139 -148
  52. package/dist/esm/connectivity/network.js +45 -0
  53. package/dist/esm/connectivity/reconnect-helper.js +42 -0
  54. package/dist/esm/connectivity/singleton.js +7 -0
  55. package/dist/esm/disconnected-reason-mapper.js +18 -3
  56. package/dist/esm/emitter.js +3 -6
  57. package/dist/esm/error-code-mapper.js +14 -7
  58. package/dist/esm/feature-flags/index.js +5 -10
  59. package/dist/esm/feature-flags/types.js +2 -0
  60. package/dist/esm/helpers/const.js +6 -15
  61. package/dist/esm/helpers/utils.js +0 -3
  62. package/dist/esm/provider/catchup.js +35 -36
  63. package/dist/esm/provider/commit-step.js +58 -0
  64. package/dist/esm/provider/index.js +535 -745
  65. package/dist/esm/provider/telepointers.js +69 -0
  66. package/dist/esm/socket-io-provider.js +2 -5
  67. package/dist/esm/types.js +1 -1
  68. package/dist/esm/version-wrapper.js +1 -1
  69. package/dist/esm/version.json +1 -1
  70. package/dist/types/analytics/index.d.ts +8 -2
  71. package/dist/types/analytics/performance.d.ts +5 -5
  72. package/dist/types/analytics/ufo.d.ts +3 -0
  73. package/dist/types/channel.d.ts +12 -5
  74. package/dist/types/connectivity/network.d.ts +17 -0
  75. package/dist/types/connectivity/reconnect-helper.d.ts +8 -0
  76. package/dist/types/connectivity/singleton.d.ts +3 -0
  77. package/dist/types/disconnected-reason-mapper.d.ts +1 -0
  78. package/dist/types/error-code-mapper.d.ts +4 -0
  79. package/dist/types/helpers/const.d.ts +116 -19
  80. package/dist/types/provider/commit-step.d.ts +14 -0
  81. package/dist/types/provider/index.d.ts +9 -5
  82. package/dist/types/provider/telepointers.d.ts +5 -0
  83. package/dist/types/socket-io-provider.d.ts +2 -1
  84. package/dist/types/types.d.ts +30 -11
  85. package/package.json +7 -6
  86. package/report.api.md +35 -0
  87. package/.vscode/settings.json +0 -3
@@ -1,79 +1,88 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
- import { utils } from '@atlaskit/util-service-support'; // TODO: Validate if this is actually equivalent to the AnalyticsWebClient in @atlassiansox/analytics-web-client
3
-
2
+ import { utils } from '@atlaskit/util-service-support';
4
3
  import { Emitter } from './emitter';
5
4
  import { ErrorCodeMapper } from './error-code-mapper';
6
5
  import { createLogger, getProduct, getSubProduct } from './helpers/utils';
7
6
  import { MEASURE_NAME, startMeasure, stopMeasure } from './analytics/performance';
8
- import { triggerAnalyticsEvent } from './analytics';
9
7
  import { EVENT_ACTION, EVENT_STATUS } from './helpers/const';
10
- import { ExperiencePerformanceTypes, ExperienceTypes, UFOExperience } from '@atlaskit/ufo';
8
+ import ReconnectHelper from './connectivity/reconnect-helper';
9
+ import { createDocInitExp } from './analytics/ufo';
10
+ import Network from './connectivity/network';
11
11
  const logger = createLogger('Channel', 'green');
12
12
  export class Channel extends Emitter {
13
- constructor(config) {
13
+ constructor(config, analyticsHelper) {
14
14
  super();
15
-
16
15
  _defineProperty(this, "connected", false);
17
-
18
16
  _defineProperty(this, "socket", null);
19
-
17
+ _defineProperty(this, "reconnectHelper", null);
20
18
  _defineProperty(this, "initialized", false);
21
-
22
- _defineProperty(this, "initExperience", new UFOExperience('collab-provider.document-init', {
23
- type: ExperienceTypes.Load,
24
- performanceType: ExperiencePerformanceTypes.Custom,
25
- performanceConfig: {
26
- histogram: {
27
- [ExperiencePerformanceTypes.Custom]: {
28
- duration: '250_500_1000_1500_2000_3000_4000'
29
- }
30
- }
31
- }
32
- }));
33
-
19
+ _defineProperty(this, "network", null);
34
20
  _defineProperty(this, "getInitialized", () => this.initialized);
35
-
36
21
  _defineProperty(this, "getConnected", () => this.connected);
37
-
38
22
  _defineProperty(this, "getSocket", () => this.socket);
39
-
23
+ _defineProperty(this, "onConnectError", error => {
24
+ var _this$analyticsHelper, _this$analyticsHelper2;
25
+ const measure = stopMeasure(MEASURE_NAME.SOCKET_CONNECT, this.analyticsHelper);
26
+ (_this$analyticsHelper = this.analyticsHelper) === null || _this$analyticsHelper === void 0 ? void 0 : _this$analyticsHelper.sendActionEvent(EVENT_ACTION.CONNECTION, EVENT_STATUS.FAILURE, {
27
+ latency: measure === null || measure === void 0 ? void 0 : measure.duration
28
+ });
29
+ (_this$analyticsHelper2 = this.analyticsHelper) === null || _this$analyticsHelper2 === void 0 ? void 0 : _this$analyticsHelper2.sendErrorEvent(error, 'Error while establishing connection');
30
+ // If error received with `data`, it means the connection is rejected
31
+ // by the server on purpose for example no permission, so no need to
32
+ // keep the underneath connection, need to close. But some error like
33
+ // `xhr polling error` needs to retry.
34
+ if (!!error.data) {
35
+ var _this$socket;
36
+ (_this$socket = this.socket) === null || _this$socket === void 0 ? void 0 : _this$socket.close();
37
+ }
38
+ this.emit('error', {
39
+ message: error.message,
40
+ data: error.data
41
+ });
42
+ });
43
+ _defineProperty(this, "onReconnectError", error => {
44
+ var _this$reconnectHelper, _this$reconnectHelper2;
45
+ (_this$reconnectHelper = this.reconnectHelper) === null || _this$reconnectHelper === void 0 ? void 0 : _this$reconnectHelper.countReconnectError();
46
+ if ((_this$reconnectHelper2 = this.reconnectHelper) !== null && _this$reconnectHelper2 !== void 0 && _this$reconnectHelper2.isLikelyNetworkIssue()) {
47
+ var _this$analyticsHelper3;
48
+ (_this$analyticsHelper3 = this.analyticsHelper) === null || _this$analyticsHelper3 === void 0 ? void 0 : _this$analyticsHelper3.sendErrorEvent(error, 'Likely network issue while reconnecting the channel');
49
+ this.emit('error', {
50
+ message: 'Reconnection failed 8 times when browser was offline, likely there was a network issue.',
51
+ data: {
52
+ status: 400,
53
+ code: 'RECONNECTION_NETWORK_ISSUE'
54
+ }
55
+ });
56
+ }
57
+ });
40
58
  _defineProperty(this, "onConnect", () => {
59
+ var _this$analyticsHelper4;
41
60
  this.connected = true;
42
61
  logger('Connected.', this.socket.id);
43
- const measure = stopMeasure(MEASURE_NAME.SOCKET_CONNECT);
44
- triggerAnalyticsEvent({
45
- eventAction: EVENT_ACTION.CONNECTION,
46
- attributes: {
47
- eventStatus: EVENT_STATUS.SUCCESS,
48
- latency: measure === null || measure === void 0 ? void 0 : measure.duration,
49
- documentAri: this.config.documentAri
50
- }
51
- }, this.analyticsClient);
62
+ const measure = stopMeasure(MEASURE_NAME.SOCKET_CONNECT, this.analyticsHelper);
63
+ (_this$analyticsHelper4 = this.analyticsHelper) === null || _this$analyticsHelper4 === void 0 ? void 0 : _this$analyticsHelper4.sendActionEvent(EVENT_ACTION.CONNECTION, EVENT_STATUS.SUCCESS, {
64
+ latency: measure === null || measure === void 0 ? void 0 : measure.duration
65
+ });
52
66
  this.emit('connected', {
53
67
  sid: this.socket.id,
54
68
  initialized: this.initialized
55
69
  });
56
70
  });
57
-
58
71
  _defineProperty(this, "onReceiveData", data => {
59
72
  logger('Received data', data);
60
73
  logger('Session ID is', this.socket.id);
61
-
62
74
  if (data.type === 'initial') {
63
75
  if (!this.initialized) {
64
- const measure = stopMeasure(MEASURE_NAME.DOCUMENT_INIT);
65
- this.initExperience.success();
66
- triggerAnalyticsEvent({
67
- eventAction: EVENT_ACTION.DOCUMENT_INIT,
68
- attributes: {
69
- eventStatus: EVENT_STATUS.SUCCESS,
70
- // TODO: detect when document init fails and fire corresponding event for it
71
- latency: measure === null || measure === void 0 ? void 0 : measure.duration,
72
- documentAri: this.config.documentAri,
73
- requiredPageRecovery: data === null || data === void 0 ? void 0 : data.requiredPageRecovery,
74
- ttlEnabled: data === null || data === void 0 ? void 0 : data.ttlEnabled
75
- }
76
- }, this.analyticsClient);
76
+ var _this$initExperience, _this$analyticsHelper5;
77
+ const measure = stopMeasure(MEASURE_NAME.DOCUMENT_INIT, this.analyticsHelper);
78
+ (_this$initExperience = this.initExperience) === null || _this$initExperience === void 0 ? void 0 : _this$initExperience.success();
79
+ (_this$analyticsHelper5 = this.analyticsHelper) === null || _this$analyticsHelper5 === void 0 ? void 0 : _this$analyticsHelper5.sendActionEvent(EVENT_ACTION.DOCUMENT_INIT,
80
+ // TODO: detect when document init fails and fire corresponding event for it
81
+ EVENT_STATUS.SUCCESS, {
82
+ latency: measure === null || measure === void 0 ? void 0 : measure.duration,
83
+ resetReason: data === null || data === void 0 ? void 0 : data.resetReason,
84
+ ttlEnabled: data === null || data === void 0 ? void 0 : data.ttlEnabled
85
+ });
77
86
  const {
78
87
  doc,
79
88
  version,
@@ -106,26 +115,31 @@ export class Channel extends Emitter {
106
115
  this.emit('steps:added', data);
107
116
  }
108
117
  });
109
-
118
+ _defineProperty(this, "onOnlineHandler", () => {
119
+ // Force an immediate reconnect, the socket must first be closed to reset reconnection delay logic
120
+ if (!this.connected) {
121
+ var _this$socket2, _this$socket3;
122
+ (_this$socket2 = this.socket) === null || _this$socket2 === void 0 ? void 0 : _this$socket2.close();
123
+ (_this$socket3 = this.socket) === null || _this$socket3 === void 0 ? void 0 : _this$socket3.connect();
124
+ }
125
+ });
110
126
  this.config = config;
127
+ this.analyticsHelper = analyticsHelper;
128
+ this.initExperience = createDocInitExp(this.analyticsHelper);
129
+ }
111
130
 
112
- if (config.analyticsClient) {
113
- this.analyticsClient = config.analyticsClient;
114
- }
115
- } // read-only getters used for tests
116
-
131
+ // read-only getters used for tests
117
132
 
118
133
  /**
119
134
  * Connect to collab service using websockets
120
135
  */
121
136
  connect() {
122
- startMeasure(MEASURE_NAME.SOCKET_CONNECT);
123
-
137
+ startMeasure(MEASURE_NAME.SOCKET_CONNECT, this.analyticsHelper);
124
138
  if (!this.initialized) {
125
- startMeasure(MEASURE_NAME.DOCUMENT_INIT);
126
- this.initExperience.start();
139
+ var _this$initExperience2;
140
+ startMeasure(MEASURE_NAME.DOCUMENT_INIT, this.analyticsHelper);
141
+ (_this$initExperience2 = this.initExperience) === null || _this$initExperience2 === void 0 ? void 0 : _this$initExperience2.start();
127
142
  }
128
-
129
143
  const {
130
144
  documentAri,
131
145
  url
@@ -137,11 +151,11 @@ export class Channel extends Emitter {
137
151
  permissionTokenRefresh
138
152
  } = this.config;
139
153
  let authCb = null;
140
-
141
154
  if (permissionTokenRefresh) {
142
155
  authCb = cb => {
143
156
  permissionTokenRefresh().then(token => {
144
- cb({ // The permission token.
157
+ cb({
158
+ // The permission token.
145
159
  ...(token ? {
146
160
  token
147
161
  } : {}),
@@ -164,11 +178,11 @@ export class Channel extends Emitter {
164
178
  });
165
179
  };
166
180
  }
181
+ this.socket = createSocket(`${url}/session/${documentAri}`, authCb, this.config.productInfo);
167
182
 
168
- this.socket = createSocket(`${url}/session/${documentAri}`, authCb, this.config.productInfo); // Due to https://github.com/socketio/socket.io-client/issues/1473,
183
+ // Due to https://github.com/socketio/socket.io-client/issues/1473,
169
184
  // reconnect no longer fired on the socket.
170
185
  // We should use `connect` for better cross platform compatibility(Mobile/Web).
171
-
172
186
  this.socket.on('connect', this.onConnect);
173
187
  this.socket.on('data', this.onReceiveData);
174
188
  this.socket.on('steps:added', data => {
@@ -201,8 +215,9 @@ export class Channel extends Emitter {
201
215
  });
202
216
  this.socket.on('metadata:changed', payload => {
203
217
  this.emit('metadata:changed', payload);
204
- }); // ESS-2916 namespace status event - lock/unlock
218
+ });
205
219
 
220
+ // ESS-2916 namespace status event - lock/unlock
206
221
  this.socket.on('status', data => {
207
222
  this.emit('status', data);
208
223
  });
@@ -212,50 +227,48 @@ export class Channel extends Emitter {
212
227
  this.emit('disconnect', {
213
228
  reason
214
229
  });
215
-
216
230
  if (reason === 'io server disconnect' && this.socket) {
217
231
  // The disconnection was initiated by the server, we need to reconnect manually.
218
- this.socket.connect();
232
+ try {
233
+ this.socket.connect();
234
+ } catch (error) {
235
+ var _this$analyticsHelper6;
236
+ (_this$analyticsHelper6 = this.analyticsHelper) === null || _this$analyticsHelper6 === void 0 ? void 0 : _this$analyticsHelper6.sendErrorEvent(error, 'Error while reconnecting the channel');
237
+ this.emit('error', {
238
+ message: 'Caught error during reconnection.',
239
+ data: {
240
+ status: 500,
241
+ code: 'RECONNECTION_ERROR'
242
+ }
243
+ });
244
+ }
219
245
  }
220
- }); // Socket error, including errors from `packetMiddleware`, `controllers` and `onconnect` and more. Paramter is a plain JSON object.
246
+ });
221
247
 
248
+ // Socket error, including errors from `packetMiddleware`, `controllers` and `onconnect` and more. Parameter is a plain JSON object.
222
249
  this.socket.on('error', error => {
223
250
  this.emit('error', error);
224
- }); // `connect_error`'s paramter type is `Error`.
225
- // Ensure the error emit to the provider has the same structure, so we can handle them unified.
226
-
227
- this.socket.on('connect_error', error => {
228
- const measure = stopMeasure(MEASURE_NAME.SOCKET_CONNECT);
229
- triggerAnalyticsEvent({
230
- eventAction: EVENT_ACTION.CONNECTION,
231
- attributes: {
232
- eventStatus: EVENT_STATUS.FAILURE,
233
- error: error,
234
- latency: measure === null || measure === void 0 ? void 0 : measure.duration,
235
- documentAri: this.config.documentAri
236
- }
237
- }, this.analyticsClient); // If error received with `data`, it means the connection is rejected
238
- // by the server on purpose for example no permission, so no need to
239
- // keep the underneath connection, need to close. But some error like
240
- // `xhr polling error` needs to retry.
241
-
242
- if (!!error.data) {
243
- var _this$socket;
251
+ });
244
252
 
245
- (_this$socket = this.socket) === null || _this$socket === void 0 ? void 0 : _this$socket.close();
246
- }
253
+ // `connect_error`'s parameter type is `Error`.
254
+ // Ensure the error emit to the provider has the same structure, so we can handle them unified.
255
+ this.socket.on('connect_error', this.onConnectError);
247
256
 
248
- this.emit('error', {
249
- message: error.message,
250
- data: error.data
257
+ // To trigger reconnection when browser comes back online
258
+ if (!this.network) {
259
+ this.network = new Network({
260
+ onlineCallback: this.onOnlineHandler
251
261
  });
252
- });
253
- }
262
+ }
254
263
 
264
+ // Helper class to track reconnection issues, to emit an error if too many failed reconnection attempts happen
265
+ this.reconnectHelper = new ReconnectHelper();
266
+ // Fired upon a reconnection attempt error (from Socket.IO Manager)
267
+ this.socket.io.on('reconnect_error', this.onReconnectError);
268
+ }
255
269
  async fetchCatchup(fromVersion) {
256
270
  try {
257
271
  var _await$this$config$pe;
258
-
259
272
  const {
260
273
  doc,
261
274
  version,
@@ -267,7 +280,8 @@ export class Channel extends Emitter {
267
280
  version: fromVersion
268
281
  },
269
282
  requestInit: {
270
- headers: { ...(this.config.permissionTokenRefresh ? {
283
+ headers: {
284
+ ...(this.config.permissionTokenRefresh ? {
271
285
  'x-token': (_await$this$config$pe = await this.config.permissionTokenRefresh()) !== null && _await$this$config$pe !== void 0 ? _await$this$config$pe : undefined
272
286
  } : {}),
273
287
  'x-product': getProduct(this.config.productInfo),
@@ -282,6 +296,17 @@ export class Channel extends Emitter {
282
296
  metadata
283
297
  };
284
298
  } catch (error) {
299
+ if (error.code === 404) {
300
+ const errorNotFound = {
301
+ message: ErrorCodeMapper.documentNotFound.message,
302
+ data: {
303
+ status: error.code,
304
+ code: ErrorCodeMapper.documentNotFound.code
305
+ }
306
+ };
307
+ this.emit('error', errorNotFound);
308
+ return {};
309
+ }
285
310
  logger("Can't fetch the catchup", error.message);
286
311
  const errorCatchup = {
287
312
  message: ErrorCodeMapper.catchupFail.message,
@@ -291,48 +316,44 @@ export class Channel extends Emitter {
291
316
  }
292
317
  };
293
318
  this.emit('error', errorCatchup);
294
- return {};
319
+ throw error;
295
320
  }
296
321
  }
322
+
297
323
  /**
298
324
  * Send message to service. Timestamp will be added server side.
299
325
  */
300
-
301
-
302
326
  broadcast(type, data, callback) {
303
327
  if (!this.connected || !this.socket) {
304
328
  return;
305
329
  }
306
-
307
330
  this.socket.emit('broadcast', {
308
331
  type,
309
332
  ...data
310
333
  }, callback);
311
334
  }
312
-
313
335
  sendMetadata(metadata) {
314
336
  if (!this.connected || !this.socket) {
315
337
  return;
316
338
  }
317
-
318
339
  this.socket.emit('metadata', metadata);
319
340
  }
320
-
321
341
  sendPresenceJoined() {
322
342
  if (!this.connected || !this.socket) {
323
343
  return;
324
344
  }
325
-
326
345
  this.socket.emit('presence:joined');
327
346
  }
328
-
329
347
  disconnect() {
348
+ var _this$network;
330
349
  this.unsubscribeAll();
331
-
350
+ (_this$network = this.network) === null || _this$network === void 0 ? void 0 : _this$network.destroy();
351
+ this.network = null;
332
352
  if (this.socket) {
353
+ var _this$reconnectHelper3;
333
354
  this.socket.close();
334
355
  this.socket = null;
356
+ (_this$reconnectHelper3 = this.reconnectHelper) === null || _this$reconnectHelper3 === void 0 ? void 0 : _this$reconnectHelper3.destroy();
335
357
  }
336
358
  }
337
-
338
359
  }
@@ -0,0 +1,34 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ export let NetworkStatus;
3
+ (function (NetworkStatus) {
4
+ NetworkStatus["ONLINE"] = "ONLINE";
5
+ NetworkStatus["OFFLINE"] = "OFFLINE";
6
+ })(NetworkStatus || (NetworkStatus = {}));
7
+ export default class Network {
8
+ constructor(props) {
9
+ _defineProperty(this, "offlineHandler", () => {
10
+ this.status = NetworkStatus.OFFLINE;
11
+ });
12
+ _defineProperty(this, "onlineHandler", () => {
13
+ this.status = NetworkStatus.ONLINE;
14
+ if (this.onlineCallback) {
15
+ this.onlineCallback();
16
+ }
17
+ });
18
+ if (props !== null && props !== void 0 && props.initialStatus) {
19
+ this.status = props.initialStatus;
20
+ }
21
+ if (props !== null && props !== void 0 && props.onlineCallback) {
22
+ this.onlineCallback = props.onlineCallback;
23
+ }
24
+ window.addEventListener('offline', this.offlineHandler);
25
+ window.addEventListener('online', this.onlineHandler);
26
+ }
27
+ getStatus() {
28
+ return this.status || null;
29
+ }
30
+ destroy() {
31
+ window.removeEventListener('offline', this.offlineHandler);
32
+ window.removeEventListener('online', this.onlineHandler);
33
+ }
34
+ }
@@ -0,0 +1,29 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import { NetworkStatus } from './network';
3
+ import { network } from './singleton';
4
+
5
+ // Calculation for max 8 offline reconnect attempts, with reconnection delay 200ms, randomization factor 0.5, reconnection delay max 128s
6
+ // Min: 800ms, Avg: 51s, Max: 6m
7
+ const FAILED_RECONNECTS_WHILE_OFFLINE_THRESHOLD = 8;
8
+ export default class ReconnectHelper {
9
+ constructor() {
10
+ _defineProperty(this, "failedReconnectCount", 0);
11
+ _defineProperty(this, "onlineHandler", () => {
12
+ this.failedReconnectCount = 0;
13
+ });
14
+ window.addEventListener('online', this.onlineHandler);
15
+ }
16
+ countReconnectError() {
17
+ // Only count the reconnection attempts when offline
18
+ if (network.getStatus() === NetworkStatus.OFFLINE) {
19
+ this.failedReconnectCount++;
20
+ }
21
+ }
22
+ isLikelyNetworkIssue() {
23
+ const isLikelyNetworkIssue = this.failedReconnectCount >= FAILED_RECONNECTS_WHILE_OFFLINE_THRESHOLD;
24
+ return isLikelyNetworkIssue;
25
+ }
26
+ destroy() {
27
+ window.removeEventListener('online', this.onlineHandler);
28
+ }
29
+ }
@@ -0,0 +1,7 @@
1
+ import Network, { NetworkStatus } from './network';
2
+
3
+ // Assume the connection is established at first
4
+ const network = new Network({
5
+ initialStatus: NetworkStatus.ONLINE
6
+ });
7
+ export { network };
@@ -9,10 +9,9 @@ export const socketIOReasons = {
9
9
  TRANSPORT_ERROR: 'transport error',
10
10
  // The connection was closed (example: the user has lost connection, or the network was changed from WiFi to 4G)
11
11
  PING_TIMEOUT: 'ping timeout' // The connection has encountered an error (example: the server was killed during a HTTP long-polling cycle)
12
-
13
12
  };
14
- export let DisconnectReason;
15
13
 
14
+ export let DisconnectReason;
16
15
  (function (DisconnectReason) {
17
16
  DisconnectReason["CLIENT_DISCONNECT"] = "CLIENT_DISCONNECT";
18
17
  DisconnectReason["SERVER_DISCONNECT"] = "SERVER_DISCONNECT";
@@ -20,4 +19,20 @@ export let DisconnectReason;
20
19
  DisconnectReason["SOCKET_ERROR"] = "SOCKET_ERROR";
21
20
  DisconnectReason["SOCKET_TIMEOUT"] = "SOCKET_TIMEOUT";
22
21
  DisconnectReason["UNKNOWN_DISCONNECT"] = "UNKNOWN_DISCONNECT";
23
- })(DisconnectReason || (DisconnectReason = {}));
22
+ })(DisconnectReason || (DisconnectReason = {}));
23
+ export const disconnectedReasonMapper = reason => {
24
+ switch (reason) {
25
+ case socketIOReasons.IO_CLIENT_DISCONNECT:
26
+ return DisconnectReason.CLIENT_DISCONNECT;
27
+ case socketIOReasons.IO_SERVER_DISCONNECT:
28
+ return DisconnectReason.SERVER_DISCONNECT;
29
+ case socketIOReasons.TRANSPORT_CLOSED:
30
+ return DisconnectReason.SOCKET_CLOSED;
31
+ case socketIOReasons.TRANSPORT_ERROR:
32
+ return DisconnectReason.SOCKET_ERROR;
33
+ case socketIOReasons.PING_TIMEOUT:
34
+ return DisconnectReason.SOCKET_TIMEOUT;
35
+ default:
36
+ return DisconnectReason.UNKNOWN_DISCONNECT;
37
+ }
38
+ };
@@ -4,7 +4,6 @@ export class Emitter {
4
4
  constructor() {
5
5
  _defineProperty(this, "eventEmitter", new EventEmitter2());
6
6
  }
7
-
8
7
  /**
9
8
  * Emit events to subscribers
10
9
  */
@@ -12,32 +11,28 @@ export class Emitter {
12
11
  this.eventEmitter.emit(evt, data);
13
12
  return this;
14
13
  }
14
+
15
15
  /**
16
16
  * Subscribe to events emitted by this provider
17
17
  */
18
-
19
-
20
18
  on(evt, handler) {
21
19
  this.eventEmitter.on(evt, handler);
22
20
  return this;
23
21
  }
22
+
24
23
  /**
25
24
  * Unsubscribe from events emitted by this provider
26
25
  */
27
-
28
-
29
26
  off(evt, handler) {
30
27
  this.eventEmitter.off(evt, handler);
31
28
  return this;
32
29
  }
30
+
33
31
  /**
34
32
  * Unsubscribe from all events emitted by this provider.
35
33
  */
36
-
37
-
38
34
  unsubscribeAll(evt) {
39
35
  this.eventEmitter.removeAllListeners(evt);
40
36
  return this;
41
37
  }
42
-
43
38
  }
@@ -23,31 +23,33 @@ export const ErrorCodeMapper = {
23
23
  code: 'FAIL_TO_SAVE',
24
24
  message: 'Collab service is not able to save changes'
25
25
  },
26
+ restoreError: {
27
+ code: 'DOCUMENT_RESTORE_ERROR',
28
+ message: 'Collab service unable to restore document'
29
+ },
26
30
  internalError: {
27
31
  code: 'INTERNAL_SERVICE_ERROR',
28
- message: 'Collab service has return internal server error'
32
+ message: 'Collab service has experienced an internal server error'
29
33
  }
30
34
  };
31
35
  export const errorCodeMapper = error => {
32
36
  var _error$data;
33
-
34
37
  switch ((_error$data = error.data) === null || _error$data === void 0 ? void 0 : _error$data.code) {
35
38
  case 'INSUFFICIENT_EDITING_PERMISSION':
36
39
  return {
37
40
  status: 403,
38
41
  code: ErrorCodeMapper.noPermissionError.code,
39
42
  message: ErrorCodeMapper.noPermissionError.message,
40
- reason: // Typescript magic so it detects the union type
43
+ reason:
44
+ // Typescript magic so it detects the union type
41
45
  typeof error.data.meta === 'object' ? error.data.meta.reason : undefined
42
46
  };
43
-
44
47
  case 'DOCUMENT_NOT_FOUND':
45
48
  return {
46
49
  status: 404,
47
50
  code: ErrorCodeMapper.documentNotFound.code,
48
51
  message: ErrorCodeMapper.documentNotFound.message
49
52
  };
50
-
51
53
  case 'FAILED_ON_S3':
52
54
  case 'DYNAMO_ERROR':
53
55
  return {
@@ -55,7 +57,13 @@ export const errorCodeMapper = error => {
55
57
  code: ErrorCodeMapper.failToSave.code,
56
58
  message: ErrorCodeMapper.failToSave.message
57
59
  };
58
-
60
+ case 'DOCUMENT_RESTORE_ERROR':
61
+ return {
62
+ status: 500,
63
+ code: ErrorCodeMapper.restoreError.code,
64
+ message: ErrorCodeMapper.restoreError.message
65
+ };
66
+ // Temporarily re-added so we don't emit errors to Confluence by default as they will disconnect the collab provider
59
67
  case 'CATCHUP_FAILED':
60
68
  case 'GET_QUERY_TIME_OUT':
61
69
  case 'INIT_DATA_LOAD_FAILED':
@@ -64,7 +72,6 @@ export const errorCodeMapper = error => {
64
72
  code: ErrorCodeMapper.internalError.code,
65
73
  message: ErrorCodeMapper.internalError.message
66
74
  };
67
-
68
75
  default:
69
76
  break;
70
77
  }
@@ -1,26 +1,24 @@
1
1
  const defaultNCSFeatureFlags = {
2
2
  testFF: false
3
3
  };
4
+
4
5
  /**
5
6
  * Note that Confluence should have the same FF sets as NCS
6
7
  */
7
-
8
8
  const productKeys = {
9
9
  confluence: {
10
10
  testFF: 'confluence.fe.collab.provider.testFF'
11
11
  }
12
12
  };
13
-
14
13
  const filterFeatureFlagNames = flags => {
15
14
  const pairs = Object.entries(flags);
16
15
  return pairs.filter(([_key, value]) => !!value).map(([key]) => key);
17
16
  };
17
+
18
18
  /**
19
19
  * Takes a record of {NCS Feature Flag Names → boolean} and a supported product name.
20
20
  * Returns the corresponding product’s Launch Darkly Keys for each of the flags set as true in the input record.
21
21
  * */
22
-
23
-
24
22
  export const getProductSpecificFeatureFlags = (flags, product) => {
25
23
  const ncsFeatureFlags = filterFeatureFlagNames(flags);
26
24
  return ncsFeatureFlags.map(key => productKeys[product][key]);
@@ -29,6 +27,5 @@ export function getCollabProviderFeatureFlag(flagName, featureFlags) {
29
27
  if (featureFlags) {
30
28
  return flagName in featureFlags ? featureFlags[flagName] : defaultNCSFeatureFlags[flagName];
31
29
  }
32
-
33
30
  return defaultNCSFeatureFlags[flagName];
34
31
  }
@@ -1,3 +1,5 @@
1
1
  // NCS feature flags - type and defaults defined here in one source of truth
2
+
2
3
  // With this type we ensure the object will contain all the flags
4
+
3
5
  export const supportedProducts = ['confluence'];