@atlaskit/collab-provider 9.17.3 → 9.17.4

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,12 @@
1
1
  # @atlaskit/collab-provider
2
2
 
3
+ ## 9.17.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [#57317](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/pull-requests/57317) [`d989b4568594`](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/commits/d989b4568594) - ESS-4149: Add send steps queue under FF
8
+ - Updated dependencies
9
+
3
10
  ## 9.17.3
4
11
 
5
12
  ### Patch Changes
@@ -20,6 +20,7 @@ var _editorJsonTransformer = require("@atlaskit/editor-json-transformer");
20
20
  var _provider = require("../provider");
21
21
  var _catchup = require("./catchup");
22
22
  var _stepQueueState = require("./step-queue-state");
23
+ var _featureFlags = require("../feature-flags");
23
24
  var _errorTypes = require("../errors/error-types");
24
25
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
25
26
  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; }
@@ -43,11 +44,12 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
43
44
  * @param onErrorHandled - Callback to handle
44
45
  * @param metadataService
45
46
  * @param enableErrorOnFailedDocumentApply - Enable failed document update exceptions.
47
+ * @param enableSendStepsQueue - Enable send steps queue.
46
48
  */
47
49
  function DocumentService(participantsService, analyticsHelper, fetchCatchup, fetchReconcile, providerEmitCallback, broadcast, getUserId, onErrorHandled, metadataService) {
48
50
  var _this = this;
49
51
  var enableErrorOnFailedDocumentApply = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : false;
50
- var enableSendStepsQueue = arguments.length > 10 && arguments[10] !== undefined ? arguments[10] : false;
52
+ var enableSendStepsQueue = arguments.length > 10 && arguments[10] !== undefined ? arguments[10] : (0, _featureFlags.getCollabProviderFeatureFlag)('sendStepsQueueFF');
51
53
  (0, _classCallCheck2.default)(this, DocumentService);
52
54
  // Fires analytics to editor when collab editor cannot sync up
53
55
  (0, _defineProperty2.default)(this, "stepRejectCounter", 0);
@@ -112,7 +114,7 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
112
114
  _context.prev = 16;
113
115
  _this.stepQueue.resumeQueue();
114
116
  _this.processQueue();
115
- _this.sendStepsFromCurrentState(); // this will eventually retry catchup as it calls commitStep which will either catchup on onStepsAdded or onErrorHandled
117
+ _this.sendStepsFromCurrentState(); // this will eventually retry catchup as it calls throttledCommitStep which will either catchup on onStepsAdded or onErrorHandled
116
118
  _this.stepRejectCounter = 0;
117
119
  return _context.finish(16);
118
120
  case 22:
@@ -663,7 +665,6 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
663
665
  return;
664
666
  }
665
667
  if (this.enableSendStepsQueue) {
666
- // This is where we would call the sendStepsQueue instead of throttledCommitStep
667
668
  // Only send 1% of events to avoid useless logging
668
669
  if (Math.random() < 0.01) {
669
670
  var _this$analyticsHelper29;
@@ -672,7 +673,7 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
672
673
  // Avoid reference issues using a
673
674
  // method outside of the provider
674
675
  // scope
675
- (0, _commitStep.throttledCommitStep)({
676
+ (0, _commitStep.commitStepQueue)({
676
677
  broadcast: this.broadcast,
677
678
  userId: this.getUserId(),
678
679
  clientId: this.clientId,
@@ -4,18 +4,20 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.throttledCommitStep = exports.commitStep = void 0;
7
+ exports.throttledCommitStep = exports.commitStepQueue = exports.commitStep = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
9
  var _countBy = _interopRequireDefault(require("lodash/countBy"));
10
10
  var _throttle = _interopRequireDefault(require("lodash/throttle"));
11
11
  var _const = require("../helpers/const");
12
12
  var _types = require("../types");
13
13
  var _errorTypes = require("../errors/error-types");
14
+ var _utils = require("../helpers/utils");
14
15
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
15
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; }
16
17
  var SEND_STEPS_THROTTLE = 500; // 0.5 second
17
-
18
- var commitStep = exports.commitStep = function commitStep(_ref) {
18
+ var logger = (0, _utils.createLogger)('commit-step', 'black');
19
+ var readyToCommit = true;
20
+ var commitStepQueue = exports.commitStepQueue = function commitStepQueue(_ref) {
19
21
  var broadcast = _ref.broadcast,
20
22
  steps = _ref.steps,
21
23
  version = _ref.version,
@@ -25,6 +27,107 @@ var commitStep = exports.commitStep = function commitStep(_ref) {
25
27
  onErrorHandled = _ref.onErrorHandled,
26
28
  analyticsHelper = _ref.analyticsHelper,
27
29
  emit = _ref.emit;
30
+ if (!readyToCommit) {
31
+ logger('Not ready to commit, skip');
32
+ return;
33
+ }
34
+ // Block other sending request, before ACK
35
+ readyToCommit = false;
36
+ var timer = setTimeout(function () {
37
+ readyToCommit = true;
38
+ logger('reset readyToCommit by timer');
39
+ }, 5000);
40
+ var stepsWithClientAndUserId = steps.map(function (step) {
41
+ return _objectSpread(_objectSpread({}, step.toJSON()), {}, {
42
+ clientId: clientId,
43
+ userId: userId
44
+ });
45
+ });
46
+ var start = new Date().getTime();
47
+ emit('commit-status', {
48
+ status: 'attempt',
49
+ version: version
50
+ });
51
+ try {
52
+ broadcast('steps:commit', {
53
+ steps: stepsWithClientAndUserId,
54
+ version: version,
55
+ userId: userId
56
+ }, function (response) {
57
+ var latency = new Date().getTime() - start;
58
+ if (timer) {
59
+ clearTimeout(timer);
60
+ if (latency <= 400) {
61
+ setTimeout(function () {
62
+ readyToCommit = true;
63
+ logger('reset readyToCommit');
64
+ }, 100);
65
+ } else {
66
+ readyToCommit = true;
67
+ logger('reset readyToCommit');
68
+ }
69
+ }
70
+ if (response.type === _types.AcknowledgementResponseTypes.SUCCESS) {
71
+ onStepsAdded({
72
+ steps: stepsWithClientAndUserId,
73
+ version: response.version
74
+ });
75
+ // Sample only 10% of add steps events to avoid overwhelming the analytics
76
+ if (Math.random() < 0.1) {
77
+ analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(_const.EVENT_ACTION.ADD_STEPS, _const.EVENT_STATUS.SUCCESS_10x_SAMPLED, {
78
+ type: _const.ADD_STEPS_TYPE.ACCEPTED,
79
+ latency: latency,
80
+ stepType: (0, _countBy.default)(stepsWithClientAndUserId, function (stepWithClientAndUserId) {
81
+ return stepWithClientAndUserId.stepType;
82
+ })
83
+ });
84
+ }
85
+ emit('commit-status', {
86
+ status: 'success',
87
+ version: response.version
88
+ });
89
+ } else if (response.type === _types.AcknowledgementResponseTypes.ERROR) {
90
+ onErrorHandled(response.error);
91
+ analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(_const.EVENT_ACTION.ADD_STEPS, _const.EVENT_STATUS.FAILURE, {
92
+ // User tried committing steps but they were rejected because:
93
+ // - 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
94
+ // - VERSION_NUMBER_ALREADY_EXISTS: while storing the steps there was a conflict meaning someone else wrote steps into the database more quickly
95
+ type: response.error.data.code === _errorTypes.NCS_ERROR_CODE.HEAD_VERSION_UPDATE_FAILED || response.error.data.code === _errorTypes.NCS_ERROR_CODE.VERSION_NUMBER_ALREADY_EXISTS ? _const.ADD_STEPS_TYPE.REJECTED : _const.ADD_STEPS_TYPE.ERROR,
96
+ latency: latency
97
+ });
98
+ analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendErrorEvent(response.error, 'Error while adding steps - Acknowledgement Error');
99
+ emit('commit-status', {
100
+ status: 'failure',
101
+ version: version
102
+ });
103
+ } else {
104
+ analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendErrorEvent(
105
+ // @ts-expect-error We didn't type the invalid type case
106
+ new Error("Response type: ".concat((response === null || response === void 0 ? void 0 : response.type) || 'No response type')), 'Error while adding steps - Invalid Acknowledgement');
107
+ emit('commit-status', {
108
+ status: 'failure',
109
+ version: version
110
+ });
111
+ }
112
+ });
113
+ } catch (error) {
114
+ analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendErrorEvent(error, 'Error while adding steps - Broadcast threw exception');
115
+ emit('commit-status', {
116
+ status: 'failure',
117
+ version: version
118
+ });
119
+ }
120
+ };
121
+ var commitStep = exports.commitStep = function commitStep(_ref2) {
122
+ var broadcast = _ref2.broadcast,
123
+ steps = _ref2.steps,
124
+ version = _ref2.version,
125
+ userId = _ref2.userId,
126
+ clientId = _ref2.clientId,
127
+ onStepsAdded = _ref2.onStepsAdded,
128
+ onErrorHandled = _ref2.onErrorHandled,
129
+ analyticsHelper = _ref2.analyticsHelper,
130
+ emit = _ref2.emit;
28
131
  var stepsWithClientAndUserId = steps.map(function (step) {
29
132
  return _objectSpread(_objectSpread({}, step.toJSON()), {}, {
30
133
  clientId: clientId,
@@ -274,6 +274,12 @@ var Provider = exports.Provider = /*#__PURE__*/function (_Emitter) {
274
274
  return _this.userId;
275
275
  }, _this.onErrorHandled, _this.metadataService, _this.config.enableErrorOnFailedDocumentApply, (0, _featureFlags.getCollabProviderFeatureFlag)('sendStepsQueueFF', _this.config.featureFlags));
276
276
  _this.namespaceService = new _namespaceService.NamespaceService();
277
+ if ((0, _featureFlags.getCollabProviderFeatureFlag)('sendStepsQueueFF', _this.config.featureFlags)) {
278
+ _this.sendStepsTimer = setInterval(function () {
279
+ logger('Intervally sendStepsFromCurrentState');
280
+ _this.documentService.sendStepsFromCurrentState(true);
281
+ }, 5000);
282
+ }
277
283
  return _this;
278
284
  }
279
285
  (0, _createClass2.default)(Provider, [{
@@ -468,6 +474,12 @@ var Provider = exports.Provider = /*#__PURE__*/function (_Emitter) {
468
474
  try {
469
475
  (0, _get2.default)((0, _getPrototypeOf2.default)(Provider.prototype), "unsubscribeAll", this).call(this);
470
476
  this.channel.disconnect();
477
+ if ((0, _featureFlags.getCollabProviderFeatureFlag)('sendStepsQueueFF', this.config.featureFlags)) {
478
+ if (this.sendStepsTimer) {
479
+ clearInterval(this.sendStepsTimer);
480
+ this.sendStepsTimer = undefined;
481
+ }
482
+ }
471
483
  } catch (error) {
472
484
  var _this$analyticsHelper11;
473
485
  (_this$analyticsHelper11 = this.analyticsHelper) === null || _this$analyticsHelper11 === void 0 || _this$analyticsHelper11.sendErrorEvent(error, 'Error while shutting down the collab provider');
@@ -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 = "9.17.3";
8
+ var version = exports.version = "9.17.4";
9
9
  var nextMajorVersion = exports.nextMajorVersion = function nextMajorVersion() {
10
10
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
11
11
  };
@@ -3,12 +3,13 @@ import { ACK_MAX_TRY, EVENT_ACTION, EVENT_STATUS } from '../helpers/const';
3
3
  import { getVersion, sendableSteps } from '@atlaskit/prosemirror-collab';
4
4
  import { createLogger, getStepUGCFreeDetails, sleep } from '../helpers/utils';
5
5
  import throttle from 'lodash/throttle';
6
- import { throttledCommitStep } from '../provider/commit-step';
6
+ import { throttledCommitStep, commitStepQueue } from '../provider/commit-step';
7
7
  import { MEASURE_NAME, startMeasure, stopMeasure } from '../analytics/performance';
8
8
  import { JSONTransformer } from '@atlaskit/editor-json-transformer';
9
9
  import { MAX_STEP_REJECTED_ERROR, MAX_STEP_REJECTED_ERROR_AGGRESSIVE } from '../provider';
10
10
  import { catchup } from './catchup';
11
11
  import { StepQueueState } from './step-queue-state';
12
+ import { getCollabProviderFeatureFlag } from '../feature-flags';
12
13
  import { CantSyncUpError, INTERNAL_ERROR_CODE, UpdateDocumentError } from '../errors/error-types';
13
14
  const CATCHUP_THROTTLE = 1 * 1000; // 1 second
14
15
 
@@ -30,8 +31,9 @@ export class DocumentService {
30
31
  * @param onErrorHandled - Callback to handle
31
32
  * @param metadataService
32
33
  * @param enableErrorOnFailedDocumentApply - Enable failed document update exceptions.
34
+ * @param enableSendStepsQueue - Enable send steps queue.
33
35
  */
34
- constructor(participantsService, analyticsHelper, fetchCatchup, fetchReconcile, providerEmitCallback, broadcast, getUserId, onErrorHandled, metadataService, enableErrorOnFailedDocumentApply = false, enableSendStepsQueue = false) {
36
+ constructor(participantsService, analyticsHelper, fetchCatchup, fetchReconcile, providerEmitCallback, broadcast, getUserId, onErrorHandled, metadataService, enableErrorOnFailedDocumentApply = false, enableSendStepsQueue = getCollabProviderFeatureFlag('sendStepsQueueFF')) {
35
37
  // Fires analytics to editor when collab editor cannot sync up
36
38
  _defineProperty(this, "stepRejectCounter", 0);
37
39
  _defineProperty(this, "aggressiveCatchup", false);
@@ -82,7 +84,7 @@ export class DocumentService {
82
84
  } finally {
83
85
  this.stepQueue.resumeQueue();
84
86
  this.processQueue();
85
- this.sendStepsFromCurrentState(); // this will eventually retry catchup as it calls commitStep which will either catchup on onStepsAdded or onErrorHandled
87
+ this.sendStepsFromCurrentState(); // this will eventually retry catchup as it calls throttledCommitStep which will either catchup on onStepsAdded or onErrorHandled
86
88
  this.stepRejectCounter = 0;
87
89
  }
88
90
  });
@@ -561,7 +563,6 @@ export class DocumentService {
561
563
  return;
562
564
  }
563
565
  if (this.enableSendStepsQueue) {
564
- // This is where we would call the sendStepsQueue instead of throttledCommitStep
565
566
  // Only send 1% of events to avoid useless logging
566
567
  if (Math.random() < 0.01) {
567
568
  var _this$analyticsHelper29;
@@ -570,7 +571,7 @@ export class DocumentService {
570
571
  // Avoid reference issues using a
571
572
  // method outside of the provider
572
573
  // scope
573
- throttledCommitStep({
574
+ commitStepQueue({
574
575
  broadcast: this.broadcast,
575
576
  userId: this.getUserId(),
576
577
  clientId: this.clientId,
@@ -3,8 +3,109 @@ import throttle from 'lodash/throttle';
3
3
  import { ADD_STEPS_TYPE, EVENT_ACTION, EVENT_STATUS } from '../helpers/const';
4
4
  import { AcknowledgementResponseTypes } from '../types';
5
5
  import { NCS_ERROR_CODE } from '../errors/error-types';
6
+ import { createLogger } from '../helpers/utils';
6
7
  const SEND_STEPS_THROTTLE = 500; // 0.5 second
7
-
8
+ const logger = createLogger('commit-step', 'black');
9
+ let readyToCommit = true;
10
+ export const commitStepQueue = ({
11
+ broadcast,
12
+ steps,
13
+ version,
14
+ userId,
15
+ clientId,
16
+ onStepsAdded,
17
+ onErrorHandled,
18
+ analyticsHelper,
19
+ emit
20
+ }) => {
21
+ if (!readyToCommit) {
22
+ logger('Not ready to commit, skip');
23
+ return;
24
+ }
25
+ // Block other sending request, before ACK
26
+ readyToCommit = false;
27
+ const timer = setTimeout(() => {
28
+ readyToCommit = true;
29
+ logger('reset readyToCommit by timer');
30
+ }, 5000);
31
+ const stepsWithClientAndUserId = steps.map(step => ({
32
+ ...step.toJSON(),
33
+ clientId,
34
+ userId
35
+ }));
36
+ const start = new Date().getTime();
37
+ emit('commit-status', {
38
+ status: 'attempt',
39
+ version
40
+ });
41
+ try {
42
+ broadcast('steps:commit', {
43
+ steps: stepsWithClientAndUserId,
44
+ version,
45
+ userId
46
+ }, response => {
47
+ const latency = new Date().getTime() - start;
48
+ if (timer) {
49
+ clearTimeout(timer);
50
+ if (latency <= 400) {
51
+ setTimeout(() => {
52
+ readyToCommit = true;
53
+ logger('reset readyToCommit');
54
+ }, 100);
55
+ } else {
56
+ readyToCommit = true;
57
+ logger('reset readyToCommit');
58
+ }
59
+ }
60
+ if (response.type === AcknowledgementResponseTypes.SUCCESS) {
61
+ onStepsAdded({
62
+ steps: stepsWithClientAndUserId,
63
+ version: response.version
64
+ });
65
+ // Sample only 10% of add steps events to avoid overwhelming the analytics
66
+ if (Math.random() < 0.1) {
67
+ analyticsHelper === null || analyticsHelper === void 0 ? void 0 : analyticsHelper.sendActionEvent(EVENT_ACTION.ADD_STEPS, EVENT_STATUS.SUCCESS_10x_SAMPLED, {
68
+ type: ADD_STEPS_TYPE.ACCEPTED,
69
+ latency,
70
+ stepType: countBy(stepsWithClientAndUserId, stepWithClientAndUserId => stepWithClientAndUserId.stepType)
71
+ });
72
+ }
73
+ emit('commit-status', {
74
+ status: 'success',
75
+ version: response.version
76
+ });
77
+ } else if (response.type === AcknowledgementResponseTypes.ERROR) {
78
+ onErrorHandled(response.error);
79
+ analyticsHelper === null || analyticsHelper === void 0 ? void 0 : analyticsHelper.sendActionEvent(EVENT_ACTION.ADD_STEPS, EVENT_STATUS.FAILURE, {
80
+ // User tried committing steps but they were rejected because:
81
+ // - 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
82
+ // - VERSION_NUMBER_ALREADY_EXISTS: while storing the steps there was a conflict meaning someone else wrote steps into the database more quickly
83
+ 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,
84
+ latency
85
+ });
86
+ analyticsHelper === null || analyticsHelper === void 0 ? void 0 : analyticsHelper.sendErrorEvent(response.error, 'Error while adding steps - Acknowledgement Error');
87
+ emit('commit-status', {
88
+ status: 'failure',
89
+ version
90
+ });
91
+ } else {
92
+ analyticsHelper === null || analyticsHelper === void 0 ? void 0 : analyticsHelper.sendErrorEvent(
93
+ // @ts-expect-error We didn't type the invalid type case
94
+ new Error(`Response type: ${(response === null || response === void 0 ? void 0 : response.type) || 'No response type'}`), 'Error while adding steps - Invalid Acknowledgement');
95
+ emit('commit-status', {
96
+ status: 'failure',
97
+ version
98
+ });
99
+ }
100
+ });
101
+ } catch (error) {
102
+ analyticsHelper === null || analyticsHelper === void 0 ? void 0 : analyticsHelper.sendErrorEvent(error, 'Error while adding steps - Broadcast threw exception');
103
+ emit('commit-status', {
104
+ status: 'failure',
105
+ version
106
+ });
107
+ }
108
+ };
8
109
  export const commitStep = ({
9
110
  broadcast,
10
111
  steps,
@@ -227,6 +227,12 @@ export class Provider extends Emitter {
227
227
  this.metadataService = new MetadataService(this.emitCallback, this.channel.sendMetadata);
228
228
  this.documentService = new DocumentService(this.participantsService, this.analyticsHelper, this.channel.fetchCatchup, this.channel.fetchReconcile, this.emitCallback, this.channel.broadcast, () => this.userId, this.onErrorHandled, this.metadataService, this.config.enableErrorOnFailedDocumentApply, getCollabProviderFeatureFlag('sendStepsQueueFF', this.config.featureFlags));
229
229
  this.namespaceService = new NamespaceService();
230
+ if (getCollabProviderFeatureFlag('sendStepsQueueFF', this.config.featureFlags)) {
231
+ this.sendStepsTimer = setInterval(() => {
232
+ logger('Intervally sendStepsFromCurrentState');
233
+ this.documentService.sendStepsFromCurrentState(true);
234
+ }, 5000);
235
+ }
230
236
  }
231
237
  /**
232
238
  * Initialisation logic, called by Jira with a dummy getState function, deprecated in favour of the setup method which allows more configuration
@@ -398,6 +404,12 @@ export class Provider extends Emitter {
398
404
  try {
399
405
  super.unsubscribeAll();
400
406
  this.channel.disconnect();
407
+ if (getCollabProviderFeatureFlag('sendStepsQueueFF', this.config.featureFlags)) {
408
+ if (this.sendStepsTimer) {
409
+ clearInterval(this.sendStepsTimer);
410
+ this.sendStepsTimer = undefined;
411
+ }
412
+ }
401
413
  } catch (error) {
402
414
  var _this$analyticsHelper11;
403
415
  (_this$analyticsHelper11 = this.analyticsHelper) === null || _this$analyticsHelper11 === void 0 ? void 0 : _this$analyticsHelper11.sendErrorEvent(error, 'Error while shutting down the collab provider');
@@ -1,5 +1,5 @@
1
1
  export const name = "@atlaskit/collab-provider";
2
- export const version = "9.17.3";
2
+ export const version = "9.17.4";
3
3
  export const nextMajorVersion = () => {
4
4
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
5
5
  };
@@ -9,12 +9,13 @@ import { ACK_MAX_TRY, EVENT_ACTION, EVENT_STATUS } from '../helpers/const';
9
9
  import { getVersion, sendableSteps } from '@atlaskit/prosemirror-collab';
10
10
  import { createLogger, getStepUGCFreeDetails, sleep } from '../helpers/utils';
11
11
  import throttle from 'lodash/throttle';
12
- import { throttledCommitStep } from '../provider/commit-step';
12
+ import { throttledCommitStep, commitStepQueue } from '../provider/commit-step';
13
13
  import { MEASURE_NAME, startMeasure, stopMeasure } from '../analytics/performance';
14
14
  import { JSONTransformer } from '@atlaskit/editor-json-transformer';
15
15
  import { MAX_STEP_REJECTED_ERROR, MAX_STEP_REJECTED_ERROR_AGGRESSIVE } from '../provider';
16
16
  import { catchup } from './catchup';
17
17
  import { StepQueueState } from './step-queue-state';
18
+ import { getCollabProviderFeatureFlag } from '../feature-flags';
18
19
  import { CantSyncUpError, INTERNAL_ERROR_CODE, UpdateDocumentError } from '../errors/error-types';
19
20
  var CATCHUP_THROTTLE = 1 * 1000; // 1 second
20
21
 
@@ -36,11 +37,12 @@ export var DocumentService = /*#__PURE__*/function () {
36
37
  * @param onErrorHandled - Callback to handle
37
38
  * @param metadataService
38
39
  * @param enableErrorOnFailedDocumentApply - Enable failed document update exceptions.
40
+ * @param enableSendStepsQueue - Enable send steps queue.
39
41
  */
40
42
  function DocumentService(participantsService, analyticsHelper, fetchCatchup, fetchReconcile, providerEmitCallback, broadcast, getUserId, onErrorHandled, metadataService) {
41
43
  var _this = this;
42
44
  var enableErrorOnFailedDocumentApply = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : false;
43
- var enableSendStepsQueue = arguments.length > 10 && arguments[10] !== undefined ? arguments[10] : false;
45
+ var enableSendStepsQueue = arguments.length > 10 && arguments[10] !== undefined ? arguments[10] : getCollabProviderFeatureFlag('sendStepsQueueFF');
44
46
  _classCallCheck(this, DocumentService);
45
47
  // Fires analytics to editor when collab editor cannot sync up
46
48
  _defineProperty(this, "stepRejectCounter", 0);
@@ -105,7 +107,7 @@ export var DocumentService = /*#__PURE__*/function () {
105
107
  _context.prev = 16;
106
108
  _this.stepQueue.resumeQueue();
107
109
  _this.processQueue();
108
- _this.sendStepsFromCurrentState(); // this will eventually retry catchup as it calls commitStep which will either catchup on onStepsAdded or onErrorHandled
110
+ _this.sendStepsFromCurrentState(); // this will eventually retry catchup as it calls throttledCommitStep which will either catchup on onStepsAdded or onErrorHandled
109
111
  _this.stepRejectCounter = 0;
110
112
  return _context.finish(16);
111
113
  case 22:
@@ -656,7 +658,6 @@ export var DocumentService = /*#__PURE__*/function () {
656
658
  return;
657
659
  }
658
660
  if (this.enableSendStepsQueue) {
659
- // This is where we would call the sendStepsQueue instead of throttledCommitStep
660
661
  // Only send 1% of events to avoid useless logging
661
662
  if (Math.random() < 0.01) {
662
663
  var _this$analyticsHelper29;
@@ -665,7 +666,7 @@ export var DocumentService = /*#__PURE__*/function () {
665
666
  // Avoid reference issues using a
666
667
  // method outside of the provider
667
668
  // scope
668
- throttledCommitStep({
669
+ commitStepQueue({
669
670
  broadcast: this.broadcast,
670
671
  userId: this.getUserId(),
671
672
  clientId: this.clientId,
@@ -6,9 +6,11 @@ import throttle from 'lodash/throttle';
6
6
  import { ADD_STEPS_TYPE, EVENT_ACTION, EVENT_STATUS } from '../helpers/const';
7
7
  import { AcknowledgementResponseTypes } from '../types';
8
8
  import { NCS_ERROR_CODE } from '../errors/error-types';
9
+ import { createLogger } from '../helpers/utils';
9
10
  var SEND_STEPS_THROTTLE = 500; // 0.5 second
10
-
11
- export var commitStep = function commitStep(_ref) {
11
+ var logger = createLogger('commit-step', 'black');
12
+ var readyToCommit = true;
13
+ export var commitStepQueue = function commitStepQueue(_ref) {
12
14
  var broadcast = _ref.broadcast,
13
15
  steps = _ref.steps,
14
16
  version = _ref.version,
@@ -18,6 +20,107 @@ export var commitStep = function commitStep(_ref) {
18
20
  onErrorHandled = _ref.onErrorHandled,
19
21
  analyticsHelper = _ref.analyticsHelper,
20
22
  emit = _ref.emit;
23
+ if (!readyToCommit) {
24
+ logger('Not ready to commit, skip');
25
+ return;
26
+ }
27
+ // Block other sending request, before ACK
28
+ readyToCommit = false;
29
+ var timer = setTimeout(function () {
30
+ readyToCommit = true;
31
+ logger('reset readyToCommit by timer');
32
+ }, 5000);
33
+ var stepsWithClientAndUserId = steps.map(function (step) {
34
+ return _objectSpread(_objectSpread({}, step.toJSON()), {}, {
35
+ clientId: clientId,
36
+ userId: userId
37
+ });
38
+ });
39
+ var start = new Date().getTime();
40
+ emit('commit-status', {
41
+ status: 'attempt',
42
+ version: version
43
+ });
44
+ try {
45
+ broadcast('steps:commit', {
46
+ steps: stepsWithClientAndUserId,
47
+ version: version,
48
+ userId: userId
49
+ }, function (response) {
50
+ var latency = new Date().getTime() - start;
51
+ if (timer) {
52
+ clearTimeout(timer);
53
+ if (latency <= 400) {
54
+ setTimeout(function () {
55
+ readyToCommit = true;
56
+ logger('reset readyToCommit');
57
+ }, 100);
58
+ } else {
59
+ readyToCommit = true;
60
+ logger('reset readyToCommit');
61
+ }
62
+ }
63
+ if (response.type === AcknowledgementResponseTypes.SUCCESS) {
64
+ onStepsAdded({
65
+ steps: stepsWithClientAndUserId,
66
+ version: response.version
67
+ });
68
+ // Sample only 10% of add steps events to avoid overwhelming the analytics
69
+ if (Math.random() < 0.1) {
70
+ analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(EVENT_ACTION.ADD_STEPS, EVENT_STATUS.SUCCESS_10x_SAMPLED, {
71
+ type: ADD_STEPS_TYPE.ACCEPTED,
72
+ latency: latency,
73
+ stepType: countBy(stepsWithClientAndUserId, function (stepWithClientAndUserId) {
74
+ return stepWithClientAndUserId.stepType;
75
+ })
76
+ });
77
+ }
78
+ emit('commit-status', {
79
+ status: 'success',
80
+ version: response.version
81
+ });
82
+ } else if (response.type === AcknowledgementResponseTypes.ERROR) {
83
+ onErrorHandled(response.error);
84
+ analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(EVENT_ACTION.ADD_STEPS, EVENT_STATUS.FAILURE, {
85
+ // User tried committing steps but they were rejected because:
86
+ // - 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
87
+ // - VERSION_NUMBER_ALREADY_EXISTS: while storing the steps there was a conflict meaning someone else wrote steps into the database more quickly
88
+ 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,
89
+ latency: latency
90
+ });
91
+ analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendErrorEvent(response.error, 'Error while adding steps - Acknowledgement Error');
92
+ emit('commit-status', {
93
+ status: 'failure',
94
+ version: version
95
+ });
96
+ } else {
97
+ analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendErrorEvent(
98
+ // @ts-expect-error We didn't type the invalid type case
99
+ new Error("Response type: ".concat((response === null || response === void 0 ? void 0 : response.type) || 'No response type')), 'Error while adding steps - Invalid Acknowledgement');
100
+ emit('commit-status', {
101
+ status: 'failure',
102
+ version: version
103
+ });
104
+ }
105
+ });
106
+ } catch (error) {
107
+ analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendErrorEvent(error, 'Error while adding steps - Broadcast threw exception');
108
+ emit('commit-status', {
109
+ status: 'failure',
110
+ version: version
111
+ });
112
+ }
113
+ };
114
+ export var commitStep = function commitStep(_ref2) {
115
+ var broadcast = _ref2.broadcast,
116
+ steps = _ref2.steps,
117
+ version = _ref2.version,
118
+ userId = _ref2.userId,
119
+ clientId = _ref2.clientId,
120
+ onStepsAdded = _ref2.onStepsAdded,
121
+ onErrorHandled = _ref2.onErrorHandled,
122
+ analyticsHelper = _ref2.analyticsHelper,
123
+ emit = _ref2.emit;
21
124
  var stepsWithClientAndUserId = steps.map(function (step) {
22
125
  return _objectSpread(_objectSpread({}, step.toJSON()), {}, {
23
126
  clientId: clientId,
@@ -267,6 +267,12 @@ export var Provider = /*#__PURE__*/function (_Emitter) {
267
267
  return _this.userId;
268
268
  }, _this.onErrorHandled, _this.metadataService, _this.config.enableErrorOnFailedDocumentApply, getCollabProviderFeatureFlag('sendStepsQueueFF', _this.config.featureFlags));
269
269
  _this.namespaceService = new NamespaceService();
270
+ if (getCollabProviderFeatureFlag('sendStepsQueueFF', _this.config.featureFlags)) {
271
+ _this.sendStepsTimer = setInterval(function () {
272
+ logger('Intervally sendStepsFromCurrentState');
273
+ _this.documentService.sendStepsFromCurrentState(true);
274
+ }, 5000);
275
+ }
270
276
  return _this;
271
277
  }
272
278
  _createClass(Provider, [{
@@ -461,6 +467,12 @@ export var Provider = /*#__PURE__*/function (_Emitter) {
461
467
  try {
462
468
  _get(_getPrototypeOf(Provider.prototype), "unsubscribeAll", this).call(this);
463
469
  this.channel.disconnect();
470
+ if (getCollabProviderFeatureFlag('sendStepsQueueFF', this.config.featureFlags)) {
471
+ if (this.sendStepsTimer) {
472
+ clearInterval(this.sendStepsTimer);
473
+ this.sendStepsTimer = undefined;
474
+ }
475
+ }
464
476
  } catch (error) {
465
477
  var _this$analyticsHelper11;
466
478
  (_this$analyticsHelper11 = this.analyticsHelper) === null || _this$analyticsHelper11 === void 0 || _this$analyticsHelper11.sendErrorEvent(error, 'Error while shutting down the collab provider');
@@ -1,5 +1,5 @@
1
1
  export var name = "@atlaskit/collab-provider";
2
- export var version = "9.17.3";
2
+ export var version = "9.17.4";
3
3
  export var nextMajorVersion = function nextMajorVersion() {
4
4
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
5
5
  };
@@ -39,6 +39,7 @@ export declare class DocumentService {
39
39
  * @param onErrorHandled - Callback to handle
40
40
  * @param metadataService
41
41
  * @param enableErrorOnFailedDocumentApply - Enable failed document update exceptions.
42
+ * @param enableSendStepsQueue - Enable send steps queue.
42
43
  */
43
44
  constructor(participantsService: ParticipantsService, analyticsHelper: AnalyticsHelper | undefined, fetchCatchup: (fromVersion: number, clientId: number | string | undefined) => Promise<CatchupResponse>, fetchReconcile: (currentStateDoc: string) => Promise<ReconcileResponse>, providerEmitCallback: (evt: keyof CollabEvents, data: any) => void, broadcast: <K extends keyof ChannelEvent>(type: K, data: Omit<ChannelEvent[K], 'timestamp'>, callback?: Function) => void, getUserId: () => string | undefined, onErrorHandled: (error: InternalError) => void, metadataService: MetadataService, enableErrorOnFailedDocumentApply?: boolean, enableSendStepsQueue?: boolean);
44
45
  /**
@@ -1,9 +1,20 @@
1
1
  /// <reference types="lodash" />
2
- import { ChannelEvent, StepsPayload } from '../types';
2
+ import type { ChannelEvent, StepsPayload } from '../types';
3
3
  import type { CollabCommitStatusEventPayload, CollabEvents } from '@atlaskit/editor-common/collab';
4
4
  import type { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
5
- import AnalyticsHelper from '../analytics/analytics-helper';
5
+ import type AnalyticsHelper from '../analytics/analytics-helper';
6
6
  import type { InternalError } from '../errors/error-types';
7
+ export declare const commitStepQueue: ({ broadcast, steps, version, userId, clientId, onStepsAdded, onErrorHandled, analyticsHelper, emit, }: {
8
+ broadcast: <K extends keyof ChannelEvent>(type: K, data: Omit<ChannelEvent[K], "timestamp">, callback?: Function) => void;
9
+ steps: readonly ProseMirrorStep[];
10
+ version: number;
11
+ userId: string;
12
+ clientId: number | string;
13
+ onStepsAdded: (data: StepsPayload) => void;
14
+ onErrorHandled: (error: InternalError) => void;
15
+ analyticsHelper?: AnalyticsHelper | undefined;
16
+ emit: (evt: keyof CollabEvents, data: CollabCommitStatusEventPayload) => void;
17
+ }) => void;
7
18
  export declare const commitStep: ({ broadcast, steps, version, userId, clientId, onStepsAdded, onErrorHandled, analyticsHelper, emit, }: {
8
19
  broadcast: <K extends keyof ChannelEvent>(type: K, data: Omit<ChannelEvent[K], "timestamp">, callback?: Function) => void;
9
20
  steps: readonly ProseMirrorStep[];
@@ -21,6 +21,7 @@ export declare class Provider extends Emitter<CollabEvents> implements BaseEvent
21
21
  private userId?;
22
22
  private presenceUpdateTimeout?;
23
23
  private disconnectedAt?;
24
+ private sendStepsTimer?;
24
25
  private readonly participantsService;
25
26
  private readonly metadataService;
26
27
  private readonly documentService;
@@ -39,6 +39,7 @@ export declare class DocumentService {
39
39
  * @param onErrorHandled - Callback to handle
40
40
  * @param metadataService
41
41
  * @param enableErrorOnFailedDocumentApply - Enable failed document update exceptions.
42
+ * @param enableSendStepsQueue - Enable send steps queue.
42
43
  */
43
44
  constructor(participantsService: ParticipantsService, analyticsHelper: AnalyticsHelper | undefined, fetchCatchup: (fromVersion: number, clientId: number | string | undefined) => Promise<CatchupResponse>, fetchReconcile: (currentStateDoc: string) => Promise<ReconcileResponse>, providerEmitCallback: (evt: keyof CollabEvents, data: any) => void, broadcast: <K extends keyof ChannelEvent>(type: K, data: Omit<ChannelEvent[K], 'timestamp'>, callback?: Function) => void, getUserId: () => string | undefined, onErrorHandled: (error: InternalError) => void, metadataService: MetadataService, enableErrorOnFailedDocumentApply?: boolean, enableSendStepsQueue?: boolean);
44
45
  /**
@@ -1,9 +1,20 @@
1
1
  /// <reference types="lodash" />
2
- import { ChannelEvent, StepsPayload } from '../types';
2
+ import type { ChannelEvent, StepsPayload } from '../types';
3
3
  import type { CollabCommitStatusEventPayload, CollabEvents } from '@atlaskit/editor-common/collab';
4
4
  import type { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
5
- import AnalyticsHelper from '../analytics/analytics-helper';
5
+ import type AnalyticsHelper from '../analytics/analytics-helper';
6
6
  import type { InternalError } from '../errors/error-types';
7
+ export declare const commitStepQueue: ({ broadcast, steps, version, userId, clientId, onStepsAdded, onErrorHandled, analyticsHelper, emit, }: {
8
+ broadcast: <K extends keyof ChannelEvent>(type: K, data: Omit<ChannelEvent[K], "timestamp">, callback?: Function) => void;
9
+ steps: readonly ProseMirrorStep[];
10
+ version: number;
11
+ userId: string;
12
+ clientId: number | string;
13
+ onStepsAdded: (data: StepsPayload) => void;
14
+ onErrorHandled: (error: InternalError) => void;
15
+ analyticsHelper?: AnalyticsHelper | undefined;
16
+ emit: (evt: keyof CollabEvents, data: CollabCommitStatusEventPayload) => void;
17
+ }) => void;
7
18
  export declare const commitStep: ({ broadcast, steps, version, userId, clientId, onStepsAdded, onErrorHandled, analyticsHelper, emit, }: {
8
19
  broadcast: <K extends keyof ChannelEvent>(type: K, data: Omit<ChannelEvent[K], "timestamp">, callback?: Function) => void;
9
20
  steps: readonly ProseMirrorStep[];
@@ -21,6 +21,7 @@ export declare class Provider extends Emitter<CollabEvents> implements BaseEvent
21
21
  private userId?;
22
22
  private presenceUpdateTimeout?;
23
23
  private disconnectedAt?;
24
+ private sendStepsTimer?;
24
25
  private readonly participantsService;
25
26
  private readonly metadataService;
26
27
  private readonly documentService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/collab-provider",
3
- "version": "9.17.3",
3
+ "version": "9.17.4",
4
4
  "description": "A provider for collaborative editing.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -36,7 +36,7 @@
36
36
  "dependencies": {
37
37
  "@atlaskit/analytics-gas-types": "^5.1.0",
38
38
  "@atlaskit/analytics-listeners": "^8.7.0",
39
- "@atlaskit/editor-common": "^76.23.0",
39
+ "@atlaskit/editor-common": "^76.24.0",
40
40
  "@atlaskit/editor-json-transformer": "^8.10.0",
41
41
  "@atlaskit/editor-prosemirror": "1.1.0",
42
42
  "@atlaskit/prosemirror-collab": "^0.2.0",