@hvakr/firestate 0.1.0
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/LICENSE +21 -0
- package/README.md +968 -0
- package/dist/index.d.mts +1105 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1779 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +67 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,1105 @@
|
|
|
1
|
+
import * as react0 from "react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { CollectionReference, DocumentReference, Firestore, QueryConstraint, WithFieldValue } from "firebase/firestore";
|
|
4
|
+
import { ZodType, z } from "zod";
|
|
5
|
+
|
|
6
|
+
//#region src/types.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Deep partial type that works with Records and nested objects
|
|
9
|
+
*/
|
|
10
|
+
type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
|
|
11
|
+
/**
|
|
12
|
+
* A generic object that can be stored in Firestore.
|
|
13
|
+
*
|
|
14
|
+
* Uses an `any` index signature (matching Firestore's own `DocumentData`) so
|
|
15
|
+
* that plain TypeScript interfaces — which lack an implicit index signature —
|
|
16
|
+
* can satisfy the constraint. Internal call sites cast through more specific
|
|
17
|
+
* types where needed.
|
|
18
|
+
*/
|
|
19
|
+
type FirestoreObject = Record<string, any>;
|
|
20
|
+
/**
|
|
21
|
+
* Options for update operations
|
|
22
|
+
*/
|
|
23
|
+
interface UpdateOptions {
|
|
24
|
+
/** If false, prevents this update from being added to undo stack */
|
|
25
|
+
undoable?: boolean;
|
|
26
|
+
/** Group multiple updates into a single undo action */
|
|
27
|
+
undoGroupId?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* State of a document subscription
|
|
31
|
+
*/
|
|
32
|
+
interface DocumentState<T> {
|
|
33
|
+
/** Current merged state (local changes applied to sync state) */
|
|
34
|
+
data: T | undefined;
|
|
35
|
+
/** Whether initial data has loaded */
|
|
36
|
+
isLoading: boolean;
|
|
37
|
+
/** Whether there are pending local changes */
|
|
38
|
+
isSynced: boolean;
|
|
39
|
+
/** Error from listener, if any */
|
|
40
|
+
error: Error | undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* State of a collection subscription
|
|
44
|
+
*/
|
|
45
|
+
interface CollectionState<T> {
|
|
46
|
+
/** Current merged state keyed by document ID */
|
|
47
|
+
data: Record<string, T>;
|
|
48
|
+
/** Whether initial data has loaded */
|
|
49
|
+
isLoading: boolean;
|
|
50
|
+
/** Whether there are pending local changes */
|
|
51
|
+
isSynced: boolean;
|
|
52
|
+
/** Whether the collection has been activated (for lazy loading) */
|
|
53
|
+
isActive: boolean;
|
|
54
|
+
/** Error from listener, if any */
|
|
55
|
+
error: Error | undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Document handle returned by useDocument hook
|
|
59
|
+
*/
|
|
60
|
+
interface DocumentHandle<T extends FirestoreObject> {
|
|
61
|
+
/** Current document data */
|
|
62
|
+
data: T | undefined;
|
|
63
|
+
/** Update the document with a partial diff */
|
|
64
|
+
update: (diff: WithFieldValue<DeepPartial<T>>, options?: UpdateOptions) => void;
|
|
65
|
+
/** Set the document data (creates or overwrites) */
|
|
66
|
+
set: (data: T, options?: UpdateOptions) => void;
|
|
67
|
+
/** Delete the document */
|
|
68
|
+
delete: (options?: UpdateOptions) => void;
|
|
69
|
+
/** Whether initial data is loading */
|
|
70
|
+
isLoading: boolean;
|
|
71
|
+
/** Whether all changes have synced to Firestore */
|
|
72
|
+
isSynced: boolean;
|
|
73
|
+
/** Force sync pending changes immediately */
|
|
74
|
+
sync: () => Promise<void>;
|
|
75
|
+
/** Error from listener, if any */
|
|
76
|
+
error: Error | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Firestore document reference. Undefined when the hook was called with
|
|
79
|
+
* `enabled: false` (no subscription was created).
|
|
80
|
+
*/
|
|
81
|
+
ref: DocumentReference<T> | undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Collection handle returned by useCollection hook
|
|
85
|
+
*/
|
|
86
|
+
interface CollectionHandle<T extends FirestoreObject> {
|
|
87
|
+
/** Current collection data keyed by document ID */
|
|
88
|
+
data: Record<string, T>;
|
|
89
|
+
/** Update one or more documents with partial diffs */
|
|
90
|
+
update: (diff: WithFieldValue<DeepPartial<Record<string, T>>>, options?: UpdateOptions) => void;
|
|
91
|
+
/**
|
|
92
|
+
* Add a new document to the collection. Either pass an explicit `id`, or
|
|
93
|
+
* omit it to have Firestore generate an auto-id (returned synchronously).
|
|
94
|
+
*
|
|
95
|
+
* Returns `undefined` if the mutation was dropped (read-only handle, or
|
|
96
|
+
* called before the first snapshot has arrived). Callers should narrow
|
|
97
|
+
* before using the id to navigate or persist references.
|
|
98
|
+
*/
|
|
99
|
+
add: {
|
|
100
|
+
(id: string, data: Omit<T, "id">, options?: UpdateOptions): string | undefined;
|
|
101
|
+
(data: Omit<T, "id">, options?: UpdateOptions): string | undefined;
|
|
102
|
+
};
|
|
103
|
+
/** Remove a document from the collection */
|
|
104
|
+
remove: (id: string, options?: UpdateOptions) => void;
|
|
105
|
+
/** Whether initial data is loading */
|
|
106
|
+
isLoading: boolean;
|
|
107
|
+
/** Whether all changes have synced to Firestore */
|
|
108
|
+
isSynced: boolean;
|
|
109
|
+
/** Whether subscription is active (for lazy collections) */
|
|
110
|
+
isActive: boolean;
|
|
111
|
+
/** Activate a lazy subscription */
|
|
112
|
+
load: () => void;
|
|
113
|
+
/** Force sync pending changes immediately */
|
|
114
|
+
sync: () => Promise<void>;
|
|
115
|
+
/** Error from listener, if any */
|
|
116
|
+
error: Error | undefined;
|
|
117
|
+
/**
|
|
118
|
+
* Firestore collection reference. Undefined when the hook was called with
|
|
119
|
+
* `enabled: false` (no subscription was created).
|
|
120
|
+
*/
|
|
121
|
+
ref: CollectionReference<T> | undefined;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* An undo/redo action
|
|
125
|
+
*/
|
|
126
|
+
interface UndoAction {
|
|
127
|
+
/** Function to undo the change */
|
|
128
|
+
undo: () => Promise<void> | void;
|
|
129
|
+
/** Function to redo the change */
|
|
130
|
+
redo: () => Promise<void> | void;
|
|
131
|
+
/** Optional group ID for batching multiple actions */
|
|
132
|
+
groupId?: string;
|
|
133
|
+
/** Optional path/location context for navigation-aware undo */
|
|
134
|
+
path?: string;
|
|
135
|
+
/** Human-readable description of the action */
|
|
136
|
+
description?: string;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Undo manager state
|
|
140
|
+
*/
|
|
141
|
+
interface UndoManagerState {
|
|
142
|
+
/** Stack of actions that can be undone */
|
|
143
|
+
undoStack: readonly UndoAction[];
|
|
144
|
+
/** Stack of actions that can be redone */
|
|
145
|
+
redoStack: readonly UndoAction[];
|
|
146
|
+
/** Whether undo is available */
|
|
147
|
+
canUndo: boolean;
|
|
148
|
+
/** Whether redo is available */
|
|
149
|
+
canRedo: boolean;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Undo manager handle
|
|
153
|
+
*/
|
|
154
|
+
interface UndoManager extends UndoManagerState {
|
|
155
|
+
/** Perform undo */
|
|
156
|
+
undo: () => Promise<void>;
|
|
157
|
+
/** Perform redo */
|
|
158
|
+
redo: () => Promise<void>;
|
|
159
|
+
/** Push a new action onto the undo stack */
|
|
160
|
+
push: (action: UndoAction) => void;
|
|
161
|
+
/** Clear all undo/redo history */
|
|
162
|
+
clear: () => void;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Configuration for a document definition.
|
|
166
|
+
*
|
|
167
|
+
* `TData` is the document's TypeScript shape. Provide it explicitly, or let
|
|
168
|
+
* it be inferred from `schema` when using `defineDocument`.
|
|
169
|
+
*/
|
|
170
|
+
interface DocumentDefinition<TData extends FirestoreObject> {
|
|
171
|
+
/**
|
|
172
|
+
* Optional Zod schema. When provided, firestate runs `schema.parse(...)`
|
|
173
|
+
* on full-payload writes (`set`, `add`) as a **validation guard** — bad
|
|
174
|
+
* data throws at the call site, not after a Firestore round trip. The
|
|
175
|
+
* parsed result is discarded; firestate stores the caller's original
|
|
176
|
+
* object verbatim. That means schema transforms (`.transform`, `.coerce`,
|
|
177
|
+
* default values) are NOT applied to stored data — do transforms before
|
|
178
|
+
* calling `set`/`add`. Partial `update(diff)` calls are NOT validated
|
|
179
|
+
* because diffs commonly contain Firestore sentinels (`serverTimestamp()`,
|
|
180
|
+
* `arrayUnion`, etc.) that don't satisfy a strict schema.
|
|
181
|
+
*/
|
|
182
|
+
schema?: ZodType<TData>;
|
|
183
|
+
/**
|
|
184
|
+
* Collection path. Either a static string (may include multiple `/`-
|
|
185
|
+
* separated segments) or a function that derives the path from route/
|
|
186
|
+
* params. Use the function form when the collection lives under a dynamic
|
|
187
|
+
* parent, e.g. `projects/{projectId}/revisions`.
|
|
188
|
+
*/
|
|
189
|
+
collection: string | ((params: Record<string, string>) => string);
|
|
190
|
+
/** Document ID or function to derive it */
|
|
191
|
+
id: string | ((params: Record<string, string>) => string);
|
|
192
|
+
/** Debounce interval for autosave (ms), default 1000 */
|
|
193
|
+
autosave?: number;
|
|
194
|
+
/** Minimum loading indicator time (ms), default 0 */
|
|
195
|
+
minLoadTime?: number;
|
|
196
|
+
/** Whether this document is read-only */
|
|
197
|
+
readOnly?: boolean;
|
|
198
|
+
/** Retry on listener error */
|
|
199
|
+
retryOnError?: boolean;
|
|
200
|
+
/** Retry interval (ms), default 5000 */
|
|
201
|
+
retryInterval?: number;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Configuration for a collection definition.
|
|
205
|
+
*
|
|
206
|
+
* `TData` is the document shape for entries in this collection.
|
|
207
|
+
*/
|
|
208
|
+
interface CollectionDefinition<TData extends FirestoreObject> {
|
|
209
|
+
/**
|
|
210
|
+
* Optional Zod schema for documents in the collection. When provided,
|
|
211
|
+
* firestate runs `schema.parse(...)` on full-payload writes (`add`) as
|
|
212
|
+
* a validation guard and stores the caller's original object verbatim.
|
|
213
|
+
* Schema transforms are not applied to stored data — see
|
|
214
|
+
* {@link DocumentDefinition.schema} for the full contract.
|
|
215
|
+
*/
|
|
216
|
+
schema?: ZodType<TData>;
|
|
217
|
+
/** Collection path (can include path segments) */
|
|
218
|
+
path: string | ((params: Record<string, string>) => string);
|
|
219
|
+
/** Debounce interval for autosave (ms), default 1000 */
|
|
220
|
+
autosave?: number;
|
|
221
|
+
/** Minimum loading indicator time (ms), default 0 */
|
|
222
|
+
minLoadTime?: number;
|
|
223
|
+
/** Whether this collection is read-only */
|
|
224
|
+
readOnly?: boolean;
|
|
225
|
+
/** Whether to lazy load (only subscribe when load() is called) */
|
|
226
|
+
lazy?: boolean;
|
|
227
|
+
/** Query constraints */
|
|
228
|
+
queryConstraints?: QueryConstraint[];
|
|
229
|
+
/** Retry the snapshot listener on transient errors */
|
|
230
|
+
retryOnError?: boolean;
|
|
231
|
+
/** Retry interval (ms), default 5000 */
|
|
232
|
+
retryInterval?: number;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Configuration for the Firestate store
|
|
236
|
+
*/
|
|
237
|
+
interface FirestateConfig {
|
|
238
|
+
/** Firestore instance */
|
|
239
|
+
firestore: Firestore;
|
|
240
|
+
/** Default autosave interval (ms), default 1000 */
|
|
241
|
+
autosave?: number;
|
|
242
|
+
/** Default minimum load time (ms), default 0 */
|
|
243
|
+
minLoadTime?: number;
|
|
244
|
+
/** Maximum undo stack length, default 20 */
|
|
245
|
+
maxUndoLength?: number;
|
|
246
|
+
/** Enable navigation-aware undo/redo */
|
|
247
|
+
enableNavigation?: boolean;
|
|
248
|
+
/** Custom error handler */
|
|
249
|
+
onError?: (error: Error, context: ErrorContext) => void;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Context for error handling
|
|
253
|
+
*/
|
|
254
|
+
interface ErrorContext {
|
|
255
|
+
type: "document" | "collection";
|
|
256
|
+
path: string;
|
|
257
|
+
operation: "read" | "write";
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Subscriber callback type
|
|
261
|
+
*/
|
|
262
|
+
type Subscriber<T> = (state: T) => void;
|
|
263
|
+
/**
|
|
264
|
+
* Unsubscribe function
|
|
265
|
+
*/
|
|
266
|
+
type Unsubscribe = () => void;
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/schema.d.ts
|
|
269
|
+
/**
|
|
270
|
+
* Define a typed document. `TData` is the document's TypeScript shape.
|
|
271
|
+
*
|
|
272
|
+
* **Most apps should reach for {@link createFirestate} + {@link doc} instead**
|
|
273
|
+
* — that builds a registry of every Firestore thing in one object and
|
|
274
|
+
* generates typed hooks for you. `defineDocument` is the lower-level
|
|
275
|
+
* escape hatch: use it when you need fully custom `collection` / `id`
|
|
276
|
+
* derivation, when you're calling firestate outside React, or when a
|
|
277
|
+
* registry doesn't fit your control flow.
|
|
278
|
+
*
|
|
279
|
+
* Two ways to use:
|
|
280
|
+
*
|
|
281
|
+
* 1. Plain TypeScript type (no schema, no runtime validation):
|
|
282
|
+
* ```ts
|
|
283
|
+
* interface Project { name: string; createdAt: number }
|
|
284
|
+
*
|
|
285
|
+
* const projectDoc = defineDocument<Project>({
|
|
286
|
+
* collection: 'projects',
|
|
287
|
+
* id: (params) => params.projectId,
|
|
288
|
+
* })
|
|
289
|
+
* ```
|
|
290
|
+
*
|
|
291
|
+
* 2. With a Zod schema — `TData` is inferred from `z.infer<S>`. Firestate
|
|
292
|
+
* runs `schema.parse(...)` on full-payload writes (`set`/`add`) so bad
|
|
293
|
+
* data throws at the call site. Partial `update(diff)` calls are not
|
|
294
|
+
* validated (diffs frequently contain Firestore sentinels).
|
|
295
|
+
* ```ts
|
|
296
|
+
* import { z } from 'zod'
|
|
297
|
+
*
|
|
298
|
+
* const ProjectSchema = z.object({ name: z.string(), createdAt: z.number() })
|
|
299
|
+
*
|
|
300
|
+
* const projectDoc = defineDocument({
|
|
301
|
+
* schema: ProjectSchema,
|
|
302
|
+
* collection: 'projects',
|
|
303
|
+
* id: (params) => params.projectId,
|
|
304
|
+
* })
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
declare function defineDocument<S extends ZodType<FirestoreObject>>(definition: Omit<DocumentDefinition<z.infer<S>>, "schema"> & {
|
|
308
|
+
schema: S;
|
|
309
|
+
}): DocumentDefinition<z.infer<S>>;
|
|
310
|
+
declare function defineDocument<TData extends FirestoreObject>(definition: DocumentDefinition<TData>): DocumentDefinition<TData>;
|
|
311
|
+
/**
|
|
312
|
+
* Define a typed collection. `TData` is the shape of each document in the
|
|
313
|
+
* collection. See {@link defineDocument} for the schema/plain-type tradeoff.
|
|
314
|
+
*
|
|
315
|
+
* **Most apps should reach for {@link createFirestate} + {@link col} instead.**
|
|
316
|
+
* `defineCollection` is the escape hatch for fully custom path derivation
|
|
317
|
+
* or non-React usage.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```ts
|
|
321
|
+
* interface Space { name: string; area: number }
|
|
322
|
+
*
|
|
323
|
+
* const spacesCollection = defineCollection<Space>({
|
|
324
|
+
* path: (params) => `projects/${params.projectId}/spaces`,
|
|
325
|
+
* lazy: true,
|
|
326
|
+
* })
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
declare function defineCollection<S extends ZodType<FirestoreObject>>(definition: Omit<CollectionDefinition<z.infer<S>>, "schema"> & {
|
|
330
|
+
schema: S;
|
|
331
|
+
}): CollectionDefinition<z.infer<S>>;
|
|
332
|
+
declare function defineCollection<TData extends FirestoreObject>(definition: CollectionDefinition<TData>): CollectionDefinition<TData>;
|
|
333
|
+
/**
|
|
334
|
+
* Infer the document data type from a {@link DocumentDefinition}.
|
|
335
|
+
*/
|
|
336
|
+
type InferDocumentData<T extends DocumentDefinition<FirestoreObject>> = T extends DocumentDefinition<infer D> ? D : never;
|
|
337
|
+
/**
|
|
338
|
+
* Infer the document data type (with `id` field) from a {@link DocumentDefinition}.
|
|
339
|
+
*/
|
|
340
|
+
type InferDocument<T extends DocumentDefinition<FirestoreObject>> = InferDocumentData<T> & {
|
|
341
|
+
id: string;
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* Infer the document data type from a {@link CollectionDefinition}.
|
|
345
|
+
*/
|
|
346
|
+
type InferCollectionData<T extends CollectionDefinition<FirestoreObject>> = T extends CollectionDefinition<infer D> ? D : never;
|
|
347
|
+
/**
|
|
348
|
+
* Infer the document data type (with `id` field) from a {@link CollectionDefinition}.
|
|
349
|
+
*/
|
|
350
|
+
type InferCollectionDocument<T extends CollectionDefinition<FirestoreObject>> = InferCollectionData<T> & {
|
|
351
|
+
id: string;
|
|
352
|
+
};
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/undo.d.ts
|
|
355
|
+
/**
|
|
356
|
+
* Configuration for creating an undo manager
|
|
357
|
+
*/
|
|
358
|
+
interface UndoManagerConfig {
|
|
359
|
+
/** Maximum number of undo actions to keep, default 20 */
|
|
360
|
+
maxLength?: number;
|
|
361
|
+
/** Callback when navigation is requested (for path-aware undo) */
|
|
362
|
+
onNavigate?: (path: string) => void;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Create an undo manager instance.
|
|
366
|
+
* This is a standalone, framework-agnostic implementation.
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```ts
|
|
370
|
+
* const undoManager = createUndoManager({ maxLength: 10 })
|
|
371
|
+
*
|
|
372
|
+
* undoManager.push({
|
|
373
|
+
* undo: () => restoreOldValue(),
|
|
374
|
+
* redo: () => applyNewValue(),
|
|
375
|
+
* description: 'Update project name',
|
|
376
|
+
* })
|
|
377
|
+
*
|
|
378
|
+
* await undoManager.undo() // Calls restoreOldValue()
|
|
379
|
+
* await undoManager.redo() // Calls applyNewValue()
|
|
380
|
+
* ```
|
|
381
|
+
*/
|
|
382
|
+
declare const createUndoManager: (config?: UndoManagerConfig) => UndoManager & {
|
|
383
|
+
subscribe: (fn: Subscriber<UndoManagerState>) => Unsubscribe;
|
|
384
|
+
getState: () => UndoManagerState;
|
|
385
|
+
};
|
|
386
|
+
/**
|
|
387
|
+
* Type for the undo manager with subscription capability
|
|
388
|
+
*/
|
|
389
|
+
type UndoManagerWithSubscribe = ReturnType<typeof createUndoManager>;
|
|
390
|
+
//#endregion
|
|
391
|
+
//#region src/store.d.ts
|
|
392
|
+
/**
|
|
393
|
+
* Firestate store that holds configuration and shared state
|
|
394
|
+
*/
|
|
395
|
+
interface FirestateStore {
|
|
396
|
+
/** Firestore instance */
|
|
397
|
+
readonly firestore: Firestore;
|
|
398
|
+
/** Undo manager instance */
|
|
399
|
+
readonly undoManager: UndoManagerWithSubscribe;
|
|
400
|
+
/** Default autosave interval (ms) */
|
|
401
|
+
readonly autosave: number;
|
|
402
|
+
/** Default minimum load time (ms) */
|
|
403
|
+
readonly minLoadTime: number;
|
|
404
|
+
/** Report an error */
|
|
405
|
+
reportError: (error: Error, context: ErrorContext) => void;
|
|
406
|
+
/**
|
|
407
|
+
* Replace the error handler at runtime. Used by FirestateProvider to keep
|
|
408
|
+
* the store identity stable when consumers pass an inline `onError`
|
|
409
|
+
* callback that changes reference on every render.
|
|
410
|
+
*/
|
|
411
|
+
setOnError: (handler?: (error: Error, context: ErrorContext) => void) => void;
|
|
412
|
+
/** Subscribe to sync state changes */
|
|
413
|
+
subscribeToSyncState: (fn: Subscriber<boolean>) => Unsubscribe;
|
|
414
|
+
/** Report a document/collection sync state change */
|
|
415
|
+
reportSyncState: (key: string, isSynced: boolean) => void;
|
|
416
|
+
/**
|
|
417
|
+
* Remove a sync-state key. Subscriptions call this on stop() so an
|
|
418
|
+
* unmounted hook does not leave the global isSynced stuck at false.
|
|
419
|
+
*/
|
|
420
|
+
unregisterSyncState: (key: string) => void;
|
|
421
|
+
/** Get whether all tracked resources are synced */
|
|
422
|
+
readonly isSynced: boolean;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Create a Firestate store.
|
|
426
|
+
* This is the central configuration point for your Firestore state management.
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* ```ts
|
|
430
|
+
* import { createStore } from 'firestate'
|
|
431
|
+
* import { db } from './firebase'
|
|
432
|
+
*
|
|
433
|
+
* export const store = createStore({
|
|
434
|
+
* firestore: db,
|
|
435
|
+
* autosave: 1000,
|
|
436
|
+
* maxUndoLength: 20,
|
|
437
|
+
* onError: (error, context) => {
|
|
438
|
+
* console.error(`Error in ${context.type} ${context.path}:`, error)
|
|
439
|
+
* },
|
|
440
|
+
* })
|
|
441
|
+
* ```
|
|
442
|
+
*/
|
|
443
|
+
declare const createStore: (config: FirestateConfig) => FirestateStore;
|
|
444
|
+
/**
|
|
445
|
+
* Type alias for the store type
|
|
446
|
+
*/
|
|
447
|
+
type Store = ReturnType<typeof createStore>;
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/hooks.d.ts
|
|
450
|
+
/**
|
|
451
|
+
* Context for providing the Firestate store
|
|
452
|
+
*/
|
|
453
|
+
declare const FirestateContext: react0.Context<FirestateStore | null>;
|
|
454
|
+
/**
|
|
455
|
+
* Hook to access the Firestate store
|
|
456
|
+
*/
|
|
457
|
+
declare const useStore: () => FirestateStore;
|
|
458
|
+
/**
|
|
459
|
+
* Hook to access the undo manager
|
|
460
|
+
*/
|
|
461
|
+
declare const useUndoManager: () => UndoManager;
|
|
462
|
+
/**
|
|
463
|
+
* Hook to check if all tracked resources are synced
|
|
464
|
+
*/
|
|
465
|
+
declare const useIsSynced: () => boolean;
|
|
466
|
+
/**
|
|
467
|
+
* Options for useDocument hook
|
|
468
|
+
*/
|
|
469
|
+
interface UseDocumentOptions<TData extends FirestoreObject> {
|
|
470
|
+
/** Document definition from defineDocument() */
|
|
471
|
+
definition: DocumentDefinition<TData>;
|
|
472
|
+
/** Route/path parameters for dynamic paths */
|
|
473
|
+
params?: Record<string, string>;
|
|
474
|
+
/** Override read-only setting */
|
|
475
|
+
readOnly?: boolean;
|
|
476
|
+
/** Enable undo/redo for this document (default: true) */
|
|
477
|
+
undoable?: boolean;
|
|
478
|
+
/**
|
|
479
|
+
* If false, no subscription is created and a no-op handle is returned
|
|
480
|
+
* (`{ data: undefined, isLoading: false, isSynced: true, ref: undefined }`).
|
|
481
|
+
* Use this to gate subscriptions on route params that aren't ready yet.
|
|
482
|
+
* Default: true.
|
|
483
|
+
*/
|
|
484
|
+
enabled?: boolean;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Hook to subscribe to a Firestore document with real-time updates.
|
|
488
|
+
*
|
|
489
|
+
* The subscription is keyed on the resolved document path (`definition` +
|
|
490
|
+
* computed id) and `readOnly`. When that key changes — typically because
|
|
491
|
+
* `params` produces a different id — the hook tears down the old Firestore
|
|
492
|
+
* listener and attaches a new one. Toggling `undoable` does not rebuild the
|
|
493
|
+
* subscription.
|
|
494
|
+
*
|
|
495
|
+
* Use `enabled: false` to suppress the subscription entirely (e.g., when
|
|
496
|
+
* route params aren't ready yet).
|
|
497
|
+
*
|
|
498
|
+
* **SSR.** On the server there is no Firestore listener, so this hook returns
|
|
499
|
+
* the initial handle (`{ data: undefined, isLoading: true }`). Mutations like
|
|
500
|
+
* `update`/`set` will mutate orphaned local state with no effect — avoid
|
|
501
|
+
* calling them server-side.
|
|
502
|
+
*
|
|
503
|
+
* @example
|
|
504
|
+
* ```tsx
|
|
505
|
+
* const projectDoc = defineDocument<Project>({
|
|
506
|
+
* collection: 'projects',
|
|
507
|
+
* id: (params) => params.projectId,
|
|
508
|
+
* })
|
|
509
|
+
*
|
|
510
|
+
* function ProjectEditor({ projectId }: { projectId: string }) {
|
|
511
|
+
* const { data, update, isLoading, isSynced } = useDocument({
|
|
512
|
+
* definition: projectDoc,
|
|
513
|
+
* params: { projectId },
|
|
514
|
+
* })
|
|
515
|
+
*
|
|
516
|
+
* if (isLoading) return <Spinner />
|
|
517
|
+
*
|
|
518
|
+
* return (
|
|
519
|
+
* <input
|
|
520
|
+
* value={data?.name ?? ''}
|
|
521
|
+
* onChange={(e) => update({ name: e.target.value })}
|
|
522
|
+
* />
|
|
523
|
+
* )
|
|
524
|
+
* }
|
|
525
|
+
* ```
|
|
526
|
+
*/
|
|
527
|
+
declare const useDocument: <TData extends FirestoreObject>(options: UseDocumentOptions<TData>) => DocumentHandle<TData>;
|
|
528
|
+
/**
|
|
529
|
+
* Options for useCollection hook
|
|
530
|
+
*/
|
|
531
|
+
interface UseCollectionOptions<TData extends FirestoreObject> {
|
|
532
|
+
/** Collection definition from defineCollection() */
|
|
533
|
+
definition: CollectionDefinition<TData>;
|
|
534
|
+
/** Route/path parameters for dynamic paths */
|
|
535
|
+
params?: Record<string, string>;
|
|
536
|
+
/** Override read-only setting */
|
|
537
|
+
readOnly?: boolean;
|
|
538
|
+
/** Additional query constraints */
|
|
539
|
+
queryConstraints?: QueryConstraint[];
|
|
540
|
+
/** Enable undo/redo for this collection (default: true) */
|
|
541
|
+
undoable?: boolean;
|
|
542
|
+
/**
|
|
543
|
+
* If false, no subscription is created and a no-op handle is returned
|
|
544
|
+
* (`{ data: {}, isLoading: false, isActive: false }`). Use this to gate on
|
|
545
|
+
* route params that aren't ready yet. Default: true.
|
|
546
|
+
*/
|
|
547
|
+
enabled?: boolean;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Hook to subscribe to a Firestore collection with real-time updates.
|
|
551
|
+
*
|
|
552
|
+
* The subscription is keyed on the resolved collection path, `readOnly`, and
|
|
553
|
+
* the `queryConstraints` reference. When any of these change, the listener
|
|
554
|
+
* is torn down and re-attached with the new query. Toggling `undoable` does
|
|
555
|
+
* not rebuild the subscription.
|
|
556
|
+
*
|
|
557
|
+
* **Memoize `queryConstraints`.** An inline array (`queryConstraints={[where(...)]}`)
|
|
558
|
+
* creates a new reference every render, which will thrash the listener.
|
|
559
|
+
* Wrap in `useMemo` with the underlying filter values as deps.
|
|
560
|
+
*
|
|
561
|
+
* Use `enabled: false` to suppress the subscription entirely (e.g., when
|
|
562
|
+
* route params aren't ready yet).
|
|
563
|
+
*
|
|
564
|
+
* **SSR.** On the server there is no Firestore listener, so this hook returns
|
|
565
|
+
* the initial handle (`{ data: {}, isLoading: true }` for non-lazy, or
|
|
566
|
+
* `isActive: false` for lazy). Avoid calling mutations server-side.
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* ```tsx
|
|
570
|
+
* const spacesCollection = defineCollection<Space>({
|
|
571
|
+
* path: (params) => `projects/${params.projectId}/spaces`,
|
|
572
|
+
* lazy: true,
|
|
573
|
+
* })
|
|
574
|
+
*
|
|
575
|
+
* function SpacesList({ projectId }: { projectId: string }) {
|
|
576
|
+
* const { data, update, load, isActive, isLoading } = useCollection({
|
|
577
|
+
* definition: spacesCollection,
|
|
578
|
+
* params: { projectId },
|
|
579
|
+
* })
|
|
580
|
+
*
|
|
581
|
+
* // Lazy load on mount
|
|
582
|
+
* useEffect(() => { load() }, [load])
|
|
583
|
+
*
|
|
584
|
+
* if (!isActive) return <Button onClick={load}>Load Spaces</Button>
|
|
585
|
+
* if (isLoading) return <Spinner />
|
|
586
|
+
*
|
|
587
|
+
* return (
|
|
588
|
+
* <ul>
|
|
589
|
+
* {Object.values(data).map((space) => (
|
|
590
|
+
* <li key={space.id}>{space.name}</li>
|
|
591
|
+
* ))}
|
|
592
|
+
* </ul>
|
|
593
|
+
* )
|
|
594
|
+
* }
|
|
595
|
+
* ```
|
|
596
|
+
*/
|
|
597
|
+
declare const useCollection: <TData extends FirestoreObject>(options: UseCollectionOptions<TData>) => CollectionHandle<TData>;
|
|
598
|
+
/**
|
|
599
|
+
* Keyboard shortcut hook for undo/redo
|
|
600
|
+
*
|
|
601
|
+
* @example
|
|
602
|
+
* ```tsx
|
|
603
|
+
* function App() {
|
|
604
|
+
* useUndoKeyboardShortcuts()
|
|
605
|
+
* return <YourApp />
|
|
606
|
+
* }
|
|
607
|
+
* ```
|
|
608
|
+
*/
|
|
609
|
+
declare const useUndoKeyboardShortcuts: () => void;
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region src/firestate.d.ts
|
|
612
|
+
/**
|
|
613
|
+
* Knobs forwarded from a generated document hook to {@link useDocument}.
|
|
614
|
+
* Same shape as `UseDocumentOptions` minus the fields the registry already
|
|
615
|
+
* owns (`definition`, `params`).
|
|
616
|
+
*/
|
|
617
|
+
type DocHookOptions<T extends FirestoreObject> = Omit<UseDocumentOptions<T>, "definition" | "params">;
|
|
618
|
+
/**
|
|
619
|
+
* Knobs forwarded from a generated collection hook to {@link useCollection}.
|
|
620
|
+
*/
|
|
621
|
+
type ColHookOptions<T extends FirestoreObject> = Omit<UseCollectionOptions<T>, "definition" | "params">;
|
|
622
|
+
interface CommonEntryOptions {
|
|
623
|
+
/** Debounce interval for autosave (ms). */
|
|
624
|
+
autosave?: number;
|
|
625
|
+
/** Minimum loading indicator time (ms). */
|
|
626
|
+
minLoadTime?: number;
|
|
627
|
+
/** Whether this entry is read-only. */
|
|
628
|
+
readOnly?: boolean;
|
|
629
|
+
/** Retry the snapshot listener on transient errors. */
|
|
630
|
+
retryOnError?: boolean;
|
|
631
|
+
/** Retry interval (ms). */
|
|
632
|
+
retryInterval?: number;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Document entry in a Firestate registry. Produced by {@link doc}.
|
|
636
|
+
*
|
|
637
|
+
* The `P` generic carries the path template's string-literal type so the
|
|
638
|
+
* generated hook can type-check param keys. `__kind` is a runtime
|
|
639
|
+
* discriminator; `__type` is a phantom field used purely for inference at
|
|
640
|
+
* the call site and is never read.
|
|
641
|
+
*/
|
|
642
|
+
interface DocEntry<T extends FirestoreObject, P extends string = string> extends CommonEntryOptions {
|
|
643
|
+
readonly __kind: "document";
|
|
644
|
+
readonly __type?: T;
|
|
645
|
+
/** Path template, e.g. `'taskLists/{listId}'`. */
|
|
646
|
+
path: P;
|
|
647
|
+
/**
|
|
648
|
+
* Zod schema. **Required** — firestate's registry API is opinionated
|
|
649
|
+
* about Zod. The schema is the source of `T` for the generated hooks
|
|
650
|
+
* via `z.infer`, and firestate runs `schema.parse(...)` on full-payload
|
|
651
|
+
* writes (`set`/`add`) so bad data throws at the call site rather than
|
|
652
|
+
* after a Firestore round trip. Partial `update(diff)` is NOT validated
|
|
653
|
+
* (diffs frequently contain Firestore sentinels like `serverTimestamp()`).
|
|
654
|
+
*
|
|
655
|
+
* If you don't want a schema at all, use {@link defineDocument} directly —
|
|
656
|
+
* the escape hatch keeps the plain-TypeScript form at the cost of looser
|
|
657
|
+
* param typing and no runtime validation.
|
|
658
|
+
*/
|
|
659
|
+
schema: ZodType<T>;
|
|
660
|
+
}
|
|
661
|
+
/** Collection entry in a Firestate registry. Produced by {@link col}. */
|
|
662
|
+
interface ColEntry<T extends FirestoreObject, P extends string = string> extends CommonEntryOptions {
|
|
663
|
+
readonly __kind: "collection";
|
|
664
|
+
readonly __type?: T;
|
|
665
|
+
/** Path template, e.g. `'taskLists/{listId}/tasks'`. */
|
|
666
|
+
path: P;
|
|
667
|
+
/** Zod schema. Required. See {@link DocEntry.schema}. */
|
|
668
|
+
schema: ZodType<T>;
|
|
669
|
+
/** Only subscribe when `load()` is called. */
|
|
670
|
+
lazy?: boolean;
|
|
671
|
+
/** Additional Firestore query constraints. */
|
|
672
|
+
queryConstraints?: QueryConstraint[];
|
|
673
|
+
}
|
|
674
|
+
type FirestateEntry<T extends FirestoreObject = FirestoreObject, P extends string = string> = DocEntry<T, P> | ColEntry<T, P>;
|
|
675
|
+
type FirestateRegistry = Record<string, FirestateEntry<any, any>>;
|
|
676
|
+
/**
|
|
677
|
+
* Extract `{name}` placeholders from a path template into a params shape.
|
|
678
|
+
*
|
|
679
|
+
* - `'users'` → `{}`
|
|
680
|
+
* - `'users/{userId}'` → `{ userId: string }`
|
|
681
|
+
* - `'projects/{projectId}/revisions/{revisionId}'` → `{ projectId: string; revisionId: string }`
|
|
682
|
+
*
|
|
683
|
+
* When the path is widened to `string` (no literal preserved), we fall
|
|
684
|
+
* back to `Record<string, string>` so existing call sites keep compiling.
|
|
685
|
+
*/
|
|
686
|
+
type ParamsOf<P extends string> = string extends P ? Record<string, string> : Prettify<RawParamsOf<P>>;
|
|
687
|
+
type RawParamsOf<P extends string> = P extends `${string}{${infer K}}${infer Rest}` ? { [Key in K]: string } & RawParamsOf<Rest> : {};
|
|
688
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
689
|
+
type DocOpts<T extends FirestoreObject> = Omit<DocEntry<T>, "__kind" | "__type" | "path">;
|
|
690
|
+
type ColOpts<T extends FirestoreObject> = Omit<ColEntry<T>, "__kind" | "__type" | "path">;
|
|
691
|
+
/**
|
|
692
|
+
* Declare a single-document entry for a Firestate registry.
|
|
693
|
+
*
|
|
694
|
+
* **A Zod `schema` field is required.** Both the data type (`T`) and the
|
|
695
|
+
* path's literal type (`P`) are inferred from the call — `T` via
|
|
696
|
+
* `z.infer<S>`, `P` from `path` — so the generated hook can statically
|
|
697
|
+
* type-check the params object the caller passes. The schema also runs
|
|
698
|
+
* at runtime on full-payload writes (`set`/`add`).
|
|
699
|
+
*
|
|
700
|
+
* If you'd rather not provide a schema at all, use {@link defineDocument}
|
|
701
|
+
* directly — that escape hatch keeps the plain-TypeScript form, at the
|
|
702
|
+
* cost of looser param typing on the hook and no runtime validation.
|
|
703
|
+
*
|
|
704
|
+
* ```ts
|
|
705
|
+
* import { z } from 'zod'
|
|
706
|
+
*
|
|
707
|
+
* const TaskListSchema = z.object({ name: z.string(), createdAt: z.number() })
|
|
708
|
+
* doc({ path: 'taskLists/{listId}', schema: TaskListSchema })
|
|
709
|
+
* // → DocEntry<{ name: string; createdAt: number }, 'taskLists/{listId}'>
|
|
710
|
+
* ```
|
|
711
|
+
*/
|
|
712
|
+
declare function doc<S extends ZodType<FirestoreObject>, const P extends string = string>(opts: Omit<DocOpts<z.infer<S>>, "schema"> & {
|
|
713
|
+
schema: S;
|
|
714
|
+
path: P;
|
|
715
|
+
}): DocEntry<z.infer<S>, P>;
|
|
716
|
+
/**
|
|
717
|
+
* Declare a collection entry for a Firestate registry. See {@link doc}
|
|
718
|
+
* for the schema/typing contract.
|
|
719
|
+
*/
|
|
720
|
+
declare function col<S extends ZodType<FirestoreObject>, const P extends string = string>(opts: Omit<ColOpts<z.infer<S>>, "schema"> & {
|
|
721
|
+
schema: S;
|
|
722
|
+
path: P;
|
|
723
|
+
}): ColEntry<z.infer<S>, P>;
|
|
724
|
+
type HookName<K extends string> = `use${Capitalize<K>}`;
|
|
725
|
+
type HookFor<E> = E extends DocEntry<infer T, infer P> ? keyof ParamsOf<P> extends never ? (params?: Record<string, string>, options?: DocHookOptions<T>) => DocumentHandle<T> : (params: ParamsOf<P>, options?: DocHookOptions<T>) => DocumentHandle<T> : E extends ColEntry<infer T, infer P> ? keyof ParamsOf<P> extends never ? (params?: Record<string, string>, options?: ColHookOptions<T>) => CollectionHandle<T> : (params: ParamsOf<P>, options?: ColHookOptions<T>) => CollectionHandle<T> : never;
|
|
726
|
+
type FirestateApi<R extends FirestateRegistry> = { [K in keyof R & string as HookName<K>]: HookFor<R[K]> };
|
|
727
|
+
/**
|
|
728
|
+
* Turn a Firestate registry into a map of typed React hooks. Each entry
|
|
729
|
+
* `K` produces a hook named `use{Capitalize<K>}`.
|
|
730
|
+
*
|
|
731
|
+
* ```ts
|
|
732
|
+
* export const { useTaskList, useTasks } = createFirestate({
|
|
733
|
+
* taskList: doc<TaskList>('taskLists/{listId}'),
|
|
734
|
+
* tasks: col<Task>('taskLists/{listId}/tasks'),
|
|
735
|
+
* })
|
|
736
|
+
* ```
|
|
737
|
+
*/
|
|
738
|
+
declare function createFirestate<R extends FirestateRegistry>(registry: R): FirestateApi<R>;
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region src/diff.d.ts
|
|
741
|
+
/**
|
|
742
|
+
* Check if two values are deeply equal
|
|
743
|
+
*/
|
|
744
|
+
declare const isDeepEqual: (a: unknown, b: unknown) => boolean;
|
|
745
|
+
/**
|
|
746
|
+
* Compute the minimal diff between two objects for Firestore updates.
|
|
747
|
+
* Returns only the fields that changed, using deleteField() for removed fields.
|
|
748
|
+
*
|
|
749
|
+
* @param from - The original object (sync state)
|
|
750
|
+
* @param to - The target object (local state)
|
|
751
|
+
* @returns A partial object containing only changed fields
|
|
752
|
+
*/
|
|
753
|
+
declare const computeDiff: <T extends FirestoreObject>(from: T, to: T | undefined) => WithFieldValue<DeepPartial<T>>;
|
|
754
|
+
/**
|
|
755
|
+
* Apply a Firestore diff to a target object in place (mutating).
|
|
756
|
+
* Handles deleteField(), serverTimestamp(), and nested objects.
|
|
757
|
+
*
|
|
758
|
+
* Most code should use `applyDiff` (immutable) instead.
|
|
759
|
+
* This mutable version is useful for performance-critical paths
|
|
760
|
+
* where you're already working with a cloned object.
|
|
761
|
+
*
|
|
762
|
+
* @param target - The object to mutate
|
|
763
|
+
* @param diff - The diff to apply
|
|
764
|
+
*/
|
|
765
|
+
declare const applyDiffMutable: (target: FirestoreObject, diff: Record<string, unknown>) => void;
|
|
766
|
+
/**
|
|
767
|
+
* Create a deep clone of an object that's safe for Firestore operations.
|
|
768
|
+
*
|
|
769
|
+
* Firestore opaque values (FieldValue sentinels, Timestamp,
|
|
770
|
+
* DocumentReference, GeoPoint, Bytes, VectorValue) are returned **by
|
|
771
|
+
* reference**. They are immutable from the user's perspective; cloning
|
|
772
|
+
* them by walking keys would either lose their prototype — turning a
|
|
773
|
+
* `DocumentReference` into a plain object Firestore can't recognize —
|
|
774
|
+
* or destroy a sentinel that needed to reach the server intact.
|
|
775
|
+
*/
|
|
776
|
+
declare const deepClone: <T>(value: T) => T;
|
|
777
|
+
/**
|
|
778
|
+
* Check if a diff is empty (no changes)
|
|
779
|
+
*/
|
|
780
|
+
declare const isDiffEmpty: (diff: Record<string, unknown>) => boolean;
|
|
781
|
+
/**
|
|
782
|
+
* Flatten a nested diff object to dot notation for use with Firestore's updateDoc.
|
|
783
|
+
*
|
|
784
|
+
* This converts:
|
|
785
|
+
* ```
|
|
786
|
+
* { building: { floors: 5, height: 100 }, name: 'Test' }
|
|
787
|
+
* ```
|
|
788
|
+
* To:
|
|
789
|
+
* ```
|
|
790
|
+
* { 'building.floors': 5, 'building.height': 100, 'name': 'Test' }
|
|
791
|
+
* ```
|
|
792
|
+
*
|
|
793
|
+
* Arrays, FieldValue sentinels (deleteField, serverTimestamp, …) and
|
|
794
|
+
* Firestore value types (Timestamp, DocumentReference, GeoPoint, Bytes,
|
|
795
|
+
* VectorValue) are NOT flattened — they're preserved at their path so
|
|
796
|
+
* Firestore receives them in their original form.
|
|
797
|
+
*
|
|
798
|
+
* @param diff - The nested diff object
|
|
799
|
+
* @param prefix - Internal: current path prefix for recursion
|
|
800
|
+
* @returns Flattened object with dotted keys
|
|
801
|
+
*/
|
|
802
|
+
declare const flattenDiff: (diff: Record<string, unknown>, prefix?: string) => Record<string, unknown>;
|
|
803
|
+
/**
|
|
804
|
+
* Merge two diffs together, with the second taking precedence
|
|
805
|
+
*/
|
|
806
|
+
declare const mergeDiffs: <T extends FirestoreObject>(first: WithFieldValue<DeepPartial<T>>, second: WithFieldValue<DeepPartial<T>>) => WithFieldValue<DeepPartial<T>>;
|
|
807
|
+
/**
|
|
808
|
+
* Apply a diff to an object, returning a new object.
|
|
809
|
+
* The original object is not modified.
|
|
810
|
+
*
|
|
811
|
+
* @example
|
|
812
|
+
* ```ts
|
|
813
|
+
* const original = { name: 'Project', count: 5 }
|
|
814
|
+
* const diff = { name: 'Updated', count: deleteField() }
|
|
815
|
+
* const result = applyDiff(original, diff)
|
|
816
|
+
* // result = { name: 'Updated' }
|
|
817
|
+
* // original is unchanged
|
|
818
|
+
* ```
|
|
819
|
+
*/
|
|
820
|
+
declare const applyDiff: <T extends FirestoreObject>(state: T, diff: WithFieldValue<DeepPartial<T>>) => T;
|
|
821
|
+
/**
|
|
822
|
+
* Compute the undo diff that would reverse the effect of applying a diff to a state.
|
|
823
|
+
*
|
|
824
|
+
* Given a starting state and a diff that was (or will be) applied to it,
|
|
825
|
+
* returns a new diff that when applied to the result would restore the original state.
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* ```ts
|
|
829
|
+
* const startState = { name: 'Foo', count: 5 }
|
|
830
|
+
* const diff = { name: 'Bar', count: deleteField() }
|
|
831
|
+
*
|
|
832
|
+
* // Apply the diff
|
|
833
|
+
* const endState = applyDiff(startState, diff)
|
|
834
|
+
* // endState = { name: 'Bar' }
|
|
835
|
+
*
|
|
836
|
+
* // Compute the undo
|
|
837
|
+
* const undoDiff = computeUndoDiff(startState, diff)
|
|
838
|
+
* // undoDiff = { name: 'Foo', count: 5 }
|
|
839
|
+
*
|
|
840
|
+
* // Applying undoDiff to endState restores startState
|
|
841
|
+
* const restored = applyDiff(endState, undoDiff)
|
|
842
|
+
* // restored = { name: 'Foo', count: 5 }
|
|
843
|
+
* ```
|
|
844
|
+
*/
|
|
845
|
+
declare const computeUndoDiff: <T extends FirestoreObject>(startState: T, diff: WithFieldValue<DeepPartial<T>>) => WithFieldValue<DeepPartial<T>>;
|
|
846
|
+
/**
|
|
847
|
+
* Check if a diff affects a specific path (supports dot notation).
|
|
848
|
+
*
|
|
849
|
+
* @example
|
|
850
|
+
* ```ts
|
|
851
|
+
* const diff = { building: { floors: 5 }, name: 'Test' }
|
|
852
|
+
*
|
|
853
|
+
* diffContainsPath(diff, 'name') // true
|
|
854
|
+
* diffContainsPath(diff, 'building') // true
|
|
855
|
+
* diffContainsPath(diff, 'building.floors') // true
|
|
856
|
+
* diffContainsPath(diff, 'building.height') // false
|
|
857
|
+
* diffContainsPath(diff, 'other') // false
|
|
858
|
+
* ```
|
|
859
|
+
*/
|
|
860
|
+
declare const diffContainsPath: (diff: Record<string, unknown>, path: string) => boolean;
|
|
861
|
+
/**
|
|
862
|
+
* Extract the value at a specific path from a diff (supports dot notation).
|
|
863
|
+
* Returns undefined if the path doesn't exist in the diff.
|
|
864
|
+
*
|
|
865
|
+
* @example
|
|
866
|
+
* ```ts
|
|
867
|
+
* const diff = { building: { floors: 5, height: 100 }, name: 'Test' }
|
|
868
|
+
*
|
|
869
|
+
* extractDiffValue(diff, 'name') // 'Test'
|
|
870
|
+
* extractDiffValue(diff, 'building') // { floors: 5, height: 100 }
|
|
871
|
+
* extractDiffValue(diff, 'building.floors') // 5
|
|
872
|
+
* extractDiffValue(diff, 'building.missing') // undefined
|
|
873
|
+
* ```
|
|
874
|
+
*/
|
|
875
|
+
declare const extractDiffValue: (diff: Record<string, unknown>, path: string) => unknown;
|
|
876
|
+
/**
|
|
877
|
+
* Create a diff that sets a value at a specific path (supports dot notation).
|
|
878
|
+
*
|
|
879
|
+
* @example
|
|
880
|
+
* ```ts
|
|
881
|
+
* createDiffAtPath('name', 'New Name')
|
|
882
|
+
* // { name: 'New Name' }
|
|
883
|
+
*
|
|
884
|
+
* createDiffAtPath('building.floors', 5)
|
|
885
|
+
* // { building: { floors: 5 } }
|
|
886
|
+
*
|
|
887
|
+
* createDiffAtPath('building.config.enabled', true)
|
|
888
|
+
* // { building: { config: { enabled: true } } }
|
|
889
|
+
* ```
|
|
890
|
+
*/
|
|
891
|
+
declare const createDiffAtPath: (path: string, value: unknown) => Record<string, unknown>;
|
|
892
|
+
/**
|
|
893
|
+
* Invert a flattened diff back to nested object structure.
|
|
894
|
+
* Opposite of flattenDiff.
|
|
895
|
+
*
|
|
896
|
+
* @example
|
|
897
|
+
* ```ts
|
|
898
|
+
* const flat = { 'building.floors': 5, 'building.height': 100, 'name': 'Test' }
|
|
899
|
+
* const nested = unflattenDiff(flat)
|
|
900
|
+
* // { building: { floors: 5, height: 100 }, name: 'Test' }
|
|
901
|
+
* ```
|
|
902
|
+
*/
|
|
903
|
+
declare const unflattenDiff: (flatDiff: Record<string, unknown>) => Record<string, unknown>;
|
|
904
|
+
//#endregion
|
|
905
|
+
//#region src/document.d.ts
|
|
906
|
+
/**
|
|
907
|
+
* Options for creating a document subscription
|
|
908
|
+
*/
|
|
909
|
+
interface DocumentOptions<TData extends FirestoreObject> {
|
|
910
|
+
/** The store instance */
|
|
911
|
+
store: FirestateStore;
|
|
912
|
+
/** Document definition from defineDocument() */
|
|
913
|
+
definition: DocumentDefinition<TData>;
|
|
914
|
+
/**
|
|
915
|
+
* Resolved document id. If omitted and `definition.id` is a string, that
|
|
916
|
+
* value is used. If `definition.id` is a function, this option is required.
|
|
917
|
+
*/
|
|
918
|
+
docId?: string;
|
|
919
|
+
/**
|
|
920
|
+
* Resolved collection path. If omitted and `definition.collection` is a
|
|
921
|
+
* string, that value is used. If `definition.collection` is a function,
|
|
922
|
+
* this option is required.
|
|
923
|
+
*/
|
|
924
|
+
collectionPath?: string;
|
|
925
|
+
/** Override read-only setting */
|
|
926
|
+
readOnly?: boolean;
|
|
927
|
+
/** Callback for pushing undo actions */
|
|
928
|
+
onPushUndo?: (undoAction: () => void, redoAction: () => void, options?: UpdateOptions) => void;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Create a document subscription.
|
|
932
|
+
* This is a low-level API - prefer using useDocument hook in React.
|
|
933
|
+
*
|
|
934
|
+
* @example
|
|
935
|
+
* ```ts
|
|
936
|
+
* const subscription = createDocumentSubscription({
|
|
937
|
+
* store,
|
|
938
|
+
* definition: projectDoc,
|
|
939
|
+
* docId: '123',
|
|
940
|
+
* })
|
|
941
|
+
*
|
|
942
|
+
* const unsubscribe = subscription.subscribe((state) => {
|
|
943
|
+
* console.log('Document state:', state)
|
|
944
|
+
* })
|
|
945
|
+
*
|
|
946
|
+
* subscription.load()
|
|
947
|
+
* ```
|
|
948
|
+
*/
|
|
949
|
+
declare const createDocumentSubscription: <TData extends FirestoreObject>(options: DocumentOptions<TData>) => {
|
|
950
|
+
/** Attach the Firestore listener */load: () => void; /** Stop the Firestore listener */
|
|
951
|
+
stop: () => void; /** Subscribe to state changes */
|
|
952
|
+
subscribe: (fn: Subscriber<DocumentState<TData>>) => Unsubscribe; /** Get current state */
|
|
953
|
+
getState: () => DocumentState<TData>; /** Get document handle for updates */
|
|
954
|
+
getHandle: () => DocumentHandle<TData>; /** Force sync now */
|
|
955
|
+
sync: () => Promise<void>;
|
|
956
|
+
};
|
|
957
|
+
//#endregion
|
|
958
|
+
//#region src/collection.d.ts
|
|
959
|
+
/**
|
|
960
|
+
* Options for creating a collection subscription
|
|
961
|
+
*/
|
|
962
|
+
interface CollectionOptions<TData extends FirestoreObject> {
|
|
963
|
+
/** The store instance */
|
|
964
|
+
store: FirestateStore;
|
|
965
|
+
/** Collection definition from defineCollection() */
|
|
966
|
+
definition: CollectionDefinition<TData>;
|
|
967
|
+
/**
|
|
968
|
+
* Resolved collection path. If omitted and `definition.path` is a string,
|
|
969
|
+
* that value is used. If `definition.path` is a function, this option is
|
|
970
|
+
* required.
|
|
971
|
+
*/
|
|
972
|
+
collectionPath?: string;
|
|
973
|
+
/** Override read-only setting */
|
|
974
|
+
readOnly?: boolean;
|
|
975
|
+
/** Additional query constraints */
|
|
976
|
+
queryConstraints?: QueryConstraint[];
|
|
977
|
+
/** Callback for pushing undo actions */
|
|
978
|
+
onPushUndo?: (undoAction: () => void, redoAction: () => void, options?: UpdateOptions) => void;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Create a collection subscription.
|
|
982
|
+
* This is a low-level API - prefer using useCollection hook in React.
|
|
983
|
+
*
|
|
984
|
+
* @example
|
|
985
|
+
* ```ts
|
|
986
|
+
* const subscription = createCollectionSubscription({
|
|
987
|
+
* store,
|
|
988
|
+
* definition: spacesCollection,
|
|
989
|
+
* collectionPath: 'projects/123/spaces',
|
|
990
|
+
* })
|
|
991
|
+
*
|
|
992
|
+
* const unsubscribe = subscription.subscribe((state) => {
|
|
993
|
+
* console.log('Collection state:', state)
|
|
994
|
+
* })
|
|
995
|
+
*
|
|
996
|
+
* subscription.load() // For lazy collections
|
|
997
|
+
* ```
|
|
998
|
+
*/
|
|
999
|
+
declare const createCollectionSubscription: <TData extends FirestoreObject>(options: CollectionOptions<TData>) => {
|
|
1000
|
+
/** Activate the subscription (for lazy loading) */load: () => void; /** Stop the Firestore listener */
|
|
1001
|
+
stop: () => void; /** Subscribe to state changes */
|
|
1002
|
+
subscribe: (fn: Subscriber<CollectionState<TData>>) => Unsubscribe; /** Get current state */
|
|
1003
|
+
getState: () => CollectionState<TData>; /** Get collection handle for updates */
|
|
1004
|
+
getHandle: () => CollectionHandle<TData>; /** Force sync now */
|
|
1005
|
+
sync: () => Promise<void>;
|
|
1006
|
+
};
|
|
1007
|
+
//#endregion
|
|
1008
|
+
//#region src/provider.d.ts
|
|
1009
|
+
/**
|
|
1010
|
+
* Props for FirestateProvider
|
|
1011
|
+
*/
|
|
1012
|
+
interface FirestateProviderProps {
|
|
1013
|
+
/** Firestore instance */
|
|
1014
|
+
firestore: Firestore;
|
|
1015
|
+
/** Default autosave interval (ms), default 1000 */
|
|
1016
|
+
autosave?: number;
|
|
1017
|
+
/** Default minimum load time (ms), default 0 */
|
|
1018
|
+
minLoadTime?: number;
|
|
1019
|
+
/** Maximum undo stack length, default 20 */
|
|
1020
|
+
maxUndoLength?: number;
|
|
1021
|
+
/** Custom error handler */
|
|
1022
|
+
onError?: (error: Error, context: ErrorContext) => void;
|
|
1023
|
+
/** React children */
|
|
1024
|
+
children: React.ReactNode;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Provider component that sets up Firestate for your application.
|
|
1028
|
+
*
|
|
1029
|
+
* @example
|
|
1030
|
+
* ```tsx
|
|
1031
|
+
* import { FirestateProvider } from 'firestate'
|
|
1032
|
+
* import { db } from './firebase'
|
|
1033
|
+
*
|
|
1034
|
+
* function App() {
|
|
1035
|
+
* return (
|
|
1036
|
+
* <FirestateProvider
|
|
1037
|
+
* firestore={db}
|
|
1038
|
+
* autosave={1000}
|
|
1039
|
+
* maxUndoLength={20}
|
|
1040
|
+
* onError={(error, ctx) => console.error(ctx.path, error)}
|
|
1041
|
+
* >
|
|
1042
|
+
* <YourApp />
|
|
1043
|
+
* </FirestateProvider>
|
|
1044
|
+
* )
|
|
1045
|
+
* }
|
|
1046
|
+
* ```
|
|
1047
|
+
*/
|
|
1048
|
+
declare const FirestateProvider: React.FC<FirestateProviderProps>;
|
|
1049
|
+
/**
|
|
1050
|
+
* Props for using an existing store
|
|
1051
|
+
*/
|
|
1052
|
+
interface FirestateStoreProviderProps {
|
|
1053
|
+
/** Pre-created store instance */
|
|
1054
|
+
store: FirestateStore;
|
|
1055
|
+
/** React children */
|
|
1056
|
+
children: React.ReactNode;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Provider that uses an existing store instance.
|
|
1060
|
+
* Useful when you need to create the store outside of React.
|
|
1061
|
+
*
|
|
1062
|
+
* @example
|
|
1063
|
+
* ```tsx
|
|
1064
|
+
* const store = createStore({ firestore: db })
|
|
1065
|
+
*
|
|
1066
|
+
* function App() {
|
|
1067
|
+
* return (
|
|
1068
|
+
* <FirestateStoreProvider store={store}>
|
|
1069
|
+
* <YourApp />
|
|
1070
|
+
* </FirestateStoreProvider>
|
|
1071
|
+
* )
|
|
1072
|
+
* }
|
|
1073
|
+
* ```
|
|
1074
|
+
*/
|
|
1075
|
+
declare const FirestateStoreProvider: React.FC<FirestateStoreProviderProps>;
|
|
1076
|
+
/**
|
|
1077
|
+
* Hook to use navigation blocker when there are unsaved changes.
|
|
1078
|
+
* Works with react-router or similar routers.
|
|
1079
|
+
*
|
|
1080
|
+
* @example
|
|
1081
|
+
* ```tsx
|
|
1082
|
+
* function ProjectPage() {
|
|
1083
|
+
* const shouldBlock = useUnsavedChangesBlocker()
|
|
1084
|
+
*
|
|
1085
|
+
* // Use with react-router's useBlocker
|
|
1086
|
+
* const blocker = useBlocker(
|
|
1087
|
+
* ({ currentLocation, nextLocation }) =>
|
|
1088
|
+
* currentLocation.pathname !== nextLocation.pathname && shouldBlock
|
|
1089
|
+
* )
|
|
1090
|
+
*
|
|
1091
|
+
* return (
|
|
1092
|
+
* <>
|
|
1093
|
+
* <ProjectEditor />
|
|
1094
|
+
* {blocker.state === 'blocked' && (
|
|
1095
|
+
* <Dialog>Your changes may not be saved!</Dialog>
|
|
1096
|
+
* )}
|
|
1097
|
+
* </>
|
|
1098
|
+
* )
|
|
1099
|
+
* }
|
|
1100
|
+
* ```
|
|
1101
|
+
*/
|
|
1102
|
+
declare const useUnsavedChangesBlocker: () => boolean;
|
|
1103
|
+
//#endregion
|
|
1104
|
+
export { type ColEntry, type CollectionDefinition, type CollectionHandle, type CollectionOptions, type CollectionState, type DeepPartial, type DocEntry, type DocumentDefinition, type DocumentHandle, type DocumentOptions, 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 Store, type UndoAction, type UndoManager, type UndoManagerConfig, type UndoManagerState, type UndoManagerWithSubscribe, type UpdateOptions, type UseCollectionOptions, type UseDocumentOptions, applyDiff, applyDiffMutable, col, computeDiff, computeUndoDiff, createCollectionSubscription, createDiffAtPath, createDocumentSubscription, createFirestate, createStore, createUndoManager, deepClone, defineCollection, defineDocument, diffContainsPath, doc, extractDiffValue, flattenDiff, isDeepEqual, isDiffEmpty, mergeDiffs, unflattenDiff, useCollection, useDocument, useIsSynced, useStore, useUndoKeyboardShortcuts, useUndoManager, useUnsavedChangesBlocker };
|
|
1105
|
+
//# sourceMappingURL=index.d.mts.map
|