UNPKG

@atlaskit/editor-plugin-limited-mode

Version:

LimitedMode plugin for @atlaskit/editor-core

219 lines (208 loc) • 10.6 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.createPlugin = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _analytics = require("@atlaskit/editor-common/analytics"); var _safePlugin = require("@atlaskit/editor-common/safe-plugin"); var _shouldEnableLimitedMode = require("@atlaskit/editor-common/should-enable-limited-mode"); var _expVal = require("@atlaskit/platform-feature-experiments/exp-val"); var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled"); var _pluginKey = require("./plugin-key"); var _latchDetector = require("./utils/latch-detector"); var _latchPolicy = require("./utils/latch-policy"); function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } /** * Meta shape used to latch limited mode at runtime. Dispatching this is the entire delivery * mechanism: the transaction flows through `SharedStateAPI.notifyListeners`, which diffs the plugin's * shared state and notifies every consumer. No plugin re-registration and no schema rebuild. */ /** * Hardware hints, reported with a latch so it can be correlated with device class. * * Telemetry only — the policy no longer takes the hardware into account when deciding, so this * exists to answer "which devices are latching" from the data rather than by assumption. Both hints * are optional (`deviceMemory` is Chromium-only) and are simply absent where unsupported. */ var getDeviceHints = function getDeviceHints() { if (typeof navigator === 'undefined') { return {}; } var _ref = navigator, hardwareConcurrency = _ref.hardwareConcurrency, deviceMemory = _ref.deviceMemory; return { hardwareConcurrency: hardwareConcurrency, deviceMemoryGb: deviceMemory }; }; /** Control-arm state: the document decision alone, exactly as before the experiment. */ var documentOnlyState = function documentOnlyState(doc) { var documentSizeBreachesThreshold = (0, _shouldEnableLimitedMode.shouldEnableLimitedModeForDocument)(doc); return { documentSizeBreachesThreshold: documentSizeBreachesThreshold, latchPolicyBreached: false, enabled: documentSizeBreachesThreshold }; }; /** Treatment-arm state: whatever the policy says, for both of its reasons. */ var policyState = function policyState(policy) { return { documentSizeBreachesThreshold: policy.isDocumentBreached(), latchPolicyBreached: policy.isBreached(), enabled: policy.isBreached() }; }; var createPlugin = exports.createPlugin = function createPlugin(api, injectedPolicy) { var detector; var policy; if (injectedPolicy) { policy = injectedPolicy; } else if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_dynamic_limited_mode')) { var config = (0, _expVal.expVal)('platform_editor_dynamic_limited_mode', 'policyConfig', { warmUpMs: 10000, slowInputMs: 100, latencyWindowSize: 12, latencySlowSamplesRequired: 6, freezeTaskMs: 600, freezeTasksRequired: 3, freezeWindowMs: 30000, requiredConfirmations: 2, confirmationGapMs: 30000, bulkChangeNodeSize: 100, bulkChangeSuppressionMs: 2000, docSizeThreshold: 750000, nodeCountThreshold: 5000 }); // Resolved once per editor rather than per transaction. When off, no policy is built and the // document decision runs inline exactly as it did before this experiment. policy = new _latchPolicy.LatchPolicy({ now: function now() { return performance.now(); }, config: config }); } return new _safePlugin.SafePlugin({ key: _pluginKey.limitedModePluginKey, props: { handleTextInput: function handleTextInput() { var _detector; (_detector = detector) === null || _detector === void 0 || _detector.measureInput(); // Never handle the input — this is measurement only. return false; } }, view: function view(editorView) { // No policy means the experiment is off: no observers, no per-keystroke measurement. if (!policy) { return {}; } var startedAt = performance.now(); detector = (0, _latchDetector.createLatchDetector)({ policy: policy, onLatchCriteriaMet: function onLatchCriteriaMet(details) { var _api$analytics; // Treatment arm only — the detector does not exist in control. See LimitedModeLatchedAEP. // // `details` is the policy's own snapshot of what it latched on, taken before it cleared // its evidence buffers, so it reports the closing window rather than an empty one. api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.fireAnalyticsEvent({ action: _analytics.ACTION.LIMITED_MODE_LATCHED, actionSubject: _analytics.ACTION_SUBJECT.EDITOR, eventType: _analytics.EVENT_TYPE.OPERATIONAL, attributes: _objectSpread(_objectSpread({ latched: true, reason: details.reason, firstWindowReason: details.firstWindowReason, requiredConfirmations: details.requiredConfirmations, documentAlreadyBreached: details.documentAlreadyBreached, msFromFirstWindow: details.msFromFirstWindow, latencyMedianMs: details.latencyMedianMs, totalInputSamples: details.totalInputSamples, totalSlowInputs: details.totalSlowInputs, totalFreezes: details.totalFreezes, nodeSize: editorView.state.doc.nodeSize }, getDeviceHints()), {}, { // Measured from the plugin view starting, which is a little later than the policy's // own `timeToLatchMs` (it starts at plugin construction). timeToLatch: performance.now() - startedAt }) }); // The policy already holds the latch; this transaction only prompts the plugin to // re-read it, which is what notifies every consumer through shared state. editorView.dispatch(editorView.state.tr.setMeta(_pluginKey.limitedModePluginKey, { latchPolicyBreached: true })); } }); return { destroy: function destroy() { var _detector2; (_detector2 = detector) === null || _detector2 === void 0 || _detector2.destroy(); detector = undefined; } }; }, state: { init: function init(_config, editorState) { if (!policy) { return documentOnlyState(editorState.doc); } policy.evaluateDocument(editorState.doc); return policyState(policy); }, apply: function apply(tr, currentPluginState, oldState, _newState) { var _tr$getMeta; var documentReplaced = Boolean(tr.getMeta('replaceDocument')); if (!policy) { // Control arm, unchanged: skip the traversal once already breached, but always re-check // when the document is replaced (e.g. live-to-live page navigation). if (currentPluginState.documentSizeBreachesThreshold && !documentReplaced) { return currentPluginState; } return documentOnlyState(tr.doc); } // The detector's latch arrives as a transaction so that plugin state stays a function of // the transaction stream rather than of when `apply` happens to read the policy. Dev // tooling dispatches the same meta to force limited mode by hand. if ((_tr$getMeta = tr.getMeta(_pluginKey.limitedModePluginKey)) !== null && _tr$getMeta !== void 0 && _tr$getMeta.latchPolicyBreached) { policy.latch(); } // Only on replacement, never on an ordinary edit: the check walks the whole document, so // running it per transaction is a full-document scan on every keystroke. The trade-off is // that a document editing its way past the thresholds is not noticed until it next loads. // // Deliberately not skipped when limited mode is already on. Replacement is the one moment // the document verdict can go *down* — live-to-live navigation onto a smaller page — so // skipping it there is what would strand limited mode on forever. It costs one walk per // page navigation, which is nothing next to the navigation itself. if (documentReplaced) { policy.evaluateDocument(tr.doc); } // Report bulk work so the policy can discount it. The policy decides what counts as bulk; // this only supplies the facts. // // Known gap: operations that are expensive but barely change document size — table // resize, drag-and-drop moves (delete + insert nets to ~0), type-ahead — are not // suppressed. `@atlaskit/insm` already tracks exactly these via `startHeavyTask`, but its // public facade does not expose `runningHeavyTasks`, so there is no way to read them from // here today. Closing that gap needs an accessor on the insm package. if (tr.docChanged) { var _detector3; (_detector3 = detector) === null || _detector3 === void 0 || _detector3.noteDocumentChange({ nodeSizeDelta: tr.doc.nodeSize - oldState.doc.nodeSize, isDocumentReplaced: documentReplaced }); } var next = policyState(policy); // Keep the previous object when nothing changed, so shared-state diffing stays cheap and // consumers are not notified for no reason. return next.enabled === currentPluginState.enabled && next.documentSizeBreachesThreshold === currentPluginState.documentSizeBreachesThreshold ? currentPluginState : next; } } }); };