@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
283 lines (272 loc) • 13.9 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.PortalManager = 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 _throttle = _interopRequireDefault(require("lodash/throttle"));
var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
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; }
var DEFAULT_INITIAL_BUCKETS = 50;
var DEFAULT_MAX_BUCKET_CAPACITY = 50;
var DEFAULT_SCALE_RATIO = 0.5;
var DEFAULT_THROTTLE_DELAY = 16; // ~60fps for smooth updates
/**
* Creates an empty bucket object with a specified capacity. Each bucket is designed
* to hold a certain number of React portals and has an associated updater function
* which can be null initially.
*
* @function createEmptyBucket
* @param {number} capacity - The maximum capacity of the bucket.
* @returns {PortalBucketType} An object representing an empty bucket with the specified capacity.
*/
function createEmptyBucket(capacity) {
return {
portals: {},
capacity: capacity,
updater: null
};
}
/**
* A utility class to manage and dynamically scale React portals across multiple buckets.
* It allows for efficient rendering of large numbers of React portals by distributing them
* across "buckets" and updating these buckets as necessary to balance load and performance.
*
* @class PortalManager
* @typedef {object} PortalManager
*
* @property {number} maxBucketCapacity - The maximum capacity of each bucket before a new bucket is created.
* @property {number} scaleRatio - The ratio to determine the number of new buckets to add when scaling up.
* @property {Array<PortalBucketType>} buckets - An array of bucket objects where each bucket holds a record of React portals.
* @property {Set<number>} availableBuckets - A set of indices representing buckets that have available capacity.
* @property {Map<React.Key, number>} portalToBucketMap - A map of React portal keys to their corresponding bucket indices.
* @property {PortalRendererUpdater|null} portalRendererUpdater - A function to trigger updates to the rendering of portals.
* @property {number} scaleCapacityThreshold - The threshold at which the buckets are scaled up to accommodate more portals.
* @property {Map<number, ReturnType<typeof throttle>>} throttledBucketUpdaters - A map of bucket IDs to their throttled update functions.
* @property {number} throttleDelay - The delay in milliseconds for throttling bucket updates.
*
* @param {number} [initialBuckets=DEFAULT_INITIAL_BUCKETS] - The initial number of buckets to create.
* @param {number} [maxBucketCapacity=DEFAULT_MAX_BUCKET_CAPACITY] - The maximum number of portals a single bucket can hold.
* @param {number} [scaleRatio=DEFAULT_SCALE_RATIO] - The ratio used to calculate the number of new buckets to add when scaling.
* @param {number} [throttleDelay=DEFAULT_THROTTLE_DELAY] - The delay in milliseconds for throttling updates.
*/
var PortalManager = exports.PortalManager = /*#__PURE__*/function () {
/**
* RequestAnimationFrame callback ID used to transition from synchronous to throttled update mode.
* This ensures we switch to throttled mode after the initial render cycle completes.
*/
function PortalManager() {
var initialBuckets = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : DEFAULT_INITIAL_BUCKETS;
var maxBucketCapacity = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : DEFAULT_MAX_BUCKET_CAPACITY;
var scaleRatio = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : DEFAULT_SCALE_RATIO;
var throttleDelay = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : DEFAULT_THROTTLE_DELAY;
(0, _classCallCheck2.default)(this, PortalManager);
/**
* Controls whether to use throttled updates or immediate synchronous updates.
* During initial load, we use synchronous updates to avoid delays and ensure immediate responsiveness.
* After the initial load period (one RAF cycle), we switch to throttled updates for better performance.
*/
(0, _defineProperty2.default)(this, "shouldUseThrottledUpdates", false);
this.maxBucketCapacity = maxBucketCapacity;
this.scaleRatio = scaleRatio;
this.throttleDelay = throttleDelay;
// Initialise buckets array by creating an array of length `initialBuckets` containing empty buckets
this.buckets = Array.from({
length: initialBuckets
}, function () {
return createEmptyBucket(maxBucketCapacity);
});
this.portalToBucketMap = new Map();
this.availableBuckets = new Set(Array.from({
length: initialBuckets
}, function (_, i) {
return i;
}));
this.portalRendererUpdater = null;
this.scaleCapacityThreshold = maxBucketCapacity / 2;
this.throttledBucketUpdaters = new Map();
}
return (0, _createClass2.default)(PortalManager, [{
key: "getCurrentBucket",
value: function getCurrentBucket() {
return this.availableBuckets.values().next().value;
}
}, {
key: "createBucket",
value: function createBucket() {
var _this$portalRendererU;
var currentBucket = this.getCurrentBucket();
//If the current bucket has capacity, skip this logic
// @ts-ignore - TS2538 TypeScript 5.9.2 upgrade
if (this.buckets[currentBucket].capacity > 0) {
return;
} else {
// The current bucket is full, delete the bucket from the list of available buckets
// @ts-ignore - TS2345 TypeScript 5.9.2 upgrade
this.availableBuckets.delete(currentBucket);
}
// Skip creating new bucket if there are buckets still available
if (this.availableBuckets.size > 0) {
return;
}
// Scale the buckets up only if there are no available buckets left
// Calculate how many new buckets need to be added
var numBucketsToAdd = Math.floor(this.buckets.length * this.scaleRatio);
this.buckets = (0, _toConsumableArray2.default)(this.buckets);
for (var i = 0; i < numBucketsToAdd; i++) {
this.buckets.push(createEmptyBucket(this.maxBucketCapacity));
this.availableBuckets.add(this.buckets.length - 1);
}
(_this$portalRendererU = this.portalRendererUpdater) === null || _this$portalRendererU === void 0 || _this$portalRendererU.call(this, this.buckets);
}
}, {
key: "getOrCreateThrottledUpdater",
value: function getOrCreateThrottledUpdater(id) {
var _this = this;
if (!this.throttledBucketUpdaters.has(id)) {
var throttledUpdater = (0, _throttle.default)(function () {
var _this$buckets$id, _this$buckets$id$upda;
(_this$buckets$id = _this.buckets[id]) === null || _this$buckets$id === void 0 || (_this$buckets$id$upda = _this$buckets$id.updater) === null || _this$buckets$id$upda === void 0 || _this$buckets$id$upda.call(_this$buckets$id, function () {
return _objectSpread({}, _this.buckets[id].portals);
});
}, this.throttleDelay);
this.throttledBucketUpdaters.set(id, throttledUpdater);
}
return this.throttledBucketUpdaters.get(id);
}
}, {
key: "getBuckets",
value: function getBuckets() {
return this.buckets;
}
}, {
key: "registerBucket",
value: function registerBucket(id, updater) {
var _this$buckets$id$upda2,
_this$buckets$id2,
_this2 = this;
this.buckets[id].updater = updater;
(_this$buckets$id$upda2 = (_this$buckets$id2 = this.buckets[id]).updater) === null || _this$buckets$id$upda2 === void 0 || _this$buckets$id$upda2.call(_this$buckets$id2, function () {
return _objectSpread({}, _this2.buckets[id].portals);
});
}
}, {
key: "unregisterBucket",
value: function unregisterBucket(id) {
this.buckets[id].updater = null;
// Clean up throttled updater when bucket is unregistered
if (this.throttledBucketUpdaters.has(id)) {
var _this$throttledBucket;
(_this$throttledBucket = this.throttledBucketUpdaters.get(id)) === null || _this$throttledBucket === void 0 || _this$throttledBucket.cancel();
this.throttledBucketUpdaters.delete(id);
}
}
}, {
key: "updateBuckets",
value: function updateBuckets(id) {
var _this3 = this;
var immediate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (!this.throttleActivationRAFId && (0, _expValEquals.expValEquals)('platform_editor_debounce_portal_provider', 'isEnabled', true)) {
this.throttleActivationRAFId = requestAnimationFrame(function () {
_this3.shouldUseThrottledUpdates = true;
});
}
if (immediate || !this.shouldUseThrottledUpdates || !(0, _expValEquals.expValEquals)('platform_editor_debounce_portal_provider', 'isEnabled', true)) {
var _this$buckets$id$upda3, _this$buckets$id3;
// Cancel any pending throttled update and update immediately
if (this.throttledBucketUpdaters.has(id)) {
var _this$throttledBucket2;
(_this$throttledBucket2 = this.throttledBucketUpdaters.get(id)) === null || _this$throttledBucket2 === void 0 || _this$throttledBucket2.cancel();
}
(_this$buckets$id$upda3 = (_this$buckets$id3 = this.buckets[id]).updater) === null || _this$buckets$id$upda3 === void 0 || _this$buckets$id$upda3.call(_this$buckets$id3, function () {
return _objectSpread({}, _this3.buckets[id].portals);
});
} else {
var _this$getOrCreateThro;
// Use throttled update for smooth, regular updates
(_this$getOrCreateThro = this.getOrCreateThrottledUpdater(id)) === null || _this$getOrCreateThro === void 0 || _this$getOrCreateThro();
}
}
}, {
key: "registerPortal",
value: function registerPortal(key, portal) {
var _this$portalToBucketM,
_this4 = this;
var immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
this.createBucket();
// @ts-ignore - TS2538 TypeScript 5.9.2 upgrade
this.buckets[this.getCurrentBucket()].capacity -= 1;
var id = (_this$portalToBucketM = this.portalToBucketMap.get(key)) !== null && _this$portalToBucketM !== void 0 ? _this$portalToBucketM : this.getCurrentBucket();
// @ts-ignore - TS2345 TypeScript 5.9.2 upgrade
this.portalToBucketMap.set(key, id);
// @ts-ignore - TS2538 TypeScript 5.9.2 upgrade
if (this.buckets[id].portals[key] !== portal) {
// @ts-ignore - TS2538 TypeScript 5.9.2 upgrade
this.buckets[id].portals[key] = portal;
// @ts-ignore - TS2345 TypeScript 5.9.2 upgrade
this.updateBuckets(id, immediate);
}
//returns a function to unregister the portal
return function () {
// @ts-ignore - TS2538 TypeScript 5.9.2 upgrade
delete _this4.buckets[id].portals[key];
_this4.portalToBucketMap.delete(key);
// @ts-ignore - TS2538 TypeScript 5.9.2 upgrade
_this4.buckets[id].capacity += 1;
// @ts-ignore - TS2538 TypeScript 5.9.2 upgrade
if (_this4.buckets[id].capacity > _this4.scaleCapacityThreshold) {
// @ts-ignore - TS2345 TypeScript 5.9.2 upgrade
_this4.availableBuckets.add(id);
}
// @ts-ignore - TS2345 TypeScript 5.9.2 upgrade
_this4.updateBuckets(id, immediate);
};
}
}, {
key: "registerPortalRenderer",
value: function registerPortalRenderer(updater) {
var _this5 = this;
if (!this.portalRendererUpdater) {
updater(function () {
return _this5.buckets;
});
}
this.portalRendererUpdater = updater;
}
}, {
key: "unregisterPortalRenderer",
value: function unregisterPortalRenderer() {
this.portalRendererUpdater = null;
}
/**
* Cleans up resources used by the PortalManager. This includes clearing all portals,
* unregistering all buckets, and resetting internal state.
*/
}, {
key: "destroy",
value: function destroy() {
var _this6 = this;
// Cancel all pending throttled updates
this.throttledBucketUpdaters.forEach(function (updater) {
updater.cancel();
});
this.throttledBucketUpdaters.clear();
// Iterate through each bucket and clear its portals and unset the updater function
this.buckets.forEach(function (bucket, id) {
bucket.portals = {}; // Clearing all portals from the bucket
bucket.updater = null; // Unsetting the bucket's updater function
_this6.availableBuckets.add(id); // Mark all buckets as available
});
this.portalToBucketMap.clear();
this.portalRendererUpdater = null;
this.availableBuckets = new Set(this.buckets.map(function (_, index) {
return index;
}));
}
}]);
}();