@atlaskit/collab-provider 8.3.0 → 8.5.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 (86) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/cjs/analytics/{index.js → analytics-helper.js} +45 -5
  3. package/dist/cjs/analytics/performance.js +7 -5
  4. package/dist/cjs/channel.js +318 -210
  5. package/dist/cjs/{provider → document}/catchup.js +2 -2
  6. package/dist/cjs/document/document-service.js +617 -0
  7. package/dist/cjs/document/step-queue-state.js +51 -0
  8. package/dist/cjs/errors/error-code-mapper.js +107 -0
  9. package/dist/cjs/errors/error-types.js +273 -0
  10. package/dist/cjs/helpers/const.js +2 -4
  11. package/dist/cjs/helpers/utils.js +1 -12
  12. package/dist/cjs/participants/participants-helper.js +51 -0
  13. package/dist/cjs/participants/participants-service.js +217 -0
  14. package/dist/cjs/participants/participants-state.js +53 -0
  15. package/dist/cjs/{provider/telepointers.js → participants/telepointers-helper.js} +6 -6
  16. package/dist/cjs/provider/commit-step.js +40 -36
  17. package/dist/cjs/provider/index.js +215 -762
  18. package/dist/cjs/types.js +3 -0
  19. package/dist/cjs/version-wrapper.js +1 -1
  20. package/dist/cjs/version.json +1 -1
  21. package/dist/es2019/analytics/{index.js → analytics-helper.js} +17 -5
  22. package/dist/es2019/analytics/performance.js +6 -6
  23. package/dist/es2019/channel.js +204 -129
  24. package/dist/es2019/{provider → document}/catchup.js +2 -2
  25. package/dist/es2019/document/document-service.js +495 -0
  26. package/dist/es2019/document/step-queue-state.js +30 -0
  27. package/dist/es2019/errors/error-code-mapper.js +102 -0
  28. package/dist/es2019/errors/error-types.js +151 -0
  29. package/dist/es2019/helpers/const.js +2 -4
  30. package/dist/es2019/helpers/utils.js +0 -10
  31. package/dist/es2019/participants/participants-helper.js +25 -0
  32. package/dist/es2019/participants/participants-service.js +166 -0
  33. package/dist/es2019/participants/participants-state.js +28 -0
  34. package/dist/es2019/{provider/telepointers.js → participants/telepointers-helper.js} +2 -2
  35. package/dist/es2019/provider/commit-step.js +38 -34
  36. package/dist/es2019/provider/index.js +163 -626
  37. package/dist/es2019/types.js +4 -0
  38. package/dist/es2019/version-wrapper.js +1 -1
  39. package/dist/es2019/version.json +1 -1
  40. package/dist/esm/analytics/{index.js → analytics-helper.js} +45 -5
  41. package/dist/esm/analytics/performance.js +6 -6
  42. package/dist/esm/channel.js +318 -210
  43. package/dist/esm/{provider → document}/catchup.js +2 -2
  44. package/dist/esm/document/document-service.js +609 -0
  45. package/dist/esm/document/step-queue-state.js +43 -0
  46. package/dist/esm/errors/error-code-mapper.js +102 -0
  47. package/dist/esm/errors/error-types.js +259 -0
  48. package/dist/esm/helpers/const.js +2 -4
  49. package/dist/esm/helpers/utils.js +0 -10
  50. package/dist/esm/participants/participants-helper.js +43 -0
  51. package/dist/esm/participants/participants-service.js +209 -0
  52. package/dist/esm/participants/participants-state.js +45 -0
  53. package/dist/esm/{provider/telepointers.js → participants/telepointers-helper.js} +4 -4
  54. package/dist/esm/provider/commit-step.js +40 -36
  55. package/dist/esm/provider/index.js +214 -762
  56. package/dist/esm/types.js +4 -0
  57. package/dist/esm/version-wrapper.js +1 -1
  58. package/dist/esm/version.json +1 -1
  59. package/dist/types/analytics/{index.d.ts → analytics-helper.d.ts} +3 -1
  60. package/dist/types/analytics/performance.d.ts +5 -2
  61. package/dist/types/analytics/ufo.d.ts +1 -1
  62. package/dist/types/channel.d.ts +17 -5
  63. package/dist/types/document/document-service.d.ts +105 -0
  64. package/dist/types/document/step-queue-state.d.ts +16 -0
  65. package/dist/types/errors/error-code-mapper.d.ts +2 -0
  66. package/dist/types/errors/error-types.d.ts +443 -0
  67. package/dist/types/helpers/const.d.ts +31 -8
  68. package/dist/types/helpers/utils.d.ts +0 -6
  69. package/dist/types/index.d.ts +2 -1
  70. package/dist/types/participants/participants-helper.d.ts +15 -0
  71. package/dist/types/participants/participants-service.d.ts +70 -0
  72. package/dist/types/participants/participants-state.d.ts +13 -0
  73. package/dist/types/participants/telepointers-helper.d.ts +4 -0
  74. package/dist/types/provider/commit-step.d.ts +6 -6
  75. package/dist/types/provider/index.d.ts +86 -65
  76. package/dist/types/socket-io-provider.d.ts +2 -2
  77. package/dist/types/types.d.ts +65 -33
  78. package/package.json +4 -4
  79. package/report.api.md +193 -23
  80. package/dist/cjs/error-code-mapper.js +0 -88
  81. package/dist/es2019/error-code-mapper.js +0 -78
  82. package/dist/esm/error-code-mapper.js +0 -79
  83. package/dist/types/error-code-mapper.d.ts +0 -36
  84. package/dist/types/provider/telepointers.d.ts +0 -5
  85. package/error-code-mapper/package.json +0 -15
  86. /package/dist/types/{provider → document}/catchup.d.ts +0 -0
@@ -0,0 +1,495 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import { ACK_MAX_TRY, EVENT_ACTION, EVENT_STATUS } from '../helpers/const';
3
+ import { getVersion, sendableSteps } from '@atlaskit/prosemirror-collab';
4
+ import { createLogger, sleep } from '../helpers/utils';
5
+ import throttle from 'lodash/throttle';
6
+ import { MEASURE_NAME, startMeasure, stopMeasure } from '../analytics/performance';
7
+ import { JSONTransformer } from '@atlaskit/editor-json-transformer';
8
+ import { MAX_STEP_REJECTED_ERROR, throttledCommitStep } from '../provider';
9
+ import { catchup } from './catchup';
10
+ import isEqual from 'lodash/isEqual';
11
+ import { StepQueueState } from './step-queue-state';
12
+ import { INTERNAL_ERROR_CODE } from '../errors/error-types';
13
+ const CATCHUP_THROTTLE = 1 * 1000; // 1 second
14
+
15
+ const noop = () => {};
16
+ const logger = createLogger('documentService', 'black');
17
+ export class DocumentService {
18
+ // Fires analytics to editor when collab editor cannot sync up
19
+
20
+ /**
21
+ *
22
+ * @param participantsService - The participants service, used when users are detected active when making changes to the document
23
+ * and to emit their telepointers from steps they add
24
+ * @param analyticsHelper - Helper for analytics events
25
+ * @param fetchCatchup - Function to fetch "catchup" data, data required to rebase current steps to the latest version.
26
+ * @param providerEmitCallback - Callback for emitting events to listeners on the provider
27
+ * @param broadcastMetadata - Callback for broadcasting metadata changes to other clients
28
+ * @param broadcast - Callback for broadcasting events to other clients
29
+ * @param getUserId - Callback to fetch the current user's ID
30
+ * @param onErrorHandled - Callback to handle
31
+ */
32
+ constructor(participantsService, analyticsHelper, fetchCatchup, providerEmitCallback, broadcastMetadata, broadcast, getUserId, onErrorHandled) {
33
+ _defineProperty(this, "stepRejectCounter", 0);
34
+ _defineProperty(this, "metadata", {});
35
+ _defineProperty(this, "onMetadataChanged", metadata => {
36
+ if (metadata !== undefined && !isEqual(this.metadata, metadata)) {
37
+ this.metadata = metadata;
38
+ this.providerEmitCallback('metadata:changed', metadata);
39
+ }
40
+ });
41
+ _defineProperty(this, "getMetaData", () => this.metadata);
42
+ _defineProperty(this, "throttledCatchup", throttle(() => this.catchup(), CATCHUP_THROTTLE, {
43
+ leading: false,
44
+ // TODO: why shouldn't this be leading?
45
+ trailing: true
46
+ }));
47
+ _defineProperty(this, "catchup", async () => {
48
+ const start = new Date().getTime();
49
+ // if the queue is already paused, we are busy with something else, so don't proceed.
50
+ if (this.stepQueue.isPaused()) {
51
+ logger(`Queue is paused. Aborting.`);
52
+ return;
53
+ }
54
+ this.stepQueue.pauseQueue();
55
+ try {
56
+ var _this$analyticsHelper;
57
+ await catchup({
58
+ getCurrentPmVersion: this.getCurrentPmVersion,
59
+ fetchCatchup: this.fetchCatchup,
60
+ getUnconfirmedSteps: this.getUnconfirmedSteps,
61
+ filterQueue: this.stepQueue.filterQueue,
62
+ updateDocumentWithMetadata: this.updateDocumentWithMetadata,
63
+ applyLocalSteps: this.applyLocalSteps
64
+ });
65
+ const latency = new Date().getTime() - start;
66
+ (_this$analyticsHelper = this.analyticsHelper) === null || _this$analyticsHelper === void 0 ? void 0 : _this$analyticsHelper.sendActionEvent(EVENT_ACTION.CATCHUP, EVENT_STATUS.SUCCESS, {
67
+ latency
68
+ });
69
+ } catch (error) {
70
+ var _this$analyticsHelper2, _this$analyticsHelper3;
71
+ const latency = new Date().getTime() - start;
72
+ (_this$analyticsHelper2 = this.analyticsHelper) === null || _this$analyticsHelper2 === void 0 ? void 0 : _this$analyticsHelper2.sendActionEvent(EVENT_ACTION.CATCHUP, EVENT_STATUS.FAILURE, {
73
+ latency
74
+ });
75
+ (_this$analyticsHelper3 = this.analyticsHelper) === null || _this$analyticsHelper3 === void 0 ? void 0 : _this$analyticsHelper3.sendErrorEvent(error, 'Error while catching up');
76
+ logger(`Catch-Up Failed:`, error.message);
77
+ } finally {
78
+ this.stepQueue.resumeQueue();
79
+ this.processQueue();
80
+ this.sendStepsFromCurrentState(); // this will eventually retry catchup as it calls commitStep which will either catchup on onStepsAdded or onErrorHandled
81
+ this.stepRejectCounter = 0;
82
+ }
83
+ });
84
+ _defineProperty(this, "getCurrentPmVersion", () => {
85
+ var _this$getState;
86
+ const state = (_this$getState = this.getState) === null || _this$getState === void 0 ? void 0 : _this$getState.call(this);
87
+ if (!state) {
88
+ var _this$analyticsHelper4;
89
+ (_this$analyticsHelper4 = this.analyticsHelper) === null || _this$analyticsHelper4 === void 0 ? void 0 : _this$analyticsHelper4.sendErrorEvent(new Error('No editor state when calling ProseMirror function'), 'getCurrentPmVersion called without state');
90
+ return 0;
91
+ }
92
+ return getVersion(state);
93
+ });
94
+ _defineProperty(this, "getCurrentState", async () => {
95
+ try {
96
+ var _this$metadata$title, _this$analyticsHelper5;
97
+ startMeasure(MEASURE_NAME.GET_CURRENT_STATE, this.analyticsHelper);
98
+
99
+ // Convert ProseMirror document in Editor state to ADF document
100
+ const state = this.getState();
101
+ const adfDocument = new JSONTransformer().encode(state.doc);
102
+ const currentState = {
103
+ content: adfDocument,
104
+ title: (_this$metadata$title = this.metadata.title) === null || _this$metadata$title === void 0 ? void 0 : _this$metadata$title.toString(),
105
+ stepVersion: getVersion(state)
106
+ };
107
+ const measure = stopMeasure(MEASURE_NAME.GET_CURRENT_STATE, this.analyticsHelper);
108
+ (_this$analyticsHelper5 = this.analyticsHelper) === null || _this$analyticsHelper5 === void 0 ? void 0 : _this$analyticsHelper5.sendActionEvent(EVENT_ACTION.GET_CURRENT_STATE, EVENT_STATUS.SUCCESS, {
109
+ latency: measure === null || measure === void 0 ? void 0 : measure.duration
110
+ });
111
+ return currentState;
112
+ } catch (error) {
113
+ var _this$analyticsHelper6, _this$analyticsHelper7;
114
+ const measure = stopMeasure(MEASURE_NAME.GET_CURRENT_STATE, this.analyticsHelper);
115
+ (_this$analyticsHelper6 = this.analyticsHelper) === null || _this$analyticsHelper6 === void 0 ? void 0 : _this$analyticsHelper6.sendActionEvent(EVENT_ACTION.GET_CURRENT_STATE, EVENT_STATUS.FAILURE, {
116
+ latency: measure === null || measure === void 0 ? void 0 : measure.duration
117
+ });
118
+ (_this$analyticsHelper7 = this.analyticsHelper) === null || _this$analyticsHelper7 === void 0 ? void 0 : _this$analyticsHelper7.sendErrorEvent(error, 'Error while returning ADF version of current draft document');
119
+ throw error; // Reject the promise so the consumer can react to it failing
120
+ }
121
+ });
122
+ _defineProperty(this, "getUnconfirmedStepsOrigins", () => {
123
+ var _this$getState2, _sendableSteps;
124
+ const state = (_this$getState2 = this.getState) === null || _this$getState2 === void 0 ? void 0 : _this$getState2.call(this);
125
+ if (!state) {
126
+ var _this$analyticsHelper8;
127
+ (_this$analyticsHelper8 = this.analyticsHelper) === null || _this$analyticsHelper8 === void 0 ? void 0 : _this$analyticsHelper8.sendErrorEvent(new Error('No editor state when calling ProseMirror function'), 'getUnconfirmedStepsOrigins called without state');
128
+ return;
129
+ }
130
+ return (_sendableSteps = sendableSteps(state)) === null || _sendableSteps === void 0 ? void 0 : _sendableSteps.origins;
131
+ });
132
+ _defineProperty(this, "getUnconfirmedSteps", () => {
133
+ var _this$getState3, _sendableSteps2;
134
+ const state = (_this$getState3 = this.getState) === null || _this$getState3 === void 0 ? void 0 : _this$getState3.call(this);
135
+ if (!state) {
136
+ var _this$analyticsHelper9;
137
+ (_this$analyticsHelper9 = this.analyticsHelper) === null || _this$analyticsHelper9 === void 0 ? void 0 : _this$analyticsHelper9.sendErrorEvent(new Error('No editor state when calling ProseMirror function'), 'getUnconfirmedSteps called without state');
138
+ return;
139
+ }
140
+ return (_sendableSteps2 = sendableSteps(state)) === null || _sendableSteps2 === void 0 ? void 0 : _sendableSteps2.steps;
141
+ });
142
+ _defineProperty(this, "applyLocalSteps", steps => {
143
+ // Re-apply local steps
144
+ this.providerEmitCallback('local-steps', {
145
+ steps
146
+ });
147
+ });
148
+ _defineProperty(this, "updateDocumentWithMetadata", ({
149
+ doc,
150
+ version,
151
+ metadata,
152
+ reserveCursor
153
+ }) => {
154
+ this.providerEmitCallback('init', {
155
+ doc,
156
+ version,
157
+ metadata,
158
+ ...(reserveCursor ? {
159
+ reserveCursor
160
+ } : {})
161
+ });
162
+ if (metadata && Object.keys(metadata).length > 0) {
163
+ this.metadata = metadata;
164
+ this.providerEmitCallback('metadata:changed', metadata);
165
+ }
166
+ });
167
+ _defineProperty(this, "onStepsAdded", data => {
168
+ logger(`Received steps`, {
169
+ steps: data.steps,
170
+ version: data.version
171
+ });
172
+ if (!data.steps) {
173
+ logger(`No steps.. waiting..`);
174
+ return;
175
+ }
176
+ try {
177
+ const currentVersion = this.getCurrentPmVersion();
178
+ const expectedVersion = currentVersion + data.steps.length;
179
+ if (data.version <= currentVersion) {
180
+ logger(`Received steps we already have. Ignoring.`);
181
+ } else if (data.version === expectedVersion) {
182
+ this.processSteps(data);
183
+ } else if (data.version > expectedVersion) {
184
+ logger(`Version too high. Expected "${expectedVersion}" but got "${data.version}. Current local version is ${currentVersion}.`);
185
+ this.stepQueue.queueSteps(data);
186
+ this.throttledCatchup();
187
+ }
188
+ this.participantsService.updateLastActive(data.steps.map(({
189
+ userId
190
+ }) => userId));
191
+ } catch (stepsAddedError) {
192
+ var _this$analyticsHelper10;
193
+ (_this$analyticsHelper10 = this.analyticsHelper) === null || _this$analyticsHelper10 === void 0 ? void 0 : _this$analyticsHelper10.sendErrorEvent(stepsAddedError, 'Error while adding steps in the provider');
194
+ this.onErrorHandled({
195
+ message: 'Error while adding steps in the provider',
196
+ data: {
197
+ status: 500,
198
+ // Meaningless, remove when we review error structure
199
+ code: INTERNAL_ERROR_CODE.ADD_STEPS_ERROR
200
+ }
201
+ });
202
+ }
203
+ });
204
+ _defineProperty(this, "onRestore", ({
205
+ doc,
206
+ version,
207
+ metadata
208
+ }) => {
209
+ // Preserve the unconfirmed steps to prevent data loss.
210
+ const unconfirmedSteps = this.getUnconfirmedSteps();
211
+ try {
212
+ var _this$analyticsHelper11;
213
+ // Reset the editor,
214
+ // - Replace the document, keep in sync with the server
215
+ // - Replace the version number, so editor is in sync with NCS server and can commit new changes.
216
+ // - Replace the metadata
217
+ // - Reserve the cursor position, in case a cursor jump.
218
+ this.updateDocumentWithMetadata({
219
+ doc,
220
+ version,
221
+ metadata,
222
+ reserveCursor: true
223
+ });
224
+
225
+ // Re-apply the unconfirmed steps, not 100% of them can be applied, if document is changed significantly.
226
+ if (unconfirmedSteps !== null && unconfirmedSteps !== void 0 && unconfirmedSteps.length) {
227
+ this.applyLocalSteps(unconfirmedSteps);
228
+ }
229
+ (_this$analyticsHelper11 = this.analyticsHelper) === null || _this$analyticsHelper11 === void 0 ? void 0 : _this$analyticsHelper11.sendActionEvent(EVENT_ACTION.REINITIALISE_DOCUMENT, EVENT_STATUS.SUCCESS, {
230
+ numUnconfirmedSteps: unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length
231
+ });
232
+ } catch (restoreError) {
233
+ var _this$analyticsHelper12, _this$analyticsHelper13;
234
+ (_this$analyticsHelper12 = this.analyticsHelper) === null || _this$analyticsHelper12 === void 0 ? void 0 : _this$analyticsHelper12.sendActionEvent(EVENT_ACTION.REINITIALISE_DOCUMENT, EVENT_STATUS.FAILURE, {
235
+ numUnconfirmedSteps: unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length
236
+ });
237
+ (_this$analyticsHelper13 = this.analyticsHelper) === null || _this$analyticsHelper13 === void 0 ? void 0 : _this$analyticsHelper13.sendErrorEvent(restoreError, 'Error while reinitialising document');
238
+ this.onErrorHandled({
239
+ message: 'Caught error while trying to recover the document',
240
+ data: {
241
+ status: 500,
242
+ // Meaningless, remove when we review error structure
243
+ code: INTERNAL_ERROR_CODE.DOCUMENT_RESTORE_ERROR
244
+ }
245
+ });
246
+ }
247
+ });
248
+ _defineProperty(this, "getFinalAcknowledgedState", async () => {
249
+ try {
250
+ var _this$analyticsHelper14;
251
+ startMeasure(MEASURE_NAME.PUBLISH_PAGE, this.analyticsHelper);
252
+ await this.commitUnconfirmedSteps();
253
+ const currentState = await this.getCurrentState();
254
+ const measure = stopMeasure(MEASURE_NAME.PUBLISH_PAGE, this.analyticsHelper);
255
+ (_this$analyticsHelper14 = this.analyticsHelper) === null || _this$analyticsHelper14 === void 0 ? void 0 : _this$analyticsHelper14.sendActionEvent(EVENT_ACTION.PUBLISH_PAGE, EVENT_STATUS.SUCCESS, {
256
+ latency: measure === null || measure === void 0 ? void 0 : measure.duration
257
+ });
258
+ return currentState;
259
+ } catch (error) {
260
+ var _this$analyticsHelper15, _this$analyticsHelper16;
261
+ const measure = stopMeasure(MEASURE_NAME.PUBLISH_PAGE, this.analyticsHelper);
262
+ (_this$analyticsHelper15 = this.analyticsHelper) === null || _this$analyticsHelper15 === void 0 ? void 0 : _this$analyticsHelper15.sendActionEvent(EVENT_ACTION.PUBLISH_PAGE, EVENT_STATUS.FAILURE, {
263
+ latency: measure === null || measure === void 0 ? void 0 : measure.duration
264
+ });
265
+ (_this$analyticsHelper16 = this.analyticsHelper) === null || _this$analyticsHelper16 === void 0 ? void 0 : _this$analyticsHelper16.sendErrorEvent(error, 'Error while returning ADF version of the final draft document');
266
+ throw error; // Reject the promise so the consumer can react to it failing
267
+ }
268
+ });
269
+ _defineProperty(this, "commitUnconfirmedSteps", async () => {
270
+ const unconfirmedSteps = this.getUnconfirmedSteps();
271
+ try {
272
+ if (unconfirmedSteps !== null && unconfirmedSteps !== void 0 && unconfirmedSteps.length) {
273
+ var _this$analyticsHelper17;
274
+ startMeasure(MEASURE_NAME.COMMIT_UNCONFIRMED_STEPS, this.analyticsHelper);
275
+ let count = 0;
276
+ // We use origins here as steps can be rebased. When steps are rebased a new step is created.
277
+ // This means that we can not track if it has been removed from the unconfirmed array or not.
278
+ // Origins points to the original transaction that the step was created in. This is never changed
279
+ // and gets passed down when a step is rebased.
280
+ const unconfirmedTrs = this.getUnconfirmedStepsOrigins();
281
+ const lastTr = unconfirmedTrs === null || unconfirmedTrs === void 0 ? void 0 : unconfirmedTrs[unconfirmedTrs.length - 1];
282
+ let isLastTrConfirmed = false;
283
+ while (!isLastTrConfirmed) {
284
+ this.sendStepsFromCurrentState();
285
+ await sleep(1000);
286
+ const nextUnconfirmedSteps = this.getUnconfirmedSteps();
287
+ if (nextUnconfirmedSteps !== null && nextUnconfirmedSteps !== void 0 && nextUnconfirmedSteps.length) {
288
+ const nextUnconfirmedTrs = this.getUnconfirmedStepsOrigins();
289
+ isLastTrConfirmed = !(nextUnconfirmedTrs !== null && nextUnconfirmedTrs !== void 0 && nextUnconfirmedTrs.some(tr => tr === lastTr));
290
+ } else {
291
+ isLastTrConfirmed = true;
292
+ }
293
+ if (!isLastTrConfirmed && count++ >= ACK_MAX_TRY) {
294
+ if (this.onSyncUpError) {
295
+ const state = this.getState();
296
+ this.onSyncUpError({
297
+ lengthOfUnconfirmedSteps: nextUnconfirmedSteps === null || nextUnconfirmedSteps === void 0 ? void 0 : nextUnconfirmedSteps.length,
298
+ tries: count,
299
+ maxRetries: ACK_MAX_TRY,
300
+ clientId: this.clientId,
301
+ version: getVersion(state)
302
+ });
303
+ }
304
+ throw new Error("Can't sync up with Collab Service");
305
+ }
306
+ }
307
+ const measure = stopMeasure(MEASURE_NAME.COMMIT_UNCONFIRMED_STEPS, this.analyticsHelper);
308
+ (_this$analyticsHelper17 = this.analyticsHelper) === null || _this$analyticsHelper17 === void 0 ? void 0 : _this$analyticsHelper17.sendActionEvent(EVENT_ACTION.COMMIT_UNCONFIRMED_STEPS, EVENT_STATUS.SUCCESS, {
309
+ latency: measure === null || measure === void 0 ? void 0 : measure.duration,
310
+ // upon success, emit the total number of unconfirmed steps we synced
311
+ numUnconfirmedSteps: unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length
312
+ });
313
+ }
314
+ } catch (error) {
315
+ var _this$analyticsHelper18, _this$analyticsHelper19;
316
+ const measure = stopMeasure(MEASURE_NAME.COMMIT_UNCONFIRMED_STEPS, this.analyticsHelper);
317
+ (_this$analyticsHelper18 = this.analyticsHelper) === null || _this$analyticsHelper18 === void 0 ? void 0 : _this$analyticsHelper18.sendActionEvent(EVENT_ACTION.COMMIT_UNCONFIRMED_STEPS, EVENT_STATUS.FAILURE, {
318
+ latency: measure === null || measure === void 0 ? void 0 : measure.duration,
319
+ numUnconfirmedSteps: unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length
320
+ });
321
+ (_this$analyticsHelper19 = this.analyticsHelper) === null || _this$analyticsHelper19 === void 0 ? void 0 : _this$analyticsHelper19.sendErrorEvent(error, 'Error while committing unconfirmed steps');
322
+ throw error;
323
+ }
324
+ });
325
+ _defineProperty(this, "onStepRejectedError", () => {
326
+ this.stepRejectCounter++;
327
+ logger(`Steps rejected (tries=${this.stepRejectCounter})`);
328
+ if (this.stepRejectCounter >= MAX_STEP_REJECTED_ERROR) {
329
+ logger(`The steps were rejected too many times (tries=${this.stepRejectCounter}, limit=${MAX_STEP_REJECTED_ERROR}). Trying to catch-up.`);
330
+ this.throttledCatchup();
331
+ } else {
332
+ // If committing steps failed try again automatically in 1s
333
+ // This makes it more likely that unconfirmed steps trigger a catch-up
334
+ // within 15s even if there is no one editing actively (or draft sync polling)
335
+ // reducing the risk of data loss at the expense of step commits
336
+ setTimeout(() => this.sendStepsFromCurrentState(), 1000);
337
+ }
338
+ });
339
+ this.participantsService = participantsService;
340
+ this.analyticsHelper = analyticsHelper;
341
+ this.fetchCatchup = fetchCatchup;
342
+ this.providerEmitCallback = providerEmitCallback;
343
+ this.broadcastMetadata = broadcastMetadata;
344
+ this.broadcast = broadcast;
345
+ this.getUserId = getUserId;
346
+ this.onErrorHandled = onErrorHandled;
347
+ this.stepQueue = new StepQueueState();
348
+ }
349
+
350
+ /**
351
+ * Called when a metadata is changed externally from other clients/backend.
352
+ */
353
+
354
+ setTitle(title, broadcast) {
355
+ if (broadcast) {
356
+ this.broadcastMetadata({
357
+ title
358
+ });
359
+ }
360
+ Object.assign(this.metadata, {
361
+ title
362
+ });
363
+ }
364
+ setEditorWidth(editorWidth, broadcast) {
365
+ if (broadcast) {
366
+ this.broadcastMetadata({
367
+ editorWidth
368
+ });
369
+ }
370
+ Object.assign(this.metadata, {
371
+ editorWidth
372
+ });
373
+ }
374
+
375
+ /**
376
+ * Updates the local metadata and broadcasts the metadata to other clients/backend.
377
+ * @param metadata
378
+ */
379
+ setMetadata(metadata) {
380
+ this.broadcastMetadata(metadata);
381
+ Object.assign(this.metadata, metadata);
382
+ }
383
+
384
+ /**
385
+ * To prevent calling catchup to often, use lodash throttle to reduce the frequency
386
+ */
387
+
388
+ processQueue() {
389
+ if (this.stepQueue.isPaused()) {
390
+ logger(`Queue is paused. Aborting.`);
391
+ return;
392
+ }
393
+ logger(`Looking for processable data.`);
394
+ if (this.stepQueue.getQueue().length > 0) {
395
+ const firstItem = this.stepQueue.shift();
396
+ const currentVersion = this.getCurrentPmVersion();
397
+ const expectedVersion = currentVersion + firstItem.steps.length;
398
+ if (firstItem.version === expectedVersion) {
399
+ logger(`Applying data from queue!`);
400
+ this.processSteps(firstItem);
401
+ // recur
402
+ this.processQueue();
403
+ }
404
+ }
405
+ }
406
+ processSteps(data) {
407
+ const {
408
+ version,
409
+ steps
410
+ } = data;
411
+ logger(`Processing data. Version "${version}".`);
412
+ if (steps !== null && steps !== void 0 && steps.length) {
413
+ try {
414
+ const clientIds = steps.map(({
415
+ clientId
416
+ }) => clientId);
417
+ this.providerEmitCallback('data', {
418
+ json: steps,
419
+ version,
420
+ userIds: clientIds
421
+ });
422
+ // If steps can apply to local editor successfully, no need to accumulate the error counter.
423
+ this.stepRejectCounter = 0;
424
+ this.participantsService.emitTelepointersFromSteps(steps, this.providerEmitCallback);
425
+
426
+ // Resend local steps if none of the received steps originated with us!
427
+ if (clientIds.indexOf(this.clientId) === -1) {
428
+ setTimeout(() => this.sendStepsFromCurrentState(), 100);
429
+ }
430
+ } catch (error) {
431
+ var _this$analyticsHelper20;
432
+ logger(`Processing steps failed with error: ${error}. Triggering catch up call.`);
433
+ (_this$analyticsHelper20 = this.analyticsHelper) === null || _this$analyticsHelper20 === void 0 ? void 0 : _this$analyticsHelper20.sendErrorEvent(error, 'Error while processing steps');
434
+ this.throttledCatchup();
435
+ }
436
+ }
437
+ }
438
+ setup({
439
+ getState,
440
+ onSyncUpError,
441
+ clientId
442
+ }) {
443
+ this.getState = getState;
444
+ this.onSyncUpError = onSyncUpError || noop;
445
+ this.clientId = clientId;
446
+ return this;
447
+ }
448
+
449
+ /**
450
+ * We can use this function to throttle/delay
451
+ * Any send steps operation
452
+ *
453
+ * The getState function will return the current EditorState
454
+ * from the EditorView.
455
+ */
456
+ sendStepsFromCurrentState() {
457
+ var _this$getState4;
458
+ const state = (_this$getState4 = this.getState) === null || _this$getState4 === void 0 ? void 0 : _this$getState4.call(this);
459
+ if (!state) {
460
+ return;
461
+ }
462
+ this.send(null, null, state);
463
+ }
464
+ /**
465
+ * Send steps from transaction to other participants
466
+ * It needs the superfluous arguments because we keep the interface of the send API the same as the Synchrony plugin
467
+ */
468
+ send(_tr, _oldState, newState) {
469
+ const unconfirmedStepsData = sendableSteps(newState);
470
+ const version = getVersion(newState);
471
+
472
+ // Don't send any steps before we're ready.
473
+ if (!unconfirmedStepsData) {
474
+ return;
475
+ }
476
+ const unconfirmedSteps = unconfirmedStepsData.steps;
477
+ if (!(unconfirmedSteps !== null && unconfirmedSteps !== void 0 && unconfirmedSteps.length)) {
478
+ return;
479
+ }
480
+
481
+ // Avoid reference issues using a
482
+ // method outside of the provider
483
+ // scope
484
+ throttledCommitStep({
485
+ broadcast: this.broadcast,
486
+ userId: this.getUserId(),
487
+ clientId: this.clientId,
488
+ steps: unconfirmedSteps,
489
+ version,
490
+ onStepsAdded: this.onStepsAdded,
491
+ onErrorHandled: this.onErrorHandled,
492
+ analyticsHelper: this.analyticsHelper
493
+ });
494
+ }
495
+ }
@@ -0,0 +1,30 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import { createLogger } from '../helpers/utils';
3
+ const logger = createLogger('documentService-queue', 'black');
4
+ export class StepQueueState {
5
+ constructor() {
6
+ _defineProperty(this, "queuePaused", false);
7
+ _defineProperty(this, "queue", []);
8
+ _defineProperty(this, "getQueue", () => {
9
+ return this.queue;
10
+ });
11
+ _defineProperty(this, "filterQueue", condition => {
12
+ this.queue = this.queue.filter(condition);
13
+ });
14
+ _defineProperty(this, "isPaused", () => this.queuePaused);
15
+ _defineProperty(this, "pauseQueue", () => {
16
+ this.queuePaused = true;
17
+ });
18
+ _defineProperty(this, "resumeQueue", () => {
19
+ this.queuePaused = false;
20
+ });
21
+ _defineProperty(this, "shift", () => {
22
+ return this.queue.shift();
23
+ });
24
+ }
25
+ queueSteps(data) {
26
+ logger(`Queueing data for version "${data.version}".`);
27
+ const orderedQueue = [...this.queue, data].sort((a, b) => a.version > b.version ? 1 : -1);
28
+ this.queue = orderedQueue;
29
+ }
30
+ }
@@ -0,0 +1,102 @@
1
+ import { NCS_ERROR_CODE } from './error-types';
2
+ import { INTERNAL_ERROR_CODE, PROVIDER_ERROR_CODE } from './error-types';
3
+
4
+ /*
5
+ * Maps internal collab provider errors to an emitted error format
6
+ */
7
+ export const errorCodeMapper = error => {
8
+ var _error$data, _error$data2, _error$data3;
9
+ switch ((_error$data = error.data) === null || _error$data === void 0 ? void 0 : _error$data.code) {
10
+ case NCS_ERROR_CODE.HEAD_VERSION_UPDATE_FAILED:
11
+ case NCS_ERROR_CODE.VERSION_NUMBER_ALREADY_EXISTS:
12
+ // This should never be called with these errors
13
+ return;
14
+ case INTERNAL_ERROR_CODE.ADD_STEPS_ERROR:
15
+ case INTERNAL_ERROR_CODE.RECONNECTION_ERROR:
16
+ case INTERNAL_ERROR_CODE.CONNECTION_ERROR:
17
+ // These errors shouldn't be emitted, we're hoping the provider self-recovers over time
18
+ return;
19
+ case NCS_ERROR_CODE.INSUFFICIENT_EDITING_PERMISSION:
20
+ case INTERNAL_ERROR_CODE.TOKEN_PERMISSION_ERROR:
21
+ return {
22
+ code: PROVIDER_ERROR_CODE.NO_PERMISSION_ERROR,
23
+ message: 'User does not have permissions to access this document or document is not found',
24
+ reason: error.data.meta.reason,
25
+ recoverable: true,
26
+ status: 403
27
+ };
28
+ case NCS_ERROR_CODE.FORBIDDEN_USER_TOKEN:
29
+ return {
30
+ code: PROVIDER_ERROR_CODE.INVALID_USER_TOKEN,
31
+ message: 'The user token was invalid',
32
+ recoverable: true,
33
+ status: 403
34
+ };
35
+ case INTERNAL_ERROR_CODE.DOCUMENT_NOT_FOUND:
36
+ return {
37
+ code: PROVIDER_ERROR_CODE.DOCUMENT_NOT_FOUND,
38
+ message: 'The requested document is not found',
39
+ recoverable: true,
40
+ status: 404
41
+ };
42
+ case NCS_ERROR_CODE.TENANT_INSTANCE_MAINTENANCE:
43
+ case NCS_ERROR_CODE.LOCKED_DOCUMENT:
44
+ return {
45
+ code: PROVIDER_ERROR_CODE.LOCKED,
46
+ message: 'The document is currently not available, please try again later',
47
+ recoverable: true
48
+ };
49
+ case NCS_ERROR_CODE.DYNAMO_ERROR:
50
+ return {
51
+ code: PROVIDER_ERROR_CODE.FAIL_TO_SAVE,
52
+ message: 'Collab service is not able to save changes',
53
+ recoverable: false,
54
+ status: 500
55
+ };
56
+ case INTERNAL_ERROR_CODE.DOCUMENT_RESTORE_ERROR:
57
+ return {
58
+ code: PROVIDER_ERROR_CODE.DOCUMENT_RESTORE_ERROR,
59
+ message: 'Collab service unable to restore document',
60
+ recoverable: false,
61
+ status: 500
62
+ };
63
+ case NCS_ERROR_CODE.INIT_DATA_LOAD_FAILED:
64
+ return {
65
+ code: PROVIDER_ERROR_CODE.INITIALISATION_ERROR,
66
+ message: "The initial document couldn't be loaded from the collab service",
67
+ recoverable: false,
68
+ status: 500
69
+ };
70
+ case INTERNAL_ERROR_CODE.RECONNECTION_NETWORK_ISSUE:
71
+ return {
72
+ code: PROVIDER_ERROR_CODE.NETWORK_ISSUE,
73
+ message: "Couldn't reconnect to the collab service due to network issues",
74
+ recoverable: true,
75
+ status: 500
76
+ };
77
+ case NCS_ERROR_CODE.NAMESPACE_INVALID:
78
+ case NCS_ERROR_CODE.INVALID_ACTIVATION_ID:
79
+ case NCS_ERROR_CODE.INVALID_DOCUMENT_ARI:
80
+ case NCS_ERROR_CODE.INVALID_CLOUD_ID:
81
+ return {
82
+ code: PROVIDER_ERROR_CODE.INVALID_PROVIDER_CONFIGURATION,
83
+ message: 'Invalid provider configuration',
84
+ recoverable: false,
85
+ reason: (_error$data2 = error.data) === null || _error$data2 === void 0 ? void 0 : _error$data2.code,
86
+ status: 400
87
+ };
88
+ case NCS_ERROR_CODE.NAMESPACE_NOT_FOUND:
89
+ case NCS_ERROR_CODE.ERROR_MAPPING_ERROR:
90
+ case NCS_ERROR_CODE.EMPTY_BROADCAST:
91
+ case INTERNAL_ERROR_CODE.CATCHUP_FAILED:
92
+ return {
93
+ code: PROVIDER_ERROR_CODE.INTERNAL_SERVICE_ERROR,
94
+ message: 'Collab Provider experienced an unrecoverable error',
95
+ recoverable: false,
96
+ reason: (_error$data3 = error.data) === null || _error$data3 === void 0 ? void 0 : _error$data3.code,
97
+ status: 500
98
+ };
99
+ default:
100
+ return;
101
+ }
102
+ };