UNPKG

@atlaskit/editor-plugin-collab-edit

Version:

Collab Edit plugin for @atlaskit/editor-core

198 lines (191 loc) 8.73 kB
"use strict"; 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 }); };