@atlaskit/editor-plugin-collab-edit 23.0.0 → 23.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/dist/cjs/collabEditPlugin.js +25 -23
- package/dist/cjs/pm-plugins/collabSendHold.js +76 -0
- package/dist/cjs/pm-plugins/collapseStreamingSteps.js +127 -0
- package/dist/cjs/pm-plugins/events/send-transaction.js +13 -2
- package/dist/cjs/pm-plugins/mergeOverlappingSteps.js +152 -0
- package/dist/cjs/pm-plugins/track-steps.js +1 -104
- package/dist/es2019/collabEditPlugin.js +23 -23
- package/dist/es2019/pm-plugins/collabSendHold.js +68 -0
- package/dist/es2019/pm-plugins/collapseStreamingSteps.js +114 -0
- package/dist/es2019/pm-plugins/events/send-transaction.js +12 -2
- package/dist/es2019/pm-plugins/mergeOverlappingSteps.js +144 -0
- package/dist/es2019/pm-plugins/track-steps.js +0 -93
- package/dist/esm/collabEditPlugin.js +25 -23
- package/dist/esm/pm-plugins/collabSendHold.js +70 -0
- package/dist/esm/pm-plugins/collapseStreamingSteps.js +122 -0
- package/dist/esm/pm-plugins/events/send-transaction.js +13 -2
- package/dist/esm/pm-plugins/mergeOverlappingSteps.js +147 -0
- package/dist/esm/pm-plugins/track-steps.js +0 -104
- package/dist/types/pm-plugins/collabSendHold.d.ts +51 -0
- package/dist/types/pm-plugins/collapseStreamingSteps.d.ts +18 -0
- package/dist/types/pm-plugins/events/send-transaction.d.ts +7 -1
- package/dist/types/pm-plugins/mergeOverlappingSteps.d.ts +28 -0
- package/dist/types/pm-plugins/track-steps.d.ts +0 -41
- package/package.json +8 -5
- package/tsconfig.typecheck-typecheck-dev.tsbuildinfo +1 -0
- package/dist/cjs/pm-plugins/track-step-metrics.js +0 -82
- package/dist/es2019/pm-plugins/track-step-metrics.js +0 -79
- package/dist/esm/pm-plugins/track-step-metrics.js +0 -75
- package/dist/types/pm-plugins/track-step-metrics.d.ts +0 -48
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { DEFER_COLLAB_SEND } from '@atlaskit/editor-common/collab-defer-collab-send';
|
|
2
|
+
/**
|
|
3
|
+
* Upper bound on how long the send may stay held, measured from the transaction that *started* the
|
|
4
|
+
* hold rather than the most recent one.
|
|
5
|
+
*
|
|
6
|
+
* This is the only time bound on the hold. Producers decide when to release on semantic grounds —
|
|
7
|
+
* a completed block, the end of a stream — and this bounds how long that can take. It therefore
|
|
8
|
+
* doubles as the guarantee that a producer which never releases, because it aborted or threw
|
|
9
|
+
* between hold and release, cannot stop the editor sending for the rest of the session.
|
|
10
|
+
*/
|
|
11
|
+
export var MAX_SEND_HOLD_MS = 2000;
|
|
12
|
+
/**
|
|
13
|
+
* Tracks whether sending to the collab service is currently held back.
|
|
14
|
+
*
|
|
15
|
+
* A producer that streams into the document needs several of its transactions to accumulate in the
|
|
16
|
+
* unconfirmed queue before anything is sent, so they can be composed into one step. Marking only
|
|
17
|
+
* the producer's own transactions is not enough: `sendTransaction` runs for *every* state update,
|
|
18
|
+
* and `provider.send` flushes and locks the entire queue regardless of which transaction triggered
|
|
19
|
+
* it. Acknowledgements from previous commits, remote steps, selection changes and the user's own
|
|
20
|
+
* typing all trigger it.
|
|
21
|
+
*
|
|
22
|
+
* So the hold has to be a state that spans transactions rather than a flag on one:
|
|
23
|
+
*
|
|
24
|
+
* - `tr.setMeta(DEFER_COLLAB_SEND, true)` starts the hold, and does *not* extend one already in
|
|
25
|
+
* progress
|
|
26
|
+
* - `tr.setMeta(DEFER_COLLAB_SEND, false)` releases it
|
|
27
|
+
* - anything else leaves it as it is
|
|
28
|
+
*
|
|
29
|
+
* Not extending matters. A producer marks every frame it wants held, so re-arming on each one
|
|
30
|
+
* would push the deadline back indefinitely and the bound would never be reached during an active
|
|
31
|
+
* stream. Measuring from the first held transaction instead makes the hold flush at least every
|
|
32
|
+
* {@link MAX_SEND_HOLD_MS}, with the next held frame starting a fresh window.
|
|
33
|
+
*
|
|
34
|
+
* While held, nothing is sent. Note this includes the local user's own edits: `sendableSteps`
|
|
35
|
+
* returns the queue in version order, so there is no way to send a step that sits behind held
|
|
36
|
+
* steps. Producers should keep hold windows short for that reason.
|
|
37
|
+
*/
|
|
38
|
+
export var createCollabSendHold = function createCollabSendHold() {
|
|
39
|
+
var heldSince;
|
|
40
|
+
return {
|
|
41
|
+
observe: function observe(transaction) {
|
|
42
|
+
var now = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Date.now();
|
|
43
|
+
var meta = transaction.getMeta(DEFER_COLLAB_SEND);
|
|
44
|
+
if (meta === true) {
|
|
45
|
+
// Only start a window, never extend one, so a continuous stream of held frames
|
|
46
|
+
// still reaches the bound. See the note on this type about why.
|
|
47
|
+
if (heldSince === undefined) {
|
|
48
|
+
heldSince = now;
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (meta === false) {
|
|
53
|
+
heldSince = undefined;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
isHeld: function isHeld() {
|
|
57
|
+
var now = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : Date.now();
|
|
58
|
+
if (heldSince === undefined) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (now - heldSince >= MAX_SEND_HOLD_MS) {
|
|
62
|
+
// Clear as well as report, so the next held frame opens a fresh window instead of
|
|
63
|
+
// being judged against the expired one.
|
|
64
|
+
heldSince = undefined;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
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; } } }; }
|
|
2
|
+
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; } }
|
|
3
|
+
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; }
|
|
4
|
+
import { STREAMING_COLLAPSIBLE_STEPS } from '@atlaskit/editor-common/collab-streaming-step-collapse';
|
|
5
|
+
import { Transaction } from '@atlaskit/editor-prosemirror/state';
|
|
6
|
+
import { Rebaseable } from '@atlaskit/prosemirror-collab';
|
|
7
|
+
import { mergeOverlappingSteps } from './mergeOverlappingSteps';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Is this step an intermediate frame that its producer has marked as collapsible, and is it still
|
|
11
|
+
* free to be composed?
|
|
12
|
+
*/
|
|
13
|
+
var isCollapsibleFrame = function isCollapsibleFrame(rebaseable) {
|
|
14
|
+
var origin = rebaseable.origin;
|
|
15
|
+
if (!(origin instanceof Transaction) || origin.getMeta(STREAMING_COLLAPSIBLE_STEPS) !== true) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Already sent to (or queued for) the collab service — composing it now would change a step the
|
|
20
|
+
// backend has already seen. Mirrors the `mergeIsLocked` contract in `mergeUnconfirmedSteps`.
|
|
21
|
+
return !origin.getMeta('mergeIsLocked');
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Starting document of each composed step this module has produced.
|
|
26
|
+
*
|
|
27
|
+
* A composed step is synthesised rather than taken from a transaction, so it cannot be found in
|
|
28
|
+
* `origin.steps` and its starting document cannot be recovered the way an original frame's can.
|
|
29
|
+
* `transformUnconfirmed` runs once per transaction and is handed back the steps it returned last
|
|
30
|
+
* time, so the composed step needs to carry its document forward to keep composing against later
|
|
31
|
+
* frames.
|
|
32
|
+
*
|
|
33
|
+
* Keyed on the step rather than its Rebaseable wrapper. `mergeUnconfirmedSteps` runs immediately
|
|
34
|
+
* after this module in `transformUnconfirmed` and rebuilds every wrapper it is given — merging
|
|
35
|
+
* enabled or not — so a wrapper-keyed entry would be unreachable by the very next pass. The step
|
|
36
|
+
* survives that rebuild untouched, and is still replaced whenever the association genuinely
|
|
37
|
+
* becomes stale: `rebaseSteps` maps steps into new objects, and a real merge composes a new one.
|
|
38
|
+
*/
|
|
39
|
+
var composedStepDocs = new WeakMap();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The document this step was applied to.
|
|
43
|
+
*
|
|
44
|
+
* `transformUnconfirmed` is handed steps and nothing else, but a Rebaseable keeps a reference to
|
|
45
|
+
* the transaction that produced it, and a transaction records the document before each of its
|
|
46
|
+
* steps. That is enough for `mergeOverlappingSteps` to resolve merges it cannot derive from the
|
|
47
|
+
* steps alone — notably a frame that replaces more than just what the previous frame inserted.
|
|
48
|
+
*
|
|
49
|
+
* Rebasing maps a step into a new object while keeping the original transaction, so a rebased step
|
|
50
|
+
* is not found in `origin.steps` and correctly yields no document. `mergeOverlappingSteps` then
|
|
51
|
+
* falls back to the merges it can prove without one.
|
|
52
|
+
*/
|
|
53
|
+
var docBeforeStep = function docBeforeStep(rebaseable) {
|
|
54
|
+
var origin = rebaseable.origin,
|
|
55
|
+
step = rebaseable.step;
|
|
56
|
+
var composed = composedStepDocs.get(step);
|
|
57
|
+
if (composed) {
|
|
58
|
+
return composed;
|
|
59
|
+
}
|
|
60
|
+
var index = origin.steps.indexOf(step);
|
|
61
|
+
return index >= 0 ? origin.docs[index] : undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Compose adjacent streaming frames in the unconfirmed queue so the collab service receives one
|
|
66
|
+
* step per flush instead of one step per rendered chunk.
|
|
67
|
+
*
|
|
68
|
+
* This runs in the collab plugin's `transformUnconfirmed` hook, which fires *after* each frame has
|
|
69
|
+
* already been applied to the document. Collapsing therefore has no effect on what the local user
|
|
70
|
+
* sees, on undo history, or on any other plugin state — it only changes what is sent to the collab
|
|
71
|
+
* service, and what is used as the rebase buffer when remote steps arrive.
|
|
72
|
+
*
|
|
73
|
+
* Because the unconfirmed steps double as the rebase buffer (`rebaseSteps` applies their inverses
|
|
74
|
+
* to the live document), the composition must be exact. `mergeOverlappingSteps` only returns a
|
|
75
|
+
* merge it can prove exact, and this function degrades to a no-op whenever it declines.
|
|
76
|
+
*
|
|
77
|
+
* @param steps Rebaseable steps
|
|
78
|
+
* @returns Rebaseable steps, with adjacent streaming frames composed
|
|
79
|
+
*/
|
|
80
|
+
export function collapseStreamingSteps(steps) {
|
|
81
|
+
// Fast path: nothing to do unless at least two frames could possibly compose.
|
|
82
|
+
if (steps.length < 2) {
|
|
83
|
+
return steps;
|
|
84
|
+
}
|
|
85
|
+
var collapsed = [];
|
|
86
|
+
// The document the run currently at the tail of `collapsed` started from. A composed step
|
|
87
|
+
// still applies to the document its first frame applied to, so this survives each merge.
|
|
88
|
+
var runDocBefore;
|
|
89
|
+
var _iterator = _createForOfIteratorHelper(steps),
|
|
90
|
+
_step;
|
|
91
|
+
try {
|
|
92
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
93
|
+
var rebaseable = _step.value;
|
|
94
|
+
var previous = collapsed[collapsed.length - 1];
|
|
95
|
+
if (!previous || !isCollapsibleFrame(previous) || !isCollapsibleFrame(rebaseable)) {
|
|
96
|
+
collapsed.push(rebaseable);
|
|
97
|
+
runDocBefore = docBeforeStep(rebaseable);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
var merged = mergeOverlappingSteps(previous, rebaseable, runDocBefore);
|
|
101
|
+
if (!merged) {
|
|
102
|
+
collapsed.push(rebaseable);
|
|
103
|
+
runDocBefore = docBeforeStep(rebaseable);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Keep the earliest origin, matching `mergeUnconfirmedSteps`. `commitUnconfirmedSteps` in
|
|
108
|
+
// the collab provider tracks completion by origin identity, so keeping the earliest means
|
|
109
|
+
// the composed step is not considered confirmed until the whole run has been acknowledged.
|
|
110
|
+
var composed = new Rebaseable(merged.step, merged.inverted, previous.origin);
|
|
111
|
+
if (runDocBefore) {
|
|
112
|
+
composedStepDocs.set(composed.step, runDocBefore);
|
|
113
|
+
}
|
|
114
|
+
collapsed[collapsed.length - 1] = composed;
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
_iterator.e(err);
|
|
118
|
+
} finally {
|
|
119
|
+
_iterator.f();
|
|
120
|
+
}
|
|
121
|
+
return collapsed;
|
|
122
|
+
}
|
|
@@ -8,7 +8,9 @@ export var sendTransaction = function sendTransaction(_ref) {
|
|
|
8
8
|
newEditorState = _ref.newEditorState,
|
|
9
9
|
useNativePlugin = _ref.useNativePlugin,
|
|
10
10
|
viewMode = _ref.viewMode,
|
|
11
|
-
hideTelecursorOnLoad = _ref.hideTelecursorOnLoad
|
|
11
|
+
hideTelecursorOnLoad = _ref.hideTelecursorOnLoad,
|
|
12
|
+
_ref$isSendHeld = _ref.isSendHeld,
|
|
13
|
+
isSendHeld = _ref$isSendHeld === void 0 ? false : _ref$isSendHeld;
|
|
12
14
|
return function (provider) {
|
|
13
15
|
var docChangedTransaction = transactions.find(function (tr) {
|
|
14
16
|
return tr.docChanged;
|
|
@@ -27,7 +29,16 @@ export var sendTransaction = function sendTransaction(_ref) {
|
|
|
27
29
|
// TODO: ED-8995 - We need to do this check to reduce the number of race conditions when working with tables.
|
|
28
30
|
// This metadata is coming from the scaleTable command in table-resizing plugin
|
|
29
31
|
!originalTransaction.getMeta('scaleTable') && trNoAnalytics.docChanged;
|
|
30
|
-
|
|
32
|
+
|
|
33
|
+
// A streaming producer can hold the flush back so several of its frames accumulate in the
|
|
34
|
+
// unconfirmed queue and collapse into one step before being sent. The steps are not
|
|
35
|
+
// dropped: `sendableSteps` returns the whole queue, so the first unheld state update sends
|
|
36
|
+
// everything that accumulated. Skipping `send` also skips `lockStepOrigins`, which is what
|
|
37
|
+
// keeps the accumulated frames composable.
|
|
38
|
+
//
|
|
39
|
+
// Telepointer messages below are intentionally left alone — holding them would freeze
|
|
40
|
+
// other participants' cursors for the length of the hold, and they carry no steps.
|
|
41
|
+
if (!isSendHeld && (useNativePlugin || shouldSendStepForSynchronyCollabProvider)) {
|
|
31
42
|
provider.send(trNoAnalytics, oldEditorState, newEditorState);
|
|
32
43
|
}
|
|
33
44
|
var prevPluginState = pluginKey.getState(oldEditorState);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { ReplaceStep, Transform } from '@atlaskit/editor-prosemirror/transform';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A step paired with its inverse, as stored in the collab unconfirmed queue.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `structure` is `@internal` on ReplaceStep but is part of the serialised form, so this is the
|
|
9
|
+
* supported way to read it. Structural replaces exist to stop rebased steps from overwriting
|
|
10
|
+
* content they should not, so they are never merged.
|
|
11
|
+
*/
|
|
12
|
+
var isStructural = function isStructural(step) {
|
|
13
|
+
return step.toJSON().structure === true;
|
|
14
|
+
};
|
|
15
|
+
var asPlainReplaceStep = function asPlainReplaceStep(step) {
|
|
16
|
+
return (
|
|
17
|
+
// Deliberately excludes ReplaceAroundStep — the position arithmetic below is only sound for a
|
|
18
|
+
// plain range replace.
|
|
19
|
+
step instanceof ReplaceStep && !isStructural(step) ? step : undefined
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Do the two steps touch the same region of the document?
|
|
25
|
+
*
|
|
26
|
+
* `second` is expressed in the coordinate space *after* `first` has been applied, so `first`'s
|
|
27
|
+
* footprint there is the range its slice occupies: `[from, from + slice.size]`.
|
|
28
|
+
*/
|
|
29
|
+
var overlaps = function overlaps(first, second) {
|
|
30
|
+
var firstInsertedEnd = first.from + first.slice.size;
|
|
31
|
+
return second.from <= firstInsertedEnd && second.to >= first.from;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Merge using ProseMirror's own step merging, which handles adjacent insertions and adjacent
|
|
36
|
+
* deletions. Both the steps and their inverses have to merge for the result to be usable as a
|
|
37
|
+
* rebase buffer entry — inverses compose in reverse order.
|
|
38
|
+
*/
|
|
39
|
+
var mergeNatively = function mergeNatively(first, second) {
|
|
40
|
+
var step = first.step.merge(second.step);
|
|
41
|
+
if (!step) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
var inverted = second.inverted.merge(first.inverted);
|
|
45
|
+
if (!inverted) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
step: step,
|
|
50
|
+
inverted: inverted
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Merge any two overlapping steps by applying both to the document and describing the net change
|
|
56
|
+
* as a single minimal replacement.
|
|
57
|
+
*
|
|
58
|
+
* This is the general case: it copes with `second` reaching past `first` on either side, with
|
|
59
|
+
* partial overlaps, and with slices whose open depths differ — none of which can be resolved from
|
|
60
|
+
* the steps alone, because the composed slice has to be cut out of a real document.
|
|
61
|
+
*/
|
|
62
|
+
var mergeViaDocument = function mergeViaDocument(first, second, docBefore) {
|
|
63
|
+
var docAfter;
|
|
64
|
+
try {
|
|
65
|
+
var transform = new Transform(docBefore);
|
|
66
|
+
transform.step(first.step);
|
|
67
|
+
transform.step(second.step);
|
|
68
|
+
docAfter = transform.doc;
|
|
69
|
+
} catch (_unused) {
|
|
70
|
+
// `docBefore` did not match the steps (for example the steps have since been rebased).
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
var start = docBefore.content.findDiffStart(docAfter.content);
|
|
74
|
+
if (start === null) {
|
|
75
|
+
// The steps cancel each other out. There is no single step representing "no change", so
|
|
76
|
+
// leave the pair alone rather than inventing one.
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
var diffEnd = docBefore.content.findDiffEnd(docAfter.content);
|
|
80
|
+
if (!diffEnd) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// findDiffStart and findDiffEnd can overshoot each other when the same text appears on both
|
|
85
|
+
// sides of the change; push the end out so the range stays well formed.
|
|
86
|
+
var endBefore = diffEnd.a,
|
|
87
|
+
endAfter = diffEnd.b;
|
|
88
|
+
var overshoot = start - Math.min(endBefore, endAfter);
|
|
89
|
+
if (overshoot > 0) {
|
|
90
|
+
endBefore += overshoot;
|
|
91
|
+
endAfter += overshoot;
|
|
92
|
+
}
|
|
93
|
+
var step = new ReplaceStep(start, endBefore, docAfter.slice(start, endAfter));
|
|
94
|
+
|
|
95
|
+
// Cheap guarantee that the composition is exact. If it is not, keep the original pair.
|
|
96
|
+
var applied = step.apply(docBefore);
|
|
97
|
+
if (applied.failed !== null || !applied.doc || !applied.doc.eq(docAfter)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
step: step,
|
|
102
|
+
inverted: step.invert(docBefore)
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Merge two consecutive steps into the single step they are collectively equivalent to.
|
|
108
|
+
*
|
|
109
|
+
* `second` must be expressed in the coordinate space produced by `first` — that is, the two must be
|
|
110
|
+
* consecutive steps of the same document. The returned step maps the document from before `first`
|
|
111
|
+
* to after `second`, and the returned inverse maps it back, so the result is safe to use both on
|
|
112
|
+
* the wire and as a rebase buffer entry.
|
|
113
|
+
*
|
|
114
|
+
* Returns `null` whenever the steps do not overlap, or when the merge cannot be proven exact. A
|
|
115
|
+
* `null` result always means "keep the two steps as they are", never "the steps are equivalent to
|
|
116
|
+
* nothing".
|
|
117
|
+
*
|
|
118
|
+
* @param first The earlier step and its inverse.
|
|
119
|
+
* @param second The later step and its inverse.
|
|
120
|
+
* @param docBefore The document `first` applies to. Optional, but without it only ProseMirror's own
|
|
121
|
+
* adjacent-step merges can be resolved, because the composed slice otherwise has to be cut out of a
|
|
122
|
+
* real document. Callers lose it when the queue has been rebased over a remote step.
|
|
123
|
+
*/
|
|
124
|
+
export function mergeOverlappingSteps(first, second, docBefore) {
|
|
125
|
+
var firstStep = asPlainReplaceStep(first.step);
|
|
126
|
+
var secondStep = asPlainReplaceStep(second.step);
|
|
127
|
+
|
|
128
|
+
// Non-plain replaces are only ever merged by ProseMirror itself, which knows the rules for them.
|
|
129
|
+
if (!firstStep || !secondStep) {
|
|
130
|
+
return mergeNatively(first, second);
|
|
131
|
+
}
|
|
132
|
+
if (!overlaps(firstStep, secondStep)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Try the O(1) strategy before the one that has to replay both steps against a document.
|
|
137
|
+
// `transformUnconfirmed` runs on every transaction, and adjacent insertions — which is what
|
|
138
|
+
// consecutive streaming frames often are — are exactly what ProseMirror merges natively.
|
|
139
|
+
var natively = mergeNatively(first, second);
|
|
140
|
+
if (natively) {
|
|
141
|
+
return natively;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Anything else needs the document to cut the composed slice out of, so without one there is
|
|
145
|
+
// no merge that can be proven exact.
|
|
146
|
+
return docBefore ? mergeViaDocument(first, second, docBefore) : null;
|
|
147
|
+
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
2
|
-
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; } } }; }
|
|
3
|
-
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; } }
|
|
4
|
-
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; }
|
|
5
|
-
// delete this file when cleaning up platform_editor_remove_collab_step_metrics
|
|
6
|
-
import { AnalyticsStep } from '@atlaskit/adf-schema/steps/analytics';
|
|
7
2
|
import { BatchAttrsStep } from '@atlaskit/adf-schema/steps/batch-attrs-step';
|
|
8
3
|
import { SetAttrsStep } from '@atlaskit/adf-schema/steps/set-attrs';
|
|
9
4
|
import { AddMarkStep, AddNodeMarkStep, AttrStep, DocAttrStep, RemoveMarkStep, RemoveNodeMarkStep } from '@atlaskit/editor-prosemirror/transform';
|
|
10
|
-
import { sendableSteps } from '@atlaskit/prosemirror-collab';
|
|
11
|
-
import { updateNcsSessionStepMetrics } from './track-step-metrics';
|
|
12
5
|
function groupBy(array, keyGetter) {
|
|
13
6
|
// Check group by exists, and that it's a function. If so, use the native browser code
|
|
14
7
|
if ('groupBy' in Object && typeof Object.groupBy === 'function') {
|
|
@@ -92,101 +85,4 @@ export var groupSteps = function groupSteps(sanitizedSteps) {
|
|
|
92
85
|
acc[key] = Array.isArray(value) ? value.length : 0;
|
|
93
86
|
return acc;
|
|
94
87
|
}, {});
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Processes the steps metadata from the cache and calls the callback function with the processed data.
|
|
99
|
-
*
|
|
100
|
-
* @param {CacheType} cache - A cache containing steps metadata.
|
|
101
|
-
* @param {(data: StepMetadataAnalytics[]) => void} onTrackDataProcessed - Callback function to be called with the processed data.
|
|
102
|
-
*/
|
|
103
|
-
export var task = function task(cache, onTrackDataProcessed) {
|
|
104
|
-
var stepsMetadata = [];
|
|
105
|
-
var _iterator = _createForOfIteratorHelper(cache.values()),
|
|
106
|
-
_step;
|
|
107
|
-
try {
|
|
108
|
-
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
109
|
-
var entry = _step.value;
|
|
110
|
-
var startedAt = entry.startedAt,
|
|
111
|
-
endedAt = entry.endedAt,
|
|
112
|
-
steps = entry.steps;
|
|
113
|
-
var stepTypesAmount = groupSteps(steps.map(sanitizeStep));
|
|
114
|
-
stepsMetadata.push({
|
|
115
|
-
startedAt: startedAt,
|
|
116
|
-
endedAt: endedAt,
|
|
117
|
-
stepTypesAmount: stepTypesAmount
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
} catch (err) {
|
|
121
|
-
_iterator.e(err);
|
|
122
|
-
} finally {
|
|
123
|
-
_iterator.f();
|
|
124
|
-
}
|
|
125
|
-
cache.clear();
|
|
126
|
-
if (stepsMetadata.length > 0) {
|
|
127
|
-
onTrackDataProcessed(stepsMetadata);
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
var stepsSentCache = new Map();
|
|
131
|
-
// Every ten seconds we will try to process the step data.
|
|
132
|
-
var LOW_PRIORITY_DELAY = 10000;
|
|
133
|
-
|
|
134
|
-
// See https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/
|
|
135
|
-
|
|
136
|
-
var getScheduler = function getScheduler(obj) {
|
|
137
|
-
if (!obj) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
if ('scheduler' in obj) {
|
|
141
|
-
return obj.scheduler;
|
|
142
|
-
}
|
|
143
|
-
return null;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* 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.
|
|
148
|
-
*
|
|
149
|
-
* 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/
|
|
150
|
-
*
|
|
151
|
-
* @param {TrackProps} props - The properties required for tracking steps.
|
|
152
|
-
* @param {ExtractInjectionAPI<CollabEditPlugin> | undefined} props.api - The API for the CollabEdit plugin.
|
|
153
|
-
* @param {EditorState} props.newEditorState - The new editor state.
|
|
154
|
-
* @param {Readonly<Transaction[]>} props.transactions - The transactions that contain the steps.
|
|
155
|
-
* @param {(data: StepMetadataAnalytics[]) => void} props.onTrackDataProcessed - Callback function to be called with the processed data.
|
|
156
|
-
*/
|
|
157
|
-
export var track = function track(_ref4) {
|
|
158
|
-
var api = _ref4.api,
|
|
159
|
-
newEditorState = _ref4.newEditorState,
|
|
160
|
-
transactions = _ref4.transactions,
|
|
161
|
-
onTrackDataProcessed = _ref4.onTrackDataProcessed;
|
|
162
|
-
var newSteps = transactions.flatMap(function (t) {
|
|
163
|
-
return t.steps;
|
|
164
|
-
});
|
|
165
|
-
var collabState = sendableSteps(newEditorState);
|
|
166
|
-
var scheduler = getScheduler(window);
|
|
167
|
-
if (!newSteps.length || !scheduler || !collabState) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
var version = collabState.version;
|
|
171
|
-
var buffer = stepsSentCache.get(version);
|
|
172
|
-
var startedAt = (buffer === null || buffer === void 0 ? void 0 : buffer.startedAt) || Date.now();
|
|
173
|
-
var endedAt = Date.now();
|
|
174
|
-
var steps = ((buffer === null || buffer === void 0 ? void 0 : buffer.steps) || []).concat(newSteps);
|
|
175
|
-
stepsSentCache.set(version, {
|
|
176
|
-
startedAt: startedAt,
|
|
177
|
-
endedAt: endedAt,
|
|
178
|
-
steps: steps
|
|
179
|
-
});
|
|
180
|
-
updateNcsSessionStepMetrics({
|
|
181
|
-
api: api,
|
|
182
|
-
steps: newSteps.filter(function (step) {
|
|
183
|
-
return !(step instanceof AnalyticsStep);
|
|
184
|
-
})
|
|
185
|
-
});
|
|
186
|
-
scheduler.postTask(function () {
|
|
187
|
-
task(stepsSentCache, onTrackDataProcessed);
|
|
188
|
-
}, {
|
|
189
|
-
priority: 'background',
|
|
190
|
-
delay: LOW_PRIORITY_DELAY
|
|
191
|
-
});
|
|
192
88
|
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Transaction } from '@atlaskit/editor-prosemirror/state';
|
|
2
|
+
/**
|
|
3
|
+
* Upper bound on how long the send may stay held, measured from the transaction that *started* the
|
|
4
|
+
* hold rather than the most recent one.
|
|
5
|
+
*
|
|
6
|
+
* This is the only time bound on the hold. Producers decide when to release on semantic grounds —
|
|
7
|
+
* a completed block, the end of a stream — and this bounds how long that can take. It therefore
|
|
8
|
+
* doubles as the guarantee that a producer which never releases, because it aborted or threw
|
|
9
|
+
* between hold and release, cannot stop the editor sending for the rest of the session.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MAX_SEND_HOLD_MS = 2000;
|
|
12
|
+
export type CollabSendHold = {
|
|
13
|
+
/**
|
|
14
|
+
* Whether the flush is currently held. Reads the clock, so a hold that has outlived
|
|
15
|
+
* {@link MAX_SEND_HOLD_MS} reports as released without needing a transaction to clear it.
|
|
16
|
+
*/
|
|
17
|
+
isHeld: (now?: number) => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Feed every transaction through this. Transactions that carry no {@link DEFER_COLLAB_SEND}
|
|
20
|
+
* metadata leave the hold untouched — that is the whole point of the hold being sticky rather
|
|
21
|
+
* than per-transaction.
|
|
22
|
+
*/
|
|
23
|
+
observe: (transaction: Readonly<Transaction>, now?: number) => void;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Tracks whether sending to the collab service is currently held back.
|
|
27
|
+
*
|
|
28
|
+
* A producer that streams into the document needs several of its transactions to accumulate in the
|
|
29
|
+
* unconfirmed queue before anything is sent, so they can be composed into one step. Marking only
|
|
30
|
+
* the producer's own transactions is not enough: `sendTransaction` runs for *every* state update,
|
|
31
|
+
* and `provider.send` flushes and locks the entire queue regardless of which transaction triggered
|
|
32
|
+
* it. Acknowledgements from previous commits, remote steps, selection changes and the user's own
|
|
33
|
+
* typing all trigger it.
|
|
34
|
+
*
|
|
35
|
+
* So the hold has to be a state that spans transactions rather than a flag on one:
|
|
36
|
+
*
|
|
37
|
+
* - `tr.setMeta(DEFER_COLLAB_SEND, true)` starts the hold, and does *not* extend one already in
|
|
38
|
+
* progress
|
|
39
|
+
* - `tr.setMeta(DEFER_COLLAB_SEND, false)` releases it
|
|
40
|
+
* - anything else leaves it as it is
|
|
41
|
+
*
|
|
42
|
+
* Not extending matters. A producer marks every frame it wants held, so re-arming on each one
|
|
43
|
+
* would push the deadline back indefinitely and the bound would never be reached during an active
|
|
44
|
+
* stream. Measuring from the first held transaction instead makes the hold flush at least every
|
|
45
|
+
* {@link MAX_SEND_HOLD_MS}, with the next held frame starting a fresh window.
|
|
46
|
+
*
|
|
47
|
+
* While held, nothing is sent. Note this includes the local user's own edits: `sendableSteps`
|
|
48
|
+
* returns the queue in version order, so there is no way to send a step that sits behind held
|
|
49
|
+
* steps. Producers should keep hold windows short for that reason.
|
|
50
|
+
*/
|
|
51
|
+
export declare const createCollabSendHold: () => CollabSendHold;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Rebaseable } from '@atlaskit/prosemirror-collab';
|
|
2
|
+
/**
|
|
3
|
+
* Compose adjacent streaming frames in the unconfirmed queue so the collab service receives one
|
|
4
|
+
* step per flush instead of one step per rendered chunk.
|
|
5
|
+
*
|
|
6
|
+
* This runs in the collab plugin's `transformUnconfirmed` hook, which fires *after* each frame has
|
|
7
|
+
* already been applied to the document. Collapsing therefore has no effect on what the local user
|
|
8
|
+
* sees, on undo history, or on any other plugin state — it only changes what is sent to the collab
|
|
9
|
+
* service, and what is used as the rebase buffer when remote steps arrive.
|
|
10
|
+
*
|
|
11
|
+
* Because the unconfirmed steps double as the rebase buffer (`rebaseSteps` applies their inverses
|
|
12
|
+
* to the live document), the composition must be exact. `mergeOverlappingSteps` only returns a
|
|
13
|
+
* merge it can prove exact, and this function degrades to a no-op whenever it declines.
|
|
14
|
+
*
|
|
15
|
+
* @param steps Rebaseable steps
|
|
16
|
+
* @returns Rebaseable steps, with adjacent streaming frames composed
|
|
17
|
+
*/
|
|
18
|
+
export declare function collapseStreamingSteps(steps: Rebaseable[]): Rebaseable[];
|
|
@@ -3,6 +3,12 @@ import type { ViewMode } from '@atlaskit/editor-plugin-editor-viewmode';
|
|
|
3
3
|
import type { EditorState, Transaction } from '@atlaskit/editor-prosemirror/state';
|
|
4
4
|
type Props = {
|
|
5
5
|
hideTelecursorOnLoad: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* When true the flush to the collab service is skipped for this state update. See
|
|
8
|
+
* `createCollabSendHold` — the hold spans transactions, so this is not derived from
|
|
9
|
+
* `originalTransaction` alone.
|
|
10
|
+
*/
|
|
11
|
+
isSendHeld?: boolean;
|
|
6
12
|
newEditorState: EditorState;
|
|
7
13
|
oldEditorState: EditorState;
|
|
8
14
|
originalTransaction: Readonly<Transaction>;
|
|
@@ -10,5 +16,5 @@ type Props = {
|
|
|
10
16
|
useNativePlugin: boolean;
|
|
11
17
|
viewMode?: ViewMode;
|
|
12
18
|
};
|
|
13
|
-
export declare const sendTransaction: ({ originalTransaction, transactions, oldEditorState, newEditorState, useNativePlugin, viewMode, hideTelecursorOnLoad, }: Props) => (provider: CollabEditProvider) => void;
|
|
19
|
+
export declare const sendTransaction: ({ originalTransaction, transactions, oldEditorState, newEditorState, useNativePlugin, viewMode, hideTelecursorOnLoad, isSendHeld, }: Props) => (provider: CollabEditProvider) => void;
|
|
14
20
|
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
|
|
2
|
+
import type { Step } from '@atlaskit/editor-prosemirror/transform-override';
|
|
3
|
+
/**
|
|
4
|
+
* A step paired with its inverse, as stored in the collab unconfirmed queue.
|
|
5
|
+
*/
|
|
6
|
+
export type MergeableStep = {
|
|
7
|
+
inverted: Step;
|
|
8
|
+
step: Step;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Merge two consecutive steps into the single step they are collectively equivalent to.
|
|
12
|
+
*
|
|
13
|
+
* `second` must be expressed in the coordinate space produced by `first` — that is, the two must be
|
|
14
|
+
* consecutive steps of the same document. The returned step maps the document from before `first`
|
|
15
|
+
* to after `second`, and the returned inverse maps it back, so the result is safe to use both on
|
|
16
|
+
* the wire and as a rebase buffer entry.
|
|
17
|
+
*
|
|
18
|
+
* Returns `null` whenever the steps do not overlap, or when the merge cannot be proven exact. A
|
|
19
|
+
* `null` result always means "keep the two steps as they are", never "the steps are equivalent to
|
|
20
|
+
* nothing".
|
|
21
|
+
*
|
|
22
|
+
* @param first The earlier step and its inverse.
|
|
23
|
+
* @param second The later step and its inverse.
|
|
24
|
+
* @param docBefore The document `first` applies to. Optional, but without it only ProseMirror's own
|
|
25
|
+
* adjacent-step merges can be resolved, because the composed slice otherwise has to be cut out of a
|
|
26
|
+
* real document. Callers lose it when the queue has been rebased over a remote step.
|
|
27
|
+
*/
|
|
28
|
+
export declare function mergeOverlappingSteps(first: MergeableStep, second: MergeableStep, docBefore?: PMNode): MergeableStep | null;
|
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
|
|
2
|
-
import type { EditorState, Transaction } from '@atlaskit/editor-prosemirror/state';
|
|
3
1
|
import type { Step } from '@atlaskit/editor-prosemirror/transform-override';
|
|
4
|
-
import type { CollabEditPlugin } from '../collabEditPluginType';
|
|
5
2
|
export type SanitizedStep = {
|
|
6
3
|
attr?: string;
|
|
7
4
|
markType?: string;
|
|
8
5
|
stepType: string;
|
|
9
6
|
};
|
|
10
|
-
export type StepMetadataAnalytics = {
|
|
11
|
-
endedAt: number;
|
|
12
|
-
startedAt: number;
|
|
13
|
-
stepTypesAmount: {
|
|
14
|
-
[key: string]: number;
|
|
15
|
-
};
|
|
16
|
-
};
|
|
17
7
|
/**
|
|
18
8
|
* Sanitizes a given ProseMirror step by extracting its type and non-UCG relevant attributes.
|
|
19
9
|
*
|
|
@@ -48,34 +38,3 @@ export declare const sanitizeStep: (step: Step) => SanitizedStep;
|
|
|
48
38
|
* ```
|
|
49
39
|
*/
|
|
50
40
|
export declare const groupSteps: (sanitizedSteps: SanitizedStep[]) => Record<string, number>;
|
|
51
|
-
/**
|
|
52
|
-
* Processes the steps metadata from the cache and calls the callback function with the processed data.
|
|
53
|
-
*
|
|
54
|
-
* @param {CacheType} cache - A cache containing steps metadata.
|
|
55
|
-
* @param {(data: StepMetadataAnalytics[]) => void} onTrackDataProcessed - Callback function to be called with the processed data.
|
|
56
|
-
*/
|
|
57
|
-
export declare const task: (cache: CacheType, onTrackDataProcessed: (data: StepMetadataAnalytics[]) => void) => void;
|
|
58
|
-
export type CacheType = Map<number, {
|
|
59
|
-
endedAt: number;
|
|
60
|
-
startedAt: number;
|
|
61
|
-
steps: ReadonlyArray<Step>;
|
|
62
|
-
}>;
|
|
63
|
-
type TrackProps = {
|
|
64
|
-
api: ExtractInjectionAPI<CollabEditPlugin> | undefined;
|
|
65
|
-
newEditorState: EditorState;
|
|
66
|
-
onTrackDataProcessed: (data: StepMetadataAnalytics[]) => void;
|
|
67
|
-
transactions: Readonly<Transaction[]>;
|
|
68
|
-
};
|
|
69
|
-
/**
|
|
70
|
-
* 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.
|
|
71
|
-
*
|
|
72
|
-
* 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/
|
|
73
|
-
*
|
|
74
|
-
* @param {TrackProps} props - The properties required for tracking steps.
|
|
75
|
-
* @param {ExtractInjectionAPI<CollabEditPlugin> | undefined} props.api - The API for the CollabEdit plugin.
|
|
76
|
-
* @param {EditorState} props.newEditorState - The new editor state.
|
|
77
|
-
* @param {Readonly<Transaction[]>} props.transactions - The transactions that contain the steps.
|
|
78
|
-
* @param {(data: StepMetadataAnalytics[]) => void} props.onTrackDataProcessed - Callback function to be called with the processed data.
|
|
79
|
-
*/
|
|
80
|
-
export declare const track: ({ api, newEditorState, transactions, onTrackDataProcessed, }: TrackProps) => void;
|
|
81
|
-
export {};
|