UNPKG

@atlaskit/editor-plugin-collab-edit

Version:

Collab Edit plugin for @atlaskit/editor-core

122 lines (116 loc) • 6.88 kB
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; } } }; } 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; } } 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; } import { STREAMING_COLLAPSIBLE_STEPS } from '@atlaskit/editor-common/collab-streaming-step-collapse'; import { Transaction } from '@atlaskit/editor-prosemirror/state'; import { Rebaseable } from '@atlaskit/prosemirror-collab'; import { mergeOverlappingSteps } from './mergeOverlappingSteps'; /** * Is this step an intermediate frame that its producer has marked as collapsible, and is it still * free to be composed? */ var isCollapsibleFrame = function isCollapsibleFrame(rebaseable) { var origin = rebaseable.origin; if (!(origin instanceof Transaction) || origin.getMeta(STREAMING_COLLAPSIBLE_STEPS) !== true) { return false; } // Already sent to (or queued for) the collab service — composing it now would change a step the // backend has already seen. Mirrors the `mergeIsLocked` contract in `mergeUnconfirmedSteps`. return !origin.getMeta('mergeIsLocked'); }; /** * Starting document of each composed step this module has produced. * * A composed step is synthesised rather than taken from a transaction, so it cannot be found in * `origin.steps` and its starting document cannot be recovered the way an original frame's can. * `transformUnconfirmed` runs once per transaction and is handed back the steps it returned last * time, so the composed step needs to carry its document forward to keep composing against later * frames. * * Keyed on the step rather than its Rebaseable wrapper. `mergeUnconfirmedSteps` runs immediately * after this module in `transformUnconfirmed` and rebuilds every wrapper it is given — merging * enabled or not — so a wrapper-keyed entry would be unreachable by the very next pass. The step * survives that rebuild untouched, and is still replaced whenever the association genuinely * becomes stale: `rebaseSteps` maps steps into new objects, and a real merge composes a new one. */ var composedStepDocs = new WeakMap(); /** * The document this step was applied to. * * `transformUnconfirmed` is handed steps and nothing else, but a Rebaseable keeps a reference to * the transaction that produced it, and a transaction records the document before each of its * steps. That is enough for `mergeOverlappingSteps` to resolve merges it cannot derive from the * steps alone — notably a frame that replaces more than just what the previous frame inserted. * * Rebasing maps a step into a new object while keeping the original transaction, so a rebased step * is not found in `origin.steps` and correctly yields no document. `mergeOverlappingSteps` then * falls back to the merges it can prove without one. */ var docBeforeStep = function docBeforeStep(rebaseable) { var origin = rebaseable.origin, step = rebaseable.step; var composed = composedStepDocs.get(step); if (composed) { return composed; } var index = origin.steps.indexOf(step); return index >= 0 ? origin.docs[index] : undefined; }; /** * Compose adjacent streaming frames in the unconfirmed queue so the collab service receives one * step per flush instead of one step per rendered chunk. * * This runs in the collab plugin's `transformUnconfirmed` hook, which fires *after* each frame has * already been applied to the document. Collapsing therefore has no effect on what the local user * sees, on undo history, or on any other plugin state — it only changes what is sent to the collab * service, and what is used as the rebase buffer when remote steps arrive. * * Because the unconfirmed steps double as the rebase buffer (`rebaseSteps` applies their inverses * to the live document), the composition must be exact. `mergeOverlappingSteps` only returns a * merge it can prove exact, and this function degrades to a no-op whenever it declines. * * @param steps Rebaseable steps * @returns Rebaseable steps, with adjacent streaming frames composed */ export function collapseStreamingSteps(steps) { // Fast path: nothing to do unless at least two frames could possibly compose. if (steps.length < 2) { return steps; } var collapsed = []; // The document the run currently at the tail of `collapsed` started from. A composed step // still applies to the document its first frame applied to, so this survives each merge. var runDocBefore; var _iterator = _createForOfIteratorHelper(steps), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var rebaseable = _step.value; var previous = collapsed[collapsed.length - 1]; if (!previous || !isCollapsibleFrame(previous) || !isCollapsibleFrame(rebaseable)) { collapsed.push(rebaseable); runDocBefore = docBeforeStep(rebaseable); continue; } var merged = mergeOverlappingSteps(previous, rebaseable, runDocBefore); if (!merged) { collapsed.push(rebaseable); runDocBefore = docBeforeStep(rebaseable); continue; } // Keep the earliest origin, matching `mergeUnconfirmedSteps`. `commitUnconfirmedSteps` in // the collab provider tracks completion by origin identity, so keeping the earliest means // the composed step is not considered confirmed until the whole run has been acknowledged. var composed = new Rebaseable(merged.step, merged.inverted, previous.origin); if (runDocBefore) { composedStepDocs.set(composed.step, runDocBefore); } collapsed[collapsed.length - 1] = composed; } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return collapsed; }