Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,17 @@ Preserve these unless the task explicitly changes them.
(`update`/`set`/`delete`/`add`/`remove`/`load`/`sync`) and `ref` — status
fields are absent unless the selector folds them in, so a status flip the
selector ignores (e.g. `isSynced` churning on a save) cannot re-render it.
Writers/`ref` are read live from the subscription (the snapshot is the state;
the handle is read separately), not from the memoized selection, so a rebuilt
subscription always surfaces its own methods even when the selected slice is
value-equal.
Writers/`ref` are read from the subscription's handle (the snapshot is the
state; the handle is read separately), not from the memoized selection.
- Every method on a hook's handle (`update`/`set`/`delete`/`add`/`remove`/
`load`/`sync`) looks up the facade's *current* subscription when it is
called, not when the hook renders, and keeps one identity per resolved
`shared` facade (safe in `useCallback`/`useEffect` deps). The facade can switch
subscriptions after render — `acquire()` adopts a sibling's committed entry,
or rebuilds a stopped one after refCount hit 0 — and a value-equal slice never
re-runs the handle memo. Methods copied at render time therefore wrote into an
abandoned subscription, which dropped the write for want of a snapshot
(HSF-1642). Never copy methods off `getHandle()` into the returned handle.
- A hook called WITHOUT a selector returns the **sync-agnostic default handle**:
`{ data, isLoaded, error, ...writers, ref }` for a document, plus `isActive`
for a collection. It deliberately drops `isSynced` (and `isLoading`, folded
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,12 @@ const id = add({ name: 'New Space', area: 500, floor: 1 })
remove('oldSpaceId')
```

Handle methods (`update`, `set`, `delete`, `add`, `remove`, `load`, `sync`) keep
their identity while the hook's resource (definition, path, and query) stays the
same, so they are safe in `useCallback` and `useEffect` dependency lists. Every
call reaches the resource's live subscription, whatever order the hooks on that
resource mounted in.

#### Selecting a slice (`selector` + `isEqual`)

By default a component re-renders when the data, the load state (`isLoaded`), or
Expand Down
19 changes: 13 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,19 @@ Important details:
or the standalone `useDocumentSyncStatus`/… helpers) are thin readers over
`useDocument`/`useCollection` with a fixed read-only selector, so they resolve
the same shared entry — one listener — and read the same optimistic state.
- Either way the projection deliberately excludes methods and `ref`; those are
read *live* from the current subscription's `getHandle()` at render time (a
separate read from the state snapshot). Otherwise a subscription rebuild whose
projection happened to be value-equal would be collapsed by `isEqual`, and the
hook would keep handing back the previous subscription's methods (e.g. `load()`
against torn-down constraints).
- Either way the projection deliberately excludes methods and `ref`. Otherwise a
subscription rebuild whose projection happened to be value-equal would be
collapsed by `isEqual`, and the hook would keep handing back the previous
subscription's methods (e.g. `load()` against torn-down constraints). `ref` is
read from `getHandle()` when the handle is built. Methods are wrappers built
once per `shared` facade that call `getHandle()[method]` when invoked, so they
always reach the facade's *current* subscription. This matters because the
facade can switch subscriptions after render without the projection changing:
hooks that render together each build a provisional entry, and `acquire()`
makes every hook after the first adopt the first one's entry. Likewise, a
re-acquire after refCount hit 0 rebuilds the entry's stopped subscription.
Methods copied at render time would keep calling the abandoned subscription,
which has no snapshot and drops the write.
- Disabled hooks do not resolve params or create subscriptions.
- Toggling `undoable` should not rebuild Firestore listeners.
- `queryConstraints` are keyed by semantic query identity, not array
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hvakr/firestate",
"version": "0.2.1",
"version": "0.2.2",
"description": "Firestore state management for React with real-time sync, undo/redo, optimistic updates, and Zod schema validation",
"author": "HVAKR",
"license": "MIT",
Expand Down
Loading
Loading