@data-client/core 0.16.7 → 0.18.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/CHANGELOG.md +140 -0
- package/README.md +1 -1
- package/dist/index.js +17 -14
- package/dist/index.umd.min.js +1 -1
- package/dist/mock.js +17 -14
- package/legacy/controller/Controller.js +19 -16
- package/lib/controller/Controller.d.ts.map +1 -1
- package/lib/controller/Controller.js +19 -16
- package/package.json +3 -3
- package/src/controller/Controller.ts +17 -16
- package/src/controller/__tests__/getResponse.ts +144 -1
- package/src/controller/__tests__/getResponseMeta-countRef.ts +93 -0
- package/src/controller/__tests__/getResponseMeta-paths.ts +88 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Endpoint, Entity } from '@data-client/endpoint';
|
|
2
|
+
|
|
3
|
+
import { initialState } from '../../state/reducer/createReducer';
|
|
4
|
+
import Controller from '../Controller';
|
|
5
|
+
|
|
6
|
+
/** Integration test: `Controller.getResponseMeta()` is the seam every read
|
|
7
|
+
* hook (`useSuspense`, `useCache`, `useFetch`, `useDLE`, `useLive`) calls.
|
|
8
|
+
* Multiple subscribers to the same endpoint + args + entities must observe
|
|
9
|
+
* the same `expiresAt`, otherwise late subscribers silently skip the
|
|
10
|
+
* entity-expiry refetch in `useSuspense`'s gate
|
|
11
|
+
* `if (Date.now() <= expiresAt && !forceFetch) return;`.
|
|
12
|
+
*
|
|
13
|
+
* The bug fixed by this PR was a `paths.shift()` mutation on the journey
|
|
14
|
+
* stored by reference on `WeakDependencyMap.Link.journey`. Each successive
|
|
15
|
+
* cache hit returned a journey one entry shorter than the last. The first
|
|
16
|
+
* entity dropped is the one with the *earliest* expiry, so subscriber 2's
|
|
17
|
+
* `entityExpiresAt(paths, …)` skipped it and computed a too-late
|
|
18
|
+
* `expiresAt`; subscriber 3 saw an empty array and got `Infinity`.
|
|
19
|
+
*
|
|
20
|
+
* The path is GC-independent: it fires under `ImmortalGCPolicy` too, since
|
|
21
|
+
* `entityExpiresAt` is unconditional whenever the endpoint has no
|
|
22
|
+
* top-level `meta.expiresAt` (the common case for state populated via
|
|
23
|
+
* `controller.set(Entity, …)`, SSR hydration, or `useQuery`).
|
|
24
|
+
*/
|
|
25
|
+
describe('Controller.getResponseMeta repeated calls', () => {
|
|
26
|
+
it('returns identical expiresAt across subscribers (journey is not consumed by reads)', () => {
|
|
27
|
+
class Foo extends Entity {
|
|
28
|
+
id = '';
|
|
29
|
+
pk() {
|
|
30
|
+
return this.id;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const ep = new Endpoint(() => Promise.resolve(), {
|
|
34
|
+
key: () => 'listFoo',
|
|
35
|
+
schema: [Foo],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const controller = new Controller({});
|
|
39
|
+
|
|
40
|
+
// Endpoint slot exists (so we hit the `denormalize` path) but the
|
|
41
|
+
// endpoint itself has no top-level `meta.expiresAt` yet — mirrors
|
|
42
|
+
// apps that hydrate from SSR or use `controller.set(Entity, …)`
|
|
43
|
+
// without going through an endpoint fetch. In that shape,
|
|
44
|
+
// `Controller.getResponseMeta()` falls back to walking per-entity
|
|
45
|
+
// meta via `entityExpiresAt(paths, …)`. Foo|'1' has the earliest
|
|
46
|
+
// expiry — the first journey entry the buggy `paths.shift()` would
|
|
47
|
+
// drop.
|
|
48
|
+
const FOO_1_EXPIRY = 100;
|
|
49
|
+
const FOO_2_EXPIRY = 1_000_000;
|
|
50
|
+
const state = {
|
|
51
|
+
...initialState,
|
|
52
|
+
entities: {
|
|
53
|
+
Foo: {
|
|
54
|
+
'1': { id: '1' },
|
|
55
|
+
'2': { id: '2' },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
entitiesMeta: {
|
|
59
|
+
Foo: {
|
|
60
|
+
'1': { date: 0, fetchedAt: 0, expiresAt: FOO_1_EXPIRY },
|
|
61
|
+
'2': { date: 0, fetchedAt: 0, expiresAt: FOO_2_EXPIRY },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
endpoints: {
|
|
65
|
+
[ep.key()]: ['1', '2'],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Three successive calls simulate three React components mounting
|
|
70
|
+
// with the same endpoint + args. Call 1 is a result-cache miss;
|
|
71
|
+
// calls 2 and 3 are hits.
|
|
72
|
+
const m1 = controller.getResponseMeta(ep, state);
|
|
73
|
+
const m2 = controller.getResponseMeta(ep, state);
|
|
74
|
+
const m3 = controller.getResponseMeta(ep, state);
|
|
75
|
+
|
|
76
|
+
// Subscriber 1 picks the earliest entity expiry. Subscribers 2+ must
|
|
77
|
+
// pick the same one — pre-fix they observed FOO_2_EXPIRY (m2) and
|
|
78
|
+
// Infinity (m3), silently suppressing entity-expiry refetch.
|
|
79
|
+
expect(m1.expiresAt).toBe(FOO_1_EXPIRY);
|
|
80
|
+
expect(m2.expiresAt).toBe(m1.expiresAt);
|
|
81
|
+
expect(m3.expiresAt).toBe(m1.expiresAt);
|
|
82
|
+
|
|
83
|
+
// Cached data must remain referentially equal across hits — the
|
|
84
|
+
// memo contract `useSuspense` etc. rely on for skip-rerender.
|
|
85
|
+
expect(m2.data).toBe(m1.data);
|
|
86
|
+
expect(m3.data).toBe(m1.data);
|
|
87
|
+
});
|
|
88
|
+
});
|