import { C as ComponentResolutionSource, S as SystemComponentEntry, h as KindComponentRow, f as ComponentRole, a as ComponentResolution, e as ComponentOverlayInput, K as KindComponentLayerRow, d as ComponentLayerScope } from './component-resolution.types-CNArz3ZR.js'; import { ContentIrErrorReporter } from './react.js'; export { c as ComponentLayerRef, J as JsonObject } from './component-resolution.types-CNArz3ZR.js'; /** * The Shape System component RESOLVER (rulings R1 + R6): (kind, platform, * role) → component, with two tiers: * * - eager: a COMPILED bootstrap the host ships in its bundle — the * trusted-at-boot floor, available at import so the render seam can gate * synchronously from the first streamed byte. * - warm: one `content_ir.kind_component` list fetch per app session * (`ensureWarm`). A DB row for a (kind, platform, role) OVERRIDES the * compiled entry once warm; the compiled floor keeps answering until then * (and forever, on DB failure). * - overlay (shape layers): the VIEWER's own `kind_component_layer` rows, * ABOVE the db tier — mine → my organization's (newest) → shared db → * compiled → generic. Installed per viewer by the host (`setOverlay`), * dropped on every auth / admin-lane change (`clearOverlay`). With no * overlay installed, resolution is exactly the two-tier behavior. * * Resolution is SYNCHRONOUS (the render seam calls it per block); the only * async work is the loading, off the render path. * * WHAT MOVED AND WHY. This class was matrx-frontend's `ComponentRegistry`. It * is a large part of why a second UI could not render a kind: every tier rule, * every repaint counter, every dedupe latch lived in one app. Nothing about it * is Next.js, Redux, or Supabase — the only host-specific parts were the two * loaders and the error sink, which are now constructor arguments. */ interface ComponentResolverOptions { /** * The compiled bootstrap, as a THUNK resolved on first use: a host's system * component table is often mid-initialization when this module evaluates * (matrx-frontend's registry cluster has a deliberate import cycle). */ compiledEntries?: () => SystemComponentEntry[]; /** The warm/refresh list load — every `kind_component` row the user may see. */ loadAll?: () => Promise; /** The eager single-kind load fired the moment a kind is identified mid-stream. */ loadForKind?: (kind: string, platform: string) => Promise; /** Where recovery screams land. Defaults to `console.error`, never silence. */ reportError?: ContentIrErrorReporter; /** Clock seam — the refresh rate limiter. Defaults to `Date.now`. */ now?: () => number; /** * THE BOUNDED WAIT: how long a demanded kind's lookup may stay unanswered before * {@link ComponentResolver.hasSettledFor} gives up on it (screaming once) so the route answers * with the generic floor instead of an endless loader. A late answer still repaints the kind. * Default {@link KIND_LOOKUP_BOUND_MS}. */ lookupBoundMs?: number; } /** Default for {@link ComponentResolverOptions.lookupBoundMs}: never an endless skeleton. */ declare const KIND_LOOKUP_BOUND_MS = 10000; /** One ranked overlay winner — the row plus which layer it came from. */ interface OverlayEntry { row: KindComponentLayerRow; scope: ComponentLayerScope; } /** * THE LAYER RANK for one (kind, platform, role), pure: the viewer's own row → * their organization's row (newest `updated_at` when several organizations * have one) → nothing (the shared tiers answer). A switched-off row * (`is_active=false`) is not a candidate at all — it FALLS THROUGH to the next * layer, unlike the shared tier where an inactive row is a known-held answer. * Ties break on id so the pick never depends on arrival order. */ declare function rankLayerRows(candidates: readonly OverlayEntry[]): OverlayEntry | null; declare class ComponentResolver implements ComponentResolutionSource { private compiled; private readonly db; /** * THE OVERLAY TIER (shape layers): the viewer's personal / organization * layered winners, ABOVE the db tier. Owned by exactly one viewer * (`overlayViewer`); null viewer = no overlay at all. Never filled by the * warm/refresh/cold loaders — only by {@link setOverlay}. */ private readonly overlay; private overlayViewer; /** Change signature of the installed overlay — an identical re-install is a no-op. */ private overlaySignature; /** The host's live "who is looking right now" — see {@link setOverlayViewerProbe}. */ private overlayViewerProbe; private warmPromise; /** Consecutive warm-load failures — indexes WARM_RETRY_DELAYS_MS. */ private warmFailures; private warmRetryTimer; /** * True once a list load has RUN TO COMPLETION (success or failure), or once * any rows have been ingested by any tier. Until then `resolve()` answering * null means "I have not looked yet" — never "no such component". See THE * COLD-VERDICT RULE on `ComponentResolutionSource.hasSettled`. */ private settled; /** * When the last successful wholesale refresh landed, or null for "never". * Explicitly nullable rather than 0: with a host-supplied clock that starts * near zero, a `0` sentinel makes the FIRST refresh look rate-limited and * silently skip. */ private lastRefreshAt; private refreshPromise; private readonly listeners; /** * Cold single-kind fetch dedupe (streaming eager path). In-flight is keyed * by (kind, platform) — the fetch unit; misses are keyed by (kind, platform, * role) so a miss on web/output never suppresses other roles, and CLEARED on * every wholesale refresh (a component created mid-session becomes eagerly * fetchable again — misses are cheap to re-verify). */ private readonly coldInFlight; private readonly coldMisses; /** Kinds whose demanded lookup outlived the bound (THE BOUNDED WAIT), and the pending bounds. */ private readonly lookupGaveUp; private readonly lookupBounds; /** Monotonic db-tier version — the repaint hook's snapshot key. */ private version; /** Per-kind versions + listeners (granular repaint) + wholesale epoch. */ private readonly kindVersions; private readonly kindListeners; private epoch; private readonly reportError; private readonly now; private readonly options; constructor(options?: ComponentResolverOptions); private compiledMap; /** * Synchronous resolve — the render seam's per-block call. DB override first * (once warm), compiled floor second, null for unknown. */ resolve(kind: string, platform: string, role: ComponentRole): ComponentResolution | null; /** * THE SHARED ANSWER — what EVERYONE gets: the db tier, then the compiled * floor, never the viewer's own layered view ("My view" / "My organization's * view"). Admin screens and fix-it surfaces that grade or compare the shared * component read this; reading `resolve` there mislabels a person's own view * as the shared one. `layer` is always null here. */ resolveShared(kind: string, platform: string, role: ComponentRole): ComponentResolution | null; private resolveFrom; /** * Install the viewer's overlay — REPLACES any previous overlay wholesale. * Every row is re-checked against the viewer: a user row whose audience is * not `viewerId`, or an organization row whose audience is not one of * `organizationIds`, is dropped (a host's read that returned too much — an * admin-lane request, a stale membership — can never render a stranger's * view). Always notifies every kind subscriber: the winner of any kind may * have changed. */ setOverlay(input: ComponentOverlayInput): void; /** * Let the host tell the resolver who is looking RIGHT NOW. `resolve` ignores * an installed overlay whose owner is not that viewer, so a user switch can * never render the previous person's view in the tick before the clear. */ setOverlayViewerProbe(probe: () => string | null): void; private overlayIsCurrent; /** * Drop the overlay. Hosts call this on EVERY auth change (sign-out, sign-in, * user switch) and admin-lane change, before loading the next viewer's rows: * an overlay must never outlive the viewer it was read for. */ clearOverlay(): void; /** Whose overlay is installed — null when none. */ getOverlayViewer(): string | null; /** Wholesale repaint: bump the epoch, call every per-kind listener, notify. */ private notifyWholesale; /** R6 floor check: compiled-bootstrap membership = always render-trusted. */ hasCompiled(kind: string, platform: string, role: ComponentRole): boolean; /** * Pure ingest — the warm landing point and the unit-test seam. First row per * key wins: rows arrive is_default-first / sort_order-asc from the source. */ ingestDbRows(rows: readonly KindComponentRow[]): void; /** * Mark the tier settled. THE COLD-VERDICT RULE held every block whose kind * the tier could not answer; settling changes that verdict for ALL of them, * so the flip bumps the epoch and calls every per-kind listener (as a * wholesale replace does). Before 0.13.0 a settle notified only kinds that * gained rows, and a waiting block for any other kind kept its pre-settle * rendering until something unrelated re-rendered it. */ private markSettled; private notifySettled; /** * Refresh landing point: REPLACE the db tier wholesale (same * first-row-per-key contract as {@link ingestDbRows}) so edits, deletions, * and is_active flips all take effect. Always notifies. */ replaceDbRows(rows: readonly KindComponentRow[]): void; getVersion(): number; getKindVersion(kind: string): number; subscribeKind(kind: string, listener: () => void): () => void; subscribe(listener: () => void): () => void; private bumpKind; private notifyChanged; /** * A warm metadata shell is resolved enough for routing, but not for the * host's db-component renderer. Its body must still come from the cold * single-kind loader. */ private needsColdFetch; /** * Cold rows are the authoritative heavy form of warm metadata shells. Keep * the same first-row-per-key winner contract, but replace an existing shell * so `componentSource` / `propsTransform` can land and repaint the kind. */ private ingestColdRows; /** * The eager lightweight single-kind fetch (streaming path): the moment a * cloud kind is identified mid-stream, pull ONLY that kind's resolver rows * and ingest them so {@link resolve} can answer before — or shortly after — * the region completes. Deduped in-flight and by known-miss. Fire-and-forget; * failures are loud (the warm list remains the backstop). */ requestComponent(kind: string, platform: string, role: ComponentRole): void; /** * Refresh-on-view: re-fetch the warm list and REPLACE the db tier, so an * edited `source='db'` component (its `updated_at` bump re-keys the host's * compile cache) renders fresh on the next view. Deduped in-flight and * rate-limited by `maxAgeMs` (default 10s) — mounting several previews costs * one fetch. Server-side edits do NOT push to open clients; the contract is * refresh-on-view via this call. */ refresh(maxAgeMs?: number): Promise; /** One list fetch per app session; failed loads retry on the next call. */ ensureWarm(): Promise; /** * THE BOUNDED WAIT. A demanded kind whose lookup has not answered within the bound (a hung * read, a refusal that never re-arms, a list that never lands) is settled by giving up — loudly, * once — so the reader gets the honest generic floor, never an endless loader. A late answer * still lands and repaints the kind with its real component. */ private boundKindLookup; /** @see ComponentResolutionSource.hasSettledFor */ hasSettledFor(kind: string, platform: string): boolean; /** @see ComponentResolutionSource.hasSettled */ hasSettled(): boolean; } /** * SHAPE LAYERS — the viewer's overlay LIFECYCLE, shared by every signed-in * resolver host (matrx-frontend, the dashboard, the workflow studio). * * `content_ir.kind_component_layer` holds "My view" (`audience_user_id`) and * "My organization's view" (`audience_organization_id`) components for a kind * slug. A host reads THE VIEWER's rows and hands them to * {@link ComponentResolver.setOverlay}; this module owns everything around that * read so there is ONE implementation of it: * * - THE AUDIENCE PREDICATE ({@link layerAudienceFilter}) — the read declares * its own scope (`audience_user_id = me OR audience_organization_id IN my * orgs`). RLS is the ceiling, never the view: the table carries the * mandatory platform-admin read arm, so an unscoped select on an admin-lane * page would hand an admin EVERY person's rows. The resolver re-checks every * row against the viewer as well. * - THE LIFECYCLE ({@link KindComponentOverlayLoader}) — signed out → no read, * no overlay; any change of user / lane / membership clears the overlay * before the next viewer's rows load; a read that lands after the viewer * moved is DISCARDED; a failed read is a BOUNDED outage (finite timer-free * backoff, ONE error per outage) — never a request and error storm; a * forced sync during an in-flight read coalesces into one follow-up read. * * The host injects only its two reads (organizations, one filtered chunk of * layer rows) and its viewer snapshot. Pure TS — no React, no Supabase. * Plain fields, no parameter properties: consumers compile this source * directly and a strict host (the dashboard) sets `erasableSyntaxOnly`. */ /** The resolver surface the loader drives (`ComponentResolver` satisfies it). */ interface OverlayTarget { setOverlay(input: ComponentOverlayInput): void; clearOverlay(): void; /** * Optional: let the resolver refuse an installed overlay whose owner is no * longer the current viewer (closes the window between a user switch and the * next-tick clear). */ setOverlayViewerProbe?(probe: () => string | null): void; } /** ~100 uuids per GET url — a person's memberships can run to hundreds. */ declare const OVERLAY_ORGANIZATION_CHUNK = 100; /** Timer-free backoff between retries of a FAILED read, then only `force` retries. */ declare const OVERLAY_RETRY_DELAYS_MS: readonly number[]; declare const LAYER_COLUMNS = "id, kind, platform, role, component_key, source, is_active, config, component_source, props_transform, pinned_kind_version, updated_at, created_by, audience_user_id, audience_organization_id"; /** A `kind_component_layer` row as the database returns it (LAYER_COLUMNS). */ interface LayerDbRow { id: string; kind: string; platform: string; role: string; component_key: string; source: string; is_active: boolean; config: unknown; component_source: string | null; props_transform: string | null; pinned_kind_version: number | null; updated_at: string; created_by: string | null; audience_user_id: string | null; audience_organization_id: string | null; } /** * THE AUDIENCE PREDICATE — the PostgREST `or` filter for one chunk of the * viewer's organizations. Pure, so a guard test can pin it: it always names the * viewer, and only ever the viewer's own organizations. */ declare function layerAudienceFilter(userId: string, organizationIds: readonly string[]): string; /** Project a database row onto the resolver's layer row; null for an unroutable role. */ declare function layerDbRowToLayerRow(row: LayerDbRow): KindComponentLayerRow | null; /** * ONE scoped read of the viewer's layered rows (chunked only by url length). * The host supplies `readChunk(filter)`: select {@link LAYER_COLUMNS} from * `content_ir.kind_component_layer` where `deleted_at is null` AND the given * `or` filter, read to completion (never a bare capped select). The PREDICATE * is built here, once, for every host. */ declare function readViewerLayerRows(userId: string, organizationIds: readonly string[], readChunk: (orFilter: string) => Promise): Promise; /** Who is looking, and through which lane — the overlay's identity. */ interface OverlayViewer { userId: string | null; laneOpen: boolean; /** * The host's current membership state as a stable string (e.g. the sorted * organization ids). A change re-reads — a view from an organization the * person has left must stop rendering without a reload. */ membershipKey?: string; } interface OverlayLoaderPorts { /** The current viewer (synchronous snapshot, read fresh every call). */ viewer: () => OverlayViewer; readOrganizationIds: () => Promise; readRows: (userId: string, organizationIds: readonly string[]) => Promise; reportError: (message: string, error: unknown) => void; /** Test seam. */ now?: () => number; /** Test seam / host override of {@link OVERLAY_RETRY_DELAYS_MS}. */ retryDelaysMs?: readonly number[]; } /** * The lifecycle, host-free so it is testable. `sync()` is cheap when the viewer * is unchanged; any change of user, lane or membership clears the overlay first * and then loads the new viewer's rows. */ declare class KindComponentOverlayLoader { private readonly target; private readonly ports; private loadedKey; private inFlightKey; private inFlight; private rereadAfter; /** True once the in-flight read has issued its rows query (a forced ask after that needs a follow-up). */ private rowsIssued; private generation; private outage; constructor(target: OverlayTarget, ports: OverlayLoaderPorts); private static keyOf; /** Re-read when the viewer, lane or membership changed (or `force`); no-op otherwise. */ sync(force?: boolean): Promise; private now; /** Test seam / sign-out hard reset. */ reset(): void; } /** * THE VIEWER WATCH for hosts with a push-style auth source (Supabase * `onAuthStateChange`): the signed-in user id, kept current by ONE lazy * subscription. `connect(report)` wires the host's auth source and calls * `report(userId | null)` on every change (and once on subscribe); the watch * defers `onChange` to the next tick — never call back into Supabase inside its * auth callback (it holds its auth lock there). Signed out / not yet told = null. */ interface OverlayViewerWatch { current(): string | null; /** Idempotent; a no-op outside a browser. */ start(onChange: () => void): void; } declare function createOverlayViewerWatch(connect: (report: (userId: string | null) => void) => void | Promise, options?: { isBrowser?: () => boolean; }): OverlayViewerWatch; export { ComponentLayerScope, ComponentOverlayInput, ComponentResolution, ComponentResolutionSource, ComponentResolver, ComponentRole, KIND_LOOKUP_BOUND_MS, KindComponentLayerRow, KindComponentOverlayLoader, KindComponentRow, LAYER_COLUMNS, OVERLAY_ORGANIZATION_CHUNK, OVERLAY_RETRY_DELAYS_MS, SystemComponentEntry, createOverlayViewerWatch, layerAudienceFilter, layerDbRowToLayerRow, rankLayerRows, readViewerLayerRows }; export type { ComponentResolverOptions, LayerDbRow, OverlayEntry, OverlayLoaderPorts, OverlayTarget, OverlayViewer, OverlayViewerWatch };