@atlaskit/editor-plugin-interactivity
Version:
Interactivity plugin for @atlaskit/editor-core
369 lines (354 loc) • 15.9 kB
JavaScript
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
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) { _defineProperty(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; }
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; }
import { getDocument } from '@atlaskit/browser-apis';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { EditorEventObserver } from './editor-event-observer';
import { InteractionObserver } from './interaction-observer';
import { InteractivitySession } from './interactivity-session';
import { SCHEMA_VERSION } from './bucket-boundaries';
import { LifecycleObserver } from './lifecycle-observer';
import { LongAnimationFrameObserver } from './long-animation-frame-observer';
import { SnapshotScheduler } from './snapshot-scheduler';
/**
* Collects interaction latencies for one editor mount and emits session-to-date snapshots.
*
* Every interaction is counted in `page`, and the ones with the editor again in one of the editor
* groups, which is what tells a slow editor apart from a slow page around it.
*
* `start` and `stop` bound the collecting, which happens once. Within it there can be several
* sessions, because a session covers one document in one mode: a Confluence live page
* navigates and switches between reading and editing without ever remounting the editor, and
* each of those closes the current session and opens the next.
*/
export var InteractivityCollector = /*#__PURE__*/function () {
function InteractivityCollector(options) {
var _this = this;
_classCallCheck(this, InteractivityCollector);
_defineProperty(this, "started", false);
_defineProperty(this, "stopped", false);
this.emit = options.emit;
this.getEditorDomSize = options.getEditorDomSize;
this.getNodeSize = options.getNodeSize;
this.getObjectId = options.getObjectId;
this.getSessionMode = options.getSessionMode;
this.interactionObserver = new InteractionObserver(function (entries) {
return _this.recordEntries(entries);
});
this.longAnimationFrameObserver = isExperimentEnabled('platform_editor_editor_interactivity_slowest') ? new LongAnimationFrameObserver(function (frames) {
return _this.recordFrames(frames);
}) : undefined;
this.editorEvents = new EditorEventObserver(function (event) {
return _this.recordEditorEvent(event);
});
this.snapshotScheduler = new SnapshotScheduler(function () {
return _this.onTimer();
});
this.lifecycleObserver = new LifecycleObserver({
onHidden: function onHidden() {
return _this.onHidden();
},
onVisible: function onVisible() {
return _this.onVisible();
},
onPageHide: function onPageHide() {
return _this.onPageHide();
},
onPageShow: function onPageShow() {
return _this.onPageShow();
}
});
this.session = this.createSession(0);
}
/**
* Starts collecting. Calling it again while collecting changes nothing.
*
* @returns whether collecting is running; `false` when the browser cannot support it, or
* when it has already been stopped.
*/
return _createClass(InteractivityCollector, [{
key: "start",
value: function start() {
var _this$longAnimationFr;
// Collecting is over for good once stopped: the session it covered has been reported.
if (this.stopped) {
return false;
}
if (this.started) {
return true;
}
if (!InteractionObserver.isSupported()) {
return false;
}
// Opened again so the session's timings start with the collecting, not with whenever
// this object was constructed.
this.session = this.createSession(0);
this.interactionObserver.start();
(_this$longAnimationFr = this.longAnimationFrameObserver) === null || _this$longAnimationFr === void 0 || _this$longAnimationFr.start();
this.editorEvents.observe(this.editorRoot);
this.snapshotScheduler.start();
this.lifecycleObserver.start();
this.started = true;
return true;
}
/** Reports the session as ended by the editor unmounting, and stops collecting. */
}, {
key: "stop",
value: function stop() {
var _this$longAnimationFr2;
if (this.stopped) {
return;
}
this.takeSnapshot('unmount');
this.stopped = true;
this.interactionObserver.stop();
(_this$longAnimationFr2 = this.longAnimationFrameObserver) === null || _this$longAnimationFr2 === void 0 || _this$longAnimationFr2.stop();
this.editorEvents.stop();
this.snapshotScheduler.stop();
this.lifecycleObserver.stop();
}
/**
* The element the editor renders itself into is what makes an interaction one of the editor's.
* The session is not tied to it: interactions from before it arrives are counted in `page`.
*/
}, {
key: "setEditorRoot",
value: function setEditorRoot(root) {
this.editorRoot = root !== null && root !== void 0 ? root : undefined;
if (this.started && !this.stopped) {
this.editorEvents.observe(this.editorRoot);
}
}
/**
* Rotates the session when the editor is pointed at different content.
*
* An unknown object id is no information, never a change. The provider resolves
* asynchronously after mount, and `contextIdentifierPlugin` resets its state to the
* configured provider on transactions that do not carry a new one, so the id read here goes
* missing for a moment on a document that never changed.
*/
}, {
key: "onObjectIdChanged",
value: function onObjectIdChanged() {
if (this.stopped) {
return;
}
var objectId = this.getObjectId();
if (objectId === undefined || objectId === this.session.objectId) {
return;
}
if (this.session.objectId === undefined) {
this.session.objectId = objectId;
return;
}
this.rotateSession('navigation');
}
/**
* Rotates the session when the editor switches between reading and editing. Interactions
* with a read-only page are a different population from interactions while editing, so one
* session never covers both.
*/
}, {
key: "onViewModeChanged",
value: function onViewModeChanged() {
if (this.stopped) {
return;
}
var mode = this.getSessionMode();
if (mode === undefined || mode === this.session.mode) {
return;
}
if (this.session.mode === undefined) {
this.session.mode = mode;
return;
}
this.rotateSession('modeChange');
}
/**
* @param startsAfterInteractionId the highest interaction the previous session saw, so
* entries still arriving for it are not counted here as well.
*/
}, {
key: "createSession",
value: function createSession(startsAfterInteractionId) {
var _getDocument;
return new InteractivitySession({
objectId: this.getObjectId(),
mode: this.getSessionMode(),
hidden: ((_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.visibilityState) === 'hidden',
startsAfterInteractionId: startsAfterInteractionId
});
}
/** Reports the current session as ended by `reason` and puts a new one in its place. */
}, {
key: "rotateSession",
value: function rotateSession(reason) {
this.takeSnapshot(reason);
this.session = this.createSession(this.session.tracker.lastInteractionId);
this.snapshotScheduler.restart();
}
}, {
key: "onTimer",
value: function onTimer() {
this.takeSnapshot('timer');
}
}, {
key: "onHidden",
value: function onHidden() {
if (this.session.hiddenSince === undefined) {
this.session.hiddenSince = performance.now();
}
this.takeSnapshot('hidden');
}
}, {
key: "onVisible",
value: function onVisible() {
if (this.session.hiddenSince !== undefined) {
this.session.hiddenMs += performance.now() - this.session.hiddenSince;
this.session.hiddenSince = undefined;
}
this.session.lifecycleSnapshotEmitted = false;
}
}, {
key: "onPageHide",
value: function onPageHide() {
this.takeSnapshot('pagehide');
}
/**
* The page came back from the back/forward cache, which never raises a visibility change.
* The session continues, and the signal that suspended it has been reported, so the next
* one is due.
*/
}, {
key: "onPageShow",
value: function onPageShow() {
this.session.lifecycleSnapshotEmitted = false;
}
/**
* Counts an interaction with the editor, including the ones Event Timing never reports because
* they were faster than its threshold — which is why a count alone moves the session on.
*/
}, {
key: "recordEditorEvent",
value: function recordEditorEvent(event) {
if (this.stopped) {
return;
}
// An event a script dispatched is no interaction: the browser counts none of them either.
if (!event.isTrusted) {
return;
}
var group = this.session.tracker.recordEditorEvent(event);
if (!group) {
return;
}
// The session names its editor groups after the fields they are reported in.
this.session[group].countTotal();
this.session.revision += 1;
}
/** Long Animation Frames say where the latency of a slow interaction went. */
}, {
key: "recordFrames",
value: function recordFrames(frames) {
var _this$session$slowest;
if (this.stopped) {
return;
}
if ((_this$session$slowest = this.session.slowest) !== null && _this$session$slowest !== void 0 && _this$session$slowest.trackLongAnimationFrames(frames)) {
this.session.revision += 1;
}
}
}, {
key: "recordEntries",
value: function recordEntries(entries) {
var _iterator = _createForOfIteratorHelper(entries),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var entry = _step.value;
var _iterator2 = _createForOfIteratorHelper(this.session.tracker.merge(entry)),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var _this$session$slowest2;
var update = _step2.value;
var groupsChanged = this.session.page.trackInteractionUpdate(update);
if (update.group) {
groupsChanged = this.session[update.group].trackInteractionUpdate(update) || groupsChanged;
}
var slowestChanged = (_this$session$slowest2 = this.session.slowest) === null || _this$session$slowest2 === void 0 ? void 0 : _this$session$slowest2.trackInteractionUpdate(entry, update);
if (groupsChanged || slowestChanged) {
this.session.revision += 1;
}
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
}
}, {
key: "takeSnapshot",
value: function takeSnapshot(reason) {
var _this$longAnimationFr3, _session$slowest;
if (this.stopped) {
return;
}
var session = this.session;
// Checked before anything is consumed: `visibilitychange` and `pagehide` fire back to
// back on the same transition and it is reported once, and draining first would take
// entries out of the browser's queue only to suppress the snapshot carrying them.
var lifecycleSignal = reason === 'hidden' || reason === 'pagehide';
if (lifecycleSignal && session.lifecycleSnapshotEmitted) {
return;
}
// Must run before the change check: the browser may be holding entries and frames that have
// not reached the observer callbacks yet, and on `pagehide` there is no later chance to
// pick them up. Entries first, so a record the frames answer for exists by then.
this.interactionObserver.drain();
(_this$longAnimationFr3 = this.longAnimationFrameObserver) === null || _this$longAnimationFr3 === void 0 || _this$longAnimationFr3.drain();
if (session.revision === session.emittedRevision) {
return;
}
if (lifecycleSignal) {
session.lifecycleSnapshotEmitted = true;
}
session.seq += 1;
session.emittedRevision = session.revision;
var now = performance.now();
var hiddenMs = session.hiddenMs + (session.hiddenSince === undefined ? 0 : now - session.hiddenSince);
var pageTotalCount = InteractionObserver.readPageInteractionCount() - session.interactionCountAtStart;
var slowest = (_session$slowest = session.slowest) === null || _session$slowest === void 0 ? void 0 : _session$slowest.snapshot();
this.emit(_objectSpread({
schema: SCHEMA_VERSION,
interactivitySessionId: session.id,
objectId: session.objectId,
seq: session.seq,
reason: reason,
sessionMode: session.mode,
activeMs: Math.round(Math.max(0, now - session.startedAt - hiddenMs)),
hiddenMs: Math.round(hiddenMs),
nodeSize: this.getNodeSize(),
editorDomSize: this.getEditorDomSize(),
// Only `page` is told its total; each editor group has counted its own.
page: session.page.snapshot(pageTotalCount),
editorTyping: session.editorTyping.snapshot(),
editorPointer: session.editorPointer.snapshot(),
editorOther: session.editorOther.snapshot()
}, slowest && {
slowest: slowest
}));
}
}]);
}();