@atlaskit/editor-plugin-collab-edit
Version:
Collab Edit plugin for @atlaskit/editor-core
127 lines (121 loc) • 7.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.collapseStreamingSteps = collapseStreamingSteps;
var _collabStreamingStepCollapse = require("@atlaskit/editor-common/collab-streaming-step-collapse");
var _state = require("@atlaskit/editor-prosemirror/state");
var _prosemirrorCollab = require("@atlaskit/prosemirror-collab");
var _mergeOverlappingSteps = require("./mergeOverlappingSteps");
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; }
/**
* 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 _state.Transaction) || origin.getMeta(_collabStreamingStepCollapse.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
*/
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 = (0, _mergeOverlappingSteps.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 _prosemirrorCollab.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;
}