UNPKG

@atlaskit/editor-plugin-interactivity

Version:

Interactivity plugin for @atlaskit/editor-core

327 lines (309 loc) 14.6 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.InteractionTracker = void 0; var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _boundedList = require("../collections/bounded-list"); var _boundedMap = require("../collections/bounded-map"); var _interactionEvents = require("./interaction-events"); /** * The Event Timing fields this package reads, declared optional so that a `PerformanceEntry` from * `getEntries()` is assignable without a cast — `interactionId` is missing from the DOM typings' * `PerformanceEventTiming` altogether, and the rest are only on it. */ /** * One paint, and the processing of every event it presented. * * One paint can present several events, and the handlers of all of them ran before it: a * `pointerover` handler that was still running when the user clicked held up the paint that showed * the click. So an interaction's processing is the processing of its whole paint, not of its own * events only — otherwise a handler that is not its own reads as time the user waited for nothing. * * Event Timing gives a paint no identity. The only thing an entry says about it is * `startTime + duration`, the moment it happened, so that is what identifies it. */ /** * The paint an entry was presented by, and whether the entry moved the processing that paint covers. * An entry whose handlers ran inside what the paint already covered changes nothing for any * interaction reading its boundaries from it. */ /** * The four moments an interaction's latency divides at, in order: the user acted, its handlers * started running, they finished, the screen updated. */ /** * What an entry did to the interaction it belongs to: either it is the first entry of a new * interaction, or it changed an interaction already known. Both carry the editor group of the * interaction, if it is one of the editor's, and the boundaries it now has. * * A `remeasured` where `previousLatencyMs` equals `latencyMs` is an interaction whose latency stayed * as it was and whose boundaries moved: the entry ran in the same paint without being the slowest * of them. */ /** * What is kept per interaction. The boundaries are not among these: they are derived from the paint * whenever the interaction is reported, because the paint keeps growing as the browser reports the * remaining events it presented. */ /** * How many interactions are remembered, so their growth can still be applied, and how many of * the editor's events. Entries of one interaction arrive within the interaction itself, so * anything older than the last few hundred is not needed. */ var MAX_TRACKED = 256; /** * How many paints entries can still be placed in. The entries of a paint arrive within a batch or * two of each other, so this only has to cover the paints in flight; it is what `web-vitals` keeps. */ var MAX_RECENT_PAINTS = 10; /** * Event Timing rounds `duration` down to 8 ms, so two events presented by one paint report that * paint up to this far apart — and nothing else in Event Timing says they share it. */ var PRESENTATION_ROUNDING_MS = 8; /** * Identifies the event an entry measured: an entry's `startTime` is that event's timestamp and its * `name` is its type. The type is in the key because browsers coarsen the timestamp, so two events * of one task can share it — and events of one type are always of one group, so a collision cannot * move an interaction into another group. */ function eventKey(type, timeStamp) { return "".concat(type, "|").concat(timeStamp); } /** * Makes interactions out of what the two observers report, for one session. * * Entries sharing a non-zero `interactionId` are one interaction whose latency is the * maximum `duration` among them. Entries arrive incrementally, so an interaction's latency * can grow after it was first reported — callers apply that to what they already counted * rather than counting the interaction twice. * * A non-zero `interactionId` is the browser's own definition of an interaction, which is * also what INP filters on: it is assigned to the pointer and keyboard events that make one * up, and never to scrolling or pointer movement. * * The editor's events answer what an entry cannot: which interactions were with the editor, and * how many there were, including the ones below the Event Timing reporting threshold. * * Every entry is also placed in the paint that presented it, which is what says how an * interaction's latency divides into waiting, processing and presentation. See `Paint`. */ var InteractionTracker = exports.InteractionTracker = /*#__PURE__*/function () { /** * @param startsAfterInteractionId interactions up to and including this one belong to the * previous tracker and are ignored. `interactionId` counts up over the life of the page, so * a session opening mid-page passes the highest id the one before it saw; without that, an * entry still arriving for an interaction from the previous session would look new here and * be counted in both. */ function InteractionTracker() { var startsAfterInteractionId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; (0, _classCallCheck2.default)(this, InteractionTracker); (0, _defineProperty2.default)(this, "interactions", new _boundedMap.BoundedMap(MAX_TRACKED)); (0, _defineProperty2.default)(this, "groupByEvent", new _boundedMap.BoundedMap(MAX_TRACKED)); (0, _defineProperty2.default)(this, "recentPaints", new _boundedList.BoundedList(MAX_RECENT_PAINTS)); (0, _defineProperty2.default)(this, "highestInteractionId", 0); this.startsAfterInteractionId = startsAfterInteractionId; } /** The highest `interactionId` this tracker has seen. */ return (0, _createClass2.default)(InteractionTracker, [{ key: "lastInteractionId", get: function get() { return Math.max(this.highestInteractionId, this.startsAfterInteractionId); } /** * Merges an entry into the interaction it belongs to. * * @returns what that changed about the interactions this tracker knows, the entry's own first. * More than one of them when the paint the entry ran in presented several. */ }, { key: "merge", value: function merge(entry) { if (!Number.isFinite(entry.duration) || entry.duration < 0) { return []; } var placement = this.paintOf(entry); var paint = placement === null || placement === void 0 ? void 0 : placement.paint; var interactionId = entry.interactionId; // Reported only when the entry grew the paint, because otherwise nothing an interaction reads // from it moved. Every interaction the paint presented is here, not only the entry's own: a // `first-input` or non-interaction event reports `interactionId` 0 and has none of its own, // and a second press of the same paint moved where the first one spent its latency. // // The check below is neither reached with a `0` nor needed: the interactions reported are the // ones this tracker holds, and the only way into that map is past the check. var remeasuredOthers = placement !== null && placement !== void 0 && placement.grew ? this.remeasuredUpdatesIn(placement.paint, { except: interactionId }) : []; if (!interactionId) { return remeasuredOthers; } if (interactionId <= this.startsAfterInteractionId) { // The entry belongs to the session before this one, but its handlers still ran before a // paint of this one. return remeasuredOthers; } this.highestInteractionId = Math.max(this.highestInteractionId, interactionId); var tracked = this.interactions.get(interactionId); if (tracked === undefined) { // Taken once: an entry that only makes the interaction slower has to move its count // within the group it was counted in, not into another one. var group = this.groupByEvent.get(eventKey(entry.name, entry.startTime)); var interaction = { group: group, latencyMs: entry.duration, presentedIn: paint, startedAt: entry.startTime }; this.interactions.set(interactionId, interaction); return [this.newUpdate(interactionId, interaction)].concat((0, _toConsumableArray2.default)(remeasuredOthers)); } if (entry.duration > tracked.latencyMs) { var previousLatencyMs = tracked.latencyMs; tracked.latencyMs = entry.duration; tracked.presentedIn = paint; tracked.startedAt = entry.startTime; return [this.remeasuredUpdate(interactionId, tracked, previousLatencyMs)].concat((0, _toConsumableArray2.default)(remeasuredOthers)); } // Not the slowest entry of the interaction, so its latency stands. Its handlers still ran // before the same paint, if this is that paint, and so moved where that latency went. if (!(placement !== null && placement !== void 0 && placement.grew) || paint !== tracked.presentedIn) { return remeasuredOthers; } return [this.remeasuredUpdate(interactionId, tracked, tracked.latencyMs)].concat((0, _toConsumableArray2.default)(remeasuredOthers)); } }, { key: "recordEditorEvent", value: function recordEditorEvent(event) { var kind = (0, _interactionEvents.interactionEventKind)(event.type); if (!kind) { return undefined; } // Every event of the interaction, because any of them can be the one Event Timing reports // as the slowest: for a pointer press that is usually the click. this.groupByEvent.set(eventKey(event.type, event.timeStamp), kind.group); return kind.counts ? kind.group : undefined; } /** * Every interaction whose boundaries are read from this paint, reported as measured again at the * latency it already had. * * @param except the interaction the entry measured, which the caller reports itself. `0` or * nothing when the entry measured none, and then no interaction is left out. */ }, { key: "remeasuredUpdatesIn", value: function remeasuredUpdatesIn(paint, _ref) { var _this = this; var except = _ref.except; var remeasured = []; this.interactions.forEach(function (interaction, interactionId) { if (interaction.presentedIn === paint && interactionId !== except) { remeasured.push(_this.remeasuredUpdate(interactionId, interaction, interaction.latencyMs)); } }); return remeasured; } }, { key: "newUpdate", value: function newUpdate(interactionId, tracked) { return { type: 'new', interactionId: interactionId, latencyMs: tracked.latencyMs, group: tracked.group, boundaries: this.boundariesOf(tracked) }; } }, { key: "remeasuredUpdate", value: function remeasuredUpdate(interactionId, tracked, previousLatencyMs) { return { type: 'remeasured', interactionId: interactionId, previousLatencyMs: previousLatencyMs, latencyMs: tracked.latencyMs, group: tracked.group, boundaries: this.boundariesOf(tracked) }; } /** * The four moments of an interaction, read from the paint as it stands now. * * Limited the way `web-vitals` limits its INP attribution, so the four stay in order: the paint's * handlers can have started before the event arrived, and can have finished after the paint the * event's rounded-down `duration` points at. * * @returns nothing when the browser reported no processing timestamps for the interaction, which * leaves it in no paint. */ }, { key: "boundariesOf", value: function boundariesOf(_ref2) { var latencyMs = _ref2.latencyMs, presentedIn = _ref2.presentedIn, startedAt = _ref2.startedAt; if (!presentedIn) { return undefined; } var processingStartedAt = Math.max(presentedIn.processingStartedAt, startedAt); var presentedAt = Math.max(startedAt + latencyMs, processingStartedAt); var processingEndedAt = Math.min(presentedIn.processingEndedAt, presentedAt); return { startedAt: startedAt, processingStartedAt: processingStartedAt, processingEndedAt: processingEndedAt, presentedAt: presentedAt }; } /** * The paint that presented this entry, grown to cover this entry's own processing. * * The moment being matched is always the one the first entry of the paint reported, so that a * run of entries 8 ms apart cannot walk one paint across the next. * * @returns nothing when the browser reported no processing timestamps for the entry, which * leaves nothing to place it by. */ }, { key: "paintOf", value: function paintOf(entry) { var startTime = entry.startTime, duration = entry.duration, processingStart = entry.processingStart, processingEnd = entry.processingEnd; if (typeof processingStart !== 'number' || typeof processingEnd !== 'number') { return undefined; } var presentedAt = startTime + duration; var knownPaint = this.recentPaints.findLast(function (paint) { return Math.abs(presentedAt - paint.presentedAt) <= PRESENTATION_ROUNDING_MS; }); if (knownPaint) { var grew = processingStart < knownPaint.processingStartedAt || processingEnd > knownPaint.processingEndedAt; knownPaint.processingStartedAt = Math.min(processingStart, knownPaint.processingStartedAt); knownPaint.processingEndedAt = Math.max(processingEnd, knownPaint.processingEndedAt); return { grew: grew, paint: knownPaint }; } var newPaint = { presentedAt: presentedAt, processingStartedAt: processingStart, processingEndedAt: processingEnd }; this.recentPaints.push(newPaint); return { grew: true, paint: newPaint }; } }]); }();