@atlaskit/collab-provider 10.10.0 → 10.10.2
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 +12 -0
- package/dist/cjs/document/document-service.js +14 -37
- package/dist/cjs/document/getConflictChanges.js +19 -4
- package/dist/cjs/provider/commit-step.js +60 -55
- package/dist/cjs/version-wrapper.js +1 -1
- package/dist/es2019/document/document-service.js +19 -30
- package/dist/es2019/document/getConflictChanges.js +19 -4
- package/dist/es2019/provider/commit-step.js +57 -52
- package/dist/es2019/version-wrapper.js +1 -1
- package/dist/esm/document/document-service.js +14 -37
- package/dist/esm/document/getConflictChanges.js +19 -4
- package/dist/esm/provider/commit-step.js +59 -54
- package/dist/esm/version-wrapper.js +1 -1
- package/dist/types/provider/commit-step.d.ts +1 -0
- package/dist/types/types.d.ts +2 -0
- package/dist/types-ts4.5/provider/commit-step.d.ts +1 -0
- package/dist/types-ts4.5/types.d.ts +2 -0
- package/package.json +5 -8
package/CHANGELOG.md
CHANGED
|
@@ -375,11 +375,7 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
|
|
|
375
375
|
reserveCursor: true
|
|
376
376
|
});
|
|
377
377
|
_this.metadataService.updateMetadata(metadata);
|
|
378
|
-
|
|
379
|
-
_context3.next = 27;
|
|
380
|
-
break;
|
|
381
|
-
}
|
|
382
|
-
_context3.prev = 14;
|
|
378
|
+
_context3.prev = 13;
|
|
383
379
|
(_this$analyticsHelper13 = _this.analyticsHelper) === null || _this$analyticsHelper13 === void 0 || _this$analyticsHelper13.sendActionEvent(_const.EVENT_ACTION.REINITIALISE_DOCUMENT, _const.EVENT_STATUS.INFO, {
|
|
384
380
|
numUnconfirmedSteps: unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length,
|
|
385
381
|
obfuscatedSteps: obfuscatedSteps,
|
|
@@ -392,35 +388,16 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
|
|
|
392
388
|
if (unconfirmedSteps !== null && unconfirmedSteps !== void 0 && unconfirmedSteps.length) {
|
|
393
389
|
_this.applyLocalSteps(unconfirmedSteps);
|
|
394
390
|
}
|
|
395
|
-
_context3.next =
|
|
391
|
+
_context3.next = 24;
|
|
396
392
|
break;
|
|
397
|
-
case
|
|
398
|
-
_context3.prev =
|
|
399
|
-
_context3.t0 = _context3["catch"](
|
|
393
|
+
case 18:
|
|
394
|
+
_context3.prev = 18;
|
|
395
|
+
_context3.t0 = _context3["catch"](13);
|
|
400
396
|
(_this$analyticsHelper14 = _this.analyticsHelper) === null || _this$analyticsHelper14 === void 0 || _this$analyticsHelper14.sendErrorEvent(_context3.t0, "Error while onRestore with applyLocalSteps. Will fallback to fetchReconcile");
|
|
401
397
|
useReconcile = true;
|
|
402
|
-
_context3.next =
|
|
398
|
+
_context3.next = 24;
|
|
403
399
|
return _this.fetchReconcile(JSON.stringify(currentState.content), 'fe-restore');
|
|
404
|
-
case
|
|
405
|
-
_context3.next = 33;
|
|
406
|
-
break;
|
|
407
|
-
case 27:
|
|
408
|
-
if (!(useReconcile && currentState)) {
|
|
409
|
-
_context3.next = 32;
|
|
410
|
-
break;
|
|
411
|
-
}
|
|
412
|
-
_context3.next = 30;
|
|
413
|
-
return _this.fetchReconcile(JSON.stringify(currentState.content), 'fe-restore');
|
|
414
|
-
case 30:
|
|
415
|
-
_context3.next = 33;
|
|
416
|
-
break;
|
|
417
|
-
case 32:
|
|
418
|
-
if (unconfirmedSteps !== null && unconfirmedSteps !== void 0 && unconfirmedSteps.length) {
|
|
419
|
-
// we don't want to use reconcile for restore triggered by catchup client out of sync (when targetClientId is provided)
|
|
420
|
-
// as this results in all changes made while the client was out of sync being lost
|
|
421
|
-
_this.applyLocalSteps(unconfirmedSteps);
|
|
422
|
-
}
|
|
423
|
-
case 33:
|
|
400
|
+
case 24:
|
|
424
401
|
(_this$analyticsHelper15 = _this.analyticsHelper) === null || _this$analyticsHelper15 === void 0 || _this$analyticsHelper15.sendActionEvent(_const.EVENT_ACTION.REINITIALISE_DOCUMENT, _const.EVENT_STATUS.SUCCESS, {
|
|
425
402
|
numUnconfirmedSteps: unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length,
|
|
426
403
|
hasTitle: !!(metadata !== null && metadata !== void 0 && metadata.title),
|
|
@@ -429,10 +406,10 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
|
|
|
429
406
|
targetClientId: targetClientId,
|
|
430
407
|
triggeredByCatchup: !!targetClientId
|
|
431
408
|
});
|
|
432
|
-
_context3.next =
|
|
409
|
+
_context3.next = 32;
|
|
433
410
|
break;
|
|
434
|
-
case
|
|
435
|
-
_context3.prev =
|
|
411
|
+
case 27:
|
|
412
|
+
_context3.prev = 27;
|
|
436
413
|
_context3.t1 = _context3["catch"](10);
|
|
437
414
|
(_this$analyticsHelper16 = _this.analyticsHelper) === null || _this$analyticsHelper16 === void 0 || _this$analyticsHelper16.sendActionEvent(_const.EVENT_ACTION.REINITIALISE_DOCUMENT, _const.EVENT_STATUS.FAILURE, {
|
|
438
415
|
numUnconfirmedSteps: unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length,
|
|
@@ -450,11 +427,11 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
|
|
|
450
427
|
code: _internalErrors.INTERNAL_ERROR_CODE.DOCUMENT_RESTORE_ERROR
|
|
451
428
|
}
|
|
452
429
|
});
|
|
453
|
-
case
|
|
430
|
+
case 32:
|
|
454
431
|
case "end":
|
|
455
432
|
return _context3.stop();
|
|
456
433
|
}
|
|
457
|
-
}, _callee3, null, [[10,
|
|
434
|
+
}, _callee3, null, [[10, 27], [13, 18]]);
|
|
458
435
|
}));
|
|
459
436
|
return function (_x3) {
|
|
460
437
|
return _ref7.apply(this, arguments);
|
|
@@ -640,8 +617,8 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
|
|
|
640
617
|
_context5.next = 22;
|
|
641
618
|
break;
|
|
642
619
|
}
|
|
643
|
-
//
|
|
644
|
-
_this.sendStepsFromCurrentState(undefined,
|
|
620
|
+
// this makes all commitUnconfirmedSteps skip the waiting time, which means draft-sync is sped up too.
|
|
621
|
+
_this.sendStepsFromCurrentState(undefined, 'publish');
|
|
645
622
|
_context5.next = 13;
|
|
646
623
|
return (0, _utils.sleep)(500);
|
|
647
624
|
case 13:
|
|
@@ -58,12 +58,17 @@ var getConflicts = function getConflicts(_ref) {
|
|
|
58
58
|
// Partial overlap with the end
|
|
59
59
|
localChange.fromA >= remoteChange.fromA && localChange.fromA < remoteChange.toA && localChange.toA > remoteChange.toA ||
|
|
60
60
|
// Partial overlap with the start
|
|
61
|
-
localChange.fromA < remoteChange.fromA && localChange.toA > remoteChange.fromA && localChange.toA <= remoteChange.toA
|
|
61
|
+
localChange.fromA < remoteChange.fromA && localChange.toA > remoteChange.fromA && localChange.toA <= remoteChange.toA ||
|
|
62
|
+
// One of the edges match
|
|
63
|
+
localChange.fromA === remoteChange.toA || remoteChange.fromA === localChange.toA) {
|
|
64
|
+
var remoteSlice = remoteDoc.slice(remoteChange.fromB, remoteChange.toB);
|
|
65
|
+
var isDeletion = remoteSlice.size === 0;
|
|
62
66
|
conflictingChanges.push({
|
|
63
67
|
from: Math.min(localChange.fromA, remoteChange.fromA),
|
|
64
68
|
to: Math.max(localChange.toA, remoteChange.toA),
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
// We only want to capture the exact slice deleted (without parents) so we can display and insert as expected
|
|
70
|
+
local: localDoc.slice(localChange.fromB, localChange.toB, !isDeletion),
|
|
71
|
+
remote: remoteSlice
|
|
67
72
|
});
|
|
68
73
|
}
|
|
69
74
|
});
|
|
@@ -156,6 +161,9 @@ function getConflictChanges(_ref3) {
|
|
|
156
161
|
var isConflictChange = function isConflictChange(value) {
|
|
157
162
|
return Boolean(value);
|
|
158
163
|
};
|
|
164
|
+
|
|
165
|
+
// Prevent duplicate ranges occuring
|
|
166
|
+
var seenInsertions = new Set();
|
|
159
167
|
return {
|
|
160
168
|
inserted: conflictingChanges.filter(function (i) {
|
|
161
169
|
return i.remote.size !== 0;
|
|
@@ -164,7 +172,14 @@ function getConflictChanges(_ref3) {
|
|
|
164
172
|
from: mapping.map(i.from, -1),
|
|
165
173
|
to: mapping.map(i.to)
|
|
166
174
|
});
|
|
167
|
-
}).filter(
|
|
175
|
+
}).filter(function (i) {
|
|
176
|
+
var identifier = "".concat(i.from, "-").concat(i.to);
|
|
177
|
+
if (seenInsertions.has(identifier)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
seenInsertions.add(identifier);
|
|
181
|
+
return isConflictChange(i);
|
|
182
|
+
}),
|
|
168
183
|
deleted: conflictingChanges.filter(function (d) {
|
|
169
184
|
return d.remote.size === 0;
|
|
170
185
|
}).map(function (d) {
|
|
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", {
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
|
-
exports.readyToCommit = exports.commitStepQueue = exports.RESET_READYTOCOMMIT_INTERVAL_MS = void 0;
|
|
7
|
+
exports.readyToCommit = exports.lastBroadcastRequestAcked = exports.commitStepQueue = exports.RESET_READYTOCOMMIT_INTERVAL_MS = void 0;
|
|
8
8
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
9
9
|
var _countBy = _interopRequireDefault(require("lodash/countBy"));
|
|
10
10
|
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
|
|
@@ -16,6 +16,7 @@ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbol
|
|
|
16
16
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
17
17
|
var logger = (0, _utils.createLogger)('commit-step', 'black');
|
|
18
18
|
var readyToCommit = exports.readyToCommit = true;
|
|
19
|
+
var lastBroadcastRequestAcked = exports.lastBroadcastRequestAcked = true;
|
|
19
20
|
var RESET_READYTOCOMMIT_INTERVAL_MS = exports.RESET_READYTOCOMMIT_INTERVAL_MS = 5000;
|
|
20
21
|
var commitStepQueue = exports.commitStepQueue = function commitStepQueue(_ref) {
|
|
21
22
|
var broadcast = _ref.broadcast,
|
|
@@ -31,14 +32,27 @@ var commitStepQueue = exports.commitStepQueue = function commitStepQueue(_ref) {
|
|
|
31
32
|
hasRecovered = _ref.hasRecovered,
|
|
32
33
|
collabMode = _ref.collabMode,
|
|
33
34
|
reason = _ref.reason;
|
|
35
|
+
// this timer is for waiting to send the next batch in between acks from the BE
|
|
36
|
+
var commitWaitTimer;
|
|
37
|
+
// if publishing and not waiting for an ACK, then clear the commit timer and proceed, skipping the timer
|
|
38
|
+
if (reason === 'publish' && lastBroadcastRequestAcked) {
|
|
39
|
+
if ((0, _platformFeatureFlags.fg)('skip_collab_provider_delay_on_publish')) {
|
|
40
|
+
clearTimeout(commitWaitTimer);
|
|
41
|
+
exports.readyToCommit = readyToCommit = true;
|
|
42
|
+
} // no-op if fg is turned off
|
|
43
|
+
}
|
|
34
44
|
if (!readyToCommit) {
|
|
35
45
|
logger('Not ready to commit, skip');
|
|
36
46
|
return;
|
|
37
47
|
}
|
|
38
48
|
// Block other sending request, before ACK
|
|
39
49
|
exports.readyToCommit = readyToCommit = false;
|
|
40
|
-
|
|
50
|
+
exports.lastBroadcastRequestAcked = lastBroadcastRequestAcked = false;
|
|
51
|
+
|
|
52
|
+
// this timer is a fallback for if an ACK from BE is lost - stop the queue from getting indefinitely locked
|
|
53
|
+
var fallbackTimer = setTimeout(function () {
|
|
41
54
|
exports.readyToCommit = readyToCommit = true;
|
|
55
|
+
exports.lastBroadcastRequestAcked = lastBroadcastRequestAcked = true;
|
|
42
56
|
logger('reset readyToCommit by timer');
|
|
43
57
|
}, RESET_READYTOCOMMIT_INTERVAL_MS);
|
|
44
58
|
var stepsWithClientAndUserId = steps.map(function (step) {
|
|
@@ -58,7 +72,6 @@ var commitStepQueue = exports.commitStepQueue = function commitStepQueue(_ref) {
|
|
|
58
72
|
// - is setup for last write wins,
|
|
59
73
|
// - and is just a boolean -- so no real risk of data loss.
|
|
60
74
|
if (__livePage && (0, _platformFeatureFlags.fg)('platform.editor.live-pages-expand-divergence')) {
|
|
61
|
-
// @atlaskit/platform-feature-flags
|
|
62
75
|
stepsWithClientAndUserId = stepsWithClientAndUserId.map(function (step) {
|
|
63
76
|
if (isExpandChangeStep(step)) {
|
|
64
77
|
// The title is also updated via this step, which we do want to send to the server.
|
|
@@ -94,66 +107,35 @@ var commitStepQueue = exports.commitStepQueue = function commitStepQueue(_ref) {
|
|
|
94
107
|
version: version,
|
|
95
108
|
userId: userId
|
|
96
109
|
}, function (response) {
|
|
110
|
+
exports.lastBroadcastRequestAcked = lastBroadcastRequestAcked = true;
|
|
97
111
|
var latency = new Date().getTime() - start;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
if (latency < 400) {
|
|
115
|
-
setTimeout(function () {
|
|
116
|
-
exports.readyToCommit = readyToCommit = true;
|
|
117
|
-
logger('reset readyToCommit');
|
|
118
|
-
}, 100);
|
|
119
|
-
} else {
|
|
120
|
-
exports.readyToCommit = readyToCommit = true;
|
|
121
|
-
logger('reset readyToCommit');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
112
|
+
// this most closely replicates the BE ack delay behaviour. 500ms hardcoded + 180ms network delay (tested on hello)
|
|
113
|
+
// more context: https://hello.atlassian.net/wiki/spaces/CEPS/pages/5020556010/Spike+Moving+BE+delay+to+the+FE
|
|
114
|
+
// to be switched over to backpressure delay sent from the BE in https://hello.jira.atlassian.cloud/browse/CEPS-1030
|
|
115
|
+
var delay = latency < 680 ? 680 - latency : 1;
|
|
116
|
+
if (response.delay) {
|
|
117
|
+
delay = response.delay;
|
|
118
|
+
} // if backpressure delay is sent, overwrite it
|
|
119
|
+
|
|
120
|
+
clearTimeout(fallbackTimer); // clear the fallback timer, ack was successfully sent/recieved
|
|
121
|
+
commitWaitTimer = setTimeout(function () {
|
|
122
|
+
// unlock the queue after waiting for delay
|
|
123
|
+
exports.readyToCommit = readyToCommit = true;
|
|
124
|
+
logger('reset readyToCommit');
|
|
125
|
+
}, delay);
|
|
125
126
|
if (response.type === _types.AcknowledgementResponseTypes.SUCCESS) {
|
|
126
127
|
onStepsAdded({
|
|
127
128
|
steps: stepsWithClientAndUserId,
|
|
128
129
|
version: response.version
|
|
129
130
|
});
|
|
130
|
-
|
|
131
|
-
if (Math.random() < 0.1) {
|
|
132
|
-
analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(_const.EVENT_ACTION.ADD_STEPS, _const.EVENT_STATUS.SUCCESS_10x_SAMPLED, {
|
|
133
|
-
type: _const.ADD_STEPS_TYPE.ACCEPTED,
|
|
134
|
-
latency: latency,
|
|
135
|
-
stepType: (0, _countBy.default)(stepsWithClientAndUserId,
|
|
136
|
-
// Ignored via go/ees005
|
|
137
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
138
|
-
function (stepWithClientAndUserId) {
|
|
139
|
-
return stepWithClientAndUserId.stepType;
|
|
140
|
-
})
|
|
141
|
-
});
|
|
142
|
-
}
|
|
131
|
+
sendSuccessAnalytics(latency, stepsWithClientAndUserId, analyticsHelper);
|
|
143
132
|
emit('commit-status', {
|
|
144
133
|
status: 'success',
|
|
145
134
|
version: response.version
|
|
146
135
|
});
|
|
147
136
|
} else if (response.type === _types.AcknowledgementResponseTypes.ERROR) {
|
|
148
137
|
onErrorHandled(response.error);
|
|
149
|
-
|
|
150
|
-
// User tried committing steps but they were rejected because:
|
|
151
|
-
// - HEAD_VERSION_UPDATE_FAILED: the collab service's latest stored step tail version didn't correspond to the head version of the first step submitted
|
|
152
|
-
// - VERSION_NUMBER_ALREADY_EXISTS: while storing the steps there was a conflict meaning someone else wrote steps into the database more quickly
|
|
153
|
-
type: response.error.data.code === _ncsErrors.NCS_ERROR_CODE.HEAD_VERSION_UPDATE_FAILED || response.error.data.code === _ncsErrors.NCS_ERROR_CODE.VERSION_NUMBER_ALREADY_EXISTS ? _const.ADD_STEPS_TYPE.REJECTED : _const.ADD_STEPS_TYPE.ERROR,
|
|
154
|
-
latency: latency
|
|
155
|
-
});
|
|
156
|
-
analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendErrorEvent(response.error, 'Error while adding steps - Acknowledgement Error');
|
|
138
|
+
sendFailureAnalytics(response, latency, analyticsHelper);
|
|
157
139
|
emit('commit-status', {
|
|
158
140
|
status: 'failure',
|
|
159
141
|
version: version
|
|
@@ -169,10 +151,8 @@ var commitStepQueue = exports.commitStepQueue = function commitStepQueue(_ref) {
|
|
|
169
151
|
}
|
|
170
152
|
});
|
|
171
153
|
} catch (error) {
|
|
172
|
-
// if the broadcast failed
|
|
173
|
-
|
|
174
|
-
exports.readyToCommit = readyToCommit = true;
|
|
175
|
-
}
|
|
154
|
+
// if the broadcast failed for any reason, we shouldn't keep the queue locked as the BE has not recieved any message
|
|
155
|
+
exports.readyToCommit = readyToCommit = true;
|
|
176
156
|
analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendErrorEvent(error, 'Error while adding steps - Broadcast threw exception');
|
|
177
157
|
emit('commit-status', {
|
|
178
158
|
status: 'failure',
|
|
@@ -187,4 +167,29 @@ function isExpandChangeStep(step) {
|
|
|
187
167
|
return true;
|
|
188
168
|
}
|
|
189
169
|
return false;
|
|
170
|
+
}
|
|
171
|
+
function sendSuccessAnalytics(latency, stepsWithClientAndUserId, analyticsHelper) {
|
|
172
|
+
// Sample only 10% of add steps events to avoid overwhelming the analytics
|
|
173
|
+
if (Math.random() < 0.1) {
|
|
174
|
+
analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(_const.EVENT_ACTION.ADD_STEPS, _const.EVENT_STATUS.SUCCESS_10x_SAMPLED, {
|
|
175
|
+
type: _const.ADD_STEPS_TYPE.ACCEPTED,
|
|
176
|
+
latency: latency,
|
|
177
|
+
stepType: (0, _countBy.default)(stepsWithClientAndUserId,
|
|
178
|
+
// Ignored via go/ees005
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
180
|
+
function (stepWithClientAndUserId) {
|
|
181
|
+
return stepWithClientAndUserId.stepType;
|
|
182
|
+
})
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function sendFailureAnalytics(response, latency, analyticsHelper) {
|
|
187
|
+
analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(_const.EVENT_ACTION.ADD_STEPS, _const.EVENT_STATUS.FAILURE, {
|
|
188
|
+
// User tried committing steps but they were rejected because:
|
|
189
|
+
// - HEAD_VERSION_UPDATE_FAILED: the collab service's latest stored step tail version didn't correspond to the head version of the first step submitted
|
|
190
|
+
// - VERSION_NUMBER_ALREADY_EXISTS: while storing the steps there was a conflict meaning someone else wrote steps into the database more quickly
|
|
191
|
+
type: response.error.data.code === _ncsErrors.NCS_ERROR_CODE.HEAD_VERSION_UPDATE_FAILED || response.error.data.code === _ncsErrors.NCS_ERROR_CODE.VERSION_NUMBER_ALREADY_EXISTS ? _const.ADD_STEPS_TYPE.REJECTED : _const.ADD_STEPS_TYPE.ERROR,
|
|
192
|
+
latency: latency
|
|
193
|
+
});
|
|
194
|
+
analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendErrorEvent(response.error, 'Error while adding steps - Acknowledgement Error');
|
|
190
195
|
}
|
|
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.version = exports.nextMajorVersion = exports.name = void 0;
|
|
7
7
|
var name = exports.name = "@atlaskit/collab-provider";
|
|
8
|
-
var version = exports.version = "10.10.
|
|
8
|
+
var version = exports.version = "10.10.2";
|
|
9
9
|
var nextMajorVersion = exports.nextMajorVersion = function nextMajorVersion() {
|
|
10
10
|
return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
|
|
11
11
|
};
|
|
@@ -339,36 +339,25 @@ export class DocumentService {
|
|
|
339
339
|
reserveCursor: true
|
|
340
340
|
});
|
|
341
341
|
this.metadataService.updateMetadata(metadata);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (unconfirmedSteps !== null && unconfirmedSteps !== void 0 && unconfirmedSteps.length) {
|
|
355
|
-
this.applyLocalSteps(unconfirmedSteps);
|
|
356
|
-
}
|
|
357
|
-
} catch (applyLocalStepsError) {
|
|
358
|
-
var _this$analyticsHelper14;
|
|
359
|
-
(_this$analyticsHelper14 = this.analyticsHelper) === null || _this$analyticsHelper14 === void 0 ? void 0 : _this$analyticsHelper14.sendErrorEvent(applyLocalStepsError, `Error while onRestore with applyLocalSteps. Will fallback to fetchReconcile`);
|
|
360
|
-
useReconcile = true;
|
|
361
|
-
await this.fetchReconcile(JSON.stringify(currentState.content), 'fe-restore');
|
|
362
|
-
}
|
|
363
|
-
} else {
|
|
364
|
-
// If there are unconfirmed steps, attempt to reconcile our current state with with recovered document
|
|
365
|
-
if (useReconcile && currentState) {
|
|
366
|
-
await this.fetchReconcile(JSON.stringify(currentState.content), 'fe-restore');
|
|
367
|
-
} else if (unconfirmedSteps !== null && unconfirmedSteps !== void 0 && unconfirmedSteps.length) {
|
|
368
|
-
// we don't want to use reconcile for restore triggered by catchup client out of sync (when targetClientId is provided)
|
|
369
|
-
// as this results in all changes made while the client was out of sync being lost
|
|
342
|
+
try {
|
|
343
|
+
var _this$analyticsHelper13;
|
|
344
|
+
(_this$analyticsHelper13 = this.analyticsHelper) === null || _this$analyticsHelper13 === void 0 ? void 0 : _this$analyticsHelper13.sendActionEvent(EVENT_ACTION.REINITIALISE_DOCUMENT, EVENT_STATUS.INFO, {
|
|
345
|
+
numUnconfirmedSteps: unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length,
|
|
346
|
+
obfuscatedSteps,
|
|
347
|
+
obfuscatedDoc,
|
|
348
|
+
hasTitle: !!(metadata !== null && metadata !== void 0 && metadata.title),
|
|
349
|
+
clientId: this.clientId,
|
|
350
|
+
targetClientId,
|
|
351
|
+
triggeredByCatchup: !!targetClientId
|
|
352
|
+
});
|
|
353
|
+
if (unconfirmedSteps !== null && unconfirmedSteps !== void 0 && unconfirmedSteps.length) {
|
|
370
354
|
this.applyLocalSteps(unconfirmedSteps);
|
|
371
355
|
}
|
|
356
|
+
} catch (applyLocalStepsError) {
|
|
357
|
+
var _this$analyticsHelper14;
|
|
358
|
+
(_this$analyticsHelper14 = this.analyticsHelper) === null || _this$analyticsHelper14 === void 0 ? void 0 : _this$analyticsHelper14.sendErrorEvent(applyLocalStepsError, `Error while onRestore with applyLocalSteps. Will fallback to fetchReconcile`);
|
|
359
|
+
useReconcile = true;
|
|
360
|
+
await this.fetchReconcile(JSON.stringify(currentState.content), 'fe-restore');
|
|
372
361
|
}
|
|
373
362
|
(_this$analyticsHelper15 = this.analyticsHelper) === null || _this$analyticsHelper15 === void 0 ? void 0 : _this$analyticsHelper15.sendActionEvent(EVENT_ACTION.REINITIALISE_DOCUMENT, EVENT_STATUS.SUCCESS, {
|
|
374
363
|
numUnconfirmedSteps: unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length,
|
|
@@ -543,8 +532,8 @@ export class DocumentService {
|
|
|
543
532
|
(_this$analyticsHelper24 = this.analyticsHelper) === null || _this$analyticsHelper24 === void 0 ? void 0 : _this$analyticsHelper24.sendErrorEvent(new Error('Editor state is undefined'), 'commitUnconfirmedSteps called without state');
|
|
544
533
|
}
|
|
545
534
|
while (!isLastTrConfirmed) {
|
|
546
|
-
//
|
|
547
|
-
this.sendStepsFromCurrentState(undefined,
|
|
535
|
+
// this makes all commitUnconfirmedSteps skip the waiting time, which means draft-sync is sped up too.
|
|
536
|
+
this.sendStepsFromCurrentState(undefined, 'publish');
|
|
548
537
|
await sleep(500);
|
|
549
538
|
const nextUnconfirmedSteps = this.getUnconfirmedSteps();
|
|
550
539
|
if (nextUnconfirmedSteps !== null && nextUnconfirmedSteps !== void 0 && nextUnconfirmedSteps.length) {
|
|
@@ -49,12 +49,17 @@ const getConflicts = ({
|
|
|
49
49
|
// Partial overlap with the end
|
|
50
50
|
localChange.fromA >= remoteChange.fromA && localChange.fromA < remoteChange.toA && localChange.toA > remoteChange.toA ||
|
|
51
51
|
// Partial overlap with the start
|
|
52
|
-
localChange.fromA < remoteChange.fromA && localChange.toA > remoteChange.fromA && localChange.toA <= remoteChange.toA
|
|
52
|
+
localChange.fromA < remoteChange.fromA && localChange.toA > remoteChange.fromA && localChange.toA <= remoteChange.toA ||
|
|
53
|
+
// One of the edges match
|
|
54
|
+
localChange.fromA === remoteChange.toA || remoteChange.fromA === localChange.toA) {
|
|
55
|
+
const remoteSlice = remoteDoc.slice(remoteChange.fromB, remoteChange.toB);
|
|
56
|
+
const isDeletion = remoteSlice.size === 0;
|
|
53
57
|
conflictingChanges.push({
|
|
54
58
|
from: Math.min(localChange.fromA, remoteChange.fromA),
|
|
55
59
|
to: Math.max(localChange.toA, remoteChange.toA),
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
// We only want to capture the exact slice deleted (without parents) so we can display and insert as expected
|
|
61
|
+
local: localDoc.slice(localChange.fromB, localChange.toB, !isDeletion),
|
|
62
|
+
remote: remoteSlice
|
|
58
63
|
});
|
|
59
64
|
}
|
|
60
65
|
});
|
|
@@ -146,12 +151,22 @@ export function getConflictChanges({
|
|
|
146
151
|
remoteChanges
|
|
147
152
|
});
|
|
148
153
|
const isConflictChange = value => Boolean(value);
|
|
154
|
+
|
|
155
|
+
// Prevent duplicate ranges occuring
|
|
156
|
+
const seenInsertions = new Set();
|
|
149
157
|
return {
|
|
150
158
|
inserted: conflictingChanges.filter(i => i.remote.size !== 0).map(i => ({
|
|
151
159
|
...i,
|
|
152
160
|
from: mapping.map(i.from, -1),
|
|
153
161
|
to: mapping.map(i.to)
|
|
154
|
-
})).filter(
|
|
162
|
+
})).filter(i => {
|
|
163
|
+
const identifier = `${i.from}-${i.to}`;
|
|
164
|
+
if (seenInsertions.has(identifier)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
seenInsertions.add(identifier);
|
|
168
|
+
return isConflictChange(i);
|
|
169
|
+
}),
|
|
155
170
|
deleted: conflictingChanges.filter(d => d.remote.size === 0).map(d => ({
|
|
156
171
|
...d,
|
|
157
172
|
from: mapping.map(d.from),
|
|
@@ -6,6 +6,7 @@ import { NCS_ERROR_CODE } from '../errors/ncs-errors';
|
|
|
6
6
|
import { createLogger } from '../helpers/utils';
|
|
7
7
|
const logger = createLogger('commit-step', 'black');
|
|
8
8
|
export let readyToCommit = true;
|
|
9
|
+
export let lastBroadcastRequestAcked = true;
|
|
9
10
|
export const RESET_READYTOCOMMIT_INTERVAL_MS = 5000;
|
|
10
11
|
export const commitStepQueue = ({
|
|
11
12
|
broadcast,
|
|
@@ -22,14 +23,27 @@ export const commitStepQueue = ({
|
|
|
22
23
|
collabMode,
|
|
23
24
|
reason
|
|
24
25
|
}) => {
|
|
26
|
+
// this timer is for waiting to send the next batch in between acks from the BE
|
|
27
|
+
let commitWaitTimer;
|
|
28
|
+
// if publishing and not waiting for an ACK, then clear the commit timer and proceed, skipping the timer
|
|
29
|
+
if (reason === 'publish' && lastBroadcastRequestAcked) {
|
|
30
|
+
if (fg('skip_collab_provider_delay_on_publish')) {
|
|
31
|
+
clearTimeout(commitWaitTimer);
|
|
32
|
+
readyToCommit = true;
|
|
33
|
+
} // no-op if fg is turned off
|
|
34
|
+
}
|
|
25
35
|
if (!readyToCommit) {
|
|
26
36
|
logger('Not ready to commit, skip');
|
|
27
37
|
return;
|
|
28
38
|
}
|
|
29
39
|
// Block other sending request, before ACK
|
|
30
40
|
readyToCommit = false;
|
|
31
|
-
|
|
41
|
+
lastBroadcastRequestAcked = false;
|
|
42
|
+
|
|
43
|
+
// this timer is a fallback for if an ACK from BE is lost - stop the queue from getting indefinitely locked
|
|
44
|
+
const fallbackTimer = setTimeout(() => {
|
|
32
45
|
readyToCommit = true;
|
|
46
|
+
lastBroadcastRequestAcked = true;
|
|
33
47
|
logger('reset readyToCommit by timer');
|
|
34
48
|
}, RESET_READYTOCOMMIT_INTERVAL_MS);
|
|
35
49
|
let stepsWithClientAndUserId = steps.map(step => ({
|
|
@@ -48,7 +62,6 @@ export const commitStepQueue = ({
|
|
|
48
62
|
// - is setup for last write wins,
|
|
49
63
|
// - and is just a boolean -- so no real risk of data loss.
|
|
50
64
|
if (__livePage && fg('platform.editor.live-pages-expand-divergence')) {
|
|
51
|
-
// @atlaskit/platform-feature-flags
|
|
52
65
|
stepsWithClientAndUserId = stepsWithClientAndUserId.map(step => {
|
|
53
66
|
if (isExpandChangeStep(step)) {
|
|
54
67
|
// The title is also updated via this step, which we do want to send to the server.
|
|
@@ -86,64 +99,35 @@ export const commitStepQueue = ({
|
|
|
86
99
|
version,
|
|
87
100
|
userId
|
|
88
101
|
}, response => {
|
|
102
|
+
lastBroadcastRequestAcked = true;
|
|
89
103
|
const latency = new Date().getTime() - start;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
} else {
|
|
106
|
-
if (latency < 400) {
|
|
107
|
-
setTimeout(() => {
|
|
108
|
-
readyToCommit = true;
|
|
109
|
-
logger('reset readyToCommit');
|
|
110
|
-
}, 100);
|
|
111
|
-
} else {
|
|
112
|
-
readyToCommit = true;
|
|
113
|
-
logger('reset readyToCommit');
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
104
|
+
// this most closely replicates the BE ack delay behaviour. 500ms hardcoded + 180ms network delay (tested on hello)
|
|
105
|
+
// more context: https://hello.atlassian.net/wiki/spaces/CEPS/pages/5020556010/Spike+Moving+BE+delay+to+the+FE
|
|
106
|
+
// to be switched over to backpressure delay sent from the BE in https://hello.jira.atlassian.cloud/browse/CEPS-1030
|
|
107
|
+
let delay = latency < 680 ? 680 - latency : 1;
|
|
108
|
+
if (response.delay) {
|
|
109
|
+
delay = response.delay;
|
|
110
|
+
} // if backpressure delay is sent, overwrite it
|
|
111
|
+
|
|
112
|
+
clearTimeout(fallbackTimer); // clear the fallback timer, ack was successfully sent/recieved
|
|
113
|
+
commitWaitTimer = setTimeout(() => {
|
|
114
|
+
// unlock the queue after waiting for delay
|
|
115
|
+
readyToCommit = true;
|
|
116
|
+
logger('reset readyToCommit');
|
|
117
|
+
}, delay);
|
|
117
118
|
if (response.type === AcknowledgementResponseTypes.SUCCESS) {
|
|
118
119
|
onStepsAdded({
|
|
119
120
|
steps: stepsWithClientAndUserId,
|
|
120
121
|
version: response.version
|
|
121
122
|
});
|
|
122
|
-
|
|
123
|
-
if (Math.random() < 0.1) {
|
|
124
|
-
analyticsHelper === null || analyticsHelper === void 0 ? void 0 : analyticsHelper.sendActionEvent(EVENT_ACTION.ADD_STEPS, EVENT_STATUS.SUCCESS_10x_SAMPLED, {
|
|
125
|
-
type: ADD_STEPS_TYPE.ACCEPTED,
|
|
126
|
-
latency,
|
|
127
|
-
stepType: countBy(stepsWithClientAndUserId,
|
|
128
|
-
// Ignored via go/ees005
|
|
129
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
130
|
-
stepWithClientAndUserId => stepWithClientAndUserId.stepType)
|
|
131
|
-
});
|
|
132
|
-
}
|
|
123
|
+
sendSuccessAnalytics(latency, stepsWithClientAndUserId, analyticsHelper);
|
|
133
124
|
emit('commit-status', {
|
|
134
125
|
status: 'success',
|
|
135
126
|
version: response.version
|
|
136
127
|
});
|
|
137
128
|
} else if (response.type === AcknowledgementResponseTypes.ERROR) {
|
|
138
129
|
onErrorHandled(response.error);
|
|
139
|
-
|
|
140
|
-
// User tried committing steps but they were rejected because:
|
|
141
|
-
// - HEAD_VERSION_UPDATE_FAILED: the collab service's latest stored step tail version didn't correspond to the head version of the first step submitted
|
|
142
|
-
// - VERSION_NUMBER_ALREADY_EXISTS: while storing the steps there was a conflict meaning someone else wrote steps into the database more quickly
|
|
143
|
-
type: response.error.data.code === NCS_ERROR_CODE.HEAD_VERSION_UPDATE_FAILED || response.error.data.code === NCS_ERROR_CODE.VERSION_NUMBER_ALREADY_EXISTS ? ADD_STEPS_TYPE.REJECTED : ADD_STEPS_TYPE.ERROR,
|
|
144
|
-
latency
|
|
145
|
-
});
|
|
146
|
-
analyticsHelper === null || analyticsHelper === void 0 ? void 0 : analyticsHelper.sendErrorEvent(response.error, 'Error while adding steps - Acknowledgement Error');
|
|
130
|
+
sendFailureAnalytics(response, latency, analyticsHelper);
|
|
147
131
|
emit('commit-status', {
|
|
148
132
|
status: 'failure',
|
|
149
133
|
version
|
|
@@ -159,10 +143,8 @@ export const commitStepQueue = ({
|
|
|
159
143
|
}
|
|
160
144
|
});
|
|
161
145
|
} catch (error) {
|
|
162
|
-
// if the broadcast failed
|
|
163
|
-
|
|
164
|
-
readyToCommit = true;
|
|
165
|
-
}
|
|
146
|
+
// if the broadcast failed for any reason, we shouldn't keep the queue locked as the BE has not recieved any message
|
|
147
|
+
readyToCommit = true;
|
|
166
148
|
analyticsHelper === null || analyticsHelper === void 0 ? void 0 : analyticsHelper.sendErrorEvent(error, 'Error while adding steps - Broadcast threw exception');
|
|
167
149
|
emit('commit-status', {
|
|
168
150
|
status: 'failure',
|
|
@@ -177,4 +159,27 @@ function isExpandChangeStep(step) {
|
|
|
177
159
|
return true;
|
|
178
160
|
}
|
|
179
161
|
return false;
|
|
162
|
+
}
|
|
163
|
+
function sendSuccessAnalytics(latency, stepsWithClientAndUserId, analyticsHelper) {
|
|
164
|
+
// Sample only 10% of add steps events to avoid overwhelming the analytics
|
|
165
|
+
if (Math.random() < 0.1) {
|
|
166
|
+
analyticsHelper === null || analyticsHelper === void 0 ? void 0 : analyticsHelper.sendActionEvent(EVENT_ACTION.ADD_STEPS, EVENT_STATUS.SUCCESS_10x_SAMPLED, {
|
|
167
|
+
type: ADD_STEPS_TYPE.ACCEPTED,
|
|
168
|
+
latency,
|
|
169
|
+
stepType: countBy(stepsWithClientAndUserId,
|
|
170
|
+
// Ignored via go/ees005
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
172
|
+
stepWithClientAndUserId => stepWithClientAndUserId.stepType)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function sendFailureAnalytics(response, latency, analyticsHelper) {
|
|
177
|
+
analyticsHelper === null || analyticsHelper === void 0 ? void 0 : analyticsHelper.sendActionEvent(EVENT_ACTION.ADD_STEPS, EVENT_STATUS.FAILURE, {
|
|
178
|
+
// User tried committing steps but they were rejected because:
|
|
179
|
+
// - HEAD_VERSION_UPDATE_FAILED: the collab service's latest stored step tail version didn't correspond to the head version of the first step submitted
|
|
180
|
+
// - VERSION_NUMBER_ALREADY_EXISTS: while storing the steps there was a conflict meaning someone else wrote steps into the database more quickly
|
|
181
|
+
type: response.error.data.code === NCS_ERROR_CODE.HEAD_VERSION_UPDATE_FAILED || response.error.data.code === NCS_ERROR_CODE.VERSION_NUMBER_ALREADY_EXISTS ? ADD_STEPS_TYPE.REJECTED : ADD_STEPS_TYPE.ERROR,
|
|
182
|
+
latency
|
|
183
|
+
});
|
|
184
|
+
analyticsHelper === null || analyticsHelper === void 0 ? void 0 : analyticsHelper.sendErrorEvent(response.error, 'Error while adding steps - Acknowledgement Error');
|
|
180
185
|
}
|
|
@@ -368,11 +368,7 @@ export var DocumentService = /*#__PURE__*/function () {
|
|
|
368
368
|
reserveCursor: true
|
|
369
369
|
});
|
|
370
370
|
_this.metadataService.updateMetadata(metadata);
|
|
371
|
-
|
|
372
|
-
_context3.next = 27;
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
|
-
_context3.prev = 14;
|
|
371
|
+
_context3.prev = 13;
|
|
376
372
|
(_this$analyticsHelper13 = _this.analyticsHelper) === null || _this$analyticsHelper13 === void 0 || _this$analyticsHelper13.sendActionEvent(EVENT_ACTION.REINITIALISE_DOCUMENT, EVENT_STATUS.INFO, {
|
|
377
373
|
numUnconfirmedSteps: unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length,
|
|
378
374
|
obfuscatedSteps: obfuscatedSteps,
|
|
@@ -385,35 +381,16 @@ export var DocumentService = /*#__PURE__*/function () {
|
|
|
385
381
|
if (unconfirmedSteps !== null && unconfirmedSteps !== void 0 && unconfirmedSteps.length) {
|
|
386
382
|
_this.applyLocalSteps(unconfirmedSteps);
|
|
387
383
|
}
|
|
388
|
-
_context3.next =
|
|
384
|
+
_context3.next = 24;
|
|
389
385
|
break;
|
|
390
|
-
case
|
|
391
|
-
_context3.prev =
|
|
392
|
-
_context3.t0 = _context3["catch"](
|
|
386
|
+
case 18:
|
|
387
|
+
_context3.prev = 18;
|
|
388
|
+
_context3.t0 = _context3["catch"](13);
|
|
393
389
|
(_this$analyticsHelper14 = _this.analyticsHelper) === null || _this$analyticsHelper14 === void 0 || _this$analyticsHelper14.sendErrorEvent(_context3.t0, "Error while onRestore with applyLocalSteps. Will fallback to fetchReconcile");
|
|
394
390
|
useReconcile = true;
|
|
395
|
-
_context3.next =
|
|
391
|
+
_context3.next = 24;
|
|
396
392
|
return _this.fetchReconcile(JSON.stringify(currentState.content), 'fe-restore');
|
|
397
|
-
case
|
|
398
|
-
_context3.next = 33;
|
|
399
|
-
break;
|
|
400
|
-
case 27:
|
|
401
|
-
if (!(useReconcile && currentState)) {
|
|
402
|
-
_context3.next = 32;
|
|
403
|
-
break;
|
|
404
|
-
}
|
|
405
|
-
_context3.next = 30;
|
|
406
|
-
return _this.fetchReconcile(JSON.stringify(currentState.content), 'fe-restore');
|
|
407
|
-
case 30:
|
|
408
|
-
_context3.next = 33;
|
|
409
|
-
break;
|
|
410
|
-
case 32:
|
|
411
|
-
if (unconfirmedSteps !== null && unconfirmedSteps !== void 0 && unconfirmedSteps.length) {
|
|
412
|
-
// we don't want to use reconcile for restore triggered by catchup client out of sync (when targetClientId is provided)
|
|
413
|
-
// as this results in all changes made while the client was out of sync being lost
|
|
414
|
-
_this.applyLocalSteps(unconfirmedSteps);
|
|
415
|
-
}
|
|
416
|
-
case 33:
|
|
393
|
+
case 24:
|
|
417
394
|
(_this$analyticsHelper15 = _this.analyticsHelper) === null || _this$analyticsHelper15 === void 0 || _this$analyticsHelper15.sendActionEvent(EVENT_ACTION.REINITIALISE_DOCUMENT, EVENT_STATUS.SUCCESS, {
|
|
418
395
|
numUnconfirmedSteps: unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length,
|
|
419
396
|
hasTitle: !!(metadata !== null && metadata !== void 0 && metadata.title),
|
|
@@ -422,10 +399,10 @@ export var DocumentService = /*#__PURE__*/function () {
|
|
|
422
399
|
targetClientId: targetClientId,
|
|
423
400
|
triggeredByCatchup: !!targetClientId
|
|
424
401
|
});
|
|
425
|
-
_context3.next =
|
|
402
|
+
_context3.next = 32;
|
|
426
403
|
break;
|
|
427
|
-
case
|
|
428
|
-
_context3.prev =
|
|
404
|
+
case 27:
|
|
405
|
+
_context3.prev = 27;
|
|
429
406
|
_context3.t1 = _context3["catch"](10);
|
|
430
407
|
(_this$analyticsHelper16 = _this.analyticsHelper) === null || _this$analyticsHelper16 === void 0 || _this$analyticsHelper16.sendActionEvent(EVENT_ACTION.REINITIALISE_DOCUMENT, EVENT_STATUS.FAILURE, {
|
|
431
408
|
numUnconfirmedSteps: unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length,
|
|
@@ -443,11 +420,11 @@ export var DocumentService = /*#__PURE__*/function () {
|
|
|
443
420
|
code: INTERNAL_ERROR_CODE.DOCUMENT_RESTORE_ERROR
|
|
444
421
|
}
|
|
445
422
|
});
|
|
446
|
-
case
|
|
423
|
+
case 32:
|
|
447
424
|
case "end":
|
|
448
425
|
return _context3.stop();
|
|
449
426
|
}
|
|
450
|
-
}, _callee3, null, [[10,
|
|
427
|
+
}, _callee3, null, [[10, 27], [13, 18]]);
|
|
451
428
|
}));
|
|
452
429
|
return function (_x3) {
|
|
453
430
|
return _ref7.apply(this, arguments);
|
|
@@ -633,8 +610,8 @@ export var DocumentService = /*#__PURE__*/function () {
|
|
|
633
610
|
_context5.next = 22;
|
|
634
611
|
break;
|
|
635
612
|
}
|
|
636
|
-
//
|
|
637
|
-
_this.sendStepsFromCurrentState(undefined,
|
|
613
|
+
// this makes all commitUnconfirmedSteps skip the waiting time, which means draft-sync is sped up too.
|
|
614
|
+
_this.sendStepsFromCurrentState(undefined, 'publish');
|
|
638
615
|
_context5.next = 13;
|
|
639
616
|
return sleep(500);
|
|
640
617
|
case 13:
|
|
@@ -51,12 +51,17 @@ var getConflicts = function getConflicts(_ref) {
|
|
|
51
51
|
// Partial overlap with the end
|
|
52
52
|
localChange.fromA >= remoteChange.fromA && localChange.fromA < remoteChange.toA && localChange.toA > remoteChange.toA ||
|
|
53
53
|
// Partial overlap with the start
|
|
54
|
-
localChange.fromA < remoteChange.fromA && localChange.toA > remoteChange.fromA && localChange.toA <= remoteChange.toA
|
|
54
|
+
localChange.fromA < remoteChange.fromA && localChange.toA > remoteChange.fromA && localChange.toA <= remoteChange.toA ||
|
|
55
|
+
// One of the edges match
|
|
56
|
+
localChange.fromA === remoteChange.toA || remoteChange.fromA === localChange.toA) {
|
|
57
|
+
var remoteSlice = remoteDoc.slice(remoteChange.fromB, remoteChange.toB);
|
|
58
|
+
var isDeletion = remoteSlice.size === 0;
|
|
55
59
|
conflictingChanges.push({
|
|
56
60
|
from: Math.min(localChange.fromA, remoteChange.fromA),
|
|
57
61
|
to: Math.max(localChange.toA, remoteChange.toA),
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
// We only want to capture the exact slice deleted (without parents) so we can display and insert as expected
|
|
63
|
+
local: localDoc.slice(localChange.fromB, localChange.toB, !isDeletion),
|
|
64
|
+
remote: remoteSlice
|
|
60
65
|
});
|
|
61
66
|
}
|
|
62
67
|
});
|
|
@@ -149,6 +154,9 @@ export function getConflictChanges(_ref3) {
|
|
|
149
154
|
var isConflictChange = function isConflictChange(value) {
|
|
150
155
|
return Boolean(value);
|
|
151
156
|
};
|
|
157
|
+
|
|
158
|
+
// Prevent duplicate ranges occuring
|
|
159
|
+
var seenInsertions = new Set();
|
|
152
160
|
return {
|
|
153
161
|
inserted: conflictingChanges.filter(function (i) {
|
|
154
162
|
return i.remote.size !== 0;
|
|
@@ -157,7 +165,14 @@ export function getConflictChanges(_ref3) {
|
|
|
157
165
|
from: mapping.map(i.from, -1),
|
|
158
166
|
to: mapping.map(i.to)
|
|
159
167
|
});
|
|
160
|
-
}).filter(
|
|
168
|
+
}).filter(function (i) {
|
|
169
|
+
var identifier = "".concat(i.from, "-").concat(i.to);
|
|
170
|
+
if (seenInsertions.has(identifier)) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
seenInsertions.add(identifier);
|
|
174
|
+
return isConflictChange(i);
|
|
175
|
+
}),
|
|
161
176
|
deleted: conflictingChanges.filter(function (d) {
|
|
162
177
|
return d.remote.size === 0;
|
|
163
178
|
}).map(function (d) {
|
|
@@ -9,6 +9,7 @@ import { NCS_ERROR_CODE } from '../errors/ncs-errors';
|
|
|
9
9
|
import { createLogger } from '../helpers/utils';
|
|
10
10
|
var logger = createLogger('commit-step', 'black');
|
|
11
11
|
export var readyToCommit = true;
|
|
12
|
+
export var lastBroadcastRequestAcked = true;
|
|
12
13
|
export var RESET_READYTOCOMMIT_INTERVAL_MS = 5000;
|
|
13
14
|
export var commitStepQueue = function commitStepQueue(_ref) {
|
|
14
15
|
var broadcast = _ref.broadcast,
|
|
@@ -24,14 +25,27 @@ export var commitStepQueue = function commitStepQueue(_ref) {
|
|
|
24
25
|
hasRecovered = _ref.hasRecovered,
|
|
25
26
|
collabMode = _ref.collabMode,
|
|
26
27
|
reason = _ref.reason;
|
|
28
|
+
// this timer is for waiting to send the next batch in between acks from the BE
|
|
29
|
+
var commitWaitTimer;
|
|
30
|
+
// if publishing and not waiting for an ACK, then clear the commit timer and proceed, skipping the timer
|
|
31
|
+
if (reason === 'publish' && lastBroadcastRequestAcked) {
|
|
32
|
+
if (fg('skip_collab_provider_delay_on_publish')) {
|
|
33
|
+
clearTimeout(commitWaitTimer);
|
|
34
|
+
readyToCommit = true;
|
|
35
|
+
} // no-op if fg is turned off
|
|
36
|
+
}
|
|
27
37
|
if (!readyToCommit) {
|
|
28
38
|
logger('Not ready to commit, skip');
|
|
29
39
|
return;
|
|
30
40
|
}
|
|
31
41
|
// Block other sending request, before ACK
|
|
32
42
|
readyToCommit = false;
|
|
33
|
-
|
|
43
|
+
lastBroadcastRequestAcked = false;
|
|
44
|
+
|
|
45
|
+
// this timer is a fallback for if an ACK from BE is lost - stop the queue from getting indefinitely locked
|
|
46
|
+
var fallbackTimer = setTimeout(function () {
|
|
34
47
|
readyToCommit = true;
|
|
48
|
+
lastBroadcastRequestAcked = true;
|
|
35
49
|
logger('reset readyToCommit by timer');
|
|
36
50
|
}, RESET_READYTOCOMMIT_INTERVAL_MS);
|
|
37
51
|
var stepsWithClientAndUserId = steps.map(function (step) {
|
|
@@ -51,7 +65,6 @@ export var commitStepQueue = function commitStepQueue(_ref) {
|
|
|
51
65
|
// - is setup for last write wins,
|
|
52
66
|
// - and is just a boolean -- so no real risk of data loss.
|
|
53
67
|
if (__livePage && fg('platform.editor.live-pages-expand-divergence')) {
|
|
54
|
-
// @atlaskit/platform-feature-flags
|
|
55
68
|
stepsWithClientAndUserId = stepsWithClientAndUserId.map(function (step) {
|
|
56
69
|
if (isExpandChangeStep(step)) {
|
|
57
70
|
// The title is also updated via this step, which we do want to send to the server.
|
|
@@ -87,66 +100,35 @@ export var commitStepQueue = function commitStepQueue(_ref) {
|
|
|
87
100
|
version: version,
|
|
88
101
|
userId: userId
|
|
89
102
|
}, function (response) {
|
|
103
|
+
lastBroadcastRequestAcked = true;
|
|
90
104
|
var latency = new Date().getTime() - start;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
if (latency < 400) {
|
|
108
|
-
setTimeout(function () {
|
|
109
|
-
readyToCommit = true;
|
|
110
|
-
logger('reset readyToCommit');
|
|
111
|
-
}, 100);
|
|
112
|
-
} else {
|
|
113
|
-
readyToCommit = true;
|
|
114
|
-
logger('reset readyToCommit');
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
105
|
+
// this most closely replicates the BE ack delay behaviour. 500ms hardcoded + 180ms network delay (tested on hello)
|
|
106
|
+
// more context: https://hello.atlassian.net/wiki/spaces/CEPS/pages/5020556010/Spike+Moving+BE+delay+to+the+FE
|
|
107
|
+
// to be switched over to backpressure delay sent from the BE in https://hello.jira.atlassian.cloud/browse/CEPS-1030
|
|
108
|
+
var delay = latency < 680 ? 680 - latency : 1;
|
|
109
|
+
if (response.delay) {
|
|
110
|
+
delay = response.delay;
|
|
111
|
+
} // if backpressure delay is sent, overwrite it
|
|
112
|
+
|
|
113
|
+
clearTimeout(fallbackTimer); // clear the fallback timer, ack was successfully sent/recieved
|
|
114
|
+
commitWaitTimer = setTimeout(function () {
|
|
115
|
+
// unlock the queue after waiting for delay
|
|
116
|
+
readyToCommit = true;
|
|
117
|
+
logger('reset readyToCommit');
|
|
118
|
+
}, delay);
|
|
118
119
|
if (response.type === AcknowledgementResponseTypes.SUCCESS) {
|
|
119
120
|
onStepsAdded({
|
|
120
121
|
steps: stepsWithClientAndUserId,
|
|
121
122
|
version: response.version
|
|
122
123
|
});
|
|
123
|
-
|
|
124
|
-
if (Math.random() < 0.1) {
|
|
125
|
-
analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(EVENT_ACTION.ADD_STEPS, EVENT_STATUS.SUCCESS_10x_SAMPLED, {
|
|
126
|
-
type: ADD_STEPS_TYPE.ACCEPTED,
|
|
127
|
-
latency: latency,
|
|
128
|
-
stepType: countBy(stepsWithClientAndUserId,
|
|
129
|
-
// Ignored via go/ees005
|
|
130
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
131
|
-
function (stepWithClientAndUserId) {
|
|
132
|
-
return stepWithClientAndUserId.stepType;
|
|
133
|
-
})
|
|
134
|
-
});
|
|
135
|
-
}
|
|
124
|
+
sendSuccessAnalytics(latency, stepsWithClientAndUserId, analyticsHelper);
|
|
136
125
|
emit('commit-status', {
|
|
137
126
|
status: 'success',
|
|
138
127
|
version: response.version
|
|
139
128
|
});
|
|
140
129
|
} else if (response.type === AcknowledgementResponseTypes.ERROR) {
|
|
141
130
|
onErrorHandled(response.error);
|
|
142
|
-
|
|
143
|
-
// User tried committing steps but they were rejected because:
|
|
144
|
-
// - HEAD_VERSION_UPDATE_FAILED: the collab service's latest stored step tail version didn't correspond to the head version of the first step submitted
|
|
145
|
-
// - VERSION_NUMBER_ALREADY_EXISTS: while storing the steps there was a conflict meaning someone else wrote steps into the database more quickly
|
|
146
|
-
type: response.error.data.code === NCS_ERROR_CODE.HEAD_VERSION_UPDATE_FAILED || response.error.data.code === NCS_ERROR_CODE.VERSION_NUMBER_ALREADY_EXISTS ? ADD_STEPS_TYPE.REJECTED : ADD_STEPS_TYPE.ERROR,
|
|
147
|
-
latency: latency
|
|
148
|
-
});
|
|
149
|
-
analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendErrorEvent(response.error, 'Error while adding steps - Acknowledgement Error');
|
|
131
|
+
sendFailureAnalytics(response, latency, analyticsHelper);
|
|
150
132
|
emit('commit-status', {
|
|
151
133
|
status: 'failure',
|
|
152
134
|
version: version
|
|
@@ -162,10 +144,8 @@ export var commitStepQueue = function commitStepQueue(_ref) {
|
|
|
162
144
|
}
|
|
163
145
|
});
|
|
164
146
|
} catch (error) {
|
|
165
|
-
// if the broadcast failed
|
|
166
|
-
|
|
167
|
-
readyToCommit = true;
|
|
168
|
-
}
|
|
147
|
+
// if the broadcast failed for any reason, we shouldn't keep the queue locked as the BE has not recieved any message
|
|
148
|
+
readyToCommit = true;
|
|
169
149
|
analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendErrorEvent(error, 'Error while adding steps - Broadcast threw exception');
|
|
170
150
|
emit('commit-status', {
|
|
171
151
|
status: 'failure',
|
|
@@ -180,4 +160,29 @@ function isExpandChangeStep(step) {
|
|
|
180
160
|
return true;
|
|
181
161
|
}
|
|
182
162
|
return false;
|
|
163
|
+
}
|
|
164
|
+
function sendSuccessAnalytics(latency, stepsWithClientAndUserId, analyticsHelper) {
|
|
165
|
+
// Sample only 10% of add steps events to avoid overwhelming the analytics
|
|
166
|
+
if (Math.random() < 0.1) {
|
|
167
|
+
analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(EVENT_ACTION.ADD_STEPS, EVENT_STATUS.SUCCESS_10x_SAMPLED, {
|
|
168
|
+
type: ADD_STEPS_TYPE.ACCEPTED,
|
|
169
|
+
latency: latency,
|
|
170
|
+
stepType: countBy(stepsWithClientAndUserId,
|
|
171
|
+
// Ignored via go/ees005
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
173
|
+
function (stepWithClientAndUserId) {
|
|
174
|
+
return stepWithClientAndUserId.stepType;
|
|
175
|
+
})
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function sendFailureAnalytics(response, latency, analyticsHelper) {
|
|
180
|
+
analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(EVENT_ACTION.ADD_STEPS, EVENT_STATUS.FAILURE, {
|
|
181
|
+
// User tried committing steps but they were rejected because:
|
|
182
|
+
// - HEAD_VERSION_UPDATE_FAILED: the collab service's latest stored step tail version didn't correspond to the head version of the first step submitted
|
|
183
|
+
// - VERSION_NUMBER_ALREADY_EXISTS: while storing the steps there was a conflict meaning someone else wrote steps into the database more quickly
|
|
184
|
+
type: response.error.data.code === NCS_ERROR_CODE.HEAD_VERSION_UPDATE_FAILED || response.error.data.code === NCS_ERROR_CODE.VERSION_NUMBER_ALREADY_EXISTS ? ADD_STEPS_TYPE.REJECTED : ADD_STEPS_TYPE.ERROR,
|
|
185
|
+
latency: latency
|
|
186
|
+
});
|
|
187
|
+
analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendErrorEvent(response.error, 'Error while adding steps - Acknowledgement Error');
|
|
183
188
|
}
|
|
@@ -5,6 +5,7 @@ import type AnalyticsHelper from '../analytics/analytics-helper';
|
|
|
5
5
|
import type { InternalError } from '../errors/internal-errors';
|
|
6
6
|
import type { GetResolvedEditorStateReason } from '@atlaskit/editor-common/types';
|
|
7
7
|
export declare let readyToCommit: boolean;
|
|
8
|
+
export declare let lastBroadcastRequestAcked: boolean;
|
|
8
9
|
export declare const RESET_READYTOCOMMIT_INTERVAL_MS = 5000;
|
|
9
10
|
export declare const commitStepQueue: ({ broadcast, steps, version, userId, clientId, onStepsAdded, onErrorHandled, analyticsHelper, emit, __livePage, hasRecovered, collabMode, reason, }: {
|
|
10
11
|
broadcast: <K extends keyof ChannelEvent>(type: K, data: Omit<ChannelEvent[K], 'timestamp'>, callback?: Function) => void;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -151,10 +151,12 @@ export type AcknowledgementPayload = AcknowledgementSuccessPayload | Acknowledge
|
|
|
151
151
|
export type AddStepAcknowledgementSuccessPayload = {
|
|
152
152
|
type: AcknowledgementResponseTypes.SUCCESS;
|
|
153
153
|
version: number;
|
|
154
|
+
delay?: number;
|
|
154
155
|
};
|
|
155
156
|
export type AcknowledgementErrorPayload = {
|
|
156
157
|
type: AcknowledgementResponseTypes.ERROR;
|
|
157
158
|
error: InternalError;
|
|
159
|
+
delay?: number;
|
|
158
160
|
};
|
|
159
161
|
export type AddStepAcknowledgementPayload = AddStepAcknowledgementSuccessPayload | AcknowledgementErrorPayload;
|
|
160
162
|
export type StepsPayload = {
|
|
@@ -5,6 +5,7 @@ import type AnalyticsHelper from '../analytics/analytics-helper';
|
|
|
5
5
|
import type { InternalError } from '../errors/internal-errors';
|
|
6
6
|
import type { GetResolvedEditorStateReason } from '@atlaskit/editor-common/types';
|
|
7
7
|
export declare let readyToCommit: boolean;
|
|
8
|
+
export declare let lastBroadcastRequestAcked: boolean;
|
|
8
9
|
export declare const RESET_READYTOCOMMIT_INTERVAL_MS = 5000;
|
|
9
10
|
export declare const commitStepQueue: ({ broadcast, steps, version, userId, clientId, onStepsAdded, onErrorHandled, analyticsHelper, emit, __livePage, hasRecovered, collabMode, reason, }: {
|
|
10
11
|
broadcast: <K extends keyof ChannelEvent>(type: K, data: Omit<ChannelEvent[K], 'timestamp'>, callback?: Function) => void;
|
|
@@ -151,10 +151,12 @@ export type AcknowledgementPayload = AcknowledgementSuccessPayload | Acknowledge
|
|
|
151
151
|
export type AddStepAcknowledgementSuccessPayload = {
|
|
152
152
|
type: AcknowledgementResponseTypes.SUCCESS;
|
|
153
153
|
version: number;
|
|
154
|
+
delay?: number;
|
|
154
155
|
};
|
|
155
156
|
export type AcknowledgementErrorPayload = {
|
|
156
157
|
type: AcknowledgementResponseTypes.ERROR;
|
|
157
158
|
error: InternalError;
|
|
159
|
+
delay?: number;
|
|
158
160
|
};
|
|
159
161
|
export type AddStepAcknowledgementPayload = AddStepAcknowledgementSuccessPayload | AcknowledgementErrorPayload;
|
|
160
162
|
export type StepsPayload = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/collab-provider",
|
|
3
|
-
"version": "10.10.
|
|
3
|
+
"version": "10.10.2",
|
|
4
4
|
"description": "A provider for collaborative editing.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"@atlaskit/adf-utils": "^19.19.0",
|
|
36
36
|
"@atlaskit/analytics-gas-types": "^5.1.0",
|
|
37
37
|
"@atlaskit/analytics-listeners": "^9.0.0",
|
|
38
|
-
"@atlaskit/editor-common": "^102.
|
|
38
|
+
"@atlaskit/editor-common": "^102.13.0",
|
|
39
39
|
"@atlaskit/editor-json-transformer": "^8.24.0",
|
|
40
40
|
"@atlaskit/editor-prosemirror": "7.0.0",
|
|
41
|
-
"@atlaskit/feature-gate-js-client": "^
|
|
41
|
+
"@atlaskit/feature-gate-js-client": "^5.0.0",
|
|
42
42
|
"@atlaskit/platform-feature-flags": "^1.1.0",
|
|
43
|
-
"@atlaskit/prosemirror-collab": "^0.
|
|
43
|
+
"@atlaskit/prosemirror-collab": "^0.16.0",
|
|
44
44
|
"@atlaskit/react-ufo": "^3.4.0",
|
|
45
45
|
"@atlaskit/ufo": "^0.4.0",
|
|
46
46
|
"@atlaskit/util-service-support": "^6.3.0",
|
|
@@ -80,9 +80,6 @@
|
|
|
80
80
|
"platform_editor_merge_unconfirmed_steps": {
|
|
81
81
|
"type": "boolean"
|
|
82
82
|
},
|
|
83
|
-
"restore_localstep_fallback_reconcile": {
|
|
84
|
-
"type": "boolean"
|
|
85
|
-
},
|
|
86
83
|
"tag_unconfirmed_steps_after_recovery": {
|
|
87
84
|
"type": "boolean"
|
|
88
85
|
},
|
|
@@ -95,7 +92,7 @@
|
|
|
95
92
|
"log_obfuscated_steps_for_view_only": {
|
|
96
93
|
"type": "boolean"
|
|
97
94
|
},
|
|
98
|
-
"
|
|
95
|
+
"skip_collab_provider_delay_on_publish": {
|
|
99
96
|
"type": "boolean"
|
|
100
97
|
}
|
|
101
98
|
}
|