UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

171 lines (168 loc) 5.45 kB
/** * Fallback ceiling for {@link createSettleGate}: if a registered source never * settles (its swap errored or hung), the gate opens anyway after this many * milliseconds so coordination can never be blocked forever. Matches the * coordinator's default `ultimateTimeoutMs` and the value the module-global * `layoutShiftGate` shipped with. */ export const SETTLE_SAFETY_TIMEOUT_MS = 10_000; /** * A reusable "all sources settled" gate. * * The module-global page-wide layout-shift gate (`layoutShiftGate`) is one * instance; each `StreamController` and `CoordinatedLazy` swap registers with * one too. The behavior is the original layout-shift gate's, plus two opt-in * completion signals (`expect` / `markLast`) for sources that arrive over time * (chunks streaming in across ticks) rather than all within the initial * hydration commit. * * Lifecycle: a source `register()`s and later calls the returned settle * function when it reaches its stable state. The gate opens once every * registered source has settled (and any completion constraint is met). It * opens **once** and never re-closes - a source that registers after the gate * has opened adopts the open state rather than re-closing it for everyone * ("all sources" means "all present by the initial settle"). */ /** * Options for {@link createSettleGate}. */ /** * Create an independent "all sources settled" gate. See {@link SettleGate} for * the contract. * * Isomorphic - it touches only `setTimeout` and the injectable `scheduleCheck` * (default `queueMicrotask`), so it runs in tests and during SSR without the * DOM. Client-only consumers are responsible for never registering during SSR * (the page-wide layout-shift gate is only ever touched on the client for this * reason). */ export function createSettleGate(options = {}) { const safetyTimeoutMs = options.safetyTimeoutMs ?? SETTLE_SAFETY_TIMEOUT_MS; const scheduleCheck = options.scheduleCheck ?? (typeof queueMicrotask === 'function' ? queueMicrotask : callback => { Promise.resolve().then(callback); }); let pendingCount = 0; let registeredCount = 0; // `true` once at least one source has registered - until then there is // nothing to wait for and the gate reports settled. let armed = false; let settled = false; let checkScheduled = false; // Completion constraints layered on top of "every source settled": // `expectedCount` holds the gate until that many sources have registered // (known-count); `sawLast` is the standalone terminal that opens on the next // pending-zero regardless of the count (last-chunk). let expectedCount = null; let sawLast = false; let safetyTimer = null; const settleListeners = new Set(); function isComplete() { if (sawLast) { return true; } if (expectedCount !== null) { return registeredCount >= expectedCount; } return true; } function openGate() { if (settled) { return; } settled = true; if (safetyTimer !== null) { clearTimeout(safetyTimer); safetyTimer = null; } const listeners = Array.from(settleListeners); settleListeners.clear(); for (const listener of listeners) { listener(); } } function scheduleSettleCheck() { if (settled || checkScheduled) { return; } checkScheduled = true; // Defer past the current tick so a burst of same-tick registrations - or an // early settle that precedes a sibling's registration - all land before we // declare the gate settled. scheduleCheck(() => { checkScheduled = false; if (!settled && armed && pendingCount === 0 && isComplete()) { openGate(); } }); } return { register() { if (settled) { return () => {}; } armed = true; pendingCount += 1; registeredCount += 1; if (safetyTimer === null && typeof setTimeout === 'function') { safetyTimer = setTimeout(openGate, safetyTimeoutMs); } let done = false; return () => { if (done) { return; } done = true; pendingCount -= 1; scheduleSettleCheck(); }; }, isSettled() { return settled || !armed; }, whenSettled(signal) { if (settled || !armed) { return null; } return new Promise((resolve, reject) => { const onSettle = () => { signal?.removeEventListener('abort', onAbort); resolve(); }; function onAbort() { settleListeners.delete(onSettle); reject(new DOMException('Settle gate wait aborted', 'AbortError')); } if (signal) { if (signal.aborted) { reject(new DOMException('Settle gate wait aborted', 'AbortError')); return; } signal.addEventListener('abort', onAbort); } settleListeners.add(onSettle); }); }, expect(count) { expectedCount = count; scheduleSettleCheck(); }, markLast() { sawLast = true; scheduleSettleCheck(); }, reset() { pendingCount = 0; registeredCount = 0; armed = false; settled = false; checkScheduled = false; expectedCount = null; sawLast = false; if (safetyTimer !== null) { clearTimeout(safetyTimer); safetyTimer = null; } settleListeners.clear(); } }; }