/**
 * 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 {};