@hvakr/firestate 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +274 -43
- package/dist/index.d.mts +556 -59
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1090 -475
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
package/dist/index.d.mts
CHANGED
|
@@ -27,27 +27,43 @@ interface UpdateOptions {
|
|
|
27
27
|
undoGroupId?: string;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* The full observable state of a document subscription — what a hook `selector`
|
|
31
|
+
* receives. Carries every status flag (including `isSynced`, which the default
|
|
32
|
+
* data handle deliberately omits) so a selector can react to exactly the slice
|
|
33
|
+
* it reads.
|
|
31
34
|
*/
|
|
32
35
|
interface DocumentState<T> {
|
|
33
36
|
/** Current merged state (local changes applied to sync state) */
|
|
34
37
|
data: T | undefined;
|
|
35
|
-
/** Whether initial
|
|
38
|
+
/** Whether the initial snapshot has not arrived yet */
|
|
36
39
|
isLoading: boolean;
|
|
37
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Whether the initial snapshot has arrived and data is ready to render — the
|
|
42
|
+
* completion of {@link DocumentState.isLoading} (`!isLoading` for a live
|
|
43
|
+
* subscription; `false` while the hook is disabled).
|
|
44
|
+
*/
|
|
45
|
+
isLoaded: boolean;
|
|
46
|
+
/** Whether all local changes have synced to Firestore (no pending writes) */
|
|
38
47
|
isSynced: boolean;
|
|
39
48
|
/** Error from listener, if any */
|
|
40
49
|
error: Error | undefined;
|
|
41
50
|
}
|
|
42
51
|
/**
|
|
43
|
-
*
|
|
52
|
+
* The full observable state of a collection subscription — what a hook
|
|
53
|
+
* `selector` receives. See {@link DocumentState}.
|
|
44
54
|
*/
|
|
45
55
|
interface CollectionState<T> {
|
|
46
56
|
/** Current merged state keyed by document ID */
|
|
47
57
|
data: Record<string, T>;
|
|
48
|
-
/** Whether initial
|
|
58
|
+
/** Whether the initial snapshot has not arrived yet */
|
|
49
59
|
isLoading: boolean;
|
|
50
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Whether the collection is active and its initial snapshot has arrived —
|
|
62
|
+
* `isActive && !isLoading`. `false` for a lazy collection before `load()`,
|
|
63
|
+
* and while the hook is disabled.
|
|
64
|
+
*/
|
|
65
|
+
isLoaded: boolean;
|
|
66
|
+
/** Whether all local changes have synced to Firestore (no pending writes) */
|
|
51
67
|
isSynced: boolean;
|
|
52
68
|
/** Whether the collection has been activated (for lazy loading) */
|
|
53
69
|
isActive: boolean;
|
|
@@ -55,7 +71,38 @@ interface CollectionState<T> {
|
|
|
55
71
|
error: Error | undefined;
|
|
56
72
|
}
|
|
57
73
|
/**
|
|
58
|
-
*
|
|
74
|
+
* Sync status of a single resource, returned by the per-entry
|
|
75
|
+
* `use{Name}SyncStatus` hook. Opt-in: only components that render save/dirty
|
|
76
|
+
* state subscribe to it, so the common data path does not re-render when a write
|
|
77
|
+
* settles. Shares the resource's one `onSnapshot` listener.
|
|
78
|
+
*/
|
|
79
|
+
interface SyncStatus {
|
|
80
|
+
/** Whether all local changes have synced to Firestore (no pending writes) */
|
|
81
|
+
isSynced: boolean;
|
|
82
|
+
/** Whether there are pending local changes still being saved (`!isSynced`) */
|
|
83
|
+
isSaving: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Loading status of a single resource, returned by the per-entry
|
|
87
|
+
* `use{Name}LoadingStatus` hook. A spinner-only channel: it re-renders on load
|
|
88
|
+
* transitions but never on data changes. Shares the resource's listener.
|
|
89
|
+
*/
|
|
90
|
+
interface LoadingStatus {
|
|
91
|
+
/** Whether the initial snapshot has not arrived yet */
|
|
92
|
+
isLoading: boolean;
|
|
93
|
+
/** Whether the initial snapshot has arrived (the completion of `isLoading`) */
|
|
94
|
+
isLoaded: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Document handle returned by the `useDocument` hook.
|
|
98
|
+
*
|
|
99
|
+
* **Sync-agnostic by default.** The handle carries `data`, `isLoaded`, `error`,
|
|
100
|
+
* the writers, and `ref` — but NOT `isSynced`. A document hook therefore does
|
|
101
|
+
* not re-render when a write settles (the `isSynced` flip on every autosave),
|
|
102
|
+
* so "just render the record" is the cheap, default path. Components that
|
|
103
|
+
* actually render save/dirty state opt into the per-entry `use{Name}SyncStatus`
|
|
104
|
+
* hook ({@link SyncStatus}), which shares the same listener. The raw
|
|
105
|
+
* `isLoading`/`isSynced` flags remain on {@link DocumentState} for selectors.
|
|
59
106
|
*/
|
|
60
107
|
interface DocumentHandle<T extends FirestoreObject> {
|
|
61
108
|
/** Current document data */
|
|
@@ -66,10 +113,13 @@ interface DocumentHandle<T extends FirestoreObject> {
|
|
|
66
113
|
set: (data: T, options?: UpdateOptions) => void;
|
|
67
114
|
/** Delete the document */
|
|
68
115
|
delete: (options?: UpdateOptions) => void;
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Whether the initial snapshot has arrived and data is ready to render — the
|
|
118
|
+
* completion of `isLoading`. `false` while loading or when the hook is
|
|
119
|
+
* disabled. (Use `use{Name}LoadingStatus` for an `isLoading`/`isLoaded`
|
|
120
|
+
* channel that does not re-render on data changes.)
|
|
121
|
+
*/
|
|
122
|
+
isLoaded: boolean;
|
|
73
123
|
/** Force sync pending changes immediately */
|
|
74
124
|
sync: () => Promise<void>;
|
|
75
125
|
/** Error from listener, if any */
|
|
@@ -81,7 +131,13 @@ interface DocumentHandle<T extends FirestoreObject> {
|
|
|
81
131
|
ref: DocumentReference<T> | undefined;
|
|
82
132
|
}
|
|
83
133
|
/**
|
|
84
|
-
* Collection handle returned by useCollection hook
|
|
134
|
+
* Collection handle returned by the `useCollection` hook.
|
|
135
|
+
*
|
|
136
|
+
* Sync-agnostic by default, exactly like {@link DocumentHandle}: it carries
|
|
137
|
+
* `data`, `isLoaded`, `isActive`, `error`, the writers, `load`, and `ref` — but
|
|
138
|
+
* NOT `isSynced`. Opt into `use{Name}SyncStatus` for save state. `isActive`
|
|
139
|
+
* stays (lazy collections gate a "Load" button on it); `isLoaded` is
|
|
140
|
+
* `isActive && !isLoading`.
|
|
85
141
|
*/
|
|
86
142
|
interface CollectionHandle<T extends FirestoreObject> {
|
|
87
143
|
/** Current collection data keyed by document ID */
|
|
@@ -102,10 +158,12 @@ interface CollectionHandle<T extends FirestoreObject> {
|
|
|
102
158
|
};
|
|
103
159
|
/** Remove a document from the collection */
|
|
104
160
|
remove: (id: string, options?: UpdateOptions) => void;
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Whether the collection is active and its initial snapshot has arrived
|
|
163
|
+
* (ready to render) — `isActive && !isLoading`. `false` for a lazy
|
|
164
|
+
* collection before `load()`, while loading, or when the hook is disabled.
|
|
165
|
+
*/
|
|
166
|
+
isLoaded: boolean;
|
|
109
167
|
/** Whether subscription is active (for lazy collections) */
|
|
110
168
|
isActive: boolean;
|
|
111
169
|
/** Activate a lazy subscription */
|
|
@@ -120,6 +178,46 @@ interface CollectionHandle<T extends FirestoreObject> {
|
|
|
120
178
|
*/
|
|
121
179
|
ref: CollectionReference<T> | undefined;
|
|
122
180
|
}
|
|
181
|
+
/** Reactive status fields a selector drops unless it folds them into its slice. */
|
|
182
|
+
type DocumentStatusKeys = "isLoaded" | "error";
|
|
183
|
+
type CollectionStatusKeys = DocumentStatusKeys | "isActive";
|
|
184
|
+
/**
|
|
185
|
+
* A {@link DocumentHandle} reduced to a hook-level `selector`'s output. The
|
|
186
|
+
* selector receives the full observable state ({@link DocumentState}) and
|
|
187
|
+
* returns the slice this component reacts to; the handle re-renders *only* when
|
|
188
|
+
* that slice changes.
|
|
189
|
+
*
|
|
190
|
+
* A selected handle deliberately exposes **only** `data` (the slice) plus the
|
|
191
|
+
* writer surface (`update`/`set`/`delete`/`sync`) and `ref` — never the status
|
|
192
|
+
* fields (`isLoaded`/`error`). Status is not a freebie here: if a component
|
|
193
|
+
* needs it, it must select it (`s => ({ slice: s.data?.x, loading:
|
|
194
|
+
* s.isLoading })`), so what you re-render on is exactly what you select. The
|
|
195
|
+
* writers stay typed against the full document `TData`, because a selector
|
|
196
|
+
* changes what you *read*, never what you *write*.
|
|
197
|
+
*
|
|
198
|
+
* Note: `update(diff)` takes a *partial* of the full document and merges it, so
|
|
199
|
+
* writing a selected field is `update({ field: next })`. `set(data)` still
|
|
200
|
+
* *replaces the entire document*, not the slice — never pass the selected value
|
|
201
|
+
* to `set`, or you will overwrite every other field. Prefer `update` from a
|
|
202
|
+
* narrowed handle; reach for `set` only when you hold the full document.
|
|
203
|
+
*/
|
|
204
|
+
interface SelectedDocumentHandle<TData extends FirestoreObject, TSelected> extends Omit<DocumentHandle<TData>, "data" | DocumentStatusKeys> {
|
|
205
|
+
/** The slice produced by the hook's `selector`. */
|
|
206
|
+
data: TSelected;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* A {@link CollectionHandle} reduced to a hook-level `selector`'s output. As
|
|
210
|
+
* with {@link SelectedDocumentHandle}, the selector receives the full
|
|
211
|
+
* observable state ({@link CollectionState}) and the handle exposes only the
|
|
212
|
+
* slice plus the writer surface (`update`/`add`/`remove`/`load`/`sync`) and
|
|
213
|
+
* `ref` — status fields (`isLoaded`/`isActive`/`error`) are dropped unless
|
|
214
|
+
* folded into the slice. Writers stay typed against the full collection of
|
|
215
|
+
* `TData`.
|
|
216
|
+
*/
|
|
217
|
+
interface SelectedCollectionHandle<TData extends FirestoreObject, TSelected> extends Omit<CollectionHandle<TData>, "data" | CollectionStatusKeys> {
|
|
218
|
+
/** The slice produced by the hook's `selector`. */
|
|
219
|
+
data: TSelected;
|
|
220
|
+
}
|
|
123
221
|
/**
|
|
124
222
|
* An undo/redo action
|
|
125
223
|
*/
|
|
@@ -269,7 +367,7 @@ type Subscriber<T> = (state: T) => void;
|
|
|
269
367
|
*/
|
|
270
368
|
type Unsubscribe = () => void;
|
|
271
369
|
//#endregion
|
|
272
|
-
//#region src/schema.d.ts
|
|
370
|
+
//#region src/registry/schema.d.ts
|
|
273
371
|
/**
|
|
274
372
|
* Define a typed document. `TData` is the document's TypeScript shape.
|
|
275
373
|
*
|
|
@@ -355,7 +453,7 @@ type InferCollectionDocument<T extends CollectionDefinition<FirestoreObject>> =
|
|
|
355
453
|
id: string;
|
|
356
454
|
};
|
|
357
455
|
//#endregion
|
|
358
|
-
//#region src/undo.d.ts
|
|
456
|
+
//#region src/utils/undo.d.ts
|
|
359
457
|
/**
|
|
360
458
|
* Configuration for creating an undo manager
|
|
361
459
|
*/
|
|
@@ -392,7 +490,7 @@ declare const createUndoManager: (config?: UndoManagerConfig) => UndoManager & {
|
|
|
392
490
|
*/
|
|
393
491
|
type UndoManagerWithSubscribe = ReturnType<typeof createUndoManager>;
|
|
394
492
|
//#endregion
|
|
395
|
-
//#region src/store.d.ts
|
|
493
|
+
//#region src/core/store.d.ts
|
|
396
494
|
/**
|
|
397
495
|
* Firestate store that holds configuration and shared state
|
|
398
496
|
*/
|
|
@@ -456,7 +554,65 @@ declare const createStore: (config: FirestateConfig) => FirestateStore;
|
|
|
456
554
|
*/
|
|
457
555
|
type Store = ReturnType<typeof createStore>;
|
|
458
556
|
//#endregion
|
|
459
|
-
//#region src/hooks.d.ts
|
|
557
|
+
//#region src/react/hooks.d.ts
|
|
558
|
+
/**
|
|
559
|
+
* Opts a {@link useDocument} call into a selected slice. The hook still returns
|
|
560
|
+
* a full handle (writers, `ref`, status) — only `data` is narrowed to whatever
|
|
561
|
+
* `selector` returns.
|
|
562
|
+
*/
|
|
563
|
+
interface DocumentSelectorOptions<TData extends FirestoreObject, TSelected> {
|
|
564
|
+
/**
|
|
565
|
+
* Project the document's observable state down to the slice this component
|
|
566
|
+
* reacts to. The selector receives the full {@link DocumentState} —
|
|
567
|
+
* `{ data, isLoading, isLoaded, isSynced, error }`, where `data` is
|
|
568
|
+
* `undefined` while the document is loading or the hook is disabled — and
|
|
569
|
+
* the component
|
|
570
|
+
* re-renders *only* when the returned slice changes (per `isEqual`). Status
|
|
571
|
+
* is not a freebie: read `s.isLoading`/`s.isSynced`/`s.error` here if you
|
|
572
|
+
* want to react to them (e.g. `s => ({ title: s.data?.title, saving:
|
|
573
|
+
* !s.isSynced })`). What you select is exactly what re-renders, and the
|
|
574
|
+
* returned handle exposes only the slice plus writers/`ref`.
|
|
575
|
+
*/
|
|
576
|
+
selector: (state: DocumentState<TData>) => TSelected;
|
|
577
|
+
/**
|
|
578
|
+
* Decide whether two consecutive slices are equal; the hook re-renders only
|
|
579
|
+
* when this returns `false`. Defaults to a deep value comparison, so a
|
|
580
|
+
* selector that returns a fresh object/array of the same shape does not
|
|
581
|
+
* over-render. Pass {@link shallow} for a one-level compare, or a custom
|
|
582
|
+
* comparator.
|
|
583
|
+
*/
|
|
584
|
+
isEqual?: (a: TSelected, b: TSelected) => boolean;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Opts a {@link useCollection} call into a selected slice. See
|
|
588
|
+
* {@link DocumentSelectorOptions}; the only difference is the selector receives
|
|
589
|
+
* the collection's keyed record.
|
|
590
|
+
*/
|
|
591
|
+
interface CollectionSelectorOptions<TData extends FirestoreObject, TSelected> {
|
|
592
|
+
/**
|
|
593
|
+
* Project the collection's observable state down to the slice this component
|
|
594
|
+
* reacts to. The selector receives the full {@link CollectionState} —
|
|
595
|
+
* `{ data, isLoading, isLoaded, isSynced, isActive, error }`, where `data` is
|
|
596
|
+
* the keyed record (e.g. `s => s.data[id]` or `s => Object.keys(s.data)`) —
|
|
597
|
+
* and the
|
|
598
|
+
* component re-renders *only* when the returned slice changes. As with
|
|
599
|
+
* {@link DocumentSelectorOptions.selector}, status is reactive only if you
|
|
600
|
+
* select it, and the returned handle exposes just the slice plus
|
|
601
|
+
* writers/`ref`.
|
|
602
|
+
*/
|
|
603
|
+
selector: (state: CollectionState<TData>) => TSelected;
|
|
604
|
+
/** See {@link DocumentSelectorOptions.isEqual}. */
|
|
605
|
+
isEqual?: (a: TSelected, b: TSelected) => boolean;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Shape used by the non-selector hook overload to *exclude* selector options,
|
|
609
|
+
* so passing a real `selector` falls through to the selector overload (which
|
|
610
|
+
* infers `TSelected`) instead of silently resolving to the full-data return.
|
|
611
|
+
*/
|
|
612
|
+
type WithoutSelector = {
|
|
613
|
+
selector?: undefined;
|
|
614
|
+
isEqual?: undefined;
|
|
615
|
+
};
|
|
460
616
|
/**
|
|
461
617
|
* Context for providing the Firestate store
|
|
462
618
|
*/
|
|
@@ -487,9 +643,8 @@ interface UseDocumentOptions<TData extends FirestoreObject> {
|
|
|
487
643
|
undoable?: boolean;
|
|
488
644
|
/**
|
|
489
645
|
* If false, no subscription is created and a no-op handle is returned
|
|
490
|
-
* (`{ data: undefined,
|
|
491
|
-
*
|
|
492
|
-
* Default: true.
|
|
646
|
+
* (`{ data: undefined, isLoaded: false, ref: undefined }`). Use this to gate
|
|
647
|
+
* subscriptions on route params that aren't ready yet. Default: true.
|
|
493
648
|
*/
|
|
494
649
|
enabled?: boolean;
|
|
495
650
|
}
|
|
@@ -497,19 +652,29 @@ interface UseDocumentOptions<TData extends FirestoreObject> {
|
|
|
497
652
|
* Hook to subscribe to a Firestore document with real-time updates.
|
|
498
653
|
*
|
|
499
654
|
* The subscription is keyed on the resolved document path (`definition` +
|
|
500
|
-
* computed id)
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
655
|
+
* computed id). When that key changes — typically because `params` produces a
|
|
656
|
+
* different id — the hook tears down the old Firestore listener and attaches a
|
|
657
|
+
* new one. Toggling `undoable` does not rebuild the subscription.
|
|
658
|
+
*
|
|
659
|
+
* `readOnly` is a *per-handle capability*, not part of the key: a `readOnly`
|
|
660
|
+
* hook shares the same listener and optimistic state as a writable hook on the
|
|
661
|
+
* same document (a write through the writable handle is instantly visible to
|
|
662
|
+
* the read-only reader), and only this handle's own writers (`update`/`set`/
|
|
663
|
+
* `delete`) and `sync` are disabled.
|
|
504
664
|
*
|
|
505
665
|
* Use `enabled: false` to suppress the subscription entirely (e.g., when
|
|
506
666
|
* route params aren't ready yet).
|
|
507
667
|
*
|
|
508
668
|
* **SSR.** On the server there is no Firestore listener, so this hook returns
|
|
509
|
-
* the initial handle (`{ data: undefined,
|
|
669
|
+
* the initial handle (`{ data: undefined, isLoaded: false }`). Mutations like
|
|
510
670
|
* `update`/`set` will mutate orphaned local state with no effect — avoid
|
|
511
671
|
* calling them server-side.
|
|
512
672
|
*
|
|
673
|
+
* The default handle is **sync-agnostic** — it carries `data`/`isLoaded`/`error`
|
|
674
|
+
* but not `isSynced`, so it does not re-render when a write settles. Render save
|
|
675
|
+
* state via the per-entry `use{Name}SyncStatus` hook, or fold `isSynced` into a
|
|
676
|
+
* `selector`.
|
|
677
|
+
*
|
|
513
678
|
* @example
|
|
514
679
|
* ```tsx
|
|
515
680
|
* const projectDoc = defineDocument<Project>({
|
|
@@ -518,12 +683,12 @@ interface UseDocumentOptions<TData extends FirestoreObject> {
|
|
|
518
683
|
* })
|
|
519
684
|
*
|
|
520
685
|
* function ProjectEditor({ projectId }: { projectId: string }) {
|
|
521
|
-
* const { data, update,
|
|
686
|
+
* const { data, update, isLoaded } = useDocument({
|
|
522
687
|
* definition: projectDoc,
|
|
523
688
|
* params: { projectId },
|
|
524
689
|
* })
|
|
525
690
|
*
|
|
526
|
-
* if (
|
|
691
|
+
* if (!isLoaded) return <Spinner />
|
|
527
692
|
*
|
|
528
693
|
* return (
|
|
529
694
|
* <input
|
|
@@ -534,7 +699,24 @@ interface UseDocumentOptions<TData extends FirestoreObject> {
|
|
|
534
699
|
* }
|
|
535
700
|
* ```
|
|
536
701
|
*/
|
|
537
|
-
declare
|
|
702
|
+
declare function useDocument<TData extends FirestoreObject>(options: UseDocumentOptions<TData> & WithoutSelector): DocumentHandle<TData>;
|
|
703
|
+
/**
|
|
704
|
+
* Selector overload: pass `selector` to narrow the returned `data` to a slice
|
|
705
|
+
* and re-render only when that slice changes. Writers (`update`/`set`/`delete`)
|
|
706
|
+
* and `ref` keep operating on the full document. See
|
|
707
|
+
* {@link DocumentSelectorOptions}.
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* ```tsx
|
|
711
|
+
* // Re-renders only when the title changes, not on any other field.
|
|
712
|
+
* const { data: title, update } = useDocument({
|
|
713
|
+
* definition: projectDoc,
|
|
714
|
+
* params: { projectId },
|
|
715
|
+
* selector: (s) => s.data?.title,
|
|
716
|
+
* })
|
|
717
|
+
* ```
|
|
718
|
+
*/
|
|
719
|
+
declare function useDocument<TData extends FirestoreObject, TSelected>(options: UseDocumentOptions<TData> & DocumentSelectorOptions<TData, TSelected>): SelectedDocumentHandle<TData, TSelected>;
|
|
538
720
|
/**
|
|
539
721
|
* Options for useCollection hook
|
|
540
722
|
*/
|
|
@@ -551,7 +733,7 @@ interface UseCollectionOptions<TData extends FirestoreObject> {
|
|
|
551
733
|
undoable?: boolean;
|
|
552
734
|
/**
|
|
553
735
|
* If false, no subscription is created and a no-op handle is returned
|
|
554
|
-
* (`{ data: {},
|
|
736
|
+
* (`{ data: {}, isLoaded: false, isActive: false }`). Use this to gate on
|
|
555
737
|
* route params that aren't ready yet. Default: true.
|
|
556
738
|
*/
|
|
557
739
|
enabled?: boolean;
|
|
@@ -559,10 +741,12 @@ interface UseCollectionOptions<TData extends FirestoreObject> {
|
|
|
559
741
|
/**
|
|
560
742
|
* Hook to subscribe to a Firestore collection with real-time updates.
|
|
561
743
|
*
|
|
562
|
-
* The subscription is keyed on the resolved collection path
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
* `
|
|
744
|
+
* The subscription is keyed on the resolved collection path and the *semantic
|
|
745
|
+
* identity* of `queryConstraints`. When either changes, the listener is torn
|
|
746
|
+
* down and re-attached with the new query. Toggling `undoable` does not rebuild
|
|
747
|
+
* the subscription. `readOnly` is a per-handle capability, not part of the key —
|
|
748
|
+
* a `readOnly` hook shares one listener and optimistic state with a writable
|
|
749
|
+
* hook on the same query (see {@link useDocument}).
|
|
566
750
|
*
|
|
567
751
|
* **You do not need to memoize `queryConstraints`.** `QueryConstraint` objects
|
|
568
752
|
* are opaque, so Firestate compares the *built query* with Firestore's own
|
|
@@ -588,8 +772,13 @@ interface UseCollectionOptions<TData extends FirestoreObject> {
|
|
|
588
772
|
* route params aren't ready yet).
|
|
589
773
|
*
|
|
590
774
|
* **SSR.** On the server there is no Firestore listener, so this hook returns
|
|
591
|
-
* the initial handle (`{ data: {},
|
|
592
|
-
*
|
|
775
|
+
* the initial handle (`{ data: {}, isLoaded: false }`, `isActive: false` for
|
|
776
|
+
* lazy). Avoid calling mutations server-side.
|
|
777
|
+
*
|
|
778
|
+
* Like {@link useDocument}, the default handle is **sync-agnostic** — `data`,
|
|
779
|
+
* `isLoaded`, `isActive`, `error`, but not `isSynced`. `isActive` stays so a
|
|
780
|
+
* lazy collection can gate a "Load" button; `isLoaded` is `isActive &&
|
|
781
|
+
* !isLoading`. Render save state via `use{Name}SyncStatus`.
|
|
593
782
|
*
|
|
594
783
|
* @example
|
|
595
784
|
* ```tsx
|
|
@@ -599,7 +788,7 @@ interface UseCollectionOptions<TData extends FirestoreObject> {
|
|
|
599
788
|
* })
|
|
600
789
|
*
|
|
601
790
|
* function SpacesList({ projectId }: { projectId: string }) {
|
|
602
|
-
* const { data, update, load, isActive,
|
|
791
|
+
* const { data, update, load, isActive, isLoaded } = useCollection({
|
|
603
792
|
* definition: spacesCollection,
|
|
604
793
|
* params: { projectId },
|
|
605
794
|
* })
|
|
@@ -608,7 +797,7 @@ interface UseCollectionOptions<TData extends FirestoreObject> {
|
|
|
608
797
|
* useEffect(() => { load() }, [load])
|
|
609
798
|
*
|
|
610
799
|
* if (!isActive) return <Button onClick={load}>Load Spaces</Button>
|
|
611
|
-
* if (
|
|
800
|
+
* if (!isLoaded) return <Spinner />
|
|
612
801
|
*
|
|
613
802
|
* return (
|
|
614
803
|
* <ul>
|
|
@@ -620,7 +809,96 @@ interface UseCollectionOptions<TData extends FirestoreObject> {
|
|
|
620
809
|
* }
|
|
621
810
|
* ```
|
|
622
811
|
*/
|
|
623
|
-
declare
|
|
812
|
+
declare function useCollection<TData extends FirestoreObject>(options: UseCollectionOptions<TData> & WithoutSelector): CollectionHandle<TData>;
|
|
813
|
+
/**
|
|
814
|
+
* Selector overload: pass `selector` to narrow the returned `data` to a slice
|
|
815
|
+
* of the collection and re-render only when that slice changes. Writers
|
|
816
|
+
* (`update`/`add`/`remove`) and `ref` keep operating on the full collection.
|
|
817
|
+
* See {@link CollectionSelectorOptions}.
|
|
818
|
+
*
|
|
819
|
+
* @example
|
|
820
|
+
* ```tsx
|
|
821
|
+
* // Re-renders only when this one document's slice changes.
|
|
822
|
+
* const { data: space } = useCollection({
|
|
823
|
+
* definition: spacesCollection,
|
|
824
|
+
* params: { projectId },
|
|
825
|
+
* selector: (s) => s.data[spaceId],
|
|
826
|
+
* })
|
|
827
|
+
* ```
|
|
828
|
+
*/
|
|
829
|
+
declare function useCollection<TData extends FirestoreObject, TSelected>(options: UseCollectionOptions<TData> & CollectionSelectorOptions<TData, TSelected>): SelectedCollectionHandle<TData, TSelected>;
|
|
830
|
+
/** Options for the document status hooks (a subset of {@link UseDocumentOptions}). */
|
|
831
|
+
interface UseDocumentStatusOptions<TData extends FirestoreObject> {
|
|
832
|
+
/** Document definition from defineDocument(). */
|
|
833
|
+
definition: DocumentDefinition<TData>;
|
|
834
|
+
/** Route/path parameters for dynamic paths. */
|
|
835
|
+
params?: Record<string, string>;
|
|
836
|
+
/**
|
|
837
|
+
* If false, no subscription is created and the idle status is returned
|
|
838
|
+
* (`{ isSynced: true, isSaving: false }` / `{ isLoading: false, isLoaded:
|
|
839
|
+
* false }`). Default: true.
|
|
840
|
+
*/
|
|
841
|
+
enabled?: boolean;
|
|
842
|
+
}
|
|
843
|
+
/** Options for the collection status hooks. Adds `queryConstraints`. */
|
|
844
|
+
interface UseCollectionStatusOptions<TData extends FirestoreObject> {
|
|
845
|
+
/** Collection definition from defineCollection(). */
|
|
846
|
+
definition: CollectionDefinition<TData>;
|
|
847
|
+
/** Route/path parameters for dynamic paths. */
|
|
848
|
+
params?: Record<string, string>;
|
|
849
|
+
/**
|
|
850
|
+
* Query constraints. Must produce the same query the data hook uses, or the
|
|
851
|
+
* status hook resolves a *different* shared entry (a second listener) —
|
|
852
|
+
* sharing is keyed by semantic query identity.
|
|
853
|
+
*/
|
|
854
|
+
queryConstraints?: QueryConstraint[];
|
|
855
|
+
/** See {@link UseDocumentStatusOptions.enabled}. */
|
|
856
|
+
enabled?: boolean;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Subscribe to a document's **sync status only** — `{ isSynced, isSaving }`.
|
|
860
|
+
*
|
|
861
|
+
* The opt-in counterpart to the sync-agnostic default handle (see
|
|
862
|
+
* {@link DocumentHandle}): it re-renders when sync state flips but never on data
|
|
863
|
+
* changes, and shares the resource's one `onSnapshot` listener with
|
|
864
|
+
* `useDocument` and any slice hooks, so opting in adds no listener. While
|
|
865
|
+
* disabled it reports `{ isSynced: true, isSaving: false }`.
|
|
866
|
+
*/
|
|
867
|
+
declare function useDocumentSyncStatus<TData extends FirestoreObject>(options: UseDocumentStatusOptions<TData>): SyncStatus;
|
|
868
|
+
/**
|
|
869
|
+
* Subscribe to a document's **loading status only** — `{ isLoading, isLoaded }`.
|
|
870
|
+
*
|
|
871
|
+
* A spinner channel that shares the resource's listener and does NOT re-render
|
|
872
|
+
* on data changes — for a progress indicator rendered apart from the data. The
|
|
873
|
+
* data handle keeps `isLoaded` for the common render path; this is an extra
|
|
874
|
+
* channel, not a replacement.
|
|
875
|
+
*/
|
|
876
|
+
declare function useDocumentLoadingStatus<TData extends FirestoreObject>(options: UseDocumentStatusOptions<TData>): LoadingStatus;
|
|
877
|
+
/**
|
|
878
|
+
* Collection counterpart of {@link useDocumentSyncStatus} — `{ isSynced,
|
|
879
|
+
* isSaving }` over a collection query, sharing its one listener.
|
|
880
|
+
*
|
|
881
|
+
* **Lazy caveat.** On a `lazy` collection this hook never calls `load()` itself:
|
|
882
|
+
* activating a lazy listener is the data hook's job, and a passive status reader
|
|
883
|
+
* must not silently start the listener (and bill the reads) the laziness exists
|
|
884
|
+
* to defer. As the *lone* subscriber it therefore attaches no listener and stays
|
|
885
|
+
* at the idle `{ isSynced: true, isSaving: false }`. Mount it alongside a
|
|
886
|
+
* {@link useCollection} on the same query whose `load()` has run — the status
|
|
887
|
+
* hook rides that one shared listener and reports real sync state. Non-lazy
|
|
888
|
+
* collections activate on mount, so this hook works standalone there.
|
|
889
|
+
*/
|
|
890
|
+
declare function useCollectionSyncStatus<TData extends FirestoreObject>(options: UseCollectionStatusOptions<TData>): SyncStatus;
|
|
891
|
+
/**
|
|
892
|
+
* Collection counterpart of {@link useDocumentLoadingStatus} — `{ isLoading,
|
|
893
|
+
* isLoaded }` over a collection query, sharing its one listener.
|
|
894
|
+
*
|
|
895
|
+
* Same lazy caveat as {@link useCollectionSyncStatus}: on a `lazy` collection it
|
|
896
|
+
* never calls `load()`, so as the lone subscriber it attaches no listener and
|
|
897
|
+
* stays at the idle `{ isLoading: false, isLoaded: false }` until a co-mounted
|
|
898
|
+
* {@link useCollection} (or any active hook on the same query) activates the
|
|
899
|
+
* shared listener via `load()`. Non-lazy collections activate on mount.
|
|
900
|
+
*/
|
|
901
|
+
declare function useCollectionLoadingStatus<TData extends FirestoreObject>(options: UseCollectionStatusOptions<TData>): LoadingStatus;
|
|
624
902
|
/**
|
|
625
903
|
* Keyboard shortcut hook for undo/redo
|
|
626
904
|
*
|
|
@@ -634,7 +912,7 @@ declare const useCollection: <TData extends FirestoreObject>(options: UseCollect
|
|
|
634
912
|
*/
|
|
635
913
|
declare const useUndoKeyboardShortcuts: () => void;
|
|
636
914
|
//#endregion
|
|
637
|
-
//#region src/firestate.d.ts
|
|
915
|
+
//#region src/registry/firestate.d.ts
|
|
638
916
|
/**
|
|
639
917
|
* Knobs forwarded from a generated document hook to {@link useDocument}.
|
|
640
918
|
* Same shape as `UseDocumentOptions` minus the fields the registry already
|
|
@@ -668,8 +946,13 @@ interface CommonEntryOptions {
|
|
|
668
946
|
interface DocEntry<T extends FirestoreObject, P extends string = string> extends CommonEntryOptions {
|
|
669
947
|
readonly __kind: "document";
|
|
670
948
|
readonly __type?: T;
|
|
671
|
-
/**
|
|
672
|
-
|
|
949
|
+
/**
|
|
950
|
+
* Path template, e.g. `'taskLists/{listId}'`, or a function returning the
|
|
951
|
+
* **full document path** at runtime (the collection/id split happens
|
|
952
|
+
* per-call via {@link splitDocPath}). Use the function form for paths that
|
|
953
|
+
* branch on a param. See {@link PathArg}.
|
|
954
|
+
*/
|
|
955
|
+
path: PathArg<P>;
|
|
673
956
|
/**
|
|
674
957
|
* Zod schema. **Required** — firestate's registry API is opinionated
|
|
675
958
|
* about Zod. The schema is the source of `T` for the generated hooks
|
|
@@ -683,22 +966,126 @@ interface DocEntry<T extends FirestoreObject, P extends string = string> extends
|
|
|
683
966
|
* param typing and no runtime validation.
|
|
684
967
|
*/
|
|
685
968
|
schema: ZodType<T>;
|
|
969
|
+
/**
|
|
970
|
+
* Derive a **named slice-hook** off this document, sharing its schema and
|
|
971
|
+
* path — the schema is handed to firestate once, here, and never
|
|
972
|
+
* re-specified. The `selector` receives the full {@link DocumentState};
|
|
973
|
+
* return the slice the generated hook reacts to. For a *parameterized* slice,
|
|
974
|
+
* declare the extra params as the selector's second argument — the generated
|
|
975
|
+
* hook then requires the path params **and** those, merged into one bag.
|
|
976
|
+
*
|
|
977
|
+
* Pass the result to {@link createFirestate} under the key the hook is named
|
|
978
|
+
* for. Status is reactive only if the slice reads it, exactly as the inline
|
|
979
|
+
* `selector` option (see {@link DocumentHandle}); the comparator (`isEqual`)
|
|
980
|
+
* is baked in here, not passed per call.
|
|
981
|
+
*
|
|
982
|
+
* ```ts
|
|
983
|
+
* const project = doc({ path: 'projects/{projectId}', schema: ProjectSchema })
|
|
984
|
+
* const { useProject, useProjectTitle } = createFirestate({
|
|
985
|
+
* project, // → useProject (full)
|
|
986
|
+
* projectTitle: project.select((s) => s.data?.name), // → useProjectTitle
|
|
987
|
+
* })
|
|
988
|
+
* ```
|
|
989
|
+
*
|
|
990
|
+
* A derived entry is a leaf, not a base: there is intentionally no
|
|
991
|
+
* `.select(...).select(...)` chaining.
|
|
992
|
+
*
|
|
993
|
+
* `PExtra` (the selector's own params) defaults to `{}`: a one-argument
|
|
994
|
+
* selector leaves it unbound, a two-argument one infers it from the annotated
|
|
995
|
+
* second parameter. One signature keeps the selector's `state` arg reliably
|
|
996
|
+
* typed in both cases. `PExtra` is intentionally unconstrained — leaving it
|
|
997
|
+
* `extends Record<string, string>` made TS resolve a param-less selector's
|
|
998
|
+
* `PExtra` to that constraint (not the `{}` default), wrongly forcing a
|
|
999
|
+
* `params` arg on no-placeholder paths; unconstrained also lets a slice take
|
|
1000
|
+
* non-string params (e.g. `{ index: number }`).
|
|
1001
|
+
*/
|
|
1002
|
+
select<TSelected, PExtra = {}>(selector: (state: DocumentState<T>, params: PExtra) => TSelected, options?: SelectOptions<TSelected>): SelectedDocEntry<T, P, PExtra, TSelected>;
|
|
686
1003
|
}
|
|
687
1004
|
/** Collection entry in a Firestate registry. Produced by {@link col}. */
|
|
688
1005
|
interface ColEntry<T extends FirestoreObject, P extends string = string> extends CommonEntryOptions {
|
|
689
1006
|
readonly __kind: "collection";
|
|
690
1007
|
readonly __type?: T;
|
|
691
|
-
/**
|
|
692
|
-
|
|
1008
|
+
/**
|
|
1009
|
+
* Path template, e.g. `'taskLists/{listId}/tasks'`, or a function returning
|
|
1010
|
+
* the **collection path** at runtime. Use the function form for paths that
|
|
1011
|
+
* branch on a param. See {@link PathArg}.
|
|
1012
|
+
*/
|
|
1013
|
+
path: PathArg<P>;
|
|
693
1014
|
/** Zod schema. Required. See {@link DocEntry.schema}. */
|
|
694
1015
|
schema: ZodType<T>;
|
|
695
1016
|
/** Only subscribe when `load()` is called. */
|
|
696
1017
|
lazy?: boolean;
|
|
697
1018
|
/** Additional Firestore query constraints. */
|
|
698
1019
|
queryConstraints?: QueryConstraint[];
|
|
1020
|
+
/**
|
|
1021
|
+
* Derive a **named slice-hook** off this collection, sharing its schema,
|
|
1022
|
+
* path, and query — see {@link DocEntry.select}. The `selector` receives the
|
|
1023
|
+
* full {@link CollectionState} (`s.data` is the keyed record), and a
|
|
1024
|
+
* parameterized slice declares its extra params as the selector's second
|
|
1025
|
+
* argument.
|
|
1026
|
+
*
|
|
1027
|
+
* ```ts
|
|
1028
|
+
* const tasks = col({ path: 'projects/{projectId}/tasks', schema: TaskSchema })
|
|
1029
|
+
* const { useTasks, useTaskIds, useTaskById } = createFirestate({
|
|
1030
|
+
* tasks, // → useTasks (full)
|
|
1031
|
+
* taskIds: tasks.select((s) => Object.keys(s.data)), // → useTaskIds
|
|
1032
|
+
* taskById: tasks.select((s, p: { id: string }) => s.data[p.id]), // → useTaskById
|
|
1033
|
+
* })
|
|
1034
|
+
* // useTaskById requires the merged bag: useTaskById({ projectId, id })
|
|
1035
|
+
* ```
|
|
1036
|
+
*
|
|
1037
|
+
* `PExtra` defaults to `{}` and is unconstrained — see {@link DocEntry.select}.
|
|
1038
|
+
*/
|
|
1039
|
+
select<TSelected, PExtra = {}>(selector: (state: CollectionState<T>, params: PExtra) => TSelected, options?: SelectOptions<TSelected>): SelectedColEntry<T, P, PExtra, TSelected>;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Options bundled into a `.select(...)` entry at definition time. Kept separate
|
|
1043
|
+
* from the runtime hook options (`enabled`/`readOnly`/`queryConstraints`)
|
|
1044
|
+
* because these are baked into the named hook, not passed per call.
|
|
1045
|
+
*/
|
|
1046
|
+
interface SelectOptions<TSelected> {
|
|
1047
|
+
/**
|
|
1048
|
+
* Comparator for this named hook's slice; the hook re-renders only when it
|
|
1049
|
+
* returns `false`. Defaults to a deep value compare (so a fresh object/array
|
|
1050
|
+
* of equal shape does not over-render). Pass {@link shallow} or a custom fn.
|
|
1051
|
+
*/
|
|
1052
|
+
isEqual?: (a: TSelected, b: TSelected) => boolean;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* A {@link DocEntry} narrowed by a `.select(...)` projection. Produced by
|
|
1056
|
+
* {@link DocEntry.select}, consumed by {@link createFirestate}, which turns it
|
|
1057
|
+
* into a hook whose `data` is the slice (`TSelected`) and whose params are the
|
|
1058
|
+
* path params (`P`) merged with the selector's own params (`PExtra`).
|
|
1059
|
+
*
|
|
1060
|
+
* The schema/path/options live on `base` — a derived entry never re-declares
|
|
1061
|
+
* them. `PExtra` is `{}` for an un-parameterized selector.
|
|
1062
|
+
*/
|
|
1063
|
+
interface SelectedDocEntry<T extends FirestoreObject, P extends string, PExtra, TSelected> {
|
|
1064
|
+
readonly __kind: "document-selected";
|
|
1065
|
+
/** Base entry carrying schema/path/options — handed to firestate once. */
|
|
1066
|
+
readonly base: DocEntry<T, P>;
|
|
1067
|
+
/** Projection over the full state; receives the merged params bag at runtime. */
|
|
1068
|
+
readonly selector: (state: DocumentState<T>, params: PExtra) => TSelected;
|
|
1069
|
+
/** Comparator baked in at definition time (see {@link SelectOptions}). */
|
|
1070
|
+
readonly isEqual?: (a: TSelected, b: TSelected) => boolean;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* A {@link ColEntry} narrowed by a `.select(...)` projection. See
|
|
1074
|
+
* {@link SelectedDocEntry}; the selector receives the collection's keyed state.
|
|
1075
|
+
*/
|
|
1076
|
+
interface SelectedColEntry<T extends FirestoreObject, P extends string, PExtra, TSelected> {
|
|
1077
|
+
readonly __kind: "collection-selected";
|
|
1078
|
+
/** Base entry carrying schema/path/query/options — handed to firestate once. */
|
|
1079
|
+
readonly base: ColEntry<T, P>;
|
|
1080
|
+
/** Projection over the full state; receives the merged params bag at runtime. */
|
|
1081
|
+
readonly selector: (state: CollectionState<T>, params: PExtra) => TSelected;
|
|
1082
|
+
/** Comparator baked in at definition time (see {@link SelectOptions}). */
|
|
1083
|
+
readonly isEqual?: (a: TSelected, b: TSelected) => boolean;
|
|
699
1084
|
}
|
|
700
1085
|
type FirestateEntry<T extends FirestoreObject = FirestoreObject, P extends string = string> = DocEntry<T, P> | ColEntry<T, P>;
|
|
701
|
-
|
|
1086
|
+
/** Any `.select(...)`-derived entry, regardless of its type parameters. */
|
|
1087
|
+
type AnySelectedEntry = SelectedDocEntry<any, any, any, any> | SelectedColEntry<any, any, any, any>;
|
|
1088
|
+
type FirestateRegistry = Record<string, FirestateEntry<any, any> | AnySelectedEntry>;
|
|
702
1089
|
/**
|
|
703
1090
|
* Extract `{name}` placeholders from a path template into a params shape.
|
|
704
1091
|
*
|
|
@@ -712,8 +1099,20 @@ type FirestateRegistry = Record<string, FirestateEntry<any, any>>;
|
|
|
712
1099
|
type ParamsOf<P extends string> = string extends P ? Record<string, string> : Prettify<RawParamsOf<P>>;
|
|
713
1100
|
type RawParamsOf<P extends string> = P extends `${string}{${infer K}}${infer Rest}` ? { [Key in K]: string } & RawParamsOf<Rest> : {};
|
|
714
1101
|
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
715
|
-
|
|
716
|
-
|
|
1102
|
+
/**
|
|
1103
|
+
* The `path` accepted by {@link doc} / {@link col}. Either a static template
|
|
1104
|
+
* (whose `{param}` placeholders are interpolated and whose param keys are
|
|
1105
|
+
* inferred via {@link ParamsOf}), or a function that returns the path at
|
|
1106
|
+
* runtime — for paths that branch on a param, e.g. live
|
|
1107
|
+
* `projects/{projectId}/spaces` vs. revision
|
|
1108
|
+
* `projects/{projectId}/revisions/{revisionId}/spaces`.
|
|
1109
|
+
*
|
|
1110
|
+
* With the function form, params can't be inferred from a template, so the
|
|
1111
|
+
* generated hook's params fall back to `Record<string, string>`.
|
|
1112
|
+
*/
|
|
1113
|
+
type PathArg<P extends string> = P | ((params: Record<string, string>) => string);
|
|
1114
|
+
type DocOpts<T extends FirestoreObject> = Omit<DocEntry<T>, "__kind" | "__type" | "path" | "select">;
|
|
1115
|
+
type ColOpts<T extends FirestoreObject> = Omit<ColEntry<T>, "__kind" | "__type" | "path" | "select">;
|
|
717
1116
|
/**
|
|
718
1117
|
* Declare a single-document entry for a Firestate registry.
|
|
719
1118
|
*
|
|
@@ -727,6 +1126,10 @@ type ColOpts<T extends FirestoreObject> = Omit<ColEntry<T>, "__kind" | "__type"
|
|
|
727
1126
|
* directly — that escape hatch keeps the plain-TypeScript form, at the
|
|
728
1127
|
* cost of looser param typing on the hook and no runtime validation.
|
|
729
1128
|
*
|
|
1129
|
+
* `path` may also be a function returning the full document path at runtime —
|
|
1130
|
+
* for paths that branch on a param. Param keys can't be inferred from a
|
|
1131
|
+
* function, so they fall back to `Record<string, string>`. See {@link PathArg}.
|
|
1132
|
+
*
|
|
730
1133
|
* ```ts
|
|
731
1134
|
* import { z } from 'zod'
|
|
732
1135
|
*
|
|
@@ -737,19 +1140,58 @@ type ColOpts<T extends FirestoreObject> = Omit<ColEntry<T>, "__kind" | "__type"
|
|
|
737
1140
|
*/
|
|
738
1141
|
declare function doc<S extends ZodType<FirestoreObject>, const P extends string = string>(opts: Omit<DocOpts<z.infer<S>>, "schema"> & {
|
|
739
1142
|
schema: S;
|
|
740
|
-
path: P
|
|
1143
|
+
path: PathArg<P>;
|
|
741
1144
|
}): DocEntry<z.infer<S>, P>;
|
|
742
1145
|
/**
|
|
743
1146
|
* Declare a collection entry for a Firestate registry. See {@link doc}
|
|
744
|
-
* for the schema/typing contract.
|
|
1147
|
+
* for the schema/typing contract. `path` may also be a function returning
|
|
1148
|
+
* the collection path at runtime — see {@link PathArg}.
|
|
745
1149
|
*/
|
|
746
1150
|
declare function col<S extends ZodType<FirestoreObject>, const P extends string = string>(opts: Omit<ColOpts<z.infer<S>>, "schema"> & {
|
|
747
1151
|
schema: S;
|
|
748
|
-
path: P
|
|
1152
|
+
path: PathArg<P>;
|
|
749
1153
|
}): ColEntry<z.infer<S>, P>;
|
|
750
1154
|
type HookName<K extends string> = `use${Capitalize<K>}`;
|
|
751
|
-
type
|
|
752
|
-
|
|
1155
|
+
type NoSelector = {
|
|
1156
|
+
selector?: undefined;
|
|
1157
|
+
isEqual?: undefined;
|
|
1158
|
+
};
|
|
1159
|
+
interface DocHookOptionalParams<T extends FirestoreObject> {
|
|
1160
|
+
(params?: Record<string, string>, options?: DocHookOptions<T> & NoSelector): DocumentHandle<T>;
|
|
1161
|
+
<TSelected>(params: Record<string, string> | undefined, options: DocHookOptions<T> & DocumentSelectorOptions<T, TSelected>): SelectedDocumentHandle<T, TSelected>;
|
|
1162
|
+
}
|
|
1163
|
+
interface DocHookRequiredParams<T extends FirestoreObject, P extends string> {
|
|
1164
|
+
(params: ParamsOf<P>, options?: DocHookOptions<T> & NoSelector): DocumentHandle<T>;
|
|
1165
|
+
<TSelected>(params: ParamsOf<P>, options: DocHookOptions<T> & DocumentSelectorOptions<T, TSelected>): SelectedDocumentHandle<T, TSelected>;
|
|
1166
|
+
}
|
|
1167
|
+
interface ColHookOptionalParams<T extends FirestoreObject> {
|
|
1168
|
+
(params?: Record<string, string>, options?: ColHookOptions<T> & NoSelector): CollectionHandle<T>;
|
|
1169
|
+
<TSelected>(params: Record<string, string> | undefined, options: ColHookOptions<T> & CollectionSelectorOptions<T, TSelected>): SelectedCollectionHandle<T, TSelected>;
|
|
1170
|
+
}
|
|
1171
|
+
interface ColHookRequiredParams<T extends FirestoreObject, P extends string> {
|
|
1172
|
+
(params: ParamsOf<P>, options?: ColHookOptions<T> & NoSelector): CollectionHandle<T>;
|
|
1173
|
+
<TSelected>(params: ParamsOf<P>, options: ColHookOptions<T> & CollectionSelectorOptions<T, TSelected>): SelectedCollectionHandle<T, TSelected>;
|
|
1174
|
+
}
|
|
1175
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
1176
|
+
type SelectedParams<P extends string, PExtra> = IsAny<PExtra> extends true ? ParamsOf<P> : Prettify<ParamsOf<P> & PExtra>;
|
|
1177
|
+
type SelectedDocHookFor<T extends FirestoreObject, P extends string, PExtra, TSelected> = keyof SelectedParams<P, PExtra> extends never ? (params?: Record<string, string>, options?: DocHookOptions<T>) => SelectedDocumentHandle<T, TSelected> : (params: SelectedParams<P, PExtra>, options?: DocHookOptions<T>) => SelectedDocumentHandle<T, TSelected>;
|
|
1178
|
+
type SelectedColHookFor<T extends FirestoreObject, P extends string, PExtra, TSelected> = keyof SelectedParams<P, PExtra> extends never ? (params?: Record<string, string>, options?: ColHookOptions<T>) => SelectedCollectionHandle<T, TSelected> : (params: SelectedParams<P, PExtra>, options?: ColHookOptions<T>) => SelectedCollectionHandle<T, TSelected>;
|
|
1179
|
+
type HookFor<E> = E extends SelectedDocEntry<infer T, infer P, infer PExtra, infer TSelected> ? SelectedDocHookFor<T, P, PExtra, TSelected> : E extends SelectedColEntry<infer T, infer P, infer PExtra, infer TSelected> ? SelectedColHookFor<T, P, PExtra, TSelected> : E extends DocEntry<infer T, infer P> ? keyof ParamsOf<P> extends never ? DocHookOptionalParams<T> : DocHookRequiredParams<T, P> : E extends ColEntry<infer T, infer P> ? keyof ParamsOf<P> extends never ? ColHookOptionalParams<T> : ColHookRequiredParams<T, P> : never;
|
|
1180
|
+
type SyncStatusHookName<K extends string> = `${HookName<K>}SyncStatus`;
|
|
1181
|
+
type LoadingStatusHookName<K extends string> = `${HookName<K>}LoadingStatus`;
|
|
1182
|
+
type BaseEntry = DocEntry<any, any> | ColEntry<any, any>;
|
|
1183
|
+
type DocStatusHookOptions = {
|
|
1184
|
+
enabled?: boolean;
|
|
1185
|
+
};
|
|
1186
|
+
type ColStatusHookOptions = {
|
|
1187
|
+
enabled?: boolean;
|
|
1188
|
+
queryConstraints?: QueryConstraint[];
|
|
1189
|
+
};
|
|
1190
|
+
type DocStatusHookFor<P extends string, Ret> = keyof ParamsOf<P> extends never ? (params?: Record<string, string>, options?: DocStatusHookOptions) => Ret : (params: ParamsOf<P>, options?: DocStatusHookOptions) => Ret;
|
|
1191
|
+
type ColStatusHookFor<P extends string, Ret> = keyof ParamsOf<P> extends never ? (params?: Record<string, string>, options?: ColStatusHookOptions) => Ret : (params: ParamsOf<P>, options?: ColStatusHookOptions) => Ret;
|
|
1192
|
+
type SyncStatusHookFor<E> = E extends DocEntry<any, infer P> ? DocStatusHookFor<P, SyncStatus> : E extends ColEntry<any, infer P> ? ColStatusHookFor<P, SyncStatus> : never;
|
|
1193
|
+
type LoadingStatusHookFor<E> = E extends DocEntry<any, infer P> ? DocStatusHookFor<P, LoadingStatus> : E extends ColEntry<any, infer P> ? ColStatusHookFor<P, LoadingStatus> : never;
|
|
1194
|
+
type FirestateApi<R extends FirestateRegistry> = { [K in keyof R & string as HookName<K>]: HookFor<R[K]> } & { [K in keyof R & string as R[K] extends BaseEntry ? SyncStatusHookName<K> : never]: SyncStatusHookFor<R[K]> } & { [K in keyof R & string as R[K] extends BaseEntry ? LoadingStatusHookName<K> : never]: LoadingStatusHookFor<R[K]> };
|
|
753
1195
|
/**
|
|
754
1196
|
* Turn a Firestate registry into a map of typed React hooks. Each entry
|
|
755
1197
|
* `K` produces a hook named `use{Capitalize<K>}`.
|
|
@@ -763,7 +1205,7 @@ type FirestateApi<R extends FirestateRegistry> = { [K in keyof R & string as Hoo
|
|
|
763
1205
|
*/
|
|
764
1206
|
declare function createFirestate<R extends FirestateRegistry>(registry: R): FirestateApi<R>;
|
|
765
1207
|
//#endregion
|
|
766
|
-
//#region src/diff.d.ts
|
|
1208
|
+
//#region src/utils/diff.d.ts
|
|
767
1209
|
/**
|
|
768
1210
|
* Check if two values are deeply equal
|
|
769
1211
|
*/
|
|
@@ -821,11 +1263,44 @@ declare const isDiffEmpty: (diff: Record<string, unknown>) => boolean;
|
|
|
821
1263
|
* VectorValue) are NOT flattened — they're preserved at their path so
|
|
822
1264
|
* Firestore receives them in their original form.
|
|
823
1265
|
*
|
|
1266
|
+
* ⚠️ The dot-joined keys this produces are AMBIGUOUS to Firestore if any map
|
|
1267
|
+
* key itself contains a "." (e.g. an email key `a@b.com`): `updateDoc` parses
|
|
1268
|
+
* the "." as a path separator and writes to the wrong nested field. The
|
|
1269
|
+
* write path uses {@link flattenDiffToFieldPaths} + `FieldPath` instead, which
|
|
1270
|
+
* keeps every segment literal. Reach for this only when dotted-string keys are
|
|
1271
|
+
* what you actually want.
|
|
1272
|
+
*
|
|
824
1273
|
* @param diff - The nested diff object
|
|
825
1274
|
* @param prefix - Internal: current path prefix for recursion
|
|
826
1275
|
* @returns Flattened object with dotted keys
|
|
827
1276
|
*/
|
|
828
1277
|
declare const flattenDiff: (diff: Record<string, unknown>, prefix?: string) => Record<string, unknown>;
|
|
1278
|
+
/**
|
|
1279
|
+
* Flatten a nested diff into a list of `{ segments, value }` entries, where
|
|
1280
|
+
* `segments` is the path expressed as an array of *literal* key segments
|
|
1281
|
+
* (never joined into a dotted string).
|
|
1282
|
+
*
|
|
1283
|
+
* This is the segment-preserving sibling of {@link flattenDiff}. It exists
|
|
1284
|
+
* because dot-joined string keys are ambiguous to Firestore: `updateDoc(ref,
|
|
1285
|
+
* { "users.a@b.com.role": 4 })` parses the `.` inside the email key as a path
|
|
1286
|
+
* separator and writes to `users → "a@b" → "com" → role` instead of the
|
|
1287
|
+
* literal key `"a@b.com"`. Feeding these segments to `new FieldPath(...segments)`
|
|
1288
|
+
* (with the variadic `updateDoc(ref, fieldPath, value, …)` form) keeps every
|
|
1289
|
+
* segment literal, so a "." inside a map key is never re-interpreted.
|
|
1290
|
+
*
|
|
1291
|
+
* The opaque/plain-object rules mirror {@link flattenDiff} exactly: arrays,
|
|
1292
|
+
* FieldValue sentinels (deleteField, serverTimestamp, …), and Firestore value
|
|
1293
|
+
* types (Timestamp, DocumentReference, GeoPoint, Bytes, VectorValue) ride
|
|
1294
|
+
* through verbatim at their path; only plain objects are recursed into.
|
|
1295
|
+
*
|
|
1296
|
+
* @param diff - The nested diff object
|
|
1297
|
+
* @param prefix - Internal: accumulated path segments for recursion
|
|
1298
|
+
* @returns Flat list of `{ segments, value }` entries
|
|
1299
|
+
*/
|
|
1300
|
+
declare const flattenDiffToFieldPaths: (diff: Record<string, unknown>, prefix?: string[]) => Array<{
|
|
1301
|
+
segments: string[];
|
|
1302
|
+
value: unknown;
|
|
1303
|
+
}>;
|
|
829
1304
|
/**
|
|
830
1305
|
* Merge two diffs together, with the second taking precedence
|
|
831
1306
|
*/
|
|
@@ -928,7 +1403,29 @@ declare const createDiffAtPath: (path: string, value: unknown) => Record<string,
|
|
|
928
1403
|
*/
|
|
929
1404
|
declare const unflattenDiff: (flatDiff: Record<string, unknown>) => Record<string, unknown>;
|
|
930
1405
|
//#endregion
|
|
931
|
-
//#region src/
|
|
1406
|
+
//#region src/utils/shallow.d.ts
|
|
1407
|
+
/**
|
|
1408
|
+
* Shallow structural equality.
|
|
1409
|
+
*
|
|
1410
|
+
* Returns `true` when `a` and `b` are identical by `Object.is`, or are two
|
|
1411
|
+
* arrays / two plain objects whose entries are pairwise `Object.is`-equal one
|
|
1412
|
+
* level deep. Anything else (different shapes, nested objects that aren't
|
|
1413
|
+
* reference-equal) is `false`.
|
|
1414
|
+
*
|
|
1415
|
+
* Intended as the `isEqual` for a hook `selector` that builds a fresh array or
|
|
1416
|
+
* object every render — e.g. `data => Object.values(data).map(d => d.id)` or
|
|
1417
|
+
* `data => ({ name: data?.name, done: data?.done })`. The default selector
|
|
1418
|
+
* comparison is a *deep* value compare, which is correct but does more work
|
|
1419
|
+
* than needed for a flat projection; `shallow` re-renders on a genuine change
|
|
1420
|
+
* to any entry while collapsing the fresh-reference-same-entries case.
|
|
1421
|
+
*
|
|
1422
|
+
* Not recursive on purpose: if a selected entry is itself an object you mutate
|
|
1423
|
+
* in place rather than replace, prefer the default deep comparison or a
|
|
1424
|
+
* bespoke `isEqual`.
|
|
1425
|
+
*/
|
|
1426
|
+
declare const shallow: <T>(a: T, b: T) => boolean;
|
|
1427
|
+
//#endregion
|
|
1428
|
+
//#region src/core/document.d.ts
|
|
932
1429
|
/**
|
|
933
1430
|
* Options for creating a document subscription
|
|
934
1431
|
*/
|
|
@@ -981,7 +1478,7 @@ declare const createDocumentSubscription: <TData extends FirestoreObject>(option
|
|
|
981
1478
|
sync: () => Promise<void>;
|
|
982
1479
|
};
|
|
983
1480
|
//#endregion
|
|
984
|
-
//#region src/collection.d.ts
|
|
1481
|
+
//#region src/core/collection.d.ts
|
|
985
1482
|
/**
|
|
986
1483
|
* Options for creating a collection subscription
|
|
987
1484
|
*/
|
|
@@ -1031,7 +1528,7 @@ declare const createCollectionSubscription: <TData extends FirestoreObject>(opti
|
|
|
1031
1528
|
sync: () => Promise<void>;
|
|
1032
1529
|
};
|
|
1033
1530
|
//#endregion
|
|
1034
|
-
//#region src/provider.d.ts
|
|
1531
|
+
//#region src/react/provider.d.ts
|
|
1035
1532
|
/**
|
|
1036
1533
|
* Props for FirestateProvider
|
|
1037
1534
|
*/
|
|
@@ -1147,5 +1644,5 @@ declare const FirestateStoreProvider: React.FC<FirestateStoreProviderProps>;
|
|
|
1147
1644
|
*/
|
|
1148
1645
|
declare const useUnsavedChangesBlocker: () => boolean;
|
|
1149
1646
|
//#endregion
|
|
1150
|
-
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 };
|
|
1647
|
+
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 };
|
|
1151
1648
|
//# sourceMappingURL=index.d.mts.map
|