/** * The store behind a computed display value. * * A record's title is asked for far more often than it changes, and by many * components at once: a list of fifty rows, each row's relation chips, the * breadcrumb above them. Resolving per component is what makes an async display * value a bad idea — fifty rows becomes fifty reads, then fifty more on the next * render. * * So resolution is keyed by record *and* role, in-flight calls are shared, and * results are kept until something says otherwise. Deliberately not a React * thing: the same store answers an imperative caller (an export, a breadcrumb * built outside the tree), and it is testable without a renderer. */ import type { EntityDisplayRole } from "@rebasepro/cms-types"; export type EntityDisplayKey = string; /** The identity of one role of one record, as a cache key. */ export declare function entityDisplayKey(path: string, entityId: string | number | undefined, role: EntityDisplayRole): EntityDisplayKey; export declare class EntityDisplayCache { private readonly entries; private readonly listeners; /** * The resolved value, or `undefined` when this pair has not been resolved * yet. `null` is a resolved absence, and the two must stay distinct: a * caller that reads "not yet" as "nothing" flickers its fallback in on every * mount. */ peek(key: EntityDisplayKey): unknown | undefined; /** True while a resolution for this pair is in flight. */ isLoading(key: EntityDisplayKey): boolean; /** * Resolve once per record and role. Concurrent callers share the first * call's promise; later callers get the cached value with no promise at all. * * A resolver that throws is recorded as "nothing" rather than retried: the * alternative is every render re-running a call that just failed. And it is * reported here, which is a correction. * * It used to say "the caller that saw the rejection is the one that logs * it", and `useEntityDisplay` duly attached a `.catch()` that warned. But * both failure paths below swallow and return a *resolved* promise, so that * catch could never run — the two halves each did the reasonable thing and * between them the log was unreachable. A resolver that blew up produced a * blank chip and total silence, which is the failure mode * `EntityDisplayResolver`'s own contract ("treated as `undefined` and logged * once") exists to rule out. * * Reporting belongs here for the reason the caller could not do it: this is * the one place that runs exactly once per key, so "once" is a property of * the code rather than a hope about how many components mount. */ resolve(key: EntityDisplayKey, resolver: () => unknown): Promise; /** * Drop what is known about a record, so the next ask resolves again. Called * after a write: the row that just saved may be called something else now. */ invalidate(path: string, entityId?: string | number): void; /** Drop everything. The user signed out, or the app swapped datasource. */ clear(): void; subscribe(listener: () => void): () => void; private set; /** * Record a failed resolution as "nothing", and say so once. * * The key is the message: it is ` `, which is exactly what a * reader needs to find the resolver that blew up. A warning with no key would * tell them a display resolver failed somewhere in a list of fifty rows. * * `console.warn` rather than a thrown error, because this runs while a row is * rendering: the contract is that a title which cannot be fetched must not * take down the row that shows it. */ private fail; private emit; }