@atlaskit/collab-provider 9.7.2 → 9.7.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/channel.js +95 -3
  3. package/dist/cjs/document/catchup.js +3 -3
  4. package/dist/cjs/errors/error-code-mapper.js +7 -1
  5. package/dist/cjs/errors/error-types.js +5 -0
  6. package/dist/cjs/version-wrapper.js +1 -1
  7. package/dist/cjs/version.json +1 -1
  8. package/dist/es2019/channel.js +66 -4
  9. package/dist/es2019/document/catchup.js +1 -1
  10. package/dist/es2019/errors/error-code-mapper.js +7 -1
  11. package/dist/es2019/errors/error-types.js +5 -0
  12. package/dist/es2019/version-wrapper.js +1 -1
  13. package/dist/es2019/version.json +1 -1
  14. package/dist/esm/channel.js +96 -4
  15. package/dist/esm/document/catchup.js +1 -1
  16. package/dist/esm/errors/error-code-mapper.js +7 -1
  17. package/dist/esm/errors/error-types.js +5 -0
  18. package/dist/esm/version-wrapper.js +1 -1
  19. package/dist/esm/version.json +1 -1
  20. package/dist/types/channel.d.ts +12 -2
  21. package/dist/types/document/catchup.d.ts +1 -1
  22. package/dist/types/document/document-service.d.ts +2 -2
  23. package/dist/types/errors/error-types.d.ts +20 -2
  24. package/dist/types/helpers/utils.d.ts +1 -1
  25. package/dist/types/provider/commit-step.d.ts +1 -1
  26. package/dist/types/provider/index.d.ts +2 -2
  27. package/dist/types/types.d.ts +10 -1
  28. package/dist/types-ts4.5/channel.d.ts +12 -2
  29. package/dist/types-ts4.5/document/catchup.d.ts +1 -1
  30. package/dist/types-ts4.5/document/document-service.d.ts +2 -2
  31. package/dist/types-ts4.5/errors/error-types.d.ts +20 -2
  32. package/dist/types-ts4.5/helpers/utils.d.ts +1 -1
  33. package/dist/types-ts4.5/provider/commit-step.d.ts +1 -1
  34. package/dist/types-ts4.5/provider/index.d.ts +2 -2
  35. package/dist/types-ts4.5/types.d.ts +10 -1
  36. package/package.json +4 -6
  37. package/report.api.md +10 -3
  38. package/tmp/api-report-tmp.d.ts +10 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @atlaskit/collab-provider
2
2
 
3
+ ## 9.7.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [`4e6f1bf8511`](https://bitbucket.org/atlassian/atlassian-frontend/commits/4e6f1bf8511) - [ED-19233] Import prosemirror libraries from internal facade package
8
+
9
+ ## 9.7.3
10
+
11
+ ### Patch Changes
12
+
13
+ - [`04fa8eb5246`](https://bitbucket.org/atlassian/atlassian-frontend/commits/04fa8eb5246) - Added rate limiting options to collab provider
14
+
3
15
  ## 9.7.2
4
16
 
5
17
  ### Patch Changes
@@ -26,6 +26,9 @@ var _network = _interopRequireDefault(require("./connectivity/network"));
26
26
  var _errorTypes = require("./errors/error-types");
27
27
  var _socketMessageMetrics = require("./helpers/socket-message-metrics");
28
28
  var _featureFlags = require("./feature-flags");
29
+ function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
30
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
31
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
29
32
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
30
33
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
31
34
  function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = (0, _getPrototypeOf2.default)(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = (0, _getPrototypeOf2.default)(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2.default)(this, result); }; }
@@ -38,11 +41,19 @@ var Channel = /*#__PURE__*/function (_Emitter) {
38
41
  var _this;
39
42
  (0, _classCallCheck2.default)(this, Channel);
40
43
  _this = _super.call(this);
44
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "RATE_LIMIT_TYPE_NONE", 0);
45
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "RATE_LIMIT_TYPE_SOFT", 1);
46
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "RATE_LIMIT_TYPE_HARD", 2);
41
47
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "connected", false);
42
48
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "socket", null);
43
49
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "reconnectHelper", null);
44
50
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "initialized", false);
45
51
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "network", null);
52
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "rateLimitWindowDurationMs", 60000);
53
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "rateLimitWindowStartMs", 0);
54
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "stepCounter", 0);
55
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "stepSizeCounter", 0);
56
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "maxStepSize", 0);
46
57
  // read-only getters used for tests
47
58
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "getInitialized", function () {
48
59
  return _this.initialized;
@@ -72,7 +83,7 @@ var Channel = /*#__PURE__*/function (_Emitter) {
72
83
  reason: data.reason,
73
84
  // Potentially incorrect when value of token changes between connecting and connect.
74
85
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
75
- usedCachedToken: _this.token ? true : false
86
+ usedCachedToken: !!_this.token
76
87
  });
77
88
  _this.unsetToken();
78
89
  });
@@ -83,7 +94,7 @@ var Channel = /*#__PURE__*/function (_Emitter) {
83
94
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
84
95
  // Potentially incorrect when value of token changes between connecting and connect.
85
96
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
86
- usedCachedToken: _this.token ? true : false
97
+ usedCachedToken: !!_this.token
87
98
  });
88
99
  (_this$analyticsHelper3 = _this.analyticsHelper) === null || _this$analyticsHelper3 === void 0 ? void 0 : _this$analyticsHelper3.sendErrorEvent(error, 'Error while establishing connection');
89
100
  // If error received with `data`, it means the connection is rejected
@@ -137,7 +148,7 @@ var Channel = /*#__PURE__*/function (_Emitter) {
137
148
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
138
149
  // Potentially incorrect when value of token changes between connecting and connect.
139
150
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
140
- usedCachedToken: _this.token ? true : false
151
+ usedCachedToken: !!_this.token
141
152
  });
142
153
  _this.emit('connected', {
143
154
  sid: _this.socket.id,
@@ -577,6 +588,12 @@ var Channel = /*#__PURE__*/function (_Emitter) {
577
588
  // Ensure the error emit to the provider has the same structure, so we can handle them unified.
578
589
  this.socket.on('connect_error', this.onConnectError);
579
590
  this.socket.on('permission:invalidateToken', this.handlePermissionInvalidateToken);
591
+ this.socket.onAnyOutgoing(function (event) {
592
+ for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
593
+ args[_key - 1] = arguments[_key];
594
+ }
595
+ return _this2.onAnyOutgoingHandler(Date.now(), args);
596
+ });
580
597
 
581
598
  // To trigger reconnection when browser comes back online
582
599
  if (!this.network) {
@@ -590,6 +607,81 @@ var Channel = /*#__PURE__*/function (_Emitter) {
590
607
  // Fired upon a reconnection attempt error (from Socket.IO Manager)
591
608
  this.socket.io.on('reconnect_error', this.onReconnectError);
592
609
  }
610
+ }, {
611
+ key: "onAnyOutgoingHandler",
612
+ value: function onAnyOutgoingHandler(currentTimeMs, args) {
613
+ var rateLimitType = this.config.rateLimitType || this.RATE_LIMIT_TYPE_NONE;
614
+ if (rateLimitType === this.RATE_LIMIT_TYPE_NONE) {
615
+ return;
616
+ }
617
+ var stepLimit = this.config.rateLimitStepCount || 0;
618
+ var stepSizeLimit = this.config.rateLimitTotalStepSize || 0;
619
+ var maxStepSizeLimit = this.config.rateLimitMaxStepSize || 0;
620
+ var _iterator = _createForOfIteratorHelper(args),
621
+ _step;
622
+ try {
623
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
624
+ var arg = _step.value;
625
+ if (arg.type === 'steps:commit') {
626
+ if (currentTimeMs - this.rateLimitWindowStartMs > this.rateLimitWindowDurationMs) {
627
+ // Start a new window
628
+ this.rateLimitWindowStartMs = currentTimeMs;
629
+ this.stepCounter = 0;
630
+ this.stepSizeCounter = 0;
631
+ this.maxStepSize = 0;
632
+ }
633
+ this.stepCounter += arg.steps.length;
634
+ var _iterator2 = _createForOfIteratorHelper(arg.steps),
635
+ _step2;
636
+ try {
637
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
638
+ var step = _step2.value;
639
+ var stepSize = JSON.stringify(step).length;
640
+ this.stepSizeCounter += stepSize;
641
+ if (stepSize > this.maxStepSize) {
642
+ this.maxStepSize = stepSize;
643
+ }
644
+ }
645
+ } catch (err) {
646
+ _iterator2.e(err);
647
+ } finally {
648
+ _iterator2.f();
649
+ }
650
+ if (this.isLimitExceeded(stepLimit, stepSizeLimit, maxStepSizeLimit)) {
651
+ var rateLimitError = {
652
+ message: 'Rate limited',
653
+ data: {
654
+ code: _errorTypes.NCS_ERROR_CODE.RATE_LIMIT_ERROR,
655
+ status: 500,
656
+ meta: {
657
+ rateLimitType: rateLimitType,
658
+ maxStepSize: this.maxStepSize,
659
+ stepSizeCounter: this.stepSizeCounter,
660
+ stepCounter: this.stepCounter
661
+ }
662
+ }
663
+ };
664
+ if (rateLimitType === this.RATE_LIMIT_TYPE_HARD) {
665
+ this.emit('error', rateLimitError);
666
+ throw new Error();
667
+ } else if (rateLimitType === this.RATE_LIMIT_TYPE_SOFT) {
668
+ var _this$analyticsHelper7;
669
+ (_this$analyticsHelper7 = this.analyticsHelper) === null || _this$analyticsHelper7 === void 0 ? void 0 : _this$analyticsHelper7.sendErrorEvent(rateLimitError, 'Rate limited');
670
+ }
671
+ }
672
+ }
673
+ }
674
+ } catch (err) {
675
+ _iterator.e(err);
676
+ } finally {
677
+ _iterator.f();
678
+ }
679
+ }
680
+ }, {
681
+ key: "isLimitExceeded",
682
+ value: function isLimitExceeded(stepLimit, stepSizeLimit, maxStepSizeLimit) {
683
+ return stepLimit > 0 && this.stepCounter > stepLimit || stepSizeLimit > 0 && this.stepSizeCounter > stepSizeLimit || maxStepSizeLimit > 0 && this.maxStepSize > maxStepSizeLimit;
684
+ }
593
685
  }, {
594
686
  key: "disconnect",
595
687
  value: function disconnect() {
@@ -10,7 +10,7 @@ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"))
10
10
  var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
11
11
  var _const = require("../helpers/const");
12
12
  var _utils = require("../helpers/utils");
13
- var _prosemirrorTransform = require("prosemirror-transform");
13
+ var _transform = require("@atlaskit/editor-prosemirror/transform");
14
14
  function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
15
15
  function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
16
16
  function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
@@ -114,11 +114,11 @@ var catchup = /*#__PURE__*/function () {
114
114
  inverted = _ref2.inverted;
115
115
  // Due to @types/prosemirror-transform mismatch with the actual
116
116
  // constructor, hack to set the `inverted`.
117
- var stepMap = new _prosemirrorTransform.StepMap(ranges);
117
+ var stepMap = new _transform.StepMap(ranges);
118
118
  stepMap.inverted = inverted;
119
119
  return stepMap;
120
120
  }); // create Mapping used for Step.map
121
- mapping = new _prosemirrorTransform.Mapping(stepMaps);
121
+ mapping = new _transform.Mapping(stepMaps);
122
122
  logger("".concat(_unconfirmedSteps.length, " unconfirmed steps before rebased: ").concat(JSON.stringify(_unconfirmedSteps)));
123
123
  newUnconfirmedSteps = rebaseSteps(_unconfirmedSteps, mapping);
124
124
  if ((newUnconfirmedSteps === null || newUnconfirmedSteps === void 0 ? void 0 : newUnconfirmedSteps.length) < _unconfirmedSteps.length) {
@@ -103,7 +103,7 @@ var errorCodeMapper = function errorCodeMapper(error) {
103
103
  return {
104
104
  code: _collab.PROVIDER_ERROR_CODE.INTERNAL_SERVICE_ERROR,
105
105
  message: 'Collab Provider experienced an unrecoverable error',
106
- recoverable: false,
106
+ recoverable: true,
107
107
  reason: (_error$data3 = error.data) === null || _error$data3 === void 0 ? void 0 : _error$data3.code,
108
108
  status: 500
109
109
  };
@@ -115,6 +115,12 @@ var errorCodeMapper = function errorCodeMapper(error) {
115
115
  reason: (_error$data4 = error.data) === null || _error$data4 === void 0 ? void 0 : _error$data4.code,
116
116
  status: 500
117
117
  };
118
+ case _errorTypes.NCS_ERROR_CODE.RATE_LIMIT_ERROR:
119
+ return {
120
+ code: _collab.PROVIDER_ERROR_CODE.FAIL_TO_SAVE,
121
+ message: 'Document rate limit',
122
+ recoverable: false
123
+ };
118
124
  default:
119
125
  return;
120
126
  }
@@ -46,6 +46,7 @@ var NCS_ERROR_CODE = /*#__PURE__*/function (NCS_ERROR_CODE) {
46
46
  NCS_ERROR_CODE["INVALID_ACTIVATION_ID"] = "INVALID_ACTIVATION_ID";
47
47
  NCS_ERROR_CODE["INVALID_DOCUMENT_ARI"] = "INVALID_DOCUMENT_ARI";
48
48
  NCS_ERROR_CODE["INVALID_CLOUD_ID"] = "INVALID_CLOUD_ID";
49
+ NCS_ERROR_CODE["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR";
49
50
  return NCS_ERROR_CODE;
50
51
  }({}); // TODO: Import emitted error codes from NCS
51
52
  // NCS Errors
@@ -60,6 +61,10 @@ var NCS_ERROR_CODE = /*#__PURE__*/function (NCS_ERROR_CODE) {
60
61
  * When we try to apply state updates to the editor, if that fails to apply the user can enter an invalid state where no
61
62
  * changes can be saved to NCS.
62
63
  */
64
+ /**
65
+ * The client is trying to send too many messages or messages that are too large. This not expected to be a standard
66
+ * operating condition and should only ever indicate a frontend bug.
67
+ */
63
68
  /**
64
69
  * A union of all possible internal errors, that are mapped to another error if being emitted to the editor.
65
70
  */
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.version = exports.nextMajorVersion = exports.name = void 0;
7
7
  var name = "@atlaskit/collab-provider";
8
8
  exports.name = name;
9
- var version = "9.7.2";
9
+ var version = "9.7.4";
10
10
  exports.version = version;
11
11
  var nextMajorVersion = function nextMajorVersion() {
12
12
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/collab-provider",
3
- "version": "9.7.2",
3
+ "version": "9.7.4",
4
4
  "sideEffects": false
5
5
  }
@@ -8,18 +8,26 @@ import ReconnectHelper from './connectivity/reconnect-helper';
8
8
  import { createDocInitExp } from './analytics/ufo';
9
9
  import { socketIOReasons } from './disconnected-reason-mapper';
10
10
  import Network from './connectivity/network';
11
- import { NotConnectedError, NotInitializedError, INTERNAL_ERROR_CODE } from './errors/error-types';
11
+ import { NotConnectedError, NotInitializedError, INTERNAL_ERROR_CODE, NCS_ERROR_CODE } from './errors/error-types';
12
12
  import { SocketMessageMetrics } from './helpers/socket-message-metrics';
13
13
  import { getCollabProviderFeatureFlag } from './feature-flags';
14
14
  const logger = createLogger('Channel', 'green');
15
15
  export class Channel extends Emitter {
16
16
  constructor(config, analyticsHelper) {
17
17
  super();
18
+ _defineProperty(this, "RATE_LIMIT_TYPE_NONE", 0);
19
+ _defineProperty(this, "RATE_LIMIT_TYPE_SOFT", 1);
20
+ _defineProperty(this, "RATE_LIMIT_TYPE_HARD", 2);
18
21
  _defineProperty(this, "connected", false);
19
22
  _defineProperty(this, "socket", null);
20
23
  _defineProperty(this, "reconnectHelper", null);
21
24
  _defineProperty(this, "initialized", false);
22
25
  _defineProperty(this, "network", null);
26
+ _defineProperty(this, "rateLimitWindowDurationMs", 60000);
27
+ _defineProperty(this, "rateLimitWindowStartMs", 0);
28
+ _defineProperty(this, "stepCounter", 0);
29
+ _defineProperty(this, "stepSizeCounter", 0);
30
+ _defineProperty(this, "maxStepSize", 0);
23
31
  // read-only getters used for tests
24
32
  _defineProperty(this, "getInitialized", () => this.initialized);
25
33
  _defineProperty(this, "getConnected", () => this.connected);
@@ -39,7 +47,7 @@ export class Channel extends Emitter {
39
47
  reason: data.reason,
40
48
  // Potentially incorrect when value of token changes between connecting and connect.
41
49
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
42
- usedCachedToken: this.token ? true : false
50
+ usedCachedToken: !!this.token
43
51
  });
44
52
  this.unsetToken();
45
53
  });
@@ -50,7 +58,7 @@ export class Channel extends Emitter {
50
58
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
51
59
  // Potentially incorrect when value of token changes between connecting and connect.
52
60
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
53
- usedCachedToken: this.token ? true : false
61
+ usedCachedToken: !!this.token
54
62
  });
55
63
  (_this$analyticsHelper3 = this.analyticsHelper) === null || _this$analyticsHelper3 === void 0 ? void 0 : _this$analyticsHelper3.sendErrorEvent(error, 'Error while establishing connection');
56
64
  // If error received with `data`, it means the connection is rejected
@@ -105,7 +113,7 @@ export class Channel extends Emitter {
105
113
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
106
114
  // Potentially incorrect when value of token changes between connecting and connect.
107
115
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
108
- usedCachedToken: this.token ? true : false
116
+ usedCachedToken: !!this.token
109
117
  });
110
118
  this.emit('connected', {
111
119
  sid: this.socket.id,
@@ -444,6 +452,7 @@ export class Channel extends Emitter {
444
452
  // Ensure the error emit to the provider has the same structure, so we can handle them unified.
445
453
  this.socket.on('connect_error', this.onConnectError);
446
454
  this.socket.on('permission:invalidateToken', this.handlePermissionInvalidateToken);
455
+ this.socket.onAnyOutgoing((event, ...args) => this.onAnyOutgoingHandler(Date.now(), args));
447
456
 
448
457
  // To trigger reconnection when browser comes back online
449
458
  if (!this.network) {
@@ -457,6 +466,59 @@ export class Channel extends Emitter {
457
466
  // Fired upon a reconnection attempt error (from Socket.IO Manager)
458
467
  this.socket.io.on('reconnect_error', this.onReconnectError);
459
468
  }
469
+ onAnyOutgoingHandler(currentTimeMs, args) {
470
+ const rateLimitType = this.config.rateLimitType || this.RATE_LIMIT_TYPE_NONE;
471
+ if (rateLimitType === this.RATE_LIMIT_TYPE_NONE) {
472
+ return;
473
+ }
474
+ const stepLimit = this.config.rateLimitStepCount || 0;
475
+ const stepSizeLimit = this.config.rateLimitTotalStepSize || 0;
476
+ const maxStepSizeLimit = this.config.rateLimitMaxStepSize || 0;
477
+ for (const arg of args) {
478
+ if (arg.type === 'steps:commit') {
479
+ if (currentTimeMs - this.rateLimitWindowStartMs > this.rateLimitWindowDurationMs) {
480
+ // Start a new window
481
+ this.rateLimitWindowStartMs = currentTimeMs;
482
+ this.stepCounter = 0;
483
+ this.stepSizeCounter = 0;
484
+ this.maxStepSize = 0;
485
+ }
486
+ this.stepCounter += arg.steps.length;
487
+ for (const step of arg.steps) {
488
+ const stepSize = JSON.stringify(step).length;
489
+ this.stepSizeCounter += stepSize;
490
+ if (stepSize > this.maxStepSize) {
491
+ this.maxStepSize = stepSize;
492
+ }
493
+ }
494
+ if (this.isLimitExceeded(stepLimit, stepSizeLimit, maxStepSizeLimit)) {
495
+ const rateLimitError = {
496
+ message: 'Rate limited',
497
+ data: {
498
+ code: NCS_ERROR_CODE.RATE_LIMIT_ERROR,
499
+ status: 500,
500
+ meta: {
501
+ rateLimitType,
502
+ maxStepSize: this.maxStepSize,
503
+ stepSizeCounter: this.stepSizeCounter,
504
+ stepCounter: this.stepCounter
505
+ }
506
+ }
507
+ };
508
+ if (rateLimitType === this.RATE_LIMIT_TYPE_HARD) {
509
+ this.emit('error', rateLimitError);
510
+ throw new Error();
511
+ } else if (rateLimitType === this.RATE_LIMIT_TYPE_SOFT) {
512
+ var _this$analyticsHelper8;
513
+ (_this$analyticsHelper8 = this.analyticsHelper) === null || _this$analyticsHelper8 === void 0 ? void 0 : _this$analyticsHelper8.sendErrorEvent(rateLimitError, 'Rate limited');
514
+ }
515
+ }
516
+ }
517
+ }
518
+ }
519
+ isLimitExceeded(stepLimit, stepSizeLimit, maxStepSizeLimit) {
520
+ return stepLimit > 0 && this.stepCounter > stepLimit || stepSizeLimit > 0 && this.stepSizeCounter > stepSizeLimit || maxStepSizeLimit > 0 && this.maxStepSize > maxStepSizeLimit;
521
+ }
460
522
  disconnect() {
461
523
  var _this$network;
462
524
  this.unsubscribeAll();
@@ -1,6 +1,6 @@
1
1
  import { EVENT_ACTION, EVENT_STATUS } from '../helpers/const';
2
2
  import { createLogger } from '../helpers/utils';
3
- import { StepMap, Mapping } from 'prosemirror-transform';
3
+ import { StepMap, Mapping } from '@atlaskit/editor-prosemirror/transform';
4
4
  const logger = createLogger('Catchup', 'red');
5
5
 
6
6
  /**
@@ -99,7 +99,7 @@ export const errorCodeMapper = error => {
99
99
  return {
100
100
  code: PROVIDER_ERROR_CODE.INTERNAL_SERVICE_ERROR,
101
101
  message: 'Collab Provider experienced an unrecoverable error',
102
- recoverable: false,
102
+ recoverable: true,
103
103
  reason: (_error$data3 = error.data) === null || _error$data3 === void 0 ? void 0 : _error$data3.code,
104
104
  status: 500
105
105
  };
@@ -111,6 +111,12 @@ export const errorCodeMapper = error => {
111
111
  reason: (_error$data4 = error.data) === null || _error$data4 === void 0 ? void 0 : _error$data4.code,
112
112
  status: 500
113
113
  };
114
+ case NCS_ERROR_CODE.RATE_LIMIT_ERROR:
115
+ return {
116
+ code: PROVIDER_ERROR_CODE.FAIL_TO_SAVE,
117
+ message: 'Document rate limit',
118
+ recoverable: false
119
+ };
114
120
  default:
115
121
  return;
116
122
  }
@@ -32,6 +32,7 @@ export let NCS_ERROR_CODE = /*#__PURE__*/function (NCS_ERROR_CODE) {
32
32
  NCS_ERROR_CODE["INVALID_ACTIVATION_ID"] = "INVALID_ACTIVATION_ID";
33
33
  NCS_ERROR_CODE["INVALID_DOCUMENT_ARI"] = "INVALID_DOCUMENT_ARI";
34
34
  NCS_ERROR_CODE["INVALID_CLOUD_ID"] = "INVALID_CLOUD_ID";
35
+ NCS_ERROR_CODE["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR";
35
36
  return NCS_ERROR_CODE;
36
37
  }({});
37
38
 
@@ -49,6 +50,10 @@ export let NCS_ERROR_CODE = /*#__PURE__*/function (NCS_ERROR_CODE) {
49
50
  * When we try to apply state updates to the editor, if that fails to apply the user can enter an invalid state where no
50
51
  * changes can be saved to NCS.
51
52
  */
53
+ /**
54
+ * The client is trying to send too many messages or messages that are too large. This not expected to be a standard
55
+ * operating condition and should only ever indicate a frontend bug.
56
+ */
52
57
  /**
53
58
  * A union of all possible internal errors, that are mapped to another error if being emitted to the editor.
54
59
  */
@@ -1,5 +1,5 @@
1
1
  export const name = "@atlaskit/collab-provider";
2
- export const version = "9.7.2";
2
+ export const version = "9.7.4";
3
3
  export const nextMajorVersion = () => {
4
4
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
5
5
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/collab-provider",
3
- "version": "9.7.2",
3
+ "version": "9.7.4",
4
4
  "sideEffects": false
5
5
  }
@@ -6,6 +6,9 @@ import _inherits from "@babel/runtime/helpers/inherits";
6
6
  import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn";
7
7
  import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";
8
8
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
9
+ function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
10
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
11
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
9
12
  import _regeneratorRuntime from "@babel/runtime/regenerator";
10
13
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
11
14
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
@@ -20,7 +23,7 @@ import ReconnectHelper from './connectivity/reconnect-helper';
20
23
  import { createDocInitExp } from './analytics/ufo';
21
24
  import { socketIOReasons } from './disconnected-reason-mapper';
22
25
  import Network from './connectivity/network';
23
- import { NotConnectedError, NotInitializedError, INTERNAL_ERROR_CODE } from './errors/error-types';
26
+ import { NotConnectedError, NotInitializedError, INTERNAL_ERROR_CODE, NCS_ERROR_CODE } from './errors/error-types';
24
27
  import { SocketMessageMetrics } from './helpers/socket-message-metrics';
25
28
  import { getCollabProviderFeatureFlag } from './feature-flags';
26
29
  var logger = createLogger('Channel', 'green');
@@ -31,11 +34,19 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
31
34
  var _this;
32
35
  _classCallCheck(this, Channel);
33
36
  _this = _super.call(this);
37
+ _defineProperty(_assertThisInitialized(_this), "RATE_LIMIT_TYPE_NONE", 0);
38
+ _defineProperty(_assertThisInitialized(_this), "RATE_LIMIT_TYPE_SOFT", 1);
39
+ _defineProperty(_assertThisInitialized(_this), "RATE_LIMIT_TYPE_HARD", 2);
34
40
  _defineProperty(_assertThisInitialized(_this), "connected", false);
35
41
  _defineProperty(_assertThisInitialized(_this), "socket", null);
36
42
  _defineProperty(_assertThisInitialized(_this), "reconnectHelper", null);
37
43
  _defineProperty(_assertThisInitialized(_this), "initialized", false);
38
44
  _defineProperty(_assertThisInitialized(_this), "network", null);
45
+ _defineProperty(_assertThisInitialized(_this), "rateLimitWindowDurationMs", 60000);
46
+ _defineProperty(_assertThisInitialized(_this), "rateLimitWindowStartMs", 0);
47
+ _defineProperty(_assertThisInitialized(_this), "stepCounter", 0);
48
+ _defineProperty(_assertThisInitialized(_this), "stepSizeCounter", 0);
49
+ _defineProperty(_assertThisInitialized(_this), "maxStepSize", 0);
39
50
  // read-only getters used for tests
40
51
  _defineProperty(_assertThisInitialized(_this), "getInitialized", function () {
41
52
  return _this.initialized;
@@ -65,7 +76,7 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
65
76
  reason: data.reason,
66
77
  // Potentially incorrect when value of token changes between connecting and connect.
67
78
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
68
- usedCachedToken: _this.token ? true : false
79
+ usedCachedToken: !!_this.token
69
80
  });
70
81
  _this.unsetToken();
71
82
  });
@@ -76,7 +87,7 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
76
87
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
77
88
  // Potentially incorrect when value of token changes between connecting and connect.
78
89
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
79
- usedCachedToken: _this.token ? true : false
90
+ usedCachedToken: !!_this.token
80
91
  });
81
92
  (_this$analyticsHelper3 = _this.analyticsHelper) === null || _this$analyticsHelper3 === void 0 ? void 0 : _this$analyticsHelper3.sendErrorEvent(error, 'Error while establishing connection');
82
93
  // If error received with `data`, it means the connection is rejected
@@ -130,7 +141,7 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
130
141
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
131
142
  // Potentially incorrect when value of token changes between connecting and connect.
132
143
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
133
- usedCachedToken: _this.token ? true : false
144
+ usedCachedToken: !!_this.token
134
145
  });
135
146
  _this.emit('connected', {
136
147
  sid: _this.socket.id,
@@ -570,6 +581,12 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
570
581
  // Ensure the error emit to the provider has the same structure, so we can handle them unified.
571
582
  this.socket.on('connect_error', this.onConnectError);
572
583
  this.socket.on('permission:invalidateToken', this.handlePermissionInvalidateToken);
584
+ this.socket.onAnyOutgoing(function (event) {
585
+ for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
586
+ args[_key - 1] = arguments[_key];
587
+ }
588
+ return _this2.onAnyOutgoingHandler(Date.now(), args);
589
+ });
573
590
 
574
591
  // To trigger reconnection when browser comes back online
575
592
  if (!this.network) {
@@ -583,6 +600,81 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
583
600
  // Fired upon a reconnection attempt error (from Socket.IO Manager)
584
601
  this.socket.io.on('reconnect_error', this.onReconnectError);
585
602
  }
603
+ }, {
604
+ key: "onAnyOutgoingHandler",
605
+ value: function onAnyOutgoingHandler(currentTimeMs, args) {
606
+ var rateLimitType = this.config.rateLimitType || this.RATE_LIMIT_TYPE_NONE;
607
+ if (rateLimitType === this.RATE_LIMIT_TYPE_NONE) {
608
+ return;
609
+ }
610
+ var stepLimit = this.config.rateLimitStepCount || 0;
611
+ var stepSizeLimit = this.config.rateLimitTotalStepSize || 0;
612
+ var maxStepSizeLimit = this.config.rateLimitMaxStepSize || 0;
613
+ var _iterator = _createForOfIteratorHelper(args),
614
+ _step;
615
+ try {
616
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
617
+ var arg = _step.value;
618
+ if (arg.type === 'steps:commit') {
619
+ if (currentTimeMs - this.rateLimitWindowStartMs > this.rateLimitWindowDurationMs) {
620
+ // Start a new window
621
+ this.rateLimitWindowStartMs = currentTimeMs;
622
+ this.stepCounter = 0;
623
+ this.stepSizeCounter = 0;
624
+ this.maxStepSize = 0;
625
+ }
626
+ this.stepCounter += arg.steps.length;
627
+ var _iterator2 = _createForOfIteratorHelper(arg.steps),
628
+ _step2;
629
+ try {
630
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
631
+ var step = _step2.value;
632
+ var stepSize = JSON.stringify(step).length;
633
+ this.stepSizeCounter += stepSize;
634
+ if (stepSize > this.maxStepSize) {
635
+ this.maxStepSize = stepSize;
636
+ }
637
+ }
638
+ } catch (err) {
639
+ _iterator2.e(err);
640
+ } finally {
641
+ _iterator2.f();
642
+ }
643
+ if (this.isLimitExceeded(stepLimit, stepSizeLimit, maxStepSizeLimit)) {
644
+ var rateLimitError = {
645
+ message: 'Rate limited',
646
+ data: {
647
+ code: NCS_ERROR_CODE.RATE_LIMIT_ERROR,
648
+ status: 500,
649
+ meta: {
650
+ rateLimitType: rateLimitType,
651
+ maxStepSize: this.maxStepSize,
652
+ stepSizeCounter: this.stepSizeCounter,
653
+ stepCounter: this.stepCounter
654
+ }
655
+ }
656
+ };
657
+ if (rateLimitType === this.RATE_LIMIT_TYPE_HARD) {
658
+ this.emit('error', rateLimitError);
659
+ throw new Error();
660
+ } else if (rateLimitType === this.RATE_LIMIT_TYPE_SOFT) {
661
+ var _this$analyticsHelper7;
662
+ (_this$analyticsHelper7 = this.analyticsHelper) === null || _this$analyticsHelper7 === void 0 ? void 0 : _this$analyticsHelper7.sendErrorEvent(rateLimitError, 'Rate limited');
663
+ }
664
+ }
665
+ }
666
+ }
667
+ } catch (err) {
668
+ _iterator.e(err);
669
+ } finally {
670
+ _iterator.f();
671
+ }
672
+ }
673
+ }, {
674
+ key: "isLimitExceeded",
675
+ value: function isLimitExceeded(stepLimit, stepSizeLimit, maxStepSizeLimit) {
676
+ return stepLimit > 0 && this.stepCounter > stepLimit || stepSizeLimit > 0 && this.stepSizeCounter > stepSizeLimit || maxStepSizeLimit > 0 && this.maxStepSize > maxStepSizeLimit;
677
+ }
586
678
  }, {
587
679
  key: "disconnect",
588
680
  value: function disconnect() {
@@ -5,7 +5,7 @@ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o =
5
5
  function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
6
6
  import { EVENT_ACTION, EVENT_STATUS } from '../helpers/const';
7
7
  import { createLogger } from '../helpers/utils';
8
- import { StepMap, Mapping } from 'prosemirror-transform';
8
+ import { StepMap, Mapping } from '@atlaskit/editor-prosemirror/transform';
9
9
  var logger = createLogger('Catchup', 'red');
10
10
 
11
11
  /**
@@ -99,7 +99,7 @@ export var errorCodeMapper = function errorCodeMapper(error) {
99
99
  return {
100
100
  code: PROVIDER_ERROR_CODE.INTERNAL_SERVICE_ERROR,
101
101
  message: 'Collab Provider experienced an unrecoverable error',
102
- recoverable: false,
102
+ recoverable: true,
103
103
  reason: (_error$data3 = error.data) === null || _error$data3 === void 0 ? void 0 : _error$data3.code,
104
104
  status: 500
105
105
  };
@@ -111,6 +111,12 @@ export var errorCodeMapper = function errorCodeMapper(error) {
111
111
  reason: (_error$data4 = error.data) === null || _error$data4 === void 0 ? void 0 : _error$data4.code,
112
112
  status: 500
113
113
  };
114
+ case NCS_ERROR_CODE.RATE_LIMIT_ERROR:
115
+ return {
116
+ code: PROVIDER_ERROR_CODE.FAIL_TO_SAVE,
117
+ message: 'Document rate limit',
118
+ recoverable: false
119
+ };
114
120
  default:
115
121
  return;
116
122
  }
@@ -41,6 +41,7 @@ export var NCS_ERROR_CODE = /*#__PURE__*/function (NCS_ERROR_CODE) {
41
41
  NCS_ERROR_CODE["INVALID_ACTIVATION_ID"] = "INVALID_ACTIVATION_ID";
42
42
  NCS_ERROR_CODE["INVALID_DOCUMENT_ARI"] = "INVALID_DOCUMENT_ARI";
43
43
  NCS_ERROR_CODE["INVALID_CLOUD_ID"] = "INVALID_CLOUD_ID";
44
+ NCS_ERROR_CODE["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR";
44
45
  return NCS_ERROR_CODE;
45
46
  }({});
46
47
 
@@ -58,6 +59,10 @@ export var NCS_ERROR_CODE = /*#__PURE__*/function (NCS_ERROR_CODE) {
58
59
  * When we try to apply state updates to the editor, if that fails to apply the user can enter an invalid state where no
59
60
  * changes can be saved to NCS.
60
61
  */
62
+ /**
63
+ * The client is trying to send too many messages or messages that are too large. This not expected to be a standard
64
+ * operating condition and should only ever indicate a frontend bug.
65
+ */
61
66
  /**
62
67
  * A union of all possible internal errors, that are mapped to another error if being emitted to the editor.
63
68
  */
@@ -1,5 +1,5 @@
1
1
  export var name = "@atlaskit/collab-provider";
2
- export var version = "9.7.2";
2
+ export var version = "9.7.4";
3
3
  export var nextMajorVersion = function nextMajorVersion() {
4
4
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
5
5
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/collab-provider",
3
- "version": "9.7.2",
3
+ "version": "9.7.4",
4
4
  "sideEffects": false
5
5
  }
@@ -4,16 +4,24 @@ import type { Socket } from 'socket.io-client';
4
4
  import AnalyticsHelper from './analytics/analytics-helper';
5
5
  import type { Metadata } from '@atlaskit/editor-common/collab';
6
6
  export declare class Channel extends Emitter<ChannelEvent> {
7
+ private readonly RATE_LIMIT_TYPE_NONE;
8
+ private readonly RATE_LIMIT_TYPE_SOFT;
9
+ private readonly RATE_LIMIT_TYPE_HARD;
7
10
  private connected;
8
- private config;
11
+ private readonly config;
9
12
  private socket;
10
13
  private reconnectHelper?;
11
14
  private initialized;
12
- private analyticsHelper?;
15
+ private readonly analyticsHelper?;
13
16
  private initExperience?;
14
17
  private token?;
15
18
  private network;
16
19
  private socketMessageMetrics?;
20
+ private readonly rateLimitWindowDurationMs;
21
+ private rateLimitWindowStartMs;
22
+ private stepCounter;
23
+ private stepSizeCounter;
24
+ private maxStepSize;
17
25
  constructor(config: Config, analyticsHelper: AnalyticsHelper);
18
26
  getInitialized: () => boolean;
19
27
  getConnected: () => boolean;
@@ -25,6 +33,8 @@ export declare class Channel extends Emitter<ChannelEvent> {
25
33
  * Connect to collab service using websockets
26
34
  */
27
35
  connect(shouldInitialize?: boolean): void;
36
+ onAnyOutgoingHandler(currentTimeMs: number, args: any[]): void;
37
+ private isLimitExceeded;
28
38
  private handlePermissionInvalidateToken;
29
39
  private onConnectError;
30
40
  private onReconnectError;
@@ -1,5 +1,5 @@
1
1
  import type { CatchupOptions } from '../types';
2
- import { Mapping, Step } from 'prosemirror-transform';
2
+ import { Mapping, Step } from '@atlaskit/editor-prosemirror/transform';
3
3
  /**
4
4
  * Rebase the steps based on the mapping pipeline.
5
5
  * Some steps could be lost, if they are no longer
@@ -2,10 +2,10 @@
2
2
  import AnalyticsHelper from '../analytics/analytics-helper';
3
3
  import { CatchupResponse, ChannelEvent, StepsPayload } from '../types';
4
4
  import type { SyncUpErrorFunction, ResolvedEditorState } from '@atlaskit/editor-common/collab';
5
- import type { Step as ProseMirrorStep } from 'prosemirror-transform';
5
+ import type { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
6
6
  import type { MetadataService } from '../metadata/metadata-service';
7
7
  import type { CollabEvents, CollabInitPayload } from '@atlaskit/editor-common/collab';
8
- import type { EditorState, Transaction } from 'prosemirror-state';
8
+ import type { EditorState, Transaction } from '@atlaskit/editor-prosemirror/state';
9
9
  import { ParticipantsService } from '../participants/participants-service';
10
10
  import type { InternalError } from '../errors/error-types';
11
11
  export declare class DocumentService {
@@ -26,7 +26,8 @@ export declare enum NCS_ERROR_CODE {
26
26
  DYNAMO_ERROR = "DYNAMO_ERROR",
27
27
  INVALID_ACTIVATION_ID = "INVALID_ACTIVATION_ID",
28
28
  INVALID_DOCUMENT_ARI = "INVALID_DOCUMENT_ARI",
29
- INVALID_CLOUD_ID = "INVALID_CLOUD_ID"
29
+ INVALID_CLOUD_ID = "INVALID_CLOUD_ID",
30
+ RATE_LIMIT_ERROR = "RATE_LIMIT_ERROR"
30
31
  }
31
32
  type HeadVersionUpdateFailedError = {
32
33
  message: string;
@@ -236,10 +237,27 @@ export type InternalDocumentUpdateFailure = {
236
237
  status: 500;
237
238
  };
238
239
  };
240
+ /**
241
+ * The client is trying to send too many messages or messages that are too large. This not expected to be a standard
242
+ * operating condition and should only ever indicate a frontend bug.
243
+ */
244
+ export type RateLimitError = {
245
+ message: string;
246
+ data: {
247
+ code: NCS_ERROR_CODE.RATE_LIMIT_ERROR;
248
+ meta: {
249
+ rateLimitType: number;
250
+ maxStepSize: number;
251
+ stepSizeCounter: number;
252
+ stepCounter: number;
253
+ };
254
+ status: 500;
255
+ };
256
+ };
239
257
  /**
240
258
  * A union of all possible internal errors, that are mapped to another error if being emitted to the editor.
241
259
  */
242
- export type InternalError = NCSErrors | DocumentRecoveryError | AddStepsError | CatchUpFailedError | TokenPermissionError | ReconnectionError | ConnectionError | ReconnectionNetworkError | DocumentNotFoundError | InternalDocumentUpdateFailure;
260
+ export type InternalError = NCSErrors | DocumentRecoveryError | AddStepsError | CatchUpFailedError | TokenPermissionError | ReconnectionError | ConnectionError | ReconnectionNetworkError | DocumentNotFoundError | InternalDocumentUpdateFailure | RateLimitError;
243
261
  type ValidEventAttributeType = boolean | string | number;
244
262
  export declare class CustomError extends Error {
245
263
  extraEventAttributes?: {
@@ -1,5 +1,5 @@
1
1
  import type { ProductInformation } from '../types';
2
- import type { Step as ProseMirrorStep } from 'prosemirror-transform';
2
+ import type { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
3
3
  export declare const createLogger: (prefix: string, color?: string) => (msg: string, data?: any) => void;
4
4
  export declare function sleep(ms: number): Promise<unknown>;
5
5
  export declare const getProduct: (productInfo?: ProductInformation) => string;
@@ -1,7 +1,7 @@
1
1
  /// <reference types="lodash" />
2
2
  import { ChannelEvent, StepsPayload } from '../types';
3
3
  import type { CollabCommitStatusEventPayload, CollabEvents } from '@atlaskit/editor-common/collab';
4
- import type { Step as ProseMirrorStep } from 'prosemirror-transform';
4
+ import type { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
5
5
  import AnalyticsHelper from '../analytics/analytics-helper';
6
6
  import type { InternalError } from '../errors/error-types';
7
7
  export declare const commitStep: ({ broadcast, steps, version, userId, clientId, onStepsAdded, onErrorHandled, analyticsHelper, emit, }: {
@@ -1,5 +1,5 @@
1
- import type { EditorState, Transaction } from 'prosemirror-state';
2
- import type { Step as ProseMirrorStep } from 'prosemirror-transform';
1
+ import type { EditorState, Transaction } from '@atlaskit/editor-prosemirror/state';
2
+ import type { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
3
3
  import { Emitter } from '../emitter';
4
4
  import type { Config } from '../types';
5
5
  import type { CollabEditProvider, CollabEvents, CollabTelepointerPayload, ResolvedEditorState, Metadata, SyncUpErrorFunction } from '@atlaskit/editor-common/collab';
@@ -1,4 +1,4 @@
1
- import type { Step } from 'prosemirror-transform';
1
+ import type { Step } from '@atlaskit/editor-prosemirror/transform';
2
2
  import type { AnalyticsWebClient } from '@atlaskit/analytics-listeners';
3
3
  import type { Manager } from 'socket.io-client';
4
4
  import type { InternalError } from './errors/error-types';
@@ -63,6 +63,15 @@ export interface Config {
63
63
  * throwing a non-recoverable error if it's detected.
64
64
  */
65
65
  enableErrorOnFailedDocumentApply?: boolean;
66
+ /**
67
+ * Configure the client side circuit breaker in the event that abnormal behaviour causes the client to flood
68
+ * NCS with too many steps or too large a volume of data. This can result in either a soft fail or a hard (fatal) fail
69
+ * depending on the configured rate limit type.
70
+ */
71
+ rateLimitMaxStepSize?: number;
72
+ rateLimitStepCount?: number;
73
+ rateLimitTotalStepSize?: number;
74
+ rateLimitType?: number;
66
75
  }
67
76
  export interface InitAndAuthData {
68
77
  initialized: boolean;
@@ -4,16 +4,24 @@ import type { Socket } from 'socket.io-client';
4
4
  import AnalyticsHelper from './analytics/analytics-helper';
5
5
  import type { Metadata } from '@atlaskit/editor-common/collab';
6
6
  export declare class Channel extends Emitter<ChannelEvent> {
7
+ private readonly RATE_LIMIT_TYPE_NONE;
8
+ private readonly RATE_LIMIT_TYPE_SOFT;
9
+ private readonly RATE_LIMIT_TYPE_HARD;
7
10
  private connected;
8
- private config;
11
+ private readonly config;
9
12
  private socket;
10
13
  private reconnectHelper?;
11
14
  private initialized;
12
- private analyticsHelper?;
15
+ private readonly analyticsHelper?;
13
16
  private initExperience?;
14
17
  private token?;
15
18
  private network;
16
19
  private socketMessageMetrics?;
20
+ private readonly rateLimitWindowDurationMs;
21
+ private rateLimitWindowStartMs;
22
+ private stepCounter;
23
+ private stepSizeCounter;
24
+ private maxStepSize;
17
25
  constructor(config: Config, analyticsHelper: AnalyticsHelper);
18
26
  getInitialized: () => boolean;
19
27
  getConnected: () => boolean;
@@ -25,6 +33,8 @@ export declare class Channel extends Emitter<ChannelEvent> {
25
33
  * Connect to collab service using websockets
26
34
  */
27
35
  connect(shouldInitialize?: boolean): void;
36
+ onAnyOutgoingHandler(currentTimeMs: number, args: any[]): void;
37
+ private isLimitExceeded;
28
38
  private handlePermissionInvalidateToken;
29
39
  private onConnectError;
30
40
  private onReconnectError;
@@ -1,5 +1,5 @@
1
1
  import type { CatchupOptions } from '../types';
2
- import { Mapping, Step } from 'prosemirror-transform';
2
+ import { Mapping, Step } from '@atlaskit/editor-prosemirror/transform';
3
3
  /**
4
4
  * Rebase the steps based on the mapping pipeline.
5
5
  * Some steps could be lost, if they are no longer
@@ -2,10 +2,10 @@
2
2
  import AnalyticsHelper from '../analytics/analytics-helper';
3
3
  import { CatchupResponse, ChannelEvent, StepsPayload } from '../types';
4
4
  import type { SyncUpErrorFunction, ResolvedEditorState } from '@atlaskit/editor-common/collab';
5
- import type { Step as ProseMirrorStep } from 'prosemirror-transform';
5
+ import type { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
6
6
  import type { MetadataService } from '../metadata/metadata-service';
7
7
  import type { CollabEvents, CollabInitPayload } from '@atlaskit/editor-common/collab';
8
- import type { EditorState, Transaction } from 'prosemirror-state';
8
+ import type { EditorState, Transaction } from '@atlaskit/editor-prosemirror/state';
9
9
  import { ParticipantsService } from '../participants/participants-service';
10
10
  import type { InternalError } from '../errors/error-types';
11
11
  export declare class DocumentService {
@@ -26,7 +26,8 @@ export declare enum NCS_ERROR_CODE {
26
26
  DYNAMO_ERROR = "DYNAMO_ERROR",
27
27
  INVALID_ACTIVATION_ID = "INVALID_ACTIVATION_ID",
28
28
  INVALID_DOCUMENT_ARI = "INVALID_DOCUMENT_ARI",
29
- INVALID_CLOUD_ID = "INVALID_CLOUD_ID"
29
+ INVALID_CLOUD_ID = "INVALID_CLOUD_ID",
30
+ RATE_LIMIT_ERROR = "RATE_LIMIT_ERROR"
30
31
  }
31
32
  type HeadVersionUpdateFailedError = {
32
33
  message: string;
@@ -236,10 +237,27 @@ export type InternalDocumentUpdateFailure = {
236
237
  status: 500;
237
238
  };
238
239
  };
240
+ /**
241
+ * The client is trying to send too many messages or messages that are too large. This not expected to be a standard
242
+ * operating condition and should only ever indicate a frontend bug.
243
+ */
244
+ export type RateLimitError = {
245
+ message: string;
246
+ data: {
247
+ code: NCS_ERROR_CODE.RATE_LIMIT_ERROR;
248
+ meta: {
249
+ rateLimitType: number;
250
+ maxStepSize: number;
251
+ stepSizeCounter: number;
252
+ stepCounter: number;
253
+ };
254
+ status: 500;
255
+ };
256
+ };
239
257
  /**
240
258
  * A union of all possible internal errors, that are mapped to another error if being emitted to the editor.
241
259
  */
242
- export type InternalError = NCSErrors | DocumentRecoveryError | AddStepsError | CatchUpFailedError | TokenPermissionError | ReconnectionError | ConnectionError | ReconnectionNetworkError | DocumentNotFoundError | InternalDocumentUpdateFailure;
260
+ export type InternalError = NCSErrors | DocumentRecoveryError | AddStepsError | CatchUpFailedError | TokenPermissionError | ReconnectionError | ConnectionError | ReconnectionNetworkError | DocumentNotFoundError | InternalDocumentUpdateFailure | RateLimitError;
243
261
  type ValidEventAttributeType = boolean | string | number;
244
262
  export declare class CustomError extends Error {
245
263
  extraEventAttributes?: {
@@ -1,5 +1,5 @@
1
1
  import type { ProductInformation } from '../types';
2
- import type { Step as ProseMirrorStep } from 'prosemirror-transform';
2
+ import type { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
3
3
  export declare const createLogger: (prefix: string, color?: string) => (msg: string, data?: any) => void;
4
4
  export declare function sleep(ms: number): Promise<unknown>;
5
5
  export declare const getProduct: (productInfo?: ProductInformation) => string;
@@ -1,7 +1,7 @@
1
1
  /// <reference types="lodash" />
2
2
  import { ChannelEvent, StepsPayload } from '../types';
3
3
  import type { CollabCommitStatusEventPayload, CollabEvents } from '@atlaskit/editor-common/collab';
4
- import type { Step as ProseMirrorStep } from 'prosemirror-transform';
4
+ import type { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
5
5
  import AnalyticsHelper from '../analytics/analytics-helper';
6
6
  import type { InternalError } from '../errors/error-types';
7
7
  export declare const commitStep: ({ broadcast, steps, version, userId, clientId, onStepsAdded, onErrorHandled, analyticsHelper, emit, }: {
@@ -1,5 +1,5 @@
1
- import type { EditorState, Transaction } from 'prosemirror-state';
2
- import type { Step as ProseMirrorStep } from 'prosemirror-transform';
1
+ import type { EditorState, Transaction } from '@atlaskit/editor-prosemirror/state';
2
+ import type { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
3
3
  import { Emitter } from '../emitter';
4
4
  import type { Config } from '../types';
5
5
  import type { CollabEditProvider, CollabEvents, CollabTelepointerPayload, ResolvedEditorState, Metadata, SyncUpErrorFunction } from '@atlaskit/editor-common/collab';
@@ -1,4 +1,4 @@
1
- import type { Step } from 'prosemirror-transform';
1
+ import type { Step } from '@atlaskit/editor-prosemirror/transform';
2
2
  import type { AnalyticsWebClient } from '@atlaskit/analytics-listeners';
3
3
  import type { Manager } from 'socket.io-client';
4
4
  import type { InternalError } from './errors/error-types';
@@ -63,6 +63,15 @@ export interface Config {
63
63
  * throwing a non-recoverable error if it's detected.
64
64
  */
65
65
  enableErrorOnFailedDocumentApply?: boolean;
66
+ /**
67
+ * Configure the client side circuit breaker in the event that abnormal behaviour causes the client to flood
68
+ * NCS with too many steps or too large a volume of data. This can result in either a soft fail or a hard (fatal) fail
69
+ * depending on the configured rate limit type.
70
+ */
71
+ rateLimitMaxStepSize?: number;
72
+ rateLimitStepCount?: number;
73
+ rateLimitTotalStepSize?: number;
74
+ rateLimitType?: number;
66
75
  }
67
76
  export interface InitAndAuthData {
68
77
  initialized: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/collab-provider",
3
- "version": "9.7.2",
3
+ "version": "9.7.4",
4
4
  "description": "A provider for collaborative editing.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -36,15 +36,15 @@
36
36
  "dependencies": {
37
37
  "@atlaskit/analytics-gas-types": "^5.1.0",
38
38
  "@atlaskit/analytics-listeners": "^8.7.0",
39
- "@atlaskit/editor-common": "^74.22.0",
39
+ "@atlaskit/editor-common": "^74.29.0",
40
40
  "@atlaskit/editor-json-transformer": "^8.10.0",
41
+ "@atlaskit/editor-prosemirror": "1.0.2",
41
42
  "@atlaskit/prosemirror-collab": "^0.2.0",
42
43
  "@atlaskit/ufo": "^0.2.0",
43
44
  "@atlaskit/util-service-support": "^6.2.0",
44
45
  "@babel/runtime": "^7.0.0",
45
46
  "eventemitter2": "^4.1.0",
46
47
  "lodash": "^4.17.21",
47
- "prosemirror-transform": "1.3.2",
48
48
  "socket.io-client": "^4.7.1"
49
49
  },
50
50
  "techstack": {
@@ -62,12 +62,10 @@
62
62
  }
63
63
  },
64
64
  "devDependencies": {
65
- "@atlaskit/adf-schema": "^26.2.0",
65
+ "@atlaskit/adf-schema": "^26.4.0",
66
66
  "@atlaskit/analytics-listeners": "^8.7.0",
67
67
  "@atlaskit/editor-test-helpers": "^18.10.0",
68
68
  "@atlassian/atlassian-frontend-prettier-config-1.0.1": "npm:@atlassian/atlassian-frontend-prettier-config@1.0.1",
69
- "prosemirror-model": "1.16.0",
70
- "prosemirror-state": "1.3.4",
71
69
  "typescript": "~4.9.5"
72
70
  },
73
71
  "prettier": "@atlassian/atlassian-frontend-prettier-config-1.0.1"
package/report.api.md CHANGED
@@ -34,7 +34,7 @@ import { CollabParticipant } from '@atlaskit/editor-common/collab';
34
34
  import { CollabPresencePayload } from '@atlaskit/editor-common/collab';
35
35
  import { CollabSendableSelection } from '@atlaskit/editor-common/collab';
36
36
  import { CollabTelepointerPayload } from '@atlaskit/editor-common/collab';
37
- import type { EditorState } from 'prosemirror-state';
37
+ import type { EditorState } from '@atlaskit/editor-prosemirror/state';
38
38
  import { JSONDocNode } from '@atlaskit/editor-json-transformer';
39
39
  import type { Manager } from 'socket.io-client';
40
40
  import type { Metadata as Metadata_2 } from '@atlaskit/editor-common/collab';
@@ -43,9 +43,9 @@ import { PROVIDER_ERROR_CODE } from '@atlaskit/editor-common/collab';
43
43
  import { ProviderError } from '@atlaskit/editor-common/collab';
44
44
  import { ProviderParticipant } from '@atlaskit/editor-common/collab';
45
45
  import { ResolvedEditorState } from '@atlaskit/editor-common/collab';
46
- import type { Step } from 'prosemirror-transform';
46
+ import type { Step } from '@atlaskit/editor-prosemirror/transform';
47
47
  import { SyncUpErrorFunction } from '@atlaskit/editor-common/collab';
48
- import type { Transaction } from 'prosemirror-state';
48
+ import type { Transaction } from '@atlaskit/editor-prosemirror/state';
49
49
 
50
50
  // @public (undocumented)
51
51
  type AuthCallback = (cb: (data: InitAndAuthData) => void) => void;
@@ -144,6 +144,13 @@ interface Config {
144
144
  permissionTokenRefresh?: () => Promise<null | string>;
145
145
  // (undocumented)
146
146
  productInfo?: ProductInformation;
147
+ rateLimitMaxStepSize?: number;
148
+ // (undocumented)
149
+ rateLimitStepCount?: number;
150
+ // (undocumented)
151
+ rateLimitTotalStepSize?: number;
152
+ // (undocumented)
153
+ rateLimitType?: number;
147
154
  // (undocumented)
148
155
  storage?: Storage_2;
149
156
  throwOnNotConnected?: boolean;
@@ -23,7 +23,7 @@ import { CollabParticipant } from '@atlaskit/editor-common/collab';
23
23
  import { CollabPresencePayload } from '@atlaskit/editor-common/collab';
24
24
  import { CollabSendableSelection } from '@atlaskit/editor-common/collab';
25
25
  import { CollabTelepointerPayload } from '@atlaskit/editor-common/collab';
26
- import type { EditorState } from 'prosemirror-state';
26
+ import type { EditorState } from '@atlaskit/editor-prosemirror/state';
27
27
  import { JSONDocNode } from '@atlaskit/editor-json-transformer';
28
28
  import type { Manager } from 'socket.io-client';
29
29
  import type { Metadata as Metadata_2 } from '@atlaskit/editor-common/collab';
@@ -32,9 +32,9 @@ import { PROVIDER_ERROR_CODE } from '@atlaskit/editor-common/collab';
32
32
  import { ProviderError } from '@atlaskit/editor-common/collab';
33
33
  import { ProviderParticipant } from '@atlaskit/editor-common/collab';
34
34
  import { ResolvedEditorState } from '@atlaskit/editor-common/collab';
35
- import type { Step } from 'prosemirror-transform';
35
+ import type { Step } from '@atlaskit/editor-prosemirror/transform';
36
36
  import { SyncUpErrorFunction } from '@atlaskit/editor-common/collab';
37
- import type { Transaction } from 'prosemirror-state';
37
+ import type { Transaction } from '@atlaskit/editor-prosemirror/state';
38
38
 
39
39
  // @public (undocumented)
40
40
  type AuthCallback = (cb: (data: InitAndAuthData) => void) => void;
@@ -120,6 +120,13 @@ interface Config {
120
120
  permissionTokenRefresh?: () => Promise<null | string>;
121
121
  // (undocumented)
122
122
  productInfo?: ProductInformation;
123
+ rateLimitMaxStepSize?: number;
124
+ // (undocumented)
125
+ rateLimitStepCount?: number;
126
+ // (undocumented)
127
+ rateLimitTotalStepSize?: number;
128
+ // (undocumented)
129
+ rateLimitType?: number;
123
130
  // (undocumented)
124
131
  storage?: Storage_2;
125
132
  throwOnNotConnected?: boolean;