UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

439 lines 19.7 kB
/** * Generic same-tab preference coordinator. Its primary purpose is * to **fold many concurrent value changes into a single layout-shift * commit**: sibling component instances that share a `channelKey` * coordinate so the visible flip happens together rather than as a * cascade of independent re-layouts. * * Each peer self-classifies a given target value via * `causesLayoutShift` (consulted for non-originator peers only; * originators always take the barrier path so a user click always * feels coordinated): * * - **`causesLayoutShift(target) === true`** — the peer joins a * channel-wide barrier. Every joining peer's `preload` runs * serially across the channel (so no main-thread contention * while we prepare the swap), and all `onCommit`s fire together * in a single microtask once everyone is ready. Use for changes * that visibly resize content (collapse/expand, code transforms, * image swaps with different aspect ratios). * - **`causesLayoutShift(target) === false`** — the peer runs its * `preload`+`commit` on its own self-serial chain. Multiple * peers' lazy chains run concurrently with each other and with * any in-flight barrier. Use for changes that are visually * non-disruptive (e.g. updating a value that only shows on hover). * * Different peers may classify the same target differently — each * peer's classification governs only that peer's path through the * coordinator. * * **Cross-tab behavior is intentionally out of scope.** Tabs sync via * the underlying state primitive (`useLocalStorageState` etc.); this * coordinator only handles peers in the same JS context. A receiving * tab independently runs its own barrier across its local peers, * which is sequenced naturally after the originator's commit because * the originator defers the underlying `setValue` write until its * own barrier commits (see `useCoordinated`). * * No React, no DOM, no BroadcastChannel — pure module-scoped state * suitable for any state primitive. * * **Browser-only.** All state (channels, barriers, lazy queues) is * held in module scope and would persist across requests if this * module were ever evaluated in a long-lived server-side runtime. * The consuming surface is the `useCoordinated` React hook, whose * `registerPeer`/`announceTarget` calls are gated behind * `useLayoutEffect`/event handlers and therefore never reached * during SSR. Do not import this module from server-side code * paths that fan out per request. */ /** Identifier assigned to each peer at registration time. */ export type PeerId = string; /** Channel scope. Peers sharing a `channelKey` coordinate with each other. */ export type ChannelKey = string; /** * Sentinel returned by the coordinator's `announceTarget` when no * commit is needed for this peer (e.g. its current value already * matches the target). Always-defined to keep the API ergonomic. */ export interface AnnounceHandle { /** * Abort a pending announcement (e.g. before the preload has run, or * before the barrier has resolved). After `cancel()` no `onCommit` * fires for this peer. */ cancel(): void; /** * Resolves when this peer's `onCommit` has fired or the announcement * has been cancelled. Useful for tests and for callers that want to * `await` a settled coordination. */ settled: Promise<void>; } export interface AnnounceOptions<TValue, TPreload> { /** * Per-target classifier consulted for **non-originator peers only**. * Return `true` when applying this target would visibly shift * layout — the peer will join the channel-wide barrier so all * such peers commit together. Return `false` for non-disruptive * changes — the peer will commit lazily on its own self-serial * chain. Originators (the peer the user is directly interacting * with) always take the barrier path regardless of this * classifier, so a click always feels coordinated with whichever * peers join. Different peers may classify the same target * differently. */ causesLayoutShift: (target: TValue) => boolean; /** * Per-peer preload work. The returned value is handed back to * `onCommit`. Receives an `AbortSignal` that fires if the * announcement is cancelled (e.g. superseded by a newer target). * Barrier peers run their `preload` in serial across the channel; * lazy peers run their own self-serial chain. May be omitted for * pure value-flip use cases. */ preload?: (target: TValue, signal: AbortSignal) => TPreload | Promise<TPreload>; /** * Fired when this peer's slice of the coordination has settled. * For the barrier path this is inside the batched commit (all * barrier peers' `onCommit`s run in the same microtask). For the * lazy path this is immediately after this peer's own preload * completes. * * `preloaded` may be `undefined` even when `preload` was provided: * - the barrier force-resolved at `ultimateTimeoutMs` before this * peer's slow preload settled, * - the preload threw (logged via `console.error`, treated as a * no-op so the rest of the channel still commits), or * - the preload returned `undefined`/no value. * * A superseded announce does not call this `onCommit` — the * superseding announce takes over and only its `onCommit` runs. * * Callers must tolerate `preloaded === undefined` and fall back to * a synchronous render path (or skip the side effect entirely) * rather than throwing. */ onCommit: (target: TValue, preloaded: TPreload | undefined) => void; /** * Minimum wall-clock time before commit, measured from * `announceTime`. On the barrier path: the barrier stays open at * least this long so consumers can play an exit animation on the * outgoing state. On the lazy path: the per-peer self-serial chain * waits at least this long after `announceTime` (anchored, not * relative to when preload resolves) so a non-layout-shift peer * can land its swap on the same wall-clock window as a sibling * barrier instead of cascading. Default 0. */ minWaitMs?: number; /** * Additional wait applied to `minWaitMs` on the barrier path when * more than one peer is registered on the channel at the time the * barrier opens. Lets callers express "no extra delay when this * demo is alone, but give late siblings a frame to join when they * exist" without leaking solo-peer churn through a baseline * `minWaitMs`. Default 0. */ multiPeerExtraMinWaitMs?: number; /** * Overrides `minWaitMs` for the lazy path only. Useful when * non-layout-shift peers should land their swap *after* the * sibling barrier has finished its expand-swap-collapse window * (e.g. `2 * minWaitMs`) so the page settles in one paint instead * of cascading. Falls back to `minWaitMs` when unset. */ lazyMinWaitMs?: number; /** * Lazy-path opt-in: when a same-target barrier is pending at the * moment this peer announces, run its `preload` concurrently with * the barrier's preloads instead of waiting for the barrier to * commit. Use only for I/O-bound preloads that don't tax the main * thread (e.g. fetching a JSON payload) — main-thread-heavy * preloads (parsing, highlighting, layout measurement) should * leave this `false` so the barrier's layout-shifting peers get * uncontended CPU time to settle their swap. * * The lazy peer's `onCommit` still waits until the render after * the barrier commits, regardless of this flag — the visible flip * never lands before the layout-shifting siblings have painted. * * No effect when no barrier exists at announce time (the lazy * pipeline runs immediately on its own clock). * * Default `false`. */ preloadAll?: boolean; /** * Lazy-path only: scheduling priority for the per-peer commit. * * - `'idle'` (default) — the commit is scheduled via * `requestIdleCallback` so the browser can yield to * higher-priority work (input, in-flight barrier paints) * before the swap lands. Useful when the lazy peer's commit * itself is main-thread heavy (DOM reconciliation of a * freshly transformed tree, etc.). * - `'normal'` — the commit lands as soon as the preload * resolves, without an idle defer. Use this for I/O-bound * preloads where the commit is cheap and you want each peer's * swap to surface immediately; otherwise idle scheduling can * cluster commits together near the slowest peer's settle, * defeating the visible "cascade" the lazy path is meant to * provide. * * Has no effect on the barrier path — barrier commits are batched * synchronously inside the barrier's resolve microtask regardless. * * `'idle'` falls back to a synchronous commit if * `requestIdleCallback` isn't available on `globalThis`. */ lazyCommitPriority?: 'idle' | 'normal'; /** * Time past `minWaitMs` after which `onWaitingForPeers` fires if * the barrier still hasn't resolved. The barrier itself keeps * waiting up to `ultimateTimeoutMs`. Default 300ms. */ gracePeriodMs?: number; /** * Absolute ceiling past announcement after which the barrier * force-resolves and logs a warning, regardless of outstanding * peers. Default 10s. */ ultimateTimeoutMs?: number; /** * Called once when `gracePeriodMs` elapses with the barrier still * unresolved. Only meaningful for originators (the peer that the * user is directly interacting with) so they can surface a * "waiting for peers" indicator. */ onWaitingForPeers?: () => void; /** * Whether this peer originated the change (user click) versus * received it from elsewhere (storage event from another tab). Only * affects which peer's `onWaitingForPeers` may fire and which peer * opens the channel-wide barrier. */ isOriginator: boolean; /** Wall-clock anchor (`Date.now()`) for barrier timers. */ announceTime: number; } /** * Fired on a registered peer when *another* peer in the same channel * calls `announceTarget`. Lets a peer learn about a sibling-driven * change without having to wait for the underlying state primitive * (e.g. `useLocalStorageState`) to echo the new value back — that * echo only happens after the originator commits, which itself is * gated on every sibling joining the barrier. Without this hook * sibling peers would deadlock the barrier until * `ultimateTimeoutMs` for any same-tab coordination where the * underlying primitive only notifies after the originator's write. * * Implementations should typically call into their local equivalent * of `runCoordination(target, isOriginator=false)` so the peer * joins the active barrier (or kicks off its own lazy chain on the * same wall-clock window). Implementations must be idempotent for * repeated calls with the same `target` because notifications can * fan out from each subsequent join. */ export type OnSiblingAnnounce<TValue> = (target: TValue) => void; interface RegisteredPeer<TValue> { id: PeerId; /** * Last value this peer reported via `reportValue`. Used to skip * peers that are already at the target when classifying barrier * expectations. Initialized lazily on first `reportValue`. */ currentValue: { has: false; } | { has: true; value: TValue; }; /** * Optional notifier invoked when *another* peer on the channel * announces a target. See {@link OnSiblingAnnounce}. */ onSiblingAnnounce?: OnSiblingAnnounce<TValue>; /** * In-flight lazy-path work, keyed by the AbortController used to * cancel it. The value is the target each entry is committing to, * so barrier creation can selectively skip a peer only when its * pending lazy work matches the new barrier's target. * Cancelled when the peer is unregistered or a new lazy-path * announcement supersedes a still-queued one. */ lazyInFlight: Map<AbortController, TValue>; /** * Per-peer serialization queue for lazy-path announcements. Each * entry is a starter callback that kicks off its preload + commit * pipeline. We drain via callback chaining (not Promise.then) so * the entire pipeline stays on macrotasks — important for tests * driving fake timers without microtask drains. */ lazyQueue: Array<() => void>; lazyActive: boolean; } interface BarrierWaiter<TValue, TPreload> { peerId: PeerId; isOriginator: boolean; preloaded: { has: false; } | { has: true; value: TPreload | undefined; }; onCommit: (target: TValue, preloaded: TPreload | undefined) => void; onWaitingForPeers?: () => void; /** Resolves the waiter's `settled` promise. */ settle: () => void; /** Cancel handle for the waiter's enqueued preload work. */ abort: AbortController; } interface PendingBarrier<TValue, TPreload> { target: TValue; /** * Wall-clock anchor (`Date.now()`) recorded when the barrier was * opened. Used by {@link getBarrierAnnounceTime} so late-joining * peers can align their local timers to the originator's window * instead of restarting a fresh one. */ announceTime: number; /** * Performance mark name recorded when the barrier was opened. * Used as the start mark when measuring the barrier's resolution * duration so a single {@link PerformanceObserver} entry captures * the full open → resolve window for the channel. */ openMark?: string; /** * Peers expected to participate. Set when the barrier opens and on * each waiter join (peers register themselves as they classify the * target as 'high'). The barrier resolves when every expected peer * has its `preloaded` field populated AND the minimum wait has * elapsed. */ waiters: Map<PeerId, BarrierWaiter<TValue, TPreload>>; /** * Peers that explicitly opted out of this barrier by taking the * lazy path for the same `target`. The barrier may resolve once * `waiters.size + skipped.size >= channel.peers.size`. */ skipped: Set<PeerId>; /** `true` once the minimum-wait timer has fired. */ minWaitPassed: boolean; minWaitTimer: ReturnType<typeof setTimeout>; waitingForPeersTimer: ReturnType<typeof setTimeout>; waitingForPeersNotified: boolean; ultimateTimer: ReturnType<typeof setTimeout>; ultimateTimeoutMs: number; /** * Callbacks queued by lazy peers that announced the same target * while this barrier was pending. Fired one macrotask after every * waiter's `onCommit` runs, so the lazy peers' commits land in * the render *after* the barrier's batched commit — keeping the * main thread clear while the layout-shifting siblings paint. */ deferredLazyReleases: Array<() => void>; } interface Channel<TValue> { channelKey: ChannelKey; peers: Map<PeerId, RegisteredPeer<TValue>>; /** * `true` once any peer has called `announceTarget` on this channel * since the channel was created. Surfaced by * {@link hasEverAnnounced} so callers can distinguish "first * paint, nobody has interacted" from "someone interacted then * everyone settled". Never reset — a channel that's been * announced on and then emptied will still report `true` until * its last peer unregisters and the channel disposes. */ hasEverAnnounced: boolean; /** * Channel-wide serial queue for barrier-path preloads. Each barrier-path * announcement appends its preload work; only one preload runs at a * time across all this-tab barrier-path peers, preventing main-thread * contention when many sibling demos all need to precompute. */ barrierTail: Promise<unknown>; /** * At most one pending barrier-path barrier per encoded target value. * Keyed by the result of `encodeTarget` so any hashable target * works. */ pendingBarriers: Map<string, PendingBarrier<TValue, unknown>>; } /** * Set a custom target encoder. Returns a function that restores the * previous encoder. Intended for tests; consumed via * `coordinatePreference.testUtils`. */ declare function setTargetEncoder(impl: (value: unknown) => string): () => void; /** * Register a peer with a channel. Returns an `unregister` function * that removes the peer; calling it cancels any in-flight lazy-path * work owned by the peer and drops it from any open barrier-path barriers. * * Pass `onSiblingAnnounce` to learn about target announcements made * by other peers on the channel — this is what lets a peer join the * originator's barrier window without waiting for the underlying * state primitive to echo the new value (which only happens after * the originator commits, creating a deadlock when every peer is * waiting on it). */ export declare function registerPeer<TValue>(channelKey: ChannelKey, peerId: PeerId, onSiblingAnnounce?: OnSiblingAnnounce<TValue>): () => void; /** * Report a peer's current value to the coordinator. Used to exclude * already-at-target peers from barrier expectations. */ export declare function reportValue<TValue>(channelKey: ChannelKey, peerId: PeerId, currentValue: TValue): void; /** * Announce a target value for this peer. Routes into the barrier or * lazy path based on `causesLayoutShift(target)`. * * For the barrier path: the peer joins the channel-wide barrier for * this target (creating it if needed), enqueues its `preload` into * the channel's serial queue, and awaits the barrier's batched * commit. * * For the lazy path: the peer enqueues `preload` + `onCommit` onto * its own self-serial chain and returns immediately. Multiple peers' * lazy chains run concurrently with each other and with any * in-flight barrier work. */ export declare function announceTarget<TValue, TPreload>(channelKey: ChannelKey, peerId: PeerId, target: TValue, options: AnnounceOptions<TValue, TPreload>): AnnounceHandle; /** * Returns `true` if any peer has ever called `announceTarget` on this * channel since the channel was created (i.e., since the first peer * registered without an existing channel object). Useful for * first-render reconciliation: a peer that wakes up post-hydration * and finds the channel "fresh" (no announcements yet) can safely * fast-forward its committed value to the latest underlying value * without going through a barrier, because no peer is mid-animation. * * Returns `false` when the channel doesn't exist (no peers have * registered yet) or exists but hasn't seen an announce. */ export declare function hasEverAnnounced(channelKey: ChannelKey): boolean; /** * Returns the `announceTime` recorded when the active barrier for * `target` was opened, or `null` if no barrier is currently pending * for that target on `channelKey`. Late-joining peers can use this * to anchor their local timers to the originator's wall-clock window * instead of restarting a fresh one \u2014 e.g. a peer whose state * propagated 200ms after the originator's click should commit 200ms * earlier than its local `Date.now()` would suggest, so the visible * paint lines up. */ export declare function getBarrierAnnounceTime<TValue>(channelKey: ChannelKey, target: TValue): number | null; /** * Internal handles for the `coordinatePreference.testUtils` sibling. * * Not part of the public API. Do not import this from production * code or from tests directly — use the helpers re-exported from * `./coordinatePreference.testUtils` instead so that the boundary * between runtime API and test affordances stays clear. */ export declare const __testInternals: { channels: Map<string, Channel<unknown>>; setTargetEncoder: typeof setTargetEncoder; }; export {};