@atlaskit/collab-provider 9.7.1 → 9.7.3

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 (43) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/channel.js +107 -4
  3. package/dist/cjs/errors/error-code-mapper.js +7 -1
  4. package/dist/cjs/errors/error-types.js +5 -0
  5. package/dist/cjs/feature-flags/__test__/index.unit.js +3 -2
  6. package/dist/cjs/feature-flags/index.js +4 -2
  7. package/dist/cjs/helpers/const.js +2 -1
  8. package/dist/cjs/helpers/socket-message-metrics.js +54 -0
  9. package/dist/cjs/version-wrapper.js +1 -1
  10. package/dist/cjs/version.json +1 -1
  11. package/dist/es2019/channel.js +77 -4
  12. package/dist/es2019/errors/error-code-mapper.js +7 -1
  13. package/dist/es2019/errors/error-types.js +5 -0
  14. package/dist/es2019/feature-flags/__test__/index.unit.js +3 -2
  15. package/dist/es2019/feature-flags/index.js +4 -2
  16. package/dist/es2019/helpers/const.js +2 -1
  17. package/dist/es2019/helpers/socket-message-metrics.js +40 -0
  18. package/dist/es2019/version-wrapper.js +1 -1
  19. package/dist/es2019/version.json +1 -1
  20. package/dist/esm/channel.js +108 -5
  21. package/dist/esm/errors/error-code-mapper.js +7 -1
  22. package/dist/esm/errors/error-types.js +5 -0
  23. package/dist/esm/feature-flags/__test__/index.unit.js +3 -2
  24. package/dist/esm/feature-flags/index.js +4 -2
  25. package/dist/esm/helpers/const.js +2 -1
  26. package/dist/esm/helpers/socket-message-metrics.js +45 -0
  27. package/dist/esm/version-wrapper.js +1 -1
  28. package/dist/esm/version.json +1 -1
  29. package/dist/types/channel.d.ts +13 -2
  30. package/dist/types/errors/error-types.d.ts +20 -2
  31. package/dist/types/feature-flags/types.d.ts +1 -0
  32. package/dist/types/helpers/const.d.ts +12 -2
  33. package/dist/types/helpers/socket-message-metrics.d.ts +14 -0
  34. package/dist/types/types.d.ts +9 -0
  35. package/dist/types-ts4.5/channel.d.ts +13 -2
  36. package/dist/types-ts4.5/errors/error-types.d.ts +20 -2
  37. package/dist/types-ts4.5/feature-flags/types.d.ts +1 -0
  38. package/dist/types-ts4.5/helpers/const.d.ts +12 -2
  39. package/dist/types-ts4.5/helpers/socket-message-metrics.d.ts +14 -0
  40. package/dist/types-ts4.5/types.d.ts +9 -0
  41. package/package.json +2 -2
  42. package/report.api.md +7 -0
  43. package/tmp/api-report-tmp.d.ts +7 -0
@@ -1,5 +1,5 @@
1
1
  export const name = "@atlaskit/collab-provider";
2
- export const version = "9.7.1";
2
+ export const version = "9.7.3";
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.1",
3
+ "version": "9.7.3",
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,9 @@ 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';
27
+ import { SocketMessageMetrics } from './helpers/socket-message-metrics';
28
+ import { getCollabProviderFeatureFlag } from './feature-flags';
24
29
  var logger = createLogger('Channel', 'green');
25
30
  export var Channel = /*#__PURE__*/function (_Emitter) {
26
31
  _inherits(Channel, _Emitter);
@@ -29,11 +34,19 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
29
34
  var _this;
30
35
  _classCallCheck(this, Channel);
31
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);
32
40
  _defineProperty(_assertThisInitialized(_this), "connected", false);
33
41
  _defineProperty(_assertThisInitialized(_this), "socket", null);
34
42
  _defineProperty(_assertThisInitialized(_this), "reconnectHelper", null);
35
43
  _defineProperty(_assertThisInitialized(_this), "initialized", false);
36
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);
37
50
  // read-only getters used for tests
38
51
  _defineProperty(_assertThisInitialized(_this), "getInitialized", function () {
39
52
  return _this.initialized;
@@ -63,7 +76,7 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
63
76
  reason: data.reason,
64
77
  // Potentially incorrect when value of token changes between connecting and connect.
65
78
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
66
- usedCachedToken: _this.token ? true : false
79
+ usedCachedToken: !!_this.token
67
80
  });
68
81
  _this.unsetToken();
69
82
  });
@@ -74,7 +87,7 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
74
87
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
75
88
  // Potentially incorrect when value of token changes between connecting and connect.
76
89
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
77
- usedCachedToken: _this.token ? true : false
90
+ usedCachedToken: !!_this.token
78
91
  });
79
92
  (_this$analyticsHelper3 = _this.analyticsHelper) === null || _this$analyticsHelper3 === void 0 ? void 0 : _this$analyticsHelper3.sendErrorEvent(error, 'Error while establishing connection');
80
93
  // If error received with `data`, it means the connection is rejected
@@ -118,6 +131,9 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
118
131
  });
119
132
  _defineProperty(_assertThisInitialized(_this), "onConnect", function () {
120
133
  var _this$analyticsHelper5;
134
+ if (getCollabProviderFeatureFlag('socketMessageMetricsFF', _this.config.featureFlags) && _this.socketMessageMetrics) {
135
+ _this.socketMessageMetrics.setupSocketMessageMetrics();
136
+ }
121
137
  _this.connected = true;
122
138
  logger('Connected.', _this.socket.id);
123
139
  var measure = stopMeasure(MEASURE_NAME.SOCKET_CONNECT, _this.analyticsHelper);
@@ -125,7 +141,7 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
125
141
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
126
142
  // Potentially incorrect when value of token changes between connecting and connect.
127
143
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
128
- usedCachedToken: _this.token ? true : false
144
+ usedCachedToken: !!_this.token
129
145
  });
130
146
  _this.emit('connected', {
131
147
  sid: _this.socket.id,
@@ -467,6 +483,9 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
467
483
  };
468
484
  }
469
485
  this.socket = createSocket("".concat(url, "/session/").concat(documentAri), auth, this.config.productInfo);
486
+ if (this.socket && this.analyticsHelper) {
487
+ this.socketMessageMetrics = new SocketMessageMetrics(this.socket, this.analyticsHelper);
488
+ }
470
489
 
471
490
  // Due to https://github.com/socketio/socket.io-client/issues/1473,
472
491
  // reconnect no longer fired on the socket.
@@ -518,6 +537,9 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
518
537
  return _regeneratorRuntime.wrap(function _callee3$(_context3) {
519
538
  while (1) switch (_context3.prev = _context3.next) {
520
539
  case 0:
540
+ if (getCollabProviderFeatureFlag('socketMessageMetricsFF', _this2.config.featureFlags) && _this2.socketMessageMetrics) {
541
+ _this2.socketMessageMetrics.closeSocketMessageMetrics();
542
+ }
521
543
  _this2.connected = false;
522
544
  logger("disconnect reason: ".concat(reason));
523
545
  _this2.emit('disconnect', {
@@ -539,7 +561,7 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
539
561
  _this2.emit('error', reconnectionError);
540
562
  }
541
563
  }
542
- case 4:
564
+ case 5:
543
565
  case "end":
544
566
  return _context3.stop();
545
567
  }
@@ -559,6 +581,12 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
559
581
  // Ensure the error emit to the provider has the same structure, so we can handle them unified.
560
582
  this.socket.on('connect_error', this.onConnectError);
561
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
+ });
562
590
 
563
591
  // To trigger reconnection when browser comes back online
564
592
  if (!this.network) {
@@ -572,6 +600,81 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
572
600
  // Fired upon a reconnection attempt error (from Socket.IO Manager)
573
601
  this.socket.io.on('reconnect_error', this.onReconnectError);
574
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
+ }
575
678
  }, {
576
679
  key: "disconnect",
577
680
  value: function disconnect() {
@@ -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
  */
@@ -2,9 +2,10 @@ import { getProductSpecificFeatureFlags, getCollabProviderFeatureFlag } from '..
2
2
  describe('Feature flags', function () {
3
3
  it('getProductSpecificFeatureFlags', function () {
4
4
  var result = getProductSpecificFeatureFlags({
5
- testFF: true
5
+ testFF: true,
6
+ socketMessageMetricsFF: true
6
7
  }, 'confluence');
7
- expect(result).toEqual(['confluence.fe.collab.provider.testFF']);
8
+ expect(result).toEqual(['confluence.fe.collab.provider.testFF', 'confluence.fe.collab.provider.socketMessageMetricsFF']);
8
9
  });
9
10
  it('getCollabProviderFeatureFlag return true', function () {
10
11
  var result = getCollabProviderFeatureFlag('testFF', {
@@ -1,6 +1,7 @@
1
1
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
2
  var defaultNCSFeatureFlags = {
3
- testFF: false
3
+ testFF: false,
4
+ socketMessageMetricsFF: false
4
5
  };
5
6
 
6
7
  /**
@@ -8,7 +9,8 @@ var defaultNCSFeatureFlags = {
8
9
  */
9
10
  var productKeys = {
10
11
  confluence: {
11
- testFF: 'confluence.fe.collab.provider.testFF'
12
+ testFF: 'confluence.fe.collab.provider.testFF',
13
+ socketMessageMetricsFF: 'confluence.fe.collab.provider.socketMessageMetricsFF'
12
14
  }
13
15
  };
14
16
  var filterFeatureFlagNames = function filterFeatureFlagNames(flags) {
@@ -13,8 +13,9 @@ export var EVENT_ACTION = /*#__PURE__*/function (EVENT_ACTION) {
13
13
  EVENT_ACTION["SEND_STEPS_RETRY"] = "sendStepsRetry";
14
14
  EVENT_ACTION["CATCHUP_AFTER_MAX_SEND_STEPS_RETRY"] = "catchupAfterMaxSendStepsRetry";
15
15
  EVENT_ACTION["DROPPED_STEPS"] = "droppedStepInCatchup";
16
+ EVENT_ACTION["WEBSOCKET_MESSAGE_VOLUME_METRIC"] = "websocketMessageVolumeMetric";
16
17
  return EVENT_ACTION;
17
- }({}); // https://data-portal.internal.atlassian.com/analytics/registry/53724
18
+ }({}); // https://data-portal.internal.atlassian.com/analytics/registry/53596
18
19
  export var EVENT_STATUS = /*#__PURE__*/function (EVENT_STATUS) {
19
20
  EVENT_STATUS["SUCCESS"] = "SUCCESS";
20
21
  EVENT_STATUS["FAILURE"] = "FAILURE";
@@ -0,0 +1,45 @@
1
+ import _createClass from "@babel/runtime/helpers/createClass";
2
+ import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
3
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
+ import { EVENT_ACTION, EVENT_STATUS } from './const';
5
+ import { createLogger } from './utils';
6
+ var logger = createLogger('SocketMessageMetrics', 'green');
7
+ export var WEBSOCKET_MESSAGE_VOLUME_METRIC_SEND_INTERVAL_MS = 60000;
8
+ export var SocketMessageMetrics = /*#__PURE__*/_createClass(function SocketMessageMetrics(socket, analyticsHelper) {
9
+ var _this = this;
10
+ _classCallCheck(this, SocketMessageMetrics);
11
+ _defineProperty(this, "messageCount", 0);
12
+ _defineProperty(this, "totalMessageSize", 0);
13
+ _defineProperty(this, "metricsIntervalID", undefined);
14
+ _defineProperty(this, "socketMessageMetricsListener", function (event) {
15
+ _this.messageCount++;
16
+ for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
17
+ args[_key - 1] = arguments[_key];
18
+ }
19
+ _this.totalMessageSize += Buffer.byteLength(JSON.stringify(args), 'utf8');
20
+ });
21
+ _defineProperty(this, "setupSocketMessageMetrics", function () {
22
+ if (_this.metricsIntervalID !== undefined) {
23
+ logger('calling setupSocketMessageMetrics function with metricsIntervalID that is not undefined');
24
+ return;
25
+ }
26
+ _this.socket.onAnyOutgoing(_this.socketMessageMetricsListener);
27
+
28
+ // send metrics every 60 seconds
29
+ _this.metricsIntervalID = window.setInterval(function () {
30
+ _this.analyticsHelper.sendActionEvent(EVENT_ACTION.WEBSOCKET_MESSAGE_VOLUME_METRIC, EVENT_STATUS.INFO, {
31
+ messageCount: _this.messageCount,
32
+ totalMessageSize: _this.totalMessageSize
33
+ });
34
+ _this.messageCount = 0;
35
+ _this.totalMessageSize = 0;
36
+ }, WEBSOCKET_MESSAGE_VOLUME_METRIC_SEND_INTERVAL_MS);
37
+ });
38
+ _defineProperty(this, "closeSocketMessageMetrics", function () {
39
+ clearInterval(_this.metricsIntervalID);
40
+ _this.metricsIntervalID = undefined;
41
+ _this.socket.offAnyOutgoing(_this.socketMessageMetricsListener);
42
+ });
43
+ this.socket = socket;
44
+ this.analyticsHelper = analyticsHelper;
45
+ });
@@ -1,5 +1,5 @@
1
1
  export var name = "@atlaskit/collab-provider";
2
- export var version = "9.7.1";
2
+ export var version = "9.7.3";
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.1",
3
+ "version": "9.7.3",
4
4
  "sideEffects": false
5
5
  }
@@ -4,15 +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;
19
+ private socketMessageMetrics?;
20
+ private readonly rateLimitWindowDurationMs;
21
+ private rateLimitWindowStartMs;
22
+ private stepCounter;
23
+ private stepSizeCounter;
24
+ private maxStepSize;
16
25
  constructor(config: Config, analyticsHelper: AnalyticsHelper);
17
26
  getInitialized: () => boolean;
18
27
  getConnected: () => boolean;
@@ -24,6 +33,8 @@ export declare class Channel extends Emitter<ChannelEvent> {
24
33
  * Connect to collab service using websockets
25
34
  */
26
35
  connect(shouldInitialize?: boolean): void;
36
+ onAnyOutgoingHandler(currentTimeMs: number, args: any[]): void;
37
+ private isLimitExceeded;
27
38
  private handlePermissionInvalidateToken;
28
39
  private onConnectError;
29
40
  private onReconnectError;
@@ -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,6 @@
1
1
  export interface NCSFeatureFlags {
2
2
  testFF?: boolean;
3
+ socketMessageMetricsFF?: boolean;
3
4
  }
4
5
  export interface WithNCSFeatureFlags {
5
6
  featureFlags?: NCSFeatureFlags;
@@ -13,7 +13,8 @@ export declare enum EVENT_ACTION {
13
13
  INVALIDATE_TOKEN = "invalidateToken",
14
14
  SEND_STEPS_RETRY = "sendStepsRetry",
15
15
  CATCHUP_AFTER_MAX_SEND_STEPS_RETRY = "catchupAfterMaxSendStepsRetry",
16
- DROPPED_STEPS = "droppedStepInCatchup"
16
+ DROPPED_STEPS = "droppedStepInCatchup",
17
+ WEBSOCKET_MESSAGE_VOLUME_METRIC = "websocketMessageVolumeMetric"
17
18
  }
18
19
  export declare enum EVENT_STATUS {
19
20
  SUCCESS = "SUCCESS",
@@ -215,7 +216,16 @@ type CatchupAfterMaxSendStepsRetryAnalyticsEvent = {
215
216
  eventStatus: EVENT_STATUS.INFO;
216
217
  };
217
218
  };
218
- export type ActionAnalyticsEvent = AddStepsSuccessAnalyticsEvent | AddStepsFailureAnalyticsEvent | ReInitDocFailAnalyticsEvent | ReInitDocSuccessAnalyticsEvent | ConnectionSuccessAnalyticsEvent | ConnectionFailureAnalyticsEvent | CatchUpSuccessAnalyticsEvent | CatchUpFailureAnalyticsEvent | DocumentInitSuccessAnalyticsEvent | UpdateParticipantsSuccessAnalyticsEvent | CommitUnconfirmedStepsSuccessAnalyticsEvent | CommitUnconfirmedStepsFailureAnalyticsEvent | PublishPageSuccessAnalyticsEvent | PublishPageFailureAnalyticsEvent | GetCurrentStateSuccessAnalyticsEvent | GetCurrentStateFailureAnalyticsEvent | InvalidateTokenAnalyticsEvent | SendStepsRetryAnalyticsEvent | CatchupAfterMaxSendStepsRetryAnalyticsEvent | CatchUpDroppedStepsEvent;
219
+ type WebsocketMessageVolumeMetricEvent = {
220
+ eventAction: EVENT_ACTION.WEBSOCKET_MESSAGE_VOLUME_METRIC;
221
+ attributes: {
222
+ documentAri?: string;
223
+ eventStatus: EVENT_STATUS.INFO;
224
+ messageCount: number;
225
+ messageSize: number;
226
+ };
227
+ };
228
+ export type ActionAnalyticsEvent = AddStepsSuccessAnalyticsEvent | AddStepsFailureAnalyticsEvent | ReInitDocFailAnalyticsEvent | ReInitDocSuccessAnalyticsEvent | ConnectionSuccessAnalyticsEvent | ConnectionFailureAnalyticsEvent | CatchUpSuccessAnalyticsEvent | CatchUpFailureAnalyticsEvent | DocumentInitSuccessAnalyticsEvent | UpdateParticipantsSuccessAnalyticsEvent | CommitUnconfirmedStepsSuccessAnalyticsEvent | CommitUnconfirmedStepsFailureAnalyticsEvent | PublishPageSuccessAnalyticsEvent | PublishPageFailureAnalyticsEvent | GetCurrentStateSuccessAnalyticsEvent | GetCurrentStateFailureAnalyticsEvent | InvalidateTokenAnalyticsEvent | SendStepsRetryAnalyticsEvent | CatchupAfterMaxSendStepsRetryAnalyticsEvent | CatchUpDroppedStepsEvent | WebsocketMessageVolumeMetricEvent;
219
229
  export declare const ACK_MAX_TRY = 60;
220
230
  export declare const CONFLUENCE = "confluence";
221
231
  export {};
@@ -0,0 +1,14 @@
1
+ import AnalyticsHelper from '../analytics/analytics-helper';
2
+ import type { Socket } from 'socket.io-client';
3
+ export declare const WEBSOCKET_MESSAGE_VOLUME_METRIC_SEND_INTERVAL_MS = 60000;
4
+ export declare class SocketMessageMetrics {
5
+ private messageCount;
6
+ private totalMessageSize;
7
+ private metricsIntervalID;
8
+ private socket;
9
+ private analyticsHelper;
10
+ constructor(socket: Socket, analyticsHelper: AnalyticsHelper);
11
+ socketMessageMetricsListener: (event: any, ...args: any[]) => void;
12
+ setupSocketMessageMetrics: () => void;
13
+ closeSocketMessageMetrics: () => void;
14
+ }
@@ -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,15 +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;
19
+ private socketMessageMetrics?;
20
+ private readonly rateLimitWindowDurationMs;
21
+ private rateLimitWindowStartMs;
22
+ private stepCounter;
23
+ private stepSizeCounter;
24
+ private maxStepSize;
16
25
  constructor(config: Config, analyticsHelper: AnalyticsHelper);
17
26
  getInitialized: () => boolean;
18
27
  getConnected: () => boolean;
@@ -24,6 +33,8 @@ export declare class Channel extends Emitter<ChannelEvent> {
24
33
  * Connect to collab service using websockets
25
34
  */
26
35
  connect(shouldInitialize?: boolean): void;
36
+ onAnyOutgoingHandler(currentTimeMs: number, args: any[]): void;
37
+ private isLimitExceeded;
27
38
  private handlePermissionInvalidateToken;
28
39
  private onConnectError;
29
40
  private onReconnectError;
@@ -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,6 @@
1
1
  export interface NCSFeatureFlags {
2
2
  testFF?: boolean;
3
+ socketMessageMetricsFF?: boolean;
3
4
  }
4
5
  export interface WithNCSFeatureFlags {
5
6
  featureFlags?: NCSFeatureFlags;
@@ -13,7 +13,8 @@ export declare enum EVENT_ACTION {
13
13
  INVALIDATE_TOKEN = "invalidateToken",
14
14
  SEND_STEPS_RETRY = "sendStepsRetry",
15
15
  CATCHUP_AFTER_MAX_SEND_STEPS_RETRY = "catchupAfterMaxSendStepsRetry",
16
- DROPPED_STEPS = "droppedStepInCatchup"
16
+ DROPPED_STEPS = "droppedStepInCatchup",
17
+ WEBSOCKET_MESSAGE_VOLUME_METRIC = "websocketMessageVolumeMetric"
17
18
  }
18
19
  export declare enum EVENT_STATUS {
19
20
  SUCCESS = "SUCCESS",
@@ -215,7 +216,16 @@ type CatchupAfterMaxSendStepsRetryAnalyticsEvent = {
215
216
  eventStatus: EVENT_STATUS.INFO;
216
217
  };
217
218
  };
218
- export type ActionAnalyticsEvent = AddStepsSuccessAnalyticsEvent | AddStepsFailureAnalyticsEvent | ReInitDocFailAnalyticsEvent | ReInitDocSuccessAnalyticsEvent | ConnectionSuccessAnalyticsEvent | ConnectionFailureAnalyticsEvent | CatchUpSuccessAnalyticsEvent | CatchUpFailureAnalyticsEvent | DocumentInitSuccessAnalyticsEvent | UpdateParticipantsSuccessAnalyticsEvent | CommitUnconfirmedStepsSuccessAnalyticsEvent | CommitUnconfirmedStepsFailureAnalyticsEvent | PublishPageSuccessAnalyticsEvent | PublishPageFailureAnalyticsEvent | GetCurrentStateSuccessAnalyticsEvent | GetCurrentStateFailureAnalyticsEvent | InvalidateTokenAnalyticsEvent | SendStepsRetryAnalyticsEvent | CatchupAfterMaxSendStepsRetryAnalyticsEvent | CatchUpDroppedStepsEvent;
219
+ type WebsocketMessageVolumeMetricEvent = {
220
+ eventAction: EVENT_ACTION.WEBSOCKET_MESSAGE_VOLUME_METRIC;
221
+ attributes: {
222
+ documentAri?: string;
223
+ eventStatus: EVENT_STATUS.INFO;
224
+ messageCount: number;
225
+ messageSize: number;
226
+ };
227
+ };
228
+ export type ActionAnalyticsEvent = AddStepsSuccessAnalyticsEvent | AddStepsFailureAnalyticsEvent | ReInitDocFailAnalyticsEvent | ReInitDocSuccessAnalyticsEvent | ConnectionSuccessAnalyticsEvent | ConnectionFailureAnalyticsEvent | CatchUpSuccessAnalyticsEvent | CatchUpFailureAnalyticsEvent | DocumentInitSuccessAnalyticsEvent | UpdateParticipantsSuccessAnalyticsEvent | CommitUnconfirmedStepsSuccessAnalyticsEvent | CommitUnconfirmedStepsFailureAnalyticsEvent | PublishPageSuccessAnalyticsEvent | PublishPageFailureAnalyticsEvent | GetCurrentStateSuccessAnalyticsEvent | GetCurrentStateFailureAnalyticsEvent | InvalidateTokenAnalyticsEvent | SendStepsRetryAnalyticsEvent | CatchupAfterMaxSendStepsRetryAnalyticsEvent | CatchUpDroppedStepsEvent | WebsocketMessageVolumeMetricEvent;
219
229
  export declare const ACK_MAX_TRY = 60;
220
230
  export declare const CONFLUENCE = "confluence";
221
231
  export {};
@@ -0,0 +1,14 @@
1
+ import AnalyticsHelper from '../analytics/analytics-helper';
2
+ import type { Socket } from 'socket.io-client';
3
+ export declare const WEBSOCKET_MESSAGE_VOLUME_METRIC_SEND_INTERVAL_MS = 60000;
4
+ export declare class SocketMessageMetrics {
5
+ private messageCount;
6
+ private totalMessageSize;
7
+ private metricsIntervalID;
8
+ private socket;
9
+ private analyticsHelper;
10
+ constructor(socket: Socket, analyticsHelper: AnalyticsHelper);
11
+ socketMessageMetricsListener: (event: any, ...args: any[]) => void;
12
+ setupSocketMessageMetrics: () => void;
13
+ closeSocketMessageMetrics: () => void;
14
+ }