UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

226 lines (216 loc) • 10.8 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import throttle from 'lodash/throttle'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; const DEFAULT_INITIAL_BUCKETS = 50; const DEFAULT_MAX_BUCKET_CAPACITY = 50; const DEFAULT_SCALE_RATIO = 0.5; const 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, 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. */ export class PortalManager { /** * 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. */ constructor(initialBuckets = DEFAULT_INITIAL_BUCKETS, maxBucketCapacity = DEFAULT_MAX_BUCKET_CAPACITY, scaleRatio = DEFAULT_SCALE_RATIO, throttleDelay = DEFAULT_THROTTLE_DELAY) { /** * 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. */ _defineProperty(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 }, () => createEmptyBucket(maxBucketCapacity)); this.portalToBucketMap = new Map(); this.availableBuckets = new Set(Array.from({ length: initialBuckets }, (_, i) => i)); this.portalRendererUpdater = null; this.scaleCapacityThreshold = maxBucketCapacity / 2; this.throttledBucketUpdaters = new Map(); } getCurrentBucket() { return this.availableBuckets.values().next().value; } createBucket() { var _this$portalRendererU; const 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 const numBucketsToAdd = Math.floor(this.buckets.length * this.scaleRatio); this.buckets = [...this.buckets]; for (let 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 ? void 0 : _this$portalRendererU.call(this, this.buckets); } getOrCreateThrottledUpdater(id) { if (!this.throttledBucketUpdaters.has(id)) { const throttledUpdater = throttle(() => { var _this$buckets$id, _this$buckets$id$upda; (_this$buckets$id = this.buckets[id]) === null || _this$buckets$id === void 0 ? void 0 : (_this$buckets$id$upda = _this$buckets$id.updater) === null || _this$buckets$id$upda === void 0 ? void 0 : _this$buckets$id$upda.call(_this$buckets$id, () => ({ ...this.buckets[id].portals })); }, this.throttleDelay); this.throttledBucketUpdaters.set(id, throttledUpdater); } return this.throttledBucketUpdaters.get(id); } getBuckets() { return this.buckets; } registerBucket(id, updater) { var _this$buckets$id$upda2, _this$buckets$id2; this.buckets[id].updater = updater; (_this$buckets$id$upda2 = (_this$buckets$id2 = this.buckets[id]).updater) === null || _this$buckets$id$upda2 === void 0 ? void 0 : _this$buckets$id$upda2.call(_this$buckets$id2, () => ({ ...this.buckets[id].portals })); } 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 ? void 0 : _this$throttledBucket.cancel(); this.throttledBucketUpdaters.delete(id); } } updateBuckets(id, immediate = false) { if (!this.throttleActivationRAFId && expValEquals('platform_editor_debounce_portal_provider', 'isEnabled', true)) { this.throttleActivationRAFId = requestAnimationFrame(() => { this.shouldUseThrottledUpdates = true; }); } if (immediate || !this.shouldUseThrottledUpdates || !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 ? void 0 : _this$throttledBucket2.cancel(); } (_this$buckets$id$upda3 = (_this$buckets$id3 = this.buckets[id]).updater) === null || _this$buckets$id$upda3 === void 0 ? void 0 : _this$buckets$id$upda3.call(_this$buckets$id3, () => ({ ...this.buckets[id].portals })); } else { var _this$getOrCreateThro; // Use throttled update for smooth, regular updates (_this$getOrCreateThro = this.getOrCreateThrottledUpdater(id)) === null || _this$getOrCreateThro === void 0 ? void 0 : _this$getOrCreateThro(); } } registerPortal(key, portal, immediate = false) { var _this$portalToBucketM; this.createBucket(); // @ts-ignore - TS2538 TypeScript 5.9.2 upgrade this.buckets[this.getCurrentBucket()].capacity -= 1; const 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 () => { // @ts-ignore - TS2538 TypeScript 5.9.2 upgrade delete this.buckets[id].portals[key]; this.portalToBucketMap.delete(key); // @ts-ignore - TS2538 TypeScript 5.9.2 upgrade this.buckets[id].capacity += 1; // @ts-ignore - TS2538 TypeScript 5.9.2 upgrade if (this.buckets[id].capacity > this.scaleCapacityThreshold) { // @ts-ignore - TS2345 TypeScript 5.9.2 upgrade this.availableBuckets.add(id); } // @ts-ignore - TS2345 TypeScript 5.9.2 upgrade this.updateBuckets(id, immediate); }; } registerPortalRenderer(updater) { if (!this.portalRendererUpdater) { updater(() => this.buckets); } this.portalRendererUpdater = updater; } unregisterPortalRenderer() { this.portalRendererUpdater = null; } /** * Cleans up resources used by the PortalManager. This includes clearing all portals, * unregistering all buckets, and resetting internal state. */ destroy() { // Cancel all pending throttled updates this.throttledBucketUpdaters.forEach(updater => { updater.cancel(); }); this.throttledBucketUpdaters.clear(); // Iterate through each bucket and clear its portals and unset the updater function this.buckets.forEach((bucket, id) => { bucket.portals = {}; // Clearing all portals from the bucket bucket.updater = null; // Unsetting the bucket's updater function this.availableBuckets.add(id); // Mark all buckets as available }); this.portalToBucketMap.clear(); this.portalRendererUpdater = null; this.availableBuckets = new Set(this.buckets.map((_, index) => index)); } }