@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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @atlaskit/collab-provider
2
2
 
3
+ ## 10.10.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 10.10.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies
14
+
3
15
  ## 10.10.0
4
16
 
5
17
  ### Minor Changes
@@ -375,11 +375,7 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
375
375
  reserveCursor: true
376
376
  });
377
377
  _this.metadataService.updateMetadata(metadata);
378
- if (!(0, _platformFeatureFlags.fg)('restore_localstep_fallback_reconcile')) {
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 = 25;
391
+ _context3.next = 24;
396
392
  break;
397
- case 19:
398
- _context3.prev = 19;
399
- _context3.t0 = _context3["catch"](14);
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 = 25;
398
+ _context3.next = 24;
403
399
  return _this.fetchReconcile(JSON.stringify(currentState.content), 'fe-restore');
404
- case 25:
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 = 41;
409
+ _context3.next = 32;
433
410
  break;
434
- case 36:
435
- _context3.prev = 36;
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 41:
430
+ case 32:
454
431
  case "end":
455
432
  return _context3.stop();
456
433
  }
457
- }, _callee3, null, [[10, 36], [14, 19]]);
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
- // forcePublish = true, this is because commitUnconfirmedSteps is only called when the Editor publishes a document
644
- _this.sendStepsFromCurrentState(undefined, reason);
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
- local: localDoc.slice(localChange.fromB, localChange.toB, true),
66
- remote: remoteDoc.slice(remoteChange.fromB, remoteChange.toB)
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(isConflictChange),
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
- var timer = setTimeout(function () {
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
- if (timer) {
99
- clearTimeout(timer);
100
- if ((0, _platformFeatureFlags.fg)('make_collab_provider_ack_delay_agnostic')) {
101
- if (latency < 680) {
102
- // this most closely replicates the BE ack delay behaviour. 500ms hardcoded + 180ms network delay (tested on hello)
103
- // more context: https://hello.atlassian.net/wiki/spaces/CEPS/pages/5020556010/Spike+Moving+BE+delay+to+the+FE
104
- // to be switched over to backpressure delay sent from the BE in https://hello.jira.atlassian.cloud/browse/CEPS-1030
105
- setTimeout(function () {
106
- exports.readyToCommit = readyToCommit = true;
107
- logger('reset readyToCommit');
108
- }, 680 - latency);
109
- } else {
110
- exports.readyToCommit = readyToCommit = true;
111
- logger('reset readyToCommit');
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
- // Sample only 10% of add steps events to avoid overwhelming the analytics
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
- analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(_const.EVENT_ACTION.ADD_STEPS, _const.EVENT_STATUS.FAILURE, {
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 due to not yet being connected, then set readyToCommit to true so that we don't get stuck in the timeout if the connection is slow to succeed
173
- if (error.name === 'NotConnectedError') {
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.0";
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
- if (fg('restore_localstep_fallback_reconcile')) {
343
- try {
344
- var _this$analyticsHelper13;
345
- (_this$analyticsHelper13 = this.analyticsHelper) === null || _this$analyticsHelper13 === void 0 ? void 0 : _this$analyticsHelper13.sendActionEvent(EVENT_ACTION.REINITIALISE_DOCUMENT, EVENT_STATUS.INFO, {
346
- numUnconfirmedSteps: unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length,
347
- obfuscatedSteps,
348
- obfuscatedDoc,
349
- hasTitle: !!(metadata !== null && metadata !== void 0 && metadata.title),
350
- clientId: this.clientId,
351
- targetClientId,
352
- triggeredByCatchup: !!targetClientId
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
- // forcePublish = true, this is because commitUnconfirmedSteps is only called when the Editor publishes a document
547
- this.sendStepsFromCurrentState(undefined, reason);
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
- local: localDoc.slice(localChange.fromB, localChange.toB, true),
57
- remote: remoteDoc.slice(remoteChange.fromB, remoteChange.toB)
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(isConflictChange),
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
- const timer = setTimeout(() => {
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
- if (timer) {
91
- clearTimeout(timer);
92
- if (fg('make_collab_provider_ack_delay_agnostic')) {
93
- if (latency < 680) {
94
- // this most closely replicates the BE ack delay behaviour. 500ms hardcoded + 180ms network delay (tested on hello)
95
- // more context: https://hello.atlassian.net/wiki/spaces/CEPS/pages/5020556010/Spike+Moving+BE+delay+to+the+FE
96
- // to be switched over to backpressure delay sent from the BE in https://hello.jira.atlassian.cloud/browse/CEPS-1030
97
- setTimeout(() => {
98
- readyToCommit = true;
99
- logger('reset readyToCommit');
100
- }, 680 - latency);
101
- } else {
102
- readyToCommit = true;
103
- logger('reset readyToCommit');
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
- // Sample only 10% of add steps events to avoid overwhelming the analytics
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
- analyticsHelper === null || analyticsHelper === void 0 ? void 0 : analyticsHelper.sendActionEvent(EVENT_ACTION.ADD_STEPS, EVENT_STATUS.FAILURE, {
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 due to not yet being connected, then set readyToCommit to true so that we don't get stuck in the timeout if the connection is slow to succeed
163
- if (error.name === 'NotConnectedError') {
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
  }
@@ -1,5 +1,5 @@
1
1
  export const name = "@atlaskit/collab-provider";
2
- export const version = "10.10.0";
2
+ export const version = "10.10.2";
3
3
  export const nextMajorVersion = () => {
4
4
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
5
5
  };
@@ -368,11 +368,7 @@ export var DocumentService = /*#__PURE__*/function () {
368
368
  reserveCursor: true
369
369
  });
370
370
  _this.metadataService.updateMetadata(metadata);
371
- if (!fg('restore_localstep_fallback_reconcile')) {
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 = 25;
384
+ _context3.next = 24;
389
385
  break;
390
- case 19:
391
- _context3.prev = 19;
392
- _context3.t0 = _context3["catch"](14);
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 = 25;
391
+ _context3.next = 24;
396
392
  return _this.fetchReconcile(JSON.stringify(currentState.content), 'fe-restore');
397
- case 25:
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 = 41;
402
+ _context3.next = 32;
426
403
  break;
427
- case 36:
428
- _context3.prev = 36;
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 41:
423
+ case 32:
447
424
  case "end":
448
425
  return _context3.stop();
449
426
  }
450
- }, _callee3, null, [[10, 36], [14, 19]]);
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
- // forcePublish = true, this is because commitUnconfirmedSteps is only called when the Editor publishes a document
637
- _this.sendStepsFromCurrentState(undefined, reason);
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
- local: localDoc.slice(localChange.fromB, localChange.toB, true),
59
- remote: remoteDoc.slice(remoteChange.fromB, remoteChange.toB)
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(isConflictChange),
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
- var timer = setTimeout(function () {
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
- if (timer) {
92
- clearTimeout(timer);
93
- if (fg('make_collab_provider_ack_delay_agnostic')) {
94
- if (latency < 680) {
95
- // this most closely replicates the BE ack delay behaviour. 500ms hardcoded + 180ms network delay (tested on hello)
96
- // more context: https://hello.atlassian.net/wiki/spaces/CEPS/pages/5020556010/Spike+Moving+BE+delay+to+the+FE
97
- // to be switched over to backpressure delay sent from the BE in https://hello.jira.atlassian.cloud/browse/CEPS-1030
98
- setTimeout(function () {
99
- readyToCommit = true;
100
- logger('reset readyToCommit');
101
- }, 680 - latency);
102
- } else {
103
- readyToCommit = true;
104
- logger('reset readyToCommit');
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
- // Sample only 10% of add steps events to avoid overwhelming the analytics
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
- analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(EVENT_ACTION.ADD_STEPS, EVENT_STATUS.FAILURE, {
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 due to not yet being connected, then set readyToCommit to true so that we don't get stuck in the timeout if the connection is slow to succeed
166
- if (error.name === 'NotConnectedError') {
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
  }
@@ -1,5 +1,5 @@
1
1
  export var name = "@atlaskit/collab-provider";
2
- export var version = "10.10.0";
2
+ export var version = "10.10.2";
3
3
  export var nextMajorVersion = function nextMajorVersion() {
4
4
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
5
5
  };
@@ -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 = {
@@ -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.0",
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.10.0",
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": "^4.26.0",
41
+ "@atlaskit/feature-gate-js-client": "^5.0.0",
42
42
  "@atlaskit/platform-feature-flags": "^1.1.0",
43
- "@atlaskit/prosemirror-collab": "^0.15.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
- "make_collab_provider_ack_delay_agnostic": {
95
+ "skip_collab_provider_delay_on_publish": {
99
96
  "type": "boolean"
100
97
  }
101
98
  }