Commit 21855f8
* test(client-react): give the package a test harness and pin realtime hook behavior (#4682)
`packages/client-react` shipped 8 public hooks with `build` and `typecheck` as
its only scripts and zero test files. `tsc --noEmit` is structurally blind to
what actually breaks in a hook: a dependency array is a value, not a type, so a
missing entry, a missing cleanup and a callback that never fires all typecheck
perfectly. #4678 was exactly that shape — `useAutoRefresh` ignored predicate
writes, the case that dirties a list hardest, and no type noticed.
Adds the workspace's first DOM test environment (jsdom + @testing-library/react,
`environment: 'jsdom'`; every other package runs `node`) and 17 tests over the
realtime hooks covering the three things the type checker cannot see:
- re-subscription driven by the dependency array — changing `object` opens a
subscription on the new name and releases the old one, an unrelated re-render
churns nothing, and the hooks key on the primitive `options?.recordId` /
`options?.packageId` rather than the options object's identity, so an
equal-but-new object stays a no-op;
- release on unmount, including `useAutoRefresh`, which holds two subscriptions;
- delivery — events reach state and callbacks, and `useAutoRefresh` refetches on
the per-record *and* the bulk stream (the #4678 regression pin).
Every assertion was verified by sabotage rather than assumed: dropping the
`object` dep (1 failure), deleting a cleanup (3) and reverting `useAutoRefresh`
to the single-stream version (3) each turn the suite red; reverting turns it
green again.
CI needs no wiring — `Test Core` partitions by package off `turbo ls`, and the
new `test` script puts client-react on shard 2 (verified with
scripts/partition-test-shards.mjs).
No runtime code changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYnZrTwXbrctB8E8HpJAPT
* fix(client-react): typecheck the package's own tests instead of excluding them
`check:type-check-coverage` failed on the first push, and it was right. The
initial commit added `**/*.test.tsx` to tsconfig's `exclude` to match the
`**/*.test.ts` entry every sibling package carries — but that entry is frozen
DEBT the repo is migrating away from (#4311), not a convention to copy. The
gate's own summary makes it explicit: 21 packages still exclude their tests,
carrying 2243 frozen raw errors in TEST_DEBT. Excluding one more would have
reported green over source `tsc` never read.
Drops the test exclusion entirely, so client-react typechecks its own tests and
stays out of that ledger.
Doing so immediately surfaced a real gap: the `METADATA_EVENT` fixture was
declared and never used, because `useMetadataSubscription` was covered for
re-subscription and unmount but not for delivery. Adds that assertion rather
than deleting the fixture — 18 tests now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYnZrTwXbrctB8E8HpJAPT
* fix(client-react): stop five hooks from looping on dependency identity (#4693, #4694)
Five hooks keyed a `useCallback`/`useEffect` on values the caller supplies
inline — `where`/`fields`/`orderBy` objects, `onSuccess`/`onError` handlers, and
the `fetcher` `useMetadata` takes as a required positional argument. Inline
means a fresh identity every render, so the effect re-ran every render; because
the fetch hooks call `setState`, that render caused another. Under the hooks'
own documented usage this was an unbounded request loop.
Requests issued in 250ms by one mounted component, before → after:
useQuery (inline where) 4691 → 1
useInfiniteQuery (inline where) 6611 → 1
useObject (NO options at all) 4306 → 1
useView (inline onSuccess) 8197 → 1
useMetadata (inline fetcher) 7654 → 1
useObject and useMetadata needed no particular usage to loop: the former
depended on its own `data`/`etag` state while writing both, the latter takes its
fetcher positionally so there is no non-inline way to call it. useMutation was
never affected — no effect drives it.
The same root cause churned the realtime subscriptions (#4694): useAutoRefresh
with an unmemoized `refetch` — which is exactly what useQuery returned every
render — resubscribed on both streams every render, losing any event delivered
in the unsubscribe/resubscribe gap.
Adds two internal primitives (not exported): `stableKey` derives a dependency
from a structural VALUE (sorted keys, array order preserved, since `orderBy` is
positional) so a rebuilt-but-equal object is a no-op; `useEventCallback` gives a
handler a fixed identity while always invoking its latest version, synced in an
effect rather than during render so a discarded concurrent render cannot publish
a handler that never committed.
Fixing this by asking callers to memoize was rejected: the TSDoc examples
themselves pass object literals, and correctness must not rest on every call
site remembering `useMemo`.
13 tests, each verified by reverting the fix it guards — restoring identity deps
in useQuery (3 red), useObject's self-referential state (3), useMetadata's
fetcher dep (2) and the subscription callback dep (2). Counts are asserted
exactly rather than as an upper bound, which would pass on a loop that merely
got slower. Coverage includes the inverse direction: a changed value still
refetches, and every stabilized handler runs its newest version, so the ref
indirection cannot silently trade a loop for a stale closure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYnZrTwXbrctB8E8HpJAPT
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 071d0dc commit 21855f8
6 files changed
Lines changed: 586 additions & 37 deletions
File tree
- .changeset
- packages/client-react/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
113 | 114 | | |
114 | 115 | | |
115 | 116 | | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
116 | 137 | | |
117 | 138 | | |
118 | 139 | | |
| |||
141 | 162 | | |
142 | 163 | | |
143 | 164 | | |
144 | | - | |
| 165 | + | |
145 | 166 | | |
146 | 167 | | |
147 | 168 | | |
148 | | - | |
| 169 | + | |
149 | 170 | | |
150 | 171 | | |
151 | 172 | | |
152 | 173 | | |
153 | | - | |
| 174 | + | |
154 | 175 | | |
155 | 176 | | |
156 | 177 | | |
| |||
520 | 541 | | |
521 | 542 | | |
522 | 543 | | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
523 | 556 | | |
524 | 557 | | |
525 | 558 | | |
| |||
564 | 597 | | |
565 | 598 | | |
566 | 599 | | |
567 | | - | |
| 600 | + | |
568 | 601 | | |
569 | 602 | | |
570 | 603 | | |
571 | | - | |
| 604 | + | |
572 | 605 | | |
573 | 606 | | |
574 | 607 | | |
575 | 608 | | |
576 | | - | |
| 609 | + | |
577 | 610 | | |
578 | 611 | | |
579 | 612 | | |
| |||
0 commit comments