@atlaskit/editor-plugin-limited-mode
Version:
LimitedMode plugin for @atlaskit/editor-core
219 lines (208 loc) • 10.6 kB
JavaScript
;
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;
}
}
});
};