import type { AppCaches, Logger, OperationalMetrics } from '@cat-factory/kernel'; import type { AbstractNotificationConsumer, BackgroundWorkScheduler, GroupNotificationPublisher, InMemoryGroupCache } from 'layered-loader/core'; import type { CacheGenerationStore } from './generationCoherency.js'; /** Per-cache tuning knobs; a facade passes a profile so TTLs can differ per runtime. */ export interface GroupCacheProfile { /** * `false` ⇒ pass-through: no in-memory tier is built and every read runs its * load. The Worker's isolate-safe stance for caches of MUTABLE cross-instance * state — an isolate has no cross-isolate invalidation bus, so a TTL'd cache * there would serve stale data after a write on another isolate. */ enabled: boolean; /** Entry freshness backstop; invalidation, not the TTL, is the coherence story. */ ttlInMsecs: number; /** LRU bound on distinct groups (workspaces, typically). */ maxGroups: number; /** LRU bound on entries within one group. */ maxItemsPerGroup: number; /** * Preemptive-refresh window for git-backed caches (layered-loader ≥ 14.5.3 * supports it in-memory-only): an entry hit with less than this much TTL left * refreshes in the background — via the caller's cheap `isStillCurrent` probe * (TTL bump when the source hasn't moved) when one is passed to `get`, else a * full background reload. Unset ⇒ entries simply expire at `ttlInMsecs` * (correct for the invalidation-driven DB-backed caches, where a probe would * cost as much as the load). */ ttlLeftBeforeRefreshInMsecs?: number; /** * Pull-coherency probe cadence, for an enabled cache of our own mutable state on a runtime * with no push bus (the Worker): a read whose group snapshot in the injected * {@link CacheGenerationStore} is older than this re-reads the directory before serving, * applying the group invalidation locally on a moved counter. Bounds cross-isolate staleness * at roughly this window. Requires `generationStore`: {@link createAppCaches} REFUSES a * profile that sets it on an enabled cache without one, because an enabled TTL'd cache of * mutable state with no coherency mechanism is the exact bug the isolate-safe profile * exists to prevent. */ coherencyWindowMsecs?: number; /** * Declares that this cache's owning service calls {@link GroupCacheHandle.invalidateAll}. * * Only meaningful together with `coherencyWindowMsecs`, and it costs something real: a * cache-wide invalidation rides the reserved epoch counter, whose shard is ONE globally * placed Durable Object, so declaring this puts a cross-colo probe on the cache's read path. * A cache with no `invalidateAll` call site leaves it off and probes only its own group. * * Getting it wrong cannot go quiet: `invalidateAll` on a coherent cache that did NOT declare * it THROWS, because dropping the entries locally while peers keep serving them for a full * TTL is the failure this flag exists to prevent. */ cacheWideInvalidation?: boolean; } /** One profile entry per named cache in the kernel {@link AppCaches} bag. */ export interface AppCachesProfile { fragmentCatalog: GroupCacheProfile; skillCatalog: GroupCacheProfile; foundationalServiceCatalog: GroupCacheProfile; fragmentDocumentBody: GroupCacheProfile; linkedDocumentVersion: GroupCacheProfile; repoProjection: GroupCacheProfile; repoFiles: GroupCacheProfile; accountModelPolicy: GroupCacheProfile; accountSettings: GroupCacheProfile; workspaceSettings: GroupCacheProfile; accountBudgetLimit: GroupCacheProfile; userBudgetLimit: GroupCacheProfile; viewerRepos: GroupCacheProfile; patInstallationRepos: GroupCacheProfile; riskPolicy: GroupCacheProfile; modelPreset: GroupCacheProfile; localModelDeclarations: GroupCacheProfile; workspaceAccess: GroupCacheProfile; userSessionGeneration: GroupCacheProfile; ssoDiscovery: GroupCacheProfile; } /** The default (Node/local/test) profile: caching on, modest bounds. */ export declare const DEFAULT_APP_CACHES_PROFILE: AppCachesProfile; /** * The Cloudflare Worker profile: every cache of mutable cross-instance state is * pass-through, because a Worker isolate has no cross-isolate invalidation bus * (and no Redis) — see the package README. Caches of immutable or self-verifying * entries (sha-pinned reads, static catalogs) may enable real TTLs here. * * `fragmentDocumentBody` stays ENABLED here: its entries are external page content * re-validated by a cheap version probe, so a peer isolate's cached body self-heals * within the refresh window without an invalidation bus (the same reasoning that * lets sha-pinned reads keep a TTL on the Worker) — its staleness is bounded by the * probe, not indefinite. Only `fragmentCatalog`, which mirrors our own mutable D1 * state, must pass through. * * Every ENABLED entry below is read against ONE FACT that the numbers on the Node profile were * not chosen for: the Worker's bag is one per ISOLATE (`appCachesHost.ts`), so an entry lives * its whole TTL across requests. It used to be rebuilt per invocation, which quietly capped * every one of these at the length of a single wake. The two probe-backed slices therefore * widen their refresh window to cover the full TTL here (see each entry); the two that have no * probe keep their TTL as the entire bound, which their own numbers were already sized as: * `linkedDocumentVersion` at 60s, and `ssoDiscovery` at 15 minutes of EXTERNAL state that * additionally self-heals on an unknown `kid`. */ export declare const ISOLATE_SAFE_APP_CACHES_PROFILE: AppCachesProfile; /** * The isolate-safe profile plus the caches the generation directory makes coherent: selected * by the Worker ONLY when its `CACHE_GENERATIONS` Durable Object binding exists (and so a * {@link CacheGenerationStore} is injected); with no binding the Worker keeps the pass-through * stance above. Flipping a cache here means giving it a real TTL on the Worker with a * generation probe bounding its cross-isolate staleness at `coherencyWindowMsecs`; the cache's * EVERY invalidation site then also bumps the directory (the handle does both together). * * `workspaceSettings` is the pilot: exactly one invalidation site * (`WorkspaceSettingsService.update`), no `invalidateAll`, and hot on the Worker (read per * recorded LLM call, per task-limit guard, per pricing resolution, each a live D1 read * today). Further flips are one profile row each, in their own slice * (docs/initiatives/caching-layer.md). */ export declare const ISOLATE_COHERENT_APP_CACHES_PROFILE: AppCachesProfile; /** * A per-cache invalidation-notification pair (layered-loader's group publisher + * consumer). Produced by the facade's factory — Redis-backed in a multi-node Node * deployment, a fake sharing an in-memory bus in tests. */ export interface GroupCacheNotifications { publisher: GroupNotificationPublisher; consumer: AbstractNotificationConsumer>; } /** * Builds the notification pair for one named cache (each cache gets its own * channel, `:`). Returning `undefined` leaves that cache bare * in-memory. The factory is per-CACHE so a facade can wire dedicated clients per * channel — layered-loader closes a pair's clients with its loader. */ export type GroupNotificationPairFactory = (cacheName: string) => GroupCacheNotifications | undefined; export interface CreateAppCachesOptions { /** Per-cache overrides merged over {@link DEFAULT_APP_CACHES_PROFILE}. */ profile?: Partial; /** Absent ⇒ bare in-memory loaders (single replica, local mode, tests). */ notificationPairFactory?: GroupNotificationPairFactory; /** Error sink for background cache/notification failures. */ logger?: Logger; /** * Where each read's hit/miss is counted. Hit RATE is the only way to tell a cache that is * doing its job from one whose invalidation is firing so often it never serves anything — * two states with identical latency graphs and opposite fixes. Absent ⇒ uncounted. */ operationalMetrics?: OperationalMetrics; /** * The shared generation directory backing every profile entry with a * `coherencyWindowMsecs` (see that field's doc). One directory serves the whole bag, so all * coherent caches share each group's probe. Absent ⇒ no entry may set a window. */ generationStore?: CacheGenerationStore; /** * Adopter for the work the loaders start and do not await (preemptive refreshes, staleness * probes, notification publishes). On Node the default detached run is correct; an isolate * runtime hands the promise to the current request's `ctx.waitUntil` instead, because I/O * there is scoped to the request that created it. The promise always settles fulfilled. */ scheduleBackgroundWork?: BackgroundWorkScheduler; /** * Supplied ONLY by an isolate runtime (the Worker): returns an object identifying the * invocation currently being served, or `undefined` outside a bracketed entry point. * * Its presence switches every cache MISS off layered-loader's shared load path and onto the * per-invocation one in {@link InvocationScopedLoads}, because the bag is one per ISOLATE * and workerd destroys, uncatchably, any invocation that awaits a promise another one * created. Absent (Node, where one process serves every request out of one I/O context) ⇒ * the loader's own coalescing is kept, unchanged. */ currentInvocation?: () => object | undefined; } /** * Build the app-owned cache bag. Called once per process by a facade's * composition root and threaded through the dependency bag as the kernel * {@link AppCaches} port; `createCore` builds a bare default when a harness * passes none. */ export declare function createAppCaches(options?: CreateAppCachesOptions): AppCaches; //# sourceMappingURL=appCaches.d.ts.map