@atlaskit/collab-provider 8.4.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.
- package/CHANGELOG.md +34 -0
- package/dist/cjs/analytics/{index.js → analytics-helper.js} +43 -4
- package/dist/cjs/analytics/performance.js +6 -5
- package/dist/cjs/channel.js +222 -224
- package/dist/cjs/{provider → document}/catchup.js +2 -2
- package/dist/cjs/document/document-service.js +617 -0
- package/dist/cjs/document/step-queue-state.js +51 -0
- package/dist/cjs/errors/error-code-mapper.js +86 -67
- package/dist/cjs/errors/error-types.js +251 -21
- package/dist/cjs/helpers/utils.js +1 -12
- package/dist/cjs/participants/participants-helper.js +51 -0
- package/dist/cjs/participants/participants-service.js +217 -0
- package/dist/cjs/participants/participants-state.js +53 -0
- package/dist/cjs/{provider/telepointers.js → participants/telepointers-helper.js} +6 -6
- package/dist/cjs/provider/commit-step.js +4 -4
- package/dist/cjs/provider/index.js +212 -774
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/version-wrapper.js +1 -1
- package/dist/cjs/version.json +1 -1
- package/dist/es2019/analytics/{index.js → analytics-helper.js} +15 -4
- package/dist/es2019/analytics/performance.js +5 -6
- package/dist/es2019/channel.js +123 -116
- package/dist/es2019/{provider → document}/catchup.js +2 -2
- package/dist/es2019/document/document-service.js +495 -0
- package/dist/es2019/document/step-queue-state.js +30 -0
- package/dist/es2019/errors/error-code-mapper.js +87 -63
- package/dist/es2019/errors/error-types.js +143 -5
- package/dist/es2019/helpers/utils.js +0 -10
- package/dist/es2019/participants/participants-helper.js +25 -0
- package/dist/es2019/participants/participants-service.js +166 -0
- package/dist/es2019/participants/participants-state.js +28 -0
- package/dist/es2019/{provider/telepointers.js → participants/telepointers-helper.js} +2 -2
- package/dist/es2019/provider/commit-step.js +4 -4
- package/dist/es2019/provider/index.js +162 -637
- package/dist/es2019/types.js +4 -0
- package/dist/es2019/version-wrapper.js +1 -1
- package/dist/es2019/version.json +1 -1
- package/dist/esm/analytics/{index.js → analytics-helper.js} +43 -4
- package/dist/esm/analytics/performance.js +5 -6
- package/dist/esm/channel.js +224 -226
- package/dist/esm/{provider → document}/catchup.js +2 -2
- package/dist/esm/document/document-service.js +609 -0
- package/dist/esm/document/step-queue-state.js +43 -0
- package/dist/esm/errors/error-code-mapper.js +87 -64
- package/dist/esm/errors/error-types.js +243 -18
- package/dist/esm/helpers/utils.js +0 -10
- package/dist/esm/participants/participants-helper.js +43 -0
- package/dist/esm/participants/participants-service.js +209 -0
- package/dist/esm/participants/participants-state.js +45 -0
- package/dist/esm/{provider/telepointers.js → participants/telepointers-helper.js} +4 -4
- package/dist/esm/provider/commit-step.js +4 -4
- package/dist/esm/provider/index.js +212 -774
- package/dist/esm/types.js +4 -0
- package/dist/esm/version-wrapper.js +1 -1
- package/dist/esm/version.json +1 -1
- package/dist/types/analytics/{index.d.ts → analytics-helper.d.ts} +3 -1
- package/dist/types/analytics/performance.d.ts +3 -1
- package/dist/types/analytics/ufo.d.ts +1 -1
- package/dist/types/channel.d.ts +12 -5
- package/dist/types/document/document-service.d.ts +105 -0
- package/dist/types/document/step-queue-state.d.ts +16 -0
- package/dist/types/errors/error-code-mapper.d.ts +2 -36
- package/dist/types/errors/error-types.d.ts +439 -4
- package/dist/types/helpers/const.d.ts +2 -2
- package/dist/types/helpers/utils.d.ts +0 -6
- package/dist/types/index.d.ts +2 -1
- package/dist/types/participants/participants-helper.d.ts +15 -0
- package/dist/types/participants/participants-service.d.ts +70 -0
- package/dist/types/participants/participants-state.d.ts +13 -0
- package/dist/types/participants/telepointers-helper.d.ts +4 -0
- package/dist/types/provider/commit-step.d.ts +6 -6
- package/dist/types/provider/index.d.ts +80 -73
- package/dist/types/types.d.ts +47 -30
- package/package.json +4 -4
- package/report.api.md +172 -19
- package/dist/types/provider/telepointers.d.ts +0 -5
- /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
|
+
}
|