@atlaskit/editor-plugin-collab-edit
Version:
Collab Edit plugin for @atlaskit/editor-core
198 lines (191 loc) • 8.73 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.track = exports.task = exports.sanitizeStep = exports.groupSteps = void 0;
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _steps = require("@atlaskit/adf-schema/steps");
var _transform = require("@atlaskit/editor-prosemirror/transform");
var _prosemirrorCollab = require("@atlaskit/prosemirror-collab");
var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
var _trackStepMetrics = require("./track-step-metrics");
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; } // delete this file when cleaning up platform_editor_remove_collab_step_metrics
function groupBy(array, keyGetter) {
// Check group by exists, and that it's a function. If so, use the native browser code
if ('groupBy' in Object && typeof Object.groupBy === 'function') {
// @ts-ignore TS2322 - Type 'Partial<Record<string, T[]>>' is not assignable to type 'Record<string, T[]>'.
return Object.groupBy(array, keyGetter);
}
// Fallback to custom implementation
var map = {};
array.forEach(function (item) {
var key = keyGetter(item);
if (!map[key]) {
map[key] = [];
}
map[key].push(item);
});
return map;
}
/**
* Sanitizes a given ProseMirror step by extracting its type and non-UCG relevant attributes.
*
* @param {Step} step - The ProseMirror step to be sanitized.
* @returns {SanitizedStep} - The sanitized step with only necessary information.
*
* @example
* ```
* const step = new AttrStep(10, 'colwidth', [123, 451] );
* const sanitized = sanitizeStep(step);
*
* // Output: { stepType: 'attr', attr: 'example' }
* ```
*/
var sanitizeStep = exports.sanitizeStep = function sanitizeStep(step) {
var serializedStep = step.toJSON();
var sanitizedStep = {
stepType: serializedStep.stepType
};
if (step instanceof _transform.AttrStep || step instanceof _transform.DocAttrStep) {
sanitizedStep.attr = step.attr;
} else if (step instanceof _steps.SetAttrsStep) {
// Combines all attrs keys separated by _ to one single string
sanitizedStep.attr = Object.keys(step.attrs).sort().join('_');
} else if (step instanceof _transform.AddMarkStep || step instanceof _transform.RemoveMarkStep || step instanceof _transform.RemoveNodeMarkStep || step instanceof _transform.AddNodeMarkStep) {
sanitizedStep.markType = step.mark.type.name;
} else if (step instanceof _steps.BatchAttrsStep) {
var batched = step.data.map(function (_ref) {
var nodeType = _ref.nodeType,
attrs = _ref.attrs;
return "".concat(nodeType, "_").concat(Object.keys(attrs).sort().join('_'));
});
sanitizedStep.attr = batched.sort().join('_');
}
return sanitizedStep;
};
/**
* Groups sanitized steps by their type and counts their occurrences.
*
* @param {SanitizedStep[]} sanitizedSteps - An array of sanitized steps.
* @returns {Record<string, number>} - An object where keys are step types and values are their counts.
*
* @example
* ```
* const input = [
* { stepType: 'attr', attr: 'colwidth' },
* { stepType: 'mark', markType: 'bold' },
* { stepType: 'attr', attr: 'colwidth' }
* ];
*
* const grouped = groupSteps(input);
* // Output: { 'attr_example': 2, 'mark_bold': 1 }
* ```
*/
var groupSteps = exports.groupSteps = function groupSteps(sanitizedSteps) {
var grouped = groupBy(sanitizedSteps, function (e) {
return Object.values(e).join('_');
});
return Object.entries(grouped).reduce(function (acc, _ref2) {
var _ref3 = (0, _slicedToArray2.default)(_ref2, 2),
key = _ref3[0],
value = _ref3[1];
acc[key] = Array.isArray(value) ? value.length : 0;
return acc;
}, {});
};
/**
* Processes the steps metadata from the cache and calls the callback function with the processed data.
*
* @param {CacheType} cache - A cache containing steps metadata.
* @param {(data: StepMetadataAnalytics[]) => void} onTrackDataProcessed - Callback function to be called with the processed data.
*/
var task = exports.task = function task(cache, onTrackDataProcessed) {
var stepsMetadata = [];
var _iterator = _createForOfIteratorHelper(cache.values()),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var entry = _step.value;
var startedAt = entry.startedAt,
endedAt = entry.endedAt,
steps = entry.steps;
var stepTypesAmount = groupSteps(steps.map(sanitizeStep));
stepsMetadata.push({
startedAt: startedAt,
endedAt: endedAt,
stepTypesAmount: stepTypesAmount
});
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
cache.clear();
if (stepsMetadata.length > 0) {
onTrackDataProcessed(stepsMetadata);
}
};
var stepsSentCache = new Map();
// Every ten seconds we will try to process the step data.
var LOW_PRIORITY_DELAY = 10000;
// See https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/
var getScheduler = function getScheduler(obj) {
if (!obj) {
return null;
}
if ('scheduler' in obj) {
return obj.scheduler;
}
return null;
};
/**
* 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.
*
* 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/
*
* @param {TrackProps} props - The properties required for tracking steps.
* @param {ExtractInjectionAPI<CollabEditPlugin> | undefined} props.api - The API for the CollabEdit plugin.
* @param {EditorState} props.newEditorState - The new editor state.
* @param {Readonly<Transaction[]>} props.transactions - The transactions that contain the steps.
* @param {(data: StepMetadataAnalytics[]) => void} props.onTrackDataProcessed - Callback function to be called with the processed data.
*/
var track = exports.track = function track(_ref4) {
var api = _ref4.api,
newEditorState = _ref4.newEditorState,
transactions = _ref4.transactions,
onTrackDataProcessed = _ref4.onTrackDataProcessed;
var newSteps = transactions.flatMap(function (t) {
return t.steps;
});
var collabState = (0, _prosemirrorCollab.sendableSteps)(newEditorState);
var scheduler = getScheduler(window);
if (!newSteps.length || !scheduler || !collabState) {
return;
}
var version = collabState.version;
var buffer = stepsSentCache.get(version);
var startedAt = (buffer === null || buffer === void 0 ? void 0 : buffer.startedAt) || Date.now();
var endedAt = Date.now();
var steps = ((buffer === null || buffer === void 0 ? void 0 : buffer.steps) || []).concat(newSteps);
stepsSentCache.set(version, {
startedAt: startedAt,
endedAt: endedAt,
steps: steps
});
(0, _trackStepMetrics.updateNcsSessionStepMetrics)({
api: api,
steps: (0, _experiments.editorExperiment)('platform_editor_reduce_noisy_steps_ncs', true) ? newSteps.filter(function (step) {
return !(step instanceof _steps.AnalyticsStep);
}) : newSteps
});
scheduler.postTask(function () {
task(stepsSentCache, onTrackDataProcessed);
}, {
priority: 'background',
delay: LOW_PRIORITY_DELAY
});
};