@atlaskit/editor-plugin-collab-edit 23.0.0 → 23.0.1

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 (30) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/cjs/collabEditPlugin.js +25 -23
  3. package/dist/cjs/pm-plugins/collabSendHold.js +76 -0
  4. package/dist/cjs/pm-plugins/collapseStreamingSteps.js +127 -0
  5. package/dist/cjs/pm-plugins/events/send-transaction.js +13 -2
  6. package/dist/cjs/pm-plugins/mergeOverlappingSteps.js +152 -0
  7. package/dist/cjs/pm-plugins/track-steps.js +1 -104
  8. package/dist/es2019/collabEditPlugin.js +23 -23
  9. package/dist/es2019/pm-plugins/collabSendHold.js +68 -0
  10. package/dist/es2019/pm-plugins/collapseStreamingSteps.js +114 -0
  11. package/dist/es2019/pm-plugins/events/send-transaction.js +12 -2
  12. package/dist/es2019/pm-plugins/mergeOverlappingSteps.js +144 -0
  13. package/dist/es2019/pm-plugins/track-steps.js +0 -93
  14. package/dist/esm/collabEditPlugin.js +25 -23
  15. package/dist/esm/pm-plugins/collabSendHold.js +70 -0
  16. package/dist/esm/pm-plugins/collapseStreamingSteps.js +122 -0
  17. package/dist/esm/pm-plugins/events/send-transaction.js +13 -2
  18. package/dist/esm/pm-plugins/mergeOverlappingSteps.js +147 -0
  19. package/dist/esm/pm-plugins/track-steps.js +0 -104
  20. package/dist/types/pm-plugins/collabSendHold.d.ts +51 -0
  21. package/dist/types/pm-plugins/collapseStreamingSteps.d.ts +18 -0
  22. package/dist/types/pm-plugins/events/send-transaction.d.ts +7 -1
  23. package/dist/types/pm-plugins/mergeOverlappingSteps.d.ts +28 -0
  24. package/dist/types/pm-plugins/track-steps.d.ts +0 -41
  25. package/package.json +8 -5
  26. package/tsconfig.typecheck-typecheck-dev.tsbuildinfo +1 -0
  27. package/dist/cjs/pm-plugins/track-step-metrics.js +0 -82
  28. package/dist/es2019/pm-plugins/track-step-metrics.js +0 -79
  29. package/dist/esm/pm-plugins/track-step-metrics.js +0 -75
  30. package/dist/types/pm-plugins/track-step-metrics.d.ts +0 -48
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # @atlaskit/editor-plugin-collab-edit
2
2
 
3
+ ## 23.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`5b30c74e8080f`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/5b30c74e8080f) -
8
+ Compose the intermediate steps of an AI streaming response into a single collab step, so a
9
+ streamed edit costs the collab service roughly one step per block instead of one per chunk.
10
+
11
+ Each streaming frame replaces the whole AI region with the cumulative content so far, so the
12
+ frames are individually large and grow as the response does. They are also thrown away at the end
13
+ — `buildCommitTransaction` rebuilds the final document from the response rather than from what was
14
+ streamed — which is what makes composing them safe.
15
+
16
+ `mergeOverlappingSteps` merges two consecutive overlapping steps into the single step they are
17
+ collectively equivalent to, and `collapseStreamingSteps` folds it across the steps in the collab
18
+ unconfirmed queue. Composition is exact in both directions: the forward step is verified against
19
+ the document, and the inverse is composed alongside it because `rebaseSteps` applies inverses to
20
+ the live document whenever a remote step arrives.
21
+
22
+ Composing only helps while the steps are still unsent, because `lockStepOrigins` marks everything
23
+ a flush sends and a locked step can never be merged. `createCollabSendHold` therefore holds the
24
+ send back across transactions — not per transaction, since acks, remote steps and the user's own
25
+ typing all trigger sends that would otherwise lock the queue. Producers drive it with the
26
+ `DEFER_COLLAB_SEND` meta: `true` starts the hold, `false` releases it, anything else leaves it
27
+ untouched. `true` does not extend a hold already in progress, so a continuous stream of held
28
+ frames still reaches the bound.
29
+
30
+ `editor-common` gains two new entrypoints for the metadata keys shared between the producer and
31
+ the collab layer: `./collab-streaming-step-collapse` and `./collab-defer-collab-send`. The changes
32
+ in `editor-plugin-collab-edit` and `editor-plugin-ai` are internal — neither package's exports map
33
+ changes.
34
+
35
+ While the send is held nothing is sent, including the user's own edits, up to the hold's time
36
+ bound. `sendableSteps` returns the queue in version order, so a later step cannot be sent ahead of
37
+ held ones. Peers also do not see in-progress content until a flush, and content in a held queue
38
+ exists only on the client until then.
39
+
40
+ All of this is behind the `platform_editor_ai_collapse_streaming_steps` feature gate. With the
41
+ gate off, streaming frames are not stamped, the send is never held, and the unconfirmed queue is
42
+ left exactly as it is today.
43
+
44
+ - [`5f61320693517`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/5f61320693517) -
45
+ Clean up experiment `platform_editor_remove_collab_step_metrics`
46
+ - Updated dependencies
47
+
3
48
  ## 23.0.0
4
49
 
5
50
  ### Patch Changes
@@ -15,8 +15,9 @@ var _JSONTransformer = require("@atlaskit/editor-json-transformer/JSONTransforme
15
15
  var _transform = require("@atlaskit/editor-prosemirror/transform");
16
16
  var _fg = require("@atlaskit/platform-feature-flags/fg");
17
17
  var _prosemirrorCollab = require("@atlaskit/prosemirror-collab");
18
- var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
19
18
  var _analytics2 = require("./pm-plugins/analytics");
19
+ var _collabSendHold = require("./pm-plugins/collabSendHold");
20
+ var _collapseStreamingSteps = require("./pm-plugins/collapseStreamingSteps");
20
21
  var _sendTransaction = require("./pm-plugins/events/send-transaction");
21
22
  var _filterAnalytics = require("./pm-plugins/filterAnalytics");
22
23
  var _main = require("./pm-plugins/main");
@@ -28,7 +29,6 @@ var _trackAndFilterSpammingSteps = require("./pm-plugins/track-and-filter-spammi
28
29
  var _trackLastOrganicChange = require("./pm-plugins/track-last-organic-change");
29
30
  var _trackNcsInitialization = require("./pm-plugins/track-ncs-initialization");
30
31
  var _trackReconnectionConflict = require("./pm-plugins/track-reconnection-conflict");
31
- var _trackSteps = require("./pm-plugins/track-steps");
32
32
  var _utils2 = require("./pm-plugins/utils");
33
33
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
34
34
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@@ -114,6 +114,9 @@ var collabEditPlugin = exports.collabEditPlugin = function collabEditPlugin(_ref
114
114
  providerResolver = _providerResolver;
115
115
  });
116
116
  var executeProviderCode = providerBuilder(collabEditProviderPromise);
117
+ // Spans transactions: a streaming producer holds the flush across many state updates, not just
118
+ // the ones it dispatches itself. See `createCollabSendHold`.
119
+ var sendHold = (0, _collabSendHold.createCollabSendHold)();
117
120
  return {
118
121
  name: 'collabEdit',
119
122
  getSharedState: function getSharedState(state) {
@@ -205,7 +208,10 @@ var collabEditPlugin = exports.collabEditPlugin = function collabEditPlugin(_ref
205
208
  _ref5$userId = _ref5.userId,
206
209
  userId = _ref5$userId === void 0 ? null : _ref5$userId;
207
210
  var transformUnconfirmed = function transformUnconfirmed(steps) {
208
- var transformed = (0, _filterAnalytics.filterAnalyticsSteps)(steps);
211
+ // Analytics steps are filtered first so they cannot break up an otherwise adjacent
212
+ // run of streaming frames.
213
+ var filtered = (0, _filterAnalytics.filterAnalyticsSteps)(steps);
214
+ var transformed = (0, _fg.fg)('platform_editor_ai_collapse_streaming_steps') ? (0, _collapseStreamingSteps.collapseStreamingSteps)(filtered) : filtered;
209
215
  return (0, _mergeUnconfirmed.mergeUnconfirmedSteps)(transformed, api);
210
216
  };
211
217
  var plugins = [].concat((0, _toConsumableArray2.default)(useNativePlugin ? [{
@@ -265,6 +271,18 @@ var collabEditPlugin = exports.collabEditPlugin = function collabEditPlugin(_ref
265
271
  },
266
272
  onEditorViewStateUpdated: function onEditorViewStateUpdated(props) {
267
273
  var _api$analytics2, _api$editorViewMode, _options$useNativePlu, _options$hideTelecurs;
274
+ // `props.transactions` is what `applyTransaction` produced: the dispatched transaction
275
+ // followed by everything `appendTransaction` added. Streaming frames are built in an
276
+ // appendTransaction, so their hold metadata is never on `originalTransaction`.
277
+ // Evaluated once and used for both the observation and the send decision. If the gate
278
+ // is turned off mid-stream, an already-active hold must stop applying immediately
279
+ // rather than persisting until the backstop expires and blocking every send.
280
+ var collapseStreamingStepsEnabled = (0, _fg.fg)('platform_editor_ai_collapse_streaming_steps');
281
+ if (collapseStreamingStepsEnabled) {
282
+ props.transactions.forEach(function (transaction) {
283
+ return sendHold.observe(transaction);
284
+ });
285
+ }
268
286
  var addErrorAnalytics = (0, _analytics2.addSynchronyErrorAnalytics)(props.newEditorState, props.newEditorState.tr, featureFlags, api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions);
269
287
  var isEmptyDoc = (0, _utils.isEmptyDocument)(props.newEditorState.doc);
270
288
  var viewMode = api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode;
@@ -275,32 +293,16 @@ var collabEditPlugin = exports.collabEditPlugin = function collabEditPlugin(_ref
275
293
  newEditorState: props.newEditorState,
276
294
  useNativePlugin: (_options$useNativePlu = options.useNativePlugin) !== null && _options$useNativePlu !== void 0 ? _options$useNativePlu : false,
277
295
  hideTelecursorOnLoad: !isEmptyDoc && ((_options$hideTelecurs = options.hideTelecursorOnLoad) !== null && _options$hideTelecurs !== void 0 ? _options$hideTelecurs : false),
278
- viewMode: viewMode
296
+ viewMode: viewMode,
297
+ isSendHeld: collapseStreamingStepsEnabled && sendHold.isHeld()
279
298
  }), addErrorAnalytics);
280
- if (!(0, _expValEquals.expValEquals)('platform_editor_remove_collab_step_metrics', 'isEnabled', true)) {
281
- (0, _trackSteps.track)(_objectSpread(_objectSpread({
282
- api: api
283
- }, props), {}, {
284
- onTrackDataProcessed: function onTrackDataProcessed(steps) {
285
- var _api$analytics3;
286
- api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 || (_api$analytics3 = _api$analytics3.actions) === null || _api$analytics3 === void 0 || _api$analytics3.fireAnalyticsEvent({
287
- action: _analytics.ACTION.STEPS_TRACKED,
288
- actionSubject: _analytics.ACTION_SUBJECT.COLLAB,
289
- attributes: {
290
- steps: steps
291
- },
292
- eventType: _analytics.EVENT_TYPE.OPERATIONAL
293
- });
294
- }
295
- }));
296
- }
297
299
  if ((0, _fg.fg)('platform_editor_collab_organic_change_reporting')) {
298
300
  (0, _monitorOrganicChanges.monitorOrganic)(_objectSpread(_objectSpread({
299
301
  api: api
300
302
  }, props), {}, {
301
303
  onDataProcessed: function onDataProcessed(data) {
302
- var _api$analytics4;
303
- api === null || api === void 0 || (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 || (_api$analytics4 = _api$analytics4.actions) === null || _api$analytics4 === void 0 || _api$analytics4.fireAnalyticsEvent({
304
+ var _api$analytics3;
305
+ api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 || (_api$analytics3 = _api$analytics3.actions) === null || _api$analytics3 === void 0 || _api$analytics3.fireAnalyticsEvent({
304
306
  action: _analytics.ACTION.ORGANIC_CHANGES_TRACKED,
305
307
  actionSubject: _analytics.ACTION_SUBJECT.COLLAB,
306
308
  attributes: {
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createCollabSendHold = exports.MAX_SEND_HOLD_MS = void 0;
7
+ var _collabDeferCollabSend = require("@atlaskit/editor-common/collab-defer-collab-send");
8
+ /**
9
+ * Upper bound on how long the send may stay held, measured from the transaction that *started* the
10
+ * hold rather than the most recent one.
11
+ *
12
+ * This is the only time bound on the hold. Producers decide when to release on semantic grounds —
13
+ * a completed block, the end of a stream — and this bounds how long that can take. It therefore
14
+ * doubles as the guarantee that a producer which never releases, because it aborted or threw
15
+ * between hold and release, cannot stop the editor sending for the rest of the session.
16
+ */
17
+ var MAX_SEND_HOLD_MS = exports.MAX_SEND_HOLD_MS = 2000;
18
+ /**
19
+ * Tracks whether sending to the collab service is currently held back.
20
+ *
21
+ * A producer that streams into the document needs several of its transactions to accumulate in the
22
+ * unconfirmed queue before anything is sent, so they can be composed into one step. Marking only
23
+ * the producer's own transactions is not enough: `sendTransaction` runs for *every* state update,
24
+ * and `provider.send` flushes and locks the entire queue regardless of which transaction triggered
25
+ * it. Acknowledgements from previous commits, remote steps, selection changes and the user's own
26
+ * typing all trigger it.
27
+ *
28
+ * So the hold has to be a state that spans transactions rather than a flag on one:
29
+ *
30
+ * - `tr.setMeta(DEFER_COLLAB_SEND, true)` starts the hold, and does *not* extend one already in
31
+ * progress
32
+ * - `tr.setMeta(DEFER_COLLAB_SEND, false)` releases it
33
+ * - anything else leaves it as it is
34
+ *
35
+ * Not extending matters. A producer marks every frame it wants held, so re-arming on each one
36
+ * would push the deadline back indefinitely and the bound would never be reached during an active
37
+ * stream. Measuring from the first held transaction instead makes the hold flush at least every
38
+ * {@link MAX_SEND_HOLD_MS}, with the next held frame starting a fresh window.
39
+ *
40
+ * While held, nothing is sent. Note this includes the local user's own edits: `sendableSteps`
41
+ * returns the queue in version order, so there is no way to send a step that sits behind held
42
+ * steps. Producers should keep hold windows short for that reason.
43
+ */
44
+ var createCollabSendHold = exports.createCollabSendHold = function createCollabSendHold() {
45
+ var heldSince;
46
+ return {
47
+ observe: function observe(transaction) {
48
+ var now = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Date.now();
49
+ var meta = transaction.getMeta(_collabDeferCollabSend.DEFER_COLLAB_SEND);
50
+ if (meta === true) {
51
+ // Only start a window, never extend one, so a continuous stream of held frames
52
+ // still reaches the bound. See the note on this type about why.
53
+ if (heldSince === undefined) {
54
+ heldSince = now;
55
+ }
56
+ return;
57
+ }
58
+ if (meta === false) {
59
+ heldSince = undefined;
60
+ }
61
+ },
62
+ isHeld: function isHeld() {
63
+ var now = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : Date.now();
64
+ if (heldSince === undefined) {
65
+ return false;
66
+ }
67
+ if (now - heldSince >= MAX_SEND_HOLD_MS) {
68
+ // Clear as well as report, so the next held frame opens a fresh window instead of
69
+ // being judged against the expired one.
70
+ heldSince = undefined;
71
+ return false;
72
+ }
73
+ return true;
74
+ }
75
+ };
76
+ };
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.collapseStreamingSteps = collapseStreamingSteps;
7
+ var _collabStreamingStepCollapse = require("@atlaskit/editor-common/collab-streaming-step-collapse");
8
+ var _state = require("@atlaskit/editor-prosemirror/state");
9
+ var _prosemirrorCollab = require("@atlaskit/prosemirror-collab");
10
+ var _mergeOverlappingSteps = require("./mergeOverlappingSteps");
11
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, 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 o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
12
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
13
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
14
+ /**
15
+ * Is this step an intermediate frame that its producer has marked as collapsible, and is it still
16
+ * free to be composed?
17
+ */
18
+ var isCollapsibleFrame = function isCollapsibleFrame(rebaseable) {
19
+ var origin = rebaseable.origin;
20
+ if (!(origin instanceof _state.Transaction) || origin.getMeta(_collabStreamingStepCollapse.STREAMING_COLLAPSIBLE_STEPS) !== true) {
21
+ return false;
22
+ }
23
+
24
+ // Already sent to (or queued for) the collab service — composing it now would change a step the
25
+ // backend has already seen. Mirrors the `mergeIsLocked` contract in `mergeUnconfirmedSteps`.
26
+ return !origin.getMeta('mergeIsLocked');
27
+ };
28
+
29
+ /**
30
+ * Starting document of each composed step this module has produced.
31
+ *
32
+ * A composed step is synthesised rather than taken from a transaction, so it cannot be found in
33
+ * `origin.steps` and its starting document cannot be recovered the way an original frame's can.
34
+ * `transformUnconfirmed` runs once per transaction and is handed back the steps it returned last
35
+ * time, so the composed step needs to carry its document forward to keep composing against later
36
+ * frames.
37
+ *
38
+ * Keyed on the step rather than its Rebaseable wrapper. `mergeUnconfirmedSteps` runs immediately
39
+ * after this module in `transformUnconfirmed` and rebuilds every wrapper it is given — merging
40
+ * enabled or not — so a wrapper-keyed entry would be unreachable by the very next pass. The step
41
+ * survives that rebuild untouched, and is still replaced whenever the association genuinely
42
+ * becomes stale: `rebaseSteps` maps steps into new objects, and a real merge composes a new one.
43
+ */
44
+ var composedStepDocs = new WeakMap();
45
+
46
+ /**
47
+ * The document this step was applied to.
48
+ *
49
+ * `transformUnconfirmed` is handed steps and nothing else, but a Rebaseable keeps a reference to
50
+ * the transaction that produced it, and a transaction records the document before each of its
51
+ * steps. That is enough for `mergeOverlappingSteps` to resolve merges it cannot derive from the
52
+ * steps alone — notably a frame that replaces more than just what the previous frame inserted.
53
+ *
54
+ * Rebasing maps a step into a new object while keeping the original transaction, so a rebased step
55
+ * is not found in `origin.steps` and correctly yields no document. `mergeOverlappingSteps` then
56
+ * falls back to the merges it can prove without one.
57
+ */
58
+ var docBeforeStep = function docBeforeStep(rebaseable) {
59
+ var origin = rebaseable.origin,
60
+ step = rebaseable.step;
61
+ var composed = composedStepDocs.get(step);
62
+ if (composed) {
63
+ return composed;
64
+ }
65
+ var index = origin.steps.indexOf(step);
66
+ return index >= 0 ? origin.docs[index] : undefined;
67
+ };
68
+
69
+ /**
70
+ * Compose adjacent streaming frames in the unconfirmed queue so the collab service receives one
71
+ * step per flush instead of one step per rendered chunk.
72
+ *
73
+ * This runs in the collab plugin's `transformUnconfirmed` hook, which fires *after* each frame has
74
+ * already been applied to the document. Collapsing therefore has no effect on what the local user
75
+ * sees, on undo history, or on any other plugin state — it only changes what is sent to the collab
76
+ * service, and what is used as the rebase buffer when remote steps arrive.
77
+ *
78
+ * Because the unconfirmed steps double as the rebase buffer (`rebaseSteps` applies their inverses
79
+ * to the live document), the composition must be exact. `mergeOverlappingSteps` only returns a
80
+ * merge it can prove exact, and this function degrades to a no-op whenever it declines.
81
+ *
82
+ * @param steps Rebaseable steps
83
+ * @returns Rebaseable steps, with adjacent streaming frames composed
84
+ */
85
+ function collapseStreamingSteps(steps) {
86
+ // Fast path: nothing to do unless at least two frames could possibly compose.
87
+ if (steps.length < 2) {
88
+ return steps;
89
+ }
90
+ var collapsed = [];
91
+ // The document the run currently at the tail of `collapsed` started from. A composed step
92
+ // still applies to the document its first frame applied to, so this survives each merge.
93
+ var runDocBefore;
94
+ var _iterator = _createForOfIteratorHelper(steps),
95
+ _step;
96
+ try {
97
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
98
+ var rebaseable = _step.value;
99
+ var previous = collapsed[collapsed.length - 1];
100
+ if (!previous || !isCollapsibleFrame(previous) || !isCollapsibleFrame(rebaseable)) {
101
+ collapsed.push(rebaseable);
102
+ runDocBefore = docBeforeStep(rebaseable);
103
+ continue;
104
+ }
105
+ var merged = (0, _mergeOverlappingSteps.mergeOverlappingSteps)(previous, rebaseable, runDocBefore);
106
+ if (!merged) {
107
+ collapsed.push(rebaseable);
108
+ runDocBefore = docBeforeStep(rebaseable);
109
+ continue;
110
+ }
111
+
112
+ // Keep the earliest origin, matching `mergeUnconfirmedSteps`. `commitUnconfirmedSteps` in
113
+ // the collab provider tracks completion by origin identity, so keeping the earliest means
114
+ // the composed step is not considered confirmed until the whole run has been acknowledged.
115
+ var composed = new _prosemirrorCollab.Rebaseable(merged.step, merged.inverted, previous.origin);
116
+ if (runDocBefore) {
117
+ composedStepDocs.set(composed.step, runDocBefore);
118
+ }
119
+ collapsed[collapsed.length - 1] = composed;
120
+ }
121
+ } catch (err) {
122
+ _iterator.e(err);
123
+ } finally {
124
+ _iterator.f();
125
+ }
126
+ return collapsed;
127
+ }
@@ -14,7 +14,9 @@ var sendTransaction = exports.sendTransaction = function sendTransaction(_ref) {
14
14
  newEditorState = _ref.newEditorState,
15
15
  useNativePlugin = _ref.useNativePlugin,
16
16
  viewMode = _ref.viewMode,
17
- hideTelecursorOnLoad = _ref.hideTelecursorOnLoad;
17
+ hideTelecursorOnLoad = _ref.hideTelecursorOnLoad,
18
+ _ref$isSendHeld = _ref.isSendHeld,
19
+ isSendHeld = _ref$isSendHeld === void 0 ? false : _ref$isSendHeld;
18
20
  return function (provider) {
19
21
  var docChangedTransaction = transactions.find(function (tr) {
20
22
  return tr.docChanged;
@@ -33,7 +35,16 @@ var sendTransaction = exports.sendTransaction = function sendTransaction(_ref) {
33
35
  // TODO: ED-8995 - We need to do this check to reduce the number of race conditions when working with tables.
34
36
  // This metadata is coming from the scaleTable command in table-resizing plugin
35
37
  !originalTransaction.getMeta('scaleTable') && trNoAnalytics.docChanged;
36
- if (useNativePlugin || shouldSendStepForSynchronyCollabProvider) {
38
+
39
+ // A streaming producer can hold the flush back so several of its frames accumulate in the
40
+ // unconfirmed queue and collapse into one step before being sent. The steps are not
41
+ // dropped: `sendableSteps` returns the whole queue, so the first unheld state update sends
42
+ // everything that accumulated. Skipping `send` also skips `lockStepOrigins`, which is what
43
+ // keeps the accumulated frames composable.
44
+ //
45
+ // Telepointer messages below are intentionally left alone — holding them would freeze
46
+ // other participants' cursors for the length of the hold, and they carry no steps.
47
+ if (!isSendHeld && (useNativePlugin || shouldSendStepForSynchronyCollabProvider)) {
37
48
  provider.send(trNoAnalytics, oldEditorState, newEditorState);
38
49
  }
39
50
  var prevPluginState = _pluginKey.pluginKey.getState(oldEditorState);
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.mergeOverlappingSteps = mergeOverlappingSteps;
7
+ var _transform = require("@atlaskit/editor-prosemirror/transform");
8
+ /**
9
+ * A step paired with its inverse, as stored in the collab unconfirmed queue.
10
+ */
11
+
12
+ /**
13
+ * `structure` is `@internal` on ReplaceStep but is part of the serialised form, so this is the
14
+ * supported way to read it. Structural replaces exist to stop rebased steps from overwriting
15
+ * content they should not, so they are never merged.
16
+ */
17
+ var isStructural = function isStructural(step) {
18
+ return step.toJSON().structure === true;
19
+ };
20
+ var asPlainReplaceStep = function asPlainReplaceStep(step) {
21
+ return (
22
+ // Deliberately excludes ReplaceAroundStep — the position arithmetic below is only sound for a
23
+ // plain range replace.
24
+ step instanceof _transform.ReplaceStep && !isStructural(step) ? step : undefined
25
+ );
26
+ };
27
+
28
+ /**
29
+ * Do the two steps touch the same region of the document?
30
+ *
31
+ * `second` is expressed in the coordinate space *after* `first` has been applied, so `first`'s
32
+ * footprint there is the range its slice occupies: `[from, from + slice.size]`.
33
+ */
34
+ var overlaps = function overlaps(first, second) {
35
+ var firstInsertedEnd = first.from + first.slice.size;
36
+ return second.from <= firstInsertedEnd && second.to >= first.from;
37
+ };
38
+
39
+ /**
40
+ * Merge using ProseMirror's own step merging, which handles adjacent insertions and adjacent
41
+ * deletions. Both the steps and their inverses have to merge for the result to be usable as a
42
+ * rebase buffer entry — inverses compose in reverse order.
43
+ */
44
+ var mergeNatively = function mergeNatively(first, second) {
45
+ var step = first.step.merge(second.step);
46
+ if (!step) {
47
+ return null;
48
+ }
49
+ var inverted = second.inverted.merge(first.inverted);
50
+ if (!inverted) {
51
+ return null;
52
+ }
53
+ return {
54
+ step: step,
55
+ inverted: inverted
56
+ };
57
+ };
58
+
59
+ /**
60
+ * Merge any two overlapping steps by applying both to the document and describing the net change
61
+ * as a single minimal replacement.
62
+ *
63
+ * This is the general case: it copes with `second` reaching past `first` on either side, with
64
+ * partial overlaps, and with slices whose open depths differ — none of which can be resolved from
65
+ * the steps alone, because the composed slice has to be cut out of a real document.
66
+ */
67
+ var mergeViaDocument = function mergeViaDocument(first, second, docBefore) {
68
+ var docAfter;
69
+ try {
70
+ var transform = new _transform.Transform(docBefore);
71
+ transform.step(first.step);
72
+ transform.step(second.step);
73
+ docAfter = transform.doc;
74
+ } catch (_unused) {
75
+ // `docBefore` did not match the steps (for example the steps have since been rebased).
76
+ return null;
77
+ }
78
+ var start = docBefore.content.findDiffStart(docAfter.content);
79
+ if (start === null) {
80
+ // The steps cancel each other out. There is no single step representing "no change", so
81
+ // leave the pair alone rather than inventing one.
82
+ return null;
83
+ }
84
+ var diffEnd = docBefore.content.findDiffEnd(docAfter.content);
85
+ if (!diffEnd) {
86
+ return null;
87
+ }
88
+
89
+ // findDiffStart and findDiffEnd can overshoot each other when the same text appears on both
90
+ // sides of the change; push the end out so the range stays well formed.
91
+ var endBefore = diffEnd.a,
92
+ endAfter = diffEnd.b;
93
+ var overshoot = start - Math.min(endBefore, endAfter);
94
+ if (overshoot > 0) {
95
+ endBefore += overshoot;
96
+ endAfter += overshoot;
97
+ }
98
+ var step = new _transform.ReplaceStep(start, endBefore, docAfter.slice(start, endAfter));
99
+
100
+ // Cheap guarantee that the composition is exact. If it is not, keep the original pair.
101
+ var applied = step.apply(docBefore);
102
+ if (applied.failed !== null || !applied.doc || !applied.doc.eq(docAfter)) {
103
+ return null;
104
+ }
105
+ return {
106
+ step: step,
107
+ inverted: step.invert(docBefore)
108
+ };
109
+ };
110
+
111
+ /**
112
+ * Merge two consecutive steps into the single step they are collectively equivalent to.
113
+ *
114
+ * `second` must be expressed in the coordinate space produced by `first` — that is, the two must be
115
+ * consecutive steps of the same document. The returned step maps the document from before `first`
116
+ * to after `second`, and the returned inverse maps it back, so the result is safe to use both on
117
+ * the wire and as a rebase buffer entry.
118
+ *
119
+ * Returns `null` whenever the steps do not overlap, or when the merge cannot be proven exact. A
120
+ * `null` result always means "keep the two steps as they are", never "the steps are equivalent to
121
+ * nothing".
122
+ *
123
+ * @param first The earlier step and its inverse.
124
+ * @param second The later step and its inverse.
125
+ * @param docBefore The document `first` applies to. Optional, but without it only ProseMirror's own
126
+ * adjacent-step merges can be resolved, because the composed slice otherwise has to be cut out of a
127
+ * real document. Callers lose it when the queue has been rebased over a remote step.
128
+ */
129
+ function mergeOverlappingSteps(first, second, docBefore) {
130
+ var firstStep = asPlainReplaceStep(first.step);
131
+ var secondStep = asPlainReplaceStep(second.step);
132
+
133
+ // Non-plain replaces are only ever merged by ProseMirror itself, which knows the rules for them.
134
+ if (!firstStep || !secondStep) {
135
+ return mergeNatively(first, second);
136
+ }
137
+ if (!overlaps(firstStep, secondStep)) {
138
+ return null;
139
+ }
140
+
141
+ // Try the O(1) strategy before the one that has to replay both steps against a document.
142
+ // `transformUnconfirmed` runs on every transaction, and adjacent insertions — which is what
143
+ // consecutive streaming frames often are — are exactly what ProseMirror merges natively.
144
+ var natively = mergeNatively(first, second);
145
+ if (natively) {
146
+ return natively;
147
+ }
148
+
149
+ // Anything else needs the document to cut the composed slice out of, so without one there is
150
+ // no merge that can be proven exact.
151
+ return docBefore ? mergeViaDocument(first, second, docBefore) : null;
152
+ }
@@ -4,17 +4,11 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.track = exports.task = exports.sanitizeStep = exports.groupSteps = void 0;
7
+ exports.sanitizeStep = exports.groupSteps = void 0;
8
8
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
9
- var _analytics = require("@atlaskit/adf-schema/steps/analytics");
10
9
  var _batchAttrsStep = require("@atlaskit/adf-schema/steps/batch-attrs-step");
11
10
  var _setAttrs = require("@atlaskit/adf-schema/steps/set-attrs");
12
11
  var _transform = require("@atlaskit/editor-prosemirror/transform");
13
- var _prosemirrorCollab = require("@atlaskit/prosemirror-collab");
14
- var _trackStepMetrics = require("./track-step-metrics");
15
- function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, 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 o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
16
- function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
17
- function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } // delete this file when cleaning up platform_editor_remove_collab_step_metrics
18
12
  function groupBy(array, keyGetter) {
19
13
  // Check group by exists, and that it's a function. If so, use the native browser code
20
14
  if ('groupBy' in Object && typeof Object.groupBy === 'function') {
@@ -98,101 +92,4 @@ var groupSteps = exports.groupSteps = function groupSteps(sanitizedSteps) {
98
92
  acc[key] = Array.isArray(value) ? value.length : 0;
99
93
  return acc;
100
94
  }, {});
101
- };
102
-
103
- /**
104
- * Processes the steps metadata from the cache and calls the callback function with the processed data.
105
- *
106
- * @param {CacheType} cache - A cache containing steps metadata.
107
- * @param {(data: StepMetadataAnalytics[]) => void} onTrackDataProcessed - Callback function to be called with the processed data.
108
- */
109
- var task = exports.task = function task(cache, onTrackDataProcessed) {
110
- var stepsMetadata = [];
111
- var _iterator = _createForOfIteratorHelper(cache.values()),
112
- _step;
113
- try {
114
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
115
- var entry = _step.value;
116
- var startedAt = entry.startedAt,
117
- endedAt = entry.endedAt,
118
- steps = entry.steps;
119
- var stepTypesAmount = groupSteps(steps.map(sanitizeStep));
120
- stepsMetadata.push({
121
- startedAt: startedAt,
122
- endedAt: endedAt,
123
- stepTypesAmount: stepTypesAmount
124
- });
125
- }
126
- } catch (err) {
127
- _iterator.e(err);
128
- } finally {
129
- _iterator.f();
130
- }
131
- cache.clear();
132
- if (stepsMetadata.length > 0) {
133
- onTrackDataProcessed(stepsMetadata);
134
- }
135
- };
136
- var stepsSentCache = new Map();
137
- // Every ten seconds we will try to process the step data.
138
- var LOW_PRIORITY_DELAY = 10000;
139
-
140
- // See https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/
141
-
142
- var getScheduler = function getScheduler(obj) {
143
- if (!obj) {
144
- return null;
145
- }
146
- if ('scheduler' in obj) {
147
- return obj.scheduler;
148
- }
149
- return null;
150
- };
151
-
152
- /**
153
- * Tracks the steps sent by the client by storing them in a cache and scheduling a task to process them. Once the steps are processed, the onTrackDataProcessed callabck will be called.
154
- *
155
- * This is a non-critical code. If the browser doesn't support the Scheduler API https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/
156
- *
157
- * @param {TrackProps} props - The properties required for tracking steps.
158
- * @param {ExtractInjectionAPI<CollabEditPlugin> | undefined} props.api - The API for the CollabEdit plugin.
159
- * @param {EditorState} props.newEditorState - The new editor state.
160
- * @param {Readonly<Transaction[]>} props.transactions - The transactions that contain the steps.
161
- * @param {(data: StepMetadataAnalytics[]) => void} props.onTrackDataProcessed - Callback function to be called with the processed data.
162
- */
163
- var track = exports.track = function track(_ref4) {
164
- var api = _ref4.api,
165
- newEditorState = _ref4.newEditorState,
166
- transactions = _ref4.transactions,
167
- onTrackDataProcessed = _ref4.onTrackDataProcessed;
168
- var newSteps = transactions.flatMap(function (t) {
169
- return t.steps;
170
- });
171
- var collabState = (0, _prosemirrorCollab.sendableSteps)(newEditorState);
172
- var scheduler = getScheduler(window);
173
- if (!newSteps.length || !scheduler || !collabState) {
174
- return;
175
- }
176
- var version = collabState.version;
177
- var buffer = stepsSentCache.get(version);
178
- var startedAt = (buffer === null || buffer === void 0 ? void 0 : buffer.startedAt) || Date.now();
179
- var endedAt = Date.now();
180
- var steps = ((buffer === null || buffer === void 0 ? void 0 : buffer.steps) || []).concat(newSteps);
181
- stepsSentCache.set(version, {
182
- startedAt: startedAt,
183
- endedAt: endedAt,
184
- steps: steps
185
- });
186
- (0, _trackStepMetrics.updateNcsSessionStepMetrics)({
187
- api: api,
188
- steps: newSteps.filter(function (step) {
189
- return !(step instanceof _analytics.AnalyticsStep);
190
- })
191
- });
192
- scheduler.postTask(function () {
193
- task(stepsSentCache, onTrackDataProcessed);
194
- }, {
195
- priority: 'background',
196
- delay: LOW_PRIORITY_DELAY
197
- });
198
95
  };