@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
171 lines (168 loc) • 5.45 kB
JavaScript
/**
* 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();
}
};
}