import * as react0 from "react"; import React from "react"; import { CollectionReference, DocumentReference, Firestore, QueryConstraint, WithFieldValue } from "firebase/firestore"; import { ZodType, z } from "zod"; //#region src/types.d.ts /** * Deep partial type that works with Records and nested objects */ type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial } : T; /** * A generic object that can be stored in Firestore. * * Uses an `any` index signature (matching Firestore's own `DocumentData`) so * that plain TypeScript interfaces — which lack an implicit index signature — * can satisfy the constraint. Internal call sites cast through more specific * types where needed. */ type FirestoreObject = Record; /** * Options for update operations */ interface UpdateOptions { /** If false, prevents this update from being added to undo stack */ undoable?: boolean; /** Group multiple updates into a single undo action */ undoGroupId?: string; } /** * The full observable state of a document subscription — what a hook `selector` * receives. Carries every status flag (including `isSynced`, which the default * data handle deliberately omits) so a selector can react to exactly the slice * it reads. */ interface DocumentState { /** Current merged state (local changes applied to sync state) */ data: T | undefined; /** Whether the initial snapshot has not arrived yet */ isLoading: boolean; /** * Whether the initial snapshot has arrived and data is ready to render — the * completion of {@link DocumentState.isLoading} (`!isLoading` for a live * subscription; `false` while the hook is disabled). */ isLoaded: boolean; /** Whether all local changes have synced to Firestore (no pending writes) */ isSynced: boolean; /** Error from listener, if any */ error: Error | undefined; } /** * The full observable state of a collection subscription — what a hook * `selector` receives. See {@link DocumentState}. */ interface CollectionState { /** Current merged state keyed by document ID */ data: Record; /** Whether the initial snapshot has not arrived yet */ isLoading: boolean; /** * Whether the collection is active and its initial snapshot has arrived — * `isActive && !isLoading`. `false` for a lazy collection before `load()`, * and while the hook is disabled. */ isLoaded: boolean; /** Whether all local changes have synced to Firestore (no pending writes) */ isSynced: boolean; /** Whether the collection has been activated (for lazy loading) */ isActive: boolean; /** Error from listener, if any */ error: Error | undefined; } /** * Sync status of a single resource, returned by the per-entry * `use{Name}SyncStatus` hook. Opt-in: only components that render save/dirty * state subscribe to it, so the common data path does not re-render when a write * settles. Shares the resource's one `onSnapshot` listener. */ interface SyncStatus { /** Whether all local changes have synced to Firestore (no pending writes) */ isSynced: boolean; /** Whether there are pending local changes still being saved (`!isSynced`) */ isSaving: boolean; } /** * Loading status of a single resource, returned by the per-entry * `use{Name}LoadingStatus` hook. A spinner-only channel: it re-renders on load * transitions but never on data changes. Shares the resource's listener. */ interface LoadingStatus { /** Whether the initial snapshot has not arrived yet */ isLoading: boolean; /** Whether the initial snapshot has arrived (the completion of `isLoading`) */ isLoaded: boolean; } /** * Document handle returned by the `useDocument` hook. * * **Sync-agnostic by default.** The handle carries `data`, `isLoaded`, `error`, * the writers, and `ref` — but NOT `isSynced`. A document hook therefore does * not re-render when a write settles (the `isSynced` flip on every autosave), * so "just render the record" is the cheap, default path. Components that * actually render save/dirty state opt into the per-entry `use{Name}SyncStatus` * hook ({@link SyncStatus}), which shares the same listener. The raw * `isLoading`/`isSynced` flags remain on {@link DocumentState} for selectors. */ interface DocumentHandle { /** Current document data */ data: T | undefined; /** Update the document with a partial diff */ update: (diff: WithFieldValue>, options?: UpdateOptions) => void; /** Set the document data (creates or overwrites) */ set: (data: T, options?: UpdateOptions) => void; /** Delete the document */ delete: (options?: UpdateOptions) => void; /** * Whether the initial snapshot has arrived and data is ready to render — the * completion of `isLoading`. `false` while loading or when the hook is * disabled. (Use `use{Name}LoadingStatus` for an `isLoading`/`isLoaded` * channel that does not re-render on data changes.) */ isLoaded: boolean; /** Force sync pending changes immediately */ sync: () => Promise; /** Error from listener, if any */ error: Error | undefined; /** * Firestore document reference. Undefined when the hook was called with * `enabled: false` (no subscription was created). */ ref: DocumentReference | undefined; } /** * Collection handle returned by the `useCollection` hook. * * Sync-agnostic by default, exactly like {@link DocumentHandle}: it carries * `data`, `isLoaded`, `isActive`, `error`, the writers, `load`, and `ref` — but * NOT `isSynced`. Opt into `use{Name}SyncStatus` for save state. `isActive` * stays (lazy collections gate a "Load" button on it); `isLoaded` is * `isActive && !isLoading`. */ interface CollectionHandle { /** Current collection data keyed by document ID */ data: Record; /** Update one or more documents with partial diffs */ update: (diff: WithFieldValue>>, options?: UpdateOptions) => void; /** * Add a new document to the collection. Either pass an explicit `id`, or * omit it to have Firestore generate an auto-id (returned synchronously). * * Returns `undefined` if the mutation was dropped (read-only handle, or * called before the first snapshot has arrived). Callers should narrow * before using the id to navigate or persist references. */ add: { (id: string, data: Omit, options?: UpdateOptions): string | undefined; (data: Omit, options?: UpdateOptions): string | undefined; }; /** Remove a document from the collection */ remove: (id: string, options?: UpdateOptions) => void; /** * Whether the collection is active and its initial snapshot has arrived * (ready to render) — `isActive && !isLoading`. `false` for a lazy * collection before `load()`, while loading, or when the hook is disabled. */ isLoaded: boolean; /** Whether subscription is active (for lazy collections) */ isActive: boolean; /** Activate a lazy subscription */ load: () => void; /** Force sync pending changes immediately */ sync: () => Promise; /** Error from listener, if any */ error: Error | undefined; /** * Firestore collection reference. Undefined when the hook was called with * `enabled: false` (no subscription was created). */ ref: CollectionReference | undefined; } /** Reactive status fields a selector drops unless it folds them into its slice. */ type DocumentStatusKeys = 'isLoaded' | 'error'; type CollectionStatusKeys = DocumentStatusKeys | 'isActive'; /** * A {@link DocumentHandle} reduced to a hook-level `selector`'s output. The * selector receives the full observable state ({@link DocumentState}) and * returns the slice this component reacts to; the handle re-renders *only* when * that slice changes. * * A selected handle deliberately exposes **only** `data` (the slice) plus the * writer surface (`update`/`set`/`delete`/`sync`) and `ref` — never the status * fields (`isLoaded`/`error`). Status is not a freebie here: if a component * needs it, it must select it (`s => ({ slice: s.data?.x, loading: * s.isLoading })`), so what you re-render on is exactly what you select. The * writers stay typed against the full document `TData`, because a selector * changes what you *read*, never what you *write*. * * Note: `update(diff)` takes a *partial* of the full document and merges it, so * writing a selected field is `update({ field: next })`. `set(data)` still * *replaces the entire document*, not the slice — never pass the selected value * to `set`, or you will overwrite every other field. Prefer `update` from a * narrowed handle; reach for `set` only when you hold the full document. */ interface SelectedDocumentHandle extends Omit, 'data' | DocumentStatusKeys> { /** The slice produced by the hook's `selector`. */ data: TSelected; } /** * A {@link CollectionHandle} reduced to a hook-level `selector`'s output. As * with {@link SelectedDocumentHandle}, the selector receives the full * observable state ({@link CollectionState}) and the handle exposes only the * slice plus the writer surface (`update`/`add`/`remove`/`load`/`sync`) and * `ref` — status fields (`isLoaded`/`isActive`/`error`) are dropped unless * folded into the slice. Writers stay typed against the full collection of * `TData`. */ interface SelectedCollectionHandle extends Omit, 'data' | CollectionStatusKeys> { /** The slice produced by the hook's `selector`. */ data: TSelected; } /** * An undo/redo action */ interface UndoAction { /** Function to undo the change */ undo: () => Promise | void; /** Function to redo the change */ redo: () => Promise | void; /** Optional group ID for batching multiple actions */ groupId?: string; /** Optional path/location context for navigation-aware undo */ path?: string; /** Human-readable description of the action */ description?: string; } /** * Undo manager state */ interface UndoManagerState { /** Stack of actions that can be undone */ undoStack: readonly UndoAction[]; /** Stack of actions that can be redone */ redoStack: readonly UndoAction[]; /** Whether undo is available */ canUndo: boolean; /** Whether redo is available */ canRedo: boolean; } /** * Undo manager handle */ interface UndoManager extends UndoManagerState { /** Perform undo */ undo: () => Promise; /** Perform redo */ redo: () => Promise; /** Push a new action onto the undo stack */ push: (action: UndoAction) => void; /** Clear all undo/redo history */ clear: () => void; } /** * Configuration for a document definition. * * `TData` is the document's TypeScript shape. Provide it explicitly, or let * it be inferred from `schema` when using `defineDocument`. */ interface DocumentDefinition { /** * Optional Zod schema. When provided, firestate runs `schema.parse(...)` * on full-payload writes (`set`, `add`) as a **validation guard** — bad * data throws at the call site, not after a Firestore round trip. The * parsed result is discarded; firestate stores the caller's original * object verbatim. That means schema transforms (`.transform`, `.coerce`, * default values) are NOT applied to stored data — do transforms before * calling `set`/`add`. Partial `update(diff)` calls are NOT validated * because diffs commonly contain Firestore sentinels (`serverTimestamp()`, * `arrayUnion`, etc.) that don't satisfy a strict schema. */ schema?: ZodType; /** * Collection path. Either a static string (may include multiple `/`- * separated segments) or a function that derives the path from route/ * params. Use the function form when the collection lives under a dynamic * parent, e.g. `projects/{projectId}/revisions`. */ collection: string | ((params: Record) => string); /** Document ID or function to derive it */ id: string | ((params: Record) => string); /** Debounce interval for autosave (ms), default 1000 */ autosave?: number; /** Minimum loading indicator time (ms), default 0 */ minLoadTime?: number; /** Whether this document is read-only */ readOnly?: boolean; /** Retry on listener error */ retryOnError?: boolean; /** Retry interval (ms), default 5000 */ retryInterval?: number; } /** * Configuration for a collection definition. * * `TData` is the document shape for entries in this collection. */ interface CollectionDefinition { /** * Optional Zod schema for documents in the collection. When provided, * firestate runs `schema.parse(...)` on full-payload writes (`add`) as * a validation guard and stores the caller's original object verbatim. * Schema transforms are not applied to stored data — see * {@link DocumentDefinition.schema} for the full contract. */ schema?: ZodType; /** Collection path (can include path segments) */ path: string | ((params: Record) => string); /** Debounce interval for autosave (ms), default 1000 */ autosave?: number; /** Minimum loading indicator time (ms), default 0 */ minLoadTime?: number; /** Whether this collection is read-only */ readOnly?: boolean; /** Whether to lazy load (only subscribe when load() is called) */ lazy?: boolean; /** Query constraints */ queryConstraints?: QueryConstraint[]; /** Retry the snapshot listener on transient errors */ retryOnError?: boolean; /** Retry interval (ms), default 5000 */ retryInterval?: number; } /** * Configuration for the Firestate store */ interface FirestateConfig { /** Firestore instance */ firestore: Firestore; /** Default autosave interval (ms), default 1000 */ autosave?: number; /** Default minimum load time (ms), default 0 */ minLoadTime?: number; /** Maximum undo stack length, default 20 */ maxUndoLength?: number; /** * Callback invoked before undo/redo when the action carries a `path`. * Wire your router's `navigate` here so undo/redo returns the user to * where a change occurred before reverting it. */ onNavigate?: (path: string) => void; /** * Called when a handle write (`update`/`add`/`remove`) pushes an undo * action, to stamp the current router path onto that action. The stamped * `path` is what {@link FirestateConfig.onNavigate} later receives, so * handle-driven undo can return the user to where the change happened * before reverting it. Return `undefined` to leave the action pathless. * Firestate can't know the router path itself — wire this to your router. */ getUndoPath?: () => string | undefined; /** Called after an undo action has been successfully applied. */ onUndo?: (action: UndoAction) => void; /** Called after a redo action has been successfully applied. */ onRedo?: (action: UndoAction) => void; /** Custom error handler */ onError?: (error: Error, context: ErrorContext) => void; } /** * Context for error handling */ interface ErrorContext { type: 'document' | 'collection' | 'undo'; path: string; operation: 'read' | 'write' | 'undo' | 'redo'; } /** * Subscriber callback type */ type Subscriber = (state: T) => void; /** * Unsubscribe function */ type Unsubscribe = () => void; //#endregion //#region src/registry/schema.d.ts /** * Define a typed document. `TData` is the document's TypeScript shape. * * **Most apps should reach for {@link createFirestate} + {@link doc} instead** * — that builds a registry of every Firestore thing in one object and * generates typed hooks for you. `defineDocument` is the lower-level * escape hatch: use it when you need fully custom `collection` / `id` * derivation, when you're calling firestate outside React, or when a * registry doesn't fit your control flow. * * Two ways to use: * * 1. Plain TypeScript type (no schema, no runtime validation): * ```ts * interface Project { name: string; createdAt: number } * * const projectDoc = defineDocument({ * collection: 'projects', * id: (params) => params.projectId, * }) * ``` * * 2. With a Zod schema — `TData` is inferred from `z.infer`. Firestate * runs `schema.parse(...)` on full-payload writes (`set`/`add`) so bad * data throws at the call site. Partial `update(diff)` calls are not * validated (diffs frequently contain Firestore sentinels). * ```ts * import { z } from 'zod' * * const ProjectSchema = z.object({ name: z.string(), createdAt: z.number() }) * * const projectDoc = defineDocument({ * schema: ProjectSchema, * collection: 'projects', * id: (params) => params.projectId, * }) * ``` */ declare function defineDocument>(definition: Omit>, "schema"> & { schema: S; }): DocumentDefinition>; declare function defineDocument(definition: DocumentDefinition): DocumentDefinition; /** * Define a typed collection. `TData` is the shape of each document in the * collection. See {@link defineDocument} for the schema/plain-type tradeoff. * * **Most apps should reach for {@link createFirestate} + {@link col} instead.** * `defineCollection` is the escape hatch for fully custom path derivation * or non-React usage. * * @example * ```ts * interface Space { name: string; area: number } * * const spacesCollection = defineCollection({ * path: (params) => `projects/${params.projectId}/spaces`, * lazy: true, * }) * ``` */ declare function defineCollection>(definition: Omit>, "schema"> & { schema: S; }): CollectionDefinition>; declare function defineCollection(definition: CollectionDefinition): CollectionDefinition; /** * Infer the document data type from a {@link DocumentDefinition}. */ type InferDocumentData> = T extends DocumentDefinition ? D : never; /** * Infer the document data type (with `id` field) from a {@link DocumentDefinition}. */ type InferDocument> = InferDocumentData & { id: string; }; /** * Infer the document data type from a {@link CollectionDefinition}. */ type InferCollectionData> = T extends CollectionDefinition ? D : never; /** * Infer the document data type (with `id` field) from a {@link CollectionDefinition}. */ type InferCollectionDocument> = InferCollectionData & { id: string; }; //#endregion //#region src/utils/undo.d.ts /** * Configuration for creating an undo manager */ interface UndoManagerConfig { /** Maximum number of undo actions to keep, default 20 */ maxLength?: number; /** Callback when navigation is requested (for path-aware undo) */ onNavigate?: (path: string) => void; /** Callback after an undo action has been successfully applied */ onUndo?: (action: UndoAction) => void; /** Callback after a redo action has been successfully applied */ onRedo?: (action: UndoAction) => void; /** Callback when an undo or redo action fails */ onError?: (error: Error, action: UndoAction, operation: 'undo' | 'redo') => void; } /** * Create an undo manager instance. * This is a standalone, framework-agnostic implementation. * * @example * ```ts * const undoManager = createUndoManager({ maxLength: 10 }) * * undoManager.push({ * undo: () => restoreOldValue(), * redo: () => applyNewValue(), * description: 'Update project name', * }) * * await undoManager.undo() // Calls restoreOldValue() * await undoManager.redo() // Calls applyNewValue() * ``` */ declare const createUndoManager: (config?: UndoManagerConfig) => UndoManager & { subscribe: (fn: Subscriber) => Unsubscribe; getState: () => UndoManagerState; }; /** * Type for the undo manager with subscription capability */ type UndoManagerWithSubscribe = ReturnType; //#endregion //#region src/core/store.d.ts /** * Firestate store that holds configuration and shared state */ interface FirestateStore { /** Firestore instance */ readonly firestore: Firestore; /** Undo manager instance */ readonly undoManager: UndoManagerWithSubscribe; /** Default autosave interval (ms) */ readonly autosave: number; /** Default minimum load time (ms) */ readonly minLoadTime: number; /** Report an error */ reportError: (error: Error, context: ErrorContext) => void; /** * Replace the error handler at runtime. Used by FirestateProvider to keep * the store identity stable when consumers pass an inline `onError` * callback that changes reference on every render. */ setOnError: (handler?: (error: Error, context: ErrorContext) => void) => void; /** * Replace the navigation handler at runtime. Used by FirestateProvider to * keep the store identity stable when consumers pass an inline `onNavigate` * callback that changes reference on every render. */ setOnNavigate: (handler?: (path: string) => void) => void; /** * Resolve the router path to stamp onto a handle-pushed undo action. * Delegates to the config's `getUndoPath`; returns `undefined` when none * is configured. Called by handle writers as they push undo actions. */ getUndoPath: () => string | undefined; /** * Replace the undo-path resolver at runtime. Used by FirestateProvider to * keep the store identity stable when consumers pass an inline * `getUndoPath` callback that changes reference on every render. */ setGetUndoPath: (handler?: () => string | undefined) => void; /** Replace the successful-undo handler without recreating the store. */ setOnUndo: (handler?: (action: UndoAction) => void) => void; /** Replace the successful-redo handler without recreating the store. */ setOnRedo: (handler?: (action: UndoAction) => void) => void; /** Subscribe to sync state changes */ subscribeToSyncState: (fn: Subscriber) => Unsubscribe; /** Report a document/collection sync state change */ reportSyncState: (key: string, isSynced: boolean) => void; /** * Remove a sync-state key. Subscriptions call this on stop() so an * unmounted hook does not leave the global isSynced stuck at false. */ unregisterSyncState: (key: string) => void; /** Get whether all tracked resources are synced */ readonly isSynced: boolean; } /** * Create a Firestate store. * This is the central configuration point for your Firestore state management. * * @example * ```ts * import { createStore } from 'firestate' * import { db } from './firebase' * * export const store = createStore({ * firestore: db, * autosave: 1000, * maxUndoLength: 20, * onError: (error, context) => { * console.error(`Error in ${context.type} ${context.path}:`, error) * }, * }) * ``` */ declare const createStore: (config: FirestateConfig) => FirestateStore; /** * Type alias for the store type */ type Store = ReturnType; //#endregion //#region src/react/hooks.d.ts /** * Opts a {@link useDocument} call into a selected slice. The hook still returns * a full handle (writers, `ref`, status) — only `data` is narrowed to whatever * `selector` returns. */ interface DocumentSelectorOptions { /** * Project the document's observable state down to the slice this component * reacts to. The selector receives the full {@link DocumentState} — * `{ data, isLoading, isLoaded, isSynced, error }`, where `data` is * `undefined` while the document is loading or the hook is disabled — and * the component * re-renders *only* when the returned slice changes (per `isEqual`). Status * is not a freebie: read `s.isLoading`/`s.isSynced`/`s.error` here if you * want to react to them (e.g. `s => ({ title: s.data?.title, saving: * !s.isSynced })`). What you select is exactly what re-renders, and the * returned handle exposes only the slice plus writers/`ref`. */ selector: (state: DocumentState) => TSelected; /** * Decide whether two consecutive slices are equal; the hook re-renders only * when this returns `false`. Defaults to a deep value comparison, so a * selector that returns a fresh object/array of the same shape does not * over-render. Pass {@link shallow} for a one-level compare, or a custom * comparator. */ isEqual?: (a: TSelected, b: TSelected) => boolean; } /** * Opts a {@link useCollection} call into a selected slice. See * {@link DocumentSelectorOptions}; the only difference is the selector receives * the collection's keyed record. */ interface CollectionSelectorOptions { /** * Project the collection's observable state down to the slice this component * reacts to. The selector receives the full {@link CollectionState} — * `{ data, isLoading, isLoaded, isSynced, isActive, error }`, where `data` is * the keyed record (e.g. `s => s.data[id]` or `s => Object.keys(s.data)`) — * and the * component re-renders *only* when the returned slice changes. As with * {@link DocumentSelectorOptions.selector}, status is reactive only if you * select it, and the returned handle exposes just the slice plus * writers/`ref`. */ selector: (state: CollectionState) => TSelected; /** See {@link DocumentSelectorOptions.isEqual}. */ isEqual?: (a: TSelected, b: TSelected) => boolean; } /** * Shape used by the non-selector hook overload to *exclude* selector options, * so passing a real `selector` falls through to the selector overload (which * infers `TSelected`) instead of silently resolving to the full-data return. */ type WithoutSelector = { selector?: undefined; isEqual?: undefined; }; /** * Context for providing the Firestate store */ declare const FirestateContext: react0.Context; /** * Hook to access the Firestate store */ declare const useStore: () => FirestateStore; /** * Hook to access the undo manager */ declare const useUndoManager: () => UndoManager; /** * Hook to check if all tracked resources are synced */ declare const useIsSynced: () => boolean; /** * Options for useDocument hook */ interface UseDocumentOptions { /** Document definition from defineDocument() */ definition: DocumentDefinition; /** Route/path parameters for dynamic paths */ params?: Record; /** Override read-only setting */ readOnly?: boolean; /** Enable undo/redo for this document (default: false) */ undoable?: boolean; /** * If false, no subscription is created and a no-op handle is returned * (`{ data: undefined, isLoaded: false, ref: undefined }`). Use this to gate * subscriptions on route params that aren't ready yet. Default: true. */ enabled?: boolean; } /** * Hook to subscribe to a Firestore document with real-time updates. * * The subscription is keyed on the resolved document path (`definition` + * computed id). When that key changes — typically because `params` produces a * different id — the hook tears down the old Firestore listener and attaches a * new one. Toggling `undoable` does not rebuild the subscription. * * `readOnly` is a *per-handle capability*, not part of the key: a `readOnly` * hook shares the same listener and optimistic state as a writable hook on the * same document (a write through the writable handle is instantly visible to * the read-only reader), and only this handle's own writers (`update`/`set`/ * `delete`) and `sync` are disabled. * * Use `enabled: false` to suppress the subscription entirely (e.g., when * route params aren't ready yet). * * **SSR.** On the server there is no Firestore listener, so this hook returns * the initial handle (`{ data: undefined, isLoaded: false }`). Mutations like * `update`/`set` will mutate orphaned local state with no effect — avoid * calling them server-side. * * The default handle is **sync-agnostic** — it carries `data`/`isLoaded`/`error` * but not `isSynced`, so it does not re-render when a write settles. Render save * state via the per-entry `use{Name}SyncStatus` hook, or fold `isSynced` into a * `selector`. * * @example * ```tsx * const projectDoc = defineDocument({ * collection: 'projects', * id: (params) => params.projectId, * }) * * function ProjectEditor({ projectId }: { projectId: string }) { * const { data, update, isLoaded } = useDocument({ * definition: projectDoc, * params: { projectId }, * }) * * if (!isLoaded) return * * return ( * update({ name: e.target.value })} * /> * ) * } * ``` */ declare function useDocument(options: UseDocumentOptions & WithoutSelector): DocumentHandle; /** * Selector overload: pass `selector` to narrow the returned `data` to a slice * and re-render only when that slice changes. Writers (`update`/`set`/`delete`) * and `ref` keep operating on the full document. See * {@link DocumentSelectorOptions}. * * @example * ```tsx * // Re-renders only when the title changes, not on any other field. * const { data: title, update } = useDocument({ * definition: projectDoc, * params: { projectId }, * selector: (s) => s.data?.title, * }) * ``` */ declare function useDocument(options: UseDocumentOptions & DocumentSelectorOptions): SelectedDocumentHandle; /** * Options for useCollection hook */ interface UseCollectionOptions { /** Collection definition from defineCollection() */ definition: CollectionDefinition; /** Route/path parameters for dynamic paths */ params?: Record; /** Override read-only setting */ readOnly?: boolean; /** Additional query constraints */ queryConstraints?: QueryConstraint[]; /** Enable undo/redo for this collection (default: false) */ undoable?: boolean; /** * If false, no subscription is created and a no-op handle is returned * (`{ data: {}, isLoaded: false, isActive: false }`). Use this to gate on * route params that aren't ready yet. Default: true. */ enabled?: boolean; } /** * Hook to subscribe to a Firestore collection with real-time updates. * * The subscription is keyed on the resolved collection path and the *semantic * identity* of `queryConstraints`. When either changes, the listener is torn * down and re-attached with the new query. Toggling `undoable` does not rebuild * the subscription. `readOnly` is a per-handle capability, not part of the key — * a `readOnly` hook shares one listener and optimistic state with a writable * hook on the same query (see {@link useDocument}). * * **You do not need to memoize `queryConstraints`.** `QueryConstraint` objects * are opaque, so Firestate compares the *built query* with Firestore's own * `queryEqual` instead of comparing array references. A fresh array that * produces the same query (e.g. constraint inputs read from a document that * Firestate deep-clones on optimistic updates) does not rebuild the listener; * only a genuine change to the query does: * * ```tsx * // stationIds may change reference on every edit to its parent document, * // even when its contents are unchanged — the listener survives anyway. * const stations = useCollection({ * definition: weatherStations, * queryConstraints: [where(documentId(), 'in', stationIds)], * }) * ``` * * Memoizing is still a fine micro-optimization (it skips the per-render query * build + compare via the reference fast-path), but it is no longer required * for listener stability. * * Use `enabled: false` to suppress the subscription entirely (e.g., when * route params aren't ready yet). * * **SSR.** On the server there is no Firestore listener, so this hook returns * the initial handle (`{ data: {}, isLoaded: false }`, `isActive: false` for * lazy). Avoid calling mutations server-side. * * Like {@link useDocument}, the default handle is **sync-agnostic** — `data`, * `isLoaded`, `isActive`, `error`, but not `isSynced`. `isActive` stays so a * lazy collection can gate a "Load" button; `isLoaded` is `isActive && * !isLoading`. Render save state via `use{Name}SyncStatus`. * * @example * ```tsx * const spacesCollection = defineCollection({ * path: (params) => `projects/${params.projectId}/spaces`, * lazy: true, * }) * * function SpacesList({ projectId }: { projectId: string }) { * const { data, update, load, isActive, isLoaded } = useCollection({ * definition: spacesCollection, * params: { projectId }, * }) * * // Lazy load on mount * useEffect(() => { load() }, [load]) * * if (!isActive) return * if (!isLoaded) return * * return ( *
    * {Object.values(data).map((space) => ( *
  • {space.name}
  • * ))} *
* ) * } * ``` */ declare function useCollection(options: UseCollectionOptions & WithoutSelector): CollectionHandle; /** * Selector overload: pass `selector` to narrow the returned `data` to a slice * of the collection and re-render only when that slice changes. Writers * (`update`/`add`/`remove`) and `ref` keep operating on the full collection. * See {@link CollectionSelectorOptions}. * * @example * ```tsx * // Re-renders only when this one document's slice changes. * const { data: space } = useCollection({ * definition: spacesCollection, * params: { projectId }, * selector: (s) => s.data[spaceId], * }) * ``` */ declare function useCollection(options: UseCollectionOptions & CollectionSelectorOptions): SelectedCollectionHandle; /** Options for the document status hooks (a subset of {@link UseDocumentOptions}). */ interface UseDocumentStatusOptions { /** Document definition from defineDocument(). */ definition: DocumentDefinition; /** Route/path parameters for dynamic paths. */ params?: Record; /** * If false, no subscription is created and the idle status is returned * (`{ isSynced: true, isSaving: false }` / `{ isLoading: false, isLoaded: * false }`). Default: true. */ enabled?: boolean; } /** Options for the collection status hooks. Adds `queryConstraints`. */ interface UseCollectionStatusOptions { /** Collection definition from defineCollection(). */ definition: CollectionDefinition; /** Route/path parameters for dynamic paths. */ params?: Record; /** * Query constraints. Must produce the same query the data hook uses, or the * status hook resolves a *different* shared entry (a second listener) — * sharing is keyed by semantic query identity. */ queryConstraints?: QueryConstraint[]; /** See {@link UseDocumentStatusOptions.enabled}. */ enabled?: boolean; } /** * Subscribe to a document's **sync status only** — `{ isSynced, isSaving }`. * * The opt-in counterpart to the sync-agnostic default handle (see * {@link DocumentHandle}): it re-renders when sync state flips but never on data * changes, and shares the resource's one `onSnapshot` listener with * `useDocument` and any slice hooks, so opting in adds no listener. While * disabled it reports `{ isSynced: true, isSaving: false }`. */ declare function useDocumentSyncStatus(options: UseDocumentStatusOptions): SyncStatus; /** * Subscribe to a document's **loading status only** — `{ isLoading, isLoaded }`. * * A spinner channel that shares the resource's listener and does NOT re-render * on data changes — for a progress indicator rendered apart from the data. The * data handle keeps `isLoaded` for the common render path; this is an extra * channel, not a replacement. */ declare function useDocumentLoadingStatus(options: UseDocumentStatusOptions): LoadingStatus; /** * Collection counterpart of {@link useDocumentSyncStatus} — `{ isSynced, * isSaving }` over a collection query, sharing its one listener. * * **Lazy caveat.** On a `lazy` collection this hook never calls `load()` itself: * activating a lazy listener is the data hook's job, and a passive status reader * must not silently start the listener (and bill the reads) the laziness exists * to defer. As the *lone* subscriber it therefore attaches no listener and stays * at the idle `{ isSynced: true, isSaving: false }`. Mount it alongside a * {@link useCollection} on the same query whose `load()` has run — the status * hook rides that one shared listener and reports real sync state. Non-lazy * collections activate on mount, so this hook works standalone there. */ declare function useCollectionSyncStatus(options: UseCollectionStatusOptions): SyncStatus; /** * Collection counterpart of {@link useDocumentLoadingStatus} — `{ isLoading, * isLoaded }` over a collection query, sharing its one listener. * * Same lazy caveat as {@link useCollectionSyncStatus}: on a `lazy` collection it * never calls `load()`, so as the lone subscriber it attaches no listener and * stays at the idle `{ isLoading: false, isLoaded: false }` until a co-mounted * {@link useCollection} (or any active hook on the same query) activates the * shared listener via `load()`. Non-lazy collections activate on mount. */ declare function useCollectionLoadingStatus(options: UseCollectionStatusOptions): LoadingStatus; /** * Keyboard shortcut hook for undo/redo * * @example * ```tsx * function App() { * useUndoKeyboardShortcuts() * return * } * ``` */ declare const useUndoKeyboardShortcuts: () => void; //#endregion //#region src/registry/firestate.d.ts /** * Knobs forwarded from a generated document hook to {@link useDocument}. * Same shape as `UseDocumentOptions` minus the fields the registry already * owns (`definition`, `params`). */ type DocHookOptions = Omit, "definition" | "params">; /** * Knobs forwarded from a generated collection hook to {@link useCollection}. */ type ColHookOptions = Omit, "definition" | "params">; interface CommonEntryOptions { /** Debounce interval for autosave (ms). */ autosave?: number; /** Minimum loading indicator time (ms). */ minLoadTime?: number; /** Whether this entry is read-only. */ readOnly?: boolean; /** Retry the snapshot listener on transient errors. */ retryOnError?: boolean; /** Retry interval (ms). */ retryInterval?: number; } /** * Document entry in a Firestate registry. Produced by {@link doc}. * * The `P` generic carries the path template's string-literal type so the * generated hook can type-check param keys. `__kind` is a runtime * discriminator; `__type` is a phantom field used purely for inference at * the call site and is never read. */ interface DocEntry extends CommonEntryOptions { readonly __kind: "document"; readonly __type?: T; /** * Path template, e.g. `'taskLists/{listId}'`, or a function returning the * **full document path** at runtime (the collection/id split happens * per-call via {@link splitDocPath}). Use the function form for paths that * branch on a param. See {@link PathArg}. */ path: PathArg

; /** * Zod schema. **Required** — firestate's registry API is opinionated * about Zod. The schema is the source of `T` for the generated hooks * via `z.infer`, and firestate runs `schema.parse(...)` on full-payload * writes (`set`/`add`) so bad data throws at the call site rather than * after a Firestore round trip. Partial `update(diff)` is NOT validated * (diffs frequently contain Firestore sentinels like `serverTimestamp()`). * * If you don't want a schema at all, use {@link defineDocument} directly — * the escape hatch keeps the plain-TypeScript form at the cost of looser * param typing and no runtime validation. */ schema: ZodType; /** * Derive a **named slice-hook** off this document, sharing its schema and * path — the schema is handed to firestate once, here, and never * re-specified. The `selector` receives the full {@link DocumentState}; * return the slice the generated hook reacts to. For a *parameterized* slice, * declare the extra params as the selector's second argument — the generated * hook then requires the path params **and** those, merged into one bag. * * Pass the result to {@link createFirestate} under the key the hook is named * for. Status is reactive only if the slice reads it, exactly as the inline * `selector` option (see {@link DocumentHandle}); the comparator (`isEqual`) * is baked in here, not passed per call. * * ```ts * const project = doc({ path: 'projects/{projectId}', schema: ProjectSchema }) * const { useProject, useProjectTitle } = createFirestate({ * project, // → useProject (full) * projectTitle: project.select((s) => s.data?.name), // → useProjectTitle * }) * ``` * * A derived entry is a leaf, not a base: there is intentionally no * `.select(...).select(...)` chaining. * * `PExtra` (the selector's own params) defaults to `{}`: a one-argument * selector leaves it unbound, a two-argument one infers it from the annotated * second parameter. One signature keeps the selector's `state` arg reliably * typed in both cases. `PExtra` is intentionally unconstrained — leaving it * `extends Record` made TS resolve a param-less selector's * `PExtra` to that constraint (not the `{}` default), wrongly forcing a * `params` arg on no-placeholder paths; unconstrained also lets a slice take * non-string params (e.g. `{ index: number }`). */ select(selector: (state: DocumentState, params: PExtra) => TSelected, options?: SelectOptions): SelectedDocEntry; } /** Collection entry in a Firestate registry. Produced by {@link col}. */ interface ColEntry extends CommonEntryOptions { readonly __kind: "collection"; readonly __type?: T; /** * Path template, e.g. `'taskLists/{listId}/tasks'`, or a function returning * the **collection path** at runtime. Use the function form for paths that * branch on a param. See {@link PathArg}. */ path: PathArg

; /** Zod schema. Required. See {@link DocEntry.schema}. */ schema: ZodType; /** Only subscribe when `load()` is called. */ lazy?: boolean; /** Additional Firestore query constraints. */ queryConstraints?: QueryConstraint[]; /** * Derive a **named slice-hook** off this collection, sharing its schema, * path, and query — see {@link DocEntry.select}. The `selector` receives the * full {@link CollectionState} (`s.data` is the keyed record), and a * parameterized slice declares its extra params as the selector's second * argument. * * ```ts * const tasks = col({ path: 'projects/{projectId}/tasks', schema: TaskSchema }) * const { useTasks, useTaskIds, useTaskById } = createFirestate({ * tasks, // → useTasks (full) * taskIds: tasks.select((s) => Object.keys(s.data)), // → useTaskIds * taskById: tasks.select((s, p: { id: string }) => s.data[p.id]), // → useTaskById * }) * // useTaskById requires the merged bag: useTaskById({ projectId, id }) * ``` * * `PExtra` defaults to `{}` and is unconstrained — see {@link DocEntry.select}. */ select(selector: (state: CollectionState, params: PExtra) => TSelected, options?: SelectOptions): SelectedColEntry; } /** * Options bundled into a `.select(...)` entry at definition time. Kept separate * from the runtime hook options (`enabled`/`readOnly`/`queryConstraints`) * because these are baked into the named hook, not passed per call. */ interface SelectOptions { /** * Comparator for this named hook's slice; the hook re-renders only when it * returns `false`. Defaults to a deep value compare (so a fresh object/array * of equal shape does not over-render). Pass {@link shallow} or a custom fn. */ isEqual?: (a: TSelected, b: TSelected) => boolean; } /** * A {@link DocEntry} narrowed by a `.select(...)` projection. Produced by * {@link DocEntry.select}, consumed by {@link createFirestate}, which turns it * into a hook whose `data` is the slice (`TSelected`) and whose params are the * path params (`P`) merged with the selector's own params (`PExtra`). * * The schema/path/options live on `base` — a derived entry never re-declares * them. `PExtra` is `{}` for an un-parameterized selector. */ interface SelectedDocEntry { readonly __kind: "document-selected"; /** Base entry carrying schema/path/options — handed to firestate once. */ readonly base: DocEntry; /** Projection over the full state; receives the merged params bag at runtime. */ readonly selector: (state: DocumentState, params: PExtra) => TSelected; /** Comparator baked in at definition time (see {@link SelectOptions}). */ readonly isEqual?: (a: TSelected, b: TSelected) => boolean; } /** * A {@link ColEntry} narrowed by a `.select(...)` projection. See * {@link SelectedDocEntry}; the selector receives the collection's keyed state. */ interface SelectedColEntry { readonly __kind: "collection-selected"; /** Base entry carrying schema/path/query/options — handed to firestate once. */ readonly base: ColEntry; /** Projection over the full state; receives the merged params bag at runtime. */ readonly selector: (state: CollectionState, params: PExtra) => TSelected; /** Comparator baked in at definition time (see {@link SelectOptions}). */ readonly isEqual?: (a: TSelected, b: TSelected) => boolean; } type FirestateEntry = DocEntry | ColEntry; /** Any `.select(...)`-derived entry, regardless of its type parameters. */ type AnySelectedEntry = SelectedDocEntry | SelectedColEntry; type FirestateRegistry = Record | AnySelectedEntry>; /** * Extract `{name}` placeholders from a path template into a params shape. * * - `'users'` → `{}` * - `'users/{userId}'` → `{ userId: string }` * - `'projects/{projectId}/revisions/{revisionId}'` → `{ projectId: string; revisionId: string }` * * When the path is widened to `string` (no literal preserved), we fall * back to `Record` so existing call sites keep compiling. */ type ParamsOf

= string extends P ? Record : Prettify>; type RawParamsOf

= P extends `${string}{${infer K}}${infer Rest}` ? { [Key in K]: string } & RawParamsOf : {}; type Prettify = { [K in keyof T]: T[K] } & {}; /** * The `path` accepted by {@link doc} / {@link col}. Either a static template * (whose `{param}` placeholders are interpolated and whose param keys are * inferred via {@link ParamsOf}), or a function that returns the path at * runtime — for paths that branch on a param, e.g. live * `projects/{projectId}/spaces` vs. revision * `projects/{projectId}/revisions/{revisionId}/spaces`. * * With the function form, params can't be inferred from a template, so the * generated hook's params fall back to `Record`. */ type PathArg

= P | ((params: Record) => string); type DocOpts = Omit, "__kind" | "__type" | "path" | "select">; type ColOpts = Omit, "__kind" | "__type" | "path" | "select">; /** * Declare a single-document entry for a Firestate registry. * * **A Zod `schema` field is required.** Both the data type (`T`) and the * path's literal type (`P`) are inferred from the call — `T` via * `z.infer`, `P` from `path` — so the generated hook can statically * type-check the params object the caller passes. The schema also runs * at runtime on full-payload writes (`set`/`add`). * * If you'd rather not provide a schema at all, use {@link defineDocument} * directly — that escape hatch keeps the plain-TypeScript form, at the * cost of looser param typing on the hook and no runtime validation. * * `path` may also be a function returning the full document path at runtime — * for paths that branch on a param. Param keys can't be inferred from a * function, so they fall back to `Record`. See {@link PathArg}. * * ```ts * import { z } from 'zod' * * const TaskListSchema = z.object({ name: z.string(), createdAt: z.number() }) * doc({ path: 'taskLists/{listId}', schema: TaskListSchema }) * // → DocEntry<{ name: string; createdAt: number }, 'taskLists/{listId}'> * ``` */ declare function doc, const P extends string = string>(opts: Omit>, "schema"> & { schema: S; path: PathArg

; }): DocEntry, P>; /** * Declare a collection entry for a Firestate registry. See {@link doc} * for the schema/typing contract. `path` may also be a function returning * the collection path at runtime — see {@link PathArg}. */ declare function col, const P extends string = string>(opts: Omit>, "schema"> & { schema: S; path: PathArg

; }): ColEntry, P>; type HookName = `use${Capitalize}`; type NoSelector = { selector?: undefined; isEqual?: undefined; }; interface DocHookOptionalParams { (params?: Record, options?: DocHookOptions & NoSelector): DocumentHandle; (params: Record | undefined, options: DocHookOptions & DocumentSelectorOptions): SelectedDocumentHandle; } interface DocHookRequiredParams { (params: ParamsOf

, options?: DocHookOptions & NoSelector): DocumentHandle; (params: ParamsOf

, options: DocHookOptions & DocumentSelectorOptions): SelectedDocumentHandle; } interface ColHookOptionalParams { (params?: Record, options?: ColHookOptions & NoSelector): CollectionHandle; (params: Record | undefined, options: ColHookOptions & CollectionSelectorOptions): SelectedCollectionHandle; } interface ColHookRequiredParams { (params: ParamsOf

, options?: ColHookOptions & NoSelector): CollectionHandle; (params: ParamsOf

, options: ColHookOptions & CollectionSelectorOptions): SelectedCollectionHandle; } type IsAny = 0 extends 1 & T ? true : false; type SelectedParams

= IsAny extends true ? ParamsOf

: Prettify & PExtra>; type SelectedDocHookFor = keyof SelectedParams extends never ? (params?: Record, options?: DocHookOptions) => SelectedDocumentHandle : (params: SelectedParams, options?: DocHookOptions) => SelectedDocumentHandle; type SelectedColHookFor = keyof SelectedParams extends never ? (params?: Record, options?: ColHookOptions) => SelectedCollectionHandle : (params: SelectedParams, options?: ColHookOptions) => SelectedCollectionHandle; type HookFor = E extends SelectedDocEntry ? SelectedDocHookFor : E extends SelectedColEntry ? SelectedColHookFor : E extends DocEntry ? keyof ParamsOf

extends never ? DocHookOptionalParams : DocHookRequiredParams : E extends ColEntry ? keyof ParamsOf

extends never ? ColHookOptionalParams : ColHookRequiredParams : never; type SyncStatusHookName = `${HookName}SyncStatus`; type LoadingStatusHookName = `${HookName}LoadingStatus`; type BaseEntry = DocEntry | ColEntry; type DocStatusHookOptions = { enabled?: boolean; }; type ColStatusHookOptions = { enabled?: boolean; queryConstraints?: QueryConstraint[]; }; type DocStatusHookFor

= keyof ParamsOf

extends never ? (params?: Record, options?: DocStatusHookOptions) => Ret : (params: ParamsOf

, options?: DocStatusHookOptions) => Ret; type ColStatusHookFor

= keyof ParamsOf

extends never ? (params?: Record, options?: ColStatusHookOptions) => Ret : (params: ParamsOf

, options?: ColStatusHookOptions) => Ret; type SyncStatusHookFor = E extends DocEntry ? DocStatusHookFor : E extends ColEntry ? ColStatusHookFor : never; type LoadingStatusHookFor = E extends DocEntry ? DocStatusHookFor : E extends ColEntry ? ColStatusHookFor : never; type FirestateApi = { [K in keyof R & string as HookName]: HookFor } & { [K in keyof R & string as R[K] extends BaseEntry ? SyncStatusHookName : never]: SyncStatusHookFor } & { [K in keyof R & string as R[K] extends BaseEntry ? LoadingStatusHookName : never]: LoadingStatusHookFor }; /** * Turn a Firestate registry into a map of typed React hooks. Each entry * `K` produces a hook named `use{Capitalize}`. * * ```ts * export const { useTaskList, useTasks } = createFirestate({ * taskList: doc('taskLists/{listId}'), * tasks: col('taskLists/{listId}/tasks'), * }) * ``` */ declare function createFirestate(registry: R): FirestateApi; //#endregion //#region src/utils/diff.d.ts /** * Check if two values are deeply equal */ declare const isDeepEqual: (a: unknown, b: unknown) => boolean; /** * Compute the minimal diff between two objects for Firestore updates. * Returns only the fields that changed, using deleteField() for removed fields. * * @param from - The original object (sync state) * @param to - The target object (local state) * @returns A partial object containing only changed fields */ declare const computeDiff: (from: T, to: T | undefined) => WithFieldValue>; /** * Apply a Firestore diff to a target object in place (mutating). * Handles deleteField(), serverTimestamp(), and nested objects. * * Most code should use `applyDiff` (immutable) instead. * This mutable version is useful for performance-critical paths * where you're already working with a cloned object. * * @param target - The object to mutate * @param diff - The diff to apply */ declare const applyDiffMutable: (target: FirestoreObject, diff: Record) => void; /** * Create a deep clone of an object that's safe for Firestore operations. * * Firestore opaque values (FieldValue sentinels, Timestamp, * DocumentReference, GeoPoint, Bytes, VectorValue) are returned **by * reference**. They are immutable from the user's perspective; cloning * them by walking keys would either lose their prototype — turning a * `DocumentReference` into a plain object Firestore can't recognize — * or destroy a sentinel that needed to reach the server intact. */ declare const deepClone: (value: T) => T; /** * Check if a diff is empty (no changes) */ declare const isDiffEmpty: (diff: Record) => boolean; /** * Flatten a nested diff object to dot notation for use with Firestore's updateDoc. * * This converts: * ``` * { building: { floors: 5, height: 100 }, name: 'Test' } * ``` * To: * ``` * { 'building.floors': 5, 'building.height': 100, 'name': 'Test' } * ``` * * Arrays, FieldValue sentinels (deleteField, serverTimestamp, …) and * Firestore value types (Timestamp, DocumentReference, GeoPoint, Bytes, * VectorValue) are NOT flattened — they're preserved at their path so * Firestore receives them in their original form. * * ⚠️ The dot-joined keys this produces are AMBIGUOUS to Firestore if any map * key itself contains a "." (e.g. an email key `a@b.com`): `updateDoc` parses * the "." as a path separator and writes to the wrong nested field. The * write path uses {@link flattenDiffToFieldPaths} + `FieldPath` instead, which * keeps every segment literal. Reach for this only when dotted-string keys are * what you actually want. * * @param diff - The nested diff object * @param prefix - Internal: current path prefix for recursion * @returns Flattened object with dotted keys */ declare const flattenDiff: (diff: Record, prefix?: string) => Record; /** * Flatten a nested diff into a list of `{ segments, value }` entries, where * `segments` is the path expressed as an array of *literal* key segments * (never joined into a dotted string). * * This is the segment-preserving sibling of {@link flattenDiff}. It exists * because dot-joined string keys are ambiguous to Firestore: `updateDoc(ref, * { "users.a@b.com.role": 4 })` parses the `.` inside the email key as a path * separator and writes to `users → "a@b" → "com" → role` instead of the * literal key `"a@b.com"`. Feeding these segments to `new FieldPath(...segments)` * (with the variadic `updateDoc(ref, fieldPath, value, …)` form) keeps every * segment literal, so a "." inside a map key is never re-interpreted. * * The opaque/plain-object rules mirror {@link flattenDiff} exactly: arrays, * FieldValue sentinels (deleteField, serverTimestamp, …), and Firestore value * types (Timestamp, DocumentReference, GeoPoint, Bytes, VectorValue) ride * through verbatim at their path; only plain objects are recursed into. * * @param diff - The nested diff object * @param prefix - Internal: accumulated path segments for recursion * @returns Flat list of `{ segments, value }` entries */ declare const flattenDiffToFieldPaths: (diff: Record, prefix?: string[]) => Array<{ segments: string[]; value: unknown; }>; /** * Merge two diffs together, with the second taking precedence */ declare const mergeDiffs: (first: WithFieldValue>, second: WithFieldValue>) => WithFieldValue>; /** * Apply a diff to an object, returning a new object. * The original object is not modified. * * @example * ```ts * const original = { name: 'Project', count: 5 } * const diff = { name: 'Updated', count: deleteField() } * const result = applyDiff(original, diff) * // result = { name: 'Updated' } * // original is unchanged * ``` */ declare const applyDiff: (state: T, diff: WithFieldValue>) => T; /** * Compute the undo diff that would reverse the effect of applying a diff to a state. * * Given a starting state and a diff that was (or will be) applied to it, * returns a new diff that when applied to the result would restore the original state. * * @example * ```ts * const startState = { name: 'Foo', count: 5 } * const diff = { name: 'Bar', count: deleteField() } * * // Apply the diff * const endState = applyDiff(startState, diff) * // endState = { name: 'Bar' } * * // Compute the undo * const undoDiff = computeUndoDiff(startState, diff) * // undoDiff = { name: 'Foo', count: 5 } * * // Applying undoDiff to endState restores startState * const restored = applyDiff(endState, undoDiff) * // restored = { name: 'Foo', count: 5 } * ``` */ declare const computeUndoDiff: (startState: T, diff: WithFieldValue>) => WithFieldValue>; /** * Check if a diff affects a specific path (supports dot notation). * * @example * ```ts * const diff = { building: { floors: 5 }, name: 'Test' } * * diffContainsPath(diff, 'name') // true * diffContainsPath(diff, 'building') // true * diffContainsPath(diff, 'building.floors') // true * diffContainsPath(diff, 'building.height') // false * diffContainsPath(diff, 'other') // false * ``` */ declare const diffContainsPath: (diff: Record, path: string) => boolean; /** * Extract the value at a specific path from a diff (supports dot notation). * Returns undefined if the path doesn't exist in the diff. * * @example * ```ts * const diff = { building: { floors: 5, height: 100 }, name: 'Test' } * * extractDiffValue(diff, 'name') // 'Test' * extractDiffValue(diff, 'building') // { floors: 5, height: 100 } * extractDiffValue(diff, 'building.floors') // 5 * extractDiffValue(diff, 'building.missing') // undefined * ``` */ declare const extractDiffValue: (diff: Record, path: string) => unknown; /** * Create a diff that sets a value at a specific path (supports dot notation). * * @example * ```ts * createDiffAtPath('name', 'New Name') * // { name: 'New Name' } * * createDiffAtPath('building.floors', 5) * // { building: { floors: 5 } } * * createDiffAtPath('building.config.enabled', true) * // { building: { config: { enabled: true } } } * ``` */ declare const createDiffAtPath: (path: string, value: unknown) => Record; /** * Invert a flattened diff back to nested object structure. * Opposite of flattenDiff. * * @example * ```ts * const flat = { 'building.floors': 5, 'building.height': 100, 'name': 'Test' } * const nested = unflattenDiff(flat) * // { building: { floors: 5, height: 100 }, name: 'Test' } * ``` */ declare const unflattenDiff: (flatDiff: Record) => Record; //#endregion //#region src/utils/shallow.d.ts /** * Shallow structural equality. * * Returns `true` when `a` and `b` are identical by `Object.is`, or are two * arrays / two plain objects whose entries are pairwise `Object.is`-equal one * level deep. Anything else (different shapes, nested objects that aren't * reference-equal) is `false`. * * Intended as the `isEqual` for a hook `selector` that builds a fresh array or * object every render — e.g. `data => Object.values(data).map(d => d.id)` or * `data => ({ name: data?.name, done: data?.done })`. The default selector * comparison is a *deep* value compare, which is correct but does more work * than needed for a flat projection; `shallow` re-renders on a genuine change * to any entry while collapsing the fresh-reference-same-entries case. * * Not recursive on purpose: if a selected entry is itself an object you mutate * in place rather than replace, prefer the default deep comparison or a * bespoke `isEqual`. */ declare const shallow: (a: T, b: T) => boolean; //#endregion //#region src/core/document.d.ts /** * Options for creating a document subscription */ interface DocumentOptions { /** The store instance */ store: FirestateStore; /** Document definition from defineDocument() */ definition: DocumentDefinition; /** * Resolved document id. If omitted and `definition.id` is a string, that * value is used. If `definition.id` is a function, this option is required. */ docId?: string; /** * Resolved collection path. If omitted and `definition.collection` is a * string, that value is used. If `definition.collection` is a function, * this option is required. */ collectionPath?: string; /** Override read-only setting */ readOnly?: boolean; /** Callback for pushing undo actions */ onPushUndo?: (undoAction: () => void, redoAction: () => void, options?: UpdateOptions) => void; } /** * Create a document subscription. * This is a low-level API - prefer using useDocument hook in React. * * @example * ```ts * const subscription = createDocumentSubscription({ * store, * definition: projectDoc, * docId: '123', * }) * * const unsubscribe = subscription.subscribe((state) => { * console.log('Document state:', state) * }) * * subscription.load() * ``` */ declare const createDocumentSubscription: (options: DocumentOptions) => { /** Attach the Firestore listener */load: () => void; /** Stop the Firestore listener */ stop: () => void; /** Subscribe to state changes */ subscribe: (fn: Subscriber>) => Unsubscribe; /** Get current state */ getState: () => DocumentState; /** Get document handle for updates */ getHandle: () => DocumentHandle; /** Force sync now */ sync: () => Promise; }; //#endregion //#region src/core/collection.d.ts /** * Options for creating a collection subscription */ interface CollectionOptions { /** The store instance */ store: FirestateStore; /** Collection definition from defineCollection() */ definition: CollectionDefinition; /** * Resolved collection path. If omitted and `definition.path` is a string, * that value is used. If `definition.path` is a function, this option is * required. */ collectionPath?: string; /** Override read-only setting */ readOnly?: boolean; /** Additional query constraints */ queryConstraints?: QueryConstraint[]; /** Callback for pushing undo actions */ onPushUndo?: (undoAction: () => void, redoAction: () => void, options?: UpdateOptions) => void; } /** * Create a collection subscription. * This is a low-level API - prefer using useCollection hook in React. * * @example * ```ts * const subscription = createCollectionSubscription({ * store, * definition: spacesCollection, * collectionPath: 'projects/123/spaces', * }) * * const unsubscribe = subscription.subscribe((state) => { * console.log('Collection state:', state) * }) * * subscription.load() // For lazy collections * ``` */ declare const createCollectionSubscription: (options: CollectionOptions) => { /** Activate the subscription (for lazy loading) */load: () => void; /** Stop the Firestore listener */ stop: () => void; /** Subscribe to state changes */ subscribe: (fn: Subscriber>) => Unsubscribe; /** Get current state */ getState: () => CollectionState; /** Get collection handle for updates */ getHandle: () => CollectionHandle; /** Force sync now */ sync: () => Promise; }; //#endregion //#region src/react/provider.d.ts /** * Props for FirestateProvider */ interface FirestateProviderProps { /** Firestore instance */ firestore: Firestore; /** Default autosave interval (ms), default 1000 */ autosave?: number; /** Default minimum load time (ms), default 0 */ minLoadTime?: number; /** Maximum undo stack length, default 20 */ maxUndoLength?: number; /** * Called before undo/redo when the action carries a `path`. Wire your * router's `navigate` here to return users to where a change occurred * before reverting it. * * @example * ```tsx * import { useNavigate } from 'react-router-dom' * * function App() { * const navigate = useNavigate() * return ( * navigate(path)}> * {children} * * ) * } * ``` */ onNavigate?: (path: string) => void; /** * Called when a handle write (`update`/`add`/`remove`) pushes an undo * action, to stamp the current router path onto it. That path is what * `onNavigate` later receives, so handle-driven undo can return users to * where a change occurred. Read the path from your router here. * * @example * ```tsx * import { useLocation } from 'react-router-dom' * * function App() { * const location = useLocation() * return ( * location.pathname}> * {children} * * ) * } * ``` */ getUndoPath?: () => string | undefined; /** Custom error handler */ onError?: (error: Error, context: ErrorContext) => void; /** Called after an undo action has been successfully applied */ onUndo?: (action: UndoAction) => void; /** Called after a redo action has been successfully applied */ onRedo?: (action: UndoAction) => void; /** React children */ children: React.ReactNode; } /** * Provider component that sets up Firestate for your application. * * @example * ```tsx * import { FirestateProvider } from 'firestate' * import { db } from './firebase' * * function App() { * return ( * console.error(ctx.path, error)} * > * * * ) * } * ``` */ declare const FirestateProvider: React.FC; /** * Props for using an existing store */ interface FirestateStoreProviderProps { /** Pre-created store instance */ store: FirestateStore; /** React children */ children: React.ReactNode; } /** * Provider that uses an existing store instance. * Useful when you need to create the store outside of React. * * @example * ```tsx * const store = createStore({ firestore: db }) * * function App() { * return ( * * * * ) * } * ``` */ declare const FirestateStoreProvider: React.FC; /** * Hook to use navigation blocker when there are unsaved changes. * Works with react-router or similar routers. * * @example * ```tsx * function ProjectPage() { * const shouldBlock = useUnsavedChangesBlocker() * * // Use with react-router's useBlocker * const blocker = useBlocker( * ({ currentLocation, nextLocation }) => * currentLocation.pathname !== nextLocation.pathname && shouldBlock * ) * * return ( * <> * * {blocker.state === 'blocked' && ( *

Your changes may not be saved! * )} * * ) * } * ``` */ declare const useUnsavedChangesBlocker: () => boolean; //#endregion export { type AnySelectedEntry, type ColEntry, type CollectionDefinition, type CollectionHandle, type CollectionOptions, type CollectionSelectorOptions, type CollectionState, type DeepPartial, type DocEntry, type DocumentDefinition, type DocumentHandle, type DocumentOptions, type DocumentSelectorOptions, type DocumentState, type ErrorContext, type FirestateApi, type FirestateConfig, FirestateContext, type FirestateEntry, FirestateProvider, type FirestateProviderProps, type FirestateRegistry, type FirestateStore, FirestateStoreProvider, type FirestateStoreProviderProps, type FirestoreObject, type InferCollectionData, type InferCollectionDocument, type InferDocument, type InferDocumentData, type LoadingStatus, type SelectOptions, type SelectedColEntry, type SelectedCollectionHandle, type SelectedDocEntry, type SelectedDocumentHandle, type Store, type SyncStatus, type UndoAction, type UndoManager, type UndoManagerConfig, type UndoManagerState, type UndoManagerWithSubscribe, type UpdateOptions, type UseCollectionOptions, type UseCollectionStatusOptions, type UseDocumentOptions, type UseDocumentStatusOptions, applyDiff, applyDiffMutable, col, computeDiff, computeUndoDiff, createCollectionSubscription, createDiffAtPath, createDocumentSubscription, createFirestate, createStore, createUndoManager, deepClone, defineCollection, defineDocument, diffContainsPath, doc, extractDiffValue, flattenDiff, flattenDiffToFieldPaths, isDeepEqual, isDiffEmpty, mergeDiffs, shallow, unflattenDiff, useCollection, useCollectionLoadingStatus, useCollectionSyncStatus, useDocument, useDocumentLoadingStatus, useDocumentSyncStatus, useIsSynced, useStore, useUndoKeyboardShortcuts, useUndoManager, useUnsavedChangesBlocker }; //# sourceMappingURL=index.d.mts.map