@happyvertical/smrt-web 0.38.2 → 0.38.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @happyvertical/smrt-web — browser client data runtime (#1761).\n *\n * A typed collection factory that materializes the manifest-generated web\n * collection definitions (`@happyvertical/smrt-virt-web`) as cached, reactive\n * collections over the generated SMRT REST surface.\n *\n * This package is the **engine-absorption boundary**: the client-data engine\n * (currently TanStack DB) is an implementation detail held entirely inside\n * this module. Its types never appear on the public API — collections are\n * handed back as the SMRT-owned {@link SmrtWebCollection}, and the shared cache\n * as the opaque {@link SmrtWebClient} — so the engine stays swappable without a\n * consumer-visible break. Consumers never import `@tanstack/*` directly.\n *\n * Framework-agnostic by construction: this entry imports no UI framework.\n * Svelte live-query bindings ship separately (see PRD #1755) so this core never\n * pulls the Svelte-only `@tanstack/svelte-db` export condition.\n *\n * Scope of this slice:\n * - stale-while-revalidate reads (a `staleTimeMs` window, background revalidation)\n * - concurrent-read dedup (one network request per in-flight collection load)\n * - optimistic create that persists through the generated REST surface and\n * rolls back automatically when the server errors\n * - relationship-derived invalidation (#1761): a settled mutation invalidates\n * the caches of the collections related to the mutated one, with the edges\n * derived from the manifest (`definition.relationships`) — no hand-wired\n * cache keys. Cross-collection reach requires a shared client from\n * {@link createSmrtWebClient}; with a private client only the mutated\n * collection refetches.\n * - hydration seeding (#1761): rows fetched server-side (a SvelteKit\n * `+page.server.ts` load) seed the shared cache via\n * {@link CreateSmrtCollectionOptions.initialData}, so the first client read\n * serves them WITHOUT a duplicate first-render fetch.\n *\n * Deliberately NOT here yet (see PRD #1755): offline outbox, SSE invalidation,\n * persistence, version awareness.\n */\n\nimport { createCollection } from '@tanstack/db';\nimport { QueryClient } from '@tanstack/query-core';\nimport { queryCollectionOptions } from '@tanstack/query-db-collection';\n\n// ---------------------------------------------------------------------------\n// Generated definition contract (mirrors @happyvertical/smrt-virt-web)\n// ---------------------------------------------------------------------------\n\n/**\n * Field metadata emitted per column by the `@happyvertical/smrt-virt-web`\n * virtual module (generated from the package manifest).\n */\nexport interface SmrtWebFieldDefinition {\n type: string;\n required?: boolean;\n default?: unknown;\n}\n\n/** The relationship kinds a generated web collection edge can describe. */\nexport type SmrtWebRelationshipKind =\n | 'foreignKey'\n | 'crossPackageRef'\n | 'oneToMany'\n | 'manyToMany';\n\n/**\n * A manifest-derived edge from this collection to a sibling REST collection,\n * emitted by the `@happyvertical/smrt-virt-web` virtual module. When a mutation\n * on this collection settles, the caches of the collections named by these\n * edges are invalidated (relationship-derived invalidation, #1761), so a\n * dependent view refetches without any hand-wired cache key.\n *\n * SMRT-owned data — no client-engine (`@tanstack/*`) type appears here, so it\n * stays inside the engine-absorption boundary.\n */\nexport interface SmrtWebRelationship {\n /** The declaring field carrying the relationship (e.g. `groupId`, `items`). */\n field: string;\n /** The relationship kind, mirroring the manifest field type. */\n kind: SmrtWebRelationshipKind;\n /** REST collection name the edge resolves to (e.g. `ad_groups`). */\n relatedCollection: string;\n}\n\n/**\n * One generated collection definition: everything needed to construct a client\n * collection over the generated REST surface. The `_row` property is a phantom\n * type carrier threaded through codegen — it never exists at runtime, it only\n * lets factories infer the row type from a definition.\n */\nexport interface SmrtWebCollectionDefinition<TData extends object = object> {\n /** REST collection name (e.g. `products`). */\n name: string;\n /** Source class name (e.g. `Product`). */\n className: string;\n /** Path under the API base path (e.g. `/products`). */\n endpoint: string;\n /** Primary key field name (`id` for SmrtObject). */\n idField: string;\n /** CRUD + custom actions exposed by the api decorator config. */\n actions: string[];\n /** Persisted field metadata keyed by field name. */\n fields: Record<string, SmrtWebFieldDefinition>;\n /**\n * Manifest-derived relationship edges to sibling REST collections. Drives\n * relationship-derived cache invalidation: a settled mutation on this\n * collection invalidates the caches of the collections these edges name.\n * Optional so hand-built definitions (older codegen, tests) still satisfy the\n * type; a missing value means \"no derived edges\".\n */\n relationships?: SmrtWebRelationship[];\n /** Phantom row-type carrier — never present at runtime. */\n _row?: TData;\n}\n\n// ---------------------------------------------------------------------------\n// Fetcher contract + payload normalization\n// ---------------------------------------------------------------------------\n\n/**\n * The per-collection CRUD surface of the generated REST client\n * (`createClient(basePath).<collection>` from `@happyvertical/smrt-virt-client`).\n *\n * Return types are `unknown` on purpose: generated fetchers resolve with\n * whatever the server sent, so this package normalizes and validates payloads\n * centrally — see {@link unwrapListResult} / {@link unwrapItemResult}.\n */\nexport interface SmrtCrudFetchers {\n list(params?: Record<string, unknown>): Promise<unknown>;\n get?(id: string): Promise<unknown>;\n create(data: Record<string, unknown>): Promise<unknown>;\n update?(id: string, data: Record<string, unknown>): Promise<unknown>;\n delete?(id: string): Promise<unknown>;\n}\n\n/**\n * Raised when a generated-client call resolved with an error payload\n * (`{ error: string }` from the generated REST routes) or an unexpected shape.\n * Thrown inside a mutation handler, this triggers the automatic rollback of\n * optimistic state.\n */\nexport class SmrtWebRequestError extends Error {\n readonly payload: unknown;\n\n constructor(message: string, payload?: unknown) {\n super(message);\n this.name = 'SmrtWebRequestError';\n this.payload = payload;\n }\n}\n\n/** A row as stored in the client collection: the DTO plus a required key. */\nexport type SmrtWebRow<TData extends object> = TData & { id: string };\n\n/**\n * Normalize a generated-client list result to an array of rows.\n *\n * The generated REST routes return a bare JSON array; `{ error }` payloads are\n * surfaced as failures. The `{ data: [...] }` envelope is tolerated for\n * ApiResponse-shaped clients (e.g. a mock client).\n */\nexport function unwrapListResult(\n result: unknown,\n collectionName: string,\n): Array<Record<string, unknown>> {\n if (Array.isArray(result)) {\n return result as Array<Record<string, unknown>>;\n }\n if (result && typeof result === 'object') {\n const record = result as Record<string, unknown>;\n if (typeof record.error === 'string') {\n throw new SmrtWebRequestError(\n `[smrt-web] list(${collectionName}) failed: ${record.error}`,\n result,\n );\n }\n if (Array.isArray(record.data)) {\n return record.data as Array<Record<string, unknown>>;\n }\n }\n throw new SmrtWebRequestError(\n `[smrt-web] list(${collectionName}) returned an unexpected payload shape`,\n result,\n );\n}\n\n/**\n * Normalize a generated-client item result (create/update) to a row.\n * `{ error }` payloads become failures — inside mutation handlers this is what\n * makes optimistic state roll back.\n */\nexport function unwrapItemResult(\n result: unknown,\n context: string,\n): Record<string, unknown> {\n if (result && typeof result === 'object' && !Array.isArray(result)) {\n const record = result as Record<string, unknown>;\n if (typeof record.error === 'string') {\n throw new SmrtWebRequestError(\n `[smrt-web] ${context} failed: ${record.error}`,\n result,\n );\n }\n if (\n record.data &&\n typeof record.data === 'object' &&\n !Array.isArray(record.data)\n ) {\n return record.data as Record<string, unknown>;\n }\n return record;\n }\n throw new SmrtWebRequestError(\n `[smrt-web] ${context} returned an unexpected payload shape`,\n result,\n );\n}\n\n/**\n * Build CRUD fetchers from a generated collection definition — the same URL\n * scheme and payload handling as the generated REST client\n * (`basePath + endpoint`), with one improvement: HTTP error statuses reject\n * with the server's `{ error }` body instead of resolving with it.\n */\nexport function createDefinitionFetchers(\n definition: SmrtWebCollectionDefinition<object>,\n basePath = '/api/v1',\n fetchFn: typeof fetch = (...args) => globalThis.fetch(...args),\n): SmrtCrudFetchers {\n const collectionUrl = `${basePath}${definition.endpoint}`;\n const headers = { 'Content-Type': 'application/json' };\n\n const parse = async (response: Response): Promise<unknown> => {\n const payload: unknown = await response.json().catch(() => null);\n if (!response.ok) {\n const message =\n payload &&\n typeof payload === 'object' &&\n typeof (payload as Record<string, unknown>).error === 'string'\n ? String((payload as Record<string, unknown>).error)\n : `HTTP ${response.status}`;\n throw new SmrtWebRequestError(\n `[smrt-web] ${definition.name} request failed: ${message}`,\n payload,\n );\n }\n return payload;\n };\n\n return {\n list: async () => parse(await fetchFn(collectionUrl, { headers })),\n get: async (id) =>\n parse(await fetchFn(`${collectionUrl}/${id}`, { headers })),\n create: async (data) =>\n parse(\n await fetchFn(collectionUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify(data),\n }),\n ),\n update: async (id, data) =>\n parse(\n await fetchFn(`${collectionUrl}/${id}`, {\n method: 'PUT',\n headers,\n body: JSON.stringify(data),\n }),\n ),\n delete: async (id) => {\n const response = await fetchFn(`${collectionUrl}/${id}`, {\n method: 'DELETE',\n headers,\n });\n if (!response.ok) {\n throw new SmrtWebRequestError(\n `[smrt-web] delete(${definition.name}) failed: HTTP ${response.status}`,\n );\n }\n return true;\n },\n };\n}\n\n/**\n * Generate a client-local id for optimistic inserts. The generated REST layer\n * strips client-supplied ids on create (mass-assignment guard #1540), so this\n * id only identifies the optimistic row until the post-persist refetch swaps in\n * the server-assigned row.\n */\nexport function newLocalId(): string {\n const cryptoRef = globalThis.crypto as Crypto | undefined;\n if (cryptoRef?.randomUUID) {\n return cryptoRef.randomUUID();\n }\n return `local-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n\n// ---------------------------------------------------------------------------\n// Engine-absorbing public surface (no @tanstack/* types leak past here)\n// ---------------------------------------------------------------------------\n\n/**\n * Opaque handle to the shared client cache / request-dedup layer. Create one\n * with {@link createSmrtWebClient} and pass the SAME instance to every\n * collection that should share a cache and deduplicate in-flight requests.\n *\n * The engine (currently a TanStack Query client) is intentionally hidden behind\n * this brand so it stays swappable — do not depend on its concrete shape.\n */\nexport interface SmrtWebClient {\n /** Phantom brand — this handle wraps the hidden client-cache engine. */\n readonly __smrtWebClient: 'SmrtWebClient';\n}\n\n/**\n * Engine-side shape of a {@link SmrtWebClient}. Never exported, so the engine\n * type never reaches the public surface. Extends the public brand so the value\n * created here carries the brand at runtime (enabling the validation below).\n */\ninterface SmrtWebClientEngine extends SmrtWebClient {\n readonly queryClient: QueryClient;\n}\n\n/**\n * Create a shared client-cache handle. Pass the returned handle as\n * {@link CreateSmrtCollectionOptions.client} to every collection that should\n * share a cache and deduplicate requests app-wide.\n */\nexport function createSmrtWebClient(): SmrtWebClient {\n const engine: SmrtWebClientEngine = {\n __smrtWebClient: 'SmrtWebClient',\n queryClient: new QueryClient(),\n };\n return engine;\n}\n\nfunction resolveQueryClient(client?: SmrtWebClient): QueryClient {\n if (!client) return new QueryClient();\n const engine = client as Partial<SmrtWebClientEngine>;\n if (engine.__smrtWebClient !== 'SmrtWebClient' || !engine.queryClient) {\n throw new SmrtWebRequestError(\n '[smrt-web] options.client must be a handle from createSmrtWebClient()',\n );\n }\n return engine.queryClient;\n}\n\n/**\n * Project an engine row to a plain public DTO. The client-data engine decorates\n * stored rows with enumerable virtual props (`$synced`/`$origin`/`$key`/\n * `$collectionId`) that would otherwise cross the SMRT boundary through spread\n * or JSON serialization. The `$` prefix is reserved for the engine; SMRT\n * columns never begin with it.\n */\nfunction toPlainRow<TData extends object>(row: unknown): SmrtWebRow<TData> {\n const plain: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(row as Record<string, unknown>)) {\n if (key.charCodeAt(0) !== 36 /* '$' */) plain[key] = value;\n }\n return plain as SmrtWebRow<TData>;\n}\n\n/** Project the row values carried by a change notification to plain DTOs. */\nfunction projectChanges(changes: unknown): unknown {\n if (!Array.isArray(changes)) return changes;\n return changes.map((change) => {\n if (!change || typeof change !== 'object') return change;\n const record = change as Record<string, unknown>;\n const projected: Record<string, unknown> = { ...record };\n if (record.value && typeof record.value === 'object') {\n projected.value = toPlainRow(record.value);\n }\n if (record.previousValue && typeof record.previousValue === 'object') {\n projected.previousValue = toPlainRow(record.previousValue);\n }\n return projected;\n });\n}\n\n/**\n * A pending optimistic mutation. Await {@link isPersisted} to observe the\n * server outcome: it resolves once the write has been persisted through the\n * REST surface, and rejects (rolling the optimistic state back) on error.\n */\nexport interface SmrtWebTransaction {\n readonly isPersisted: { readonly promise: Promise<unknown> };\n}\n\n/** A change-subscription handle. Call {@link unsubscribe} to detach. */\nexport interface SmrtWebSubscription {\n unsubscribe(): void;\n}\n\n/**\n * A live, cached collection of plain-DTO rows — the SMRT-owned public contract\n * over the client-data engine. Exposes only the committed surface; the engine's\n * own type is never named here so it stays swappable.\n */\nexport interface SmrtWebCollection<TData extends object> {\n /** All rows currently in the collection (plain DTOs, insertion order). */\n readonly toArray: ReadonlyArray<SmrtWebRow<TData>>;\n /** Number of rows currently in the collection. */\n readonly size: number;\n /** True when a row with `key` is present. */\n has(key: string): boolean;\n /** The row with `key`, or `undefined`. */\n get(key: string): SmrtWebRow<TData> | undefined;\n /** Resolve once the first load has completed. */\n preload(): Promise<void>;\n /** Tear down subscriptions and cached state. */\n cleanup(): Promise<void>;\n /** Subscribe to change notifications; returns a detach handle. */\n subscribeChanges(callback: (changes: unknown) => void): SmrtWebSubscription;\n /**\n * Optimistically insert a row and persist it through the create fetcher. The\n * row is visible synchronously; the returned transaction settles on the\n * server outcome (see {@link SmrtWebTransaction}).\n */\n insert(row: SmrtWebRow<TData>): SmrtWebTransaction;\n}\n\n/**\n * Options for {@link createSmrtCollection}. Generic in the collection's row\n * type `TData` so {@link initialData} is checked against the same DTO the\n * collection stores; every other option is row-type-agnostic, so the parameter\n * defaults to `object` and can be omitted at call sites that pass no seed.\n */\nexport interface CreateSmrtCollectionOptions<TData extends object = object> {\n /**\n * Generated REST client surface for this collection, e.g.\n * `createClient('/api/v1').products` from the virt-client module. When\n * omitted, fetchers are derived from the definition's endpoint and `basePath`\n * with the same URL scheme and payload shapes the generated client uses.\n */\n fetchers?: SmrtCrudFetchers;\n /** API base path for definition-derived fetchers (default `/api/v1`). */\n basePath?: string;\n /** Fetch implementation override (tests, SSR). Defaults to global fetch. */\n fetchFn?: typeof fetch;\n /**\n * Shared cache handle from {@link createSmrtWebClient}. Pass one app-wide\n * instance so collections share a cache and deduplicate requests; a private\n * cache is created when omitted.\n */\n client?: SmrtWebClient;\n /**\n * Cache namespace for this collection's reads. Fold a backend / tenant /\n * preview discriminator in here when the SAME generated collection is\n * materialized against DIFFERENT backends while sharing one {@link client} —\n * without it those reads share a cache key and could serve one backend's rows\n * for the other for the whole `staleTimeMs` window. Omit for the common\n * single-backend case.\n */\n scope?: string;\n /**\n * Stale-while-revalidate window in milliseconds (default 30s): reads within\n * the window are served from the local collection without a network request;\n * the first read after it revalidates in the background.\n */\n staleTimeMs?: number;\n /** Retry failed loads (default false: fail fast, surface errors). */\n retry?: boolean;\n /**\n * Rows to seed this collection's cache with, before its first read — the\n * hydration path for server-rendered data (#1761). Fetch rows in a SvelteKit\n * `+page.server.ts` load, pass them here on the client, and the first read\n * serves them from cache WITHOUT a duplicate first-render network request\n * (SMRT-owned type, so no engine type appears on the option).\n *\n * The seed is written to the cache with a fresh timestamp, so it counts as\n * fresh for `staleTimeMs`: with the default window the first read does not\n * fetch, and the collection revalidates in the background only once the window\n * elapses (or immediately if `staleTimeMs` is 0). Seed the SAME rows the\n * server serialized so the pre- and post-hydration renders match.\n *\n * Seeds the SAME cache key the reads use — so with a shared {@link client},\n * fold the backend / tenant discriminator into {@link scope} to match, exactly\n * as reads do; otherwise one backend's seed would serve the other for the\n * `staleTimeMs` window.\n */\n initialData?: SmrtWebRow<TData>[];\n}\n\n/**\n * Registry mapping a public collection handle to its underlying engine\n * collection. Keyed weakly so a handle and its engine collection are collected\n * together. Read only through {@link getEngineCollection}.\n */\nconst engineCollections = new WeakMap<object, unknown>();\n\n/**\n * Retrieve the underlying engine collection backing a handle — an advanced\n * bridge for trusted framework bindings (e.g. the smrt-svelte live-query\n * binding), which must feed the engine collection to the query builder. Returns\n * `unknown` so no engine type crosses the boundary; callers cast. Throws for a\n * handle not produced by {@link createSmrtCollection}. Not needed for normal\n * use.\n */\nexport function getEngineCollection<TData extends object>(\n handle: SmrtWebCollection<TData>,\n): unknown {\n const engine = engineCollections.get(handle);\n if (engine === undefined) {\n throw new SmrtWebRequestError(\n '[smrt-web] getEngineCollection: not a smrt-web collection handle',\n );\n }\n return engine;\n}\n\n/**\n * Create a typed client collection over a generated SMRT collection definition\n * and the matching generated REST client fetchers.\n *\n * Reads: stale-while-revalidate. The first subscriber triggers a fetch;\n * re-subscribing within `staleTimeMs` serves local data with no request. N\n * concurrent identical reads coalesce into one network request.\n *\n * Writes: `collection.insert({ ...data, id: newLocalId() })` applies instantly,\n * persists through `fetchers.create()` (the temp id is stripped — the server\n * assigns the real one), then refetches to reconcile. A failed create rejects\n * the transaction and the optimistic row rolls back automatically.\n *\n * Relationship-derived invalidation: once a create/update/delete has persisted,\n * the query caches of this collection AND the collections named by\n * `definition.relationships` (manifest-derived edges) are invalidated, so\n * dependent views refetch. Reaching OTHER collections requires them to share\n * this collection's `client` (see {@link createSmrtWebClient}); with a private\n * client only this collection refetches.\n *\n * Hydration seeding: pass rows fetched server-side as\n * {@link CreateSmrtCollectionOptions.initialData} and the collection's first\n * read is served from them with NO network request (until `staleTimeMs`\n * elapses) — the SvelteKit `+page.server.ts` → hydrate path.\n */\nexport function createSmrtCollection<TData extends object>(\n definition: SmrtWebCollectionDefinition<TData>,\n options: CreateSmrtCollectionOptions<TData>,\n): SmrtWebCollection<TData> {\n type Row = SmrtWebRow<TData>;\n\n const { staleTimeMs = 30_000, retry = false, scope, initialData } = options;\n const fetchers =\n options.fetchers ??\n createDefinitionFetchers(definition, options.basePath, options.fetchFn);\n const queryClient = resolveQueryClient(options.client);\n const idField = definition.idField || 'id';\n\n // Scope discriminates the cache key so a shared client can materialize the\n // same collection against different backends without cross-serving reads.\n const cacheId = scope\n ? `smrt:${scope}:${definition.name}`\n : `smrt:${definition.name}`;\n const queryKey = scope\n ? ['smrt', scope, definition.name]\n : ['smrt', definition.name];\n\n // Hydration seeding (#1761): if the caller passed rows fetched server-side,\n // write them into the query cache BEFORE the collection's engine starts its\n // sync. On the first read the engine finds cached data and populates from it\n // instead of fetching (verified: zero list() calls). `setQueryData` stamps a\n // fresh `dataUpdatedAt`, so the seed counts as fresh for `staleTime` — the\n // first read serves it with no request, and revalidation fires only once\n // `staleTimeMs` elapses (or immediately when it is 0). An explicit empty seed\n // is honored too: it means \"the server returned zero rows\", a valid fresh\n // state that likewise suppresses the first fetch.\n //\n // Seed only when the key is empty, via the ATOMIC updater form: a plain\n // get-then-set would let two collections sharing this key and materialized in\n // the same tick both observe `undefined` and have the later seed clobber the\n // earlier one. `(existing) => existing ?? initialData` keeps the first seed\n // (or any already-cached rows, which may be newer than this late SSR payload)\n // in a single cache write.\n if (initialData !== undefined) {\n queryClient.setQueryData<Row[]>(\n queryKey,\n (existing) => existing ?? initialData,\n );\n }\n\n // Relationship-derived invalidation target set (#1761): the collections\n // whose caches a settled mutation on THIS collection must invalidate. Always\n // includes this collection itself (so its own read revalidates) plus every\n // manifest-derived related collection. Built once; a settled write matches\n // any cached query whose collection-name segment (the LAST queryKey element,\n // mirroring the `['smrt', (scope,) name]` scheme above) is in this set.\n //\n // Over-invalidation is safe — a stale query merely refetches. Under-\n // invalidation is the bug (a dependent view showing stale rows), so the\n // predicate matches by collection name across ALL scopes rather than an exact\n // key: a mutation in one scope refreshes the related collection in every\n // scope sharing the client.\n const invalidationTargets = new Set<string>([definition.name]);\n for (const relationship of definition.relationships ?? []) {\n invalidationTargets.add(relationship.relatedCollection);\n }\n\n /**\n * Invalidate the query caches of this collection and its manifest-derived\n * related collections. Cross-collection reach requires those collections to\n * share this collection's `client` (from {@link createSmrtWebClient}); with a\n * private client only THIS collection's query lives here, so only it\n * refetches. Fire-and-forget: invalidation schedules a background refetch and\n * must not delay the mutation's own settle.\n */\n const invalidateRelated = (): void => {\n void queryClient.invalidateQueries({\n predicate: (query) => {\n const key = query.queryKey;\n if (!Array.isArray(key) || key.length === 0) return false;\n const collectionSegment = key[key.length - 1];\n return (\n typeof collectionSegment === 'string' &&\n invalidationTargets.has(collectionSegment)\n );\n },\n });\n };\n\n const collection = createCollection(\n queryCollectionOptions<Row>({\n id: cacheId,\n queryKey,\n queryClient,\n staleTime: staleTimeMs,\n retry,\n queryFn: async () =>\n unwrapListResult(await fetchers.list(), definition.name) as Array<Row>,\n getKey: (row) => String((row as Record<string, unknown>)[idField]),\n onInsert: async ({ transaction }) => {\n for (const mutation of transaction.mutations) {\n const modified = mutation.modified as Record<string, unknown>;\n // Strip the client-local id: the generated REST layer rejects or\n // ignores client-supplied ids on create (#1540); the follow-up\n // refetch swaps the optimistic row for the server-assigned one.\n const { [idField]: _localId, ...data } = modified;\n unwrapItemResult(\n await fetchers.create(data),\n `create(${definition.name})`,\n );\n }\n // Persisted: refresh this collection and its related collections. Runs\n // only after every create resolved — a rejected create rolls the\n // optimistic row back and never reaches here.\n invalidateRelated();\n },\n onUpdate: fetchers.update\n ? async ({ transaction }) => {\n for (const mutation of transaction.mutations) {\n const key = String(mutation.key);\n const changes = mutation.changes as Record<string, unknown>;\n unwrapItemResult(\n // biome-ignore lint/style/noNonNullAssertion: guarded by the surrounding ternary\n await fetchers.update!(key, changes),\n `update(${definition.name})`,\n );\n }\n invalidateRelated();\n }\n : undefined,\n onDelete: fetchers.delete\n ? async ({ transaction }) => {\n for (const mutation of transaction.mutations) {\n // biome-ignore lint/style/noNonNullAssertion: guarded by the surrounding ternary\n await fetchers.delete!(String(mutation.key));\n }\n invalidateRelated();\n }\n : undefined,\n }),\n );\n\n // Wrap the engine collection in the SMRT-owned public surface. The wrapper\n // projects rows to plain DTOs at every read boundary (toArray/get and change\n // payloads) so the engine's virtual props never escape, and confines the\n // engine's own types to this module.\n const handle: SmrtWebCollection<TData> = {\n get toArray() {\n return collection.toArray.map((row) => toPlainRow<TData>(row));\n },\n get size() {\n return collection.size;\n },\n has(key) {\n return collection.has(key);\n },\n get(key) {\n const row = collection.get(key);\n return row === undefined ? undefined : toPlainRow<TData>(row);\n },\n preload() {\n return collection.preload();\n },\n cleanup() {\n return collection.cleanup();\n },\n subscribeChanges(callback) {\n const subscription = collection.subscribeChanges((changes: unknown) =>\n callback(projectChanges(changes)),\n );\n return { unsubscribe: () => subscription.unsubscribe() };\n },\n insert(row) {\n return collection.insert(row) as unknown as SmrtWebTransaction;\n },\n };\n\n engineCollections.set(handle, collection);\n return handle;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["released","release","reason"],"sources":["../src/capability.ts","../src/durable-store.ts","../src/offline/durable-queue.ts","../src/offline/leader.ts","../src/offline/types.ts","../src/offline/engine.ts","../src/offline.ts","../src/sse-client.ts","../src/index.ts"],"sourcesContent":["/**\n * @happyvertical/smrt-web — capability extension seam (#1755).\n *\n * A capability is a small plug-in that hooks the {@link createSmrtCollection}\n * lifecycle so a follow-on client slice — the offline outbox (#1762),\n * persistence (#1764), or live SSE invalidation (#1763-client) — can live in\n * its OWN module instead of contending on `index.ts`. The factory runs the six\n * hooks below at fixed points; a collection with no capabilities is\n * byte-for-byte the collection of today (the no-op guarantee).\n *\n * ## The engine boundary is sacred\n *\n * Every type here is expressed ENTIRELY in existing SMRT-owned terms\n * (`SmrtWebCollectionDefinition`, `SmrtCrudFetchers`, `SmrtWebRow`, plain TS).\n * No `@tanstack/*` type — no `QueryClient`, no engine `Collection` — appears on\n * the capability surface, so the build's `.d.ts` boundary check stays green and\n * the engine stays swappable. A capability that genuinely needs deeper engine\n * access reaches it through the existing `getEngineCollection()` unknown-bridge\n * from inside its own module — NEVER by widening these types.\n *\n * These types intentionally import nothing from `./index.ts` beyond the\n * SMRT-owned contracts they name, so the seam is reviewable and unit-testable on\n * its own (see {@link runWrapMutation}).\n */\n\nimport type {\n SmrtCrudFetchers,\n SmrtWebCollectionDefinition,\n SmrtWebRow,\n} from './index.js';\n\n/**\n * The context every capability hook receives — a stable, engine-free view of\n * the collection being built. All fields are the exact values the factory uses\n * internally (the resolved cache key/id, the same fetchers), so a capability\n * keys its own storage or subscriptions off the identical discriminators.\n */\nexport interface SmrtWebCapabilityContext<TData extends object = object> {\n /** The generated definition this collection materializes. */\n readonly definition: SmrtWebCollectionDefinition<TData>;\n /** The CRUD fetchers the collection persists through. */\n readonly fetchers: SmrtCrudFetchers;\n /**\n * The `queryKey` segments the collection's reads use — a LIVE view: from\n * `onAttach`/`warmStart` onward it is the final key (including every\n * capability-contributed segment); read during `contributeCacheKey` it omits\n * that capability's own not-yet-applied segment. Treat as read-only.\n */\n readonly cacheKey: readonly string[];\n /** The engine-collection id string (mirrors {@link cacheKey}). */\n readonly cacheId: string;\n /**\n * Enter the SAME relationship-derived invalidation the factory runs after a\n * settled mutation. A capability calls this to refetch this collection and its\n * manifest-related collections in response to an EXTERNAL trigger (e.g. an SSE\n * message) without reaching into the engine.\n */\n invalidate(): void;\n}\n\n/**\n * A single mutation described in SMRT-owned terms — handed to\n * {@link SmrtWebCapability.wrapMutation} and {@link SmrtWebCapability.onSettled}\n * so a capability can inspect or intercept a write without touching the engine's\n * transaction type.\n */\nexport interface SmrtWebMutationEnvelope {\n /** Which mutation kind this describes. */\n readonly kind: 'insert' | 'update' | 'delete';\n /** The row key: the client-local id on insert, else the target row's id. */\n readonly key: string;\n /**\n * The write payload: the full row on insert, the changed fields on update,\n * and an empty object on delete (the key carries the target).\n */\n readonly data: Record<string, unknown>;\n /**\n * The server `updated_at` / `updatedAt` value the mutation is based on, when\n * known. Update/delete handlers capture this from the original row so offline\n * replay can preserve the sync/apply conflict guard even when the write\n * payload only carries changed fields (or no fields for delete).\n */\n readonly baseUpdatedAt?: string;\n}\n\n/** The outcome handed to {@link SmrtWebCapability.wrapMutation}. */\nexport type WrapMutationOutcome =\n | { handled: true; result: unknown }\n | { handled: false }\n | undefined;\n\n/** The settle outcome handed to {@link SmrtWebCapability.onSettled}. */\nexport type MutationSettleOutcome =\n | { ok: true; result: unknown }\n | { ok: false; error: unknown };\n\n/**\n * A capability plugged into {@link createSmrtCollection}. Every hook is\n * optional; a capability implements only the points its slice needs. Hooks fire\n * in these fixed places, and capabilities run in ARRAY ORDER:\n *\n * - `contributeCacheKey` — ONCE, before the engine collection is constructed.\n * - `warmStart` — ONCE, before the first read (seeds the cache).\n * - `wrapMutation` — per mutation, BEFORE the fetcher, with a chance to handle\n * the write itself (offline).\n * - `onSettled` — per mutation, AFTER it settles (success AND fetcher-throw).\n * - `onAttach` — ONCE, right after the engine collection is constructed; the\n * ONLY place a capability registers an external (non-mutation) trigger such as\n * an SSE subscription.\n * - `teardown` — in `cleanup()`, after the engine's own cleanup.\n */\nexport interface SmrtWebCapability<TData extends object = object> {\n /** Diagnostic name (e.g. `'offline-outbox'`). */\n readonly name: string;\n\n /**\n * Contribute extra cache-key segments so this capability can partition the\n * collection's cache (e.g. an outbox variant). Runs ONCE, before construction;\n * returned segments are appended to the collection's cache key/id. Return\n * `undefined` (or omit) to contribute nothing.\n */\n contributeCacheKey?(\n ctx: SmrtWebCapabilityContext<TData>,\n ): string[] | undefined;\n\n /**\n * Provide rows to seed the cache before the first read — the persistence\n * slice's rehydrate-from-disk path. Runs ONCE, before construction. NOTE:\n * caller-supplied `initialData` (fresher same-request SSR truth) WINS over any\n * capability `warmStart`. Return `undefined` to contribute no seed.\n */\n warmStart?(\n ctx: SmrtWebCapabilityContext<TData>,\n ): Promise<SmrtWebRow<TData>[] | undefined> | SmrtWebRow<TData>[] | undefined;\n\n /**\n * Intercept a mutation BEFORE its fetcher runs. Return `{ handled: true,\n * result }` to take over the write (the fetcher is skipped and `result`\n * reconciles the optimistic row — the offline path); return `{ handled: false\n * }` or `undefined` to fall through to the real fetcher. With multiple\n * capabilities the FIRST `{ handled: true }` wins and later capabilities'\n * `wrapMutation` are skipped.\n */\n wrapMutation?(\n envelope: SmrtWebMutationEnvelope,\n ctx: SmrtWebCapabilityContext<TData>,\n ): Promise<WrapMutationOutcome> | WrapMutationOutcome;\n\n /**\n * Observe a mutation after it settles — on BOTH a successful persist and a\n * fetcher throw (before the optimistic rollback propagates). Never swallows\n * the error: a thrown fetcher error still rolls the transaction back.\n */\n onSettled?(\n envelope: SmrtWebMutationEnvelope,\n outcome: MutationSettleOutcome,\n ctx: SmrtWebCapabilityContext<TData>,\n ): void;\n\n /**\n * Run ONCE, right after the engine collection is constructed. The ONLY hook\n * where a capability wires an external trigger (SSE subscription, focus\n * listener) — `ctx.invalidate()` is callable here.\n */\n onAttach?(ctx: SmrtWebCapabilityContext<TData>): void;\n\n /** Run inside `cleanup()`, AFTER the engine's own cleanup. Awaited if async. */\n teardown?(ctx: SmrtWebCapabilityContext<TData>): void | Promise<void>;\n}\n\n/**\n * Run the `wrapMutation` hook across `capabilities` in array order for one\n * mutation, short-circuiting on the FIRST capability that returns `{ handled:\n * true }` (later capabilities' `wrapMutation` are then skipped). A capability\n * that returns `{ handled: false }`, `undefined`, or omits the hook declines,\n * and the next is tried. When every capability declines, resolves `{ handled:\n * false }` so the factory falls through to the real fetcher.\n */\nexport async function runWrapMutation<TData extends object>(\n capabilities: readonly SmrtWebCapability<TData>[],\n envelope: SmrtWebMutationEnvelope,\n ctx: SmrtWebCapabilityContext<TData>,\n): Promise<{ handled: true; result: unknown } | { handled: false }> {\n for (const capability of capabilities) {\n if (!capability.wrapMutation) continue;\n const outcome = await capability.wrapMutation(envelope, ctx);\n if (outcome?.handled) {\n return { handled: true, result: outcome.result };\n }\n }\n return { handled: false };\n}\n","/**\n * @happyvertical/smrt-web — shared durable-store foundation (#1755).\n *\n * The ONE SMRT-layer namespacing + wipe registry that the future offline\n * outbox (#1762) and persistence (#1764) slices both build on. Pure bookkeeping\n * — ZERO client-data-engine (`@tanstack/*`) imports — so it stays inside the\n * engine-absorption boundary and ships now, before either consumer exists, so\n * the two slices agree on it from day one.\n *\n * Why a SMRT-layer registry rather than one storage engine: TanStack DB\n * persistence (SQLite-WASM / OPFS) and `@tanstack/offline-transactions`\n * (IndexedDB) are SEPARATE storage engines. There is no single primitive that\n * spans both, so the shared foundation lives one level up — a deterministic\n * namespace both slices key their own storage under, plus a registry so\n * {@link wipeDurableStore} can clear BOTH through one call without the outbox\n * and persistence modules importing each other. A logout / tenant-switch wipes\n * every durable artifact for a namespace in one place.\n *\n * Nothing in this package calls these yet — the seam ships ahead of its\n * consumers by design (see PRD #1755).\n */\n\n/**\n * The identity a durable namespace is derived from. Combining the API base with\n * the tenant + identity + manifest hash means a logout, a tenant switch, or a\n * schema change each land on a DIFFERENT namespace, so durable artifacts are\n * never reused across those boundaries.\n *\n * `manifestHash` is supplied by the caller (its source is #1764's call — a\n * schema-shape digest); this module is source-agnostic and treats it as an\n * opaque discriminator.\n */\nexport interface DurableStoreKey {\n /** API base path the durable data was fetched against (e.g. `/api/v1`). */\n apiBase: string;\n /** Active tenant id, if any — absent means the single-tenant / global scope. */\n tenantId?: string;\n /** Authenticated identity id, if any — absent means anonymous. */\n identityId?: string;\n /** Opaque schema-shape digest; a change forces a fresh namespace. */\n manifestHash: string;\n}\n\n/**\n * Compute the deterministic storage namespace for a {@link DurableStoreKey}.\n * The same key always yields the same string; **any differing segment yields a\n * different one** (injective) — this is critical because the namespace IS the\n * tenant/identity/api isolation + wipe boundary, so a collision would reuse or\n * wipe durable data ACROSS those boundaries.\n *\n * Injectivity is guaranteed three ways: every segment is `encodeURIComponent`-\n * encoded, so a raw `:` in a value becomes `%3A` and can never be mistaken for a\n * separator; an ABSENT `tenantId` / `identityId` maps to the empty string while\n * a PRESENT value is prefixed with `_` (so undefined→``, ``→`_`, `x`→`_x`) — so\n * an explicitly-EMPTY id no longer collides with \"no id\", nor does a real id of\n * `-`. Both future slices key their own storage primitive (IndexedDB store name,\n * OPFS path, …) under this string.\n */\nexport function durableStoreNamespace(key: DurableStoreKey): string {\n // Optional segments: encode PRESENCE distinctly. undefined → '' (absent);\n // any present value (including '') → `_<encoded>`, so '' → '_' can never equal\n // the absent '' .\n const optional = (value: string | undefined): string =>\n value === undefined ? '' : `_${encodeURIComponent(value)}`;\n return `smrt-web:${encodeURIComponent(key.apiBase)}:${optional(key.tenantId)}:${optional(key.identityId)}:${encodeURIComponent(key.manifestHash)}`;\n}\n\n/**\n * A durable artifact registered under a namespace — the outbox queue (#1762) or\n * a persisted collection store (#1764). Each owns its storage engine and\n * exposes only a `clear()` so {@link wipeDurableStore} can tear it down without\n * knowing which engine backs it.\n */\nexport interface DurableResource {\n /** Which slice owns this artifact — for diagnostics and selective sweeps. */\n readonly kind: 'outbox' | 'persisted-collection';\n /** Drop this artifact's durable storage. Best-effort; may reject. */\n clear(): Promise<void>;\n}\n\n/**\n * Registry of durable artifacts keyed by namespace. A `Set` per namespace so a\n * resource registers and unregisters without positional bookkeeping, and so two\n * slices (outbox + persistence) coexist under one namespace.\n */\nconst registry = new Map<string, Set<DurableResource>>();\n\n/**\n * Register a durable artifact under a namespace so {@link wipeDurableStore} can\n * later clear it. Returns an unregister function that removes just this\n * resource — call it when the artifact is disposed on its own (before any\n * namespace-wide wipe) so it is not cleared twice.\n */\nexport function registerDurableResource(\n namespace: string,\n resource: DurableResource,\n): () => void {\n let resources = registry.get(namespace);\n if (!resources) {\n resources = new Set<DurableResource>();\n registry.set(namespace, resources);\n }\n resources.add(resource);\n\n return () => {\n const current = registry.get(namespace);\n if (!current) return;\n current.delete(resource);\n if (current.size === 0) registry.delete(namespace);\n };\n}\n\n/**\n * Clear every durable artifact registered under `namespace`, then drop the\n * namespace. This is the single teardown point a logout / tenant-switch calls:\n * it fans out across BOTH the outbox and persistence slices via the registry,\n * so neither module needs to import the other.\n *\n * Best-effort: a resource whose `clear()` rejects does not abort the sweep —\n * every registered resource is still cleared (a wipe is a teardown, not a\n * transaction). A safe no-op on an unknown or empty namespace.\n */\nexport async function wipeDurableStore(namespace: string): Promise<void> {\n const resources = registry.get(namespace);\n if (!resources || resources.size === 0) {\n registry.delete(namespace);\n return;\n }\n // Snapshot so a resource that unregisters itself inside clear() cannot mutate\n // the set mid-iteration. allSettled keeps a rejected clear() from aborting the\n // others.\n const snapshot = [...resources];\n registry.delete(namespace);\n await Promise.allSettled(snapshot.map((resource) => resource.clear()));\n}\n","/**\n * @happyvertical/smrt-web — the durable IndexedDB outbox queue (#1762).\n *\n * A raw IndexedDB FIFO queue: the persistence half of the offline outbox. One\n * database per durable-store namespace ({@link durableStoreNamespace}), one\n * object store (`outbox`) keyed by an AUTO-INCREMENTING `seq` — so insertion\n * order IS replay order for free — plus a `state` index so the replay loop can\n * scan only rows that still need work.\n *\n * ## Why raw IndexedDB, not `@tanstack/offline-transactions`\n *\n * The decisive finding (#1762): the TanStack offline layer's public API is\n * engine-typed (`Collection<…>`, `mutationFns`), so importing it emits a\n * `@tanstack/` specifier into `dist/offline.d.ts`, which\n * `scripts/check-smrt-web-engine-boundary.mjs` scans for unconditionally — the\n * build would fail. It also pins `@tanstack/db` exactly and competes with\n * smrt-web's OWN mutation lifecycle. So the outbox is hand-rolled over browser\n * globals (`indexedDB`, `navigator.locks`) with ZERO new runtime dependency.\n *\n * This module is engine-free (no `@tanstack/*` import): it stays inside the\n * engine-absorption boundary (#1761) and speaks only the sync-apply contract's\n * op shape (`packages/core/src/sync/apply.ts`).\n *\n * Row payloads are stored as structured-clone-safe plain objects; IndexedDB\n * clones on write, so callers may keep mutating the source object after enqueue.\n */\n\nimport type { SyncApplyOp } from './types.js';\n\n/** IndexedDB object-store name inside every outbox database. */\nexport const OUTBOX_STORE = 'outbox';\n/** Index over {@link OutboxRow.state} — the replay scan reads it. */\nexport const OUTBOX_STATE_INDEX = 'state';\n/** IndexedDB schema version. Bump only on a store/index migration. */\nexport const OUTBOX_DB_VERSION = 1;\n\n/**\n * Durable lifecycle state of one queued mutation. Deliberately a SUPERSET of\n * the app-observable sync state: `pending` rows are due for (re)send, while\n * `synced`/`failed` are terminal tombstones removed after each drain. The\n * app-facing {@link OutboxSyncState} (`pending`/`uploading`/`synced`/`failed`)\n * is derived by the engine — `uploading` is a transient in-flight marker the\n * engine emits, never persisted; this enum is the on-disk truth.\n */\nexport type OutboxRowState = 'pending' | 'synced' | 'failed';\n\n/**\n * One durable outbox row — a single client-authored mutation awaiting replay\n * through `POST {basePath}/sync/apply`. Fields mirror the sync-apply item so a\n * row maps to a batch item with no translation, plus the durable bookkeeping the\n * replay loop needs (`state`, `attempts`, `nextAttemptAt`, `lastError`).\n */\nexport interface OutboxRow {\n /** Auto-incrementing primary key — monotonic, so `seq` order IS FIFO. */\n seq?: number;\n /**\n * The sync-apply `itemId`: the client's idempotency/correlation handle. A\n * fresh UUID minted at enqueue time, distinct from {@link id} (the row UUID),\n * so it survives coalescing and is echoed back verbatim by the endpoint.\n */\n itemId: string;\n /** The collection route segment (e.g. `products`) — sync-apply `object`. */\n object: string;\n /** Which mutation kind — sync-apply `op`. */\n op: SyncApplyOp;\n /** The client-generated row UUID — sync-apply `id`. */\n id: string;\n /** Field data for create/update; omitted for delete. */\n payload?: Record<string, unknown>;\n /** The server `updated_at` the client last saw — the conflict guard base. */\n baseUpdatedAt?: string;\n /** On-disk lifecycle state. */\n state: OutboxRowState;\n /** How many replay attempts this row has made (drives backoff). */\n attempts: number;\n /** Epoch ms before which this row must not be retried (backoff gate). */\n nextAttemptAt: number;\n /** Last replay error message, for diagnostics/`snapshot()`. */\n lastError?: string;\n /** Epoch ms the row was first enqueued — stable ordering tiebreak + age. */\n enqueuedAt: number;\n}\n\n/** The fields a caller supplies to {@link DurableOutboxQueue.enqueue}. */\nexport interface EnqueueInput {\n itemId: string;\n object: string;\n op: SyncApplyOp;\n id: string;\n payload?: Record<string, unknown>;\n baseUpdatedAt?: string;\n}\n\n/**\n * Promisify a single IndexedDB request. Rejects with the request's error (or a\n * generic one) so callers get a real rejection, never a silent `undefined`.\n */\nfunction promisifyRequest<T>(request: IDBRequest<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () =>\n reject(request.error ?? new Error('[smrt-web] IndexedDB request failed'));\n });\n}\n\n/**\n * Promisify a transaction's completion. IndexedDB writes are only durable once\n * the TRANSACTION completes (not when the request succeeds), so mutations await\n * this — a queue that reported success before the txn committed could lose a\n * write on a crash in the commit window, defeating the whole point.\n */\nfunction awaitTransaction(tx: IDBTransaction): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () =>\n reject(tx.error ?? new Error('[smrt-web] IndexedDB transaction failed'));\n tx.onabort = () =>\n reject(tx.error ?? new Error('[smrt-web] IndexedDB transaction aborted'));\n });\n}\n\n/**\n * Feature-detect a usable IndexedDB. Some environments expose `indexedDB` but\n * throw on `open()` (private-mode Firefox historically, sandboxed iframes), so a\n * real probe opens a throwaway database. Returns `false` rather than throwing so\n * the engine can degrade to a no-op instead of crashing the host.\n */\nexport async function probeIndexedDb(): Promise<boolean> {\n const idb = (globalThis as { indexedDB?: IDBFactory }).indexedDB;\n if (!idb) return false;\n const probeName = '__smrt_web_outbox_probe__';\n try {\n const db = await new Promise<IDBDatabase>((resolve, reject) => {\n const request = idb.open(probeName, 1);\n request.onsuccess = () => resolve(request.result);\n request.onerror = () =>\n reject(request.error ?? new Error('probe failed'));\n request.onblocked = () => reject(new Error('probe blocked'));\n });\n db.close();\n // Best-effort cleanup; ignore failure.\n try {\n idb.deleteDatabase(probeName);\n } catch {\n /* ignore */\n }\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * The durable FIFO outbox queue over one IndexedDB database. Constructed via\n * {@link openDurableOutboxQueue} (async open + schema upgrade), then used\n * through the async methods below (each resolves once its txn commits).\n *\n * NOT concurrency-controlled across tabs on its own — the OutboxEngine layers\n * Web Locks leader election on top so exactly one tab drains it. Within one tab,\n * IndexedDB's own transaction serialization is sufficient.\n */\nexport class DurableOutboxQueue {\n private readonly db: IDBDatabase;\n /** The IndexedDB database name (== the durable-store namespace). */\n readonly dbName: string;\n\n constructor(db: IDBDatabase, dbName: string) {\n this.db = db;\n this.dbName = dbName;\n }\n\n /**\n * Append a mutation to the tail of the queue in state `pending`, due\n * immediately (`nextAttemptAt = 0`, `attempts = 0`). Resolves with the\n * assigned `seq` once the write is durably committed.\n */\n async enqueue(input: EnqueueInput): Promise<number> {\n const now = Date.now();\n const row: OutboxRow = {\n itemId: input.itemId,\n object: input.object,\n op: input.op,\n id: input.id,\n payload: input.payload,\n baseUpdatedAt: input.baseUpdatedAt,\n state: 'pending',\n attempts: 0,\n nextAttemptAt: 0,\n enqueuedAt: now,\n };\n const tx = this.db.transaction(OUTBOX_STORE, 'readwrite');\n const store = tx.objectStore(OUTBOX_STORE);\n const seq = await promisifyRequest(store.add(row));\n await awaitTransaction(tx);\n return seq as number;\n }\n\n /**\n * Persist a state transition (and any of attempts/backoff/error) for the row\n * at `seq`, reading-then-writing inside ONE transaction so a concurrent drain\n * in the same tab can't lose the update. A no-op if the row is already gone\n * (removed by a prior terminal transition).\n */\n async markState(\n seq: number,\n patch: Partial<\n Pick<OutboxRow, 'state' | 'attempts' | 'nextAttemptAt' | 'lastError'>\n >,\n ): Promise<void> {\n const tx = this.db.transaction(OUTBOX_STORE, 'readwrite');\n const store = tx.objectStore(OUTBOX_STORE);\n const existing = await promisifyRequest(\n store.get(seq) as IDBRequest<OutboxRow | undefined>,\n );\n if (!existing) {\n // Nothing to update; let the txn commit as a no-op.\n await awaitTransaction(tx);\n return;\n }\n const next: OutboxRow = { ...existing, ...patch, seq };\n await promisifyRequest(store.put(next));\n await awaitTransaction(tx);\n }\n\n /**\n * All rows that are due to (re)send at `now`: state `pending` AND\n * `nextAttemptAt <= now`, in ascending `seq` (FIFO). Uses the `state` index to\n * avoid scanning terminal tombstones. Terminal rows (`synced`/`failed`) are\n * excluded — they await removal, not replay.\n */\n async listPending(now: number): Promise<OutboxRow[]> {\n const tx = this.db.transaction(OUTBOX_STORE, 'readonly');\n const index = tx.objectStore(OUTBOX_STORE).index(OUTBOX_STATE_INDEX);\n const rows = await promisifyRequest(\n index.getAll(IDBKeyRange.only('pending')) as IDBRequest<OutboxRow[]>,\n );\n await awaitTransaction(tx);\n return rows\n .filter((row) => row.nextAttemptAt <= now)\n .sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));\n }\n\n /** Remove the row at `seq` (a terminal transition drops it). */\n async remove(seq: number): Promise<void> {\n const tx = this.db.transaction(OUTBOX_STORE, 'readwrite');\n await promisifyRequest(tx.objectStore(OUTBOX_STORE).delete(seq));\n await awaitTransaction(tx);\n }\n\n /** Every row currently in the queue (any state), ascending `seq`. */\n async all(): Promise<OutboxRow[]> {\n const tx = this.db.transaction(OUTBOX_STORE, 'readonly');\n const rows = await promisifyRequest(\n tx.objectStore(OUTBOX_STORE).getAll() as IDBRequest<OutboxRow[]>,\n );\n await awaitTransaction(tx);\n return rows.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));\n }\n\n /**\n * Drop every row — the durable-store `clear()` for `wipeDurableStore`. Empties\n * the store but keeps the database (and its `seq` autoincrement counter) so a\n * subsequent enqueue still gets fresh monotonic ids.\n */\n async clear(): Promise<void> {\n const tx = this.db.transaction(OUTBOX_STORE, 'readwrite');\n await promisifyRequest(tx.objectStore(OUTBOX_STORE).clear());\n await awaitTransaction(tx);\n }\n\n /** Close the underlying database handle (called on engine dispose). */\n close(): void {\n this.db.close();\n }\n}\n\n/**\n * Open (creating/upgrading as needed) the outbox database for `dbName` and wrap\n * it in a {@link DurableOutboxQueue}. `dbName` MUST be a\n * {@link durableStoreNamespace} string — already `encodeURIComponent`-safe and\n * carrying the api/tenant/identity/manifest isolation boundary, so two\n * identities never share one queue database.\n *\n * The `onupgradeneeded` handler creates the `outbox` store keyed by\n * autoincrementing `seq` and the `state` index. Idempotent: re-opening an\n * existing database at the same version skips the upgrade — which is exactly the\n * \"reload\" path (a new tab re-opens the same durable backing and sees prior\n * rows).\n */\nexport function openDurableOutboxQueue(\n dbName: string,\n): Promise<DurableOutboxQueue> {\n const idb = (globalThis as { indexedDB?: IDBFactory }).indexedDB;\n if (!idb) {\n return Promise.reject(\n new Error('[smrt-web] IndexedDB is unavailable in this environment'),\n );\n }\n return new Promise<DurableOutboxQueue>((resolve, reject) => {\n const request = idb.open(dbName, OUTBOX_DB_VERSION);\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(OUTBOX_STORE)) {\n const store = db.createObjectStore(OUTBOX_STORE, {\n keyPath: 'seq',\n autoIncrement: true,\n });\n store.createIndex(OUTBOX_STATE_INDEX, 'state', { unique: false });\n }\n };\n request.onsuccess = () =>\n resolve(new DurableOutboxQueue(request.result, dbName));\n request.onerror = () =>\n reject(\n request.error ??\n new Error(`[smrt-web] failed to open outbox database \"${dbName}\"`),\n );\n request.onblocked = () =>\n reject(\n new Error(`[smrt-web] opening outbox database \"${dbName}\" was blocked`),\n );\n });\n}\n","/**\n * @happyvertical/smrt-web — Web Locks leader election for the outbox (#1762).\n *\n * With multiple tabs open, exactly ONE must replay the shared outbox queue, or\n * two tabs would POST the same batch concurrently. The mechanism is the native\n * Web Locks API (`navigator.locks`): a tab requests an EXCLUSIVE lock keyed by\n * the durable-store namespace and holds it — via a promise that resolves only on\n * release — for as long as it should be leader. The browser grants the lock to\n * exactly one waiter at a time and AUTO-RELEASES it on tab crash/close/navigation\n * with no heartbeat, so the next waiting tab becomes leader instantly and there\n * is never a stuck lock.\n *\n * Why Web Locks over a BroadcastChannel election (v1 decision, locked): Web\n * Locks gives crash-safe, race-free, browser-arbitrated exclusion for free —\n * a hand-rolled BroadcastChannel election has to solve liveness (heartbeats,\n * takeover on a silent crash) itself. BroadcastChannel is a possible future\n * enhancement (cross-tab state fan-out), not the election primitive.\n *\n * ## Single-tab fallback (documented gap)\n *\n * When `navigator.locks` is absent (older Safari, non-browser hosts), this warns\n * ONCE and falls back to acquiring leadership immediately and unconditionally.\n * The outbox STILL replays — a lone tab is trivially the only replayer — but the\n * multi-tab exactly-one-replayer guarantee does not hold across tabs that all\n * took the fallback. This is a deliberate v1 limitation, NOT masked by a\n * BroadcastChannel shim.\n *\n * Engine-free: no `@tanstack/*` import — stays inside the boundary (#1761).\n */\n\n/**\n * A handle to relinquish leadership: call it to release the Web Lock (or cancel\n * a still-pending request). Idempotent.\n */\nexport type LeadershipHandle = () => void;\n\n/** Structural view of the Web Locks API surface this module uses. */\ninterface LockManagerLike {\n request(\n name: string,\n options: { signal?: AbortSignal; mode?: 'exclusive' | 'shared' },\n callback: () => Promise<unknown>,\n ): Promise<unknown>;\n}\n\n/** Detect the Web Locks API on the current `navigator`. */\nfunction getLockManager(): LockManagerLike | undefined {\n const nav = (globalThis as { navigator?: { locks?: unknown } }).navigator;\n const locks = nav?.locks as LockManagerLike | undefined;\n if (locks && typeof locks.request === 'function') return locks;\n return undefined;\n}\n\n/** Gate so the single-tab-fallback warning is logged at most once per session. */\nlet warnedNoLocks = false;\n\n/**\n * Acquire cross-tab leadership for `lockName` (the outbox's\n * `smrt-web-outbox-leader:<namespace>`).\n *\n * When the lock is granted this tab becomes leader and `onAcquired()` fires; the\n * tab stays leader — holding the lock — until the returned handle is called (or\n * the tab dies, when the browser auto-releases). On release, `onReleased()`\n * fires. Requesting is non-blocking: this returns synchronously with the\n * release handle while the request waits in the background for its turn.\n *\n * Contract:\n * - The lock is requested EXCLUSIVE; the callback returns a promise that stays\n * pending until the release handle resolves it, so the browser considers the\n * lock held for exactly that window.\n * - Calling the handle before the lock is even granted aborts the pending\n * request (via `AbortSignal`) so a torn-down engine never becomes leader\n * later. `onReleased` still fires so callers can settle their own state.\n * - No Web Locks support → single-tab fallback: `onAcquired()` fires on the next\n * microtask (leadership is immediate and unconditional), the handle just fires\n * `onReleased()`.\n */\nexport function acquireLeadership(\n lockName: string,\n onAcquired: () => void,\n onReleased: () => void,\n): LeadershipHandle {\n const locks = getLockManager();\n\n // --- Single-tab fallback: no Web Locks in this environment. ---\n if (!locks) {\n if (!warnedNoLocks) {\n warnedNoLocks = true;\n // biome-ignore lint/suspicious/noConsole: smrt-web has no logger dep (TanStack-only); the single-tab-fallback gap is surfaced via a one-time console.warn by design (#1762)\n console.warn(\n '[smrt-web] Web Locks API unavailable — the offline outbox falls back to single-tab leadership; the multi-tab exactly-one-replayer guarantee does not hold across tabs.',\n );\n }\n let released = false;\n const release = () => {\n if (released) return;\n released = true;\n onReleased();\n };\n // Grant leadership asynchronously so callers can wire state before it fires,\n // matching the Web-Locks path (never synchronous inside the constructor).\n queueMicrotask(() => {\n if (!released) onAcquired();\n });\n return release;\n }\n\n // --- Web Locks path. ---\n const controller = new AbortController();\n // The promise the lock callback returns; it resolves ONLY when we release, so\n // the browser holds the lock for exactly the leadership window.\n let releaseHeldLock: (() => void) | undefined;\n let released = false;\n let acquired = false;\n\n const release: LeadershipHandle = () => {\n if (released) return;\n released = true;\n if (acquired && releaseHeldLock) {\n // We hold the lock: resolve the callback's promise to release it.\n releaseHeldLock();\n } else {\n // Still waiting in the queue: abort the pending request so we never\n // become leader after teardown.\n controller.abort();\n }\n onReleased();\n };\n\n void locks\n .request(lockName, { signal: controller.signal, mode: 'exclusive' }, () => {\n // Granted — we are now leader. Hold the lock until release() resolves this.\n acquired = true;\n // If release() already ran while the request was pending, the abort above\n // fired instead and this callback never runs; guard anyway.\n if (released) return Promise.resolve();\n onAcquired();\n return new Promise<void>((resolve) => {\n releaseHeldLock = resolve;\n });\n })\n .catch((error: unknown) => {\n // An AbortError is the expected outcome when release() cancels a pending\n // request — not a fault. Any other rejection means the lock could not be\n // held; surface it so a lock subsystem problem is visible, and ensure\n // onReleased still fires so callers settle.\n const name = (error as { name?: string })?.name;\n if (name !== 'AbortError') {\n // biome-ignore lint/suspicious/noConsole: smrt-web has no logger dep; a lock-request failure is surfaced via console.warn (#1762)\n console.warn('[smrt-web] outbox leader lock request failed', error);\n }\n if (!released) {\n released = true;\n onReleased();\n }\n });\n\n return release;\n}\n","/**\n * @happyvertical/smrt-web — offline outbox shared types (#1762).\n *\n * The SMRT-owned vocabulary the outbox modules (`durable-queue`, `leader`,\n * `engine`) and the public surface (`offline.ts`) share. Kept in its own leaf\n * module — imported by everything, importing nothing from the package — so there\n * is no cycle back through `index.ts` and the whole outbox stays inside the\n * engine-absorption boundary (#1761): NOT ONE `@tanstack/*` type appears here.\n *\n * The sync-apply op/status/reason names mirror\n * `packages/core/src/sync/apply.ts` (the replay target) verbatim, so a queue row\n * maps to a batch item and a batch result maps to a state transition with no\n * translation layer. smrt-web has no inter-smrt dependency (dependency-DAG\n * guardrails), so these are re-declared here rather than imported from core.\n */\n\n/**\n * Mutation kinds the sync-apply batch endpoint accepts. Mirrors core's\n * `SyncApplyOp`. The outbox maps the capability seam's insert/update/delete\n * envelope kinds onto these (`insert → create`).\n */\nexport type SyncApplyOp = 'create' | 'update' | 'delete';\n\n/**\n * Per-item outcome status from a sync-apply batch result. Mirrors core's\n * `SyncApplyStatus`. The engine maps each onto a durable transition per the\n * contract's \"Web outbox (#1762)\" consumer notes.\n */\nexport type SyncApplyStatus = 'applied' | 'conflict' | 'rejected';\n\n/**\n * Machine-readable reasons a sync-apply item was `conflict` or `rejected`.\n * Mirrors core's `SyncApplyReason` union. Drives the engine's result mapping:\n * `auth_required`/`forbidden` pause the loop; `write_failed` retries; the rest\n * are terminal.\n */\nexport type SyncApplyReason =\n | 'invalid_item'\n | 'invalid_id'\n | 'invalid_payload'\n | 'unknown_object'\n | 'op_not_allowed'\n | 'auth_required'\n | 'forbidden'\n | 'not_found'\n | 'id_conflict'\n | 'write_failed'\n | 'stale_write'\n | 'create_conflict';\n\n/**\n * One item in a `POST {basePath}/sync/apply` request body. Mirrors core's\n * `SyncApplyItem`. Built from an {@link OutboxRow} 1:1.\n */\nexport interface SyncApplyItem {\n itemId: string;\n object: string;\n op: SyncApplyOp;\n id: string;\n payload?: Record<string, unknown>;\n baseUpdatedAt?: string;\n}\n\n/**\n * One entry in a sync-apply batch response's `results[]`. Mirrors core's\n * `SyncApplyItemResult`. `results[i]` corresponds to `items[i]` positionally;\n * `itemId` is echoed for convenience.\n */\nexport interface SyncApplyItemResult {\n itemId: string | null;\n id: string | null;\n status: SyncApplyStatus;\n reason?: SyncApplyReason;\n updatedAt?: string;\n}\n\n/** The HTTP-200 response body of `POST {basePath}/sync/apply`. */\nexport interface SyncApplyBatchResponse {\n results: SyncApplyItemResult[];\n}\n\n/**\n * Maximum items the endpoint accepts in one batch (core's\n * `MAX_SYNC_APPLY_BATCH_SIZE`). The engine chunks a larger backlog into\n * successive POSTs, oldest-first, so FIFO order is preserved across chunks.\n */\nexport const MAX_SYNC_APPLY_BATCH_SIZE = 1000;\n\n/**\n * The URL segments of the batch apply endpoint relative to the API base path:\n * `POST {basePath}/sync/apply` (core's `SYNC_APPLY_ROUTE_SEGMENTS`).\n */\nexport const SYNC_APPLY_ROUTE_SEGMENTS = ['sync', 'apply'] as const;\n\n/**\n * The app-observable sync state of a queued mutation — the SAME four-state\n * machine the KMP mobile foundation exposes (ADR 0001), so web and mobile\n * outbox indicators render identically:\n *\n * - `pending` — enqueued, awaiting (re)send.\n * - `uploading` — currently in a sync-apply POST.\n * - `synced` — the mutation's effect is confirmed on the server (a terminal\n * success — INCLUDING a surfaced conflict, which is a RESOLVED outcome: the\n * server state won and the item left the queue).\n * - `failed` — a terminal rejection the client cannot resolve by retrying\n * (`invalid_*`, `not_found`, `id_conflict`, `op_not_allowed`, …) — surfaced\n * for app-level handling.\n */\nexport type OutboxSyncState = 'pending' | 'uploading' | 'synced' | 'failed';\n\n/**\n * A sync-state transition delivered to {@link OfflineOutboxConfig.onSyncStateChange}.\n * A PUSH callback (not a subscribable store): smrt-svelte wraps it into a\n * reactive binding later (#1762 follow-on). Carries enough for an app to render\n * a per-row indicator and a retry affordance.\n */\nexport interface SyncStateEvent {\n /** The queue row's `itemId` (its idempotency handle) this state is for. */\n itemId: string;\n /** The client-generated row UUID the mutation targets. */\n rowId: string;\n /** The collection route segment (e.g. `products`). */\n object: string;\n /** The new observable state. */\n state: OutboxSyncState;\n /** How many replay attempts have run (0 until the first send). */\n attempts: number;\n /** The last replay error message, when `state` is `pending` after a retry. */\n error?: string;\n}\n\n/**\n * A conflict surfaced to {@link OfflineOutboxConfig.onConflict} when a replayed\n * item comes back `conflict`. Per the contract this is a RESOLVED outcome — the\n * server state won, the item is removed, and its terminal observable state is\n * `synced` — so an app treats it as \"your write was superseded; here is the\n * server's `updatedAt` to rebase from\", NOT as a failure to retry.\n */\nexport interface OutboxConflict {\n /** The queue row's `itemId`. */\n itemId: string;\n /** The collection route segment. */\n object: string;\n /** The client-generated row UUID that conflicted. */\n rowId: string;\n /**\n * Why the item conflicted: `stale_write` (an update/delete whose\n * `baseUpdatedAt` was older than the server row) or `create_conflict` (a\n * create landing on an existing, diverged row).\n */\n reason: 'stale_write' | 'create_conflict';\n /**\n * The server row's `updated_at` after processing — the value to persist as\n * the new `baseUpdatedAt` before re-editing. Present when the endpoint\n * returned it.\n */\n serverUpdatedAt?: string;\n}\n\n/** Exponential-backoff tuning for retryable replay failures. */\nexport interface OutboxBackoff {\n /** Delay before the first retry, ms (default 1000). */\n initialDelayMs?: number;\n /** Multiplier applied per attempt (default 2). */\n multiplier?: number;\n /** Ceiling on the computed delay, ms (default 60000). */\n maxDelayMs?: number;\n}\n\n/** Resolved backoff config (defaults filled in). */\nexport interface ResolvedBackoff {\n initialDelayMs: number;\n multiplier: number;\n maxDelayMs: number;\n}\n\n/** Backoff defaults, per the blueprint (initial=1s, ×2, cap 60s). */\nexport const DEFAULT_BACKOFF: ResolvedBackoff = {\n initialDelayMs: 1000,\n multiplier: 2,\n maxDelayMs: 60000,\n};\n\n/**\n * Compute the backoff delay before the NEXT attempt given how many attempts\n * have already been made. Exponential with a ceiling, then multiplied by a\n * jitter factor in `[0.5, 1.0)` to spread a thundering herd of tabs/rows\n * reconnecting at once: `min(maxDelay, initial * mult ** attempts) * jitter`.\n *\n * `attempts` is the count BEFORE this retry (so the first retry, after attempts\n * became 1, uses `initial * mult ** 1`? no — see below). We pass the attempt\n * count AFTER incrementing, and index the exponent off `attempts - 1` so the\n * first retry waits ~`initialDelayMs`. `random` is injectable for deterministic\n * tests.\n */\nexport function computeBackoffDelay(\n attempts: number,\n backoff: ResolvedBackoff,\n random: () => number = Math.random,\n): number {\n const exponent = Math.max(0, attempts - 1);\n const raw = backoff.initialDelayMs * backoff.multiplier ** exponent;\n const capped = Math.min(backoff.maxDelayMs, raw);\n // Jitter in [0.5, 1.0): full jitter's lower half, so a retry never waits\n // longer than the capped delay but is spread over half of it.\n const jitter = 0.5 + random() * 0.5;\n return Math.round(capped * jitter);\n}\n","/**\n * @happyvertical/smrt-web — the shared, namespace-keyed outbox engine (#1762).\n *\n * The engine owns the durable queue, the leader lock, and the replay loop for\n * ONE durable-store namespace, and is REF-COUNTED so that N collections sharing\n * a namespace share exactly ONE engine — one IndexedDB database, one leader\n * lock, one FIFO queue. This sharing is REQUIRED for correctness, not an\n * optimization: if each collection held its OWN leader lock, two tabs could each\n * win a different collection's lock and both replay the (shared) queue,\n * double-POSTing. One lock per namespace ⇒ one replayer per namespace across all\n * tabs.\n *\n * Replay maps sync-apply results onto durable transitions per the contract's\n * \"Web outbox (#1762)\" consumer notes\n * (docs/content/architecture/sync-apply-contract.md):\n *\n * | apply result | queue transition |\n * |-------------------------------------|----------------------------------------|\n * | `applied` | remove → `synced` |\n * | `conflict` (stale_write/create_conflict) | remove + fire onConflict → `synced` (a RESOLVED outcome) |\n * | `rejected` `write_failed` | keep, attempts++, backoff → `pending` |\n * | `rejected` `auth_required`/`forbidden` | PAUSE the loop, keep queued → `pending` |\n * | `rejected` other (`invalid_*`/`unknown_object`/`not_found`/`op_not_allowed`/`id_conflict`) | remove → `failed` (terminal) |\n * | network reject / non-200 / lost response | whole batch stays `pending`, drain stops behind it (blind replay is safe by construction — idempotency) |\n *\n * Because items carry client-generated UUIDs and the endpoint is idempotent\n * (`_insertOnly` create + no-op re-apply), a batch that was sent but whose\n * response was lost can be blindly re-sent with no duplicate rows — which is\n * why the network-failure path simply leaves the batch `pending`.\n *\n * Engine-free public surface: no `@tanstack/*` import — inside the boundary\n * (#1761). The one engine-adjacent value it receives is the SMRT-owned\n * durable-store namespace + registration hooks passed in `config`.\n */\n\nimport {\n type DurableOutboxQueue,\n type OutboxRow,\n openDurableOutboxQueue,\n probeIndexedDb,\n} from './durable-queue.js';\nimport { acquireLeadership, type LeadershipHandle } from './leader.js';\nimport {\n computeBackoffDelay,\n MAX_SYNC_APPLY_BATCH_SIZE,\n type OutboxConflict,\n type ResolvedBackoff,\n SYNC_APPLY_ROUTE_SEGMENTS,\n type SyncApplyBatchResponse,\n type SyncApplyItem,\n type SyncApplyItemResult,\n type SyncApplyOp,\n type SyncStateEvent,\n} from './types.js';\n\n/** Maps a capability-seam envelope kind to a sync-apply op. */\nexport function envelopeKindToOp(\n kind: 'insert' | 'update' | 'delete',\n): SyncApplyOp {\n return kind === 'insert' ? 'create' : kind;\n}\n\n/**\n * The bookkeeping a namespace's durable-store registration needs, supplied by\n * the public surface so the engine can key its queue and register for\n * {@link wipeDurableStore} WITHOUT importing `durable-store.ts` itself (keeping\n * this module's dependency surface minimal and the namespacing single-sourced in\n * the caller). `namespace` is the {@link durableStoreNamespace} string.\n */\nexport interface OutboxEngineConfig {\n /** The durable-store namespace string — the IDB dbName + lock-name root. */\n namespace: string;\n /** Absolute base path the sync-apply endpoint lives under (e.g. `/api/v1`). */\n syncApplyBasePath: string;\n /** Fetch implementation (injectable for tests/SSR). */\n fetchFn: typeof fetch;\n /** Resolved backoff parameters. */\n backoff: ResolvedBackoff;\n /**\n * Register this engine's queue as a durable resource so `wipeDurableStore`\n * can clear it; returns the unregister fn. Wraps `registerDurableResource`\n * from the caller so the engine stays decoupled from that module.\n */\n registerResource: (clear: () => Promise<void>) => () => void;\n /** Injectable RNG for deterministic backoff jitter in tests. */\n random?: () => number;\n}\n\n/**\n * A pending optimistic write to enqueue, in capability-seam terms. Callbacks are\n * NOT carried here — they are registered per-`object` at\n * {@link OutboxEngine.registerCollection} so that reloaded rows (which this\n * session never enqueued) still route their events to the collection.\n */\nexport interface OutboxEnqueueRequest {\n kind: 'insert' | 'update' | 'delete';\n /** The collection route segment (definition.name). */\n object: string;\n /** The client-generated row UUID (the optimistic row's id). */\n rowId: string;\n /** Full row (insert) / changed fields (update) / ignored (delete). */\n data: Record<string, unknown>;\n /** The server updated_at last seen for this row, for the conflict guard. */\n baseUpdatedAt?: string;\n}\n\n/** A read-only view of one queued item, for {@link OutboxEngine.snapshot}. */\nexport interface OutboxSnapshotItem {\n itemId: string;\n object: string;\n op: SyncApplyOp;\n rowId: string;\n state: 'pending' | 'synced' | 'failed';\n attempts: number;\n nextAttemptAt: number;\n lastError?: string;\n}\n\n/** Generate a fresh UUID itemId, falling back when crypto.randomUUID is absent. */\nfunction newItemId(): string {\n const cryptoRef = (globalThis as { crypto?: Crypto }).crypto;\n if (cryptoRef?.randomUUID) return cryptoRef.randomUUID();\n return `item-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n\n/** Is `navigator.onLine` telling us we're definitely offline? */\nfunction isDefinitelyOffline(): boolean {\n const nav = (globalThis as { navigator?: { onLine?: boolean } }).navigator;\n return nav?.onLine === false;\n}\n\n/**\n * The shared per-namespace outbox engine. Create/lookup via\n * {@link getOrCreateOutboxEngine}; never construct directly (the module keeps\n * the ref-counted registry).\n */\nexport class OutboxEngine {\n private readonly config: OutboxEngineConfig;\n /**\n * Per-collection callback sets, keyed by the collection route segment\n * (`object`). Keyed by `object` — NOT by the queue row's `itemId` — precisely\n * so replayed rows that were REHYDRATED from IndexedDB after a reload (whose\n * itemIds this session never enqueued) still route their state/conflict events\n * to the reloaded collection's callbacks. A `Set` per object so N collections\n * sharing one engine+object each get every event (the common case is one\n * collection per object, but the shared-engine model does not forbid more).\n */\n private readonly listenersByObject = new Map<\n string,\n Set<{\n onSyncStateChange?: (event: SyncStateEvent) => void;\n onConflict?: (conflict: OutboxConflict) => void;\n }>\n >();\n\n /** Ref count: number of collections currently attached to this engine. */\n private refCount = 0;\n /** The durable queue, once opened. undefined while opening / if IDB absent. */\n private queue: DurableOutboxQueue | undefined;\n /** Resolves once the async open settles (success or degraded). */\n private readonly ready: Promise<void>;\n /** True when IndexedDB was unavailable and the engine is a durable no-op. */\n private degraded = false;\n /** Leadership handle; set once we've requested the leader lock. */\n private leadership: LeadershipHandle | undefined;\n /** True while this tab holds leadership. */\n private isLeader = false;\n /** Unregister fn from the durable-store registry. */\n private unregisterResource: (() => void) | undefined;\n /** True once dispose() ran — guards late async continuations. */\n private disposed = false;\n /**\n * Paused by an auth_required/forbidden result: the loop stops draining until\n * a later enqueue (the app re-authenticated and is writing again) or an\n * explicit retry wakes it. Items stay queued.\n */\n private paused = false;\n /** True while a drain pass is running, to coalesce concurrent triggers. */\n private draining = false;\n /** A drain requested while one was in flight — run one more pass after. */\n private drainQueued = false;\n /** Timer for the next backoff-scheduled drain, if any. */\n private backoffTimer: ReturnType<typeof setTimeout> | undefined;\n /** The `online` event listener, so we can remove it on dispose. */\n private onlineListener: (() => void) | undefined;\n\n constructor(config: OutboxEngineConfig) {\n this.config = config;\n this.ready = this.open();\n this.wireOnlineListener();\n this.requestLeadership();\n // Kick a drain once the queue has finished opening. Leadership can be\n // granted (single-tab fallback: a microtask; Web Locks: whenever the lock\n // frees) BEFORE the async `open()` resolves — in which case that early\n // `drain()` returned at the `!this.queue` guard and nothing re-triggered it.\n // Draining after `ready` closes that race, so a freshly-constructed engine\n // that is already leader with a backlog on disk (the reload / leader-handoff\n // paths) replays without waiting for an external event.\n void this.ready.then(() => {\n if (!this.disposed) void this.drain();\n });\n }\n\n /** Open the durable queue (or mark degraded if IndexedDB is unusable). */\n private async open(): Promise<void> {\n const usable = await probeIndexedDb();\n if (!usable) {\n this.degraded = true;\n // biome-ignore lint/suspicious/noConsole: smrt-web has no logger dep; a degraded (no-IndexedDB) outbox is surfaced via console.warn (#1762)\n console.warn(\n '[smrt-web] IndexedDB unavailable — the offline outbox is disabled; offline writes will not be durable.',\n );\n return;\n }\n try {\n this.queue = await openDurableOutboxQueue(this.config.namespace);\n // Register for wipeDurableStore now that the queue exists.\n this.unregisterResource = this.config.registerResource(async () => {\n // A wipe clears the durable rows; drop the in-flight schedule too.\n await this.queue?.clear();\n });\n if (this.disposed) {\n // Disposed while opening — tear the just-opened queue back down.\n this.queue.close();\n this.queue = undefined;\n this.unregisterResource?.();\n this.unregisterResource = undefined;\n return;\n }\n } catch (error) {\n this.degraded = true;\n // biome-ignore lint/suspicious/noConsole: surface an outbox open failure (#1762)\n console.warn('[smrt-web] failed to open the offline outbox', error);\n }\n }\n\n /** Wake the drain loop immediately when connectivity returns. */\n private wireOnlineListener(): void {\n const target = globalThis as {\n addEventListener?: (t: string, l: () => void) => void;\n };\n if (typeof target.addEventListener !== 'function') return;\n const listener = () => {\n // Reconnected: an auth pause is unrelated to connectivity, so leave\n // `paused` as-is, but a network-stalled backlog should retry now.\n void this.drain();\n };\n target.addEventListener('online', listener);\n this.onlineListener = listener;\n }\n\n /** Request cross-tab leadership; drain whenever we hold it. */\n private requestLeadership(): void {\n const lockName = `smrt-web-outbox-leader:${this.config.namespace}`;\n this.leadership = acquireLeadership(\n lockName,\n () => {\n this.isLeader = true;\n void this.drain();\n },\n () => {\n this.isLeader = false;\n },\n );\n }\n\n /**\n * Attach a collection: register its per-object callbacks and bump the ref\n * count. Returns the exact callback record registered so the caller can pass\n * it back to {@link unregisterCollection} for precise removal (two collections\n * on the same object must each detach only their own callbacks). Registering\n * by `object` is what lets rehydrated rows (reloaded from IDB) reach this\n * collection's callbacks even though this session never enqueued them.\n */\n registerCollection(binding: {\n object: string;\n onSyncStateChange?: (event: SyncStateEvent) => void;\n onConflict?: (conflict: OutboxConflict) => void;\n }): {\n onSyncStateChange?: (event: SyncStateEvent) => void;\n onConflict?: (conflict: OutboxConflict) => void;\n } {\n this.refCount += 1;\n const record = {\n onSyncStateChange: binding.onSyncStateChange,\n onConflict: binding.onConflict,\n };\n let set = this.listenersByObject.get(binding.object);\n if (!set) {\n set = new Set();\n this.listenersByObject.set(binding.object, set);\n }\n set.add(record);\n return record;\n }\n\n /**\n * Detach a collection: remove its callback record and decrement the ref count;\n * when it reaches zero, dispose the engine (release the lock, unregister from\n * the durable-store registry, close IndexedDB). The durable ROWS are NOT\n * cleared — they must survive to replay after a reload; only the in-memory\n * engine is torn down. Returns true if it disposed.\n */\n async unregisterCollection(object: string, record: object): Promise<boolean> {\n const set = this.listenersByObject.get(object);\n if (set) {\n set.delete(record as never);\n if (set.size === 0) this.listenersByObject.delete(object);\n }\n this.refCount = Math.max(0, this.refCount - 1);\n if (this.refCount > 0) return false;\n await this.dispose();\n return true;\n }\n\n /** Current ref count (test/introspection aid). */\n get referenceCount(): number {\n return this.refCount;\n }\n\n /**\n * Enqueue an optimistic write into the durable queue and fire the initial\n * `pending` state, then kick a drain. Resolves once the row is durably\n * committed (so the caller's `wrapMutation` only reports handled after\n * persistence). Replay events for this row (and for rows this session did not\n * enqueue — reloaded from disk) route to the registered per-`object`\n * callbacks, so a reload does not lose observability.\n *\n * In degraded (no-IndexedDB) mode the write is NOT durable, so this returns\n * `undefined` and the capability falls through to the real fetcher instead\n * of acknowledging an optimistic-only write.\n */\n async enqueue(request: OutboxEnqueueRequest): Promise<string | undefined> {\n await this.ready;\n if (!this.queue || this.degraded) return undefined;\n\n const itemId = newItemId();\n const op = envelopeKindToOp(request.kind);\n\n // A delete carries no payload; create/update carry the row/changed fields.\n const payload = op === 'delete' ? undefined : request.data;\n\n await this.queue.enqueue({\n itemId,\n object: request.object,\n op,\n id: request.rowId,\n payload,\n baseUpdatedAt: request.baseUpdatedAt,\n });\n\n this.emit({\n itemId,\n rowId: request.rowId,\n object: request.object,\n state: 'pending',\n attempts: 0,\n });\n\n // A fresh write means there's work; if an auth pause was in effect the app\n // is evidently active again, so clear it and try.\n this.paused = false;\n void this.drain();\n return itemId;\n }\n\n /**\n * Force a retry of a specific queued item now: clears its backoff gate and\n * wakes the loop. The bridge `OutboxHandle.retry(itemId)` calls this so an app\n * \"retry\" button can flush a backed-off or auth-paused item. A no-op for an\n * item that is not (or no longer) queued.\n */\n async retry(itemId: string): Promise<void> {\n await this.ready;\n if (!this.queue) return;\n const rows = await this.queue.all();\n const row = rows.find((r) => r.itemId === itemId && r.state === 'pending');\n if (!row || row.seq === undefined) return;\n await this.queue.markState(row.seq, { nextAttemptAt: 0 });\n this.paused = false;\n void this.drain();\n }\n\n /**\n * A read-only snapshot of the durable queue — the basis of\n * `OutboxHandle.snapshot()`. Because the READ cache is NOT rehydrated after a\n * reload in this slice (that's #1764's warmStart), the snapshot + the raw IDB\n * store are how a test/app proves durability, not `collection.toArray()`.\n */\n async snapshot(): Promise<OutboxSnapshotItem[]> {\n await this.ready;\n if (!this.queue) return [];\n const rows = await this.queue.all();\n return rows.map((row) => ({\n itemId: row.itemId,\n object: row.object,\n op: row.op,\n rowId: row.id,\n state: row.state,\n attempts: row.attempts,\n nextAttemptAt: row.nextAttemptAt,\n lastError: row.lastError,\n }));\n }\n\n /**\n * Deliver a state event to every callback registered for the event's\n * collection `object` (best-effort). Routing by `object` — not `itemId` —\n * means a row REHYDRATED from IndexedDB after a reload still reaches the\n * reloaded collection's callback even though this session never enqueued it.\n */\n private emit(event: SyncStateEvent): void {\n const set = this.listenersByObject.get(event.object);\n if (!set) return;\n for (const listener of set) {\n try {\n listener.onSyncStateChange?.(event);\n } catch (error) {\n // biome-ignore lint/suspicious/noConsole: a throwing app callback must not break the loop (#1762)\n console.warn('[smrt-web] onSyncStateChange callback threw', error);\n }\n }\n }\n\n /** Deliver a conflict to every callback registered for its collection. */\n private emitConflict(conflict: OutboxConflict): void {\n const set = this.listenersByObject.get(conflict.object);\n if (!set) return;\n for (const listener of set) {\n try {\n listener.onConflict?.(conflict);\n } catch (error) {\n // biome-ignore lint/suspicious/noConsole: a throwing app callback must not break the loop (#1762)\n console.warn('[smrt-web] onConflict callback threw', error);\n }\n }\n }\n\n /**\n * The replay loop. Gated on: (a) holding leadership, (b) not paused by an\n * auth failure, (c) `navigator.onLine !== false`, (d) IndexedDB usable. Drains\n * all rows due now (`nextAttemptAt <= now`), oldest-first, chunked into\n * batches of ≤1000 per POST, one POST at a time to preserve FIFO across\n * chunks. Concurrency-coalesced: a drain requested while one runs sets a flag\n * to run exactly one more pass, so overlapping triggers never interleave.\n */\n private async drain(): Promise<void> {\n if (this.draining) {\n this.drainQueued = true;\n return;\n }\n this.draining = true;\n try {\n // Loop so a queued re-request (or a freshly-eligible backoff row) runs\n // without re-entrancy.\n for (;;) {\n this.drainQueued = false;\n await this.drainOnce();\n if (!this.drainQueued) break;\n }\n } finally {\n this.draining = false;\n }\n }\n\n /** One drain pass: send every currently-due batch, then schedule backoff. */\n private async drainOnce(): Promise<void> {\n if (this.disposed) return;\n if (!this.isLeader) return;\n if (this.paused) return;\n if (this.degraded || !this.queue) return;\n if (isDefinitelyOffline()) return;\n\n const pending = (await this.queue.all()).filter(\n (row) => row.state === 'pending',\n );\n if (pending.length === 0) return;\n\n const now = Date.now();\n const firstBlocked = pending.findIndex((row) => row.nextAttemptAt > now);\n const due = firstBlocked === -1 ? pending : pending.slice(0, firstBlocked);\n if (due.length === 0) {\n // The oldest pending row is backed off; FIFO forbids draining newer rows.\n await this.scheduleNextBackoff();\n return;\n }\n\n // Chunk oldest-first into ≤1000-item batches; send one at a time so FIFO\n // holds across chunks.\n for (let i = 0; i < due.length; i += MAX_SYNC_APPLY_BATCH_SIZE) {\n if (this.disposed || this.paused || !this.isLeader) break;\n const chunk = due.slice(i, i + MAX_SYNC_APPLY_BATCH_SIZE);\n const drained = await this.sendBatch(chunk);\n if (!drained) break;\n }\n\n // After processing, some rows may have been re-queued with a backoff gate;\n // schedule the next wake.\n await this.scheduleNextBackoff();\n }\n\n /**\n * Send one chunk through `POST {basePath}/sync/apply` and map results back\n * onto durable transitions. On a network reject / non-200 / lost/mismatched\n * response the WHOLE chunk stays `pending` (blind replay is safe) — every row\n * goes back to `pending` with an incremented attempt + backoff so the loop\n * doesn't hot-spin. Returns false when a retryable row remains pending, which\n * stops this drain pass so newer FIFO chunks do not overtake it.\n */\n private async sendBatch(chunk: OutboxRow[]): Promise<boolean> {\n // Mark the chunk uploading (observable), build the request items in order.\n for (const row of chunk) {\n this.emit({\n itemId: row.itemId,\n rowId: row.id,\n object: row.object,\n state: 'uploading',\n attempts: row.attempts,\n });\n }\n const items: SyncApplyItem[] = chunk.map((row) => ({\n itemId: row.itemId,\n object: row.object,\n op: row.op,\n id: row.id,\n payload: row.payload,\n baseUpdatedAt: row.baseUpdatedAt,\n }));\n\n let results: SyncApplyItemResult[] | undefined;\n try {\n results = await this.postBatch(items);\n } catch {\n // Network reject / non-200 / lost response: keep the whole batch pending.\n await this.requeueBatch(chunk, 'network error during sync');\n return false;\n }\n if (!results) {\n await this.requeueBatch(chunk, 'unexpected sync response shape');\n return false;\n }\n\n // Results are positional (results[i] ↔ items[i] ↔ chunk[i]). Map each.\n let drained = true;\n for (let i = 0; i < chunk.length; i += 1) {\n const row = chunk[i];\n const result = results[i];\n // A missing/short result array for this position → treat as retryable\n // (leave pending); safer than dropping the item.\n if (!result) {\n await this.requeueRow(row, 'missing result for item');\n drained = false;\n continue;\n }\n const applied = await this.applyResult(row, result);\n drained = drained && applied;\n }\n return drained;\n }\n\n /**\n * POST a batch to `{basePath}/sync/apply`. Throws on a non-2xx or a network\n * error (the caller treats a throw as \"response lost → keep pending\"). Returns\n * the positional `results` array, or `undefined` on a malformed 200 body.\n */\n private async postBatch(\n items: SyncApplyItem[],\n ): Promise<SyncApplyItemResult[] | undefined> {\n const url = `${this.config.syncApplyBasePath}/${SYNC_APPLY_ROUTE_SEGMENTS.join('/')}`;\n const response = await this.config.fetchFn(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ items }),\n });\n if (!response.ok) {\n // HTTP 400 (bad batch) and any other non-2xx: throw so the batch stays\n // pending. A 400 on a well-formed client batch is unexpected; blind\n // replay stays safe by construction, so retrying is acceptable.\n throw new Error(`[smrt-web] sync/apply returned HTTP ${response.status}`);\n }\n const body = (await response\n .json()\n .catch(() => null)) as SyncApplyBatchResponse | null;\n if (!body || !Array.isArray(body.results)) return undefined;\n return body.results;\n }\n\n /**\n * Map one positional apply result onto a durable transition + observable\n * state, per the contract's consumer notes. See the class doc's mapping table.\n */\n private async applyResult(\n row: OutboxRow,\n result: SyncApplyItemResult,\n ): Promise<boolean> {\n if (row.seq === undefined) return true;\n\n if (result.status === 'applied') {\n await this.finishSynced(row);\n return true;\n }\n\n if (result.status === 'conflict') {\n // A conflict is a RESOLVED outcome: the server state won, the item leaves\n // the queue, its terminal observable state is `synced`, and the app is\n // notified so it can rebase from the returned updatedAt.\n const reason =\n result.reason === 'create_conflict' ? 'create_conflict' : 'stale_write';\n this.emitConflict({\n itemId: row.itemId,\n object: row.object,\n rowId: row.id,\n reason,\n serverUpdatedAt: result.updatedAt,\n });\n await this.finishSynced(row);\n return true;\n }\n\n // status === 'rejected'\n const reason = result.reason;\n if (reason === 'auth_required' || reason === 'forbidden') {\n // Pause the WHOLE loop until re-auth; keep the item queued as `pending`.\n this.paused = true;\n await this.queue?.markState(row.seq, {\n state: 'pending',\n lastError: `sync ${reason}`,\n });\n this.emit({\n itemId: row.itemId,\n rowId: row.id,\n object: row.object,\n state: 'pending',\n attempts: row.attempts,\n error: `sync ${reason}`,\n });\n return false;\n }\n\n if (reason === 'write_failed') {\n // Retryable: keep, count an attempt, back off.\n await this.requeueRow(row, 'sync write_failed');\n return false;\n }\n\n // Any other rejection (invalid_item/invalid_id/invalid_payload/\n // unknown_object/not_found/op_not_allowed/id_conflict) is terminal — a\n // retry cannot succeed. Remove and surface `failed`.\n await this.queue?.remove(row.seq);\n this.emit({\n itemId: row.itemId,\n rowId: row.id,\n object: row.object,\n state: 'failed',\n attempts: row.attempts,\n error: reason ? `sync ${reason}` : 'sync rejected',\n });\n return true;\n }\n\n /** Remove a successfully-applied (or conflict-resolved) row → `synced`. */\n private async finishSynced(row: OutboxRow): Promise<void> {\n if (row.seq !== undefined) await this.queue?.remove(row.seq);\n this.emit({\n itemId: row.itemId,\n rowId: row.id,\n object: row.object,\n state: 'synced',\n attempts: row.attempts,\n });\n }\n\n /** Re-queue every row of a failed batch (network path) with backoff. */\n private async requeueBatch(chunk: OutboxRow[], error: string): Promise<void> {\n for (const row of chunk) {\n await this.requeueRow(row, error);\n }\n }\n\n /** Re-queue one row: attempts++, backoff gate, `pending` event. */\n private async requeueRow(row: OutboxRow, error: string): Promise<void> {\n if (row.seq === undefined) return;\n const attempts = row.attempts + 1;\n const delay = computeBackoffDelay(\n attempts,\n this.config.backoff,\n this.config.random,\n );\n const nextAttemptAt = Date.now() + delay;\n await this.queue?.markState(row.seq, {\n state: 'pending',\n attempts,\n nextAttemptAt,\n lastError: error,\n });\n this.emit({\n itemId: row.itemId,\n rowId: row.id,\n object: row.object,\n state: 'pending',\n attempts,\n error,\n });\n }\n\n /**\n * Schedule the next drain for the soonest backed-off row's `nextAttemptAt`.\n * Only one timer is ever pending; a sooner schedule replaces a later one.\n */\n private async scheduleNextBackoff(): Promise<void> {\n if (this.disposed || this.paused || !this.queue) return;\n const rows = await this.queue.all();\n const firstPending = rows.find((r) => r.state === 'pending');\n if (!firstPending) return;\n const now = Date.now();\n // FIFO: the oldest pending row gates every newer row, even if a newer row\n // has no backoff delay.\n const delay = Math.max(0, firstPending.nextAttemptAt - now);\n if (this.backoffTimer) clearTimeout(this.backoffTimer);\n const timers = globalThis as {\n setTimeout?: typeof setTimeout;\n };\n if (typeof timers.setTimeout !== 'function') return;\n this.backoffTimer = timers.setTimeout(() => {\n this.backoffTimer = undefined;\n void this.drain();\n }, delay);\n // Node's timer keeps the process alive; unref so tests/SSR don't hang.\n (this.backoffTimer as { unref?: () => void }).unref?.();\n }\n\n /**\n * Tear down the in-memory engine: release leadership, remove the online\n * listener, clear timers, unregister from the durable-store registry, and\n * close IndexedDB. Does NOT clear the durable rows — they must survive to\n * replay on the next load.\n */\n private async dispose(): Promise<void> {\n if (this.disposed) return;\n this.disposed = true;\n if (this.backoffTimer) {\n clearTimeout(this.backoffTimer);\n this.backoffTimer = undefined;\n }\n const target = globalThis as {\n removeEventListener?: (t: string, l: () => void) => void;\n };\n if (\n this.onlineListener &&\n typeof target.removeEventListener === 'function'\n ) {\n target.removeEventListener('online', this.onlineListener);\n this.onlineListener = undefined;\n }\n this.leadership?.();\n this.leadership = undefined;\n this.unregisterResource?.();\n this.unregisterResource = undefined;\n // Wait for any in-flight open to settle before closing.\n await this.ready.catch(() => undefined);\n this.queue?.close();\n this.queue = undefined;\n this.listenersByObject.clear();\n }\n}\n\n/**\n * Module-scoped, ref-counted registry of engines by namespace string. This is\n * the mechanism that makes N collections sharing a namespace share ONE engine.\n * Keyed by the durable-store namespace, which already folds\n * api/tenant/identity/manifest — so a logout/tenant-switch lands on a different\n * key and a fresh engine.\n */\nconst engines = new Map<string, OutboxEngine>();\n\n/**\n * Get the shared engine for `config.namespace`, creating it on first use and\n * ref-counting it. Every collection opting into the outbox under the same\n * namespace gets the SAME engine — one IndexedDB db, one leader lock, one FIFO\n * queue. The caller MUST pair each `getOrCreateOutboxEngine(...).registerCollection()`\n * with a later `unregisterCollection()` (via `teardown`) so the engine disposes\n * when its last collection detaches.\n */\nexport function getOrCreateOutboxEngine(\n config: OutboxEngineConfig,\n): OutboxEngine {\n let engine = engines.get(config.namespace);\n if (!engine) {\n engine = new OutboxEngine(config);\n engines.set(config.namespace, engine);\n // Auto-evict from the shared registry once the engine disposes, so a later\n // collection under the same namespace gets a fresh engine rather than a\n // torn-down one. We detect disposal by wrapping unregisterCollection at the\n // call site (below in registerCollection/unregisterCollection helpers).\n }\n return engine;\n}\n\n/** The per-collection callback binding registered when a collection attaches. */\nexport interface OutboxCollectionBinding {\n object: string;\n onSyncStateChange?: (event: SyncStateEvent) => void;\n onConflict?: (conflict: OutboxConflict) => void;\n}\n\n/**\n * Attach a collection to the namespace's engine (creating it if needed),\n * register its per-`object` callbacks, and increment its ref count. Returns the\n * engine plus the exact callback `record` to hand back to\n * {@link releaseOutboxEngine} for precise removal. Pair with\n * {@link releaseOutboxEngine}.\n */\nexport function acquireOutboxEngine(\n config: OutboxEngineConfig,\n binding: OutboxCollectionBinding,\n): { engine: OutboxEngine; record: object } {\n const engine = getOrCreateOutboxEngine(config);\n const record = engine.registerCollection(binding);\n return { engine, record };\n}\n\n/**\n * Detach a collection from an engine (removing its callback record) and, if it\n * was the last one, dispose it and evict it from the shared registry so the\n * namespace starts fresh next time.\n */\nexport async function releaseOutboxEngine(\n namespace: string,\n engine: OutboxEngine,\n object: string,\n record: object,\n): Promise<void> {\n const disposed = await engine.unregisterCollection(object, record);\n if (disposed && engines.get(namespace) === engine) {\n engines.delete(namespace);\n }\n}\n\n/** Test-only: current engine count (for leak assertions). */\nexport function _outboxEngineCount(): number {\n return engines.size;\n}\n","/**\n * @happyvertical/smrt-web — the durable offline outbox capability (#1762).\n *\n * The web twin of the KMP mobile write queue: mutations against opted-in\n * collections are captured in a durable browser-side outbox (IndexedDB) that\n * survives reloads and crashes, then replayed FIFO against the idempotent\n * sync-apply batch contract when connectivity returns, with exponential-backoff\n * retries. Because items carry client-generated UUIDs and the endpoint is\n * idempotent (`_insertOnly` create + no-op re-apply), blind retries after an\n * ambiguous failure (request sent, response lost) can never duplicate rows — see\n * the \"Idempotency\" section of docs/content/architecture/sync-apply-contract.md.\n *\n * This is a {@link SmrtWebCapability} plug-in, so it lives in its OWN module and\n * an app opts a collection in by passing `offlineOutbox(config)` in that\n * collection's `capabilities` array. A collection WITHOUT it is byte-for-byte\n * the collection of today (the seam's no-op guarantee), which is exactly the\n * \"offline is opt-in per model\" acceptance criterion.\n *\n * ## How it plugs into the seam\n *\n * - `wrapMutation(envelope, ctx)` — BEFORE the fetcher, it enqueues the write\n * durably and returns `{ handled: true, result: envelope.data }`. The factory\n * then SUPPRESSES the post-mutation refetch + `invalidateRelated()` (its\n * `{ refetch: false }` path), so the optimistic row the app just inserted\n * STANDS instead of being dropped by a refetch of a server list that has never\n * seen the offline write. If the durable queue is unavailable, it returns\n * `{ handled: false }` so the normal fetcher path runs instead of silently\n * acknowledging a non-durable write. A handled write never touches\n * `ctx.fetchers.create` — it replays ONLY through `sync/apply`, which is\n * load-bearing: the normal REST create strips the client id (#1540) and would\n * mint a NEW server id, orphaning the optimistic row; sync/apply's\n * strict-insert path preserves the client UUID (#1540, #1540's\n * mass-assignment guard on the sync path).\n * - `onAttach(ctx)` — attaches this collection to the shared, namespace-keyed\n * {@link OutboxEngine} (ref-counted so N collections share ONE engine / IDB\n * db / leader lock / FIFO queue).\n * - `teardown(ctx)` — detaches; the last detach disposes the engine (the durable\n * ROWS survive for the next load).\n *\n * ## Observable state (push callbacks, not a store)\n *\n * Per-mutation sync state — `pending → uploading → synced` (and `→ failed` /\n * conflict) — is delivered through {@link OfflineOutboxConfig.onSyncStateChange}\n * and {@link OfflineOutboxConfig.onConflict}, PUSH callbacks matching the KMP\n * foundation's `pending/uploading/synced/failed` state machine. This is\n * deliberately not a subscribable store: smrt-svelte wraps it into a reactive\n * binding in a later slice. Programmatic access to the durable queue is via\n * {@link getOutboxHandle} (a bridge like `getEngineCollection`).\n *\n * ## Known gap (this slice's scope)\n *\n * The outbox does NOT rehydrate the READ cache after a reload — a reloaded tab's\n * `collection.toArray()` will NOT show captured-offline rows until a fetch runs;\n * that read-side rehydrate is #1764's `warmStart`. So the outbox's durability is\n * proven via {@link OutboxHandle.snapshot} / the raw IndexedDB store, NOT via\n * `collection.toArray()`. The WRITE side (capture → durable → exactly-once\n * replay) is complete here.\n *\n * Engine-free public surface: no `@tanstack/*` type appears — inside the\n * engine-absorption boundary (#1761), verified by\n * scripts/check-smrt-web-engine-boundary.mjs.\n */\n\nimport type { SmrtWebCapability } from './capability.js';\nimport {\n type DurableStoreKey,\n durableStoreNamespace,\n registerDurableResource,\n} from './durable-store.js';\nimport {\n acquireOutboxEngine,\n type OutboxEngine,\n releaseOutboxEngine,\n} from './offline/engine.js';\nimport {\n DEFAULT_BACKOFF,\n type OutboxBackoff,\n type OutboxConflict,\n type ResolvedBackoff,\n type SyncStateEvent,\n} from './offline/types.js';\n\nexport type {\n OutboxBackoff,\n OutboxConflict,\n OutboxSyncState,\n SyncStateEvent,\n} from './offline/types.js';\n\n/**\n * Configuration for {@link offlineOutbox}. Generic in the collection's row type\n * `TData` so the capability matches the collection it plugs into.\n */\nexport interface OfflineOutboxConfig<TData extends object = object> {\n /**\n * The generated collection definition this outbox serves — its `name` is the\n * sync-apply `object` route segment, so it MUST be the SAME definition passed\n * to {@link createSmrtCollection}. (Named `object` to mirror the capability\n * context; carries the collection's REST route segment.)\n */\n object: { name: string; _row?: TData };\n /**\n * The durable-store identity this outbox's queue is namespaced under — folds\n * api base / tenant / identity / manifest hash, so a logout or tenant switch\n * lands on a different IndexedDB database (never cross-identity reuse). Shared\n * with #1764's persistence slice via the same {@link durableStoreNamespace}.\n * `manifestHash` is opaque caller-supplied config for #1762; its canonical\n * source is #1764's call (a schema-shape digest) — comment your call site.\n */\n namespace: DurableStoreKey;\n /**\n * API base path the sync-apply endpoint lives under (`POST\n * {syncApplyBasePath}/sync/apply`). Defaults to `/api/v1`, matching the REST\n * generator's default. Set to the SvelteKit route base (`/api`) when replaying\n * against the generated SvelteKit `sync/apply/+server.ts`.\n */\n syncApplyBasePath?: string;\n /** Fetch implementation override (tests, SSR). Defaults to global fetch. */\n fetchFn?: typeof fetch;\n /** Exponential-backoff tuning for retryable replay failures. */\n backoff?: OutboxBackoff;\n /**\n * Called on every observable sync-state transition of a captured mutation\n * (`pending → uploading → synced`, `→ failed`, and the `pending` re-arm after\n * a retryable failure). A push callback; smrt-svelte turns it into a reactive\n * binding later.\n */\n onSyncStateChange?: (event: SyncStateEvent) => void;\n /**\n * Called when a replayed mutation comes back `conflict` — a RESOLVED outcome\n * (server state won; the item leaves the queue with terminal state `synced`).\n * Persist `serverUpdatedAt` as the new `baseUpdatedAt`, refetch the row, and\n * surface the conflict to the user as appropriate.\n */\n onConflict?: (conflict: OutboxConflict) => void;\n /** Test-only: inject a deterministic RNG for backoff jitter. */\n random?: () => number;\n}\n\n/**\n * A programmatic handle to one namespace's outbox — the bridge pattern (like\n * `getEngineCollection`) for trusted callers (an app's outbox panel, a\n * smrt-svelte binding, tests). Fetched via {@link getOutboxHandle}.\n */\nexport interface OutboxHandle {\n /**\n * A read-only snapshot of the durable queue (every state). The way to prove\n * durability after a reload, since this slice does not rehydrate the read\n * cache (that's #1764).\n */\n snapshot(): Promise<OutboxSnapshotItem[]>;\n /**\n * Force a retry of a specific queued item now (clears its backoff gate, wakes\n * the loop, and clears an auth pause). A no-op for an item no longer queued.\n */\n retry(itemId: string): Promise<void>;\n}\n\n/** One item in an {@link OutboxHandle.snapshot} result. */\nexport interface OutboxSnapshotItem {\n /** The queue row's idempotency handle. */\n itemId: string;\n /** The collection route segment. */\n object: string;\n /** The mutation kind, in sync-apply terms. */\n op: 'create' | 'update' | 'delete';\n /** The client-generated row UUID. */\n rowId: string;\n /** The on-disk lifecycle state. */\n state: 'pending' | 'synced' | 'failed';\n /** Replay attempts so far. */\n attempts: number;\n /** Epoch ms before which this row won't be retried (backoff). */\n nextAttemptAt: number;\n /** Last replay error, if any. */\n lastError?: string;\n}\n\n/** Fill backoff defaults. */\nfunction resolveBackoff(backoff?: OutboxBackoff): ResolvedBackoff {\n return {\n initialDelayMs: backoff?.initialDelayMs ?? DEFAULT_BACKOFF.initialDelayMs,\n multiplier: backoff?.multiplier ?? DEFAULT_BACKOFF.multiplier,\n maxDelayMs: backoff?.maxDelayMs ?? DEFAULT_BACKOFF.maxDelayMs,\n };\n}\n\n/**\n * Registry mapping a namespace string to its live engine + bridge handle, so\n * {@link getOutboxHandle} can reach an engine attached by a running collection.\n * A weak-ish bookkeeping map: entries are added on `onAttach` and removed on the\n * final `teardown`.\n */\nconst handlesByNamespace = new Map<string, OutboxEngine>();\n\n/** Extract a timestamp from a payload for direct wrapMutation callers. */\nfunction getPayloadUpdatedAt(\n data: Record<string, unknown>,\n): string | undefined {\n const value = data.updatedAt ?? data.updated_at;\n if (typeof value === 'string') return value;\n if (value instanceof Date) return value.toISOString();\n return undefined;\n}\n\n/**\n * Build a durable offline-outbox capability for a collection. Add the returned\n * capability to the collection's `capabilities` array; a collection without it\n * is unaffected (the seam's no-op guarantee — the \"opt-in per model\" AC).\n *\n * The same `namespace` across multiple collections shares ONE engine (one IDB\n * db, one leader lock, one FIFO queue) — so cross-collection ordering and the\n * multi-tab single-replayer guarantee hold across every opted-in collection of a\n * given identity.\n */\nexport function offlineOutbox<TData extends object = object>(\n config: OfflineOutboxConfig<TData>,\n): SmrtWebCapability<TData> {\n const namespace = durableStoreNamespace(config.namespace);\n const syncApplyBasePath = config.syncApplyBasePath ?? '/api/v1';\n const fetchFn =\n config.fetchFn ??\n ((...args) => globalThis.fetch(...(args as [RequestInfo])));\n const backoff = resolveBackoff(config.backoff);\n\n const object = config.object.name;\n // The engine this collection attaches to (set in onAttach, released in\n // teardown). Held here so wrapMutation can enqueue through it. `record` is the\n // exact callback binding registered, handed back on teardown for precise\n // removal (two collections on one object each detach only their own).\n let engine: OutboxEngine | undefined;\n let record: object | undefined;\n\n return {\n name: 'offline-outbox',\n\n onAttach() {\n const acquired = acquireOutboxEngine(\n {\n namespace,\n syncApplyBasePath,\n fetchFn,\n backoff,\n random: config.random,\n registerResource: (clear) =>\n registerDurableResource(namespace, { kind: 'outbox', clear }),\n },\n {\n object,\n onSyncStateChange: config.onSyncStateChange,\n onConflict: config.onConflict,\n },\n );\n engine = acquired.engine;\n record = acquired.record;\n handlesByNamespace.set(namespace, engine);\n },\n\n async wrapMutation(envelope) {\n // If onAttach hasn't run yet (shouldn't happen — wrapMutation fires only\n // after construction), fall through so the write isn't silently dropped.\n if (!engine) return { handled: false };\n const itemId = await engine.enqueue({\n kind: envelope.kind,\n object,\n rowId: envelope.key,\n data: envelope.data,\n baseUpdatedAt:\n envelope.baseUpdatedAt ?? getPayloadUpdatedAt(envelope.data),\n });\n if (!itemId) return { handled: false };\n // The optimistic row (envelope.data) stands in for the fetcher result; the\n // factory suppresses the refetch so it isn't dropped. Exactly-once replay\n // then reconciles it server-side via sync/apply.\n return { handled: true, result: envelope.data };\n },\n\n async teardown() {\n if (!engine || !record) return;\n const current = engine;\n const currentRecord = record;\n engine = undefined;\n record = undefined;\n // Detach; the final detach disposes the engine. Only drop the handle map\n // entry if THIS release actually evicted the engine (ref count hit 0).\n const before = current.referenceCount;\n await releaseOutboxEngine(namespace, current, object, currentRecord);\n if (before <= 1 && handlesByNamespace.get(namespace) === current) {\n handlesByNamespace.delete(namespace);\n }\n },\n };\n}\n\n/**\n * Retrieve the programmatic {@link OutboxHandle} for a durable-store namespace,\n * or `undefined` if no opted-in collection is currently attached under it. The\n * bridge for trusted callers (outbox UI, smrt-svelte binding, tests) to read the\n * durable queue and force retries. Pass the SAME\n * `durableStoreNamespace(config.namespace)` string the capability used.\n *\n * Mirrors the `getEngineCollection` bridge convention: an escape hatch that\n * returns a narrow SMRT-owned surface rather than the engine itself.\n */\nexport function getOutboxHandle(namespace: string): OutboxHandle | undefined {\n const engine = handlesByNamespace.get(namespace);\n if (!engine) return undefined;\n return {\n snapshot: () => engine.snapshot(),\n retry: (itemId: string) => engine.retry(itemId),\n };\n}\n","/**\n * @happyvertical/smrt-web — live-updates subscriber (#1763, CLIENT half).\n *\n * The browser half of live cache invalidation: ONE app-wide subscriber that\n * turns the server's coarse change signals into collection refetches, so a\n * dashboard reflects another session's writes without a manual refresh or\n * aggressive polling. It speaks the two channels the #1763 server half\n * generated:\n *\n * - the push channel — the generated `_events` Server-Sent-Events route\n * (`packages/core/src/generators/events-route.ts`): NAMED events\n * `event: change` / `event: resync` whose `data` is `{table, operation,\n * rowId, tenantId}`, with the cursor `seq` carried ONLY in the SSE `id:`\n * field (mirrored by the browser to `MessageEvent.lastEventId`). Heartbeats\n * are `: heartbeat` comment lines EventSource ignores natively.\n * - the pull channel — the generated `_changes` route\n * (`packages/core/src/generators/changes-route.ts`): `GET {changesUrl}\n * ?since=&tables=&limit=` →\n * `{changes, cursor, resyncRequired?, resyncCursor?}`, the full fallback\n * where SSE is unavailable.\n *\n * ## No row payload, no tenant logic, no authorization here\n *\n * The wire carries only a signal (table + row id + operation), never a row.\n * A signal makes the subscriber INVALIDATE — the refetch re-reads through the\n * authorized collection routes, so authorization and tenant-scoping stay\n * ENTIRELY on the read path, exactly as the server enforces (the `_events`\n * stream is auth-guarded and tenant-scoped at connection open; `_changes` is\n * tenant-scoped per request). This module therefore does NO tenant filtering\n * and needs no identity — it just maps `table → invalidate`.\n *\n * ## One app-wide instance (mirrors createSmrtWebClient)\n *\n * A consumer constructs ONE subscriber and passes it to every\n * {@link liveInvalidation} capability — the same \"one shared handle\" convention\n * as {@link createSmrtWebClient}. It is NOT auto-derived per collection: one\n * EventSource / one poll loop feeds every registered collection.\n *\n * ## Idempotent invalidation → no client-side dedup\n *\n * Re-invalidating on a replayed change (a reconnect replays the tail from the\n * cursor) is safe: invalidation only schedules a background refetch, and the\n * factory's relationship-derived invalidation is itself idempotent\n * (over-invalidation merely refetches). So the subscriber deliberately does NO\n * seq dedup — a replayed signal STILL fires, which is exactly what guarantees a\n * gap misses no invalidation. `lastSeq` is a resume cursor for the poll\n * fallback, not a dedup filter.\n *\n * Engine boundary: no `@happyvertical/smrt-*` import and no `@tanstack/*`\n * import — URLs and `fetch` arrive as config, and the refetch primitive arrives\n * as the `invalidate` callback each capability wires from `ctx.invalidate()`.\n * smrt-web has no logger dependency, so faults are surfaced via `console.warn`\n * (matching `warnCapability` / core's `deliverLocally` style).\n */\n\nimport type { SmrtWebCapability } from './capability.js';\n\n/**\n * The minimal `EventSource` surface the subscriber uses — declared here so a\n * test injects a fake without a real DOM, and so this module compiles without\n * DOM `lib` beyond the ambient global. A real `EventSource` satisfies it\n * structurally.\n */\nexport interface SmrtWebEventSource {\n /** Native reconnect fires this after (re)connect. */\n onopen: ((this: unknown, ev: unknown) => unknown) | null;\n /**\n * Fires on a stream error. A transient drop leaves `readyState` at OPEN\n * (0→…) and the browser auto-reconnects with `Last-Event-ID`; a fatal error\n * (server 401 / route disabled) leaves it CLOSED.\n */\n onerror: ((this: unknown, ev: unknown) => unknown) | null;\n /**\n * Fires only for UNNAMED (`message`) events. The `_events` frames are NAMED\n * (`change` / `resync`), so this never fires for them — the subscriber uses\n * {@link addEventListener} instead. Present only to satisfy the structural\n * type of a real EventSource.\n */\n onmessage: ((this: unknown, ev: unknown) => unknown) | null;\n /** Register a listener for a NAMED event (`change`, `resync`). */\n addEventListener(\n type: string,\n listener: (ev: { data: string; lastEventId: string }) => void,\n ): void;\n /** Close the stream (stops reconnection). */\n close(): void;\n /** `CONNECTING` (0), `OPEN` (1), or `CLOSED` (2). */\n readonly readyState: number;\n}\n\n/** EventSource.CLOSED — a fatal, non-reconnecting state. */\nconst EVENT_SOURCE_CLOSED = 2;\n\n/** Factory that constructs an {@link SmrtWebEventSource} for a URL. */\nexport type SmrtWebEventSourceFactory = (\n url: string,\n init: { withCredentials: boolean },\n) => SmrtWebEventSource | null | undefined;\n\n/** Configuration for {@link createSmrtWebEventSubscriber}. */\nexport interface SmrtWebEventSubscriberConfig {\n /** Absolute or same-origin URL of the generated `_events` SSE route. */\n eventsUrl: string;\n /** Absolute or same-origin URL of the generated `_changes` route (fallback). */\n changesUrl: string;\n /** `fetch` implementation for the polling fallback. Defaults to global fetch. */\n fetchFn?: typeof fetch;\n /**\n * Factory for the EventSource. Defaults to `new globalThis.EventSource(url,\n * { withCredentials })`, guarded by a `typeof` check — when EventSource is\n * absent (or the factory yields nothing) the subscriber starts on the polling\n * fallback instead.\n */\n eventSourceFactory?: SmrtWebEventSourceFactory;\n /** Poll interval for the `_changes` fallback (ms). Default 5000. */\n pollIntervalMs?: number;\n /** `withCredentials` for the EventSource (cookie auth). Default true. */\n withCredentials?: boolean;\n}\n\n/** Which transport a subscriber is currently using. */\nexport type SmrtWebSubscriberTransport = 'sse' | 'polling' | 'idle';\n\n/**\n * The ONE app-wide live-updates subscriber. Construct once (see\n * {@link createSmrtWebEventSubscriber}) and pass it to every\n * {@link liveInvalidation} capability.\n */\nexport interface SmrtWebEventSubscriber {\n /**\n * The live transport: `'sse'` while the EventSource is the source of truth,\n * `'polling'` when running (or downgraded to) the `_changes` fallback,\n * `'idle'` before any transport starts.\n */\n readonly transport: SmrtWebSubscriberTransport;\n /**\n * Register an invalidator for a physical table. Returns an unregister\n * function. Multiple invalidators may share a table (two live collections);\n * a `change` for that table fires them all.\n */\n registerTable(table: string, invalidate: () => void): () => void;\n /** Invalidate every registered table (a `resync` / `resyncRequired`). */\n invalidateAll(): void;\n /** Tear down the live transport and drop all registrations. */\n close(): void;\n}\n\n/**\n * Default EventSource factory: `new globalThis.EventSource(url, init)` when the\n * global exists, else `undefined` so the caller falls back to polling.\n */\nfunction defaultEventSourceFactory(\n url: string,\n init: { withCredentials: boolean },\n): SmrtWebEventSource | undefined {\n const EventSourceCtor = (\n globalThis as { EventSource?: new (u: string, i?: unknown) => unknown }\n ).EventSource;\n if (typeof EventSourceCtor !== 'function') return undefined;\n return new EventSourceCtor(url, init) as unknown as SmrtWebEventSource;\n}\n\n/**\n * One entry of a `_changes` page (mirrors core's `ChangeFeedEntry`) — the only\n * fields the subscriber reads. Declared locally so this module keeps its\n * zero-`@happyvertical/smrt-*`-import boundary.\n */\ninterface ChangeEntryLike {\n table: string;\n seq?: number;\n}\n\n/** A `_changes` page (mirrors core's `ChangeFeedPage`). */\ninterface ChangesPageLike {\n changes: ChangeEntryLike[];\n cursor: number;\n resyncRequired?: boolean;\n resyncCursor?: number;\n}\n\n/**\n * Create the app-wide live-updates subscriber. Feature-detects ONCE at\n * construction: an available EventSource connects the SSE stream; otherwise it\n * starts the `_changes` poll loop. A fatal SSE error later downgrades to\n * polling for the rest of the subscriber's life (no flap-back).\n */\nexport function createSmrtWebEventSubscriber(\n config: SmrtWebEventSubscriberConfig,\n): SmrtWebEventSubscriber {\n const {\n eventsUrl,\n changesUrl,\n fetchFn = (...args: Parameters<typeof fetch>) => globalThis.fetch(...args),\n eventSourceFactory = defaultEventSourceFactory,\n pollIntervalMs = 5000,\n withCredentials = true,\n } = config;\n\n // table → set of invalidators. A Set so a collection registers/unregisters\n // without positional bookkeeping and two collections coexist under one table.\n const tableInvalidators = new Map<string, Set<() => void>>();\n\n // The resume cursor: the highest seq observed (SSE) or the last `_changes`\n // cursor (polling). NOT a dedup filter — invalidation is idempotent.\n let lastSeq: number | null = null;\n\n let transport: SmrtWebSubscriberTransport = 'idle';\n let eventSource: SmrtWebEventSource | null = null;\n let pollTimer: ReturnType<typeof setInterval> | null = null;\n let closed = false;\n\n const registeredTables = (): string[] =>\n [...tableInvalidators.keys()].sort((a, b) => a.localeCompare(b));\n\n const buildChangesUrl = (since: number, tables: string[]): string => {\n const url = new URL(changesUrl, 'http://smrt.local/');\n url.searchParams.set('since', String(since));\n url.searchParams.set('tables', tables.join(','));\n if (/^[a-zA-Z][a-zA-Z\\d+\\-.]*:/.test(changesUrl)) return url.href;\n const pathQueryHash = `${url.pathname}${url.search}${url.hash}`;\n if (changesUrl.startsWith('//')) return `//${url.host}${pathQueryHash}`;\n if (changesUrl.startsWith('/')) return pathQueryHash;\n return pathQueryHash.startsWith('/')\n ? pathQueryHash.slice(1)\n : pathQueryHash;\n };\n\n const isFiniteNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value);\n\n /**\n * Report a fault without a logger dep (smrt-web is TanStack-only) — a\n * swallowed diagnostic in the package's fail-loud-in-the-console style.\n */\n const warn = (message: string, error?: unknown): void => {\n // biome-ignore lint/suspicious/noConsole: smrt-web has no logger dep (TanStack-only); live-subscriber faults surface via console.warn by design (#1763)\n console.warn(`[smrt-web] live subscriber: ${message}`, error);\n };\n\n /**\n * Fire one set of invalidators, each in its OWN try/catch (snapshot first so\n * an invalidator that unregisters mid-delivery can't mutate the set) — one\n * throwing invalidator never blocks its siblings.\n */\n const fireAll = (invalidators: Set<() => void> | undefined): void => {\n if (!invalidators || invalidators.size === 0) return;\n for (const invalidate of [...invalidators]) {\n try {\n invalidate();\n } catch (error) {\n warn('an invalidator threw; ignoring', error);\n }\n }\n };\n\n /** Invalidate every collection registered for `table`. */\n const invalidateTable = (table: string): void => {\n fireAll(tableInvalidators.get(table));\n };\n\n /** Invalidate every registered collection across every table. */\n const invalidateAll = (): void => {\n for (const invalidators of tableInvalidators.values()) {\n fireAll(invalidators);\n }\n };\n\n /** Keep the resume cursor from an SSE `id:` / MessageEvent.lastEventId. */\n const advanceLastSeqFromEventId = (lastEventId: string): void => {\n const seq = Number(lastEventId);\n if (Number.isFinite(seq)) lastSeq = seq;\n };\n\n /**\n * Parse a `change` frame's `data` DEFENSIVELY: malformed JSON (or a payload\n * with no string `table`) is logged and dropped — NEVER thrown back into the\n * EventSource message loop (a throw there would break subsequent delivery).\n */\n const onChange = (ev: { data: string; lastEventId: string }): void => {\n if (closed) return;\n let table: string | undefined;\n try {\n const parsed = JSON.parse(ev.data) as { table?: unknown };\n if (typeof parsed.table === 'string') table = parsed.table;\n } catch (error) {\n warn('dropping malformed change frame', error);\n return;\n }\n if (table === undefined) {\n warn('dropping change frame with no table', ev.data);\n return;\n }\n // The browser mirrors the SSE `id:` field to lastEventId; keep it as the\n // resume cursor (used only by the poll fallback), guarding a non-numeric id.\n advanceLastSeqFromEventId(ev.lastEventId);\n invalidateTable(table);\n };\n\n /**\n * A `resync` frame: the server's cursor is stale — invalidate everything and\n * keep the server-provided horizon from `id:` for any later polling downgrade.\n */\n const onResync = (ev: { data: string; lastEventId: string }): void => {\n if (closed) return;\n advanceLastSeqFromEventId(ev.lastEventId);\n invalidateAll();\n };\n\n /**\n * Poll `_changes` once from the resume cursor. `resyncRequired` (HTTP 200)\n * invalidates everything, then resumes from the server-provided\n * `resyncCursor` horizon so a pruned `since=0` does not loop forever.\n * Otherwise each change invalidates its table and the cursor advances to the\n * page cursor. A fetch rejection is caught+logged; the interval keeps ticking\n * (self-heals) — a path distinct from `resyncRequired`.\n */\n const poll = async (): Promise<void> => {\n if (closed) return;\n const tables = registeredTables();\n if (tables.length === 0) return;\n try {\n const since = lastSeq ?? 0;\n const url = buildChangesUrl(since, tables);\n const response = await fetchFn(url, { credentials: 'include' });\n if (closed) return;\n const page = (await response.json()) as ChangesPageLike;\n if (closed) return;\n if (page.resyncRequired) {\n invalidateAll();\n if (isFiniteNumber(page.resyncCursor)) {\n lastSeq = page.resyncCursor;\n } else if (isFiniteNumber(page.cursor) && page.cursor > since) {\n lastSeq = page.cursor;\n } else {\n lastSeq = null;\n }\n return;\n }\n for (const change of page.changes ?? []) {\n if (typeof change.table === 'string') invalidateTable(change.table);\n }\n if (typeof page.cursor === 'number') lastSeq = page.cursor;\n } catch (error) {\n // A rejection (network down) or a bad body — logged; the interval keeps\n // ticking so the next poll self-heals.\n warn('poll failed; will retry on the next interval', error);\n }\n };\n\n /** Start the `_changes` poll loop — the full fallback / downgrade target. */\n const startPolling = (): void => {\n if (closed || transport === 'polling') return;\n transport = 'polling';\n pollTimer = setInterval(() => {\n void poll();\n }, pollIntervalMs);\n // Don't keep the event loop alive solely for polling (Node/test parity).\n (pollTimer as { unref?: () => void }).unref?.();\n };\n\n /**\n * Connect the SSE stream. The frames are NAMED events, so `change`/`resync`\n * are wired via `addEventListener` — `onmessage` would NEVER fire for them.\n * A transient `onerror` needs no code (EventSource auto-reconnects with\n * Last-Event-ID); a FATAL error (readyState CLOSED — server 401 / route\n * disabled) downgrades to polling once and stays there.\n */\n const connectSse = (source: SmrtWebEventSource): void => {\n transport = 'sse';\n eventSource = source;\n source.addEventListener('change', onChange);\n source.addEventListener('resync', onResync);\n source.onerror = () => {\n if (closed) return;\n // Only a fatal (CLOSED) error means SSE is unavailable for good — fall\n // back to polling. A transient drop (still CONNECTING/OPEN) is handled\n // natively by the browser's reconnect; do nothing.\n if (source.readyState === EVENT_SOURCE_CLOSED) {\n // Close the dead source and stop listening to it, then downgrade. No\n // flap-back: once polling, we never re-attempt SSE.\n try {\n source.close();\n } catch {\n // ignore — already dead\n }\n eventSource = null;\n startPolling();\n }\n };\n };\n\n // Feature-detect ONCE. An EventSource we can construct → SSE; otherwise (or\n // if the factory yields nothing) → polling.\n const initialSource = eventSourceFactory(eventsUrl, { withCredentials });\n if (initialSource) {\n connectSse(initialSource);\n } else {\n startPolling();\n }\n\n return {\n get transport() {\n return transport;\n },\n registerTable(table, invalidate) {\n let set = tableInvalidators.get(table);\n if (!set) {\n set = new Set<() => void>();\n tableInvalidators.set(table, set);\n }\n set.add(invalidate);\n return () => {\n const current = tableInvalidators.get(table);\n if (!current) return;\n current.delete(invalidate);\n if (current.size === 0) tableInvalidators.delete(table);\n };\n },\n invalidateAll,\n close() {\n if (closed) return;\n closed = true;\n if (eventSource) {\n try {\n eventSource.close();\n } catch {\n // ignore — best-effort teardown\n }\n eventSource = null;\n }\n if (pollTimer) {\n clearInterval(pollTimer);\n pollTimer = null;\n }\n tableInvalidators.clear();\n transport = 'idle';\n },\n };\n}\n\n/** Configuration for the {@link liveInvalidation} capability. */\nexport interface LiveInvalidationConfig {\n /** The one app-wide subscriber from {@link createSmrtWebEventSubscriber}. */\n subscriber: Pick<\n SmrtWebEventSubscriber,\n 'registerTable' | 'invalidateAll' | 'transport'\n >;\n /**\n * The PHYSICAL table name this collection reads. EXPLICIT because a\n * `SmrtWebCollectionDefinition` has no physical-table field and STI children\n * share one base table — the subscriber keys signals by physical table, so a\n * guess would mis-route invalidations.\n */\n tableName: string;\n}\n\n/**\n * A thin per-collection capability that subscribes THIS collection to live\n * signals for its `tableName`. On attach it registers `ctx.invalidate` (the\n * factory's relationship-derived refetch primitive) with the shared subscriber;\n * on teardown it unregisters. All transport, reconnection, and fan-out live in\n * the {@link createSmrtWebEventSubscriber}; this capability is just the wire\n * between one collection and that one subscriber.\n */\nexport function liveInvalidation<TData extends object = object>(\n config: LiveInvalidationConfig,\n): SmrtWebCapability<TData> {\n const { subscriber, tableName } = config;\n let unregister: (() => void) | undefined;\n return {\n name: 'live-invalidation',\n onAttach(ctx) {\n unregister = subscriber.registerTable(tableName, () => ctx.invalidate());\n },\n teardown() {\n unregister?.();\n unregister = undefined;\n },\n };\n}\n","/**\n * @happyvertical/smrt-web — browser client data runtime (#1761).\n *\n * A typed collection factory that materializes the manifest-generated web\n * collection definitions (`@happyvertical/smrt-virt-web`) as cached, reactive\n * collections over the generated SMRT REST surface.\n *\n * This package is the **engine-absorption boundary**: the client-data engine\n * (currently TanStack DB) is an implementation detail held entirely inside\n * this module. Its types never appear on the public API — collections are\n * handed back as the SMRT-owned {@link SmrtWebCollection}, and the shared cache\n * as the opaque {@link SmrtWebClient} — so the engine stays swappable without a\n * consumer-visible break. Consumers never import `@tanstack/*` directly.\n *\n * Framework-agnostic by construction: this entry imports no UI framework.\n * Svelte live-query bindings ship separately (see PRD #1755) so this core never\n * pulls the Svelte-only `@tanstack/svelte-db` export condition.\n *\n * Scope of this slice:\n * - stale-while-revalidate reads (a `staleTimeMs` window, background revalidation)\n * - concurrent-read dedup (one network request per in-flight collection load)\n * - optimistic create that persists through the generated REST surface and\n * rolls back automatically when the server errors\n * - relationship-derived invalidation (#1761): a settled mutation invalidates\n * the caches of the collections related to the mutated one, with the edges\n * derived from the manifest (`definition.relationships`) — no hand-wired\n * cache keys. Cross-collection reach requires a shared client from\n * {@link createSmrtWebClient}; with a private client only the mutated\n * collection refetches.\n * - hydration seeding (#1761): rows fetched server-side (a SvelteKit\n * `+page.server.ts` load) seed the shared cache via\n * {@link CreateSmrtCollectionOptions.initialData}, so the first client read\n * serves them WITHOUT a duplicate first-render fetch.\n *\n * Deliberately NOT here yet (see PRD #1755): offline outbox, SSE invalidation,\n * persistence, version awareness.\n */\n\nimport { createCollection } from '@tanstack/db';\nimport { QueryClient } from '@tanstack/query-core';\nimport { queryCollectionOptions } from '@tanstack/query-db-collection';\n\nimport type {\n SmrtWebCapability,\n SmrtWebCapabilityContext,\n SmrtWebMutationEnvelope,\n} from './capability.js';\nimport { runWrapMutation } from './capability.js';\n\n// Re-export the capability seam (#1755) and the shared durable-store foundation\n// through this single entry — the package ships one export subpath, so both are\n// reachable as `@happyvertical/smrt-web`. Kept in their own modules so each is\n// reviewable and testable on its own; surfaced here for consumers-to-be (the\n// offline outbox #1762, persistence #1764, and live SSE invalidation\n// #1763-client slices).\nexport type {\n MutationSettleOutcome,\n SmrtWebCapability,\n SmrtWebCapabilityContext,\n SmrtWebMutationEnvelope,\n WrapMutationOutcome,\n} from './capability.js';\nexport { runWrapMutation } from './capability.js';\nexport type { DurableResource, DurableStoreKey } from './durable-store.js';\nexport {\n durableStoreNamespace,\n registerDurableResource,\n wipeDurableStore,\n} from './durable-store.js';\n// The durable offline outbox capability (#1762) — the first concrete capability\n// over the seam above. A hand-rolled IndexedDB queue + Web Locks leader election\n// (NOT @tanstack/offline-transactions, which would leak an engine type into the\n// .d.ts and fail the boundary check). Surfaced here as the root barrel; no\n// subpath. The public surface is engine-free by construction.\nexport type {\n OfflineOutboxConfig,\n OutboxBackoff,\n OutboxConflict,\n OutboxHandle,\n OutboxSnapshotItem,\n OutboxSyncState,\n SyncStateEvent,\n} from './offline.js';\nexport { getOutboxHandle, offlineOutbox } from './offline.js';\n// Live-updates subscriber (#1763-client): the ONE app-wide SSE `_events`\n// subscriber (with `_changes` polling fallback) and the thin per-collection\n// `liveInvalidation` capability that wires a collection's `ctx.invalidate()` to\n// it. Its own module for the same reason as the seam above — reviewable and\n// testable on its own (mock only EventSource + fetch).\nexport type {\n LiveInvalidationConfig,\n SmrtWebEventSource,\n SmrtWebEventSourceFactory,\n SmrtWebEventSubscriber,\n SmrtWebEventSubscriberConfig,\n SmrtWebSubscriberTransport,\n} from './sse-client.js';\nexport {\n createSmrtWebEventSubscriber,\n liveInvalidation,\n} from './sse-client.js';\n\n// ---------------------------------------------------------------------------\n// Generated definition contract (mirrors @happyvertical/smrt-virt-web)\n// ---------------------------------------------------------------------------\n\n/**\n * Field metadata emitted per column by the `@happyvertical/smrt-virt-web`\n * virtual module (generated from the package manifest).\n */\nexport interface SmrtWebFieldDefinition {\n type: string;\n required?: boolean;\n default?: unknown;\n}\n\n/** The relationship kinds a generated web collection edge can describe. */\nexport type SmrtWebRelationshipKind =\n | 'foreignKey'\n | 'crossPackageRef'\n | 'oneToMany'\n | 'manyToMany';\n\n/**\n * A manifest-derived edge from this collection to a sibling REST collection,\n * emitted by the `@happyvertical/smrt-virt-web` virtual module. When a mutation\n * on this collection settles, the caches of the collections named by these\n * edges are invalidated (relationship-derived invalidation, #1761), so a\n * dependent view refetches without any hand-wired cache key.\n *\n * SMRT-owned data — no client-engine (`@tanstack/*`) type appears here, so it\n * stays inside the engine-absorption boundary.\n */\nexport interface SmrtWebRelationship {\n /** The declaring field carrying the relationship (e.g. `groupId`, `items`). */\n field: string;\n /** The relationship kind, mirroring the manifest field type. */\n kind: SmrtWebRelationshipKind;\n /** REST collection name the edge resolves to (e.g. `ad_groups`). */\n relatedCollection: string;\n}\n\n/**\n * One generated collection definition: everything needed to construct a client\n * collection over the generated REST surface. The `_row` property is a phantom\n * type carrier threaded through codegen — it never exists at runtime, it only\n * lets factories infer the row type from a definition.\n */\nexport interface SmrtWebCollectionDefinition<TData extends object = object> {\n /** REST collection name (e.g. `products`). */\n name: string;\n /** Source class name (e.g. `Product`). */\n className: string;\n /** Path under the API base path (e.g. `/products`). */\n endpoint: string;\n /** Primary key field name (`id` for SmrtObject). */\n idField: string;\n /** CRUD + custom actions exposed by the api decorator config. */\n actions: string[];\n /** Persisted field metadata keyed by field name. */\n fields: Record<string, SmrtWebFieldDefinition>;\n /**\n * Manifest-derived relationship edges to sibling REST collections. Drives\n * relationship-derived cache invalidation: a settled mutation on this\n * collection invalidates the caches of the collections these edges name.\n * Optional so hand-built definitions (older codegen, tests) still satisfy the\n * type; a missing value means \"no derived edges\".\n */\n relationships?: SmrtWebRelationship[];\n /** Phantom row-type carrier — never present at runtime. */\n _row?: TData;\n}\n\n// ---------------------------------------------------------------------------\n// Fetcher contract + payload normalization\n// ---------------------------------------------------------------------------\n\n/**\n * The per-collection CRUD surface of the generated REST client\n * (`createClient(basePath).<collection>` from `@happyvertical/smrt-virt-client`).\n *\n * Return types are `unknown` on purpose: generated fetchers resolve with\n * whatever the server sent, so this package normalizes and validates payloads\n * centrally — see {@link unwrapListResult} / {@link unwrapItemResult}.\n */\nexport interface SmrtCrudFetchers {\n list(params?: Record<string, unknown>): Promise<unknown>;\n get?(id: string): Promise<unknown>;\n create(data: Record<string, unknown>): Promise<unknown>;\n update?(id: string, data: Record<string, unknown>): Promise<unknown>;\n delete?(id: string): Promise<unknown>;\n}\n\n/**\n * Raised when a generated-client call resolved with an error payload\n * (`{ error: string }` from the generated REST routes) or an unexpected shape.\n * Thrown inside a mutation handler, this triggers the automatic rollback of\n * optimistic state.\n */\nexport class SmrtWebRequestError extends Error {\n readonly payload: unknown;\n\n constructor(message: string, payload?: unknown) {\n super(message);\n this.name = 'SmrtWebRequestError';\n this.payload = payload;\n }\n}\n\n/** A row as stored in the client collection: the DTO plus a required key. */\nexport type SmrtWebRow<TData extends object> = TData & { id: string };\n\n/**\n * Normalize a generated-client list result to an array of rows.\n *\n * The generated REST routes return a bare JSON array; `{ error }` payloads are\n * surfaced as failures. The `{ data: [...] }` envelope is tolerated for\n * ApiResponse-shaped clients (e.g. a mock client).\n */\nexport function unwrapListResult(\n result: unknown,\n collectionName: string,\n): Array<Record<string, unknown>> {\n if (Array.isArray(result)) {\n return result as Array<Record<string, unknown>>;\n }\n if (result && typeof result === 'object') {\n const record = result as Record<string, unknown>;\n if (typeof record.error === 'string') {\n throw new SmrtWebRequestError(\n `[smrt-web] list(${collectionName}) failed: ${record.error}`,\n result,\n );\n }\n if (Array.isArray(record.data)) {\n return record.data as Array<Record<string, unknown>>;\n }\n }\n throw new SmrtWebRequestError(\n `[smrt-web] list(${collectionName}) returned an unexpected payload shape`,\n result,\n );\n}\n\n/**\n * Normalize a generated-client item result (create/update) to a row.\n * `{ error }` payloads become failures — inside mutation handlers this is what\n * makes optimistic state roll back.\n */\nexport function unwrapItemResult(\n result: unknown,\n context: string,\n): Record<string, unknown> {\n if (result && typeof result === 'object' && !Array.isArray(result)) {\n const record = result as Record<string, unknown>;\n if (typeof record.error === 'string') {\n throw new SmrtWebRequestError(\n `[smrt-web] ${context} failed: ${record.error}`,\n result,\n );\n }\n if (\n record.data &&\n typeof record.data === 'object' &&\n !Array.isArray(record.data)\n ) {\n return record.data as Record<string, unknown>;\n }\n return record;\n }\n throw new SmrtWebRequestError(\n `[smrt-web] ${context} returned an unexpected payload shape`,\n result,\n );\n}\n\n/**\n * Build CRUD fetchers from a generated collection definition — the same URL\n * scheme and payload handling as the generated REST client\n * (`basePath + endpoint`), with one improvement: HTTP error statuses reject\n * with the server's `{ error }` body instead of resolving with it.\n */\nexport function createDefinitionFetchers(\n definition: SmrtWebCollectionDefinition<object>,\n basePath = '/api/v1',\n fetchFn: typeof fetch = (...args) => globalThis.fetch(...args),\n): SmrtCrudFetchers {\n const collectionUrl = `${basePath}${definition.endpoint}`;\n const headers = { 'Content-Type': 'application/json' };\n\n const parse = async (response: Response): Promise<unknown> => {\n const payload: unknown = await response.json().catch(() => null);\n if (!response.ok) {\n const message =\n payload &&\n typeof payload === 'object' &&\n typeof (payload as Record<string, unknown>).error === 'string'\n ? String((payload as Record<string, unknown>).error)\n : `HTTP ${response.status}`;\n throw new SmrtWebRequestError(\n `[smrt-web] ${definition.name} request failed: ${message}`,\n payload,\n );\n }\n return payload;\n };\n\n return {\n list: async () => parse(await fetchFn(collectionUrl, { headers })),\n get: async (id) =>\n parse(await fetchFn(`${collectionUrl}/${id}`, { headers })),\n create: async (data) =>\n parse(\n await fetchFn(collectionUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify(data),\n }),\n ),\n update: async (id, data) =>\n parse(\n await fetchFn(`${collectionUrl}/${id}`, {\n method: 'PUT',\n headers,\n body: JSON.stringify(data),\n }),\n ),\n delete: async (id) => {\n const response = await fetchFn(`${collectionUrl}/${id}`, {\n method: 'DELETE',\n headers,\n });\n if (!response.ok) {\n throw new SmrtWebRequestError(\n `[smrt-web] delete(${definition.name}) failed: HTTP ${response.status}`,\n );\n }\n return true;\n },\n };\n}\n\n/**\n * Generate a client-local id for optimistic inserts. The generated REST layer\n * strips client-supplied ids on create (mass-assignment guard #1540), so this\n * id only identifies the optimistic row until the post-persist refetch swaps in\n * the server-assigned row.\n */\nexport function newLocalId(): string {\n const cryptoRef = globalThis.crypto as Crypto | undefined;\n if (cryptoRef?.randomUUID) {\n return cryptoRef.randomUUID();\n }\n return `local-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n\n// ---------------------------------------------------------------------------\n// Engine-absorbing public surface (no @tanstack/* types leak past here)\n// ---------------------------------------------------------------------------\n\n/**\n * Opaque handle to the shared client cache / request-dedup layer. Create one\n * with {@link createSmrtWebClient} and pass the SAME instance to every\n * collection that should share a cache and deduplicate in-flight requests.\n *\n * The engine (currently a TanStack Query client) is intentionally hidden behind\n * this brand so it stays swappable — do not depend on its concrete shape.\n */\nexport interface SmrtWebClient {\n /** Phantom brand — this handle wraps the hidden client-cache engine. */\n readonly __smrtWebClient: 'SmrtWebClient';\n}\n\n/**\n * Engine-side shape of a {@link SmrtWebClient}. Never exported, so the engine\n * type never reaches the public surface. Extends the public brand so the value\n * created here carries the brand at runtime (enabling the validation below).\n */\ninterface SmrtWebClientEngine extends SmrtWebClient {\n readonly queryClient: QueryClient;\n}\n\n/**\n * Create a shared client-cache handle. Pass the returned handle as\n * {@link CreateSmrtCollectionOptions.client} to every collection that should\n * share a cache and deduplicate requests app-wide.\n */\nexport function createSmrtWebClient(): SmrtWebClient {\n const engine: SmrtWebClientEngine = {\n __smrtWebClient: 'SmrtWebClient',\n queryClient: new QueryClient(),\n };\n return engine;\n}\n\nfunction resolveQueryClient(client?: SmrtWebClient): QueryClient {\n if (!client) return new QueryClient();\n const engine = client as Partial<SmrtWebClientEngine>;\n if (engine.__smrtWebClient !== 'SmrtWebClient' || !engine.queryClient) {\n throw new SmrtWebRequestError(\n '[smrt-web] options.client must be a handle from createSmrtWebClient()',\n );\n }\n return engine.queryClient;\n}\n\n/**\n * Project an engine row to a plain public DTO. The client-data engine decorates\n * stored rows with enumerable virtual props (`$synced`/`$origin`/`$key`/\n * `$collectionId`) that would otherwise cross the SMRT boundary through spread\n * or JSON serialization. The `$` prefix is reserved for the engine; SMRT\n * columns never begin with it.\n */\nfunction toPlainRow<TData extends object>(row: unknown): SmrtWebRow<TData> {\n const plain: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(row as Record<string, unknown>)) {\n if (key.charCodeAt(0) !== 36 /* '$' */) plain[key] = value;\n }\n return plain as SmrtWebRow<TData>;\n}\n\n/** Project the row values carried by a change notification to plain DTOs. */\nfunction projectChanges(changes: unknown): unknown {\n if (!Array.isArray(changes)) return changes;\n return changes.map((change) => {\n if (!change || typeof change !== 'object') return change;\n const record = change as Record<string, unknown>;\n const projected: Record<string, unknown> = { ...record };\n if (record.value && typeof record.value === 'object') {\n projected.value = toPlainRow(record.value);\n }\n if (record.previousValue && typeof record.previousValue === 'object') {\n projected.previousValue = toPlainRow(record.previousValue);\n }\n return projected;\n });\n}\n\n/**\n * A pending optimistic mutation. Await {@link isPersisted} to observe the\n * server outcome: it resolves once the write has been persisted through the\n * REST surface, and rejects (rolling the optimistic state back) on error.\n */\nexport interface SmrtWebTransaction {\n readonly isPersisted: { readonly promise: Promise<unknown> };\n}\n\n/** A change-subscription handle. Call {@link unsubscribe} to detach. */\nexport interface SmrtWebSubscription {\n unsubscribe(): void;\n}\n\n/**\n * A live, cached collection of plain-DTO rows — the SMRT-owned public contract\n * over the client-data engine. Exposes only the committed surface; the engine's\n * own type is never named here so it stays swappable.\n */\nexport interface SmrtWebCollection<TData extends object> {\n /** All rows currently in the collection (plain DTOs, insertion order). */\n readonly toArray: ReadonlyArray<SmrtWebRow<TData>>;\n /** Number of rows currently in the collection. */\n readonly size: number;\n /** True when a row with `key` is present. */\n has(key: string): boolean;\n /** The row with `key`, or `undefined`. */\n get(key: string): SmrtWebRow<TData> | undefined;\n /** Resolve once the first load has completed. */\n preload(): Promise<void>;\n /** Tear down subscriptions and cached state. */\n cleanup(): Promise<void>;\n /** Subscribe to change notifications; returns a detach handle. */\n subscribeChanges(callback: (changes: unknown) => void): SmrtWebSubscription;\n /**\n * Optimistically insert a row and persist it through the create fetcher. The\n * row is visible synchronously; the returned transaction settles on the\n * server outcome (see {@link SmrtWebTransaction}).\n */\n insert(row: SmrtWebRow<TData>): SmrtWebTransaction;\n}\n\n/** Extract a row's server revision timestamp for sync/apply conflict guards. */\nfunction getBaseUpdatedAt(row: unknown): string | undefined {\n if (!row || typeof row !== 'object') return undefined;\n const record = row as Record<string, unknown>;\n const value = record.updatedAt ?? record.updated_at;\n if (typeof value === 'string') return value;\n if (value instanceof Date) return value.toISOString();\n return undefined;\n}\n\n/**\n * Options for {@link createSmrtCollection}. Generic in the collection's row\n * type `TData` so {@link initialData} is checked against the same DTO the\n * collection stores; every other option is row-type-agnostic, so the parameter\n * defaults to `object` and can be omitted at call sites that pass no seed.\n */\nexport interface CreateSmrtCollectionOptions<TData extends object = object> {\n /**\n * Generated REST client surface for this collection, e.g.\n * `createClient('/api/v1').products` from the virt-client module. When\n * omitted, fetchers are derived from the definition's endpoint and `basePath`\n * with the same URL scheme and payload shapes the generated client uses.\n */\n fetchers?: SmrtCrudFetchers;\n /** API base path for definition-derived fetchers (default `/api/v1`). */\n basePath?: string;\n /** Fetch implementation override (tests, SSR). Defaults to global fetch. */\n fetchFn?: typeof fetch;\n /**\n * Shared cache handle from {@link createSmrtWebClient}. Pass one app-wide\n * instance so collections share a cache and deduplicate requests; a private\n * cache is created when omitted.\n */\n client?: SmrtWebClient;\n /**\n * Cache namespace for this collection's reads. Fold a backend / tenant /\n * preview discriminator in here when the SAME generated collection is\n * materialized against DIFFERENT backends while sharing one {@link client} —\n * without it those reads share a cache key and could serve one backend's rows\n * for the other for the whole `staleTimeMs` window. Omit for the common\n * single-backend case.\n */\n scope?: string;\n /**\n * Stale-while-revalidate window in milliseconds (default 30s): reads within\n * the window are served from the local collection without a network request;\n * the first read after it revalidates in the background.\n */\n staleTimeMs?: number;\n /** Retry failed loads (default false: fail fast, surface errors). */\n retry?: boolean;\n /**\n * Rows to seed this collection's cache with, before its first read — the\n * hydration path for server-rendered data (#1761). Fetch rows in a SvelteKit\n * `+page.server.ts` load, pass them here on the client, and the first read\n * serves them from cache WITHOUT a duplicate first-render network request\n * (SMRT-owned type, so no engine type appears on the option).\n *\n * The seed is written to the cache with a fresh timestamp, so it counts as\n * fresh for `staleTimeMs`: with the default window the first read does not\n * fetch, and the collection revalidates in the background only once the window\n * elapses (or immediately if `staleTimeMs` is 0). Seed the SAME rows the\n * server serialized so the pre- and post-hydration renders match.\n *\n * Seeds the SAME cache key the reads use — so with a shared {@link client},\n * fold the backend / tenant discriminator into {@link scope} to match, exactly\n * as reads do; otherwise one backend's seed would serve the other for the\n * `staleTimeMs` window.\n */\n initialData?: SmrtWebRow<TData>[];\n /**\n * Capability plug-ins hooking the collection lifecycle (#1755) — the seam\n * that lets the offline outbox (#1762), persistence (#1764), and live SSE\n * invalidation (#1763-client) slices each live in their own module instead of\n * contending on this factory. Capabilities run in array order at six fixed\n * points (see {@link SmrtWebCapability}). Additive and defaulting to none: an\n * undefined or empty array is byte-for-byte the collection of today (the\n * no-op guarantee), so this ships zero concrete capabilities by design.\n */\n capabilities?: SmrtWebCapability<TData>[];\n}\n\n/**\n * Registry mapping a public collection handle to its underlying engine\n * collection. Keyed weakly so a handle and its engine collection are collected\n * together. Read only through {@link getEngineCollection}.\n */\nconst engineCollections = new WeakMap<object, unknown>();\n\n/**\n * Retrieve the underlying engine collection backing a handle — an advanced\n * bridge for trusted framework bindings (e.g. the smrt-svelte live-query\n * binding), which must feed the engine collection to the query builder. Returns\n * `unknown` so no engine type crosses the boundary; callers cast. Throws for a\n * handle not produced by {@link createSmrtCollection}. Not needed for normal\n * use.\n */\nexport function getEngineCollection<TData extends object>(\n handle: SmrtWebCollection<TData>,\n): unknown {\n const engine = engineCollections.get(handle);\n if (engine === undefined) {\n throw new SmrtWebRequestError(\n '[smrt-web] getEngineCollection: not a smrt-web collection handle',\n );\n }\n return engine;\n}\n\n/**\n * Report a capability hook that threw or rejected, without letting it break the\n * collection (#1755). Capability hooks are third-party plug-ins; one misbehaving\n * hook must not reject a successful mutation, abort construction, or wedge\n * `cleanup()`. smrt-web has no logger dependency (TanStack-only), so this uses\n * `console.warn` — a swallowed diagnostic, matching the package's fail-loud-in-\n * the-console style.\n */\nfunction warnCapability(\n capability: { name: string },\n hook: string,\n error: unknown,\n): void {\n // biome-ignore lint/suspicious/noConsole: smrt-web has no logger dep (TanStack-only); a swallowed capability fault is surfaced via console.warn by design (#1755)\n console.warn(\n `[smrt-web] capability \"${capability.name}\" ${hook} threw; ignoring`,\n error,\n );\n}\n\n/**\n * Create a typed client collection over a generated SMRT collection definition\n * and the matching generated REST client fetchers.\n *\n * Reads: stale-while-revalidate. The first subscriber triggers a fetch;\n * re-subscribing within `staleTimeMs` serves local data with no request. N\n * concurrent identical reads coalesce into one network request.\n *\n * Writes: `collection.insert({ ...data, id: newLocalId() })` applies instantly,\n * persists through `fetchers.create()` (the temp id is stripped — the server\n * assigns the real one), then refetches to reconcile. A failed create rejects\n * the transaction and the optimistic row rolls back automatically.\n *\n * Relationship-derived invalidation: once a create/update/delete has persisted,\n * the query caches of this collection AND the collections named by\n * `definition.relationships` (manifest-derived edges) are invalidated, so\n * dependent views refetch. Reaching OTHER collections requires them to share\n * this collection's `client` (see {@link createSmrtWebClient}); with a private\n * client only this collection refetches.\n *\n * Hydration seeding: pass rows fetched server-side as\n * {@link CreateSmrtCollectionOptions.initialData} and the collection's first\n * read is served from them with NO network request (until `staleTimeMs`\n * elapses) — the SvelteKit `+page.server.ts` → hydrate path.\n */\nexport function createSmrtCollection<TData extends object>(\n definition: SmrtWebCollectionDefinition<TData>,\n options: CreateSmrtCollectionOptions<TData>,\n): SmrtWebCollection<TData> {\n type Row = SmrtWebRow<TData>;\n\n const { staleTimeMs = 30_000, retry = false, scope, initialData } = options;\n const capabilities = options.capabilities ?? [];\n const fetchers =\n options.fetchers ??\n createDefinitionFetchers(definition, options.basePath, options.fetchFn);\n const queryClient = resolveQueryClient(options.client);\n const idField = definition.idField || 'id';\n\n // Scope discriminates the cache key so a shared client can materialize the\n // same collection against different backends without cross-serving reads.\n // `let` so capabilities can extend the scheme via `contributeCacheKey` below;\n // with no capabilities these stay exactly the base `smrt:(scope:)name` form.\n let cacheId = scope\n ? `smrt:${scope}:${definition.name}`\n : `smrt:${definition.name}`;\n let queryKey = scope\n ? ['smrt', scope, definition.name]\n : ['smrt', definition.name];\n\n // Relationship-derived invalidation target set (#1761): the collections\n // whose caches a settled mutation on THIS collection must invalidate. Always\n // includes this collection itself (so its own read revalidates) plus every\n // manifest-derived related collection. Built once; a settled write matches\n // any cached query whose collection-name segment (the LAST queryKey element,\n // mirroring the `['smrt', (scope,) name]` scheme above) is in this set.\n //\n // Over-invalidation is safe — a stale query merely refetches. Under-\n // invalidation is the bug (a dependent view showing stale rows), so the\n // predicate matches by collection name across ALL scopes rather than an exact\n // key: a mutation in one scope refreshes the related collection in every\n // scope sharing the client.\n const invalidationTargets = new Set<string>([definition.name]);\n for (const relationship of definition.relationships ?? []) {\n invalidationTargets.add(relationship.relatedCollection);\n }\n\n /**\n * Invalidate the query caches of this collection and its manifest-derived\n * related collections. Cross-collection reach requires those collections to\n * share this collection's `client` (from {@link createSmrtWebClient}); with a\n * private client only THIS collection's query lives here, so only it\n * refetches. Fire-and-forget: invalidation schedules a background refetch and\n * must not delay the mutation's own settle.\n */\n const invalidateRelated = (): void => {\n void queryClient.invalidateQueries({\n predicate: (query) => {\n const key = query.queryKey;\n if (!Array.isArray(key) || key.length === 0) return false;\n const collectionSegment = key[key.length - 1];\n return (\n typeof collectionSegment === 'string' &&\n invalidationTargets.has(collectionSegment)\n );\n },\n });\n };\n\n // The engine-free context every capability hook receives (#1755). `cacheKey`\n // / `cacheId` are getter-backed over the live `let` bindings, so a capability\n // observes the key AS IT STANDS when it reads: during `contributeCacheKey`\n // (below) that is the base key plus any EARLIER capability's segments (not its\n // own, not-yet-applied one); from `onAttach`/`warmStart` onward it is the\n // FINAL key. `invalidate` enters the same `invalidateRelated()` the factory\n // runs post-mutation, so a capability can refetch off an external trigger.\n const ctx: SmrtWebCapabilityContext<TData> = {\n definition,\n fetchers,\n get cacheKey() {\n return queryKey;\n },\n get cacheId() {\n return cacheId;\n },\n invalidate: () => invalidateRelated(),\n };\n\n // contributeCacheKey (#1755): fold each capability's returned segments into\n // the cache key/id, extending the base scope-based scheme. Runs ONCE, before\n // construction. Isolated per capability: a throwing contributeCacheKey is\n // logged and skipped rather than aborting construction. With no capabilities\n // this loop is empty and the keys are untouched — the no-op path.\n //\n // CRITICAL — segments are spliced in JUST BEFORE the collection name, so the\n // name stays the LAST queryKey element. `invalidateRelated()`'s predicate\n // (and thus relationship-derived invalidation #1761, and a capability's own\n // `ctx.invalidate()`) identifies a collection by `key[key.length - 1]`;\n // appending segments after the name would hide it from that predicate and\n // silently break invalidation for any collection using contributeCacheKey\n // (exactly the #1764 persistence slice). `cacheId` is an opaque engine id the\n // predicate never reads, so it can keep appending.\n for (const capability of capabilities) {\n let extra: string[] | undefined;\n try {\n extra = capability.contributeCacheKey?.(ctx);\n } catch (error) {\n warnCapability(capability, 'contributeCacheKey', error);\n }\n if (extra && extra.length > 0) {\n // Rebuild as [prefix..., ...extra, name] — name remains last.\n const name = queryKey[queryKey.length - 1];\n const prefix = queryKey.slice(0, -1);\n queryKey = [...prefix, ...extra, name];\n cacheId = `${cacheId}:${extra.join(':')}`;\n }\n }\n\n // Seed the query cache BEFORE the collection's engine starts its sync, so the\n // first read populates from the seed instead of fetching (verified: zero\n // list() calls). `setQueryData` stamps a fresh `dataUpdatedAt`, so the seed\n // counts as fresh for `staleTime` — the first read serves it with no request,\n // and revalidation fires only once `staleTimeMs` elapses (or immediately when\n // it is 0). An explicit empty seed is honored too: it means \"zero rows\", a\n // valid fresh state that likewise suppresses the first fetch.\n //\n // Two seed sources with a fixed precedence:\n // 1. `initialData` — hydration seeding (#1761): rows a SvelteKit\n // `+page.server.ts` load serialized, the fresher same-request SSR truth.\n // 2. a capability `warmStart` (#1755) — e.g. the persistence slice\n // rehydrating from disk. Only the FIRST capability that returns rows\n // contributes.\n // `initialData` WINS: it is resolved first, and `warmStart` is only consulted\n // when `initialData` is undefined. With no capabilities, step 2 never runs and\n // this is byte-identical to the #1761 seed — the no-op path.\n //\n // Seed via the ATOMIC updater form: a plain get-then-set would let two\n // collections sharing this key and materialized in the same tick both observe\n // `undefined` and have the later seed clobber the earlier one.\n // `(existing) => existing ?? seed` keeps the first seed (or any already-cached\n // rows, which may be newer than this late payload) in a single cache write.\n const seedCache = (rows: Row[]): void => {\n queryClient.setQueryData<Row[]>(queryKey, (existing) => existing ?? rows);\n };\n // Set by cleanup(); guards the async-warmStart continuations (#1755) so a\n // collection torn down while a rehydrate is still in flight neither seeds the\n // SHARED query cache after teardown (which could suppress the next collection's\n // real fetch on the same client/key) nor preloads the dead engine.\n let disposed = false;\n // Resolve a warmStart result to rows, isolating a sync throw / async reject\n // (logged, treated as \"no rows\") so a misbehaving provider can neither abort\n // construction nor leak an unhandled rejection.\n const warmRowsFrom = async (\n capability: SmrtWebCapability<TData>,\n warm: Promise<Row[] | undefined> | Row[] | undefined,\n ): Promise<Row[] | undefined> => {\n try {\n return await warm;\n } catch (error) {\n warnCapability(capability, 'warmStart', error);\n return undefined;\n }\n };\n\n // A pending async `warmStart` (persistence rehydrating from OPFS/IndexedDB).\n // Construction stays synchronous, but `preload()` (below) AWAITS this before\n // starting the engine's own load, so an async rehydrate reliably suppresses\n // the first `list()` on the preload path. A subscribe-driven read that races\n // an unresolved warmStart may still fetch once — bounded, and it self-heals\n // via the atomic seed updater. Undefined when there is no async warmStart.\n let warmStartPending: Promise<void> | undefined;\n if (initialData !== undefined) {\n seedCache(initialData);\n } else {\n // Honor \"the FIRST capability that returns ROWS\" across BOTH sync and async\n // warmStart. A sync provider that returns rows seeds inline immediately (so\n // it suppresses even a non-preload read). The FIRST provider that returns a\n // Promise hands off to an ORDERED async chain covering itself and every\n // LATER capability, in array order: it awaits each, skips a result that is\n // undefined or throws/rejects, and seeds the first that yields rows. So an\n // async miss or reject no longer blocks a later provider from seeding.\n for (let i = 0; i < capabilities.length; i += 1) {\n const capability = capabilities[i];\n let warm: Promise<Row[] | undefined> | Row[] | undefined;\n try {\n warm = capability.warmStart?.(ctx);\n } catch (error) {\n // A synchronous warmStart throw must not abort construction.\n warnCapability(capability, 'warmStart', error);\n continue;\n }\n if (warm === undefined) continue;\n if (warm instanceof Promise) {\n const firstPromise = warm;\n warmStartPending = (async () => {\n // This capability first (its promise is already in flight), then each\n // later capability in order until one yields rows.\n let rows = await warmRowsFrom(capability, firstPromise);\n for (\n let j = i + 1;\n rows === undefined && j < capabilities.length;\n j += 1\n ) {\n const later = capabilities[j];\n let laterWarm: Promise<Row[] | undefined> | Row[] | undefined;\n try {\n laterWarm = later.warmStart?.(ctx);\n } catch (error) {\n warnCapability(later, 'warmStart', error);\n continue;\n }\n if (laterWarm === undefined) continue;\n rows = await warmRowsFrom(later, laterWarm);\n }\n // Skip the seed if the collection was cleaned up while the rehydrate\n // was in flight — a late write to the shared cache could otherwise\n // suppress the NEXT collection's real fetch on the same client/key.\n if (rows !== undefined && !disposed) seedCache(rows);\n })();\n break;\n }\n seedCache(warm);\n break;\n }\n }\n\n // Notify every capability that a mutation settled (#1755) — on BOTH a\n // successful persist and a fetcher throw. Per-capability try/catch so this\n // genuinely NEVER throws: a throwing onSettled must not reject a SUCCESSFUL\n // mutation nor mask a fetcher error, and one bad capability must not stop the\n // others being notified. With no capabilities this loop is empty — the no-op\n // path.\n const notifySettled = (\n envelope: SmrtWebMutationEnvelope,\n outcome: { ok: true; result: unknown } | { ok: false; error: unknown },\n ): void => {\n for (const capability of capabilities) {\n try {\n capability.onSettled?.(envelope, outcome, ctx);\n } catch (error) {\n warnCapability(capability, 'onSettled', error);\n }\n }\n };\n\n /**\n * Persist one mutation through the capability seam then the real fetcher.\n * First offers the write to `wrapMutation` (array order, first-handled-wins):\n * a handling capability's result stands in for the fetcher (the offline\n * path), otherwise `runFetcher` performs the real write. On success notifies\n * `onSettled({ ok: true })`; on ANY throw notifies `onSettled({ ok: false })`\n * and RE-THROWS so the optimistic state still rolls back.\n *\n * Returns `handled` so the caller can suppress the engine's post-mutation\n * refetch for an offline write (#1762): a handled write never hit the server,\n * so refetching the server list would DROP the optimistic row the outbox must\n * keep until it replays. With no capabilities `runWrapMutation` returns\n * `{ handled: false }` and both notify loops are empty, so this reduces to\n * `await runFetcher()` with `handled: false` — behavior identical to before\n * the seam.\n */\n const persistMutation = async (\n envelope: SmrtWebMutationEnvelope,\n runFetcher: () => Promise<unknown>,\n ): Promise<{ handled: boolean; result: unknown }> => {\n try {\n const wrapped = await runWrapMutation(capabilities, envelope, ctx);\n const result = wrapped.handled ? wrapped.result : await runFetcher();\n notifySettled(envelope, { ok: true, result });\n return { handled: wrapped.handled, result };\n } catch (error) {\n notifySettled(envelope, { ok: false, error });\n throw error;\n }\n };\n\n const collection = createCollection(\n queryCollectionOptions<Row>({\n id: cacheId,\n queryKey,\n queryClient,\n staleTime: staleTimeMs,\n retry,\n queryFn: async () =>\n unwrapListResult(await fetchers.list(), definition.name) as Array<Row>,\n getKey: (row) => String((row as Record<string, unknown>)[idField]),\n onInsert: async ({ transaction }) => {\n let anyHandled = false;\n for (const mutation of transaction.mutations) {\n const modified = mutation.modified as Record<string, unknown>;\n // Offer the write to the capability seam first (#1755), then fall\n // through to the real create fetcher. The envelope carries the FULL\n // optimistic row; the fetcher closure strips the client-local id: the\n // generated REST layer rejects or ignores client-supplied ids on\n // create (#1540), and the follow-up refetch swaps the optimistic row\n // for the server-assigned one.\n const envelope: SmrtWebMutationEnvelope = {\n kind: 'insert',\n key: String(modified[idField]),\n data: modified,\n };\n const outcome = await persistMutation(envelope, async () => {\n const { [idField]: _localId, ...data } = modified;\n return unwrapItemResult(\n await fetchers.create(data),\n `create(${definition.name})`,\n );\n });\n anyHandled = anyHandled || outcome.handled;\n }\n // If a capability handled the write offline (#1762) it never reached the\n // server, so BOTH the engine's own post-mutation refetch (suppressed via\n // `{ refetch: false }`) and `invalidateRelated()` are skipped — else the\n // server list, which lacks the offline row, would drop the optimistic\n // row the outbox must keep until it replays. A mixed batch (some\n // handled, some not) is conservative: if ANY mutation was handled we\n // suppress, so an unsent row is never dropped; the common case is one\n // mutation per transaction. An UNHANDLED batch behaves exactly as\n // before: refetch runs and related caches invalidate.\n if (anyHandled) return { refetch: false };\n invalidateRelated();\n },\n onUpdate: fetchers.update\n ? async ({ transaction }) => {\n let anyHandled = false;\n for (const mutation of transaction.mutations) {\n const key = String(mutation.key);\n const changes = mutation.changes as Record<string, unknown>;\n const envelope: SmrtWebMutationEnvelope = {\n kind: 'update',\n key,\n data: changes,\n baseUpdatedAt: getBaseUpdatedAt(mutation.original),\n };\n const outcome = await persistMutation(envelope, async () =>\n unwrapItemResult(\n // biome-ignore lint/style/noNonNullAssertion: guarded by the surrounding ternary\n await fetchers.update!(key, changes),\n `update(${definition.name})`,\n ),\n );\n anyHandled = anyHandled || outcome.handled;\n }\n if (anyHandled) return { refetch: false };\n invalidateRelated();\n }\n : undefined,\n onDelete: fetchers.delete\n ? async ({ transaction }) => {\n let anyHandled = false;\n for (const mutation of transaction.mutations) {\n const key = String(mutation.key);\n const envelope: SmrtWebMutationEnvelope = {\n kind: 'delete',\n key,\n data: {},\n baseUpdatedAt: getBaseUpdatedAt(mutation.original),\n };\n const outcome = await persistMutation(envelope, async () =>\n // biome-ignore lint/style/noNonNullAssertion: guarded by the surrounding ternary\n fetchers.delete!(key),\n );\n anyHandled = anyHandled || outcome.handled;\n }\n if (anyHandled) return { refetch: false };\n invalidateRelated();\n }\n : undefined,\n }),\n );\n\n // Wrap the engine collection in the SMRT-owned public surface. The wrapper\n // projects rows to plain DTOs at every read boundary (toArray/get and change\n // payloads) so the engine's virtual props never escape, and confines the\n // engine's own types to this module.\n const handle: SmrtWebCollection<TData> = {\n get toArray() {\n return collection.toArray.map((row) => toPlainRow<TData>(row));\n },\n get size() {\n return collection.size;\n },\n has(key) {\n return collection.has(key);\n },\n get(key) {\n const row = collection.get(key);\n return row === undefined ? undefined : toPlainRow<TData>(row);\n },\n preload() {\n // Gate the first read on a pending async warmStart (#1755) so an async\n // rehydrate (persistence over OPFS/IndexedDB) reliably seeds the cache\n // BEFORE the engine's own load runs — otherwise the fetch that persistence\n // was meant to suppress would race ahead. Resolved/rejected warmStart is\n // already handled (seeded or logged); we only need to await settlement.\n //\n // When there is NO pending warmStart (incl. the no-op path and the\n // `initialData` seed path) return the engine promise DIRECTLY — no extra\n // async wrapper — so the microtask timing is byte-identical to before the\n // seam (a wrapper tick would let a staleTime:0 revalidation land early).\n if (!warmStartPending) return collection.preload();\n // If cleanup() ran while the warmStart was still pending, don't preload the\n // torn-down engine — resolve to nothing.\n return warmStartPending.then(() => {\n if (disposed) return;\n return collection.preload();\n });\n },\n async cleanup() {\n // Mark disposed FIRST so any still-pending async warmStart continuation\n // sees it and skips seeding the shared cache / preloading the dead engine\n // (#1755, Fix G) — even though the continuation runs on a later tick.\n disposed = true;\n // Tear down the engine first, THEN each capability (#1755) — so a\n // capability's teardown runs against a stopped engine. Awaited so async\n // teardowns (closing an SSE stream, flushing a store) complete before\n // cleanup() resolves. Per-capability try/catch so one rejecting teardown\n // does not skip the others nor reject cleanup(). With no capabilities this\n // awaits nothing extra.\n await collection.cleanup();\n for (const capability of capabilities) {\n try {\n await capability.teardown?.(ctx);\n } catch (error) {\n warnCapability(capability, 'teardown', error);\n }\n }\n },\n subscribeChanges(callback) {\n const subscription = collection.subscribeChanges((changes: unknown) =>\n callback(projectChanges(changes)),\n );\n return { unsubscribe: () => subscription.unsubscribe() };\n },\n insert(row) {\n return collection.insert(row) as unknown as SmrtWebTransaction;\n },\n };\n\n engineCollections.set(handle, collection);\n\n // onAttach (#1755): now that the engine collection exists, let each capability\n // wire an external (non-mutation) trigger — an SSE subscription, a focus\n // listener — with a callable `ctx.invalidate()`. Runs ONCE, in array order.\n // Per-capability try/catch so a throwing onAttach cannot break construction\n // after the handle is already registered, nor skip later capabilities. With\n // no capabilities this loop is empty — the no-op path.\n for (const capability of capabilities) {\n try {\n capability.onAttach?.(ctx);\n } catch (error) {\n warnCapability(capability, 'onAttach', error);\n }\n }\n\n return handle;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-web",
3
- "version": "0.38.2",
3
+ "version": "0.38.3",
4
4
  "description": "SMRT browser client data runtime: typed collection factory wrapping the client-data engine over generated REST clients",
5
5
  "author": "HappyVertical",
6
6
  "type": "module",
@@ -19,13 +19,14 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@tanstack/db": "^0.6.14",
22
- "@tanstack/query-core": "^5.90.5",
22
+ "@tanstack/query-core": "^5.101.2",
23
23
  "@tanstack/query-db-collection": "^1.0.46"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "24.13.2",
27
+ "fake-indexeddb": "^6.2.5",
27
28
  "typescript": "^5.9.3",
28
- "vite": "8.1.2",
29
+ "vite": "8.1.3",
29
30
  "vitest": "^4.1.9"
30
31
  },
31
32
  "publishConfig": {