@hvakr/firestate 0.1.1 → 0.1.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/README.md +201 -27
- package/dist/index.d.mts +392 -34
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +991 -438
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -1
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["syncKeyCounter","doc","doc"],"sources":["../src/schema.ts","../src/diff.ts","../src/document.ts","../src/collection.ts","../src/hooks.ts","../src/firestate.ts","../src/undo.ts","../src/store.ts","../src/provider.tsx"],"sourcesContent":["import type { ZodType, z } from \"zod\";\nimport type {\n CollectionDefinition,\n DocumentDefinition,\n FirestoreObject,\n} from \"./types\";\n\n/**\n * Define a typed document. `TData` is the document's TypeScript shape.\n *\n * **Most apps should reach for {@link createFirestate} + {@link doc} instead**\n * — that builds a registry of every Firestore thing in one object and\n * generates typed hooks for you. `defineDocument` is the lower-level\n * escape hatch: use it when you need fully custom `collection` / `id`\n * derivation, when you're calling firestate outside React, or when a\n * registry doesn't fit your control flow.\n *\n * Two ways to use:\n *\n * 1. Plain TypeScript type (no schema, no runtime validation):\n * ```ts\n * interface Project { name: string; createdAt: number }\n *\n * const projectDoc = defineDocument<Project>({\n * collection: 'projects',\n * id: (params) => params.projectId,\n * })\n * ```\n *\n * 2. With a Zod schema — `TData` is inferred from `z.infer<S>`. Firestate\n * runs `schema.parse(...)` on full-payload writes (`set`/`add`) so bad\n * data throws at the call site. Partial `update(diff)` calls are not\n * validated (diffs frequently contain Firestore sentinels).\n * ```ts\n * import { z } from 'zod'\n *\n * const ProjectSchema = z.object({ name: z.string(), createdAt: z.number() })\n *\n * const projectDoc = defineDocument({\n * schema: ProjectSchema,\n * collection: 'projects',\n * id: (params) => params.projectId,\n * })\n * ```\n */\nexport function defineDocument<S extends ZodType<FirestoreObject>>(\n definition: Omit<DocumentDefinition<z.infer<S>>, \"schema\"> & {\n schema: S;\n }\n): DocumentDefinition<z.infer<S>>;\nexport function defineDocument<TData extends FirestoreObject>(\n definition: DocumentDefinition<TData>\n): DocumentDefinition<TData>;\nexport function defineDocument(\n definition: DocumentDefinition<FirestoreObject>\n): DocumentDefinition<FirestoreObject> {\n return definition;\n}\n\n/**\n * Define a typed collection. `TData` is the shape of each document in the\n * collection. See {@link defineDocument} for the schema/plain-type tradeoff.\n *\n * **Most apps should reach for {@link createFirestate} + {@link col} instead.**\n * `defineCollection` is the escape hatch for fully custom path derivation\n * or non-React usage.\n *\n * @example\n * ```ts\n * interface Space { name: string; area: number }\n *\n * const spacesCollection = defineCollection<Space>({\n * path: (params) => `projects/${params.projectId}/spaces`,\n * lazy: true,\n * })\n * ```\n */\nexport function defineCollection<S extends ZodType<FirestoreObject>>(\n definition: Omit<CollectionDefinition<z.infer<S>>, \"schema\"> & {\n schema: S;\n }\n): CollectionDefinition<z.infer<S>>;\nexport function defineCollection<TData extends FirestoreObject>(\n definition: CollectionDefinition<TData>\n): CollectionDefinition<TData>;\nexport function defineCollection(\n definition: CollectionDefinition<FirestoreObject>\n): CollectionDefinition<FirestoreObject> {\n return definition;\n}\n\n/**\n * Infer the document data type from a {@link DocumentDefinition}.\n */\nexport type InferDocumentData<T extends DocumentDefinition<FirestoreObject>> =\n T extends DocumentDefinition<infer D> ? D : never;\n\n/**\n * Infer the document data type (with `id` field) from a {@link DocumentDefinition}.\n */\nexport type InferDocument<T extends DocumentDefinition<FirestoreObject>> =\n InferDocumentData<T> & { id: string };\n\n/**\n * Infer the document data type from a {@link CollectionDefinition}.\n */\nexport type InferCollectionData<\n T extends CollectionDefinition<FirestoreObject>\n> = T extends CollectionDefinition<infer D> ? D : never;\n\n/**\n * Infer the document data type (with `id` field) from a {@link CollectionDefinition}.\n */\nexport type InferCollectionDocument<\n T extends CollectionDefinition<FirestoreObject>\n> = InferCollectionData<T> & { id: string };\n","import {\n deleteField,\n serverTimestamp,\n Timestamp,\n WithFieldValue,\n} from 'firebase/firestore'\nimport type { DeepPartial, FirestoreObject } from './types'\n\n/**\n * Check if a value is a plain object (not array, null, or special Firestore type)\n */\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n value !== null &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n !(value instanceof Timestamp) &&\n Object.getPrototypeOf(value) === Object.prototype\n\n/**\n * Check if a value is a Firestore opaque type: a FieldValue sentinel\n * (`serverTimestamp`, `deleteField`, `increment`, `arrayUnion`,\n * `arrayRemove`, …) or a value type the SDK ships with its own identity\n * semantics (`Timestamp`, `DocumentReference`, `GeoPoint`, `Bytes`,\n * `VectorValue`). They all expose `.isEqual()` and have a non-plain\n * prototype.\n *\n * The diff / clone pipeline must treat these as **opaque**: never iterate\n * their keys, never substitute their values, never compare them by `===`.\n * Doing any of those silently breaks the write path — see the C1\n * regression where `serverTimestamp()` was replaced with `Timestamp.now()`\n * before reaching Firestore.\n */\nconst isFirestoreOpaque = (\n value: unknown\n): value is { isEqual: (other: unknown) => boolean } => {\n if (value === null || typeof value !== 'object') return false\n if (Object.getPrototypeOf(value) === Object.prototype) return false\n return (\n 'isEqual' in value &&\n typeof (value as { isEqual: unknown }).isEqual === 'function'\n )\n}\n\n// Reference sentinels used to identify specific FieldValue kinds. The\n// Firebase SDK does not export the sentinel subclasses; the only stable\n// way to ask \"is this a serverTimestamp / deleteField?\" is to construct a\n// reference instance once and delegate to its `.isEqual()`. Hoisting them\n// to module scope avoids reconstructing on every call.\nconst SERVER_TIMESTAMP_REF = serverTimestamp()\nconst DELETE_FIELD_REF = deleteField()\n\nconst isDeleteField = (value: unknown): boolean =>\n isFirestoreOpaque(value) && value.isEqual(DELETE_FIELD_REF)\n\nconst isServerTimestamp = (value: unknown): boolean =>\n isFirestoreOpaque(value) && value.isEqual(SERVER_TIMESTAMP_REF)\n\n/**\n * Check if two values are deeply equal\n */\nexport const isDeepEqual = (a: unknown, b: unknown): boolean => {\n if (a === b) return true\n if (a === null || b === null) return false\n if (typeof a !== typeof b) return false\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n return a.every((item, i) => isDeepEqual(item, b[i]))\n }\n\n // Opaque Firestore types delegate to their own `.isEqual`. Catches\n // Timestamp, DocumentReference, GeoPoint, Bytes, VectorValue and\n // every FieldValue sentinel kind.\n if (isFirestoreOpaque(a) && isFirestoreOpaque(b)) {\n return a.isEqual(b)\n }\n\n if (isPlainObject(a) && isPlainObject(b)) {\n const keysA = Object.keys(a)\n const keysB = Object.keys(b)\n if (keysA.length !== keysB.length) return false\n return keysA.every((key) => isDeepEqual(a[key], b[key]))\n }\n\n return false\n}\n\n/**\n * Compute the minimal diff between two objects for Firestore updates.\n * Returns only the fields that changed, using deleteField() for removed fields.\n *\n * @param from - The original object (sync state)\n * @param to - The target object (local state)\n * @returns A partial object containing only changed fields\n */\nexport const computeDiff = <T extends FirestoreObject>(\n from: T,\n to: T | undefined\n): WithFieldValue<DeepPartial<T>> => {\n if (to === undefined) {\n return deleteField() as WithFieldValue<DeepPartial<T>>\n }\n\n const diff: Record<string, unknown> = {}\n\n // Check for changed or added fields\n for (const key of Object.keys(to)) {\n const fromValue = from[key]\n const toValue = to[key]\n\n // Arrays are compared by value and replaced entirely\n if (Array.isArray(toValue)) {\n if (!isDeepEqual(fromValue, toValue)) {\n diff[key] = toValue\n }\n continue\n }\n\n // Nested objects get recursive diff\n if (isPlainObject(toValue)) {\n if (!isDeepEqual(fromValue, toValue)) {\n const nestedDiff = computeDiff(\n (fromValue as Record<string, unknown>) ?? {},\n toValue\n )\n if (Object.keys(nestedDiff).length > 0) {\n diff[key] = nestedDiff\n }\n }\n continue\n }\n\n // Firestore opaque values — sentinels (serverTimestamp,\n // arrayUnion, …) and value types (Timestamp, DocumentReference,\n // …). Compare via `.isEqual` so identical-by-value Timestamps\n // don't show up as spurious diffs every sync; pass the value\n // through unchanged so sentinels survive into the write payload\n // for the server to expand.\n if (isFirestoreOpaque(toValue)) {\n if (\n !isFirestoreOpaque(fromValue) ||\n !toValue.isEqual(fromValue)\n ) {\n diff[key] = toValue\n }\n continue\n }\n\n // Primitives are compared directly\n if (toValue !== undefined && fromValue !== toValue) {\n diff[key] = toValue\n }\n }\n\n // Check for removed fields. Only `undefined` triggers a delete — `null`\n // is a valid Firestore value and is preserved via the primitive-comparison\n // branch above.\n for (const key of Object.keys(from)) {\n if (to[key] === undefined) {\n diff[key] = deleteField()\n }\n }\n\n return diff as WithFieldValue<DeepPartial<T>>\n}\n\n/**\n * Apply a Firestore diff to a target object in place (mutating).\n * Handles deleteField(), serverTimestamp(), and nested objects.\n *\n * Most code should use `applyDiff` (immutable) instead.\n * This mutable version is useful for performance-critical paths\n * where you're already working with a cloned object.\n *\n * @param target - The object to mutate\n * @param diff - The diff to apply\n */\nexport const applyDiffMutable = (\n target: FirestoreObject,\n diff: Record<string, unknown>\n): void => {\n for (const key of Object.keys(diff)) {\n const value = (diff as Record<string, unknown>)[key]\n\n // Firestore opaque values: FieldValue sentinels and value types.\n if (isFirestoreOpaque(value)) {\n // `deleteField()` is structural — actually drop the key from\n // the local view. Matches what Firestore does on commit, and\n // what `computeDiff`'s removed-field branch round-trips back\n // out to a sentinel on the next diff.\n if (isDeleteField(value)) {\n delete (target as Record<string, unknown>)[key]\n continue\n }\n // Every other opaque value (serverTimestamp, increment,\n // arrayUnion/Remove, Timestamp, DocumentReference, GeoPoint,\n // Bytes, VectorValue, …) is preserved by reference. Sentinels\n // must reach Firestore in their original form so the server\n // can expand them; value types must keep their prototype.\n //\n // `serverTimestamp()` used to be substituted with\n // `Timestamp.now()` here, which silently shipped client clock\n // time to Firestore. The optimistic-display companion lives\n // in document.ts / collection.ts as `displayOverrides`.\n ;(target as Record<string, unknown>)[key] = value\n continue\n }\n\n // Handle nested objects\n if (isPlainObject(value)) {\n const existingValue = (target as Record<string, unknown>)[key]\n if (!isPlainObject(existingValue)) {\n ;(target as Record<string, unknown>)[key] = {}\n }\n applyDiffMutable(\n (target as Record<string, unknown>)[key] as FirestoreObject,\n value as Record<string, unknown>\n )\n continue\n }\n\n // Handle primitives and arrays\n ;(target as Record<string, unknown>)[key] = value\n }\n}\n\n/**\n * Create a deep clone of an object that's safe for Firestore operations.\n *\n * Firestore opaque values (FieldValue sentinels, Timestamp,\n * DocumentReference, GeoPoint, Bytes, VectorValue) are returned **by\n * reference**. They are immutable from the user's perspective; cloning\n * them by walking keys would either lose their prototype — turning a\n * `DocumentReference` into a plain object Firestore can't recognize —\n * or destroy a sentinel that needed to reach the server intact.\n */\nexport const deepClone = <T>(value: T): T => {\n if (value === null || typeof value !== 'object') {\n return value\n }\n\n if (isFirestoreOpaque(value)) {\n return value\n }\n\n if (Array.isArray(value)) {\n return value.map(deepClone) as T\n }\n\n const result: Record<string, unknown> = {}\n for (const key of Object.keys(value)) {\n result[key] = deepClone((value as Record<string, unknown>)[key])\n }\n return result as T\n}\n\n/**\n * Check if a diff is empty (no changes)\n */\nexport const isDiffEmpty = (diff: Record<string, unknown>): boolean =>\n Object.keys(diff).length === 0\n\n/**\n * Flatten a nested diff object to dot notation for use with Firestore's updateDoc.\n *\n * This converts:\n * ```\n * { building: { floors: 5, height: 100 }, name: 'Test' }\n * ```\n * To:\n * ```\n * { 'building.floors': 5, 'building.height': 100, 'name': 'Test' }\n * ```\n *\n * Arrays, FieldValue sentinels (deleteField, serverTimestamp, …) and\n * Firestore value types (Timestamp, DocumentReference, GeoPoint, Bytes,\n * VectorValue) are NOT flattened — they're preserved at their path so\n * Firestore receives them in their original form.\n *\n * @param diff - The nested diff object\n * @param prefix - Internal: current path prefix for recursion\n * @returns Flattened object with dotted keys\n */\nexport const flattenDiff = (\n diff: Record<string, unknown>,\n prefix = ''\n): Record<string, unknown> => {\n const result: Record<string, unknown> = {}\n\n for (const key of Object.keys(diff)) {\n const value = diff[key]\n const path = prefix ? `${prefix}.${key}` : key\n\n // Arrays, FieldValue sentinels, and Firestore value types are\n // opaque from flatten's perspective — kept at the path verbatim.\n if (Array.isArray(value) || isFirestoreOpaque(value)) {\n result[path] = value\n continue\n }\n\n // Plain objects are recursively flattened\n if (isPlainObject(value)) {\n const nested = flattenDiff(value, path)\n Object.assign(result, nested)\n continue\n }\n\n // Primitives (strings, numbers, booleans, null)\n result[path] = value\n }\n\n return result\n}\n\n/**\n * Merge two diffs together, with the second taking precedence\n */\nexport const mergeDiffs = <T extends FirestoreObject>(\n first: WithFieldValue<DeepPartial<T>>,\n second: WithFieldValue<DeepPartial<T>>\n): WithFieldValue<DeepPartial<T>> => {\n const result = deepClone(first) as Record<string, unknown>\n\n for (const key of Object.keys(second)) {\n const firstValue = result[key]\n const secondValue = (second as Record<string, unknown>)[key]\n\n if (isPlainObject(firstValue) && isPlainObject(secondValue)) {\n result[key] = mergeDiffs(\n firstValue as WithFieldValue<DeepPartial<FirestoreObject>>,\n secondValue as WithFieldValue<DeepPartial<FirestoreObject>>\n )\n } else {\n result[key] = secondValue\n }\n }\n\n return result as WithFieldValue<DeepPartial<T>>\n}\n\n/**\n * Apply a diff to an object, returning a new object.\n * The original object is not modified.\n *\n * @example\n * ```ts\n * const original = { name: 'Project', count: 5 }\n * const diff = { name: 'Updated', count: deleteField() }\n * const result = applyDiff(original, diff)\n * // result = { name: 'Updated' }\n * // original is unchanged\n * ```\n */\nexport const applyDiff = <T extends FirestoreObject>(\n state: T,\n diff: WithFieldValue<DeepPartial<T>>\n): T => {\n const result = deepClone(state)\n applyDiffMutable(result, diff as Record<string, unknown>)\n return result\n}\n\n/**\n * Compute the undo diff that would reverse the effect of applying a diff to a state.\n *\n * Given a starting state and a diff that was (or will be) applied to it,\n * returns a new diff that when applied to the result would restore the original state.\n *\n * @example\n * ```ts\n * const startState = { name: 'Foo', count: 5 }\n * const diff = { name: 'Bar', count: deleteField() }\n *\n * // Apply the diff\n * const endState = applyDiff(startState, diff)\n * // endState = { name: 'Bar' }\n *\n * // Compute the undo\n * const undoDiff = computeUndoDiff(startState, diff)\n * // undoDiff = { name: 'Foo', count: 5 }\n *\n * // Applying undoDiff to endState restores startState\n * const restored = applyDiff(endState, undoDiff)\n * // restored = { name: 'Foo', count: 5 }\n * ```\n */\nexport const computeUndoDiff = <T extends FirestoreObject>(\n startState: T,\n diff: WithFieldValue<DeepPartial<T>>\n): WithFieldValue<DeepPartial<T>> => {\n const endState = applyDiff(startState, diff)\n return computeDiff(endState, startState)\n}\n\n/**\n * Check if a diff affects a specific path (supports dot notation).\n *\n * @example\n * ```ts\n * const diff = { building: { floors: 5 }, name: 'Test' }\n *\n * diffContainsPath(diff, 'name') // true\n * diffContainsPath(diff, 'building') // true\n * diffContainsPath(diff, 'building.floors') // true\n * diffContainsPath(diff, 'building.height') // false\n * diffContainsPath(diff, 'other') // false\n * ```\n */\nexport const diffContainsPath = (\n diff: Record<string, unknown>,\n path: string\n): boolean => {\n const parts = path.split('.')\n let current: unknown = diff\n\n for (const part of parts) {\n if (current === null || typeof current !== 'object') {\n return false\n }\n if (!(part in (current as Record<string, unknown>))) {\n return false\n }\n current = (current as Record<string, unknown>)[part]\n }\n\n return true\n}\n\n/**\n * Extract the value at a specific path from a diff (supports dot notation).\n * Returns undefined if the path doesn't exist in the diff.\n *\n * @example\n * ```ts\n * const diff = { building: { floors: 5, height: 100 }, name: 'Test' }\n *\n * extractDiffValue(diff, 'name') // 'Test'\n * extractDiffValue(diff, 'building') // { floors: 5, height: 100 }\n * extractDiffValue(diff, 'building.floors') // 5\n * extractDiffValue(diff, 'building.missing') // undefined\n * ```\n */\nexport const extractDiffValue = (\n diff: Record<string, unknown>,\n path: string\n): unknown => {\n const parts = path.split('.')\n let current: unknown = diff\n\n for (const part of parts) {\n if (current === null || typeof current !== 'object') {\n return undefined\n }\n if (!(part in (current as Record<string, unknown>))) {\n return undefined\n }\n current = (current as Record<string, unknown>)[part]\n }\n\n return current\n}\n\n/**\n * Create a diff that sets a value at a specific path (supports dot notation).\n *\n * @example\n * ```ts\n * createDiffAtPath('name', 'New Name')\n * // { name: 'New Name' }\n *\n * createDiffAtPath('building.floors', 5)\n * // { building: { floors: 5 } }\n *\n * createDiffAtPath('building.config.enabled', true)\n * // { building: { config: { enabled: true } } }\n * ```\n */\nexport const createDiffAtPath = (\n path: string,\n value: unknown\n): Record<string, unknown> => {\n const parts = path.split('.')\n const result: Record<string, unknown> = {}\n\n let current = result\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n if (part === undefined) continue\n current[part] = {}\n current = current[part] as Record<string, unknown>\n }\n\n const lastPart = parts[parts.length - 1]\n if (lastPart !== undefined) {\n current[lastPart] = value\n }\n\n return result\n}\n\n/**\n * Invert a flattened diff back to nested object structure.\n * Opposite of flattenDiff.\n *\n * @example\n * ```ts\n * const flat = { 'building.floors': 5, 'building.height': 100, 'name': 'Test' }\n * const nested = unflattenDiff(flat)\n * // { building: { floors: 5, height: 100 }, name: 'Test' }\n * ```\n */\nexport const unflattenDiff = (\n flatDiff: Record<string, unknown>\n): Record<string, unknown> => {\n const result: Record<string, unknown> = {}\n\n for (const [path, value] of Object.entries(flatDiff)) {\n const parts = path.split('.')\n\n let current = result\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n if (part === undefined) continue\n if (!(part in current) || typeof current[part] !== 'object') {\n current[part] = {}\n }\n current = current[part] as Record<string, unknown>\n }\n\n const lastPart = parts[parts.length - 1]\n if (lastPart !== undefined) {\n current[lastPart] = value\n }\n }\n\n return result\n}\n\n// ---------------------------------------------------------------------------\n// Display overrides\n//\n// `serverTimestamp()` sentinels need to survive `localState` so the write\n// path can ship them to Firestore for server-side expansion (C1). That\n// leaves the optimistic UI with a `FieldValue` object sitting at the\n// field — components can't render it. The display-override layer\n// captures `Timestamp.now()` at the moment a sentinel first enters\n// `localState`, stores it keyed by dotted path, and substitutes it into\n// the merged view at read time. The captured Timestamp is frozen for the\n// lifetime of the sentinel (it doesn't drift forward on re-renders), and\n// the entry is dropped automatically once the sentinel leaves\n// `localState` — either because the server ack cleared `localState`, or\n// because the user overwrote that path with an explicit value.\n//\n// Scope: only `serverTimestamp()` gets a display override. `increment`,\n// `arrayUnion`, and `arrayRemove` would need access to the SDK's\n// non-public internal fields (`_operand`, `_elements`) to compute a\n// display value; consumers using those should gate their render code on\n// the field not being a sentinel.\n// ---------------------------------------------------------------------------\n\n/**\n * Walk a state object collecting every dotted path that currently holds\n * a `serverTimestamp()` sentinel. Arrays are not traversed — Firestore\n * doesn't allow sentinels inside arrays. Non-plain objects (Timestamps,\n * DocumentReferences, …) are leaves.\n *\n * @internal\n */\nexport const collectServerTimestampPaths = (\n state: Record<string, unknown> | null | undefined,\n prefix = '',\n out: Set<string> = new Set()\n): Set<string> => {\n if (!state) return out\n for (const key of Object.keys(state)) {\n const value = state[key]\n const path = prefix ? `${prefix}.${key}` : key\n if (isServerTimestamp(value)) {\n out.add(path)\n continue\n }\n if (isPlainObject(value)) {\n collectServerTimestampPaths(value, path, out)\n }\n }\n return out\n}\n\n/**\n * Reconcile a `displayOverrides` map against the current `localState`:\n *\n * - For each path that holds a `serverTimestamp()` sentinel but has no\n * override yet, capture `Timestamp.now()` and store it (frozen-at-\n * first-sighting).\n * - For each existing override whose path no longer holds a sentinel\n * (sentinel was overwritten, or `localState` cleared on snapshot ack),\n * drop it.\n *\n * The map is mutated in place. Pass a custom `now` for deterministic\n * tests; defaults to `Timestamp.now()`.\n *\n * @internal\n */\nexport const reconcileDisplayOverrides = (\n localState: Record<string, unknown> | null | undefined,\n overrides: Map<string, unknown>,\n now: () => unknown = () => Timestamp.now()\n): void => {\n const currentPaths = collectServerTimestampPaths(localState)\n for (const path of currentPaths) {\n if (!overrides.has(path)) {\n overrides.set(path, now())\n }\n }\n for (const path of [...overrides.keys()]) {\n if (!currentPaths.has(path)) {\n overrides.delete(path)\n }\n }\n}\n\nconst setAtPath = (\n obj: Record<string, unknown>,\n path: string,\n value: unknown\n): void => {\n const parts = path.split('.')\n let cur = obj\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]!\n if (!isPlainObject(cur[part])) cur[part] = {}\n cur = cur[part] as Record<string, unknown>\n }\n cur[parts[parts.length - 1]!] = value\n}\n\n/**\n * Apply a path → value override map to a merged view, returning a new\n * object. Used by document.ts / collection.ts to substitute display\n * values for sentinels still present in `localState`.\n *\n * @internal\n */\nexport const applyOverridesAtPaths = <T extends FirestoreObject>(\n merged: T,\n overrides: ReadonlyMap<string, unknown>\n): T => {\n if (overrides.size === 0) return merged\n const result = deepClone(merged) as Record<string, unknown>\n for (const [path, value] of overrides) {\n setAtPath(result, path, value)\n }\n return result as T\n}\n","import {\n doc,\n collection,\n onSnapshot,\n setDoc,\n updateDoc,\n deleteDoc,\n DocumentReference,\n WithFieldValue,\n} from 'firebase/firestore'\nimport type {\n DeepPartial,\n DocumentDefinition,\n DocumentHandle,\n DocumentState,\n FirestoreObject,\n Subscriber,\n Unsubscribe,\n UpdateOptions,\n} from './types'\nimport type { FirestateStore } from './store'\nimport {\n applyDiffMutable,\n applyOverridesAtPaths,\n computeDiff,\n deepClone,\n flattenDiff,\n isDeepEqual,\n reconcileDisplayOverrides,\n} from './diff'\n\n// Module-level counter so each subscription instance gets a unique sync key,\n// even when multiple instances target the same document path.\nlet syncKeyCounter = 0\n\n/**\n * Options for creating a document subscription\n */\nexport interface DocumentOptions<TData extends FirestoreObject> {\n /** The store instance */\n store: FirestateStore\n /** Document definition from defineDocument() */\n definition: DocumentDefinition<TData>\n /**\n * Resolved document id. If omitted and `definition.id` is a string, that\n * value is used. If `definition.id` is a function, this option is required.\n */\n docId?: string\n /**\n * Resolved collection path. If omitted and `definition.collection` is a\n * string, that value is used. If `definition.collection` is a function,\n * this option is required.\n */\n collectionPath?: string\n /** Override read-only setting */\n readOnly?: boolean\n /** Callback for pushing undo actions */\n onPushUndo?: (\n undoAction: () => void,\n redoAction: () => void,\n options?: UpdateOptions\n ) => void\n}\n\n/**\n * Internal state for a document subscription.\n *\n * `localState` uses three distinct values:\n * - `undefined`: no pending local changes\n * - `null`: pending delete (the autosave-driven sync will issue deleteDoc)\n * - object: pending update/set (synced via updateDoc/setDoc)\n *\n * The same convention applies to `inflightLocalState`.\n */\ninterface DocumentInternalState<T extends FirestoreObject> {\n syncState: T | undefined\n localState: T | null | undefined\n isLoading: boolean\n error: Error | undefined\n waitingForUpdate: boolean\n inflightLocalState: T | null | undefined\n /** Whether the pending operation is a full set (create/replace) vs a partial update */\n isSetOperation: boolean\n /**\n * Frozen display values for `serverTimestamp()` sentinels currently\n * sitting in `localState`. Keyed by dotted path. Captured at the\n * moment a sentinel first appears, dropped when the sentinel leaves\n * `localState` (sync ack or overwrite). Substituted into the merged\n * view by `getMergedData` so consumers always see a renderable\n * `Timestamp`, never a raw FieldValue, while the write is in flight.\n */\n displayOverrides: Map<string, unknown>\n}\n\n/**\n * Create a document subscription.\n * This is a low-level API - prefer using useDocument hook in React.\n *\n * @example\n * ```ts\n * const subscription = createDocumentSubscription({\n * store,\n * definition: projectDoc,\n * docId: '123',\n * })\n *\n * const unsubscribe = subscription.subscribe((state) => {\n * console.log('Document state:', state)\n * })\n *\n * subscription.load()\n * ```\n */\nexport const createDocumentSubscription = <TData extends FirestoreObject>(\n options: DocumentOptions<TData>\n): {\n /** Attach the Firestore listener */\n load: () => void\n /** Stop the Firestore listener */\n stop: () => void\n /** Subscribe to state changes */\n subscribe: (fn: Subscriber<DocumentState<TData>>) => Unsubscribe\n /** Get current state */\n getState: () => DocumentState<TData>\n /** Get document handle for updates */\n getHandle: () => DocumentHandle<TData>\n /** Force sync now */\n sync: () => Promise<void>\n} => {\n const { store, definition, docId, collectionPath: resolvedCollectionPath, readOnly, onPushUndo } = options\n const { firestore, autosave: defaultAutosave, minLoadTime: defaultMinLoadTime } = store\n\n const {\n collection: collectionConfig,\n id,\n autosave = defaultAutosave,\n minLoadTime = defaultMinLoadTime,\n readOnly: definitionReadOnly,\n retryOnError = false,\n retryInterval = 5000,\n schema,\n } = definition\n\n const isReadOnly = readOnly ?? definitionReadOnly ?? false\n // Prefer the caller-resolved docId. Fall back to a string `definition.id`\n // for ergonomic direct use; if both are missing, the caller forgot to\n // resolve a function id and we surface that immediately.\n const documentId = docId ?? (typeof id === 'string' ? id : undefined)\n if (documentId === undefined) {\n throw new Error(\n `createDocumentSubscription: definition.id is a function; pass a resolved docId in options.`\n )\n }\n // Same shape as docId: prefer a caller-resolved path, fall back to a\n // string `definition.collection` for direct use. Function definitions\n // must come pre-resolved from the hook layer.\n const collectionPath = resolvedCollectionPath ?? (typeof collectionConfig === 'string' ? collectionConfig : undefined)\n if (collectionPath === undefined) {\n throw new Error(\n `createDocumentSubscription: definition.collection is a function; pass a resolved collectionPath in options.`\n )\n }\n\n // Create document reference\n const docRef = doc(\n collection(firestore, collectionPath),\n documentId\n ) as DocumentReference<TData>\n\n // Internal state\n const state: DocumentInternalState<TData> = {\n syncState: undefined,\n localState: undefined,\n isLoading: true,\n error: undefined,\n waitingForUpdate: false,\n inflightLocalState: undefined,\n isSetOperation: false,\n displayOverrides: new Map(),\n }\n\n const subscribers = new Set<Subscriber<DocumentState<TData>>>()\n let unsubscribeListener: Unsubscribe | null = null\n let autosaveTimeout: ReturnType<typeof setTimeout> | null = null\n let retryTimeout: ReturnType<typeof setTimeout> | null = null\n let minLoadTimeout: ReturnType<typeof setTimeout> | null = null\n let minLoadTimeElapsed = false\n let loaded = false\n // Cached handle — returns the same reference until notify() invalidates\n // it. Lets useSyncExternalStore consumers rely on handle identity.\n let cachedHandle: DocumentHandle<TData> | null = null\n\n // Unique key for sync tracking, scoped per-instance so multiple\n // subscriptions to the same path don't share (or clobber) one entry.\n const syncKey = `doc:${collectionPath}/${documentId}#${++syncKeyCounter}`\n\n const getMergedData = (): TData | undefined => {\n // null localState marks a pending delete — surface as no data.\n if (state.localState === null) return undefined\n const base = state.localState ?? state.syncState\n if (base === undefined) return undefined\n return applyOverridesAtPaths(base, state.displayOverrides)\n }\n\n const getPublicState = (): DocumentState<TData> => ({\n data: getMergedData(),\n isLoading: state.isLoading,\n isSynced: state.localState === undefined,\n error: state.error,\n })\n\n const notify = () => {\n // Reconcile display overrides against the current localState\n // before publishing — captures Timestamp.now() for any newly\n // arrived serverTimestamp() sentinel and drops entries whose\n // sentinels have been overwritten or acked away.\n reconcileDisplayOverrides(\n state.localState && typeof state.localState === 'object'\n ? (state.localState as Record<string, unknown>)\n : undefined,\n state.displayOverrides\n )\n cachedHandle = null\n const publicState = getPublicState()\n subscribers.forEach((fn) => fn(publicState))\n store.reportSyncState(syncKey, publicState.isSynced)\n }\n\n const updateState = (\n diff: WithFieldValue<DeepPartial<TData>>,\n undoOptions: UpdateOptions = {}\n ) => {\n if (isReadOnly) return\n\n const currentData = getMergedData()\n if (!currentData) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[firestate] update() on ${collectionPath}/${documentId} was ignored: there is no current data to diff against. ` +\n `This happens when the document is still loading, has been deleted, or doesn't exist yet. ` +\n `Use set() to create the document, or gate update calls on a non-undefined data value.`\n )\n }\n return\n }\n\n // Use raw localState as the mutation base so serverTimestamp() sentinels\n // in localState survive into newLocalState. getMergedData() substitutes\n // display-override Timestamps at sentinel paths, which would erase the\n // sentinel from state.localState on the next update() call.\n const rawBase = (state.localState ?? state.syncState) as TData\n const newLocalState = deepClone(rawBase)\n applyDiffMutable(newLocalState, diff as Record<string, unknown>)\n\n // Push undo eagerly against the pre-mutation state. Cmd+Z within the\n // autosave window pops this entry, applies the inverse via updateState,\n // and the sync() no-op shortcut absorbs the resulting same-as-syncState\n // case without a Firestore write.\n if (undoOptions?.undoable !== false && onPushUndo) {\n const undoDiff = computeDiff(\n newLocalState as FirestoreObject,\n rawBase as FirestoreObject\n )\n const redoDiff = computeDiff(\n rawBase as FirestoreObject,\n newLocalState as FirestoreObject\n )\n onPushUndo(\n () => updateState(undoDiff as WithFieldValue<DeepPartial<TData>>, { undoable: false }),\n () => updateState(redoDiff as WithFieldValue<DeepPartial<TData>>, { undoable: false }),\n undoOptions\n )\n }\n\n state.localState = newLocalState\n state.isSetOperation = false\n\n notify()\n scheduleAutosave()\n }\n\n const setData = (data: TData, undoOptions: UpdateOptions = {}) => {\n if (isReadOnly) return\n\n // Use schema.parse as a validation guard — throw on bad input — but\n // discard the parsed result and store the caller's original object.\n // Storing parsed output would (a) silently drop unknown keys via\n // Zod's default `.strip()` behavior, and (b) cause undo/redo replay\n // through this same path to re-apply any schema transforms a second\n // time. Partial update() diffs are intentionally NOT validated:\n // diffs commonly carry Firestore sentinels (serverTimestamp,\n // arrayUnion, deleteField) that aren't representable in a strict\n // schema.\n if (schema) schema.parse(data)\n\n const currentData = getMergedData()\n\n // Push undo eagerly. A set against undefined data is a creation;\n // its undo is a delete. Otherwise we restore the prior snapshot via\n // setData, which is symmetric and handles full-replace semantics\n // (including field removals) correctly.\n if (undoOptions?.undoable !== false && onPushUndo) {\n const dataForRedo = deepClone(data)\n if (currentData === undefined) {\n onPushUndo(\n () => deleteDocument({ undoable: false }),\n () => setData(dataForRedo, { undoable: false }),\n undoOptions\n )\n } else {\n // Snapshot raw localState so the restore payload contains\n // serverTimestamp() sentinels, not the frozen Timestamps that\n // getMergedData() substitutes for display purposes.\n const dataToRestore = deepClone((state.localState ?? state.syncState) as TData)\n onPushUndo(\n () => setData(dataToRestore, { undoable: false }),\n () => setData(dataForRedo, { undoable: false }),\n undoOptions\n )\n }\n }\n\n state.localState = deepClone(data)\n state.isSetOperation = true\n\n notify()\n scheduleAutosave()\n }\n\n const deleteDocument = (undoOptions: UpdateOptions = {}) => {\n if (isReadOnly) return\n\n const currentData = getMergedData()\n // Nothing to delete — bail rather than queueing a no-op deleteDoc.\n if (currentData === undefined) return\n\n // Push undo against the pre-delete data (which includes any pending\n // local edits at this moment).\n if (undoOptions?.undoable !== false && onPushUndo) {\n // Snapshot raw localState — same reason as in setData above.\n const dataToRestore = deepClone((state.localState ?? state.syncState) as TData)\n onPushUndo(\n () => setData(dataToRestore, { undoable: false }),\n () => deleteDocument({ undoable: false }),\n undoOptions\n )\n }\n\n // Mark localState as a pending delete and let scheduleAutosave drive\n // the actual deleteDoc call — same flow as set/update.\n state.localState = null\n state.isSetOperation = false\n\n notify()\n scheduleAutosave()\n }\n\n const scheduleAutosave = () => {\n if (autosaveTimeout) {\n clearTimeout(autosaveTimeout)\n }\n if (autosave > 0) {\n autosaveTimeout = setTimeout(() => {\n sync()\n }, autosave)\n }\n }\n\n const sync = async () => {\n if (state.localState === undefined) return\n\n // Pending delete — issue deleteDoc and let the listener confirm via\n // handleMissingDocument. Undo was already pushed at mutation time.\n if (state.localState === null) {\n state.inflightLocalState = null\n state.waitingForUpdate = true\n\n try {\n await deleteDoc(docRef)\n } catch (error) {\n console.error('Sync failed:', error)\n state.waitingForUpdate = false\n state.inflightLocalState = undefined\n state.error = error as Error\n store.reportError(error as Error, {\n type: 'document',\n path: `${collectionPath}/${documentId}`,\n operation: 'write',\n })\n notify()\n }\n return\n }\n\n // No-op if local state already matches what the server holds. This is\n // the path that an undo-of-pending-local takes: the inverse update\n // brings localState back to syncState, and we exit without a write.\n if (state.syncState && isDeepEqual(state.localState, state.syncState)) {\n state.localState = undefined\n state.inflightLocalState = undefined\n notify()\n return\n }\n\n const wasSetOperation = state.isSetOperation\n state.isSetOperation = false\n\n // A creation occurs when there's no server state to diff against —\n // either the user explicitly called set() to create the document, or\n // the listener has reported the doc as missing. In both cases we use\n // setDoc.\n const isCreation = !state.syncState\n const useSetDoc = wasSetOperation || isCreation\n\n const diff = state.syncState\n ? computeDiff(state.syncState, state.localState)\n : undefined\n\n state.inflightLocalState = deepClone(state.localState)\n\n state.waitingForUpdate = true\n\n try {\n if (useSetDoc) {\n // Full set / creation — use setDoc to create or completely\n // replace the document.\n await setDoc(docRef, state.localState as TData)\n } else {\n // Partial update - use updateDoc with flattened diff to prevent\n // accidentally recreating deleted documents. updateDoc will fail\n // if the document doesn't exist.\n const flatDiff = flattenDiff(diff as Record<string, unknown>)\n await updateDoc(docRef, flatDiff)\n }\n } catch (error) {\n console.error('Sync failed:', error)\n state.waitingForUpdate = false\n state.inflightLocalState = undefined\n // Surface to React: handle.error reflects the failure and the\n // listener will keep state.localState so consumers can retry by\n // calling sync() or by issuing another update. Autosave is not\n // automatically rescheduled to avoid retry loops on permission\n // errors — that policy is left to the consumer.\n state.error = error as Error\n store.reportError(error as Error, {\n type: 'document',\n path: `${collectionPath}/${documentId}`,\n operation: 'write',\n })\n notify()\n }\n }\n\n const handleSnapshot = (newSyncData: TData) => {\n state.syncState = newSyncData\n // A successful snapshot supersedes any previous read or write error.\n state.error = undefined\n\n if (state.waitingForUpdate) {\n state.waitingForUpdate = false\n const inflightState = state.inflightLocalState\n state.inflightLocalState = undefined\n const currentLocal = state.localState\n\n if (inflightState === null) {\n // Inflight was a delete but the listener fired with the doc\n // still present. The deleteDoc result is still in flight (or\n // failed and reverted). Leave localState alone — it's either\n // still null (we still want the delete) or non-null (user\n // changed their mind), and either way the next sync handles it.\n } else if (currentLocal === null) {\n // User issued a delete while a set/update was inflight. The\n // pending delete is the latest intent; preserve it for the\n // next sync.\n } else if (\n inflightState &&\n currentLocal &&\n !isDeepEqual(currentLocal, inflightState)\n ) {\n // Rebase local changes onto the new sync state.\n const changesSinceInflight = computeDiff(inflightState, currentLocal)\n const rebasedLocalState = deepClone(newSyncData)\n applyDiffMutable(rebasedLocalState, changesSinceInflight as Record<string, unknown>)\n state.localState = rebasedLocalState\n } else {\n state.localState = undefined\n }\n }\n\n if (minLoadTimeElapsed) {\n state.isLoading = false\n }\n loaded = true\n\n // If local edits exist and aren't currently being synced, schedule an\n // autosave. Covers the case where set() ran before the first snapshot\n // arrived and the initial sync attempt bailed early.\n if (state.localState !== undefined) {\n scheduleAutosave()\n }\n\n notify()\n }\n\n // A document that does not exist is not an error condition — consumers\n // commonly use that state to render a \"create\" UI. Clear loading and\n // leave error undefined; data will be undefined via getMergedData().\n const handleMissingDocument = () => {\n state.syncState = undefined\n state.error = undefined\n\n // The only localState that should clear when the doc goes missing is\n // our own pending-delete marker. Any other pending edits (object\n // value) represent the user's intent to recreate the doc — the next\n // sync() will issue a setDoc against the now-missing doc and create\n // it from scratch.\n if (state.localState === null) {\n state.localState = undefined\n state.isSetOperation = false\n if (autosaveTimeout) {\n clearTimeout(autosaveTimeout)\n autosaveTimeout = null\n }\n }\n\n if (state.waitingForUpdate) {\n state.waitingForUpdate = false\n state.inflightLocalState = undefined\n }\n if (minLoadTimeElapsed) {\n state.isLoading = false\n }\n loaded = true\n notify()\n }\n\n const handleError = (error: Error) => {\n if (retryOnError) {\n console.warn('Document listener error, retrying:', error)\n retryTimeout = setTimeout(() => {\n stop()\n load()\n }, retryInterval)\n } else {\n state.error = error\n // Don't leave consumers stuck on a loading spinner — the listener\n // has reported a terminal error, so loading is done.\n state.isLoading = false\n loaded = true\n store.reportError(error, {\n type: 'document',\n path: `${collectionPath}/${documentId}`,\n operation: 'read',\n })\n notify()\n }\n }\n\n const load = () => {\n if (unsubscribeListener) return\n\n loaded = false\n minLoadTimeElapsed = false\n\n unsubscribeListener = onSnapshot(\n docRef,\n (snapshot) => {\n if (snapshot.exists()) {\n handleSnapshot(snapshot.data())\n } else if (!snapshot.metadata.fromCache) {\n handleMissingDocument()\n }\n },\n handleError\n )\n\n // Min load time handler — tracked so stop() can cancel it.\n minLoadTimeout = setTimeout(() => {\n minLoadTimeout = null\n if (loaded) {\n state.isLoading = false\n notify()\n }\n minLoadTimeElapsed = true\n }, minLoadTime)\n }\n\n const stop = () => {\n if (unsubscribeListener) {\n unsubscribeListener()\n unsubscribeListener = null\n }\n if (autosaveTimeout) {\n clearTimeout(autosaveTimeout)\n autosaveTimeout = null\n }\n if (retryTimeout) {\n clearTimeout(retryTimeout)\n retryTimeout = null\n }\n if (minLoadTimeout) {\n clearTimeout(minLoadTimeout)\n minLoadTimeout = null\n }\n // Drop this subscription's entry from the global sync-state map so\n // an unmounted hook does not leave useIsSynced stuck at false.\n store.unregisterSyncState(syncKey)\n }\n\n const subscribe = (fn: Subscriber<DocumentState<TData>>): Unsubscribe => {\n subscribers.add(fn)\n return () => subscribers.delete(fn)\n }\n\n const buildHandle = (): DocumentHandle<TData> => ({\n data: getMergedData(),\n update: updateState,\n set: setData,\n delete: deleteDocument,\n isLoading: state.isLoading,\n isSynced: state.localState === undefined,\n sync,\n error: state.error,\n ref: docRef,\n })\n\n const getHandle = (): DocumentHandle<TData> => {\n if (cachedHandle === null) {\n cachedHandle = buildHandle()\n }\n return cachedHandle\n }\n\n return {\n load,\n stop,\n subscribe,\n getState: getPublicState,\n getHandle,\n sync,\n }\n}\n","import {\n collection,\n doc,\n onSnapshot,\n query,\n writeBatch,\n deleteField,\n WithFieldValue,\n QueryConstraint,\n type CollectionReference,\n} from 'firebase/firestore'\nimport type {\n CollectionDefinition,\n CollectionHandle,\n CollectionState,\n DeepPartial,\n FirestoreObject,\n Subscriber,\n Unsubscribe,\n UpdateOptions,\n} from './types'\nimport type { FirestateStore } from './store'\nimport {\n applyDiffMutable,\n applyOverridesAtPaths,\n computeDiff,\n deepClone,\n flattenDiff,\n isDeepEqual,\n reconcileDisplayOverrides,\n} from './diff'\n\n// Module-level counter so each subscription instance gets a unique sync key,\n// even when multiple instances target the same collection path.\nlet syncKeyCounter = 0\n\n/**\n * Options for creating a collection subscription\n */\nexport interface CollectionOptions<TData extends FirestoreObject> {\n /** The store instance */\n store: FirestateStore\n /** Collection definition from defineCollection() */\n definition: CollectionDefinition<TData>\n /**\n * Resolved collection path. If omitted and `definition.path` is a string,\n * that value is used. If `definition.path` is a function, this option is\n * required.\n */\n collectionPath?: string\n /** Override read-only setting */\n readOnly?: boolean\n /** Additional query constraints */\n queryConstraints?: QueryConstraint[]\n /** Callback for pushing undo actions */\n onPushUndo?: (\n undoAction: () => void,\n redoAction: () => void,\n options?: UpdateOptions\n ) => void\n}\n\n/**\n * Internal state for a collection subscription\n */\ninterface CollectionInternalState<T extends FirestoreObject> {\n syncState: Record<string, T> | undefined\n localState: Record<string, T> | undefined\n isLoading: boolean\n isActive: boolean\n error: Error | undefined\n waitingForUpdate: boolean\n inflightLocalState: Record<string, T> | undefined\n /**\n * Frozen display values for `serverTimestamp()` sentinels currently\n * sitting in `localState`. Keyed by dotted path (e.g.\n * `\"<docId>.updatedAt\"`). See document.ts for the full contract.\n */\n displayOverrides: Map<string, unknown>\n}\n\n/**\n * Create a collection subscription.\n * This is a low-level API - prefer using useCollection hook in React.\n *\n * @example\n * ```ts\n * const subscription = createCollectionSubscription({\n * store,\n * definition: spacesCollection,\n * collectionPath: 'projects/123/spaces',\n * })\n *\n * const unsubscribe = subscription.subscribe((state) => {\n * console.log('Collection state:', state)\n * })\n *\n * subscription.load() // For lazy collections\n * ```\n */\nexport const createCollectionSubscription = <TData extends FirestoreObject>(\n options: CollectionOptions<TData>\n): {\n /** Activate the subscription (for lazy loading) */\n load: () => void\n /** Stop the Firestore listener */\n stop: () => void\n /** Subscribe to state changes */\n subscribe: (fn: Subscriber<CollectionState<TData>>) => Unsubscribe\n /** Get current state */\n getState: () => CollectionState<TData>\n /** Get collection handle for updates */\n getHandle: () => CollectionHandle<TData>\n /** Force sync now */\n sync: () => Promise<void>\n} => {\n const { store, definition, collectionPath: resolvedPath, readOnly, queryConstraints: extraConstraints, onPushUndo } = options\n const { firestore, autosave: defaultAutosave, minLoadTime: defaultMinLoadTime } = store\n\n const {\n path,\n autosave = defaultAutosave,\n minLoadTime = defaultMinLoadTime,\n readOnly: definitionReadOnly,\n lazy = false,\n queryConstraints: definitionConstraints,\n retryOnError = false,\n retryInterval = 5000,\n schema,\n } = definition\n\n const isReadOnly = readOnly ?? definitionReadOnly ?? false\n // Prefer the caller-resolved path. Fall back to a string `definition.path`\n // for ergonomic direct use; if both are missing, the caller forgot to\n // resolve a function path.\n const collectionPath = resolvedPath ?? (typeof path === 'string' ? path : undefined)\n if (collectionPath === undefined) {\n throw new Error(\n `createCollectionSubscription: definition.path is a function; pass a resolved collectionPath in options.`\n )\n }\n const allConstraints = [...(definitionConstraints ?? []), ...(extraConstraints ?? [])]\n\n // Create collection reference\n const collectionRef = collection(firestore, collectionPath) as CollectionReference<TData>\n\n // Internal state\n const state: CollectionInternalState<TData> = {\n syncState: undefined,\n localState: undefined,\n isLoading: !lazy,\n isActive: !lazy,\n error: undefined,\n waitingForUpdate: false,\n inflightLocalState: undefined,\n displayOverrides: new Map(),\n }\n\n const subscribers = new Set<Subscriber<CollectionState<TData>>>()\n let unsubscribeListener: Unsubscribe | null = null\n let autosaveTimeout: ReturnType<typeof setTimeout> | null = null\n let minLoadTimeout: ReturnType<typeof setTimeout> | null = null\n let retryTimeout: ReturnType<typeof setTimeout> | null = null\n let minLoadTimeElapsed = false\n let loaded = false\n // Cached handle — returns the same reference until notify() invalidates\n // it. Lets useSyncExternalStore consumers rely on handle identity.\n let cachedHandle: CollectionHandle<TData> | null = null\n\n // Unique key for sync tracking, scoped per-instance so multiple\n // subscriptions to the same path don't share (or clobber) one entry.\n const syncKey = `col:${collectionPath}#${++syncKeyCounter}`\n\n const getMergedData = (): Record<string, TData> => {\n const base = state.localState ?? state.syncState ?? {}\n return applyOverridesAtPaths(base, state.displayOverrides)\n }\n\n const getPublicState = (): CollectionState<TData> => ({\n data: getMergedData(),\n isLoading: state.isLoading,\n isSynced: state.localState === undefined,\n isActive: state.isActive,\n error: state.error,\n })\n\n const notify = () => {\n // Reconcile display overrides against the current localState\n // before publishing — see document.ts for the full contract.\n reconcileDisplayOverrides(\n state.localState as Record<string, unknown> | undefined,\n state.displayOverrides\n )\n cachedHandle = null\n const publicState = getPublicState()\n subscribers.forEach((fn) => fn(publicState))\n store.reportSyncState(syncKey, publicState.isSynced)\n }\n\n // Pre-snapshot mutations are unsafe because computing a partial-update\n // local state without knowing the existing server fields would cause the\n // subsequent diff to mark unrelated fields as deleted. Document mutations\n // bail the same way when there's no current data.\n const warnNoSnapshot = (method: string) => {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[firestate] ${method}() on ${collectionPath} was ignored: the first snapshot has not arrived yet. ` +\n `Gate calls on the collection's isLoading/isActive state, or await the first data before mutating.`\n )\n }\n }\n\n const updateState = (\n diff: WithFieldValue<DeepPartial<Record<string, TData>>>,\n undoOptions: UpdateOptions = {}\n ) => {\n if (isReadOnly) return\n if (state.syncState === undefined) {\n warnNoSnapshot('update')\n return\n }\n\n // Use raw localState as the mutation base so serverTimestamp() sentinels\n // in localState survive into newLocalState. getMergedData() substitutes\n // display-override Timestamps at sentinel paths, which would erase the\n // sentinel from state.localState on the next update() call.\n const rawBase = state.localState ?? state.syncState ?? {}\n const newLocalState = deepClone(rawBase)\n applyDiffMutable(newLocalState, diff as Record<string, unknown>)\n\n // Ensure each document has its id\n for (const [docId, docData] of Object.entries(newLocalState)) {\n if (docData && typeof docData === 'object') {\n ;(docData as Record<string, unknown>).id = docId\n }\n }\n\n // Push undo eagerly against the pre-mutation snapshot. Cmd+Z within\n // the autosave window pops this entry, applies the inverse via\n // updateState, and the sync() no-op shortcut absorbs the resulting\n // same-as-syncState case without a Firestore write.\n if (undoOptions?.undoable !== false && onPushUndo) {\n const undoDiff = computeDiff(\n newLocalState as FirestoreObject,\n rawBase as FirestoreObject\n )\n const redoDiff = computeDiff(\n rawBase as FirestoreObject,\n newLocalState as FirestoreObject\n )\n onPushUndo(\n () => updateState(undoDiff as WithFieldValue<DeepPartial<Record<string, TData>>>, { undoable: false }),\n () => updateState(redoDiff as WithFieldValue<DeepPartial<Record<string, TData>>>, { undoable: false }),\n undoOptions\n )\n }\n\n state.localState = newLocalState\n\n notify()\n scheduleAutosave()\n }\n\n // Overloaded: callers can pass (id, data, opts) or (data, opts). The\n // no-id form generates a Firestore auto-id via doc(collectionRef).id and\n // returns it so the caller can reference the new doc immediately.\n // Returns undefined when the mutation is dropped so callers can't\n // accidentally route on or persist an id that was never queued.\n function addDocument(\n id: string,\n data: Omit<TData, 'id'>,\n undoOptions?: UpdateOptions\n ): string | undefined\n function addDocument(\n data: Omit<TData, 'id'>,\n undoOptions?: UpdateOptions\n ): string | undefined\n function addDocument(\n idOrData: string | Omit<TData, 'id'>,\n dataOrOptions?: Omit<TData, 'id'> | UpdateOptions,\n maybeUndoOptions?: UpdateOptions\n ): string | undefined {\n const hasExplicitId = typeof idOrData === 'string'\n const data = (hasExplicitId ? dataOrOptions : idOrData) as Omit<TData, 'id'>\n const undoOptions = (hasExplicitId\n ? maybeUndoOptions\n : (dataOrOptions as UpdateOptions | undefined)) ?? {}\n\n if (isReadOnly) return undefined\n if (state.syncState === undefined) {\n // Bail rather than queueing: an explicit id that happens to exist\n // on the server would round-trip through computeDiff and clobber\n // any remote-only fields, and we have no way to know without a\n // first snapshot.\n warnNoSnapshot('add')\n return undefined\n }\n\n // Only allocate an auto-id once we know we're going to queue the\n // write — otherwise the caller might persist an id that was dropped.\n const id = hasExplicitId ? (idOrData as string) : doc(collectionRef).id\n\n // Use schema.parse as a validation guard — throw on bad input — but\n // discard the parsed result and store the caller's original object\n // with id attached. Storing parsed output would silently drop\n // unknown keys via Zod's default `.strip()` and re-transform on\n // undo/redo replay. We feed `{ ...data, id }` to parse so the same\n // validation works whether the user's schema declares `id` or not.\n const newDoc = { ...data, id } as unknown as TData\n if (schema) schema.parse(newDoc)\n\n const currentData = getMergedData()\n const newLocalState = deepClone(currentData)\n newLocalState[id] = newDoc\n\n // Push undo eagerly. Inverse diff deletes the just-added doc.\n if (undoOptions?.undoable !== false && onPushUndo) {\n const undoDiff = computeDiff(\n newLocalState as FirestoreObject,\n currentData as FirestoreObject\n )\n const redoDiff = computeDiff(\n currentData as FirestoreObject,\n newLocalState as FirestoreObject\n )\n onPushUndo(\n () => updateState(undoDiff as WithFieldValue<DeepPartial<Record<string, TData>>>, { undoable: false }),\n () => updateState(redoDiff as WithFieldValue<DeepPartial<Record<string, TData>>>, { undoable: false }),\n undoOptions\n )\n }\n\n state.localState = newLocalState\n\n notify()\n scheduleAutosave()\n\n return id\n }\n\n const removeDocument = (id: string, undoOptions: UpdateOptions = {}) => {\n if (isReadOnly) return\n if (state.syncState === undefined) {\n warnNoSnapshot('remove')\n return\n }\n\n const currentData = getMergedData()\n if (!(id in currentData)) return\n\n const newLocalState = deepClone(currentData)\n delete newLocalState[id]\n\n // Push undo eagerly. Inverse diff re-adds the removed doc.\n if (undoOptions?.undoable !== false && onPushUndo) {\n const undoDiff = computeDiff(\n newLocalState as FirestoreObject,\n currentData as FirestoreObject\n )\n const redoDiff = computeDiff(\n currentData as FirestoreObject,\n newLocalState as FirestoreObject\n )\n onPushUndo(\n () => updateState(undoDiff as WithFieldValue<DeepPartial<Record<string, TData>>>, { undoable: false }),\n () => updateState(redoDiff as WithFieldValue<DeepPartial<Record<string, TData>>>, { undoable: false }),\n undoOptions\n )\n }\n\n state.localState = newLocalState\n\n notify()\n scheduleAutosave()\n }\n\n const scheduleAutosave = () => {\n if (autosaveTimeout) {\n clearTimeout(autosaveTimeout)\n }\n if (autosave > 0) {\n autosaveTimeout = setTimeout(() => {\n sync()\n }, autosave)\n }\n }\n\n const sync = async () => {\n if (!state.localState) return\n // syncState is guaranteed defined here: every mutation that can set\n // localState bails when syncState is undefined. This guard is purely\n // defensive against a direct sync() call after stop().\n if (state.syncState === undefined) return\n\n const syncState = state.syncState\n\n if (isDeepEqual(state.localState, syncState)) {\n state.localState = undefined\n state.inflightLocalState = undefined\n notify()\n return\n }\n\n const diff = computeDiff(\n syncState as FirestoreObject,\n state.localState as FirestoreObject\n )\n state.inflightLocalState = deepClone(state.localState)\n\n state.waitingForUpdate = true\n\n try {\n const batch = writeBatch(firestore)\n const deleteFieldSentinel = deleteField()\n\n for (const [docId, docDiff] of Object.entries(diff)) {\n const docRef = doc(collectionRef, docId)\n\n // Check if this is a delete operation\n if (\n docDiff !== null &&\n typeof docDiff === 'object' &&\n 'isEqual' in docDiff &&\n typeof docDiff.isEqual === 'function' &&\n (docDiff as { isEqual: (v: unknown) => boolean }).isEqual(deleteFieldSentinel)\n ) {\n batch.delete(docRef)\n } else if (!(docId in syncState)) {\n // New document - use set to create it\n batch.set(docRef, docDiff as Record<string, unknown>)\n } else {\n // Existing document - use update with flattened diff to prevent\n // accidentally recreating deleted documents\n const flatDiff = flattenDiff(docDiff as Record<string, unknown>)\n batch.update(docRef, flatDiff)\n }\n }\n\n await batch.commit()\n } catch (error) {\n console.error('Collection sync failed:', error)\n state.waitingForUpdate = false\n state.inflightLocalState = undefined\n // Surface to React: handle.error reflects the failure and the\n // listener will keep state.localState so consumers can retry by\n // calling sync(). Autosave is not automatically rescheduled to\n // avoid retry loops on permission errors.\n state.error = error as Error\n store.reportError(error as Error, {\n type: 'collection',\n path: collectionPath,\n operation: 'write',\n })\n notify()\n }\n }\n\n const handleSnapshot = (docs: Array<{ id: string; data: TData }>) => {\n const newSyncState: Record<string, TData> = {}\n for (const { id, data } of docs) {\n newSyncState[id] = { ...data, id } as TData\n }\n\n state.syncState = newSyncState\n // A successful snapshot supersedes any previous read or write error.\n state.error = undefined\n\n if (state.waitingForUpdate) {\n state.waitingForUpdate = false\n const inflightState = state.inflightLocalState\n state.inflightLocalState = undefined\n const currentLocal = state.localState\n\n // Rebase local changes if they changed since we started the sync\n if (\n inflightState &&\n currentLocal &&\n !isDeepEqual(currentLocal, inflightState)\n ) {\n const changesSinceInflight = computeDiff(\n inflightState as FirestoreObject,\n currentLocal as FirestoreObject\n )\n const rebasedLocalState = deepClone(newSyncState)\n applyDiffMutable(rebasedLocalState, changesSinceInflight as Record<string, unknown>)\n // Re-add ids\n for (const [docId, docData] of Object.entries(rebasedLocalState)) {\n if (docData && typeof docData === 'object') {\n ;(docData as Record<string, unknown>).id = docId\n }\n }\n state.localState = rebasedLocalState\n } else {\n state.localState = undefined\n }\n }\n\n if (minLoadTimeElapsed) {\n state.isLoading = false\n }\n loaded = true\n\n // If a rebase produced fresh local edits, ensure they flush. The\n // user's update() during the inflight write already scheduled an\n // autosave, so this is mostly defensive.\n if (state.localState !== undefined) {\n scheduleAutosave()\n }\n\n notify()\n }\n\n const handleError = (error: Error) => {\n if (retryOnError) {\n console.warn('Collection listener error, retrying:', error)\n retryTimeout = setTimeout(() => {\n stop()\n startListener()\n }, retryInterval)\n } else {\n state.error = error\n // Don't leave consumers stuck on a loading spinner — the listener\n // has reported a terminal error, so loading is done.\n state.isLoading = false\n loaded = true\n store.reportError(error, {\n type: 'collection',\n path: collectionPath,\n operation: 'read',\n })\n notify()\n }\n }\n\n const startListener = () => {\n if (unsubscribeListener) return\n\n loaded = false\n minLoadTimeElapsed = false\n\n const q = allConstraints.length > 0\n ? query(collectionRef, ...allConstraints)\n : collectionRef\n\n unsubscribeListener = onSnapshot(\n q,\n (snapshot) => {\n const docs = snapshot.docs.map((docSnap) => ({\n id: docSnap.id,\n data: docSnap.data() as TData,\n }))\n handleSnapshot(docs)\n },\n handleError\n )\n\n // Min load time handler — tracked so stop() can cancel it.\n minLoadTimeout = setTimeout(() => {\n minLoadTimeout = null\n if (loaded) {\n state.isLoading = false\n notify()\n }\n minLoadTimeElapsed = true\n }, minLoadTime)\n }\n\n const load = () => {\n // Listener-level idempotency so the hook can safely call load() on\n // every mount (including Strict Mode's mount-cleanup-remount cycle).\n if (unsubscribeListener) return\n if (!state.isActive) {\n state.isActive = true\n state.isLoading = true\n notify()\n }\n startListener()\n }\n\n const stop = () => {\n if (retryTimeout) {\n clearTimeout(retryTimeout)\n retryTimeout = null\n }\n if (unsubscribeListener) {\n unsubscribeListener()\n unsubscribeListener = null\n }\n if (autosaveTimeout) {\n clearTimeout(autosaveTimeout)\n autosaveTimeout = null\n }\n if (minLoadTimeout) {\n clearTimeout(minLoadTimeout)\n minLoadTimeout = null\n }\n // Drop this subscription's entry from the global sync-state map so\n // an unmounted hook does not leave useIsSynced stuck at false.\n store.unregisterSyncState(syncKey)\n }\n\n const subscribe = (fn: Subscriber<CollectionState<TData>>): Unsubscribe => {\n subscribers.add(fn)\n return () => subscribers.delete(fn)\n }\n\n const buildHandle = (): CollectionHandle<TData> => ({\n data: getMergedData(),\n update: updateState,\n add: addDocument,\n remove: removeDocument,\n isLoading: state.isLoading,\n isSynced: state.localState === undefined,\n isActive: state.isActive,\n load,\n sync,\n error: state.error,\n ref: collectionRef,\n })\n\n const getHandle = (): CollectionHandle<TData> => {\n if (cachedHandle === null) {\n cachedHandle = buildHandle()\n }\n return cachedHandle\n }\n\n // No constructor-side auto-start: callers (the hook for non-lazy, or\n // users directly for lazy) invoke load() to attach the listener. This\n // keeps subscription creation side-effect-free, matching document.ts.\n\n return {\n load,\n stop,\n subscribe,\n getState: getPublicState,\n getHandle,\n sync,\n }\n}\n","import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport type { QueryConstraint } from \"firebase/firestore\";\nimport type {\n CollectionDefinition,\n CollectionHandle,\n DocumentDefinition,\n DocumentHandle,\n FirestoreObject,\n UndoManager,\n UndoManagerState,\n UpdateOptions,\n} from \"./types\";\nimport type { FirestateStore } from \"./store\";\nimport { createDocumentSubscription } from \"./document\";\nimport { createCollectionSubscription } from \"./collection\";\n\n/**\n * Returned when a hook is called with `enabled: false`. Module-level constants\n * so getSnapshot returns a stable reference and useSyncExternalStore doesn't\n * re-render. Cast at the call site to the generic handle type — every method\n * is a no-op so the cast is sound.\n */\nconst NOOP = () => {};\nconst ASYNC_NOOP = async () => {};\nconst EMPTY_RECORD: Record<string, never> = {};\n\nconst DISABLED_DOCUMENT_HANDLE: DocumentHandle<FirestoreObject> = {\n data: undefined,\n update: NOOP,\n set: NOOP,\n delete: NOOP,\n isLoading: false,\n isSynced: true,\n sync: ASYNC_NOOP,\n error: undefined,\n ref: undefined,\n};\n\n// The disabled add() satisfies both overloads but performs no work and\n// returns undefined to match the bail-path contract from collection.ts.\n// Consumers using `enabled: false` should not be calling mutation methods\n// on the disabled handle.\nconst DISABLED_ADD = () => undefined;\n\nconst DISABLED_COLLECTION_HANDLE: CollectionHandle<FirestoreObject> = {\n data: EMPTY_RECORD,\n update: NOOP,\n add: DISABLED_ADD,\n remove: NOOP,\n isLoading: false,\n isSynced: true,\n isActive: false,\n load: NOOP,\n sync: ASYNC_NOOP,\n error: undefined,\n ref: undefined,\n};\n\n/**\n * Context for providing the Firestate store\n */\nexport const FirestateContext = createContext<FirestateStore | null>(null);\n\n/**\n * Hook to access the Firestate store\n */\nexport const useStore = (): FirestateStore => {\n const store = useContext(FirestateContext);\n if (!store) {\n throw new Error(\"useStore must be used within a FirestateProvider\");\n }\n return store;\n};\n\n/**\n * Hook to access the undo manager\n */\nexport const useUndoManager = (): UndoManager => {\n const store = useStore();\n const { undoManager } = store;\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => undoManager.subscribe(onStoreChange),\n [undoManager]\n );\n\n // Delegate to the manager's cached snapshot so getSnapshot returns a stable\n // reference across React's multiple per-commit calls. Building the snapshot\n // inline here would create a new object every call and trip the\n // \"getSnapshot should be cached\" warning + an infinite re-render loop.\n const getSnapshot = useCallback(\n (): UndoManagerState => undoManager.getState(),\n [undoManager]\n );\n\n const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n return useMemo(\n () => ({\n ...state,\n push: undoManager.push,\n undo: undoManager.undo,\n redo: undoManager.redo,\n clear: undoManager.clear,\n }),\n [state, undoManager]\n );\n};\n\n/**\n * Hook to check if all tracked resources are synced\n */\nexport const useIsSynced = (): boolean => {\n const store = useStore();\n\n const subscribe = useCallback(\n (onChange: () => void) => store.subscribeToSyncState(() => onChange()),\n [store]\n );\n\n const getSnapshot = useCallback(() => store.isSynced, [store]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n};\n\n/**\n * Options for useDocument hook\n */\nexport interface UseDocumentOptions<TData extends FirestoreObject> {\n /** Document definition from defineDocument() */\n definition: DocumentDefinition<TData>;\n /** Route/path parameters for dynamic paths */\n params?: Record<string, string>;\n /** Override read-only setting */\n readOnly?: boolean;\n /** Enable undo/redo for this document (default: true) */\n undoable?: boolean;\n /**\n * If false, no subscription is created and a no-op handle is returned\n * (`{ data: undefined, isLoading: false, isSynced: true, ref: undefined }`).\n * Use this to gate subscriptions on route params that aren't ready yet.\n * Default: true.\n */\n enabled?: boolean;\n}\n\n/**\n * Hook to subscribe to a Firestore document with real-time updates.\n *\n * The subscription is keyed on the resolved document path (`definition` +\n * computed id) and `readOnly`. When that key changes — typically because\n * `params` produces a different id — the hook tears down the old Firestore\n * listener and attaches a new one. Toggling `undoable` does not rebuild the\n * subscription.\n *\n * Use `enabled: false` to suppress the subscription entirely (e.g., when\n * route params aren't ready yet).\n *\n * **SSR.** On the server there is no Firestore listener, so this hook returns\n * the initial handle (`{ data: undefined, isLoading: true }`). Mutations like\n * `update`/`set` will mutate orphaned local state with no effect — avoid\n * calling them server-side.\n *\n * @example\n * ```tsx\n * const projectDoc = defineDocument<Project>({\n * collection: 'projects',\n * id: (params) => params.projectId,\n * })\n *\n * function ProjectEditor({ projectId }: { projectId: string }) {\n * const { data, update, isLoading, isSynced } = useDocument({\n * definition: projectDoc,\n * params: { projectId },\n * })\n *\n * if (isLoading) return <Spinner />\n *\n * return (\n * <input\n * value={data?.name ?? ''}\n * onChange={(e) => update({ name: e.target.value })}\n * />\n * )\n * }\n * ```\n */\nexport const useDocument = <TData extends FirestoreObject>(\n options: UseDocumentOptions<TData>\n): DocumentHandle<TData> => {\n const {\n definition,\n params = {},\n readOnly,\n undoable = true,\n enabled = true,\n } = options;\n const store = useStore();\n const undoManager = store.undoManager;\n\n // Hold the latest `undoable` in a ref so the onPushUndo callback can stay\n // referentially stable. Without this, every undoable toggle would tear\n // down the Firestore listener and re-attach it for no good reason.\n const undoableRef = useRef(undoable);\n undoableRef.current = undoable;\n\n const onPushUndo = useCallback(\n (undoAction: () => void, redoAction: () => void, opts?: UpdateOptions) => {\n if (!undoableRef.current) return;\n undoManager.push({\n undo: undoAction,\n redo: redoAction,\n groupId: opts?.undoGroupId,\n });\n },\n [undoManager]\n );\n\n // Resolve the doc id and collection path at render time. When disabled we\n // skip resolution — consumers commonly pass `enabled: false` precisely\n // because params aren't ready and definition.id(params) would fail.\n const docId = enabled\n ? typeof definition.id === \"function\"\n ? definition.id(params)\n : definition.id\n : undefined;\n\n const collectionPath = enabled\n ? typeof definition.collection === \"function\"\n ? definition.collection(params)\n : definition.collection\n : undefined;\n\n const subscription = useMemo(\n () =>\n enabled && docId !== undefined && collectionPath !== undefined\n ? createDocumentSubscription({\n store,\n definition,\n docId,\n collectionPath,\n readOnly,\n onPushUndo,\n })\n : null,\n [enabled, store, definition, docId, collectionPath, readOnly, onPushUndo]\n );\n\n const subscribe = useCallback(\n (onChange: () => void) => {\n if (!subscription) return NOOP;\n const unsub = subscription.subscribe(() => onChange());\n subscription.load();\n return () => {\n unsub();\n subscription.stop();\n };\n },\n [subscription]\n );\n\n const getSnapshot = useCallback(\n () =>\n subscription\n ? subscription.getHandle()\n : (DISABLED_DOCUMENT_HANDLE as DocumentHandle<TData>),\n [subscription]\n );\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n};\n\n/**\n * Options for useCollection hook\n */\nexport interface UseCollectionOptions<TData extends FirestoreObject> {\n /** Collection definition from defineCollection() */\n definition: CollectionDefinition<TData>;\n /** Route/path parameters for dynamic paths */\n params?: Record<string, string>;\n /** Override read-only setting */\n readOnly?: boolean;\n /** Additional query constraints */\n queryConstraints?: QueryConstraint[];\n /** Enable undo/redo for this collection (default: true) */\n undoable?: boolean;\n /**\n * If false, no subscription is created and a no-op handle is returned\n * (`{ data: {}, isLoading: false, isActive: false }`). Use this to gate on\n * route params that aren't ready yet. Default: true.\n */\n enabled?: boolean;\n}\n\n/**\n * Hook to subscribe to a Firestore collection with real-time updates.\n *\n * The subscription is keyed on the resolved collection path, `readOnly`, and\n * the `queryConstraints` reference. When any of these change, the listener\n * is torn down and re-attached with the new query. Toggling `undoable` does\n * not rebuild the subscription.\n *\n * **Memoize `queryConstraints`.** An inline array (`queryConstraints={[where(...)]}`)\n * creates a new reference every render, which will thrash the listener.\n * Wrap in `useMemo` with the underlying filter values as deps.\n *\n * Use `enabled: false` to suppress the subscription entirely (e.g., when\n * route params aren't ready yet).\n *\n * **SSR.** On the server there is no Firestore listener, so this hook returns\n * the initial handle (`{ data: {}, isLoading: true }` for non-lazy, or\n * `isActive: false` for lazy). Avoid calling mutations server-side.\n *\n * @example\n * ```tsx\n * const spacesCollection = defineCollection<Space>({\n * path: (params) => `projects/${params.projectId}/spaces`,\n * lazy: true,\n * })\n *\n * function SpacesList({ projectId }: { projectId: string }) {\n * const { data, update, load, isActive, isLoading } = useCollection({\n * definition: spacesCollection,\n * params: { projectId },\n * })\n *\n * // Lazy load on mount\n * useEffect(() => { load() }, [load])\n *\n * if (!isActive) return <Button onClick={load}>Load Spaces</Button>\n * if (isLoading) return <Spinner />\n *\n * return (\n * <ul>\n * {Object.values(data).map((space) => (\n * <li key={space.id}>{space.name}</li>\n * ))}\n * </ul>\n * )\n * }\n * ```\n */\nexport const useCollection = <TData extends FirestoreObject>(\n options: UseCollectionOptions<TData>\n): CollectionHandle<TData> => {\n const {\n definition,\n params = {},\n readOnly,\n queryConstraints,\n undoable = true,\n enabled = true,\n } = options;\n const store = useStore();\n const undoManager = store.undoManager;\n\n const undoableRef = useRef(undoable);\n undoableRef.current = undoable;\n\n const onPushUndo = useCallback(\n (undoAction: () => void, redoAction: () => void, opts?: UpdateOptions) => {\n if (!undoableRef.current) return;\n undoManager.push({\n undo: undoAction,\n redo: redoAction,\n groupId: opts?.undoGroupId,\n });\n },\n [undoManager]\n );\n\n // Resolve the collection path at render time. When disabled we skip\n // resolution — consumers commonly pass `enabled: false` precisely because\n // params aren't ready.\n const collectionPath = enabled\n ? typeof definition.path === \"function\"\n ? definition.path(params)\n : definition.path\n : undefined;\n\n const subscription = useMemo(\n () =>\n enabled && collectionPath !== undefined\n ? createCollectionSubscription({\n store,\n definition,\n collectionPath,\n readOnly,\n queryConstraints,\n onPushUndo,\n })\n : null,\n [\n enabled,\n store,\n definition,\n collectionPath,\n readOnly,\n queryConstraints,\n onPushUndo,\n ]\n );\n\n const isLazy = definition.lazy ?? false;\n\n const subscribe = useCallback(\n (onChange: () => void) => {\n if (!subscription) return NOOP;\n const unsub = subscription.subscribe(() => onChange());\n if (!isLazy) {\n subscription.load();\n }\n return () => {\n unsub();\n subscription.stop();\n };\n },\n [subscription, isLazy]\n );\n\n const getSnapshot = useCallback(\n () =>\n subscription\n ? subscription.getHandle()\n : (DISABLED_COLLECTION_HANDLE as CollectionHandle<TData>),\n [subscription]\n );\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n};\n\n/**\n * Keyboard shortcut hook for undo/redo\n *\n * @example\n * ```tsx\n * function App() {\n * useUndoKeyboardShortcuts()\n * return <YourApp />\n * }\n * ```\n */\nexport const useUndoKeyboardShortcuts = (): void => {\n // Read the manager ref directly — we only need .undo() / .redo() (stable\n // refs), not its state. Subscribing via useUndoManager would re-render\n // the host component on every undo-stack change.\n const undoManager = useStore().undoManager;\n\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n const platform =\n (\n navigator as Navigator & {\n userAgentData?: { platform: string };\n }\n ).userAgentData?.platform ?? navigator.platform;\n const isMac = platform.toUpperCase().includes(\"MAC\");\n const modifier = isMac ? e.metaKey : e.ctrlKey;\n\n if (!modifier) return;\n\n if (e.key === \"z\" && !e.shiftKey) {\n e.preventDefault();\n undoManager.undo();\n } else if ((e.key === \"z\" && e.shiftKey) || e.key === \"y\") {\n e.preventDefault();\n undoManager.redo();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [undoManager]);\n};\n","/**\n * Registry-driven Firestate API.\n *\n * Declare every document and collection in a single object and let the\n * library generate the typed read/write hooks. Replaces hand-writing a\n * `useSpaces`, `useWallTypes`, ... hook per Firestore collection.\n *\n * ```ts\n * interface TaskList { name: string; createdAt: number }\n * interface Task { title: string; completed: boolean }\n *\n * export const { useTaskList, useTasks } = createFirestate({\n * taskList: doc<TaskList>('taskLists/{listId}'),\n * tasks: col<Task>('taskLists/{listId}/tasks'),\n * })\n *\n * // At the call site:\n * const taskList = useTaskList({ listId })\n * const tasks = useTasks({ listId })\n * ```\n */\nimport { defineDocument, defineCollection } from \"./schema\";\nimport {\n useDocument,\n useCollection,\n type UseDocumentOptions,\n type UseCollectionOptions,\n} from \"./hooks\";\nimport type {\n CollectionDefinition,\n CollectionHandle,\n DocumentDefinition,\n DocumentHandle,\n FirestoreObject,\n} from \"./types\";\nimport type { QueryConstraint } from \"firebase/firestore\";\nimport type { ZodType, z } from \"zod\";\n\n/**\n * Knobs forwarded from a generated document hook to {@link useDocument}.\n * Same shape as `UseDocumentOptions` minus the fields the registry already\n * owns (`definition`, `params`).\n */\nexport type DocHookOptions<T extends FirestoreObject> = Omit<\n UseDocumentOptions<T>,\n \"definition\" | \"params\"\n>;\n\n/**\n * Knobs forwarded from a generated collection hook to {@link useCollection}.\n */\nexport type ColHookOptions<T extends FirestoreObject> = Omit<\n UseCollectionOptions<T>,\n \"definition\" | \"params\"\n>;\n\n// ---------------------------------------------------------------------------\n// Registry entry shapes\n// ---------------------------------------------------------------------------\n\ninterface CommonEntryOptions {\n /** Debounce interval for autosave (ms). */\n autosave?: number;\n /** Minimum loading indicator time (ms). */\n minLoadTime?: number;\n /** Whether this entry is read-only. */\n readOnly?: boolean;\n /** Retry the snapshot listener on transient errors. */\n retryOnError?: boolean;\n /** Retry interval (ms). */\n retryInterval?: number;\n}\n\n/**\n * Document entry in a Firestate registry. Produced by {@link doc}.\n *\n * The `P` generic carries the path template's string-literal type so the\n * generated hook can type-check param keys. `__kind` is a runtime\n * discriminator; `__type` is a phantom field used purely for inference at\n * the call site and is never read.\n */\nexport interface DocEntry<\n T extends FirestoreObject,\n P extends string = string\n> extends CommonEntryOptions {\n readonly __kind: \"document\";\n readonly __type?: T;\n /** Path template, e.g. `'taskLists/{listId}'`. */\n path: P;\n /**\n * Zod schema. **Required** — firestate's registry API is opinionated\n * about Zod. The schema is the source of `T` for the generated hooks\n * via `z.infer`, and firestate runs `schema.parse(...)` on full-payload\n * writes (`set`/`add`) so bad data throws at the call site rather than\n * after a Firestore round trip. Partial `update(diff)` is NOT validated\n * (diffs frequently contain Firestore sentinels like `serverTimestamp()`).\n *\n * If you don't want a schema at all, use {@link defineDocument} directly —\n * the escape hatch keeps the plain-TypeScript form at the cost of looser\n * param typing and no runtime validation.\n */\n schema: ZodType<T>;\n}\n\n/** Collection entry in a Firestate registry. Produced by {@link col}. */\nexport interface ColEntry<\n T extends FirestoreObject,\n P extends string = string\n> extends CommonEntryOptions {\n readonly __kind: \"collection\";\n readonly __type?: T;\n /** Path template, e.g. `'taskLists/{listId}/tasks'`. */\n path: P;\n /** Zod schema. Required. See {@link DocEntry.schema}. */\n schema: ZodType<T>;\n /** Only subscribe when `load()` is called. */\n lazy?: boolean;\n /** Additional Firestore query constraints. */\n queryConstraints?: QueryConstraint[];\n}\n\nexport type FirestateEntry<\n T extends FirestoreObject = FirestoreObject,\n P extends string = string\n> = DocEntry<T, P> | ColEntry<T, P>;\n\nexport type FirestateRegistry = Record<string, FirestateEntry<any, any>>;\n\n// ---------------------------------------------------------------------------\n// Path → params extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Extract `{name}` placeholders from a path template into a params shape.\n *\n * - `'users'` → `{}`\n * - `'users/{userId}'` → `{ userId: string }`\n * - `'projects/{projectId}/revisions/{revisionId}'` → `{ projectId: string; revisionId: string }`\n *\n * When the path is widened to `string` (no literal preserved), we fall\n * back to `Record<string, string>` so existing call sites keep compiling.\n */\nexport type ParamsOf<P extends string> = string extends P\n ? Record<string, string>\n : Prettify<RawParamsOf<P>>;\n\ntype RawParamsOf<P extends string> =\n P extends `${string}{${infer K}}${infer Rest}`\n ? { [Key in K]: string } & RawParamsOf<Rest>\n : {};\n\n// Force TS to evaluate intersections so error messages show\n// `{ projectId: string; revisionId: string }` instead of an intersection.\ntype Prettify<T> = { [K in keyof T]: T[K] } & {};\n\n// ---------------------------------------------------------------------------\n// Entry factories\n// ---------------------------------------------------------------------------\n\ntype DocOpts<T extends FirestoreObject> = Omit<DocEntry<T>, \"__kind\" | \"__type\" | \"path\">;\ntype ColOpts<T extends FirestoreObject> = Omit<ColEntry<T>, \"__kind\" | \"__type\" | \"path\">;\n\n/**\n * Declare a single-document entry for a Firestate registry.\n *\n * **A Zod `schema` field is required.** Both the data type (`T`) and the\n * path's literal type (`P`) are inferred from the call — `T` via\n * `z.infer<S>`, `P` from `path` — so the generated hook can statically\n * type-check the params object the caller passes. The schema also runs\n * at runtime on full-payload writes (`set`/`add`).\n *\n * If you'd rather not provide a schema at all, use {@link defineDocument}\n * directly — that escape hatch keeps the plain-TypeScript form, at the\n * cost of looser param typing on the hook and no runtime validation.\n *\n * ```ts\n * import { z } from 'zod'\n *\n * const TaskListSchema = z.object({ name: z.string(), createdAt: z.number() })\n * doc({ path: 'taskLists/{listId}', schema: TaskListSchema })\n * // → DocEntry<{ name: string; createdAt: number }, 'taskLists/{listId}'>\n * ```\n */\nexport function doc<\n S extends ZodType<FirestoreObject>,\n const P extends string = string\n>(\n opts: Omit<DocOpts<z.infer<S>>, \"schema\"> & {\n schema: S;\n path: P;\n }\n): DocEntry<z.infer<S>, P> {\n const { path, ...rest } = opts;\n validateTemplate(path);\n // Bail at registration time if the path can't be split into a non-empty\n // collection + id — same loud-at-the-boundary spirit as interpolate.\n splitDocPath(path);\n return { __kind: \"document\", path, ...rest } as unknown as DocEntry<\n z.infer<S>,\n P\n >;\n}\n\n/**\n * Declare a collection entry for a Firestate registry. See {@link doc}\n * for the schema/typing contract.\n */\nexport function col<\n S extends ZodType<FirestoreObject>,\n const P extends string = string\n>(\n opts: Omit<ColOpts<z.infer<S>>, \"schema\"> & {\n schema: S;\n path: P;\n }\n): ColEntry<z.infer<S>, P> {\n const { path, ...rest } = opts;\n validateTemplate(path);\n return { __kind: \"collection\", path, ...rest } as unknown as ColEntry<\n z.infer<S>,\n P\n >;\n}\n\n// ---------------------------------------------------------------------------\n// createFirestate\n// ---------------------------------------------------------------------------\n\ntype HookName<K extends string> = `use${Capitalize<K>}`;\n\n// If the path template has no placeholders, `params` is optional (any\n// caller-supplied object is fine). When the template has placeholders,\n// the caller must pass an object with exactly the extracted keys.\ntype HookFor<E> = E extends DocEntry<infer T, infer P>\n ? keyof ParamsOf<P> extends never\n ? (\n params?: Record<string, string>,\n options?: DocHookOptions<T>\n ) => DocumentHandle<T>\n : (\n params: ParamsOf<P>,\n options?: DocHookOptions<T>\n ) => DocumentHandle<T>\n : E extends ColEntry<infer T, infer P>\n ? keyof ParamsOf<P> extends never\n ? (\n params?: Record<string, string>,\n options?: ColHookOptions<T>\n ) => CollectionHandle<T>\n : (\n params: ParamsOf<P>,\n options?: ColHookOptions<T>\n ) => CollectionHandle<T>\n : never;\n\nexport type FirestateApi<R extends FirestateRegistry> = {\n [K in keyof R & string as HookName<K>]: HookFor<R[K]>;\n};\n\n/**\n * Turn a Firestate registry into a map of typed React hooks. Each entry\n * `K` produces a hook named `use{Capitalize<K>}`.\n *\n * ```ts\n * export const { useTaskList, useTasks } = createFirestate({\n * taskList: doc<TaskList>('taskLists/{listId}'),\n * tasks: col<Task>('taskLists/{listId}/tasks'),\n * })\n * ```\n */\nexport function createFirestate<R extends FirestateRegistry>(\n registry: R\n): FirestateApi<R> {\n const api: Record<string, unknown> = {};\n\n for (const key of Object.keys(registry)) {\n if (!isValidKey(key)) {\n throw new Error(\n `[firestate] registry key \"${key}\" must start with a letter and contain only letters, digits, _ or $`\n );\n }\n const entry = registry[key]!;\n const hookName = toHookName(key);\n\n if (entry.__kind === \"document\") {\n const definition = buildDocumentDefinition(entry);\n api[hookName] = (\n params: Record<string, string> = {},\n options: DocHookOptions<FirestoreObject> = {}\n ) => useDocument({ ...options, definition, params });\n } else {\n const definition = buildCollectionDefinition(entry);\n api[hookName] = (\n params: Record<string, string> = {},\n options: ColHookOptions<FirestoreObject> = {}\n ) => useCollection({ ...options, definition, params });\n }\n }\n\n return api as FirestateApi<R>;\n}\n\n/**\n * Build the underlying {@link DocumentDefinition} for a registry doc entry.\n * Exported for unit testing — registry consumers should call\n * {@link createFirestate} instead.\n *\n * @internal\n */\nexport function buildDocumentDefinition<T extends FirestoreObject>(\n entry: DocEntry<T>\n): DocumentDefinition<T> {\n const { collectionPath, idTemplate } = splitDocPath(entry.path);\n // Both halves are functions so any `{param}` placeholder in the\n // collection portion (e.g. `projects/{projectId}/revisions`) is\n // resolved per-call against the params passed to the hook.\n return defineDocument<T>({\n schema: entry.schema,\n collection: (params) => interpolate(collectionPath, params),\n id: (params) => interpolate(idTemplate, params),\n autosave: entry.autosave,\n minLoadTime: entry.minLoadTime,\n readOnly: entry.readOnly,\n retryOnError: entry.retryOnError,\n retryInterval: entry.retryInterval,\n } as DocumentDefinition<T>);\n}\n\n/**\n * Build the underlying {@link CollectionDefinition} for a registry col entry.\n *\n * @internal\n */\nexport function buildCollectionDefinition<T extends FirestoreObject>(\n entry: ColEntry<T>\n): CollectionDefinition<T> {\n return defineCollection<T>({\n schema: entry.schema,\n path: (params) => interpolate(entry.path, params),\n autosave: entry.autosave,\n minLoadTime: entry.minLoadTime,\n readOnly: entry.readOnly,\n lazy: entry.lazy,\n queryConstraints: entry.queryConstraints,\n retryOnError: entry.retryOnError,\n retryInterval: entry.retryInterval,\n } as CollectionDefinition<T>);\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers (also exported for testing)\n// ---------------------------------------------------------------------------\n\nconst VALID_KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\nfunction isValidKey(key: string): boolean {\n return VALID_KEY.test(key);\n}\n\nfunction toHookName(key: string): string {\n return `use${key[0]!.toUpperCase()}${key.slice(1)}`;\n}\n\n/**\n * Replace `{name}` placeholders in a path template with values from `params`.\n * Throws if a placeholder is missing from `params` — failing loud at the\n * boundary is better than silently building a `taskLists/undefined/tasks`\n * URL and getting a useless Firestore error later.\n *\n * @internal\n */\nexport function interpolatePath(\n template: string,\n params: Record<string, string>\n): string {\n return interpolate(template, params);\n}\n\n// Matches a single `{name}` placeholder where the name is a valid JS-ish\n// identifier (letter or underscore start, then letters/digits/underscores).\n// Used both to interpolate and to validate templates up front.\nconst PLACEHOLDER = /\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g;\n\n/**\n * Validate that a path template uses only well-formed `{name}` placeholders\n * — no unclosed braces, no hyphens/dots inside placeholders, no `{1}` style\n * digit-leading names. Throws at definition time so a typo in the template\n * fails loud at `doc()` / `col()`, not three layers deep when a component\n * mounts.\n */\nfunction validateTemplate(template: string): void {\n // Strip the well-formed placeholders, then look for any stray `{` or `}` —\n // those signal a malformed (unclosed or weirdly-spelled) placeholder.\n const stripped = template.replace(PLACEHOLDER, \"\");\n if (stripped.includes(\"{\") || stripped.includes(\"}\")) {\n throw new Error(\n `[firestate] path \"${template}\" contains a malformed placeholder. ` +\n `Placeholders must look like \"{name}\" where name starts with a letter or underscore.`\n );\n }\n}\n\nfunction interpolate(template: string, params: Record<string, string>): string {\n return template.replace(PLACEHOLDER, (_, key) => {\n const v = params[key];\n if (v === undefined) {\n throw new Error(\n `[firestate] missing param \"${key}\" for path \"${template}\"`\n );\n }\n if (v === \"\") {\n // An empty value would silently produce `taskLists//tasks`, which\n // Firestore later rejects with an opaque \"Document path must not be\n // empty\" — keep the friendly error at the boundary.\n throw new Error(\n `[firestate] param \"${key}\" for path \"${template}\" must not be an empty string`\n );\n }\n return v;\n });\n}\n\n/**\n * Split a document path template into a collection path and an id template.\n * `'taskLists/{listId}'` → `{ collectionPath: 'taskLists', idTemplate: '{listId}' }`.\n *\n * @internal\n */\nexport function splitDocPath(path: string): {\n collectionPath: string;\n idTemplate: string;\n} {\n const lastSlash = path.lastIndexOf(\"/\");\n if (lastSlash === -1) {\n throw new Error(\n `[firestate] document path \"${path}\" must contain at least one '/' separating the collection from the document id`\n );\n }\n const collectionPath = path.slice(0, lastSlash);\n const idTemplate = path.slice(lastSlash + 1);\n if (collectionPath === \"\" || idTemplate === \"\") {\n throw new Error(\n `[firestate] document path \"${path}\" must have non-empty collection and id segments`\n );\n }\n return { collectionPath, idTemplate };\n}\n","import type { Subscriber, Unsubscribe, UndoAction, UndoManager, UndoManagerState } from './types'\n\n/**\n * Configuration for creating an undo manager\n */\nexport interface UndoManagerConfig {\n /** Maximum number of undo actions to keep, default 20 */\n maxLength?: number\n /** Callback when navigation is requested (for path-aware undo) */\n onNavigate?: (path: string) => void\n}\n\n/**\n * Create an undo manager instance.\n * This is a standalone, framework-agnostic implementation.\n *\n * @example\n * ```ts\n * const undoManager = createUndoManager({ maxLength: 10 })\n *\n * undoManager.push({\n * undo: () => restoreOldValue(),\n * redo: () => applyNewValue(),\n * description: 'Update project name',\n * })\n *\n * await undoManager.undo() // Calls restoreOldValue()\n * await undoManager.redo() // Calls applyNewValue()\n * ```\n */\nexport const createUndoManager = (\n config: UndoManagerConfig = {}\n): UndoManager & {\n subscribe: (fn: Subscriber<UndoManagerState>) => Unsubscribe\n getState: () => UndoManagerState\n} => {\n const { maxLength = 20, onNavigate } = config\n\n let undoStack: UndoAction[] = []\n let redoStack: UndoAction[] = []\n const subscribers = new Set<Subscriber<UndoManagerState>>()\n // Cached snapshot — returns the same reference until notify() invalidates\n // it. Required so React's useSyncExternalStore consumers (useUndoManager)\n // see a stable snapshot across the multiple getSnapshot() calls React\n // makes per commit; otherwise the inequality on Object.is triggers an\n // infinite re-render and the \"getSnapshot should be cached\" warning.\n let cachedState: UndoManagerState | null = null\n\n const getState = (): UndoManagerState => {\n if (cachedState === null) {\n cachedState = {\n undoStack,\n redoStack,\n canUndo: undoStack.length > 0,\n canRedo: redoStack.length > 0,\n }\n }\n return cachedState\n }\n\n const notify = () => {\n cachedState = null\n const state = getState()\n subscribers.forEach((fn) => fn(state))\n }\n\n const push = (action: UndoAction) => {\n // Check if we should merge with previous action (same groupId)\n if (action.groupId && undoStack.length > 0) {\n const last = undoStack[undoStack.length - 1]\n if (last?.groupId === action.groupId) {\n // Pop and merge. Undo walks the group newest→oldest so each\n // step reverses its specific change in reverse order; redo\n // walks oldest→newest to re-apply in original order. The\n // previous (older) order produced incorrect cumulative state\n // whenever grouped actions touched the same field.\n undoStack.pop()\n undoStack.push({\n undo: async () => {\n await action.undo()\n await last.undo()\n },\n redo: async () => {\n await last.redo()\n await action.redo()\n },\n groupId: action.groupId,\n path: action.path ?? last.path,\n description: action.description ?? last.description,\n })\n // Clear redo stack on any new action\n redoStack = []\n notify()\n return\n }\n }\n\n undoStack.push(action)\n\n // Enforce max length\n if (undoStack.length > maxLength) {\n undoStack.shift()\n }\n\n // Clear redo stack on any new action\n redoStack = []\n notify()\n }\n\n const undo = async () => {\n const action = undoStack.pop()\n if (!action) return\n\n // Navigate if path is set\n if (action.path && onNavigate) {\n onNavigate(action.path)\n }\n\n try {\n await action.undo()\n redoStack.push(action)\n } catch (error) {\n // Put it back on undo stack if it failed\n undoStack.push(action)\n console.error('Undo failed:', error)\n throw error\n }\n\n notify()\n }\n\n const redo = async () => {\n const action = redoStack.pop()\n if (!action) return\n\n // Navigate if path is set\n if (action.path && onNavigate) {\n onNavigate(action.path)\n }\n\n try {\n await action.redo()\n undoStack.push(action)\n\n // Enforce max length\n if (undoStack.length > maxLength) {\n undoStack.shift()\n }\n } catch (error) {\n // Put it back on redo stack if it failed\n redoStack.push(action)\n console.error('Redo failed:', error)\n throw error\n }\n\n notify()\n }\n\n const clear = () => {\n undoStack = []\n redoStack = []\n notify()\n }\n\n const subscribe = (fn: Subscriber<UndoManagerState>): Unsubscribe => {\n subscribers.add(fn)\n return () => subscribers.delete(fn)\n }\n\n return {\n get undoStack() {\n return undoStack\n },\n get redoStack() {\n return redoStack\n },\n get canUndo() {\n return undoStack.length > 0\n },\n get canRedo() {\n return redoStack.length > 0\n },\n push,\n undo,\n redo,\n clear,\n subscribe,\n getState,\n }\n}\n\n/**\n * Type for the undo manager with subscription capability\n */\nexport type UndoManagerWithSubscribe = ReturnType<typeof createUndoManager>\n","import type { Firestore } from 'firebase/firestore'\nimport type { ErrorContext, FirestateConfig, Subscriber, Unsubscribe } from './types'\nimport { createUndoManager, type UndoManagerWithSubscribe } from './undo'\n\n/**\n * Firestate store that holds configuration and shared state\n */\nexport interface FirestateStore {\n /** Firestore instance */\n readonly firestore: Firestore\n /** Undo manager instance */\n readonly undoManager: UndoManagerWithSubscribe\n /** Default autosave interval (ms) */\n readonly autosave: number\n /** Default minimum load time (ms) */\n readonly minLoadTime: number\n /** Report an error */\n reportError: (error: Error, context: ErrorContext) => void\n /**\n * Replace the error handler at runtime. Used by FirestateProvider to keep\n * the store identity stable when consumers pass an inline `onError`\n * callback that changes reference on every render.\n */\n setOnError: (handler?: (error: Error, context: ErrorContext) => void) => void\n /**\n * Replace the navigation handler at runtime. Used by FirestateProvider to\n * keep the store identity stable when consumers pass an inline `onNavigate`\n * callback that changes reference on every render.\n */\n setOnNavigate: (handler?: (path: string) => void) => void\n /** Subscribe to sync state changes */\n subscribeToSyncState: (fn: Subscriber<boolean>) => Unsubscribe\n /** Report a document/collection sync state change */\n reportSyncState: (key: string, isSynced: boolean) => void\n /**\n * Remove a sync-state key. Subscriptions call this on stop() so an\n * unmounted hook does not leave the global isSynced stuck at false.\n */\n unregisterSyncState: (key: string) => void\n /** Get whether all tracked resources are synced */\n readonly isSynced: boolean\n}\n\n/**\n * Create a Firestate store.\n * This is the central configuration point for your Firestore state management.\n *\n * @example\n * ```ts\n * import { createStore } from 'firestate'\n * import { db } from './firebase'\n *\n * export const store = createStore({\n * firestore: db,\n * autosave: 1000,\n * maxUndoLength: 20,\n * onError: (error, context) => {\n * console.error(`Error in ${context.type} ${context.path}:`, error)\n * },\n * })\n * ```\n */\nexport const createStore = (config: FirestateConfig): FirestateStore => {\n const {\n firestore,\n autosave = 1000,\n minLoadTime = 0,\n maxUndoLength = 20,\n } = config\n\n // Mutable so the provider can update them without re-creating the store.\n let onError = config.onError\n let onNavigate = config.onNavigate\n\n const undoManager = createUndoManager({\n maxLength: maxUndoLength,\n // Stable wrapper — delegates to the mutable onNavigate ref so the\n // undo manager doesn't need to be recreated when the callback changes.\n onNavigate: (path) => onNavigate?.(path),\n })\n\n // Track sync state of all documents/collections\n const syncStates = new Map<string, boolean>()\n const syncSubscribers = new Set<Subscriber<boolean>>()\n\n const computeIsSynced = (): boolean => {\n for (const synced of syncStates.values()) {\n if (!synced) return false\n }\n return true\n }\n\n const notifySyncSubscribers = () => {\n const isSynced = computeIsSynced()\n syncSubscribers.forEach((fn) => fn(isSynced))\n }\n\n return {\n firestore,\n undoManager,\n autosave,\n minLoadTime,\n\n reportError: (error, context) => {\n if (onError) {\n onError(error, context)\n } else {\n console.error(\n `Firestate error in ${context.type} ${context.path} during ${context.operation}:`,\n error\n )\n }\n },\n\n setOnError: (handler) => {\n onError = handler\n },\n\n setOnNavigate: (handler) => {\n onNavigate = handler\n },\n\n subscribeToSyncState: (fn) => {\n syncSubscribers.add(fn)\n return () => syncSubscribers.delete(fn)\n },\n\n reportSyncState: (key, isSynced) => {\n const prev = syncStates.get(key)\n if (prev !== isSynced) {\n syncStates.set(key, isSynced)\n notifySyncSubscribers()\n }\n },\n\n unregisterSyncState: (key) => {\n const prev = syncStates.get(key)\n if (prev === undefined) return\n syncStates.delete(key)\n // Removing a `false` entry can flip global isSynced to true.\n if (prev === false) {\n notifySyncSubscribers()\n }\n },\n\n get isSynced() {\n return computeIsSynced()\n },\n }\n}\n\n/**\n * Type alias for the store type\n */\nexport type Store = ReturnType<typeof createStore>\n","import React, {\n useCallback,\n useEffect,\n useMemo,\n useSyncExternalStore,\n} from \"react\";\nimport type { Firestore } from \"firebase/firestore\";\nimport { createStore, type FirestateStore } from \"./store\";\nimport { FirestateContext } from \"./hooks\";\nimport type { ErrorContext } from \"./types\";\n\n/**\n * Props for FirestateProvider\n */\nexport interface FirestateProviderProps {\n /** Firestore instance */\n firestore: Firestore;\n /** Default autosave interval (ms), default 1000 */\n autosave?: number;\n /** Default minimum load time (ms), default 0 */\n minLoadTime?: number;\n /** Maximum undo stack length, default 20 */\n maxUndoLength?: number;\n /**\n * Called before undo/redo when the action carries a `path`. Wire your\n * router's `navigate` here to return users to where a change occurred\n * before reverting it.\n *\n * @example\n * ```tsx\n * import { useNavigate } from 'react-router-dom'\n *\n * function App() {\n * const navigate = useNavigate()\n * return (\n * <FirestateProvider onNavigate={(path) => navigate(path)}>\n * {children}\n * </FirestateProvider>\n * )\n * }\n * ```\n */\n onNavigate?: (path: string) => void;\n /** Custom error handler */\n onError?: (error: Error, context: ErrorContext) => void;\n /** React children */\n children: React.ReactNode;\n}\n\n/**\n * Provider component that sets up Firestate for your application.\n *\n * @example\n * ```tsx\n * import { FirestateProvider } from 'firestate'\n * import { db } from './firebase'\n *\n * function App() {\n * return (\n * <FirestateProvider\n * firestore={db}\n * autosave={1000}\n * maxUndoLength={20}\n * onError={(error, ctx) => console.error(ctx.path, error)}\n * >\n * <YourApp />\n * </FirestateProvider>\n * )\n * }\n * ```\n */\nexport const FirestateProvider: React.FC<FirestateProviderProps> = ({\n firestore,\n autosave = 1000,\n minLoadTime = 0,\n maxUndoLength = 20,\n onError,\n onNavigate,\n children,\n}) => {\n // onError and onNavigate are intentionally excluded from the deps so that\n // inline callbacks (new reference per render) do not re-create the store and\n // drop every existing subscription. The store exposes setOnError /\n // setOnNavigate so the latest handlers can be applied without store\n // re-creation.\n const store = useMemo(\n () =>\n createStore({\n firestore,\n autosave,\n minLoadTime,\n maxUndoLength,\n onError,\n onNavigate,\n }),\n [firestore, autosave, minLoadTime, maxUndoLength]\n );\n\n useEffect(() => {\n store.setOnError(onError);\n }, [store, onError]);\n\n useEffect(() => {\n store.setOnNavigate(onNavigate);\n }, [store, onNavigate]);\n\n return (\n <FirestateContext.Provider value={store}>\n {children}\n </FirestateContext.Provider>\n );\n};\n\n/**\n * Props for using an existing store\n */\nexport interface FirestateStoreProviderProps {\n /** Pre-created store instance */\n store: FirestateStore;\n /** React children */\n children: React.ReactNode;\n}\n\n/**\n * Provider that uses an existing store instance.\n * Useful when you need to create the store outside of React.\n *\n * @example\n * ```tsx\n * const store = createStore({ firestore: db })\n *\n * function App() {\n * return (\n * <FirestateStoreProvider store={store}>\n * <YourApp />\n * </FirestateStoreProvider>\n * )\n * }\n * ```\n */\nexport const FirestateStoreProvider: React.FC<FirestateStoreProviderProps> = ({\n store,\n children,\n}) => (\n <FirestateContext.Provider value={store}>\n {children}\n </FirestateContext.Provider>\n);\n\n/**\n * Hook to use navigation blocker when there are unsaved changes.\n * Works with react-router or similar routers.\n *\n * @example\n * ```tsx\n * function ProjectPage() {\n * const shouldBlock = useUnsavedChangesBlocker()\n *\n * // Use with react-router's useBlocker\n * const blocker = useBlocker(\n * ({ currentLocation, nextLocation }) =>\n * currentLocation.pathname !== nextLocation.pathname && shouldBlock\n * )\n *\n * return (\n * <>\n * <ProjectEditor />\n * {blocker.state === 'blocked' && (\n * <Dialog>Your changes may not be saved!</Dialog>\n * )}\n * </>\n * )\n * }\n * ```\n */\nexport const useUnsavedChangesBlocker = (): boolean => {\n const store = React.useContext(FirestateContext);\n\n const subscribe = useCallback(\n (onChange: () => void) =>\n store ? store.subscribeToSyncState(() => onChange()) : () => {},\n [store]\n );\n\n const getSnapshot = useCallback(\n () => (store ? !store.isSynced : false),\n [store]\n );\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["syncKeyCounter","doc","doc","query"],"sources":["../src/registry/schema.ts","../src/utils/diff.ts","../src/core/collection.ts","../src/core/document.ts","../src/core/shared-subscription.ts","../src/react/hooks.ts","../src/registry/firestate.ts","../src/utils/shallow.ts","../src/utils/undo.ts","../src/core/store.ts","../src/react/provider.tsx"],"sourcesContent":["import type { ZodType, z } from \"zod\";\nimport type {\n CollectionDefinition,\n DocumentDefinition,\n FirestoreObject,\n} from \"../types\";\n\n/**\n * Define a typed document. `TData` is the document's TypeScript shape.\n *\n * **Most apps should reach for {@link createFirestate} + {@link doc} instead**\n * — that builds a registry of every Firestore thing in one object and\n * generates typed hooks for you. `defineDocument` is the lower-level\n * escape hatch: use it when you need fully custom `collection` / `id`\n * derivation, when you're calling firestate outside React, or when a\n * registry doesn't fit your control flow.\n *\n * Two ways to use:\n *\n * 1. Plain TypeScript type (no schema, no runtime validation):\n * ```ts\n * interface Project { name: string; createdAt: number }\n *\n * const projectDoc = defineDocument<Project>({\n * collection: 'projects',\n * id: (params) => params.projectId,\n * })\n * ```\n *\n * 2. With a Zod schema — `TData` is inferred from `z.infer<S>`. Firestate\n * runs `schema.parse(...)` on full-payload writes (`set`/`add`) so bad\n * data throws at the call site. Partial `update(diff)` calls are not\n * validated (diffs frequently contain Firestore sentinels).\n * ```ts\n * import { z } from 'zod'\n *\n * const ProjectSchema = z.object({ name: z.string(), createdAt: z.number() })\n *\n * const projectDoc = defineDocument({\n * schema: ProjectSchema,\n * collection: 'projects',\n * id: (params) => params.projectId,\n * })\n * ```\n */\nexport function defineDocument<S extends ZodType<FirestoreObject>>(\n definition: Omit<DocumentDefinition<z.infer<S>>, \"schema\"> & {\n schema: S;\n }\n): DocumentDefinition<z.infer<S>>;\nexport function defineDocument<TData extends FirestoreObject>(\n definition: DocumentDefinition<TData>\n): DocumentDefinition<TData>;\nexport function defineDocument(\n definition: DocumentDefinition<FirestoreObject>\n): DocumentDefinition<FirestoreObject> {\n return definition;\n}\n\n/**\n * Define a typed collection. `TData` is the shape of each document in the\n * collection. See {@link defineDocument} for the schema/plain-type tradeoff.\n *\n * **Most apps should reach for {@link createFirestate} + {@link col} instead.**\n * `defineCollection` is the escape hatch for fully custom path derivation\n * or non-React usage.\n *\n * @example\n * ```ts\n * interface Space { name: string; area: number }\n *\n * const spacesCollection = defineCollection<Space>({\n * path: (params) => `projects/${params.projectId}/spaces`,\n * lazy: true,\n * })\n * ```\n */\nexport function defineCollection<S extends ZodType<FirestoreObject>>(\n definition: Omit<CollectionDefinition<z.infer<S>>, \"schema\"> & {\n schema: S;\n }\n): CollectionDefinition<z.infer<S>>;\nexport function defineCollection<TData extends FirestoreObject>(\n definition: CollectionDefinition<TData>\n): CollectionDefinition<TData>;\nexport function defineCollection(\n definition: CollectionDefinition<FirestoreObject>\n): CollectionDefinition<FirestoreObject> {\n return definition;\n}\n\n/**\n * Infer the document data type from a {@link DocumentDefinition}.\n */\nexport type InferDocumentData<T extends DocumentDefinition<FirestoreObject>> =\n T extends DocumentDefinition<infer D> ? D : never;\n\n/**\n * Infer the document data type (with `id` field) from a {@link DocumentDefinition}.\n */\nexport type InferDocument<T extends DocumentDefinition<FirestoreObject>> =\n InferDocumentData<T> & { id: string };\n\n/**\n * Infer the document data type from a {@link CollectionDefinition}.\n */\nexport type InferCollectionData<\n T extends CollectionDefinition<FirestoreObject>\n> = T extends CollectionDefinition<infer D> ? D : never;\n\n/**\n * Infer the document data type (with `id` field) from a {@link CollectionDefinition}.\n */\nexport type InferCollectionDocument<\n T extends CollectionDefinition<FirestoreObject>\n> = InferCollectionData<T> & { id: string };\n","import {\n deleteField,\n FieldPath,\n serverTimestamp,\n Timestamp,\n WithFieldValue,\n} from 'firebase/firestore'\nimport type { DeepPartial, FirestoreObject } from '../types'\n\n/**\n * Check if a value is a plain object (not array, null, or special Firestore type)\n */\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n value !== null &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n !(value instanceof Timestamp) &&\n Object.getPrototypeOf(value) === Object.prototype\n\n/**\n * Check if a value is a Firestore opaque type: a FieldValue sentinel\n * (`serverTimestamp`, `deleteField`, `increment`, `arrayUnion`,\n * `arrayRemove`, …) or a value type the SDK ships with its own identity\n * semantics (`Timestamp`, `DocumentReference`, `GeoPoint`, `Bytes`,\n * `VectorValue`). They all expose `.isEqual()` and have a non-plain\n * prototype.\n *\n * The diff / clone pipeline must treat these as **opaque**: never iterate\n * their keys, never substitute their values, never compare them by `===`.\n * Doing any of those silently breaks the write path — see the C1\n * regression where `serverTimestamp()` was replaced with `Timestamp.now()`\n * before reaching Firestore.\n */\nconst isFirestoreOpaque = (\n value: unknown\n): value is { isEqual: (other: unknown) => boolean } => {\n if (value === null || typeof value !== 'object') return false\n if (Object.getPrototypeOf(value) === Object.prototype) return false\n return (\n 'isEqual' in value &&\n typeof (value as { isEqual: unknown }).isEqual === 'function'\n )\n}\n\n// Reference sentinels used to identify specific FieldValue kinds. The\n// Firebase SDK does not export the sentinel subclasses; the only stable\n// way to ask \"is this a serverTimestamp / deleteField?\" is to construct a\n// reference instance once and delegate to its `.isEqual()`. Hoisting them\n// to module scope avoids reconstructing on every call.\nconst SERVER_TIMESTAMP_REF = serverTimestamp()\nconst DELETE_FIELD_REF = deleteField()\n\nconst isDeleteField = (value: unknown): boolean =>\n isFirestoreOpaque(value) && value.isEqual(DELETE_FIELD_REF)\n\nconst isServerTimestamp = (value: unknown): boolean =>\n isFirestoreOpaque(value) && value.isEqual(SERVER_TIMESTAMP_REF)\n\n/**\n * Check if two values are deeply equal\n */\nexport const isDeepEqual = (a: unknown, b: unknown): boolean => {\n if (a === b) return true\n if (a === null || b === null) return false\n if (typeof a !== typeof b) return false\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n return a.every((item, i) => isDeepEqual(item, b[i]))\n }\n\n // Opaque Firestore types delegate to their own `.isEqual`. Catches\n // Timestamp, DocumentReference, GeoPoint, Bytes, VectorValue and\n // every FieldValue sentinel kind.\n if (isFirestoreOpaque(a) && isFirestoreOpaque(b)) {\n return a.isEqual(b)\n }\n\n if (isPlainObject(a) && isPlainObject(b)) {\n const keysA = Object.keys(a)\n const keysB = Object.keys(b)\n if (keysA.length !== keysB.length) return false\n return keysA.every((key) => isDeepEqual(a[key], b[key]))\n }\n\n return false\n}\n\n/**\n * Equality used solely to decide whether a write is a no-op (§3 of the\n * update-logic rewrite). Purpose-built to close the \"Maximum update depth\"\n * render loop, where a write whose result equals the current view must produce\n * NO new state identity. It differs from {@link isDeepEqual} in exactly the two\n * places that let the loop survive a naive guard:\n *\n * - `NaN` ≡ `NaN` (via `Object.is`). `computeDiff` compares primitives with\n * `!==`, so a field that is `NaN` on both sides emits a spurious diff.\n * - An explicit-`undefined` key ≡ a missing key (`{x: undefined}` ≡ `{}`).\n * `isDeepEqual` rejects these on a keys-length mismatch.\n *\n * Standalone ON PURPOSE — do NOT fold this into `isDeepEqual`. `computeDiff`\n * and `computeUndoDiff` depend on `isDeepEqual`'s current `NaN !== NaN` and\n * strict keys-length semantics; relaxing them there would change the wire\n * payload. Firestore opaque values (Timestamp, sentinels, refs) delegate to\n * their own `.isEqual`; arrays compare elementwise.\n */\nexport const valuesEqualForNoOp = (a: unknown, b: unknown): boolean => {\n // Primitive / reference identity, with `Object.is` so `NaN` ≡ `NaN`.\n if (Object.is(a, b)) return true\n\n // Opaque Firestore types compare by their own identity semantics.\n if (isFirestoreOpaque(a) || isFirestoreOpaque(b)) {\n return (\n isFirestoreOpaque(a) && isFirestoreOpaque(b) && a.isEqual(b)\n )\n }\n\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b)) return false\n if (a.length !== b.length) return false\n return a.every((item, i) => valuesEqualForNoOp(item, b[i]))\n }\n\n if (isPlainObject(a) && isPlainObject(b)) {\n // Union of keys: an explicit-`undefined` value on one side equals a\n // missing key on the other, because both recurse to\n // `valuesEqualForNoOp(undefined, undefined) === true`.\n const keys = new Set([...Object.keys(a), ...Object.keys(b)])\n for (const key of keys) {\n if (!valuesEqualForNoOp(a[key], b[key])) return false\n }\n return true\n }\n\n return false\n}\n\n/**\n * Snapshot-side no-op collapse comparison over the observable fields shared by\n * DocumentState and CollectionState (§3/§4): returns `true` when something a\n * consumer can observe changed. `data` uses {@link valuesEqualForNoOp} so an\n * explicit-`undefined` ≡ missing key does not count as a change. Collection\n * state additionally checks `isActive` at the call site.\n */\nexport const observableStateChanged = (\n prev: {\n isLoading: boolean\n isSynced: boolean\n error: Error | undefined\n data: unknown\n },\n next: {\n isLoading: boolean\n isSynced: boolean\n error: Error | undefined\n data: unknown\n }\n): boolean =>\n prev.isLoading !== next.isLoading ||\n prev.isSynced !== next.isSynced ||\n prev.error !== next.error ||\n !valuesEqualForNoOp(prev.data, next.data)\n\n/**\n * Compute the minimal diff between two objects for Firestore updates.\n * Returns only the fields that changed, using deleteField() for removed fields.\n *\n * @param from - The original object (sync state)\n * @param to - The target object (local state)\n * @returns A partial object containing only changed fields\n */\nexport const computeDiff = <T extends FirestoreObject>(\n from: T,\n to: T | undefined\n): WithFieldValue<DeepPartial<T>> => {\n if (to === undefined) {\n return deleteField() as WithFieldValue<DeepPartial<T>>\n }\n\n const diff: Record<string, unknown> = {}\n\n // Check for changed or added fields\n for (const key of Object.keys(to)) {\n const fromValue = from[key]\n const toValue = to[key]\n\n // Arrays are compared by value and replaced entirely\n if (Array.isArray(toValue)) {\n if (!isDeepEqual(fromValue, toValue)) {\n diff[key] = toValue\n }\n continue\n }\n\n // Nested objects get recursive diff\n if (isPlainObject(toValue)) {\n if (!isDeepEqual(fromValue, toValue)) {\n const nestedDiff = computeDiff(\n (fromValue as Record<string, unknown>) ?? {},\n toValue\n )\n if (Object.keys(nestedDiff).length > 0) {\n diff[key] = nestedDiff\n }\n }\n continue\n }\n\n // Firestore opaque values — sentinels (serverTimestamp,\n // arrayUnion, …) and value types (Timestamp, DocumentReference,\n // …). Compare via `.isEqual` so identical-by-value Timestamps\n // don't show up as spurious diffs every sync; pass the value\n // through unchanged so sentinels survive into the write payload\n // for the server to expand.\n if (isFirestoreOpaque(toValue)) {\n if (\n !isFirestoreOpaque(fromValue) ||\n !toValue.isEqual(fromValue)\n ) {\n diff[key] = toValue\n }\n continue\n }\n\n // Primitives are compared directly\n if (toValue !== undefined && fromValue !== toValue) {\n diff[key] = toValue\n }\n }\n\n // Check for removed fields. Only `undefined` triggers a delete — `null`\n // is a valid Firestore value and is preserved via the primitive-comparison\n // branch above.\n for (const key of Object.keys(from)) {\n if (to[key] === undefined) {\n diff[key] = deleteField()\n }\n }\n\n return diff as WithFieldValue<DeepPartial<T>>\n}\n\n/**\n * Strip FieldValue sentinels from a freshly-computed `edits` object when they\n * match a write the server has already durably accepted (`committed`).\n *\n * A sentinel (`serverTimestamp`, `increment`, `arrayUnion`, `arrayRemove`) is a\n * fire-once write intent: the client ships the sentinel, the server resolves it\n * to a concrete value, and the confirming snapshot carries that value. The\n * snapshot rebase re-derives pending edits with `computeDiff` and re-applies\n * them over the new server truth — but a sentinel can never compare equal to\n * its own resolved value (`isDeepEqual` only delegates to `.isEqual` when both\n * sides are opaque), so a committed sentinel would be re-derived and re-written\n * on every snapshot forever (and a non-idempotent `increment` would re-apply\n * each loop — data corruption).\n *\n * The fix: once a write's promise resolves, the caller records its payload as\n * `committed`. On the next snapshot the rebase calls this to drop any sentinel\n * that sits at the same path AND is `.isEqual` to the committed one — it has\n * landed, so the server's value is now the truth. Everything else is preserved:\n * a not-yet-sent sentinel (no `committed` entry), a re-edited sentinel (a\n * different `.isEqual` result), and every plain value flow through untouched,\n * so genuine pending edits and collaborator merges are unaffected.\n *\n * `edits` is mutated in place (it is always a fresh `computeDiff` result).\n * Recurses into plain objects, so collection diffs keyed by docId converge too:\n * when a doc's nested diff empties out, the doc entry itself is removed.\n *\n * @internal\n */\nexport const dropCommittedSentinels = (\n edits: Record<string, unknown>,\n committed: Record<string, unknown> | null | undefined\n): Record<string, unknown> => {\n if (!committed) return edits\n for (const key of Object.keys(edits)) {\n const value = edits[key]\n const sent = committed[key]\n if (isFirestoreOpaque(value)) {\n if (isFirestoreOpaque(sent) && value.isEqual(sent)) {\n delete edits[key]\n }\n } else if (isPlainObject(value) && isPlainObject(sent)) {\n dropCommittedSentinels(value, sent)\n if (Object.keys(value).length === 0) {\n delete edits[key]\n }\n }\n }\n return edits\n}\n\n/**\n * Apply a Firestore diff to a target object in place (mutating).\n * Handles deleteField(), serverTimestamp(), and nested objects.\n *\n * Most code should use `applyDiff` (immutable) instead.\n * This mutable version is useful for performance-critical paths\n * where you're already working with a cloned object.\n *\n * @param target - The object to mutate\n * @param diff - The diff to apply\n */\nexport const applyDiffMutable = (\n target: FirestoreObject,\n diff: Record<string, unknown>\n): void => {\n for (const key of Object.keys(diff)) {\n const value = (diff as Record<string, unknown>)[key]\n\n // Firestore opaque values: FieldValue sentinels and value types.\n if (isFirestoreOpaque(value)) {\n // `deleteField()` is structural — actually drop the key from\n // the local view. Matches what Firestore does on commit, and\n // what `computeDiff`'s removed-field branch round-trips back\n // out to a sentinel on the next diff.\n if (isDeleteField(value)) {\n delete (target as Record<string, unknown>)[key]\n continue\n }\n // Every other opaque value (serverTimestamp, increment,\n // arrayUnion/Remove, Timestamp, DocumentReference, GeoPoint,\n // Bytes, VectorValue, …) is preserved by reference. Sentinels\n // must reach Firestore in their original form so the server\n // can expand them; value types must keep their prototype.\n //\n // `serverTimestamp()` used to be substituted with\n // `Timestamp.now()` here, which silently shipped client clock\n // time to Firestore. The optimistic-display companion lives\n // in document.ts / collection.ts as `displayOverrides`.\n ;(target as Record<string, unknown>)[key] = value\n continue\n }\n\n // Handle nested objects\n if (isPlainObject(value)) {\n const existingValue = (target as Record<string, unknown>)[key]\n if (!isPlainObject(existingValue)) {\n ;(target as Record<string, unknown>)[key] = {}\n }\n applyDiffMutable(\n (target as Record<string, unknown>)[key] as FirestoreObject,\n value as Record<string, unknown>\n )\n continue\n }\n\n // Handle primitives and arrays\n ;(target as Record<string, unknown>)[key] = value\n }\n}\n\n/**\n * Create a deep clone of an object that's safe for Firestore operations.\n *\n * Firestore opaque values (FieldValue sentinels, Timestamp,\n * DocumentReference, GeoPoint, Bytes, VectorValue) are returned **by\n * reference**. They are immutable from the user's perspective; cloning\n * them by walking keys would either lose their prototype — turning a\n * `DocumentReference` into a plain object Firestore can't recognize —\n * or destroy a sentinel that needed to reach the server intact.\n */\nexport const deepClone = <T>(value: T): T => {\n if (value === null || typeof value !== 'object') {\n return value\n }\n\n if (isFirestoreOpaque(value)) {\n return value\n }\n\n if (Array.isArray(value)) {\n return value.map(deepClone) as T\n }\n\n const result: Record<string, unknown> = {}\n for (const key of Object.keys(value)) {\n result[key] = deepClone((value as Record<string, unknown>)[key])\n }\n return result as T\n}\n\n/**\n * Check if a diff is empty (no changes)\n */\nexport const isDiffEmpty = (diff: Record<string, unknown>): boolean =>\n Object.keys(diff).length === 0\n\n/**\n * Flatten a nested diff object to dot notation for use with Firestore's updateDoc.\n *\n * This converts:\n * ```\n * { building: { floors: 5, height: 100 }, name: 'Test' }\n * ```\n * To:\n * ```\n * { 'building.floors': 5, 'building.height': 100, 'name': 'Test' }\n * ```\n *\n * Arrays, FieldValue sentinels (deleteField, serverTimestamp, …) and\n * Firestore value types (Timestamp, DocumentReference, GeoPoint, Bytes,\n * VectorValue) are NOT flattened — they're preserved at their path so\n * Firestore receives them in their original form.\n *\n * ⚠️ The dot-joined keys this produces are AMBIGUOUS to Firestore if any map\n * key itself contains a \".\" (e.g. an email key `a@b.com`): `updateDoc` parses\n * the \".\" as a path separator and writes to the wrong nested field. The\n * write path uses {@link flattenDiffToFieldPaths} + `FieldPath` instead, which\n * keeps every segment literal. Reach for this only when dotted-string keys are\n * what you actually want.\n *\n * @param diff - The nested diff object\n * @param prefix - Internal: current path prefix for recursion\n * @returns Flattened object with dotted keys\n */\nexport const flattenDiff = (\n diff: Record<string, unknown>,\n prefix = ''\n): Record<string, unknown> => {\n const result: Record<string, unknown> = {}\n\n for (const key of Object.keys(diff)) {\n const value = diff[key]\n const path = prefix ? `${prefix}.${key}` : key\n\n // Arrays, FieldValue sentinels, and Firestore value types are\n // opaque from flatten's perspective — kept at the path verbatim.\n if (Array.isArray(value) || isFirestoreOpaque(value)) {\n result[path] = value\n continue\n }\n\n // Plain objects are recursively flattened\n if (isPlainObject(value)) {\n const nested = flattenDiff(value, path)\n Object.assign(result, nested)\n continue\n }\n\n // Primitives (strings, numbers, booleans, null)\n result[path] = value\n }\n\n return result\n}\n\n/**\n * Flatten a nested diff into a list of `{ segments, value }` entries, where\n * `segments` is the path expressed as an array of *literal* key segments\n * (never joined into a dotted string).\n *\n * This is the segment-preserving sibling of {@link flattenDiff}. It exists\n * because dot-joined string keys are ambiguous to Firestore: `updateDoc(ref,\n * { \"users.a@b.com.role\": 4 })` parses the `.` inside the email key as a path\n * separator and writes to `users → \"a@b\" → \"com\" → role` instead of the\n * literal key `\"a@b.com\"`. Feeding these segments to `new FieldPath(...segments)`\n * (with the variadic `updateDoc(ref, fieldPath, value, …)` form) keeps every\n * segment literal, so a \".\" inside a map key is never re-interpreted.\n *\n * The opaque/plain-object rules mirror {@link flattenDiff} exactly: arrays,\n * FieldValue sentinels (deleteField, serverTimestamp, …), and Firestore value\n * types (Timestamp, DocumentReference, GeoPoint, Bytes, VectorValue) ride\n * through verbatim at their path; only plain objects are recursed into.\n *\n * @param diff - The nested diff object\n * @param prefix - Internal: accumulated path segments for recursion\n * @returns Flat list of `{ segments, value }` entries\n */\nexport const flattenDiffToFieldPaths = (\n diff: Record<string, unknown>,\n prefix: string[] = []\n): Array<{ segments: string[]; value: unknown }> => {\n const result: Array<{ segments: string[]; value: unknown }> = []\n\n for (const key of Object.keys(diff)) {\n const value = diff[key]\n const segments = [...prefix, key]\n\n // Arrays, FieldValue sentinels, and Firestore value types are\n // opaque from flatten's perspective — kept at the path verbatim.\n if (Array.isArray(value) || isFirestoreOpaque(value)) {\n result.push({ segments, value })\n continue\n }\n\n // Plain objects are recursively flattened\n if (isPlainObject(value)) {\n result.push(...flattenDiffToFieldPaths(value, segments))\n continue\n }\n\n // Primitives (strings, numbers, booleans, null)\n result.push({ segments, value })\n }\n\n return result\n}\n\n/**\n * Build the variadic `(fieldPath, value, …)` argument list for the\n * `updateDoc(ref, …)` / `batch.update(ref, …)` form from a nested diff. Each\n * segment array becomes a literal `FieldPath`, so a \".\" inside a map key (e.g.\n * the email key `a@b.com`) stays a single segment instead of being re-parsed\n * as a path separator. Returns `[]` for an empty diff — callers should skip\n * the update entirely in that case.\n */\nexport const diffToFieldPathArgs = (\n diff: Record<string, unknown>\n): unknown[] =>\n flattenDiffToFieldPaths(diff).flatMap((e) => [\n new FieldPath(...e.segments),\n e.value,\n ])\n\n/**\n * Merge two diffs together, with the second taking precedence\n */\nexport const mergeDiffs = <T extends FirestoreObject>(\n first: WithFieldValue<DeepPartial<T>>,\n second: WithFieldValue<DeepPartial<T>>\n): WithFieldValue<DeepPartial<T>> => {\n const result = deepClone(first) as Record<string, unknown>\n\n for (const key of Object.keys(second)) {\n const firstValue = result[key]\n const secondValue = (second as Record<string, unknown>)[key]\n\n if (isPlainObject(firstValue) && isPlainObject(secondValue)) {\n result[key] = mergeDiffs(\n firstValue as WithFieldValue<DeepPartial<FirestoreObject>>,\n secondValue as WithFieldValue<DeepPartial<FirestoreObject>>\n )\n } else {\n result[key] = secondValue\n }\n }\n\n return result as WithFieldValue<DeepPartial<T>>\n}\n\n/**\n * Apply a diff to an object, returning a new object.\n * The original object is not modified.\n *\n * @example\n * ```ts\n * const original = { name: 'Project', count: 5 }\n * const diff = { name: 'Updated', count: deleteField() }\n * const result = applyDiff(original, diff)\n * // result = { name: 'Updated' }\n * // original is unchanged\n * ```\n */\nexport const applyDiff = <T extends FirestoreObject>(\n state: T,\n diff: WithFieldValue<DeepPartial<T>>\n): T => {\n const result = deepClone(state)\n applyDiffMutable(result, diff as Record<string, unknown>)\n return result\n}\n\n/**\n * Compute the undo diff that would reverse the effect of applying a diff to a state.\n *\n * Given a starting state and a diff that was (or will be) applied to it,\n * returns a new diff that when applied to the result would restore the original state.\n *\n * @example\n * ```ts\n * const startState = { name: 'Foo', count: 5 }\n * const diff = { name: 'Bar', count: deleteField() }\n *\n * // Apply the diff\n * const endState = applyDiff(startState, diff)\n * // endState = { name: 'Bar' }\n *\n * // Compute the undo\n * const undoDiff = computeUndoDiff(startState, diff)\n * // undoDiff = { name: 'Foo', count: 5 }\n *\n * // Applying undoDiff to endState restores startState\n * const restored = applyDiff(endState, undoDiff)\n * // restored = { name: 'Foo', count: 5 }\n * ```\n */\nexport const computeUndoDiff = <T extends FirestoreObject>(\n startState: T,\n diff: WithFieldValue<DeepPartial<T>>\n): WithFieldValue<DeepPartial<T>> => {\n const endState = applyDiff(startState, diff)\n return computeDiff(endState, startState)\n}\n\n/**\n * Check if a diff affects a specific path (supports dot notation).\n *\n * @example\n * ```ts\n * const diff = { building: { floors: 5 }, name: 'Test' }\n *\n * diffContainsPath(diff, 'name') // true\n * diffContainsPath(diff, 'building') // true\n * diffContainsPath(diff, 'building.floors') // true\n * diffContainsPath(diff, 'building.height') // false\n * diffContainsPath(diff, 'other') // false\n * ```\n */\nexport const diffContainsPath = (\n diff: Record<string, unknown>,\n path: string\n): boolean => {\n const parts = path.split('.')\n let current: unknown = diff\n\n for (const part of parts) {\n if (current === null || typeof current !== 'object') {\n return false\n }\n if (!(part in (current as Record<string, unknown>))) {\n return false\n }\n current = (current as Record<string, unknown>)[part]\n }\n\n return true\n}\n\n/**\n * Extract the value at a specific path from a diff (supports dot notation).\n * Returns undefined if the path doesn't exist in the diff.\n *\n * @example\n * ```ts\n * const diff = { building: { floors: 5, height: 100 }, name: 'Test' }\n *\n * extractDiffValue(diff, 'name') // 'Test'\n * extractDiffValue(diff, 'building') // { floors: 5, height: 100 }\n * extractDiffValue(diff, 'building.floors') // 5\n * extractDiffValue(diff, 'building.missing') // undefined\n * ```\n */\nexport const extractDiffValue = (\n diff: Record<string, unknown>,\n path: string\n): unknown => {\n const parts = path.split('.')\n let current: unknown = diff\n\n for (const part of parts) {\n if (current === null || typeof current !== 'object') {\n return undefined\n }\n if (!(part in (current as Record<string, unknown>))) {\n return undefined\n }\n current = (current as Record<string, unknown>)[part]\n }\n\n return current\n}\n\n/**\n * Create a diff that sets a value at a specific path (supports dot notation).\n *\n * @example\n * ```ts\n * createDiffAtPath('name', 'New Name')\n * // { name: 'New Name' }\n *\n * createDiffAtPath('building.floors', 5)\n * // { building: { floors: 5 } }\n *\n * createDiffAtPath('building.config.enabled', true)\n * // { building: { config: { enabled: true } } }\n * ```\n */\nexport const createDiffAtPath = (\n path: string,\n value: unknown\n): Record<string, unknown> => {\n const parts = path.split('.')\n const result: Record<string, unknown> = {}\n\n let current = result\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n if (part === undefined) continue\n current[part] = {}\n current = current[part] as Record<string, unknown>\n }\n\n const lastPart = parts[parts.length - 1]\n if (lastPart !== undefined) {\n current[lastPart] = value\n }\n\n return result\n}\n\n/**\n * Invert a flattened diff back to nested object structure.\n * Opposite of flattenDiff.\n *\n * @example\n * ```ts\n * const flat = { 'building.floors': 5, 'building.height': 100, 'name': 'Test' }\n * const nested = unflattenDiff(flat)\n * // { building: { floors: 5, height: 100 }, name: 'Test' }\n * ```\n */\nexport const unflattenDiff = (\n flatDiff: Record<string, unknown>\n): Record<string, unknown> => {\n const result: Record<string, unknown> = {}\n\n for (const [path, value] of Object.entries(flatDiff)) {\n const parts = path.split('.')\n\n let current = result\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n if (part === undefined) continue\n if (!(part in current) || typeof current[part] !== 'object') {\n current[part] = {}\n }\n current = current[part] as Record<string, unknown>\n }\n\n const lastPart = parts[parts.length - 1]\n if (lastPart !== undefined) {\n current[lastPart] = value\n }\n }\n\n return result\n}\n\n// ---------------------------------------------------------------------------\n// Display overrides\n//\n// `serverTimestamp()` sentinels need to survive `localState` so the write\n// path can ship them to Firestore for server-side expansion (C1). That\n// leaves the optimistic UI with a `FieldValue` object sitting at the\n// field — components can't render it. The display-override layer\n// captures `Timestamp.now()` at the moment a sentinel first enters\n// `localState`, stores it keyed by dotted path, and substitutes it into\n// the merged view at read time. The captured Timestamp is frozen for the\n// lifetime of the sentinel (it doesn't drift forward on re-renders), and\n// the entry is dropped automatically once the sentinel leaves\n// `localState` — either because the server ack cleared `localState`, or\n// because the user overwrote that path with an explicit value.\n//\n// Scope: only `serverTimestamp()` gets a display override. `increment`,\n// `arrayUnion`, and `arrayRemove` would need access to the SDK's\n// non-public internal fields (`_operand`, `_elements`) to compute a\n// display value; consumers using those should gate their render code on\n// the field not being a sentinel.\n// ---------------------------------------------------------------------------\n\n/**\n * Walk a state object collecting every dotted path that currently holds\n * a `serverTimestamp()` sentinel. Arrays are not traversed — Firestore\n * doesn't allow sentinels inside arrays. Non-plain objects (Timestamps,\n * DocumentReferences, …) are leaves.\n *\n * @internal\n */\nexport const collectServerTimestampPaths = (\n state: Record<string, unknown> | null | undefined,\n prefix = '',\n out: Set<string> = new Set()\n): Set<string> => {\n if (!state) return out\n for (const key of Object.keys(state)) {\n const value = state[key]\n const path = prefix ? `${prefix}.${key}` : key\n if (isServerTimestamp(value)) {\n out.add(path)\n continue\n }\n if (isPlainObject(value)) {\n collectServerTimestampPaths(value, path, out)\n }\n }\n return out\n}\n\n/**\n * Reconcile a `displayOverrides` map against the current `localState`:\n *\n * - For each path that holds a `serverTimestamp()` sentinel but has no\n * override yet, capture `Timestamp.now()` and store it (frozen-at-\n * first-sighting).\n * - For each existing override whose path no longer holds a sentinel\n * (sentinel was overwritten, or `localState` cleared on snapshot ack),\n * drop it.\n *\n * The map is mutated in place. Pass a custom `now` for deterministic\n * tests; defaults to `Timestamp.now()`.\n *\n * @internal\n */\nexport const reconcileDisplayOverrides = (\n localState: Record<string, unknown> | null | undefined,\n overrides: Map<string, unknown>,\n now: () => unknown = () => Timestamp.now()\n): void => {\n const currentPaths = collectServerTimestampPaths(localState)\n for (const path of currentPaths) {\n if (!overrides.has(path)) {\n overrides.set(path, now())\n }\n }\n for (const path of [...overrides.keys()]) {\n if (!currentPaths.has(path)) {\n overrides.delete(path)\n }\n }\n}\n\nconst setAtPath = (\n obj: Record<string, unknown>,\n path: string,\n value: unknown\n): void => {\n const parts = path.split('.')\n let cur = obj\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]!\n if (!isPlainObject(cur[part])) cur[part] = {}\n cur = cur[part] as Record<string, unknown>\n }\n cur[parts[parts.length - 1]!] = value\n}\n\n/**\n * Apply a path → value override map to a merged view, returning a new\n * object. Used by document.ts / collection.ts to substitute display\n * values for sentinels still present in `localState`.\n *\n * @internal\n */\nexport const applyOverridesAtPaths = <T extends FirestoreObject>(\n merged: T,\n overrides: ReadonlyMap<string, unknown>\n): T => {\n if (overrides.size === 0) return merged\n const result = deepClone(merged) as Record<string, unknown>\n for (const [path, value] of overrides) {\n setAtPath(result, path, value)\n }\n return result as T\n}\n","import {\n collection,\n doc,\n onSnapshot,\n query,\n writeBatch,\n deleteField,\n type FieldPath,\n WithFieldValue,\n QueryConstraint,\n type CollectionReference,\n type Query,\n} from 'firebase/firestore'\nimport type {\n CollectionDefinition,\n CollectionHandle,\n CollectionState,\n DeepPartial,\n FirestoreObject,\n Subscriber,\n Unsubscribe,\n UpdateOptions,\n} from '../types'\nimport type { FirestateStore } from './store'\nimport {\n applyDiff,\n applyDiffMutable,\n applyOverridesAtPaths,\n computeDiff,\n deepClone,\n diffToFieldPathArgs,\n dropCommittedSentinels,\n isDeepEqual,\n observableStateChanged,\n reconcileDisplayOverrides,\n valuesEqualForNoOp,\n} from '../utils/diff'\n\n// Module-level counter so each subscription instance gets a unique sync key,\n// even when multiple instances target the same collection path.\nlet syncKeyCounter = 0\n\n/**\n * Build the Firestore query a collection subscription runs: `definition`-level\n * constraints first, then hook-level `extraConstraints`. With no constraints at\n * all the bare collection reference is itself a valid `Query`.\n *\n * Single source of truth for query assembly. `useCollection` decides whether a\n * fresh `queryConstraints` array is semantically the same query — and so\n * whether to keep the existing listener instead of tearing it down — by\n * building the prospective query with this exact function and comparing via\n * Firestore's `queryEqual` (see hooks.ts). That comparison is only correct if\n * it assembles the query the same way the subscription does, so both paths MUST\n * go through here. Don't re-inline the merge order at either call site.\n */\nexport const buildCollectionQuery = <TData>(\n ref: CollectionReference<TData>,\n definitionConstraints: QueryConstraint[] | undefined,\n extraConstraints: QueryConstraint[] | undefined\n): Query<TData> => {\n const all = [...(definitionConstraints ?? []), ...(extraConstraints ?? [])]\n return all.length > 0 ? query(ref, ...all) : ref\n}\n\n/**\n * Options for creating a collection subscription\n */\nexport interface CollectionOptions<TData extends FirestoreObject> {\n /** The store instance */\n store: FirestateStore\n /** Collection definition from defineCollection() */\n definition: CollectionDefinition<TData>\n /**\n * Resolved collection path. If omitted and `definition.path` is a string,\n * that value is used. If `definition.path` is a function, this option is\n * required.\n */\n collectionPath?: string\n /** Override read-only setting */\n readOnly?: boolean\n /** Additional query constraints */\n queryConstraints?: QueryConstraint[]\n /** Callback for pushing undo actions */\n onPushUndo?: (\n undoAction: () => void,\n redoAction: () => void,\n options?: UpdateOptions\n ) => void\n}\n\n/**\n * Internal state for a collection subscription\n */\ninterface CollectionInternalState<T extends FirestoreObject> {\n syncState: Record<string, T> | undefined\n localState: Record<string, T> | undefined\n isLoading: boolean\n isActive: boolean\n error: Error | undefined\n /**\n * The payload of the last batch the server durably accepted (set the\n * moment `batch.commit()` resolves, consumed by the next snapshot's\n * rebase). Lets the rebase recognize a committed FieldValue sentinel and\n * drop it instead of re-deriving and re-writing it forever — see\n * `dropCommittedSentinels`.\n */\n committedWrite: Record<string, T> | undefined\n /**\n * Frozen display values for `serverTimestamp()` sentinels currently\n * sitting in `localState`. Keyed by dotted path (e.g.\n * `\"<docId>.updatedAt\"`). See document.ts for the full contract.\n */\n displayOverrides: Map<string, unknown>\n}\n\n/**\n * Create a collection subscription.\n * This is a low-level API - prefer using useCollection hook in React.\n *\n * @example\n * ```ts\n * const subscription = createCollectionSubscription({\n * store,\n * definition: spacesCollection,\n * collectionPath: 'projects/123/spaces',\n * })\n *\n * const unsubscribe = subscription.subscribe((state) => {\n * console.log('Collection state:', state)\n * })\n *\n * subscription.load() // For lazy collections\n * ```\n */\nexport const createCollectionSubscription = <TData extends FirestoreObject>(\n options: CollectionOptions<TData>\n): {\n /** Activate the subscription (for lazy loading) */\n load: () => void\n /** Stop the Firestore listener */\n stop: () => void\n /** Subscribe to state changes */\n subscribe: (fn: Subscriber<CollectionState<TData>>) => Unsubscribe\n /** Get current state */\n getState: () => CollectionState<TData>\n /** Get collection handle for updates */\n getHandle: () => CollectionHandle<TData>\n /** Force sync now */\n sync: () => Promise<void>\n} => {\n const { store, definition, collectionPath: resolvedPath, readOnly, queryConstraints: extraConstraints, onPushUndo } = options\n const { firestore, autosave: defaultAutosave, minLoadTime: defaultMinLoadTime } = store\n\n const {\n path,\n autosave = defaultAutosave,\n minLoadTime = defaultMinLoadTime,\n readOnly: definitionReadOnly,\n lazy = false,\n queryConstraints: definitionConstraints,\n retryOnError = false,\n retryInterval = 5000,\n schema,\n } = definition\n\n const isReadOnly = readOnly ?? definitionReadOnly ?? false\n // Prefer the caller-resolved path. Fall back to a string `definition.path`\n // for ergonomic direct use; if both are missing, the caller forgot to\n // resolve a function path.\n const collectionPath = resolvedPath ?? (typeof path === 'string' ? path : undefined)\n if (collectionPath === undefined) {\n throw new Error(\n `createCollectionSubscription: definition.path is a function; pass a resolved collectionPath in options.`\n )\n }\n // Create collection reference\n const collectionRef = collection(firestore, collectionPath) as CollectionReference<TData>\n\n // Internal state\n const state: CollectionInternalState<TData> = {\n syncState: undefined,\n localState: undefined,\n isLoading: !lazy,\n isActive: !lazy,\n error: undefined,\n committedWrite: undefined,\n displayOverrides: new Map(),\n }\n\n const subscribers = new Set<Subscriber<CollectionState<TData>>>()\n let unsubscribeListener: Unsubscribe | null = null\n let autosaveTimeout: ReturnType<typeof setTimeout> | null = null\n let minLoadTimeout: ReturnType<typeof setTimeout> | null = null\n let retryTimeout: ReturnType<typeof setTimeout> | null = null\n let minLoadTimeElapsed = false\n let loaded = false\n // Cached handle — returns the same reference until notify() invalidates\n // it. Lets useSyncExternalStore consumers rely on handle identity.\n let cachedHandle: CollectionHandle<TData> | null = null\n\n // Unique key for sync tracking, scoped per-instance so multiple\n // subscriptions to the same path don't share (or clobber) one entry.\n const syncKey = `col:${collectionPath}#${++syncKeyCounter}`\n\n const getMergedData = (): Record<string, TData> => {\n const base = state.localState ?? state.syncState ?? {}\n return applyOverridesAtPaths(base, state.displayOverrides)\n }\n\n const getPublicState = (): CollectionState<TData> => ({\n data: getMergedData(),\n isLoading: state.isLoading,\n isSynced: state.localState === undefined,\n isActive: state.isActive,\n error: state.error,\n })\n\n // Last public state actually published — see document.ts for the full\n // contract behind this snapshot-side no-op collapse (§3/§4).\n let lastPublished: CollectionState<TData> | null = null\n\n const publicStateChanged = (\n prev: CollectionState<TData>,\n next: CollectionState<TData>\n ): boolean =>\n // isActive is collection-specific (lazy loading); the rest of the\n // observable no-op collapse is shared with documents.\n prev.isActive !== next.isActive ||\n observableStateChanged(prev, next)\n\n const notify = () => {\n // Reconcile display overrides against the current localState\n // before publishing — see document.ts for the full contract.\n reconcileDisplayOverrides(\n state.localState as Record<string, unknown> | undefined,\n state.displayOverrides\n )\n const publicState = getPublicState()\n // Snapshot-side no-op collapse: nothing observable changed → publish\n // nothing, keeping the cached handle identity stable.\n if (lastPublished !== null && !publicStateChanged(lastPublished, publicState)) {\n return\n }\n lastPublished = publicState\n cachedHandle = null\n subscribers.forEach((fn) => fn(publicState))\n store.reportSyncState(syncKey, publicState.isSynced)\n }\n\n // Pre-snapshot mutations are unsafe because computing a partial-update\n // local state without knowing the existing server fields would cause the\n // subsequent diff to mark unrelated fields as deleted. Document mutations\n // bail the same way when there's no current data.\n const warnNoSnapshot = (method: string) => {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[firestate] ${method}() on ${collectionPath} was ignored: the first snapshot has not arrived yet. ` +\n `Gate calls on the collection's isLoading/isActive state, or await the first data before mutating.`\n )\n }\n }\n\n const updateState = (\n diff: WithFieldValue<DeepPartial<Record<string, TData>>>,\n undoOptions: UpdateOptions = {}\n ) => {\n if (isReadOnly) return\n if (state.syncState === undefined) {\n warnNoSnapshot('update')\n return\n }\n\n // Use raw localState as the mutation base so serverTimestamp() sentinels\n // in localState survive into newLocalState. getMergedData() substitutes\n // display-override Timestamps at sentinel paths, which would erase the\n // sentinel from state.localState on the next update() call.\n const rawBase = state.localState ?? state.syncState ?? {}\n const newLocalState = deepClone(rawBase)\n applyDiffMutable(newLocalState, diff as Record<string, unknown>)\n\n // Ensure each document has its id\n for (const [docId, docData] of Object.entries(newLocalState)) {\n if (docData && typeof docData === 'object') {\n ;(docData as Record<string, unknown>).id = docId\n }\n }\n\n // No-op collapse (§3): a write whose merged result equals the current\n // view produces NO new state identity, undo entry, notify, or\n // autosave. rawBase and newLocalState are compared after id injection\n // so both carry ids; valuesEqualForNoOp closes the NaN /\n // explicit-undefined gaps that defeat a naive guard.\n if (valuesEqualForNoOp(rawBase, newLocalState)) return\n\n // Push undo eagerly against the pre-mutation snapshot. Cmd+Z within\n // the autosave window pops this entry, applies the inverse via\n // updateState, and the sync() no-op shortcut absorbs the resulting\n // same-as-syncState case without a Firestore write.\n if (undoOptions?.undoable !== false && onPushUndo) {\n const undoDiff = computeDiff(\n newLocalState as FirestoreObject,\n rawBase as FirestoreObject\n )\n const redoDiff = computeDiff(\n rawBase as FirestoreObject,\n newLocalState as FirestoreObject\n )\n onPushUndo(\n () => updateState(undoDiff as WithFieldValue<DeepPartial<Record<string, TData>>>, { undoable: false }),\n () => updateState(redoDiff as WithFieldValue<DeepPartial<Record<string, TData>>>, { undoable: false }),\n undoOptions\n )\n }\n\n state.localState = newLocalState\n\n notify()\n scheduleAutosave()\n }\n\n // Overloaded: callers can pass (id, data, opts) or (data, opts). The\n // no-id form generates a Firestore auto-id via doc(collectionRef).id and\n // returns it so the caller can reference the new doc immediately.\n // Returns undefined when the mutation is dropped so callers can't\n // accidentally route on or persist an id that was never queued.\n function addDocument(\n id: string,\n data: Omit<TData, 'id'>,\n undoOptions?: UpdateOptions\n ): string | undefined\n function addDocument(\n data: Omit<TData, 'id'>,\n undoOptions?: UpdateOptions\n ): string | undefined\n function addDocument(\n idOrData: string | Omit<TData, 'id'>,\n dataOrOptions?: Omit<TData, 'id'> | UpdateOptions,\n maybeUndoOptions?: UpdateOptions\n ): string | undefined {\n const hasExplicitId = typeof idOrData === 'string'\n const data = (hasExplicitId ? dataOrOptions : idOrData) as Omit<TData, 'id'>\n const undoOptions = (hasExplicitId\n ? maybeUndoOptions\n : (dataOrOptions as UpdateOptions | undefined)) ?? {}\n\n if (isReadOnly) return undefined\n if (state.syncState === undefined) {\n // Bail rather than queueing: an explicit id that happens to exist\n // on the server would round-trip through computeDiff and clobber\n // any remote-only fields, and we have no way to know without a\n // first snapshot.\n warnNoSnapshot('add')\n return undefined\n }\n\n // Only allocate an auto-id once we know we're going to queue the\n // write — otherwise the caller might persist an id that was dropped.\n const id = hasExplicitId ? (idOrData as string) : doc(collectionRef).id\n\n // Use schema.parse as a validation guard — throw on bad input — but\n // discard the parsed result and store the caller's original object\n // with id attached. Storing parsed output would silently drop\n // unknown keys via Zod's default `.strip()` and re-transform on\n // undo/redo replay. We feed `{ ...data, id }` to parse so the same\n // validation works whether the user's schema declares `id` or not.\n const newDoc = { ...data, id } as unknown as TData\n if (schema) schema.parse(newDoc)\n\n const currentData = getMergedData()\n const newLocalState = deepClone(currentData)\n newLocalState[id] = newDoc\n\n // No-op collapse (§3): adding an id that already holds identical data\n // is a complete no-op. The returned id stays meaningful (the doc\n // exists with that data) but no write/undo/notify is produced.\n if (valuesEqualForNoOp(currentData, newLocalState)) return id\n\n // Push undo eagerly. Inverse diff deletes the just-added doc.\n if (undoOptions?.undoable !== false && onPushUndo) {\n const undoDiff = computeDiff(\n newLocalState as FirestoreObject,\n currentData as FirestoreObject\n )\n const redoDiff = computeDiff(\n currentData as FirestoreObject,\n newLocalState as FirestoreObject\n )\n onPushUndo(\n () => updateState(undoDiff as WithFieldValue<DeepPartial<Record<string, TData>>>, { undoable: false }),\n () => updateState(redoDiff as WithFieldValue<DeepPartial<Record<string, TData>>>, { undoable: false }),\n undoOptions\n )\n }\n\n state.localState = newLocalState\n\n notify()\n scheduleAutosave()\n\n return id\n }\n\n const removeDocument = (id: string, undoOptions: UpdateOptions = {}) => {\n if (isReadOnly) return\n if (state.syncState === undefined) {\n warnNoSnapshot('remove')\n return\n }\n\n const currentData = getMergedData()\n if (!(id in currentData)) return\n\n const newLocalState = deepClone(currentData)\n delete newLocalState[id]\n\n // Push undo eagerly. Inverse diff re-adds the removed doc.\n if (undoOptions?.undoable !== false && onPushUndo) {\n const undoDiff = computeDiff(\n newLocalState as FirestoreObject,\n currentData as FirestoreObject\n )\n const redoDiff = computeDiff(\n currentData as FirestoreObject,\n newLocalState as FirestoreObject\n )\n onPushUndo(\n () => updateState(undoDiff as WithFieldValue<DeepPartial<Record<string, TData>>>, { undoable: false }),\n () => updateState(redoDiff as WithFieldValue<DeepPartial<Record<string, TData>>>, { undoable: false }),\n undoOptions\n )\n }\n\n state.localState = newLocalState\n\n notify()\n scheduleAutosave()\n }\n\n const scheduleAutosave = () => {\n if (autosaveTimeout) {\n clearTimeout(autosaveTimeout)\n }\n if (autosave > 0) {\n autosaveTimeout = setTimeout(() => {\n sync()\n }, autosave)\n }\n }\n\n const sync = async () => {\n if (!state.localState) return\n // syncState is guaranteed defined here: every mutation that can set\n // localState bails when syncState is undefined. This guard is purely\n // defensive against a direct sync() call after stop().\n if (state.syncState === undefined) return\n\n const syncState = state.syncState\n\n if (isDeepEqual(state.localState, syncState)) {\n state.localState = undefined\n notify()\n return\n }\n\n const diff = computeDiff(\n syncState as FirestoreObject,\n state.localState as FirestoreObject\n )\n\n // Snapshot exactly what we're committing so the next snapshot's rebase\n // can recognize committed FieldValue sentinels and drop them. Captured\n // before the await because localState may be re-edited mid-flight.\n const committing = deepClone(state.localState)\n\n try {\n const batch = writeBatch(firestore)\n const deleteFieldSentinel = deleteField()\n\n for (const [docId, docDiff] of Object.entries(diff)) {\n const docRef = doc(collectionRef, docId)\n\n // Check if this is a delete operation\n if (\n docDiff !== null &&\n typeof docDiff === 'object' &&\n 'isEqual' in docDiff &&\n typeof docDiff.isEqual === 'function' &&\n (docDiff as { isEqual: (v: unknown) => boolean }).isEqual(deleteFieldSentinel)\n ) {\n batch.delete(docRef)\n } else if (!(docId in syncState)) {\n // New document - use set to create it\n batch.set(docRef, docDiff as Record<string, unknown>)\n } else {\n // Existing document — use update with the variadic\n // FieldPath form (not a flattened dotted-key object) so a\n // \".\" inside a map key (e.g. an email key `a@b.com`) stays a\n // literal segment instead of being re-parsed as a path\n // separator. update still fails if the doc doesn't exist, so\n // deleted docs are not accidentally recreated.\n const args = diffToFieldPathArgs(\n docDiff as Record<string, unknown>\n )\n if (args.length) {\n batch.update(\n docRef,\n ...(args as [\n string | FieldPath,\n unknown,\n ...unknown[]\n ])\n )\n }\n }\n }\n\n await batch.commit()\n // The server durably accepted this batch — record it for the\n // next snapshot's rebase to drop committed sentinels.\n state.committedWrite = committing\n } catch (error) {\n console.error('Collection sync failed:', error)\n // Surface to React: handle.error reflects the failure and the\n // listener will keep state.localState so consumers can retry by\n // calling sync(). Autosave is not automatically rescheduled to\n // avoid retry loops on permission errors.\n state.error = error as Error\n store.reportError(error as Error, {\n type: 'collection',\n path: collectionPath,\n operation: 'write',\n })\n notify()\n }\n }\n\n const handleSnapshot = (docs: Array<{ id: string; data: TData }>) => {\n const newSyncState: Record<string, TData> = {}\n for (const { id, data } of docs) {\n newSyncState[id] = { ...data, id } as TData\n }\n\n // `prevSync` is the BASELINE: the server snapshot our pending local\n // edits are measured against. Advanced to `newSyncState` here, after\n // capturing it for the rebase below.\n const prevSync = state.syncState\n state.syncState = newSyncState\n // A successful snapshot supersedes any previous read or write error.\n state.error = undefined\n\n // Capture and consume the last committed batch before the rebase: a\n // FieldValue sentinel present in this payload has landed on the server,\n // so the rebase must drop it rather than re-derive it (see below).\n const committed = state.committedWrite\n state.committedWrite = undefined\n\n // The rebase below runs on EVERY snapshot using `prevSync` as the\n // baseline, not only the one confirming an inflight write. Previously a\n // snapshot from another client left `localState` on a stale base, so\n // the next sync re-wrote untouched fields (collaborator clobber) and\n // recreated remotely-deleted docs (delete resurrection).\n const currentLocal = state.localState\n\n if (currentLocal !== undefined && prevSync !== undefined) {\n // Field-level merge: re-derive the user's OWN edits relative to the\n // baseline and re-apply only those over the new server truth.\n const userEdits = computeDiff(\n prevSync as FirestoreObject,\n currentLocal as FirestoreObject\n )\n // Drop FieldValue sentinels we already committed: the server has\n // resolved them, so newSyncState is the truth. Without this a\n // sentinel never compares equal to its resolved value and would be\n // re-derived and re-written on every snapshot forever.\n dropCommittedSentinels(\n userEdits as Record<string, unknown>,\n committed as Record<string, unknown> | undefined\n )\n const rebasedLocalState = applyDiff(\n newSyncState as FirestoreObject,\n userEdits\n ) as Record<string, TData>\n\n // Deletes always win (Bug 2). A doc that existed in the baseline\n // but is gone from the server was deleted remotely → drop it and\n // any pending local edits to it, and never recreate it. This is\n // unconditional: classified against the baseline, not the rebased\n // result, so a stale local copy can't resurrect a deleted doc.\n for (const docId of Object.keys(prevSync)) {\n if (!(docId in newSyncState)) {\n delete rebasedLocalState[docId]\n }\n }\n\n // Re-add ids (applyDiff may have introduced docs from the snapshot\n // and merged edits onto them).\n for (const [docId, docData] of Object.entries(rebasedLocalState)) {\n if (docData && typeof docData === 'object') {\n ;(docData as Record<string, unknown>).id = docId\n }\n }\n\n // If the rebase leaves nothing that differs from the server, the\n // edits are fully absorbed → drop localState so isSynced flips back\n // to true and no redundant write is queued.\n state.localState = isDeepEqual(rebasedLocalState, newSyncState)\n ? undefined\n : rebasedLocalState\n }\n\n if (minLoadTimeElapsed) {\n state.isLoading = false\n }\n loaded = true\n\n // If a rebase produced fresh local edits, ensure they flush. The\n // user's update() during the inflight write already scheduled an\n // autosave, so this is mostly defensive.\n if (state.localState !== undefined) {\n scheduleAutosave()\n }\n\n notify()\n }\n\n const handleError = (error: Error) => {\n if (retryOnError) {\n console.warn('Collection listener error, retrying:', error)\n retryTimeout = setTimeout(() => {\n stop()\n startListener()\n }, retryInterval)\n } else {\n state.error = error\n // Don't leave consumers stuck on a loading spinner — the listener\n // has reported a terminal error, so loading is done.\n state.isLoading = false\n loaded = true\n store.reportError(error, {\n type: 'collection',\n path: collectionPath,\n operation: 'read',\n })\n notify()\n }\n }\n\n const startListener = () => {\n if (unsubscribeListener) return\n\n loaded = false\n minLoadTimeElapsed = false\n\n const q = buildCollectionQuery(\n collectionRef,\n definitionConstraints,\n extraConstraints\n )\n\n unsubscribeListener = onSnapshot(\n q,\n (snapshot) => {\n const docs = snapshot.docs.map((docSnap) => ({\n id: docSnap.id,\n data: docSnap.data() as TData,\n }))\n handleSnapshot(docs)\n },\n handleError\n )\n\n // Min load time handler — tracked so stop() can cancel it.\n minLoadTimeout = setTimeout(() => {\n minLoadTimeout = null\n if (loaded) {\n state.isLoading = false\n notify()\n }\n minLoadTimeElapsed = true\n }, minLoadTime)\n }\n\n const load = () => {\n // Listener-level idempotency so the hook can safely call load() on\n // every mount (including Strict Mode's mount-cleanup-remount cycle).\n if (unsubscribeListener) return\n if (!state.isActive) {\n state.isActive = true\n state.isLoading = true\n notify()\n }\n startListener()\n }\n\n const stop = () => {\n if (retryTimeout) {\n clearTimeout(retryTimeout)\n retryTimeout = null\n }\n if (unsubscribeListener) {\n unsubscribeListener()\n unsubscribeListener = null\n }\n if (autosaveTimeout) {\n clearTimeout(autosaveTimeout)\n autosaveTimeout = null\n }\n if (minLoadTimeout) {\n clearTimeout(minLoadTimeout)\n minLoadTimeout = null\n }\n // Drop this subscription's entry from the global sync-state map so\n // an unmounted hook does not leave useIsSynced stuck at false.\n store.unregisterSyncState(syncKey)\n }\n\n const subscribe = (fn: Subscriber<CollectionState<TData>>): Unsubscribe => {\n subscribers.add(fn)\n return () => subscribers.delete(fn)\n }\n\n const buildHandle = (): CollectionHandle<TData> => ({\n data: getMergedData(),\n update: updateState,\n add: addDocument,\n remove: removeDocument,\n isLoading: state.isLoading,\n isSynced: state.localState === undefined,\n isActive: state.isActive,\n load,\n sync,\n error: state.error,\n ref: collectionRef,\n })\n\n const getHandle = (): CollectionHandle<TData> => {\n if (cachedHandle === null) {\n cachedHandle = buildHandle()\n }\n return cachedHandle\n }\n\n // No constructor-side auto-start: callers (the hook for non-lazy, or\n // users directly for lazy) invoke load() to attach the listener. This\n // keeps subscription creation side-effect-free, matching document.ts.\n\n return {\n load,\n stop,\n subscribe,\n getState: getPublicState,\n getHandle,\n sync,\n }\n}\n","import {\n doc,\n collection,\n onSnapshot,\n setDoc,\n updateDoc,\n deleteDoc,\n DocumentReference,\n type FieldPath,\n WithFieldValue,\n} from 'firebase/firestore'\nimport type {\n DeepPartial,\n DocumentDefinition,\n DocumentHandle,\n DocumentState,\n FirestoreObject,\n Subscriber,\n Unsubscribe,\n UpdateOptions,\n} from '../types'\nimport type { FirestateStore } from './store'\nimport {\n applyDiff,\n applyDiffMutable,\n applyOverridesAtPaths,\n computeDiff,\n deepClone,\n diffToFieldPathArgs,\n dropCommittedSentinels,\n isDeepEqual,\n observableStateChanged,\n reconcileDisplayOverrides,\n valuesEqualForNoOp,\n} from '../utils/diff'\n\n// Module-level counter so each subscription instance gets a unique sync key,\n// even when multiple instances target the same document path.\nlet syncKeyCounter = 0\n\n/**\n * Options for creating a document subscription\n */\nexport interface DocumentOptions<TData extends FirestoreObject> {\n /** The store instance */\n store: FirestateStore\n /** Document definition from defineDocument() */\n definition: DocumentDefinition<TData>\n /**\n * Resolved document id. If omitted and `definition.id` is a string, that\n * value is used. If `definition.id` is a function, this option is required.\n */\n docId?: string\n /**\n * Resolved collection path. If omitted and `definition.collection` is a\n * string, that value is used. If `definition.collection` is a function,\n * this option is required.\n */\n collectionPath?: string\n /** Override read-only setting */\n readOnly?: boolean\n /** Callback for pushing undo actions */\n onPushUndo?: (\n undoAction: () => void,\n redoAction: () => void,\n options?: UpdateOptions\n ) => void\n}\n\n/**\n * Internal state for a document subscription.\n *\n * `localState` uses three distinct values:\n * - `undefined`: no pending local changes\n * - `null`: pending delete (the autosave-driven sync will issue deleteDoc)\n * - object: pending update/set (synced via updateDoc/setDoc)\n *\n * The same convention applies to `inflightLocalState` and `committedWrite`.\n */\ninterface DocumentInternalState<T extends FirestoreObject> {\n syncState: T | undefined\n localState: T | null | undefined\n isLoading: boolean\n error: Error | undefined\n waitingForUpdate: boolean\n inflightLocalState: T | null | undefined\n /**\n * The payload of the last write the server durably accepted (set the\n * moment the write promise resolves, consumed by the next snapshot's\n * rebase). Lets the rebase recognize a committed FieldValue sentinel and\n * drop it instead of re-deriving and re-writing it forever — see\n * `dropCommittedSentinels`.\n */\n committedWrite: T | null | undefined\n /** Whether the pending operation is a full set (create/replace) vs a partial update */\n isSetOperation: boolean\n /**\n * Frozen display values for `serverTimestamp()` sentinels currently\n * sitting in `localState`. Keyed by dotted path. Captured at the\n * moment a sentinel first appears, dropped when the sentinel leaves\n * `localState` (sync ack or overwrite). Substituted into the merged\n * view by `getMergedData` so consumers always see a renderable\n * `Timestamp`, never a raw FieldValue, while the write is in flight.\n */\n displayOverrides: Map<string, unknown>\n}\n\n/**\n * Create a document subscription.\n * This is a low-level API - prefer using useDocument hook in React.\n *\n * @example\n * ```ts\n * const subscription = createDocumentSubscription({\n * store,\n * definition: projectDoc,\n * docId: '123',\n * })\n *\n * const unsubscribe = subscription.subscribe((state) => {\n * console.log('Document state:', state)\n * })\n *\n * subscription.load()\n * ```\n */\nexport const createDocumentSubscription = <TData extends FirestoreObject>(\n options: DocumentOptions<TData>\n): {\n /** Attach the Firestore listener */\n load: () => void\n /** Stop the Firestore listener */\n stop: () => void\n /** Subscribe to state changes */\n subscribe: (fn: Subscriber<DocumentState<TData>>) => Unsubscribe\n /** Get current state */\n getState: () => DocumentState<TData>\n /** Get document handle for updates */\n getHandle: () => DocumentHandle<TData>\n /** Force sync now */\n sync: () => Promise<void>\n} => {\n const { store, definition, docId, collectionPath: resolvedCollectionPath, readOnly, onPushUndo } = options\n const { firestore, autosave: defaultAutosave, minLoadTime: defaultMinLoadTime } = store\n\n const {\n collection: collectionConfig,\n id,\n autosave = defaultAutosave,\n minLoadTime = defaultMinLoadTime,\n readOnly: definitionReadOnly,\n retryOnError = false,\n retryInterval = 5000,\n schema,\n } = definition\n\n const isReadOnly = readOnly ?? definitionReadOnly ?? false\n // Prefer the caller-resolved docId. Fall back to a string `definition.id`\n // for ergonomic direct use; if both are missing, the caller forgot to\n // resolve a function id and we surface that immediately.\n const documentId = docId ?? (typeof id === 'string' ? id : undefined)\n if (documentId === undefined) {\n throw new Error(\n `createDocumentSubscription: definition.id is a function; pass a resolved docId in options.`\n )\n }\n // Same shape as docId: prefer a caller-resolved path, fall back to a\n // string `definition.collection` for direct use. Function definitions\n // must come pre-resolved from the hook layer.\n const collectionPath = resolvedCollectionPath ?? (typeof collectionConfig === 'string' ? collectionConfig : undefined)\n if (collectionPath === undefined) {\n throw new Error(\n `createDocumentSubscription: definition.collection is a function; pass a resolved collectionPath in options.`\n )\n }\n\n // Create document reference\n const docRef = doc(\n collection(firestore, collectionPath),\n documentId\n ) as DocumentReference<TData>\n\n // Internal state\n const state: DocumentInternalState<TData> = {\n syncState: undefined,\n localState: undefined,\n isLoading: true,\n error: undefined,\n waitingForUpdate: false,\n inflightLocalState: undefined,\n committedWrite: undefined,\n isSetOperation: false,\n displayOverrides: new Map(),\n }\n\n const subscribers = new Set<Subscriber<DocumentState<TData>>>()\n let unsubscribeListener: Unsubscribe | null = null\n let autosaveTimeout: ReturnType<typeof setTimeout> | null = null\n let retryTimeout: ReturnType<typeof setTimeout> | null = null\n let minLoadTimeout: ReturnType<typeof setTimeout> | null = null\n let minLoadTimeElapsed = false\n let loaded = false\n // Cached handle — returns the same reference until notify() invalidates\n // it. Lets useSyncExternalStore consumers rely on handle identity.\n let cachedHandle: DocumentHandle<TData> | null = null\n\n // Unique key for sync tracking, scoped per-instance so multiple\n // subscriptions to the same path don't share (or clobber) one entry.\n const syncKey = `doc:${collectionPath}/${documentId}#${++syncKeyCounter}`\n\n const getMergedData = (): TData | undefined => {\n // null localState marks a pending delete — surface as no data.\n if (state.localState === null) return undefined\n const base = state.localState ?? state.syncState\n if (base === undefined) return undefined\n return applyOverridesAtPaths(base, state.displayOverrides)\n }\n\n const getPublicState = (): DocumentState<TData> => ({\n data: getMergedData(),\n isLoading: state.isLoading,\n isSynced: state.localState === undefined,\n error: state.error,\n })\n\n // Last public state actually published. Used to suppress redundant\n // notifies (§3/§4 snapshot-side no-op collapse): a snapshot or write that\n // leaves every observable field unchanged must not invalidate the handle\n // or wake consumers, or the write-back render loop survives.\n let lastPublished: DocumentState<TData> | null = null\n\n const publicStateChanged = observableStateChanged\n\n const notify = () => {\n // Reconcile display overrides against the current localState\n // before publishing — captures Timestamp.now() for any newly\n // arrived serverTimestamp() sentinel and drops entries whose\n // sentinels have been overwritten or acked away.\n reconcileDisplayOverrides(\n state.localState && typeof state.localState === 'object'\n ? (state.localState as Record<string, unknown>)\n : undefined,\n state.displayOverrides\n )\n const publicState = getPublicState()\n // Snapshot-side no-op collapse: nothing a consumer can observe\n // changed → publish nothing. Keeps the cached handle identity stable\n // so useSyncExternalStore does not re-render.\n if (lastPublished !== null && !publicStateChanged(lastPublished, publicState)) {\n return\n }\n lastPublished = publicState\n cachedHandle = null\n subscribers.forEach((fn) => fn(publicState))\n store.reportSyncState(syncKey, publicState.isSynced)\n }\n\n const updateState = (\n diff: WithFieldValue<DeepPartial<TData>>,\n undoOptions: UpdateOptions = {}\n ) => {\n if (isReadOnly) return\n\n const currentData = getMergedData()\n if (!currentData) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[firestate] update() on ${collectionPath}/${documentId} was ignored: there is no current data to diff against. ` +\n `This happens when the document is still loading, has been deleted, or doesn't exist yet. ` +\n `Use set() to create the document, or gate update calls on a non-undefined data value.`\n )\n }\n return\n }\n\n // Use raw localState as the mutation base so serverTimestamp() sentinels\n // in localState survive into newLocalState. getMergedData() substitutes\n // display-override Timestamps at sentinel paths, which would erase the\n // sentinel from state.localState on the next update() call.\n const rawBase = (state.localState ?? state.syncState) as TData\n const newLocalState = deepClone(rawBase)\n applyDiffMutable(newLocalState, diff as Record<string, unknown>)\n\n // No-op collapse (§3): a write whose merged result equals the current\n // view must produce NO new state identity, undo entry, notify, or\n // autosave — otherwise the write-back render loop survives. Compare\n // raw states (decides whether to STORE); valuesEqualForNoOp closes the\n // NaN and explicit-undefined gaps that let the loop slip past a naive\n // equality check.\n if (valuesEqualForNoOp(rawBase, newLocalState)) {\n return\n }\n\n // Push undo eagerly against the pre-mutation state. Cmd+Z within the\n // autosave window pops this entry, applies the inverse via updateState,\n // and the sync() no-op shortcut absorbs the resulting same-as-syncState\n // case without a Firestore write.\n if (undoOptions?.undoable !== false && onPushUndo) {\n const undoDiff = computeDiff(\n newLocalState as FirestoreObject,\n rawBase as FirestoreObject\n )\n const redoDiff = computeDiff(\n rawBase as FirestoreObject,\n newLocalState as FirestoreObject\n )\n onPushUndo(\n () => updateState(undoDiff as WithFieldValue<DeepPartial<TData>>, { undoable: false }),\n () => updateState(redoDiff as WithFieldValue<DeepPartial<TData>>, { undoable: false }),\n undoOptions\n )\n }\n\n state.localState = newLocalState\n state.isSetOperation = false\n\n notify()\n scheduleAutosave()\n }\n\n const setData = (data: TData, undoOptions: UpdateOptions = {}) => {\n if (isReadOnly) return\n\n // Use schema.parse as a validation guard — throw on bad input — but\n // discard the parsed result and store the caller's original object.\n // Storing parsed output would (a) silently drop unknown keys via\n // Zod's default `.strip()` behavior, and (b) cause undo/redo replay\n // through this same path to re-apply any schema transforms a second\n // time. Partial update() diffs are intentionally NOT validated:\n // diffs commonly carry Firestore sentinels (serverTimestamp,\n // arrayUnion, deleteField) that aren't representable in a strict\n // schema.\n if (schema) schema.parse(data)\n\n const currentData = getMergedData()\n\n // No-op collapse (§3): a set() whose payload equals the current stored\n // value is a complete no-op. Skip only when there IS a current value —\n // a set against undefined data is a creation and must proceed. Compare\n // raw state so a held serverTimestamp() sentinel isn't masked by its\n // display Timestamp.\n if (\n currentData !== undefined &&\n valuesEqualForNoOp(state.localState ?? state.syncState, data)\n ) {\n return\n }\n\n // Push undo eagerly. A set against undefined data is a creation;\n // its undo is a delete. Otherwise we restore the prior snapshot via\n // setData, which is symmetric and handles full-replace semantics\n // (including field removals) correctly.\n if (undoOptions?.undoable !== false && onPushUndo) {\n const dataForRedo = deepClone(data)\n if (currentData === undefined) {\n onPushUndo(\n () => deleteDocument({ undoable: false }),\n () => setData(dataForRedo, { undoable: false }),\n undoOptions\n )\n } else {\n // Snapshot raw localState so the restore payload contains\n // serverTimestamp() sentinels, not the frozen Timestamps that\n // getMergedData() substitutes for display purposes.\n const dataToRestore = deepClone((state.localState ?? state.syncState) as TData)\n onPushUndo(\n () => setData(dataToRestore, { undoable: false }),\n () => setData(dataForRedo, { undoable: false }),\n undoOptions\n )\n }\n }\n\n state.localState = deepClone(data)\n state.isSetOperation = true\n\n notify()\n scheduleAutosave()\n }\n\n const deleteDocument = (undoOptions: UpdateOptions = {}) => {\n if (isReadOnly) return\n\n const currentData = getMergedData()\n // Nothing to delete — bail rather than queueing a no-op deleteDoc.\n if (currentData === undefined) {\n return\n }\n\n // Push undo against the pre-delete data (which includes any pending\n // local edits at this moment).\n if (undoOptions?.undoable !== false && onPushUndo) {\n // Snapshot raw localState — same reason as in setData above.\n const dataToRestore = deepClone((state.localState ?? state.syncState) as TData)\n onPushUndo(\n () => setData(dataToRestore, { undoable: false }),\n () => deleteDocument({ undoable: false }),\n undoOptions\n )\n }\n\n // Mark localState as a pending delete and let scheduleAutosave drive\n // the actual deleteDoc call — same flow as set/update.\n state.localState = null\n state.isSetOperation = false\n\n notify()\n scheduleAutosave()\n }\n\n const scheduleAutosave = () => {\n if (autosaveTimeout) {\n clearTimeout(autosaveTimeout)\n }\n if (autosave > 0) {\n autosaveTimeout = setTimeout(() => {\n sync()\n }, autosave)\n }\n }\n\n const sync = async () => {\n if (state.localState === undefined) {\n return\n }\n\n // Pending delete — issue deleteDoc and let the listener confirm via\n // handleMissingDocument. Undo was already pushed at mutation time.\n if (state.localState === null) {\n state.inflightLocalState = null\n state.waitingForUpdate = true\n\n try {\n await deleteDoc(docRef)\n } catch (error) {\n console.error('Sync failed:', error)\n state.waitingForUpdate = false\n state.inflightLocalState = undefined\n state.error = error as Error\n store.reportError(error as Error, {\n type: 'document',\n path: `${collectionPath}/${documentId}`,\n operation: 'write',\n })\n notify()\n }\n return\n }\n\n // No-op if local state already matches what the server holds. This is\n // the path that an undo-of-pending-local takes: the inverse update\n // brings localState back to syncState, and we exit without a write.\n if (state.syncState && isDeepEqual(state.localState, state.syncState)) {\n state.localState = undefined\n state.inflightLocalState = undefined\n notify()\n return\n }\n\n const wasSetOperation = state.isSetOperation\n state.isSetOperation = false\n\n // A creation occurs when there's no server state to diff against —\n // either the user explicitly called set() to create the document, or\n // the listener has reported the doc as missing. In both cases we use\n // setDoc.\n const isCreation = !state.syncState\n const useSetDoc = wasSetOperation || isCreation\n\n const diff = state.syncState\n ? computeDiff(state.syncState, state.localState)\n : undefined\n\n // Snapshot exactly what we're committing so the next snapshot's rebase\n // can recognize committed FieldValue sentinels and drop them. Captured\n // in a local const before the await — NOT read back from shared state —\n // because Firestore fires a local-cache echo snapshot (hasPendingWrites:\n // true) before this write acks, and handleSnapshot clears\n // state.inflightLocalState. Reading it back at line `committedWrite = …`\n // would then capture `undefined`, losing the sentinel record and making\n // serverTimestamp churn / increment double-apply. Mirrors collection.ts.\n const committing = deepClone(state.localState)\n state.inflightLocalState = committing\n\n state.waitingForUpdate = true\n\n try {\n if (useSetDoc) {\n // Full set / creation — use setDoc to create or completely\n // replace the document.\n await setDoc(docRef, state.localState as TData)\n } else {\n // Partial update — use updateDoc with the variadic FieldPath\n // form (not a flattened dotted-key object) so that a \".\" inside\n // a map key (e.g. an email key `a@b.com`) is preserved as a\n // literal segment instead of being re-parsed as a path\n // separator. updateDoc still fails if the document doesn't\n // exist, so deleted docs are not accidentally recreated.\n const args = diffToFieldPathArgs(\n diff as Record<string, unknown>\n )\n if (args.length) {\n await updateDoc(\n docRef,\n ...(args as [\n string | FieldPath,\n unknown,\n ...unknown[]\n ])\n )\n }\n }\n // The server durably accepted this payload. Record it so the next\n // snapshot's rebase can recognize committed FieldValue sentinels\n // (serverTimestamp, increment, …) and drop them instead of\n // re-deriving and re-writing them forever.\n state.committedWrite = committing\n } catch (error) {\n console.error('Sync failed:', error)\n state.waitingForUpdate = false\n state.inflightLocalState = undefined\n // Surface to React: handle.error reflects the failure and the\n // listener will keep state.localState so consumers can retry by\n // calling sync() or by issuing another update. Autosave is not\n // automatically rescheduled to avoid retry loops on permission\n // errors — that policy is left to the consumer.\n state.error = error as Error\n store.reportError(error as Error, {\n type: 'document',\n path: `${collectionPath}/${documentId}`,\n operation: 'write',\n })\n notify()\n }\n }\n\n const handleSnapshot = (newSyncData: TData) => {\n // `prevSync` is the BASELINE: the server snapshot our pending local\n // edits are measured against. We advance it to `newSyncData` below,\n // after using it to re-derive the user's own edits.\n const prevSync = state.syncState\n state.syncState = newSyncData\n // A successful snapshot supersedes any previous read or write error.\n state.error = undefined\n\n // The rebase below runs on EVERY snapshot, using `prevSync` as the\n // baseline, regardless of whether one of our own writes happened to be\n // in flight. This is the fix for the collaborator-clobber class of bugs\n // — previously the rebase only ran inside the `waitingForUpdate`\n // window, so a snapshot from another client left `localState` sitting\n // on a stale base.\n state.waitingForUpdate = false\n state.inflightLocalState = undefined\n // Capture and consume the last committed write before the rebase: a\n // FieldValue sentinel present in this payload has landed on the server,\n // so the rebase must drop it rather than re-derive it (see below).\n const committed = state.committedWrite\n state.committedWrite = undefined\n const currentLocal = state.localState\n\n if (currentLocal === null) {\n // Pending delete — our delete intent is the latest word. The doc\n // is still present on the server here; preserve the tombstone so\n // the next sync issues deleteDoc.\n } else if (currentLocal !== undefined && prevSync !== undefined) {\n // Field-level merge (Bug 1 fix). Re-derive the user's OWN edits\n // relative to the baseline, then re-apply only those over the new\n // server truth. Untouched fields follow the server; the client's\n // actual edits survive. Same-field concurrent edits stay\n // last-write-wins — the local edit is preserved and re-sent on the\n // next sync.\n const userEdits = computeDiff(prevSync, currentLocal)\n // Drop FieldValue sentinels we already committed: they've been\n // resolved by the server, so newSyncData is the truth. Without\n // this a sentinel never compares equal to its resolved value and\n // would be re-derived and re-written on every snapshot forever.\n dropCommittedSentinels(\n userEdits as Record<string, unknown>,\n committed as Record<string, unknown> | null | undefined\n )\n const rebasedLocalState = applyDiff(newSyncData, userEdits)\n // If the rebase leaves nothing that differs from the server, the\n // edit has been fully absorbed → drop localState so isSynced flips\n // back to true and no redundant write is queued.\n const absorbed = isDeepEqual(rebasedLocalState, newSyncData)\n state.localState = absorbed ? undefined : rebasedLocalState\n }\n\n if (minLoadTimeElapsed) {\n state.isLoading = false\n }\n loaded = true\n\n // If local edits exist and aren't currently being synced, schedule an\n // autosave. Covers the case where set() ran before the first snapshot\n // arrived and the initial sync attempt bailed early.\n if (state.localState !== undefined) {\n scheduleAutosave()\n }\n\n notify()\n }\n\n // A document that does not exist is not an error condition — consumers\n // commonly use that state to render a \"create\" UI. Clear loading and\n // leave error undefined; data will be undefined via getMergedData().\n const handleMissingDocument = () => {\n state.syncState = undefined\n state.error = undefined\n // Drop any committed-write record: it described a now-deleted doc and\n // must not be matched against a future recreation snapshot.\n state.committedWrite = undefined\n\n // The only localState that should clear when the doc goes missing is\n // our own pending-delete marker. Any other pending edits (object\n // value) represent the user's intent to recreate the doc — the next\n // sync() will issue a setDoc against the now-missing doc and create\n // it from scratch.\n if (state.localState === null) {\n state.localState = undefined\n state.isSetOperation = false\n if (autosaveTimeout) {\n clearTimeout(autosaveTimeout)\n autosaveTimeout = null\n }\n }\n\n if (state.waitingForUpdate) {\n state.waitingForUpdate = false\n state.inflightLocalState = undefined\n }\n if (minLoadTimeElapsed) {\n state.isLoading = false\n }\n loaded = true\n notify()\n }\n\n const handleError = (error: Error) => {\n if (retryOnError) {\n console.warn('Document listener error, retrying:', error)\n retryTimeout = setTimeout(() => {\n stop()\n load()\n }, retryInterval)\n } else {\n state.error = error\n // Don't leave consumers stuck on a loading spinner — the listener\n // has reported a terminal error, so loading is done.\n state.isLoading = false\n loaded = true\n store.reportError(error, {\n type: 'document',\n path: `${collectionPath}/${documentId}`,\n operation: 'read',\n })\n notify()\n }\n }\n\n const load = () => {\n if (unsubscribeListener) {\n return\n }\n\n loaded = false\n minLoadTimeElapsed = false\n\n unsubscribeListener = onSnapshot(\n docRef,\n (snapshot) => {\n if (snapshot.exists()) {\n handleSnapshot(snapshot.data())\n } else if (!snapshot.metadata.fromCache) {\n handleMissingDocument()\n }\n },\n handleError\n )\n\n // Min load time handler — tracked so stop() can cancel it.\n minLoadTimeout = setTimeout(() => {\n minLoadTimeout = null\n if (loaded) {\n state.isLoading = false\n notify()\n }\n minLoadTimeElapsed = true\n }, minLoadTime)\n }\n\n const stop = () => {\n if (unsubscribeListener) {\n unsubscribeListener()\n unsubscribeListener = null\n }\n if (autosaveTimeout) {\n clearTimeout(autosaveTimeout)\n autosaveTimeout = null\n }\n if (retryTimeout) {\n clearTimeout(retryTimeout)\n retryTimeout = null\n }\n if (minLoadTimeout) {\n clearTimeout(minLoadTimeout)\n minLoadTimeout = null\n }\n // Drop this subscription's entry from the global sync-state map so\n // an unmounted hook does not leave useIsSynced stuck at false.\n store.unregisterSyncState(syncKey)\n }\n\n const subscribe = (fn: Subscriber<DocumentState<TData>>): Unsubscribe => {\n subscribers.add(fn)\n return () => subscribers.delete(fn)\n }\n\n const buildHandle = (): DocumentHandle<TData> => ({\n data: getMergedData(),\n update: updateState,\n set: setData,\n delete: deleteDocument,\n isLoading: state.isLoading,\n isSynced: state.localState === undefined,\n sync,\n error: state.error,\n ref: docRef,\n })\n\n const getHandle = (): DocumentHandle<TData> => {\n if (cachedHandle === null) {\n cachedHandle = buildHandle()\n }\n return cachedHandle\n }\n\n return {\n load,\n stop,\n subscribe,\n getState: getPublicState,\n getHandle,\n sync,\n }\n}\n","import {\n collection,\n queryEqual,\n type CollectionReference,\n type Query,\n type QueryConstraint,\n} from 'firebase/firestore'\nimport type {\n CollectionDefinition,\n CollectionHandle,\n DocumentDefinition,\n DocumentHandle,\n FirestoreObject,\n UpdateOptions,\n} from '../types'\nimport type { FirestateStore } from './store'\nimport { createDocumentSubscription } from './document'\nimport {\n buildCollectionQuery,\n createCollectionSubscription,\n} from './collection'\n\n/**\n * Shared, ref-counted subscription registry.\n *\n * Every `useDocument` / `useCollection` call for the *same* resource — same\n * `(definition, resolved path, doc id / query)` — transparently\n * shares ONE underlying `createDocumentSubscription` / `createCollectionSubscription`\n * instance, and therefore one `onSnapshot` listener and one\n * reconciled/optimistic state. A write through any handle is instantly visible\n * to every reader on that resource, and the per-hook `selector` slices that one\n * shared state instead of building a private subscription per call.\n *\n * `readOnly` is deliberately NOT part of the key. It is a *per-handle\n * capability* over the shared state, not a state fork: a writable hook (the\n * typical provider — the sole writer, which needs full data for its undo bridge\n * and global sync tracking) and any number of `readOnly: true` hooks (leaves\n * that only read-select) resolve the SAME entry, so a write through the writable\n * handle is instantly visible to every read-only reader. The shared\n * subscription is always built writable; a read-only facade neuters its own\n * handle's writers (and `sync`) and leaves the shared undo flag untouched, while\n * passing reads straight through (see {@link readOnlyDocumentHandle}).\n *\n * Lifecycle is ref-counted and lazy:\n * - A facade is built when a hook resolves the resource (its render-phase\n * `useMemo`): it reuses the already-registered shared entry if one exists, or\n * builds a DETACHED entry (subscription created, no listener attached) left\n * out of the registry. `getHandle()`/`load()` work off it either way.\n * - `acquire()` (called from the hook's `subscribe` effect) registers the entry\n * on first lease — adopting whatever a sibling registered first — bumps the\n * ref count, and registers the hook's change callback. Deferring registration\n * to commit means an aborted/suspended/discarded render (whose effect never\n * runs) leaves nothing stranded in the registry. `load()` activates the\n * shared Firestore listener (idempotent — only the first activation attaches).\n * - The last lease to `release()` tears the listener down (the underlying\n * `stop()`) and evicts the entry, so a subsequent mount starts a fresh\n * subscription — a lazy collection resets to `isActive: false` exactly as a\n * single hook does today.\n *\n * The registry is scoped per {@link FirestateStore} (a WeakMap keyed by store)\n * and within that per *definition* (a WeakMap keyed by the definition object).\n * Keying by definition — not just the resolved path string — means two distinct\n * definitions that happen to resolve to the same path keep independent\n * subscriptions (their schema/autosave config may differ), while every hook\n * built from one registry entry shares correctly. This matches the headline use\n * case: a registry resource is one definition object referenced everywhere.\n */\n\n// `ReturnType<typeof fn>` resolves the generic to its `FirestoreObject`\n// constraint, giving a concrete subscription shape we can store heterogeneously\n// in the registry. Handles are cast back to the caller's `TData` at the facade\n// boundary — sound because the data shape only narrows the same stored object.\ntype AnyDocumentSubscription = ReturnType<typeof createDocumentSubscription>\ntype AnyCollectionSubscription = ReturnType<typeof createCollectionSubscription>\n\n/**\n * A registry entry: the shared subscription plus its lease bookkeeping.\n * `undoableEnabled` gates the entry's `onPushUndo` and is shared across all\n * leases (see {@link DocumentShared.setUndoable}).\n */\ninterface DocumentEntry {\n sub: AnyDocumentSubscription\n refCount: number\n undoableEnabled: boolean\n /**\n * False once the entry has been torn down (`sub.stop()` on release-to-zero).\n * A re-acquire of a non-live entry rebuilds `sub`, since `stop()` leaves\n * stale loaded/loading state (see {@link getDocumentShared}).\n */\n live: boolean\n}\n\ninterface CollectionEntry {\n sub: AnyCollectionSubscription\n refCount: number\n undoableEnabled: boolean\n /** See {@link DocumentEntry.live}. */\n live: boolean\n /**\n * The query this entry's listener runs, built with\n * {@link buildCollectionQuery}. Used to match a prospective hook against\n * existing entries via Firestore's `queryEqual` — semantic query identity,\n * not array reference (see {@link getCollectionShared}).\n */\n query: Query<unknown>\n}\n\ninterface StoreRegistry {\n docs: WeakMap<\n DocumentDefinition<FirestoreObject>,\n Map<string, DocumentEntry>\n >\n /**\n * Keyed by definition → `collectionPath` → bucket of entries that differ\n * only by query. A bucket is scanned with `queryEqual` to find the entry\n * for a given query; distinct queries on the same path live as separate\n * entries in the same bucket.\n */\n cols: WeakMap<\n CollectionDefinition<FirestoreObject>,\n Map<string, CollectionEntry[]>\n >\n}\n\nconst registries = new WeakMap<FirestateStore, StoreRegistry>()\n\nconst getRegistry = (store: FirestateStore): StoreRegistry => {\n let reg = registries.get(store)\n if (!reg) {\n reg = { docs: new WeakMap(), cols: new WeakMap() }\n registries.set(store, reg)\n }\n return reg\n}\n\n// `readOnly` is intentionally absent from both keys: read-only and writable\n// hooks on the same resource must resolve the SAME entry (one shared state).\nconst docKey = (collectionPath: string, docId: string): string =>\n `${collectionPath}\\0${docId}`\n\nconst colBucketKey = (collectionPath: string): string => collectionPath\n\n/**\n * Semantic query match, hardened for two cases the raw `queryEqual` does not\n * cover here: the same reference (the common case — a hook reuses its memoized\n * query) short-circuits true, and a `queryEqual` that throws (test harnesses\n * that mock the query builders pass non-`Query` placeholders) is treated as a\n * non-match rather than crashing the lookup.\n */\nconst sameQuery = (a: Query<unknown>, b: Query<unknown>): boolean => {\n if (a === b) return true\n try {\n return queryEqual(a, b)\n } catch {\n return false\n }\n}\n\n/** Push to the store-global undo manager, gated by the entry's shared flag. */\nconst makeOnPushUndo =\n (store: FirestateStore, entry: { undoableEnabled: boolean }) =>\n (undoAction: () => void, redoAction: () => void, opts?: UpdateOptions) => {\n if (!entry.undoableEnabled) return\n store.undoManager.push({\n undo: undoAction,\n redo: redoAction,\n groupId: opts?.undoGroupId,\n })\n }\n\n// A read-only handle is the shared handle with its writers (and `sync`)\n// replaced by no-ops. Reads — `data`, status, `ref`, and a collection's `load`\n// (activating a lazy listener is a read, not a write) — pass straight through,\n// so a read-only facade observes every optimistic edit the writer makes while\n// being unable to author or flush a write itself.\nconst noop = (): void => {}\nconst asyncNoop = async (): Promise<void> => {}\nconst noopAdd = (): undefined => undefined\n\nconst readOnlyDocumentHandle = <T extends FirestoreObject>(\n handle: DocumentHandle<T>\n): DocumentHandle<T> => ({\n ...handle,\n update: noop,\n set: noop,\n delete: noop,\n sync: asyncNoop,\n})\n\nconst readOnlyCollectionHandle = <T extends FirestoreObject>(\n handle: CollectionHandle<T>\n): CollectionHandle<T> => ({\n ...handle,\n update: noop,\n add: noopAdd,\n remove: noop,\n sync: asyncNoop,\n})\n\n/**\n * A per-hook facade over a shared registry entry. Multiple hooks on the same\n * resource hold distinct facades bound to the *same* entry, so `getHandle()`\n * returns the same identity-stable handle to all of them and `acquire()` /\n * `release()` ref-count one shared listener.\n */\nexport interface DocumentShared<T extends FirestoreObject> {\n /** The shared, identity-stable handle for this resource. */\n getHandle: () => DocumentHandle<T>\n /** Activate the shared Firestore listener (idempotent across leases). */\n load: () => void\n /** Set whether this resource records undo entries (shared across leases). */\n setUndoable: (enabled: boolean) => void\n /**\n * Register `onChange` and bump the ref count. Returns a release function\n * that unregisters it and, when it was the last lease, stops the listener\n * and evicts the entry. Release is idempotent.\n */\n acquire: (onChange: () => void) => () => void\n}\n\nexport interface CollectionShared<T extends FirestoreObject> {\n getHandle: () => CollectionHandle<T>\n load: () => void\n setUndoable: (enabled: boolean) => void\n acquire: (onChange: () => void) => () => void\n}\n\nexport interface DocumentSharedParams<T extends FirestoreObject> {\n store: FirestateStore\n definition: DocumentDefinition<T>\n collectionPath: string\n docId: string\n /**\n * Per-handle read-only capability. Neuters only THIS facade's handle\n * writers; it is not part of the share key, so a read-only and a writable\n * hook on the same document share one listener and one optimistic state.\n */\n readOnly?: boolean\n}\n\n/**\n * Find or create the shared subscription for a document resource and return a\n * facade bound to it. Creating the entry attaches no listener. Intended to be\n * called from a hook's render-phase `useMemo`.\n */\nexport const getDocumentShared = <T extends FirestoreObject>({\n store,\n definition,\n collectionPath,\n docId,\n readOnly,\n}: DocumentSharedParams<T>): DocumentShared<T> => {\n const map = getDocMap(store, definition)\n const key = docKey(collectionPath, docId)\n // Per-handle capability, resolved exactly as the subscription used to:\n // explicit hook override wins, then the definition default, then writable.\n // It gates ONLY this facade's handle — never the shared state or its key.\n const facadeReadOnly = readOnly ?? definition.readOnly ?? false\n\n // Build a fresh subscription bound to `entry`. Used for a newly built entry\n // and to revive an evicted one on re-acquire (its `sub` was stop()ed, which\n // leaves stale loaded/loading state).\n //\n // The shared subscription is ALWAYS writable: the provider (sole writer)\n // needs functional writers, and read-only facades neuter their own handle\n // rather than fork a separate read-only subscription. Passing\n // `readOnly: false` also overrides a read-only *definition*, so a hook that\n // opts back in with `readOnly: false` gets a writable handle off the same\n // shared state.\n const buildSub = (entry: DocumentEntry): AnyDocumentSubscription =>\n createDocumentSubscription({\n store,\n definition: definition as DocumentDefinition<FirestoreObject>,\n docId,\n collectionPath,\n readOnly: false,\n onPushUndo: makeOnPushUndo(store, entry),\n })\n\n const buildEntry = (): DocumentEntry => {\n const entry: DocumentEntry = {\n sub: null as unknown as AnyDocumentSubscription,\n refCount: 0,\n undoableEnabled: true,\n live: true,\n }\n entry.sub = buildSub(entry)\n return entry\n }\n\n // Reuse the already-registered shared entry if one exists; otherwise build a\n // DETACHED entry and leave it OUT of the map. Registration is deferred to\n // acquire() (commit time) so an aborted/suspended/discarded render — whose\n // effect, and therefore acquire()/release(), never runs — leaves no\n // refCount-0 entry stranded. getHandle()/load() operate on `ent` regardless.\n let ent: DocumentEntry = map.get(key) ?? buildEntry()\n let desiredUndoable = true\n // Memoize the neutered read-only handle on the underlying handle's identity,\n // so getHandle() stays referentially stable between notifies (a\n // useSyncExternalStore requirement) and rebuilds the wrapper only when the\n // shared subscription publishes a new handle.\n let readOnlySource: DocumentHandle<T> | null = null\n let readOnlyHandle: DocumentHandle<T> | null = null\n\n return {\n getHandle: () => {\n const handle = ent.sub.getHandle() as DocumentHandle<T>\n if (!facadeReadOnly) return handle\n if (handle !== readOnlySource) {\n readOnlySource = handle\n readOnlyHandle = readOnlyDocumentHandle(handle)\n }\n return readOnlyHandle as DocumentHandle<T>\n },\n load: () => ent.sub.load(),\n setUndoable: (enabled) => {\n // A read-only facade can't write, so it must not influence the\n // shared undo flag the writer relies on (last-writer-wins).\n if (facadeReadOnly) return\n desiredUndoable = enabled\n ent.undoableEnabled = enabled\n },\n acquire: (onChange) => {\n // Commit time: adopt the already-registered shared entry if one\n // exists (a sibling registered first, or this resource is still\n // live), else register ours. The map thus holds only committed\n // entries, each with refCount >= 1 — discarded renders leave nothing\n // behind. Revive a torn-down entry with a fresh sub (stop() leaves\n // stale state), honoring \"a subsequent mount starts fresh\".\n const committed = map.get(key)\n if (committed) {\n ent = committed\n } else {\n map.set(key, ent)\n }\n if (!ent.live) {\n ent.sub = buildSub(ent)\n ent.live = true\n }\n // Only writers set the shared undo flag; a read-only lease leaves it\n // as the writer (or default) left it (see setUndoable).\n if (!facadeReadOnly) ent.undoableEnabled = desiredUndoable\n ent.refCount++\n const notifyUnsub = ent.sub.subscribe(onChange)\n let released = false\n return () => {\n if (released) return\n released = true\n notifyUnsub()\n ent.refCount--\n if (ent.refCount <= 0) {\n ent.sub.stop()\n ent.live = false\n if (map.get(key) === ent) map.delete(key)\n }\n }\n },\n }\n}\n\nconst getDocMap = (\n store: FirestateStore,\n definition: DocumentDefinition<FirestoreObject>\n): Map<string, DocumentEntry> => {\n const reg = getRegistry(store)\n let map = reg.docs.get(definition)\n if (!map) {\n map = new Map()\n reg.docs.set(definition, map)\n }\n return map\n}\n\nexport interface CollectionSharedParams<T extends FirestoreObject> {\n store: FirestateStore\n definition: CollectionDefinition<T>\n collectionPath: string\n /** Per-handle read-only capability — see {@link DocumentSharedParams.readOnly}. */\n readOnly?: boolean\n /** Hook-level extra constraints, passed through to the subscription verbatim. */\n queryConstraints: QueryConstraint[] | undefined\n /**\n * The built query for this `(path, constraints)`, used to match existing\n * entries by semantic identity. Built by the caller (it can throw for\n * placeholder queries like an empty `in`); the caller only resolves a shared\n * entry when it is a valid, non-null query.\n */\n query: Query<unknown>\n}\n\n/**\n * Find or create the shared subscription whose query is `queryEqual` to\n * `params.query` and return a facade bound to it. Entries on the same path but\n * with a different query coexist in the same bucket.\n */\nexport const getCollectionShared = <T extends FirestoreObject>({\n store,\n definition,\n collectionPath,\n readOnly,\n queryConstraints,\n query,\n}: CollectionSharedParams<T>): CollectionShared<T> => {\n const bucket = getColBucket(store, definition, collectionPath)\n // See getDocumentShared: per-handle capability, never part of the key.\n const facadeReadOnly = readOnly ?? definition.readOnly ?? false\n\n // Always writable — read-only facades neuter their own handle. See\n // getDocumentShared.buildSub for the full rationale.\n const buildSub = (entry: CollectionEntry): AnyCollectionSubscription =>\n createCollectionSubscription({\n store,\n definition: definition as CollectionDefinition<FirestoreObject>,\n collectionPath,\n readOnly: false,\n queryConstraints,\n onPushUndo: makeOnPushUndo(store, entry),\n })\n\n const buildEntry = (): CollectionEntry => {\n const entry: CollectionEntry = {\n sub: null as unknown as AnyCollectionSubscription,\n refCount: 0,\n undoableEnabled: true,\n live: true,\n query,\n }\n entry.sub = buildSub(entry)\n return entry\n }\n\n // Reuse the already-registered entry whose query matches; otherwise build a\n // DETACHED one and leave it OUT of the bucket. Registration is deferred to\n // acquire() so a discarded render leaves no refCount-0 entry behind —\n // important for collections, whose buckets are linearly scanned by every\n // lookup (see getDocumentShared for the full rationale).\n let ent: CollectionEntry =\n bucket.find((e) => sameQuery(e.query, query)) ?? buildEntry()\n let desiredUndoable = true\n // Memoize the neutered read-only handle on the underlying handle's identity.\n // See getDocumentShared.\n let readOnlySource: CollectionHandle<T> | null = null\n let readOnlyHandle: CollectionHandle<T> | null = null\n\n return {\n getHandle: () => {\n const handle = ent.sub.getHandle() as CollectionHandle<T>\n if (!facadeReadOnly) return handle\n if (handle !== readOnlySource) {\n readOnlySource = handle\n readOnlyHandle = readOnlyCollectionHandle(handle)\n }\n return readOnlyHandle as CollectionHandle<T>\n },\n load: () => ent.sub.load(),\n setUndoable: (enabled) => {\n // See getDocumentShared.setUndoable.\n if (facadeReadOnly) return\n desiredUndoable = enabled\n ent.undoableEnabled = enabled\n },\n acquire: (onChange) => {\n // Commit time: adopt the matching registered entry if one exists,\n // else register ours. The bucket thus holds only committed entries\n // (refCount >= 1). Revive a torn-down entry with a fresh sub. See\n // getDocumentShared.acquire for the full rationale.\n const committed = bucket.find((e) => sameQuery(e.query, query))\n if (committed) {\n ent = committed\n } else {\n bucket.push(ent)\n }\n if (!ent.live) {\n ent.sub = buildSub(ent)\n ent.live = true\n }\n // Only writers set the shared undo flag (see setUndoable).\n if (!facadeReadOnly) ent.undoableEnabled = desiredUndoable\n ent.refCount++\n const notifyUnsub = ent.sub.subscribe(onChange)\n let released = false\n return () => {\n if (released) return\n released = true\n notifyUnsub()\n ent.refCount--\n if (ent.refCount <= 0) {\n ent.sub.stop()\n ent.live = false\n const idx = bucket.indexOf(ent)\n if (idx !== -1) bucket.splice(idx, 1)\n }\n }\n },\n }\n}\n\nconst getColBucket = (\n store: FirestateStore,\n definition: CollectionDefinition<FirestoreObject>,\n collectionPath: string\n): CollectionEntry[] => {\n const reg = getRegistry(store)\n let byKey = reg.cols.get(definition)\n if (!byKey) {\n byKey = new Map()\n reg.cols.set(definition, byKey)\n }\n const key = colBucketKey(collectionPath)\n let bucket = byKey.get(key)\n if (!bucket) {\n bucket = []\n byKey.set(key, bucket)\n }\n return bucket\n}\n\n/**\n * Build the query a collection hook would subscribe to, or `null` when the\n * constraints cannot form a valid query (e.g. a gated empty-`in` placeholder\n * Firestore refuses to construct). The hook only resolves a shared entry when\n * this is non-null — matching the lazy-before-load and `enabled`-gating\n * contracts where no listener should run yet.\n */\nexport const buildSharedCollectionQuery = (\n store: FirestateStore,\n collectionPath: string,\n definitionConstraints: QueryConstraint[] | undefined,\n extraConstraints: QueryConstraint[] | undefined\n): Query<unknown> | null => {\n const ref = collection(\n store.firestore,\n collectionPath\n ) as CollectionReference\n try {\n return buildCollectionQuery(\n ref,\n definitionConstraints,\n extraConstraints\n )\n } catch {\n return null\n }\n}\n","import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from 'react'\nimport { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'\nimport { collection, queryEqual } from 'firebase/firestore'\nimport type {\n CollectionReference,\n Firestore,\n Query,\n QueryConstraint,\n} from 'firebase/firestore'\nimport type {\n CollectionDefinition,\n CollectionHandle,\n CollectionState,\n DocumentDefinition,\n DocumentHandle,\n DocumentState,\n FirestoreObject,\n SelectedCollectionHandle,\n SelectedDocumentHandle,\n UndoManager,\n UndoManagerState,\n} from '../types'\nimport { valuesEqualForNoOp } from '../utils/diff'\nimport type { FirestateStore } from '../core/store'\nimport { buildCollectionQuery } from '../core/collection'\nimport {\n buildSharedCollectionQuery,\n getCollectionShared,\n getDocumentShared,\n} from '../core/shared-subscription'\n\n/**\n * Whether two hook-level `queryConstraints` arrays produce the same Firestore\n * query for a collection. `QueryConstraint` objects are opaque, so instead of\n * hand-rolling a deep compare we build both queries — with the same\n * `buildCollectionQuery` the subscription itself uses, so this check can never\n * drift from the query that actually runs — and defer to Firestore's own\n * `queryEqual`, which structurally compares filters, ordering, limits, and\n * cursors. This is what lets the subscription survive reference churn (e.g.\n * constraint inputs read from a deep-cloned document) while still rebuilding\n * when the query genuinely changes — no caller-supplied key.\n *\n * Building a query can throw: callers commonly gate with a deliberately invalid\n * placeholder like `where(documentId(), 'in', [])` while real IDs are pending,\n * and Firestore refuses to construct that. If building the prior snapshot\n * throws, no live listener could ever have run it, so there is nothing to\n * preserve — we treat the snapshots as unequal and let the caller adopt the\n * new constraints. This matters most for lazy collections, where a render can\n * carry such a placeholder before `load()` attaches any listener.\n */\nconst queryConstraintsEqual = (\n firestore: Firestore,\n collectionPath: string,\n definitionConstraints: QueryConstraint[] | undefined,\n a: QueryConstraint[] | undefined,\n b: QueryConstraint[] | undefined\n): boolean => {\n if (a === b) return true\n const ref = collection(firestore, collectionPath) as CollectionReference\n try {\n return queryEqual(\n buildCollectionQuery(ref, definitionConstraints, a),\n buildCollectionQuery(ref, definitionConstraints, b)\n )\n } catch {\n return false\n }\n}\n\n/**\n * Returned when a hook is called with `enabled: false`. Module-level constants\n * so getSnapshot returns a stable reference and useSyncExternalStore doesn't\n * re-render. Cast at the call site to the generic handle type — every method\n * is a no-op so the cast is sound.\n */\nconst NOOP = () => {}\nconst ASYNC_NOOP = async () => {}\nconst EMPTY_RECORD: Record<string, never> = {}\n\nconst DISABLED_DOCUMENT_HANDLE: DocumentHandle<FirestoreObject> = {\n data: undefined,\n update: NOOP,\n set: NOOP,\n delete: NOOP,\n isLoading: false,\n isSynced: true,\n sync: ASYNC_NOOP,\n error: undefined,\n ref: undefined,\n}\n\n// The disabled add() satisfies both overloads but performs no work and\n// returns undefined to match the bail-path contract from collection.ts.\n// Consumers using `enabled: false` should not be calling mutation methods\n// on the disabled handle.\nconst DISABLED_ADD = () => undefined\n\nconst DISABLED_COLLECTION_HANDLE: CollectionHandle<FirestoreObject> = {\n data: EMPTY_RECORD,\n update: NOOP,\n add: DISABLED_ADD,\n remove: NOOP,\n isLoading: false,\n isSynced: true,\n isActive: false,\n load: NOOP,\n sync: ASYNC_NOOP,\n error: undefined,\n ref: undefined,\n}\n\n/**\n * Opts a {@link useDocument} call into a selected slice. The hook still returns\n * a full handle (writers, `ref`, status) — only `data` is narrowed to whatever\n * `selector` returns.\n */\nexport interface DocumentSelectorOptions<\n TData extends FirestoreObject,\n TSelected,\n> {\n /**\n * Project the document's observable state down to the slice this component\n * reacts to. The selector receives the full {@link DocumentState} —\n * `{ data, isLoading, isSynced, error }`, where `data` is `undefined` while\n * the document is loading or the hook is disabled — and the component\n * re-renders *only* when the returned slice changes (per `isEqual`). Status\n * is not a freebie: read `s.isLoading`/`s.isSynced`/`s.error` here if you\n * want to react to them (e.g. `s => ({ title: s.data?.title, saving:\n * !s.isSynced })`). What you select is exactly what re-renders, and the\n * returned handle exposes only the slice plus writers/`ref`.\n */\n selector: (state: DocumentState<TData>) => TSelected\n /**\n * Decide whether two consecutive slices are equal; the hook re-renders only\n * when this returns `false`. Defaults to a deep value comparison, so a\n * selector that returns a fresh object/array of the same shape does not\n * over-render. Pass {@link shallow} for a one-level compare, or a custom\n * comparator.\n */\n isEqual?: (a: TSelected, b: TSelected) => boolean\n}\n\n/**\n * Opts a {@link useCollection} call into a selected slice. See\n * {@link DocumentSelectorOptions}; the only difference is the selector receives\n * the collection's keyed record.\n */\nexport interface CollectionSelectorOptions<\n TData extends FirestoreObject,\n TSelected,\n> {\n /**\n * Project the collection's observable state down to the slice this component\n * reacts to. The selector receives the full {@link CollectionState} —\n * `{ data, isLoading, isSynced, isActive, error }`, where `data` is the keyed\n * record (e.g. `s => s.data[id]` or `s => Object.keys(s.data)`) — and the\n * component re-renders *only* when the returned slice changes. As with\n * {@link DocumentSelectorOptions.selector}, status is reactive only if you\n * select it, and the returned handle exposes just the slice plus\n * writers/`ref`.\n */\n selector: (state: CollectionState<TData>) => TSelected\n /** See {@link DocumentSelectorOptions.isEqual}. */\n isEqual?: (a: TSelected, b: TSelected) => boolean\n}\n\n/**\n * Shape used by the non-selector hook overload to *exclude* selector options,\n * so passing a real `selector` falls through to the selector overload (which\n * infers `TSelected`) instead of silently resolving to the full-data return.\n */\ntype WithoutSelector = { selector?: undefined; isEqual?: undefined }\n\n/**\n * The projection used by the **no-selector** path of each hook: the full\n * observable state (`data` = the whole document/collection data plus every\n * status field; `isActive` is collection-only — `undefined` for documents, so\n * it no-ops in {@link selectionEqual}). `useSyncExternalStoreWithSelector`\n * memoizes and diffs it so a plain hook re-renders on any field or status\n * change — the pre-selector full-handle behavior. (The selector path projects\n * to the selector's output instead and gates purely on it.)\n *\n * It deliberately omits the handle's methods and `ref`: those are read *live*\n * from the subscription in the final merge, never memoized here. If they rode\n * along in this projection, a subscription rebuild (id/path/query/enabled\n * change) whose projection happened to be value-equal would be collapsed by the\n * equality check, and the hook would keep returning the *previous*\n * subscription's `load()`/`update()` — e.g. firing against torn-down, empty-`in`\n * constraints. This rationale holds for the selector path too, which is why it\n * also reads methods/`ref` live.\n */\ninterface ObservableSelection<TSelected> {\n data: TSelected\n isLoading: boolean\n isSynced: boolean\n error: Error | undefined\n isActive?: boolean\n}\n\n/**\n * Equality over an {@link ObservableSelection} (no-selector path): status fields\n * compared by identity, the `data` slice by `dataEqual` (the default value\n * comparison). This is what lets a change to a value-equal `data` be collapsed\n * while any status change still re-renders the plain handle.\n */\nconst selectionEqual = <TSelected>(\n a: ObservableSelection<TSelected>,\n b: ObservableSelection<TSelected>,\n dataEqual: (a: TSelected, b: TSelected) => boolean\n): boolean =>\n a.isLoading === b.isLoading &&\n a.isSynced === b.isSynced &&\n a.error === b.error &&\n a.isActive === b.isActive &&\n dataEqual(a.data, b.data)\n\n// Default slice comparison: the same value-based no-op compare the subscription\n// itself uses (`valuesEqualForNoOp`), so an identity selector reproduces the\n// pre-selector re-render behavior exactly, and a selector returning a fresh\n// object does not over-render.\nconst defaultDataEqual = valuesEqualForNoOp as <TSelected>(\n a: TSelected,\n b: TSelected\n) => boolean\n\n/**\n * Context for providing the Firestate store\n */\nexport const FirestateContext = createContext<FirestateStore | null>(null)\n\n/**\n * Hook to access the Firestate store\n */\nexport const useStore = (): FirestateStore => {\n const store = useContext(FirestateContext)\n if (!store) {\n throw new Error('useStore must be used within a FirestateProvider')\n }\n return store\n}\n\n/**\n * Hook to access the undo manager\n */\nexport const useUndoManager = (): UndoManager => {\n const store = useStore()\n const { undoManager } = store\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => undoManager.subscribe(onStoreChange),\n [undoManager]\n )\n\n // Delegate to the manager's cached snapshot so getSnapshot returns a stable\n // reference across React's multiple per-commit calls. Building the snapshot\n // inline here would create a new object every call and trip the\n // \"getSnapshot should be cached\" warning + an infinite re-render loop.\n const getSnapshot = useCallback(\n (): UndoManagerState => undoManager.getState(),\n [undoManager]\n )\n\n const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n\n return useMemo(\n () => ({\n ...state,\n push: undoManager.push,\n undo: undoManager.undo,\n redo: undoManager.redo,\n clear: undoManager.clear,\n }),\n [state, undoManager]\n )\n}\n\n/**\n * Hook to check if all tracked resources are synced\n */\nexport const useIsSynced = (): boolean => {\n const store = useStore()\n\n const subscribe = useCallback(\n (onChange: () => void) => store.subscribeToSyncState(() => onChange()),\n [store]\n )\n\n const getSnapshot = useCallback(() => store.isSynced, [store])\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n\n/**\n * Options for useDocument hook\n */\nexport interface UseDocumentOptions<TData extends FirestoreObject> {\n /** Document definition from defineDocument() */\n definition: DocumentDefinition<TData>\n /** Route/path parameters for dynamic paths */\n params?: Record<string, string>\n /** Override read-only setting */\n readOnly?: boolean\n /** Enable undo/redo for this document (default: true) */\n undoable?: boolean\n /**\n * If false, no subscription is created and a no-op handle is returned\n * (`{ data: undefined, isLoading: false, isSynced: true, ref: undefined }`).\n * Use this to gate subscriptions on route params that aren't ready yet.\n * Default: true.\n */\n enabled?: boolean\n}\n\n/**\n * Hook to subscribe to a Firestore document with real-time updates.\n *\n * The subscription is keyed on the resolved document path (`definition` +\n * computed id). When that key changes — typically because `params` produces a\n * different id — the hook tears down the old Firestore listener and attaches a\n * new one. Toggling `undoable` does not rebuild the subscription.\n *\n * `readOnly` is a *per-handle capability*, not part of the key: a `readOnly`\n * hook shares the same listener and optimistic state as a writable hook on the\n * same document (a write through the writable handle is instantly visible to\n * the read-only reader), and only this handle's own writers (`update`/`set`/\n * `delete`) and `sync` are disabled.\n *\n * Use `enabled: false` to suppress the subscription entirely (e.g., when\n * route params aren't ready yet).\n *\n * **SSR.** On the server there is no Firestore listener, so this hook returns\n * the initial handle (`{ data: undefined, isLoading: true }`). Mutations like\n * `update`/`set` will mutate orphaned local state with no effect — avoid\n * calling them server-side.\n *\n * @example\n * ```tsx\n * const projectDoc = defineDocument<Project>({\n * collection: 'projects',\n * id: (params) => params.projectId,\n * })\n *\n * function ProjectEditor({ projectId }: { projectId: string }) {\n * const { data, update, isLoading, isSynced } = useDocument({\n * definition: projectDoc,\n * params: { projectId },\n * })\n *\n * if (isLoading) return <Spinner />\n *\n * return (\n * <input\n * value={data?.name ?? ''}\n * onChange={(e) => update({ name: e.target.value })}\n * />\n * )\n * }\n * ```\n */\nexport function useDocument<TData extends FirestoreObject>(\n options: UseDocumentOptions<TData> & WithoutSelector\n): DocumentHandle<TData>\n/**\n * Selector overload: pass `selector` to narrow the returned `data` to a slice\n * and re-render only when that slice changes. Writers (`update`/`set`/`delete`)\n * and `ref` keep operating on the full document. See\n * {@link DocumentSelectorOptions}.\n *\n * @example\n * ```tsx\n * // Re-renders only when the title changes, not on any other field.\n * const { data: title, update } = useDocument({\n * definition: projectDoc,\n * params: { projectId },\n * selector: (s) => s.data?.title,\n * })\n * ```\n */\nexport function useDocument<TData extends FirestoreObject, TSelected>(\n options: UseDocumentOptions<TData> &\n DocumentSelectorOptions<TData, TSelected>\n): SelectedDocumentHandle<TData, TSelected>\nexport function useDocument<TData extends FirestoreObject, TSelected>(\n options: UseDocumentOptions<TData> & {\n selector?: (state: DocumentState<TData>) => TSelected\n isEqual?: (a: TSelected, b: TSelected) => boolean\n }\n): DocumentHandle<TData> | SelectedDocumentHandle<TData, TSelected> {\n const {\n definition,\n params = {},\n readOnly,\n undoable = true,\n enabled = true,\n selector,\n isEqual,\n } = options\n const store = useStore()\n\n // Resolve the doc id and collection path at render time. When disabled we\n // skip resolution — consumers commonly pass `enabled: false` precisely\n // because params aren't ready and definition.id(params) would fail.\n const docId = enabled\n ? typeof definition.id === 'function'\n ? definition.id(params)\n : definition.id\n : undefined\n\n const collectionPath = enabled\n ? typeof definition.collection === 'function'\n ? definition.collection(params)\n : definition.collection\n : undefined\n\n // Resolve (or create) the shared subscription for this resource. Every hook\n // targeting the same document shares one instance — and so one listener and\n // one optimistic state — instead of building a private subscription. Created\n // in render (no listener attached) so getSnapshot returns the real, live\n // handle immediately, including its `ref`.\n const shared = useMemo(\n () =>\n enabled && docId !== undefined && collectionPath !== undefined\n ? getDocumentShared<TData>({\n store,\n definition,\n collectionPath,\n docId,\n readOnly,\n })\n : null,\n [enabled, store, definition, docId, collectionPath, readOnly]\n )\n\n // Keep the shared subscription's undo flag in sync without re-subscribing\n // (toggling `undoable` must not tear the listener down). Last writer wins\n // across co-mounted hooks; the common case is a single value per resource.\n useEffect(() => {\n shared?.setUndoable(undoable)\n }, [shared, undoable])\n\n const subscribe = useCallback(\n (onChange: () => void) => {\n if (!shared) return NOOP\n shared.setUndoable(undoable)\n // Bump the shared ref count, register this hook's change callback, and\n // activate the listener. Release tears the listener down only when this\n // is the last lease (see shared-subscription.ts).\n const release = shared.acquire(onChange)\n // load() attaches the listener and can throw synchronously (e.g. an\n // invalid ref). acquire() has already taken the lease, so release it\n // before propagating — otherwise refCount sticks >=1, the entry is never\n // evicted, and the callback/listener leak (a later sibling on the same\n // key inherits the zombie entry).\n try {\n shared.load()\n } catch (e) {\n release()\n throw e\n }\n return release\n },\n // `undoable` intentionally omitted: the effect above syncs it without\n // resubscribing.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [shared]\n )\n\n const getSnapshot = useCallback(\n () =>\n shared\n ? shared.getHandle()\n : (DISABLED_DOCUMENT_HANDLE as DocumentHandle<TData>),\n [shared]\n )\n\n // Project the live handle to the value that drives re-renders. Keyed on\n // `selector` so a referentially-new selector (e.g. one closing over a prop)\n // re-projects; an inline selector still dedupes against the committed value\n // via `equal`, so callers need not memoize it.\n //\n // With a `selector` (pure mode): build the observable state and hand it to\n // the selector — the projection IS the selector's output, gated purely by\n // `equal` below, so a status field the selector did not read can neither\n // re-render the component nor appear on its handle. Without one: project the\n // full state and gate on `data` + every status field (legacy full handle).\n const select = useCallback(\n (handle: DocumentHandle<TData>): TSelected | DocumentState<TData> => {\n const state: DocumentState<TData> = {\n data: handle.data,\n isLoading: handle.isLoading,\n isSynced: handle.isSynced,\n error: handle.error,\n }\n return selector ? selector(state) : state\n },\n [selector]\n )\n\n const equal = useCallback(\n (\n a: TSelected | DocumentState<TData>,\n b: TSelected | DocumentState<TData>\n ): boolean =>\n selector\n ? (isEqual ?? defaultDataEqual)(a as TSelected, b as TSelected)\n : selectionEqual(\n a as DocumentState<TData>,\n b as DocumentState<TData>,\n defaultDataEqual\n ),\n [selector, isEqual]\n )\n\n const selection = useSyncExternalStoreWithSelector(\n subscribe,\n getSnapshot,\n getSnapshot,\n select,\n equal\n )\n\n // Re-wrap into a handle. Methods and `ref` are read *live* from the current\n // shared subscription (via getSnapshot) so a rebuild always hands back the\n // new subscription's methods, even when the projection was value-equal (see\n // ObservableSelection). `getSnapshot` identity changes whenever the resource\n // key does, which re-runs this memo.\n //\n // Keyed on selector *presence*, never its identity: an inline selector is a\n // fresh function each render yet yields a value-equal `selection`, so the\n // handle must keep referential identity (callers need not memoize it). Only\n // the handle's shape — selected vs full — depends on the selector.\n const hasSelector = selector != null\n return useMemo(() => {\n const handle = getSnapshot()\n if (hasSelector) {\n // Pure selected handle: `data` is the slice; status is intentionally\n // absent (select it to react to it). Writers act on the full doc.\n return {\n data: selection as TSelected,\n update: handle.update,\n set: handle.set,\n delete: handle.delete,\n sync: handle.sync,\n ref: handle.ref,\n }\n }\n const s = selection as DocumentState<TData>\n return {\n data: s.data,\n update: handle.update,\n set: handle.set,\n delete: handle.delete,\n isLoading: s.isLoading,\n isSynced: s.isSynced,\n sync: handle.sync,\n error: s.error,\n ref: handle.ref,\n }\n }, [selection, getSnapshot, hasSelector])\n}\n\n/**\n * Options for useCollection hook\n */\nexport interface UseCollectionOptions<TData extends FirestoreObject> {\n /** Collection definition from defineCollection() */\n definition: CollectionDefinition<TData>\n /** Route/path parameters for dynamic paths */\n params?: Record<string, string>\n /** Override read-only setting */\n readOnly?: boolean\n /** Additional query constraints */\n queryConstraints?: QueryConstraint[]\n /** Enable undo/redo for this collection (default: true) */\n undoable?: boolean\n /**\n * If false, no subscription is created and a no-op handle is returned\n * (`{ data: {}, isLoading: false, isActive: false }`). Use this to gate on\n * route params that aren't ready yet. Default: true.\n */\n enabled?: boolean\n}\n\n/**\n * Hook to subscribe to a Firestore collection with real-time updates.\n *\n * The subscription is keyed on the resolved collection path and the *semantic\n * identity* of `queryConstraints`. When either changes, the listener is torn\n * down and re-attached with the new query. Toggling `undoable` does not rebuild\n * the subscription. `readOnly` is a per-handle capability, not part of the key —\n * a `readOnly` hook shares one listener and optimistic state with a writable\n * hook on the same query (see {@link useDocument}).\n *\n * **You do not need to memoize `queryConstraints`.** `QueryConstraint` objects\n * are opaque, so Firestate compares the *built query* with Firestore's own\n * `queryEqual` instead of comparing array references. A fresh array that\n * produces the same query (e.g. constraint inputs read from a document that\n * Firestate deep-clones on optimistic updates) does not rebuild the listener;\n * only a genuine change to the query does:\n *\n * ```tsx\n * // stationIds may change reference on every edit to its parent document,\n * // even when its contents are unchanged — the listener survives anyway.\n * const stations = useCollection({\n * definition: weatherStations,\n * queryConstraints: [where(documentId(), 'in', stationIds)],\n * })\n * ```\n *\n * Memoizing is still a fine micro-optimization (it skips the per-render query\n * build + compare via the reference fast-path), but it is no longer required\n * for listener stability.\n *\n * Use `enabled: false` to suppress the subscription entirely (e.g., when\n * route params aren't ready yet).\n *\n * **SSR.** On the server there is no Firestore listener, so this hook returns\n * the initial handle (`{ data: {}, isLoading: true }` for non-lazy, or\n * `isActive: false` for lazy). Avoid calling mutations server-side.\n *\n * @example\n * ```tsx\n * const spacesCollection = defineCollection<Space>({\n * path: (params) => `projects/${params.projectId}/spaces`,\n * lazy: true,\n * })\n *\n * function SpacesList({ projectId }: { projectId: string }) {\n * const { data, update, load, isActive, isLoading } = useCollection({\n * definition: spacesCollection,\n * params: { projectId },\n * })\n *\n * // Lazy load on mount\n * useEffect(() => { load() }, [load])\n *\n * if (!isActive) return <Button onClick={load}>Load Spaces</Button>\n * if (isLoading) return <Spinner />\n *\n * return (\n * <ul>\n * {Object.values(data).map((space) => (\n * <li key={space.id}>{space.name}</li>\n * ))}\n * </ul>\n * )\n * }\n * ```\n */\nexport function useCollection<TData extends FirestoreObject>(\n options: UseCollectionOptions<TData> & WithoutSelector\n): CollectionHandle<TData>\n/**\n * Selector overload: pass `selector` to narrow the returned `data` to a slice\n * of the collection and re-render only when that slice changes. Writers\n * (`update`/`add`/`remove`) and `ref` keep operating on the full collection.\n * See {@link CollectionSelectorOptions}.\n *\n * @example\n * ```tsx\n * // Re-renders only when this one document's slice changes.\n * const { data: space } = useCollection({\n * definition: spacesCollection,\n * params: { projectId },\n * selector: (s) => s.data[spaceId],\n * })\n * ```\n */\nexport function useCollection<TData extends FirestoreObject, TSelected>(\n options: UseCollectionOptions<TData> &\n CollectionSelectorOptions<TData, TSelected>\n): SelectedCollectionHandle<TData, TSelected>\nexport function useCollection<TData extends FirestoreObject, TSelected>(\n options: UseCollectionOptions<TData> & {\n selector?: (state: CollectionState<TData>) => TSelected\n isEqual?: (a: TSelected, b: TSelected) => boolean\n }\n): CollectionHandle<TData> | SelectedCollectionHandle<TData, TSelected> {\n const {\n definition,\n params = {},\n readOnly,\n queryConstraints,\n undoable = true,\n enabled = true,\n selector,\n isEqual,\n } = options\n const store = useStore()\n\n // Resolve the collection path at render time. When disabled we skip\n // resolution — consumers commonly pass `enabled: false` precisely because\n // params aren't ready.\n const collectionPath = enabled\n ? typeof definition.path === 'function'\n ? definition.path(params)\n : definition.path\n : undefined\n\n // Stabilize `queryConstraints` by *query identity*. QueryConstraint objects\n // are opaque and can't be deep-compared directly, but the built query can —\n // see queryConstraintsEqual(). When the incoming array produces the same\n // query as the one we're already subscribed with (e.g. constraint inputs\n // read from a deep-cloned document churned the array reference without\n // changing the query), we keep the previous array reference so the memo\n // below does not rebuild. A genuine change adopts the new reference and\n // re-attaches the listener. When the path is unresolved we can't build a\n // query, so we just pass the constraints through (the memo returns null).\n //\n // The comparison may only run against constraints captured during an *active*\n // render (enabled with a resolved path). Constraints captured while disabled\n // or unresolved can be ones the caller is gating precisely because they don't\n // form a valid query yet — e.g. `where(documentId(), 'in', [])`, which\n // Firestore refuses to build. Building such a stale snapshot just to compare\n // would throw, so when the prior snapshot wasn't active we adopt the current\n // constraints outright. There is no live listener to preserve in that case\n // (the subscription was null), so adopting a fresh reference costs nothing.\n const stableConstraintsRef = useRef(queryConstraints)\n const stableActiveRef = useRef(false)\n const active = enabled && collectionPath !== undefined\n if (\n collectionPath === undefined ||\n !stableActiveRef.current ||\n !queryConstraintsEqual(\n store.firestore,\n collectionPath,\n definition.queryConstraints,\n stableConstraintsRef.current,\n queryConstraints\n )\n ) {\n stableConstraintsRef.current = queryConstraints\n }\n stableActiveRef.current = active\n const stableConstraints = stableConstraintsRef.current\n\n const isLazy = definition.lazy ?? false\n\n // Build the query this hook subscribes to, keyed by *query identity*\n // (stableConstraints already absorbs reference churn). This is what the shared\n // registry matches on via `queryEqual`, so two hooks with semantically equal\n // queries share one listener regardless of array identity. `null` when the\n // constraints can't form a valid query yet (e.g. a gated empty-`in`\n // placeholder, or while disabled/unresolved): no listener can run, so the hook\n // resolves no shared entry and returns the disabled handle.\n const builtQuery = useMemo<Query<unknown> | null>(\n () =>\n active\n ? buildSharedCollectionQuery(\n store,\n collectionPath!,\n definition.queryConstraints,\n stableConstraints\n )\n : null,\n [active, store, collectionPath, definition, stableConstraints]\n )\n\n // Resolve (or create) the shared subscription for this resource+query. As with\n // useDocument, every hook on the same collection+query shares one instance.\n const shared = useMemo(\n () =>\n active && builtQuery !== null\n ? getCollectionShared<TData>({\n store,\n definition,\n collectionPath: collectionPath!,\n readOnly,\n queryConstraints: stableConstraints,\n query: builtQuery,\n })\n : null,\n [\n active,\n store,\n definition,\n collectionPath,\n readOnly,\n stableConstraints,\n builtQuery,\n ]\n )\n\n useEffect(() => {\n shared?.setUndoable(undoable)\n }, [shared, undoable])\n\n const subscribe = useCallback(\n (onChange: () => void) => {\n if (!shared) return NOOP\n shared.setUndoable(undoable)\n const release = shared.acquire(onChange)\n // Lazy collections activate the shared listener only via `load()` on the\n // handle; non-lazy ones activate on mount. Either way the listener stays\n // up until the last lease releases.\n if (!isLazy) {\n // See useDocument's subscribe: release the lease acquire() just took if\n // load() throws synchronously, or the entry/listener/callback leak.\n try {\n shared.load()\n } catch (e) {\n release()\n throw e\n }\n }\n return release\n },\n // `undoable` intentionally omitted: the effect above syncs it without\n // resubscribing.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [shared, isLazy]\n )\n\n const getSnapshot = useCallback(\n () =>\n shared\n ? shared.getHandle()\n : (DISABLED_COLLECTION_HANDLE as CollectionHandle<TData>),\n [shared]\n )\n\n // See useDocument for the rationale. With a `selector` (pure mode): project\n // to the selector's output over the full collection state and gate purely on\n // it. Without one: project the full state and gate on `data` + every status\n // field. Methods/`ref` are read live in the final merge either way.\n const select = useCallback(\n (handle: CollectionHandle<TData>): TSelected | CollectionState<TData> => {\n const state: CollectionState<TData> = {\n data: handle.data,\n isLoading: handle.isLoading,\n isSynced: handle.isSynced,\n isActive: handle.isActive,\n error: handle.error,\n }\n return selector ? selector(state) : state\n },\n [selector]\n )\n\n const equal = useCallback(\n (\n a: TSelected | CollectionState<TData>,\n b: TSelected | CollectionState<TData>\n ): boolean =>\n selector\n ? (isEqual ?? defaultDataEqual)(a as TSelected, b as TSelected)\n : selectionEqual(\n a as CollectionState<TData>,\n b as CollectionState<TData>,\n defaultDataEqual\n ),\n [selector, isEqual]\n )\n\n const selection = useSyncExternalStoreWithSelector(\n subscribe,\n getSnapshot,\n getSnapshot,\n select,\n equal\n )\n\n // See useDocument: keyed on selector *presence*, not identity, so an inline\n // selector still yields a stable handle. Methods/`ref` read live from\n // getSnapshot; only the handle's shape depends on the selector.\n const hasSelector = selector != null\n return useMemo(() => {\n const handle = getSnapshot()\n if (hasSelector) {\n // Pure selected handle: status is absent (select it to react to it);\n // writers and `load` act on the full collection.\n return {\n data: selection as TSelected,\n update: handle.update,\n add: handle.add,\n remove: handle.remove,\n load: handle.load,\n sync: handle.sync,\n ref: handle.ref,\n }\n }\n const s = selection as CollectionState<TData>\n return {\n data: s.data,\n update: handle.update,\n add: handle.add,\n remove: handle.remove,\n isLoading: s.isLoading,\n isSynced: s.isSynced,\n isActive: s.isActive ?? false,\n load: handle.load,\n sync: handle.sync,\n error: s.error,\n ref: handle.ref,\n }\n }, [selection, getSnapshot, hasSelector])\n}\n\n/**\n * Keyboard shortcut hook for undo/redo\n *\n * @example\n * ```tsx\n * function App() {\n * useUndoKeyboardShortcuts()\n * return <YourApp />\n * }\n * ```\n */\nexport const useUndoKeyboardShortcuts = (): void => {\n // Read the manager ref directly — we only need .undo() / .redo() (stable\n // refs), not its state. Subscribing via useUndoManager would re-render\n // the host component on every undo-stack change.\n const undoManager = useStore().undoManager\n\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n const platform =\n (\n navigator as Navigator & {\n userAgentData?: { platform: string }\n }\n ).userAgentData?.platform ?? navigator.platform\n const isMac = platform.toUpperCase().includes('MAC')\n const modifier = isMac ? e.metaKey : e.ctrlKey\n\n if (!modifier) return\n\n if (e.key === 'z' && !e.shiftKey) {\n e.preventDefault()\n undoManager.undo()\n } else if ((e.key === 'z' && e.shiftKey) || e.key === 'y') {\n e.preventDefault()\n undoManager.redo()\n }\n }\n\n window.addEventListener('keydown', handleKeyDown)\n return () => window.removeEventListener('keydown', handleKeyDown)\n }, [undoManager])\n}\n","/**\n * Registry-driven Firestate API.\n *\n * Declare every document and collection in a single object and let the\n * library generate the typed read/write hooks. Replaces hand-writing a\n * `useSpaces`, `useWallTypes`, ... hook per Firestore collection.\n *\n * ```ts\n * interface TaskList { name: string; createdAt: number }\n * interface Task { title: string; completed: boolean }\n *\n * export const { useTaskList, useTasks } = createFirestate({\n * taskList: doc<TaskList>('taskLists/{listId}'),\n * tasks: col<Task>('taskLists/{listId}/tasks'),\n * })\n *\n * // At the call site:\n * const taskList = useTaskList({ listId })\n * const tasks = useTasks({ listId })\n * ```\n */\nimport { defineDocument, defineCollection } from \"./schema\";\nimport {\n useDocument,\n useCollection,\n type UseDocumentOptions,\n type UseCollectionOptions,\n type DocumentSelectorOptions,\n type CollectionSelectorOptions,\n} from \"../react/hooks\";\nimport type {\n CollectionDefinition,\n CollectionHandle,\n CollectionState,\n DocumentDefinition,\n DocumentHandle,\n DocumentState,\n FirestoreObject,\n SelectedCollectionHandle,\n SelectedDocumentHandle,\n} from \"../types\";\nimport type { QueryConstraint } from \"firebase/firestore\";\nimport type { ZodType, z } from \"zod\";\n\n/**\n * Knobs forwarded from a generated document hook to {@link useDocument}.\n * Same shape as `UseDocumentOptions` minus the fields the registry already\n * owns (`definition`, `params`).\n */\nexport type DocHookOptions<T extends FirestoreObject> = Omit<\n UseDocumentOptions<T>,\n \"definition\" | \"params\"\n>;\n\n/**\n * Knobs forwarded from a generated collection hook to {@link useCollection}.\n */\nexport type ColHookOptions<T extends FirestoreObject> = Omit<\n UseCollectionOptions<T>,\n \"definition\" | \"params\"\n>;\n\n// ---------------------------------------------------------------------------\n// Registry entry shapes\n// ---------------------------------------------------------------------------\n\ninterface CommonEntryOptions {\n /** Debounce interval for autosave (ms). */\n autosave?: number;\n /** Minimum loading indicator time (ms). */\n minLoadTime?: number;\n /** Whether this entry is read-only. */\n readOnly?: boolean;\n /** Retry the snapshot listener on transient errors. */\n retryOnError?: boolean;\n /** Retry interval (ms). */\n retryInterval?: number;\n}\n\n/**\n * Document entry in a Firestate registry. Produced by {@link doc}.\n *\n * The `P` generic carries the path template's string-literal type so the\n * generated hook can type-check param keys. `__kind` is a runtime\n * discriminator; `__type` is a phantom field used purely for inference at\n * the call site and is never read.\n */\nexport interface DocEntry<\n T extends FirestoreObject,\n P extends string = string\n> extends CommonEntryOptions {\n readonly __kind: \"document\";\n readonly __type?: T;\n /**\n * Path template, e.g. `'taskLists/{listId}'`, or a function returning the\n * **full document path** at runtime (the collection/id split happens\n * per-call via {@link splitDocPath}). Use the function form for paths that\n * branch on a param. See {@link PathArg}.\n */\n path: PathArg<P>;\n /**\n * Zod schema. **Required** — firestate's registry API is opinionated\n * about Zod. The schema is the source of `T` for the generated hooks\n * via `z.infer`, and firestate runs `schema.parse(...)` on full-payload\n * writes (`set`/`add`) so bad data throws at the call site rather than\n * after a Firestore round trip. Partial `update(diff)` is NOT validated\n * (diffs frequently contain Firestore sentinels like `serverTimestamp()`).\n *\n * If you don't want a schema at all, use {@link defineDocument} directly —\n * the escape hatch keeps the plain-TypeScript form at the cost of looser\n * param typing and no runtime validation.\n */\n schema: ZodType<T>;\n\n /**\n * Derive a **named slice-hook** off this document, sharing its schema and\n * path — the schema is handed to firestate once, here, and never\n * re-specified. The `selector` receives the full {@link DocumentState};\n * return the slice the generated hook reacts to. For a *parameterized* slice,\n * declare the extra params as the selector's second argument — the generated\n * hook then requires the path params **and** those, merged into one bag.\n *\n * Pass the result to {@link createFirestate} under the key the hook is named\n * for. Status is reactive only if the slice reads it, exactly as the inline\n * `selector` option (see {@link DocumentHandle}); the comparator (`isEqual`)\n * is baked in here, not passed per call.\n *\n * ```ts\n * const project = doc({ path: 'projects/{projectId}', schema: ProjectSchema })\n * const { useProject, useProjectTitle } = createFirestate({\n * project, // → useProject (full)\n * projectTitle: project.select((s) => s.data?.name), // → useProjectTitle\n * })\n * ```\n *\n * A derived entry is a leaf, not a base: there is intentionally no\n * `.select(...).select(...)` chaining.\n *\n * `PExtra` (the selector's own params) defaults to `{}`: a one-argument\n * selector leaves it unbound, a two-argument one infers it from the annotated\n * second parameter. One signature keeps the selector's `state` arg reliably\n * typed in both cases. `PExtra` is intentionally unconstrained — leaving it\n * `extends Record<string, string>` made TS resolve a param-less selector's\n * `PExtra` to that constraint (not the `{}` default), wrongly forcing a\n * `params` arg on no-placeholder paths; unconstrained also lets a slice take\n * non-string params (e.g. `{ index: number }`).\n */\n select<TSelected, PExtra = {}>(\n selector: (state: DocumentState<T>, params: PExtra) => TSelected,\n options?: SelectOptions<TSelected>\n ): SelectedDocEntry<T, P, PExtra, TSelected>;\n}\n\n/** Collection entry in a Firestate registry. Produced by {@link col}. */\nexport interface ColEntry<\n T extends FirestoreObject,\n P extends string = string\n> extends CommonEntryOptions {\n readonly __kind: \"collection\";\n readonly __type?: T;\n /**\n * Path template, e.g. `'taskLists/{listId}/tasks'`, or a function returning\n * the **collection path** at runtime. Use the function form for paths that\n * branch on a param. See {@link PathArg}.\n */\n path: PathArg<P>;\n /** Zod schema. Required. See {@link DocEntry.schema}. */\n schema: ZodType<T>;\n /** Only subscribe when `load()` is called. */\n lazy?: boolean;\n /** Additional Firestore query constraints. */\n queryConstraints?: QueryConstraint[];\n\n /**\n * Derive a **named slice-hook** off this collection, sharing its schema,\n * path, and query — see {@link DocEntry.select}. The `selector` receives the\n * full {@link CollectionState} (`s.data` is the keyed record), and a\n * parameterized slice declares its extra params as the selector's second\n * argument.\n *\n * ```ts\n * const tasks = col({ path: 'projects/{projectId}/tasks', schema: TaskSchema })\n * const { useTasks, useTaskIds, useTaskById } = createFirestate({\n * tasks, // → useTasks (full)\n * taskIds: tasks.select((s) => Object.keys(s.data)), // → useTaskIds\n * taskById: tasks.select((s, p: { id: string }) => s.data[p.id]), // → useTaskById\n * })\n * // useTaskById requires the merged bag: useTaskById({ projectId, id })\n * ```\n *\n * `PExtra` defaults to `{}` and is unconstrained — see {@link DocEntry.select}.\n */\n select<TSelected, PExtra = {}>(\n selector: (state: CollectionState<T>, params: PExtra) => TSelected,\n options?: SelectOptions<TSelected>\n ): SelectedColEntry<T, P, PExtra, TSelected>;\n}\n\n/**\n * Options bundled into a `.select(...)` entry at definition time. Kept separate\n * from the runtime hook options (`enabled`/`readOnly`/`queryConstraints`)\n * because these are baked into the named hook, not passed per call.\n */\nexport interface SelectOptions<TSelected> {\n /**\n * Comparator for this named hook's slice; the hook re-renders only when it\n * returns `false`. Defaults to a deep value compare (so a fresh object/array\n * of equal shape does not over-render). Pass {@link shallow} or a custom fn.\n */\n isEqual?: (a: TSelected, b: TSelected) => boolean;\n}\n\n/**\n * A {@link DocEntry} narrowed by a `.select(...)` projection. Produced by\n * {@link DocEntry.select}, consumed by {@link createFirestate}, which turns it\n * into a hook whose `data` is the slice (`TSelected`) and whose params are the\n * path params (`P`) merged with the selector's own params (`PExtra`).\n *\n * The schema/path/options live on `base` — a derived entry never re-declares\n * them. `PExtra` is `{}` for an un-parameterized selector.\n */\nexport interface SelectedDocEntry<\n T extends FirestoreObject,\n P extends string,\n PExtra,\n TSelected\n> {\n readonly __kind: \"document-selected\";\n /** Base entry carrying schema/path/options — handed to firestate once. */\n readonly base: DocEntry<T, P>;\n /** Projection over the full state; receives the merged params bag at runtime. */\n readonly selector: (state: DocumentState<T>, params: PExtra) => TSelected;\n /** Comparator baked in at definition time (see {@link SelectOptions}). */\n readonly isEqual?: (a: TSelected, b: TSelected) => boolean;\n}\n\n/**\n * A {@link ColEntry} narrowed by a `.select(...)` projection. See\n * {@link SelectedDocEntry}; the selector receives the collection's keyed state.\n */\nexport interface SelectedColEntry<\n T extends FirestoreObject,\n P extends string,\n PExtra,\n TSelected\n> {\n readonly __kind: \"collection-selected\";\n /** Base entry carrying schema/path/query/options — handed to firestate once. */\n readonly base: ColEntry<T, P>;\n /** Projection over the full state; receives the merged params bag at runtime. */\n readonly selector: (state: CollectionState<T>, params: PExtra) => TSelected;\n /** Comparator baked in at definition time (see {@link SelectOptions}). */\n readonly isEqual?: (a: TSelected, b: TSelected) => boolean;\n}\n\nexport type FirestateEntry<\n T extends FirestoreObject = FirestoreObject,\n P extends string = string\n> = DocEntry<T, P> | ColEntry<T, P>;\n\n/** Any `.select(...)`-derived entry, regardless of its type parameters. */\nexport type AnySelectedEntry =\n | SelectedDocEntry<any, any, any, any>\n | SelectedColEntry<any, any, any, any>;\n\nexport type FirestateRegistry = Record<\n string,\n FirestateEntry<any, any> | AnySelectedEntry\n>;\n\n// ---------------------------------------------------------------------------\n// Path → params extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Extract `{name}` placeholders from a path template into a params shape.\n *\n * - `'users'` → `{}`\n * - `'users/{userId}'` → `{ userId: string }`\n * - `'projects/{projectId}/revisions/{revisionId}'` → `{ projectId: string; revisionId: string }`\n *\n * When the path is widened to `string` (no literal preserved), we fall\n * back to `Record<string, string>` so existing call sites keep compiling.\n */\nexport type ParamsOf<P extends string> = string extends P\n ? Record<string, string>\n : Prettify<RawParamsOf<P>>;\n\ntype RawParamsOf<P extends string> =\n P extends `${string}{${infer K}}${infer Rest}`\n ? { [Key in K]: string } & RawParamsOf<Rest>\n : {};\n\n// Force TS to evaluate intersections so error messages show\n// `{ projectId: string; revisionId: string }` instead of an intersection.\ntype Prettify<T> = { [K in keyof T]: T[K] } & {};\n\n/**\n * The `path` accepted by {@link doc} / {@link col}. Either a static template\n * (whose `{param}` placeholders are interpolated and whose param keys are\n * inferred via {@link ParamsOf}), or a function that returns the path at\n * runtime — for paths that branch on a param, e.g. live\n * `projects/{projectId}/spaces` vs. revision\n * `projects/{projectId}/revisions/{revisionId}/spaces`.\n *\n * With the function form, params can't be inferred from a template, so the\n * generated hook's params fall back to `Record<string, string>`.\n */\nexport type PathArg<P extends string> =\n | P\n | ((params: Record<string, string>) => string);\n\n// ---------------------------------------------------------------------------\n// Entry factories\n// ---------------------------------------------------------------------------\n\n// `select` is excluded too: it's a method the factory attaches, never an input.\ntype DocOpts<T extends FirestoreObject> = Omit<\n DocEntry<T>,\n \"__kind\" | \"__type\" | \"path\" | \"select\"\n>;\ntype ColOpts<T extends FirestoreObject> = Omit<\n ColEntry<T>,\n \"__kind\" | \"__type\" | \"path\" | \"select\"\n>;\n\n/**\n * Declare a single-document entry for a Firestate registry.\n *\n * **A Zod `schema` field is required.** Both the data type (`T`) and the\n * path's literal type (`P`) are inferred from the call — `T` via\n * `z.infer<S>`, `P` from `path` — so the generated hook can statically\n * type-check the params object the caller passes. The schema also runs\n * at runtime on full-payload writes (`set`/`add`).\n *\n * If you'd rather not provide a schema at all, use {@link defineDocument}\n * directly — that escape hatch keeps the plain-TypeScript form, at the\n * cost of looser param typing on the hook and no runtime validation.\n *\n * `path` may also be a function returning the full document path at runtime —\n * for paths that branch on a param. Param keys can't be inferred from a\n * function, so they fall back to `Record<string, string>`. See {@link PathArg}.\n *\n * ```ts\n * import { z } from 'zod'\n *\n * const TaskListSchema = z.object({ name: z.string(), createdAt: z.number() })\n * doc({ path: 'taskLists/{listId}', schema: TaskListSchema })\n * // → DocEntry<{ name: string; createdAt: number }, 'taskLists/{listId}'>\n * ```\n */\nexport function doc<\n S extends ZodType<FirestoreObject>,\n const P extends string = string\n>(\n opts: Omit<DocOpts<z.infer<S>>, \"schema\"> & {\n schema: S;\n path: PathArg<P>;\n }\n): DocEntry<z.infer<S>, P> {\n const { path, ...rest } = opts;\n // Static templates fail loud at registration: a malformed placeholder or a\n // path that can't be split into a non-empty collection + id throws here.\n // Function paths are checked per-call in buildDocumentDefinition, once they\n // resolve to a concrete string.\n if (typeof path === \"string\") {\n validateTemplate(path);\n splitDocPath(path);\n }\n const entry = { __kind: \"document\", path, ...rest } as Record<string, unknown>;\n // Attach the `.select(...)` builder. It closes over `entry`, so a derived\n // entry's `base` is this exact object — the schema/path/options are handed to\n // firestate once, here, and reused by every slice-hook derived from it.\n entry.select = (\n selector: (\n state: DocumentState<FirestoreObject>,\n params: Record<string, string>\n ) => unknown,\n options?: SelectOptions<unknown>\n ): SelectedDocEntry<FirestoreObject, string, Record<string, string>, unknown> => ({\n __kind: \"document-selected\",\n base: entry as unknown as DocEntry<FirestoreObject, string>,\n selector,\n isEqual: options?.isEqual,\n });\n return entry as unknown as DocEntry<z.infer<S>, P>;\n}\n\n/**\n * Declare a collection entry for a Firestate registry. See {@link doc}\n * for the schema/typing contract. `path` may also be a function returning\n * the collection path at runtime — see {@link PathArg}.\n */\nexport function col<\n S extends ZodType<FirestoreObject>,\n const P extends string = string\n>(\n opts: Omit<ColOpts<z.infer<S>>, \"schema\"> & {\n schema: S;\n path: PathArg<P>;\n }\n): ColEntry<z.infer<S>, P> {\n const { path, ...rest } = opts;\n // Static templates fail loud at registration; function paths resolve later.\n if (typeof path === \"string\") {\n validateTemplate(path);\n }\n const entry = { __kind: \"collection\", path, ...rest } as Record<string, unknown>;\n // See doc(): the builder closes over `entry` so derived hooks reuse this\n // collection's schema/path/query — never re-specified.\n entry.select = (\n selector: (\n state: CollectionState<FirestoreObject>,\n params: Record<string, string>\n ) => unknown,\n options?: SelectOptions<unknown>\n ): SelectedColEntry<FirestoreObject, string, Record<string, string>, unknown> => ({\n __kind: \"collection-selected\",\n base: entry as unknown as ColEntry<FirestoreObject, string>,\n selector,\n isEqual: options?.isEqual,\n });\n return entry as unknown as ColEntry<z.infer<S>, P>;\n}\n\n// ---------------------------------------------------------------------------\n// createFirestate\n// ---------------------------------------------------------------------------\n\ntype HookName<K extends string> = `use${Capitalize<K>}`;\n\n// Excludes selector options from the non-selector overload, so passing a real\n// `selector` falls through to the selector overload (which infers `TSelected`)\n// instead of resolving to the full-data return.\ntype NoSelector = { selector?: undefined; isEqual?: undefined };\n\n// Each generated hook is overloaded: call it without a `selector` to get the\n// full handle, or with one to get a handle whose `data` is the selected slice.\n// The two `*OptionalParams` / `*RequiredParams` shapes capture whether the path\n// template has placeholders (optional vs. required `params`). In the selector\n// overload `options` is required (it must carry `selector`), so `params` cannot\n// be optional before it — the no-placeholder selector form therefore takes\n// `params` as `Record<string, string> | undefined` (pass `{}` or `undefined`).\n\ninterface DocHookOptionalParams<T extends FirestoreObject> {\n (\n params?: Record<string, string>,\n options?: DocHookOptions<T> & NoSelector\n ): DocumentHandle<T>;\n <TSelected>(\n params: Record<string, string> | undefined,\n options: DocHookOptions<T> & DocumentSelectorOptions<T, TSelected>\n ): SelectedDocumentHandle<T, TSelected>;\n}\n\ninterface DocHookRequiredParams<T extends FirestoreObject, P extends string> {\n (\n params: ParamsOf<P>,\n options?: DocHookOptions<T> & NoSelector\n ): DocumentHandle<T>;\n <TSelected>(\n params: ParamsOf<P>,\n options: DocHookOptions<T> & DocumentSelectorOptions<T, TSelected>\n ): SelectedDocumentHandle<T, TSelected>;\n}\n\ninterface ColHookOptionalParams<T extends FirestoreObject> {\n (\n params?: Record<string, string>,\n options?: ColHookOptions<T> & NoSelector\n ): CollectionHandle<T>;\n <TSelected>(\n params: Record<string, string> | undefined,\n options: ColHookOptions<T> & CollectionSelectorOptions<T, TSelected>\n ): SelectedCollectionHandle<T, TSelected>;\n}\n\ninterface ColHookRequiredParams<T extends FirestoreObject, P extends string> {\n (\n params: ParamsOf<P>,\n options?: ColHookOptions<T> & NoSelector\n ): CollectionHandle<T>;\n <TSelected>(\n params: ParamsOf<P>,\n options: ColHookOptions<T> & CollectionSelectorOptions<T, TSelected>\n ): SelectedCollectionHandle<T, TSelected>;\n}\n\n// ---------------------------------------------------------------------------\n// Selected (`.select`) hook shapes\n// ---------------------------------------------------------------------------\n\n// The merged params bag for a selected hook: the path-template params (`P`)\n// intersected with the selector's own params (`PExtra`), flattened so errors\n// read as one object instead of an intersection.\n//\n// `PExtra` arrives as `any` when the selector took no params (an empty `{}`\n// `PExtra` widens to `any` through the registry's `AnySelectedEntry` bound — the\n// `{}` is absorbed in the contravariant selector position). `any` here means \"no\n// declared params\", so the bag is just the path params; otherwise a no-arg slice\n// on a no-placeholder path would wrongly demand a `Record<string, any>`. A real\n// `PExtra` (e.g. `{ id: string }`) is never `any` and merges normally.\ntype IsAny<T> = 0 extends 1 & T ? true : false;\ntype SelectedParams<P extends string, PExtra> = IsAny<PExtra> extends true\n ? ParamsOf<P>\n : Prettify<ParamsOf<P> & PExtra>;\n\n// Generated hook for a selected document entry: `data` is the slice, `params`\n// is the merged bag, and `options` carries only the runtime knobs — the\n// selector and its comparator are baked in, so neither appears here. Params are\n// optional only when the merged bag has no keys (static path, no selector params).\ntype SelectedDocHookFor<\n T extends FirestoreObject,\n P extends string,\n PExtra,\n TSelected\n> = keyof SelectedParams<P, PExtra> extends never\n ? (\n params?: Record<string, string>,\n options?: DocHookOptions<T>\n ) => SelectedDocumentHandle<T, TSelected>\n : (\n params: SelectedParams<P, PExtra>,\n options?: DocHookOptions<T>\n ) => SelectedDocumentHandle<T, TSelected>;\n\n// As {@link SelectedDocHookFor}, for a selected collection entry.\ntype SelectedColHookFor<\n T extends FirestoreObject,\n P extends string,\n PExtra,\n TSelected\n> = keyof SelectedParams<P, PExtra> extends never\n ? (\n params?: Record<string, string>,\n options?: ColHookOptions<T>\n ) => SelectedCollectionHandle<T, TSelected>\n : (\n params: SelectedParams<P, PExtra>,\n options?: ColHookOptions<T>\n ) => SelectedCollectionHandle<T, TSelected>;\n\n// Selected entries are matched first: they carry no `path`/`schema`, so they\n// never collide with the base Doc/ColEntry arms below. For a base entry, a path\n// template with no placeholders takes optional `params`; one with placeholders\n// requires an object with exactly the extracted keys.\ntype HookFor<E> = E extends SelectedDocEntry<\n infer T,\n infer P,\n infer PExtra,\n infer TSelected\n>\n ? SelectedDocHookFor<T, P, PExtra, TSelected>\n : E extends SelectedColEntry<infer T, infer P, infer PExtra, infer TSelected>\n ? SelectedColHookFor<T, P, PExtra, TSelected>\n : E extends DocEntry<infer T, infer P>\n ? keyof ParamsOf<P> extends never\n ? DocHookOptionalParams<T>\n : DocHookRequiredParams<T, P>\n : E extends ColEntry<infer T, infer P>\n ? keyof ParamsOf<P> extends never\n ? ColHookOptionalParams<T>\n : ColHookRequiredParams<T, P>\n : never;\n\nexport type FirestateApi<R extends FirestateRegistry> = {\n [K in keyof R & string as HookName<K>]: HookFor<R[K]>;\n};\n\n/**\n * Turn a Firestate registry into a map of typed React hooks. Each entry\n * `K` produces a hook named `use{Capitalize<K>}`.\n *\n * ```ts\n * export const { useTaskList, useTasks } = createFirestate({\n * taskList: doc<TaskList>('taskLists/{listId}'),\n * tasks: col<Task>('taskLists/{listId}/tasks'),\n * })\n * ```\n */\nexport function createFirestate<R extends FirestateRegistry>(\n registry: R\n): FirestateApi<R> {\n const api: Record<string, unknown> = {};\n\n // Built definitions are memoized by their *base entry object*, so the base\n // hook and every `.select` sibling derived from it resolve the SAME definition\n // — and therefore the SAME shared subscription (one onSnapshot listener, one\n // optimistic state). `.select` stores the base entry by reference, so `entry`\n // (a base) and `entry.base` (a selected entry) hit the same key. Without this,\n // each registry key built a fresh definition and the shared-subscription\n // registry (keyed by definition identity) forked one listener per hook.\n const docDefs = new Map<\n DocEntry<FirestoreObject, string>,\n DocumentDefinition<FirestoreObject>\n >();\n const colDefs = new Map<\n ColEntry<FirestoreObject, string>,\n CollectionDefinition<FirestoreObject>\n >();\n const docDefFor = (\n base: DocEntry<FirestoreObject, string>\n ): DocumentDefinition<FirestoreObject> => {\n let def = docDefs.get(base);\n if (!def) {\n def = buildDocumentDefinition(base);\n docDefs.set(base, def);\n }\n return def;\n };\n const colDefFor = (\n base: ColEntry<FirestoreObject, string>\n ): CollectionDefinition<FirestoreObject> => {\n let def = colDefs.get(base);\n if (!def) {\n def = buildCollectionDefinition(base);\n colDefs.set(base, def);\n }\n return def;\n };\n\n for (const key of Object.keys(registry)) {\n if (!isValidKey(key)) {\n throw new Error(\n `[firestate] registry key \"${key}\" must start with a letter and contain only letters, digits, _ or $`\n );\n }\n const entry = registry[key]!;\n const hookName = toHookName(key);\n\n if (entry.__kind === \"document\") {\n const definition = docDefFor(entry);\n api[hookName] = (\n params: Record<string, string> = {},\n options: DocHookOptions<FirestoreObject> = {}\n ) => useDocument({ ...options, definition, params });\n } else if (entry.__kind === \"collection\") {\n const definition = colDefFor(entry);\n api[hookName] = (\n params: Record<string, string> = {},\n options: ColHookOptions<FirestoreObject> = {}\n ) => useCollection({ ...options, definition, params });\n } else if (entry.__kind === \"document-selected\") {\n const definition = docDefFor(entry.base);\n const { selector, isEqual } = entry;\n api[hookName] = (\n params: Record<string, string> = {},\n options: DocHookOptions<FirestoreObject> = {}\n ) =>\n useDocument({\n ...options,\n definition,\n params,\n // Adapt the (state, params) selector to Level 1's inline `selector`\n // by closing over this call's params bag — so a parameterized slice\n // (e.g. `(s, p) => s.data[p.id]`) reads its id from the same bag the\n // path resolved from. A fresh closure each render is fine: useDocument\n // dedupes on the selected *value*, not the selector's identity.\n selector: (state) => selector(state, params),\n isEqual,\n });\n } else {\n // collection-selected\n const definition = colDefFor(entry.base);\n const { selector, isEqual } = entry;\n api[hookName] = (\n params: Record<string, string> = {},\n options: ColHookOptions<FirestoreObject> = {}\n ) =>\n useCollection({\n ...options,\n definition,\n params,\n selector: (state) => selector(state, params),\n isEqual,\n });\n }\n }\n\n return api as FirestateApi<R>;\n}\n\n/**\n * Build the underlying {@link DocumentDefinition} for a registry doc entry.\n * Exported for unit testing — registry consumers should call\n * {@link createFirestate} instead.\n *\n * @internal\n */\nexport function buildDocumentDefinition<T extends FirestoreObject>(\n entry: DocEntry<T>\n): DocumentDefinition<T> {\n const { path } = entry;\n const common = {\n schema: entry.schema,\n autosave: entry.autosave,\n minLoadTime: entry.minLoadTime,\n readOnly: entry.readOnly,\n retryOnError: entry.retryOnError,\n retryInterval: entry.retryInterval,\n };\n\n if (typeof path === \"function\") {\n // The function returns the FULL document path; split it per-call. It has\n // no `{param}` placeholders left to interpolate, but splitDocPath still\n // throws loud on a missing '/' or an empty collection/id segment — the\n // boundary check, just deferred to resolution time.\n return defineDocument<T>({\n ...common,\n collection: (params) => splitDocPath(path(params)).collectionPath,\n id: (params) => splitDocPath(path(params)).idTemplate,\n } as DocumentDefinition<T>);\n }\n\n // Static template. Both halves are functions so any `{param}` placeholder in\n // the collection portion (e.g. `projects/{projectId}/revisions`) is resolved\n // per-call against the params passed to the hook.\n const { collectionPath, idTemplate } = splitDocPath(path);\n return defineDocument<T>({\n ...common,\n collection: (params) => interpolate(collectionPath, params),\n id: (params) => interpolate(idTemplate, params),\n } as DocumentDefinition<T>);\n}\n\n/**\n * Build the underlying {@link CollectionDefinition} for a registry col entry.\n *\n * @internal\n */\nexport function buildCollectionDefinition<T extends FirestoreObject>(\n entry: ColEntry<T>\n): CollectionDefinition<T> {\n const { path } = entry;\n return defineCollection<T>({\n schema: entry.schema,\n // Function paths pass straight through to defineCollection; static\n // templates are interpolated per-call.\n path:\n typeof path === \"function\"\n ? path\n : (params) => interpolate(path, params),\n autosave: entry.autosave,\n minLoadTime: entry.minLoadTime,\n readOnly: entry.readOnly,\n lazy: entry.lazy,\n queryConstraints: entry.queryConstraints,\n retryOnError: entry.retryOnError,\n retryInterval: entry.retryInterval,\n } as CollectionDefinition<T>);\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers (also exported for testing)\n// ---------------------------------------------------------------------------\n\nconst VALID_KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\nfunction isValidKey(key: string): boolean {\n return VALID_KEY.test(key);\n}\n\nfunction toHookName(key: string): string {\n return `use${key[0]!.toUpperCase()}${key.slice(1)}`;\n}\n\n/**\n * Replace `{name}` placeholders in a path template with values from `params`.\n * Throws if a placeholder is missing from `params` — failing loud at the\n * boundary is better than silently building a `taskLists/undefined/tasks`\n * URL and getting a useless Firestore error later.\n *\n * @internal\n */\nexport function interpolatePath(\n template: string,\n params: Record<string, string>\n): string {\n return interpolate(template, params);\n}\n\n// Matches a single `{name}` placeholder where the name is a valid JS-ish\n// identifier (letter or underscore start, then letters/digits/underscores).\n// Used both to interpolate and to validate templates up front.\nconst PLACEHOLDER = /\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g;\n\n/**\n * Validate that a path template uses only well-formed `{name}` placeholders\n * — no unclosed braces, no hyphens/dots inside placeholders, no `{1}` style\n * digit-leading names. Throws at definition time so a typo in the template\n * fails loud at `doc()` / `col()`, not three layers deep when a component\n * mounts.\n */\nfunction validateTemplate(template: string): void {\n // Strip the well-formed placeholders, then look for any stray `{` or `}` —\n // those signal a malformed (unclosed or weirdly-spelled) placeholder.\n const stripped = template.replace(PLACEHOLDER, \"\");\n if (stripped.includes(\"{\") || stripped.includes(\"}\")) {\n throw new Error(\n `[firestate] path \"${template}\" contains a malformed placeholder. ` +\n `Placeholders must look like \"{name}\" where name starts with a letter or underscore.`\n );\n }\n}\n\nfunction interpolate(template: string, params: Record<string, string>): string {\n return template.replace(PLACEHOLDER, (_, key) => {\n const v = params[key];\n if (v === undefined) {\n throw new Error(\n `[firestate] missing param \"${key}\" for path \"${template}\"`\n );\n }\n if (v === \"\") {\n // An empty value would silently produce `taskLists//tasks`, which\n // Firestore later rejects with an opaque \"Document path must not be\n // empty\" — keep the friendly error at the boundary.\n throw new Error(\n `[firestate] param \"${key}\" for path \"${template}\" must not be an empty string`\n );\n }\n return v;\n });\n}\n\n/**\n * Split a document path template into a collection path and an id template.\n * `'taskLists/{listId}'` → `{ collectionPath: 'taskLists', idTemplate: '{listId}' }`.\n *\n * @internal\n */\nexport function splitDocPath(path: string): {\n collectionPath: string;\n idTemplate: string;\n} {\n const lastSlash = path.lastIndexOf(\"/\");\n if (lastSlash === -1) {\n throw new Error(\n `[firestate] document path \"${path}\" must contain at least one '/' separating the collection from the document id`\n );\n }\n const collectionPath = path.slice(0, lastSlash);\n const idTemplate = path.slice(lastSlash + 1);\n if (collectionPath === \"\" || idTemplate === \"\") {\n throw new Error(\n `[firestate] document path \"${path}\" must have non-empty collection and id segments`\n );\n }\n return { collectionPath, idTemplate };\n}\n","/**\n * Shallow structural equality.\n *\n * Returns `true` when `a` and `b` are identical by `Object.is`, or are two\n * arrays / two plain objects whose entries are pairwise `Object.is`-equal one\n * level deep. Anything else (different shapes, nested objects that aren't\n * reference-equal) is `false`.\n *\n * Intended as the `isEqual` for a hook `selector` that builds a fresh array or\n * object every render — e.g. `data => Object.values(data).map(d => d.id)` or\n * `data => ({ name: data?.name, done: data?.done })`. The default selector\n * comparison is a *deep* value compare, which is correct but does more work\n * than needed for a flat projection; `shallow` re-renders on a genuine change\n * to any entry while collapsing the fresh-reference-same-entries case.\n *\n * Not recursive on purpose: if a selected entry is itself an object you mutate\n * in place rather than replace, prefer the default deep comparison or a\n * bespoke `isEqual`.\n */\nexport const shallow = <T>(a: T, b: T): boolean => {\n if (Object.is(a, b)) return true;\n\n if (\n typeof a !== \"object\" ||\n a === null ||\n typeof b !== \"object\" ||\n b === null\n ) {\n return false;\n }\n\n const aIsArray = Array.isArray(a);\n if (aIsArray !== Array.isArray(b)) return false;\n\n if (aIsArray) {\n const arrA = a as unknown[];\n const arrB = b as unknown[];\n if (arrA.length !== arrB.length) return false;\n for (let i = 0; i < arrA.length; i++) {\n if (!Object.is(arrA[i], arrB[i])) return false;\n }\n return true;\n }\n\n const objA = a as Record<string, unknown>;\n const objB = b as Record<string, unknown>;\n const keysA = Object.keys(objA);\n if (keysA.length !== Object.keys(objB).length) return false;\n for (const key of keysA) {\n if (\n !Object.prototype.hasOwnProperty.call(objB, key) ||\n !Object.is(objA[key], objB[key])\n ) {\n return false;\n }\n }\n return true;\n};\n","import type { Subscriber, Unsubscribe, UndoAction, UndoManager, UndoManagerState } from '../types'\n\n/**\n * Configuration for creating an undo manager\n */\nexport interface UndoManagerConfig {\n /** Maximum number of undo actions to keep, default 20 */\n maxLength?: number\n /** Callback when navigation is requested (for path-aware undo) */\n onNavigate?: (path: string) => void\n}\n\n/**\n * Create an undo manager instance.\n * This is a standalone, framework-agnostic implementation.\n *\n * @example\n * ```ts\n * const undoManager = createUndoManager({ maxLength: 10 })\n *\n * undoManager.push({\n * undo: () => restoreOldValue(),\n * redo: () => applyNewValue(),\n * description: 'Update project name',\n * })\n *\n * await undoManager.undo() // Calls restoreOldValue()\n * await undoManager.redo() // Calls applyNewValue()\n * ```\n */\nexport const createUndoManager = (\n config: UndoManagerConfig = {}\n): UndoManager & {\n subscribe: (fn: Subscriber<UndoManagerState>) => Unsubscribe\n getState: () => UndoManagerState\n} => {\n const { maxLength = 20, onNavigate } = config\n\n let undoStack: UndoAction[] = []\n let redoStack: UndoAction[] = []\n const subscribers = new Set<Subscriber<UndoManagerState>>()\n // Cached snapshot — returns the same reference until notify() invalidates\n // it. Required so React's useSyncExternalStore consumers (useUndoManager)\n // see a stable snapshot across the multiple getSnapshot() calls React\n // makes per commit; otherwise the inequality on Object.is triggers an\n // infinite re-render and the \"getSnapshot should be cached\" warning.\n let cachedState: UndoManagerState | null = null\n\n const getState = (): UndoManagerState => {\n if (cachedState === null) {\n cachedState = {\n undoStack,\n redoStack,\n canUndo: undoStack.length > 0,\n canRedo: redoStack.length > 0,\n }\n }\n return cachedState\n }\n\n const notify = () => {\n cachedState = null\n const state = getState()\n subscribers.forEach((fn) => fn(state))\n }\n\n const push = (action: UndoAction) => {\n // Check if we should merge with previous action (same groupId)\n if (action.groupId && undoStack.length > 0) {\n const last = undoStack[undoStack.length - 1]\n if (last?.groupId === action.groupId) {\n // Pop and merge. Undo walks the group newest→oldest so each\n // step reverses its specific change in reverse order; redo\n // walks oldest→newest to re-apply in original order. The\n // previous (older) order produced incorrect cumulative state\n // whenever grouped actions touched the same field.\n undoStack.pop()\n undoStack.push({\n undo: async () => {\n await action.undo()\n await last.undo()\n },\n redo: async () => {\n await last.redo()\n await action.redo()\n },\n groupId: action.groupId,\n path: action.path ?? last.path,\n description: action.description ?? last.description,\n })\n // Clear redo stack on any new action\n redoStack = []\n notify()\n return\n }\n }\n\n undoStack.push(action)\n\n // Enforce max length\n if (undoStack.length > maxLength) {\n undoStack.shift()\n }\n\n // Clear redo stack on any new action\n redoStack = []\n notify()\n }\n\n const undo = async () => {\n const action = undoStack.pop()\n if (!action) return\n\n // Navigate if path is set\n if (action.path && onNavigate) {\n onNavigate(action.path)\n }\n\n try {\n await action.undo()\n redoStack.push(action)\n } catch (error) {\n // Put it back on undo stack if it failed\n undoStack.push(action)\n console.error('Undo failed:', error)\n throw error\n }\n\n notify()\n }\n\n const redo = async () => {\n const action = redoStack.pop()\n if (!action) return\n\n // Navigate if path is set\n if (action.path && onNavigate) {\n onNavigate(action.path)\n }\n\n try {\n await action.redo()\n undoStack.push(action)\n\n // Enforce max length\n if (undoStack.length > maxLength) {\n undoStack.shift()\n }\n } catch (error) {\n // Put it back on redo stack if it failed\n redoStack.push(action)\n console.error('Redo failed:', error)\n throw error\n }\n\n notify()\n }\n\n const clear = () => {\n undoStack = []\n redoStack = []\n notify()\n }\n\n const subscribe = (fn: Subscriber<UndoManagerState>): Unsubscribe => {\n subscribers.add(fn)\n return () => subscribers.delete(fn)\n }\n\n return {\n get undoStack() {\n return undoStack\n },\n get redoStack() {\n return redoStack\n },\n get canUndo() {\n return undoStack.length > 0\n },\n get canRedo() {\n return redoStack.length > 0\n },\n push,\n undo,\n redo,\n clear,\n subscribe,\n getState,\n }\n}\n\n/**\n * Type for the undo manager with subscription capability\n */\nexport type UndoManagerWithSubscribe = ReturnType<typeof createUndoManager>\n","import type { Firestore } from 'firebase/firestore'\nimport type { ErrorContext, FirestateConfig, Subscriber, Unsubscribe } from '../types'\nimport { createUndoManager, type UndoManagerWithSubscribe } from '../utils/undo'\n\n/**\n * Firestate store that holds configuration and shared state\n */\nexport interface FirestateStore {\n /** Firestore instance */\n readonly firestore: Firestore\n /** Undo manager instance */\n readonly undoManager: UndoManagerWithSubscribe\n /** Default autosave interval (ms) */\n readonly autosave: number\n /** Default minimum load time (ms) */\n readonly minLoadTime: number\n /** Report an error */\n reportError: (error: Error, context: ErrorContext) => void\n /**\n * Replace the error handler at runtime. Used by FirestateProvider to keep\n * the store identity stable when consumers pass an inline `onError`\n * callback that changes reference on every render.\n */\n setOnError: (handler?: (error: Error, context: ErrorContext) => void) => void\n /**\n * Replace the navigation handler at runtime. Used by FirestateProvider to\n * keep the store identity stable when consumers pass an inline `onNavigate`\n * callback that changes reference on every render.\n */\n setOnNavigate: (handler?: (path: string) => void) => void\n /** Subscribe to sync state changes */\n subscribeToSyncState: (fn: Subscriber<boolean>) => Unsubscribe\n /** Report a document/collection sync state change */\n reportSyncState: (key: string, isSynced: boolean) => void\n /**\n * Remove a sync-state key. Subscriptions call this on stop() so an\n * unmounted hook does not leave the global isSynced stuck at false.\n */\n unregisterSyncState: (key: string) => void\n /** Get whether all tracked resources are synced */\n readonly isSynced: boolean\n}\n\n/**\n * Create a Firestate store.\n * This is the central configuration point for your Firestore state management.\n *\n * @example\n * ```ts\n * import { createStore } from 'firestate'\n * import { db } from './firebase'\n *\n * export const store = createStore({\n * firestore: db,\n * autosave: 1000,\n * maxUndoLength: 20,\n * onError: (error, context) => {\n * console.error(`Error in ${context.type} ${context.path}:`, error)\n * },\n * })\n * ```\n */\nexport const createStore = (config: FirestateConfig): FirestateStore => {\n const {\n firestore,\n autosave = 1000,\n minLoadTime = 0,\n maxUndoLength = 20,\n } = config\n\n // Mutable so the provider can update them without re-creating the store.\n let onError = config.onError\n let onNavigate = config.onNavigate\n\n const undoManager = createUndoManager({\n maxLength: maxUndoLength,\n // Stable wrapper — delegates to the mutable onNavigate ref so the\n // undo manager doesn't need to be recreated when the callback changes.\n onNavigate: (path) => onNavigate?.(path),\n })\n\n // Track sync state of all documents/collections\n const syncStates = new Map<string, boolean>()\n const syncSubscribers = new Set<Subscriber<boolean>>()\n\n const computeIsSynced = (): boolean => {\n for (const synced of syncStates.values()) {\n if (!synced) return false\n }\n return true\n }\n\n const notifySyncSubscribers = () => {\n const isSynced = computeIsSynced()\n syncSubscribers.forEach((fn) => fn(isSynced))\n }\n\n return {\n firestore,\n undoManager,\n autosave,\n minLoadTime,\n\n reportError: (error, context) => {\n if (onError) {\n onError(error, context)\n } else {\n console.error(\n `Firestate error in ${context.type} ${context.path} during ${context.operation}:`,\n error\n )\n }\n },\n\n setOnError: (handler) => {\n onError = handler\n },\n\n setOnNavigate: (handler) => {\n onNavigate = handler\n },\n\n subscribeToSyncState: (fn) => {\n syncSubscribers.add(fn)\n return () => syncSubscribers.delete(fn)\n },\n\n reportSyncState: (key, isSynced) => {\n const prev = syncStates.get(key)\n if (prev !== isSynced) {\n syncStates.set(key, isSynced)\n notifySyncSubscribers()\n }\n },\n\n unregisterSyncState: (key) => {\n const prev = syncStates.get(key)\n if (prev === undefined) return\n syncStates.delete(key)\n // Removing a `false` entry can flip global isSynced to true.\n if (prev === false) {\n notifySyncSubscribers()\n }\n },\n\n get isSynced() {\n return computeIsSynced()\n },\n }\n}\n\n/**\n * Type alias for the store type\n */\nexport type Store = ReturnType<typeof createStore>\n","import React, {\n useCallback,\n useEffect,\n useMemo,\n useSyncExternalStore,\n} from \"react\";\nimport type { Firestore } from \"firebase/firestore\";\nimport { createStore, type FirestateStore } from \"../core/store\";\nimport { FirestateContext } from \"./hooks\";\nimport type { ErrorContext } from \"../types\";\n\n/**\n * Props for FirestateProvider\n */\nexport interface FirestateProviderProps {\n /** Firestore instance */\n firestore: Firestore;\n /** Default autosave interval (ms), default 1000 */\n autosave?: number;\n /** Default minimum load time (ms), default 0 */\n minLoadTime?: number;\n /** Maximum undo stack length, default 20 */\n maxUndoLength?: number;\n /**\n * Called before undo/redo when the action carries a `path`. Wire your\n * router's `navigate` here to return users to where a change occurred\n * before reverting it.\n *\n * @example\n * ```tsx\n * import { useNavigate } from 'react-router-dom'\n *\n * function App() {\n * const navigate = useNavigate()\n * return (\n * <FirestateProvider onNavigate={(path) => navigate(path)}>\n * {children}\n * </FirestateProvider>\n * )\n * }\n * ```\n */\n onNavigate?: (path: string) => void;\n /** Custom error handler */\n onError?: (error: Error, context: ErrorContext) => void;\n /** React children */\n children: React.ReactNode;\n}\n\n/**\n * Provider component that sets up Firestate for your application.\n *\n * @example\n * ```tsx\n * import { FirestateProvider } from 'firestate'\n * import { db } from './firebase'\n *\n * function App() {\n * return (\n * <FirestateProvider\n * firestore={db}\n * autosave={1000}\n * maxUndoLength={20}\n * onError={(error, ctx) => console.error(ctx.path, error)}\n * >\n * <YourApp />\n * </FirestateProvider>\n * )\n * }\n * ```\n */\nexport const FirestateProvider: React.FC<FirestateProviderProps> = ({\n firestore,\n autosave = 1000,\n minLoadTime = 0,\n maxUndoLength = 20,\n onError,\n onNavigate,\n children,\n}) => {\n // onError and onNavigate are intentionally excluded from the deps so that\n // inline callbacks (new reference per render) do not re-create the store and\n // drop every existing subscription. The store exposes setOnError /\n // setOnNavigate so the latest handlers can be applied without store\n // re-creation.\n const store = useMemo(\n () =>\n createStore({\n firestore,\n autosave,\n minLoadTime,\n maxUndoLength,\n onError,\n onNavigate,\n }),\n [firestore, autosave, minLoadTime, maxUndoLength]\n );\n\n useEffect(() => {\n store.setOnError(onError);\n }, [store, onError]);\n\n useEffect(() => {\n store.setOnNavigate(onNavigate);\n }, [store, onNavigate]);\n\n return (\n <FirestateContext.Provider value={store}>\n {children}\n </FirestateContext.Provider>\n );\n};\n\n/**\n * Props for using an existing store\n */\nexport interface FirestateStoreProviderProps {\n /** Pre-created store instance */\n store: FirestateStore;\n /** React children */\n children: React.ReactNode;\n}\n\n/**\n * Provider that uses an existing store instance.\n * Useful when you need to create the store outside of React.\n *\n * @example\n * ```tsx\n * const store = createStore({ firestore: db })\n *\n * function App() {\n * return (\n * <FirestateStoreProvider store={store}>\n * <YourApp />\n * </FirestateStoreProvider>\n * )\n * }\n * ```\n */\nexport const FirestateStoreProvider: React.FC<FirestateStoreProviderProps> = ({\n store,\n children,\n}) => (\n <FirestateContext.Provider value={store}>\n {children}\n </FirestateContext.Provider>\n);\n\n/**\n * Hook to use navigation blocker when there are unsaved changes.\n * Works with react-router or similar routers.\n *\n * @example\n * ```tsx\n * function ProjectPage() {\n * const shouldBlock = useUnsavedChangesBlocker()\n *\n * // Use with react-router's useBlocker\n * const blocker = useBlocker(\n * ({ currentLocation, nextLocation }) =>\n * currentLocation.pathname !== nextLocation.pathname && shouldBlock\n * )\n *\n * return (\n * <>\n * <ProjectEditor />\n * {blocker.state === 'blocked' && (\n * <Dialog>Your changes may not be saved!</Dialog>\n * )}\n * </>\n * )\n * }\n * ```\n */\nexport const useUnsavedChangesBlocker = (): boolean => {\n const store = React.useContext(FirestateContext);\n\n const subscribe = useCallback(\n (onChange: () => void) =>\n store ? store.subscribeToSyncState(() => onChange()) : () => {},\n [store]\n );\n\n const getSnapshot = useCallback(\n () => (store ? !store.isSynced : false),\n [store]\n );\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n};\n"],"mappings":"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"}
|