fetchium 0.1.0 → 0.1.1

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.
Files changed (138) hide show
  1. package/CHANGELOG.md +6 -5
  2. package/dist/cjs/development/QueryClient-CLi3ONNM.js +2 -0
  3. package/dist/cjs/development/QueryClient-CLi3ONNM.js.map +1 -0
  4. package/dist/cjs/development/QueryController-BQA49OYU.js +2 -0
  5. package/dist/cjs/development/QueryController-BQA49OYU.js.map +1 -0
  6. package/dist/cjs/development/index.js +1 -1
  7. package/dist/cjs/development/index.js.map +1 -1
  8. package/dist/cjs/development/mutation-CikIl_6k.js +2 -0
  9. package/dist/cjs/development/mutation-CikIl_6k.js.map +1 -0
  10. package/dist/cjs/development/react/index.js +1 -1
  11. package/dist/cjs/development/rest/index.js +2 -0
  12. package/dist/cjs/development/rest/index.js.map +1 -0
  13. package/dist/cjs/development/topic/index.js +2 -0
  14. package/dist/cjs/development/topic/index.js.map +1 -0
  15. package/dist/cjs/production/QueryClient-N0MJmuHW.js +2 -0
  16. package/dist/cjs/production/QueryClient-N0MJmuHW.js.map +1 -0
  17. package/dist/cjs/production/QueryController-BQA49OYU.js +2 -0
  18. package/dist/cjs/production/QueryController-BQA49OYU.js.map +1 -0
  19. package/dist/cjs/production/index.js +1 -1
  20. package/dist/cjs/production/index.js.map +1 -1
  21. package/dist/cjs/production/mutation-P_Yb4LI9.js +2 -0
  22. package/dist/cjs/production/mutation-P_Yb4LI9.js.map +1 -0
  23. package/dist/cjs/production/react/index.js +1 -1
  24. package/dist/cjs/production/rest/index.js +2 -0
  25. package/dist/cjs/production/rest/index.js.map +1 -0
  26. package/dist/cjs/production/topic/index.js +2 -0
  27. package/dist/cjs/production/topic/index.js.map +1 -0
  28. package/dist/esm/MutationResult.d.ts +0 -1
  29. package/dist/esm/MutationResult.d.ts.map +1 -1
  30. package/dist/esm/QueryClient.d.ts +26 -4
  31. package/dist/esm/QueryClient.d.ts.map +1 -1
  32. package/dist/esm/QueryController.d.ts +49 -0
  33. package/dist/esm/QueryController.d.ts.map +1 -0
  34. package/dist/esm/QueryResult.d.ts +10 -10
  35. package/dist/esm/QueryResult.d.ts.map +1 -1
  36. package/dist/esm/development/QueryClient-Dtde3pss.js +2572 -0
  37. package/dist/esm/development/QueryClient-Dtde3pss.js.map +1 -0
  38. package/dist/esm/development/QueryController-Ch_ncxiI.js +14 -0
  39. package/dist/esm/development/QueryController-Ch_ncxiI.js.map +1 -0
  40. package/dist/esm/development/index.js +29 -100
  41. package/dist/esm/development/index.js.map +1 -1
  42. package/dist/esm/development/mutation-UZshUQAf.js +58 -0
  43. package/dist/esm/development/mutation-UZshUQAf.js.map +1 -0
  44. package/dist/esm/development/react/index.js +1 -1
  45. package/dist/esm/development/rest/index.js +142 -0
  46. package/dist/esm/development/rest/index.js.map +1 -0
  47. package/dist/esm/development/{shared-Dq2yW78d.js → shared-DcuVH8Pf.js} +5 -5
  48. package/dist/esm/development/{shared-Dq2yW78d.js.map → shared-DcuVH8Pf.js.map} +1 -1
  49. package/dist/esm/development/stores/async.js +6 -6
  50. package/dist/esm/development/stores/sync.js +5 -5
  51. package/dist/esm/development/topic/index.js +86 -0
  52. package/dist/esm/development/topic/index.js.map +1 -0
  53. package/dist/esm/index.d.ts +5 -4
  54. package/dist/esm/index.d.ts.map +1 -1
  55. package/dist/esm/mutation.d.ts +6 -19
  56. package/dist/esm/mutation.d.ts.map +1 -1
  57. package/dist/esm/production/{QueryClient-BP0Z1rQV.js → QueryClient-YqnBxFy1.js} +972 -968
  58. package/dist/esm/production/QueryClient-YqnBxFy1.js.map +1 -0
  59. package/dist/esm/production/QueryController-Ch_ncxiI.js +14 -0
  60. package/dist/esm/production/QueryController-Ch_ncxiI.js.map +1 -0
  61. package/dist/esm/production/index.js +29 -100
  62. package/dist/esm/production/index.js.map +1 -1
  63. package/dist/esm/production/mutation-pgFl1uIY.js +58 -0
  64. package/dist/esm/production/mutation-pgFl1uIY.js.map +1 -0
  65. package/dist/esm/production/react/index.js +1 -1
  66. package/dist/esm/production/rest/index.js +142 -0
  67. package/dist/esm/production/rest/index.js.map +1 -0
  68. package/dist/esm/production/{shared-Dq2yW78d.js → shared-DcuVH8Pf.js} +5 -5
  69. package/dist/esm/production/{shared-Dq2yW78d.js.map → shared-DcuVH8Pf.js.map} +1 -1
  70. package/dist/esm/production/stores/async.js +6 -6
  71. package/dist/esm/production/stores/sync.js +5 -5
  72. package/dist/esm/production/topic/index.js +86 -0
  73. package/dist/esm/production/topic/index.js.map +1 -0
  74. package/dist/esm/query-types.d.ts +2 -4
  75. package/dist/esm/query-types.d.ts.map +1 -1
  76. package/dist/esm/query.d.ts +17 -39
  77. package/dist/esm/query.d.ts.map +1 -1
  78. package/dist/esm/rest/RESTMutation.d.ts +18 -0
  79. package/dist/esm/rest/RESTMutation.d.ts.map +1 -0
  80. package/dist/esm/rest/RESTQuery.d.ts +24 -0
  81. package/dist/esm/rest/RESTQuery.d.ts.map +1 -0
  82. package/dist/esm/rest/RESTQueryController.d.ts +34 -0
  83. package/dist/esm/rest/RESTQueryController.d.ts.map +1 -0
  84. package/dist/esm/rest/index.d.ts +5 -0
  85. package/dist/esm/rest/index.d.ts.map +1 -0
  86. package/dist/esm/stores/shared.d.ts.map +1 -1
  87. package/dist/esm/testing/MockClient.d.ts +64 -0
  88. package/dist/esm/testing/MockClient.d.ts.map +1 -0
  89. package/dist/esm/testing/auto-generate.d.ts +20 -0
  90. package/dist/esm/testing/auto-generate.d.ts.map +1 -0
  91. package/dist/esm/testing/entity-factory.d.ts +13 -0
  92. package/dist/esm/testing/entity-factory.d.ts.map +1 -0
  93. package/dist/esm/testing/index.d.ts +6 -0
  94. package/dist/esm/testing/index.d.ts.map +1 -0
  95. package/dist/esm/testing/types.d.ts +37 -0
  96. package/dist/esm/testing/types.d.ts.map +1 -0
  97. package/dist/esm/topic/TopicQuery.d.ts +10 -0
  98. package/dist/esm/topic/TopicQuery.d.ts.map +1 -0
  99. package/dist/esm/topic/TopicQueryController.d.ts +43 -0
  100. package/dist/esm/topic/TopicQueryController.d.ts.map +1 -0
  101. package/dist/esm/topic/index.d.ts +3 -0
  102. package/dist/esm/topic/index.d.ts.map +1 -0
  103. package/dist/esm/typeDefs.d.ts +1 -1
  104. package/dist/esm/types.d.ts +9 -4
  105. package/dist/esm/types.d.ts.map +1 -1
  106. package/package.json +51 -4
  107. package/plugin/.claude-plugin/plugin.json +10 -0
  108. package/plugin/agents/fetchium.md +168 -0
  109. package/plugin/docs/api/fetchium-react.md +135 -0
  110. package/plugin/docs/api/fetchium.md +674 -0
  111. package/plugin/docs/api/stores-async.md +219 -0
  112. package/plugin/docs/api/stores-sync.md +133 -0
  113. package/plugin/docs/core/entities.md +351 -0
  114. package/plugin/docs/core/queries.md +600 -0
  115. package/plugin/docs/core/streaming.md +550 -0
  116. package/plugin/docs/core/types.md +374 -0
  117. package/plugin/docs/data/caching.md +298 -0
  118. package/plugin/docs/data/live-data.md +435 -0
  119. package/plugin/docs/data/mutations.md +465 -0
  120. package/plugin/docs/guides/auth.md +318 -0
  121. package/plugin/docs/guides/error-handling.md +351 -0
  122. package/plugin/docs/guides/offline.md +270 -0
  123. package/plugin/docs/guides/testing.md +301 -0
  124. package/plugin/docs/quickstart.md +170 -0
  125. package/plugin/docs/reference/pagination.md +519 -0
  126. package/plugin/docs/reference/rest-queries.md +107 -0
  127. package/plugin/docs/reference/why-signalium.md +364 -0
  128. package/plugin/docs/setup/project-setup.md +319 -0
  129. package/plugin/install.mjs +88 -0
  130. package/plugin/skills/design/SKILL.md +140 -0
  131. package/plugin/skills/teach/SKILL.md +105 -0
  132. package/dist/cjs/development/QueryClient-CpmwggOn.js +0 -2
  133. package/dist/cjs/development/QueryClient-CpmwggOn.js.map +0 -1
  134. package/dist/cjs/production/QueryClient-qi3bR0eD.js +0 -2
  135. package/dist/cjs/production/QueryClient-qi3bR0eD.js.map +0 -1
  136. package/dist/esm/development/QueryClient-DRZtPKFD.js +0 -2568
  137. package/dist/esm/development/QueryClient-DRZtPKFD.js.map +0 -1
  138. package/dist/esm/production/QueryClient-BP0Z1rQV.js.map +0 -1
@@ -0,0 +1,270 @@
1
+ ---
2
+ title: Offline & Persistence
3
+ ---
4
+
5
+ Fetchium has built-in support for offline operation and query persistence. It can detect network status, pause queries when the device goes offline, and persist query results across sessions so your application works even without a connection.
6
+
7
+ This guide covers the offline and persistence-specific pieces of Fetchium's infrastructure. For the caching time knobs (`staleTime`, `gcTime`, `cacheTime`) and cache invalidation patterns, see [Caching & Refetching](/data/caching).
8
+
9
+ ---
10
+
11
+ ## Network Manager
12
+
13
+ The `NetworkManager` tracks whether the device is online or offline. It automatically listens to browser `online` and `offline` events, and exposes the current status as a reactive signal so that queries can pause and resume automatically.
14
+
15
+ ### Basic usage
16
+
17
+ ```tsx
18
+ import { NetworkManager } from 'fetchium';
19
+
20
+ const networkManager = new NetworkManager();
21
+ ```
22
+
23
+ The network manager is passed to the `QueryClient` constructor:
24
+
25
+ ```tsx
26
+ import { QueryClient } from 'fetchium';
27
+ import { SyncQueryStore, MemoryPersistentStore } from 'fetchium/stores/sync';
28
+
29
+ const store = new SyncQueryStore(new MemoryPersistentStore());
30
+ const networkManager = new NetworkManager();
31
+
32
+ const client = new QueryClient(store, { fetch }, networkManager);
33
+ ```
34
+
35
+ If you do not provide a `NetworkManager`, the `QueryClient` creates one automatically.
36
+
37
+ ### Manual override
38
+
39
+ For testing or custom scenarios, you can manually override the network status:
40
+
41
+ ```tsx
42
+ networkManager.setNetworkStatus(false);
43
+ networkManager.setNetworkStatus(true);
44
+ networkManager.clearManualOverride();
45
+ ```
46
+
47
+ When a manual override is active, the browser's actual connectivity events are ignored.
48
+
49
+ ### Reactive signal
50
+
51
+ The network manager exposes its status as a reactive signal. You can read it directly in reactive functions:
52
+
53
+ ```tsx
54
+ const onlineSignal = networkManager.getOnlineSignal();
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Network Modes
60
+
61
+ Each query can configure how it behaves when the device is offline. Set `networkMode` in the query's `config` property:
62
+
63
+ ```tsx
64
+ import { t, NetworkMode } from 'fetchium';
65
+ import { RESTQuery } from 'fetchium/rest';
66
+
67
+ class GetUser extends RESTQuery {
68
+ params = { id: t.id };
69
+
70
+ path = `/users/${this.params.id}`;
71
+
72
+ result = { id: t.id, name: t.string };
73
+
74
+ config = {
75
+ networkMode: NetworkMode.OfflineFirst,
76
+ };
77
+ }
78
+ ```
79
+
80
+ There are three network modes:
81
+
82
+ ### `NetworkMode.Online` (default)
83
+
84
+ The query only fetches when the device is online. If the device goes offline while a query is active, the query pauses and resumes automatically when connectivity is restored.
85
+
86
+ This is the safest default --- it prevents failed requests and unnecessary retries while offline.
87
+
88
+ ### `NetworkMode.Always`
89
+
90
+ The query fetches regardless of network status. Use this when you have a local server, service worker, or other mechanism that can handle requests even without internet access.
91
+
92
+ ### `NetworkMode.OfflineFirst`
93
+
94
+ If cached data exists, the query returns it immediately even when offline. When the device comes back online, the query refetches to get fresh data (assuming the data is stale).
95
+
96
+ This mode is ideal for applications that need to show something to the user even when there is no connection.
97
+
98
+ {% callout %}
99
+ When using `NetworkMode.OfflineFirst`, pair it with a `QueryStore` that persists data across sessions. Otherwise, the cache will be empty on a fresh app launch and there will be nothing to show while offline.
100
+ {% /callout %}
101
+
102
+ ### Refetch on reconnect
103
+
104
+ By default, queries with `NetworkMode.Online` or `NetworkMode.OfflineFirst` refetch stale data when the device reconnects. You can disable this behavior:
105
+
106
+ ```tsx
107
+ config = {
108
+ networkMode: NetworkMode.Online,
109
+ refreshStaleOnReconnect: false,
110
+ };
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Query Stores
116
+
117
+ A query store is responsible for persisting query results and entity data to a durable backend. Fetchium provides two implementations: a synchronous store for in-memory or localStorage-style backends, and an asynchronous store for IndexedDB, AsyncStorage, or cross-worker architectures.
118
+
119
+ ### SyncQueryStore
120
+
121
+ The `SyncQueryStore` wraps a synchronous key-value store. It is the simplest option and works well for most applications.
122
+
123
+ ```tsx
124
+ import { SyncQueryStore, MemoryPersistentStore } from 'fetchium/stores/sync';
125
+
126
+ const store = new SyncQueryStore(new MemoryPersistentStore());
127
+ const client = new QueryClient(store, { fetch });
128
+ ```
129
+
130
+ The `MemoryPersistentStore` keeps everything in memory --- data is lost when the page is refreshed. For persistence across sessions, implement the `SyncPersistentStore` interface with a durable backend like `localStorage`.
131
+
132
+ ### AsyncQueryStore
133
+
134
+ The `AsyncQueryStore` is designed for asynchronous storage backends such as IndexedDB or React Native's AsyncStorage. It uses a writer-reader architecture where one instance (the writer) owns the backing store, and other instances (readers) communicate with it via messages.
135
+
136
+ ```tsx
137
+ import { AsyncQueryStore } from 'fetchium/stores/async';
138
+
139
+ const store = new AsyncQueryStore({
140
+ isWriter: true,
141
+ connect: (handleMessage) => ({
142
+ sendMessage: (msg) => handleMessage(msg),
143
+ }),
144
+ delegate: myAsyncPersistentStore,
145
+ });
146
+ ```
147
+
148
+ **Writer vs reader:**
149
+
150
+ - The **writer** (`isWriter: true`) is the only instance that writes to the backing store. It must be provided a `delegate` (an `AsyncPersistentStore` implementation).
151
+ - **Readers** (`isWriter: false`) send write operations to the writer via messages and can load data directly from their own delegate (if provided).
152
+
153
+ This architecture ensures serialized writes even when multiple tabs or workers are involved.
154
+
155
+ ---
156
+
157
+ ## Custom Persistent Stores
158
+
159
+ To build a custom persistence backend, implement either the `SyncPersistentStore` or `AsyncPersistentStore` interface. Both share the same shape --- the async version wraps each method in a `Promise`.
160
+
161
+ The store needs to handle three data types: strings (for serialized JSON values), numbers (for timestamps and reference counts), and `Uint32Array` buffers (for entity ID sets and LRU queues).
162
+
163
+ ### Example: localStorage adapter
164
+
165
+ ```tsx
166
+ class LocalStoragePersistentStore implements SyncPersistentStore {
167
+ has(key: string): boolean {
168
+ return localStorage.getItem(key) !== null;
169
+ }
170
+
171
+ getString(key: string): string | undefined {
172
+ return localStorage.getItem(key) ?? undefined;
173
+ }
174
+
175
+ setString(key: string, value: string): void {
176
+ localStorage.setItem(key, value);
177
+ }
178
+
179
+ getNumber(key: string): number | undefined {
180
+ const v = localStorage.getItem(key);
181
+ return v !== null ? Number(v) : undefined;
182
+ }
183
+
184
+ setNumber(key: string, value: number): void {
185
+ localStorage.setItem(key, String(value));
186
+ }
187
+
188
+ getBuffer(key: string): Uint32Array | undefined {
189
+ const v = localStorage.getItem(key);
190
+ if (v === null) return undefined;
191
+ return new Uint32Array(JSON.parse(v));
192
+ }
193
+
194
+ setBuffer(key: string, value: Uint32Array): void {
195
+ localStorage.setItem(key, JSON.stringify(Array.from(value)));
196
+ }
197
+
198
+ delete(key: string): void {
199
+ localStorage.removeItem(key);
200
+ }
201
+
202
+ getAllKeys(): string[] {
203
+ return Object.keys(localStorage);
204
+ }
205
+ }
206
+ ```
207
+
208
+ {% callout type="warning" %}
209
+ `localStorage` has a 5 MB limit in most browsers. For larger datasets, consider using IndexedDB via the `AsyncQueryStore` instead.
210
+ {% /callout %}
211
+
212
+ For complete API details on both store types, see the [stores/sync](/api/stores-sync) and [stores/async](/api/stores-async) API reference pages.
213
+
214
+ ---
215
+
216
+ ## Putting It All Together
217
+
218
+ Here is a complete example that sets up a `QueryClient` with persistence and network awareness for an offline-capable application:
219
+
220
+ ```tsx
221
+ import { QueryClient, NetworkManager } from 'fetchium';
222
+ import { SyncQueryStore } from 'fetchium/stores/sync';
223
+
224
+ const store = new SyncQueryStore(new LocalStoragePersistentStore());
225
+ const networkManager = new NetworkManager();
226
+
227
+ const client = new QueryClient(
228
+ store,
229
+ {
230
+ fetch: globalThis.fetch,
231
+ baseUrl: 'https://api.example.com',
232
+ },
233
+ networkManager,
234
+ );
235
+ ```
236
+
237
+ With this setup:
238
+
239
+ - Query results are persisted to `localStorage` and survive page refreshes
240
+ - Queries automatically pause when the device goes offline and resume when it reconnects
241
+ - Unused queries are evicted from memory after their `gcTime` expires, but their persisted data remains in `localStorage` for the next session
242
+
243
+ Configure individual queries for offline behavior:
244
+
245
+ ```tsx
246
+ class GetDashboard extends RESTQuery {
247
+ path = '/dashboard';
248
+
249
+ result = { stats: t.object({ visits: t.number }) };
250
+
251
+ config = {
252
+ networkMode: NetworkMode.OfflineFirst,
253
+ staleTime: 60_000,
254
+ };
255
+ }
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Next Steps
261
+
262
+ {% quick-links %}
263
+
264
+ {% quick-link title="Caching & Refetching" icon="presets" href="/data/caching" description="Understand staleTime, gcTime, cacheTime, and cache invalidation patterns" /%}
265
+
266
+ {% quick-link title="stores/sync API" icon="plugins" href="/api/stores-sync" description="Full API reference for the synchronous query store" /%}
267
+
268
+ {% quick-link title="stores/async API" icon="theming" href="/api/stores-async" description="Full API reference for the asynchronous query store" /%}
269
+
270
+ {% /quick-links %}
@@ -0,0 +1,301 @@
1
+ ---
2
+ title: Testing
3
+ ---
4
+
5
+ Fetchium ships a dedicated `fetchium/testing` package that gives you type-safe query mocking, automatic data generation from your type definitions, entity factories, and test variation knobs for exploring structural edge cases. You never need to understand Signalium internals, mock global state, or patch module imports. Just set up your mocks, write your tests, and let Fetchium handle the wiring.
6
+
7
+ ---
8
+
9
+ ## Quick Start
10
+
11
+ Every test starts with a `MockClient`. It wraps a `QueryClient` with an in-memory store and a mock fetch router so your queries resolve against canned data instead of a real server.
12
+
13
+ ```ts
14
+ import { MockClient } from 'fetchium/testing';
15
+
16
+ const mock = new MockClient();
17
+ afterEach(() => mock.reset());
18
+ afterAll(() => mock.destroy());
19
+ ```
20
+
21
+ `mock.reset()` clears all mocked routes and request history between tests. `mock.destroy()` tears down the underlying client and timers.
22
+
23
+ {% callout title="Always destroy the client" type="warning" %}
24
+ `QueryClient` manages background timers. Call `mock.destroy()` in your test teardown to avoid timer leaks.
25
+ {% /callout %}
26
+
27
+ ---
28
+
29
+ ## Setting Up Your Test Harness
30
+
31
+ `MockClient` provides a `QueryClient` via `mock.client`. You plug this into your own render utility alongside your app's providers --- auth, theme, i18n, routing, or whatever your app needs:
32
+
33
+ ```tsx
34
+ // test-utils.tsx
35
+ import { render } from '@testing-library/react';
36
+ import { ContextProvider } from 'signalium/react';
37
+ import { QueryClientContext } from 'fetchium';
38
+
39
+ export function renderApp(
40
+ ui: React.ReactElement,
41
+ { client }: { client: QueryClient },
42
+ ) {
43
+ return render(
44
+ <ContextProvider value={client} context={QueryClientContext}>
45
+ {ui}
46
+ </ContextProvider>,
47
+ );
48
+ }
49
+ ```
50
+
51
+ If your app has additional providers, add them here. Fetchium has no opinions about your provider stack --- it only needs `QueryClientContext` to be present.
52
+
53
+ ---
54
+
55
+ ## Mocking Queries
56
+
57
+ Register mock responses with `mock.when()`. The response shape is type-checked against the query's `result` definition:
58
+
59
+ ```ts
60
+ import { Entity, t } from 'fetchium';
61
+ import { RESTQuery } from 'fetchium/rest';
62
+
63
+ class User extends Entity {
64
+ __typename = t.typename('User');
65
+ id = t.id;
66
+ name = t.string;
67
+ email = t.string;
68
+ avatar = t.optional(t.string);
69
+ }
70
+
71
+ class GetUser extends RESTQuery {
72
+ params = { id: t.number };
73
+ path = `/users/${this.params.id}`;
74
+ result = { user: t.entity(User) };
75
+ }
76
+
77
+ // In your test:
78
+ mock.when(GetUser, { id: 1 }).respond({
79
+ user: mock.entity(User, { name: 'Alice', email: 'alice@example.com' }),
80
+ });
81
+ ```
82
+
83
+ ### Auto-generated responses
84
+
85
+ If you do not care about the exact field values, `.auto()` generates a complete valid response from the query's type definitions:
86
+
87
+ ```ts
88
+ mock.when(GetUser, { id: 1 }).auto();
89
+ ```
90
+
91
+ You can pass partial overrides that are deep-merged into the generated data:
92
+
93
+ ```ts
94
+ mock.when(GetUser, { id: 1 }).auto({
95
+ user: { name: 'Alice' },
96
+ });
97
+ ```
98
+
99
+ ### Catch-all routes
100
+
101
+ Omit params to match any request for that query class:
102
+
103
+ ```ts
104
+ mock.when(GetUser).auto();
105
+ ```
106
+
107
+ ### Sequential responses
108
+
109
+ Chain `.thenRespond()` to queue multiple responses:
110
+
111
+ ```ts
112
+ mock
113
+ .when(GetUser, { id: 1 })
114
+ .respond({ user: mock.entity(User, { name: 'V1' }) })
115
+ .thenRespond({ user: mock.entity(User, { name: 'V2' }) });
116
+ ```
117
+
118
+ The first fetch returns V1, the second returns V2. Subsequent fetches reuse the last response.
119
+
120
+ ### Error states
121
+
122
+ ```ts
123
+ mock.when(GetUser, { id: 999 }).error(404);
124
+ mock.when(GetUser, { id: 1 }).networkError('connection refused');
125
+ ```
126
+
127
+ ### Simulated latency
128
+
129
+ ```ts
130
+ mock.when(GetUser, { id: 1 }).delay(500).auto();
131
+ ```
132
+
133
+ ### Raw escape hatch
134
+
135
+ `.raw()` bypasses type checking entirely, for testing how your app handles unexpected data:
136
+
137
+ ```ts
138
+ mock.when(GetUser, { id: 1 }).raw({ incomplete: true });
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Generating Test Data
144
+
145
+ ### `mock.entity()`
146
+
147
+ Generates a plain JSON object matching an entity's type definition. Fields are auto-filled with debuggable sequential values (`"name_1"`, `"email_2"`, etc.), and you override whatever matters for your test:
148
+
149
+ ```ts
150
+ const alice = mock.entity(User, { name: 'Alice' });
151
+ // => { __typename: 'User', id: '1', name: 'Alice', email: 'email_1' }
152
+
153
+ const bob = mock.entity(User, { name: 'Bob', email: 'bob@test.com' });
154
+ // => { __typename: 'User', id: '2', name: 'Bob', email: 'bob@test.com' }
155
+ ```
156
+
157
+ IDs auto-increment per typename, so each entity gets a unique identity.
158
+
159
+ ### Entity Factories
160
+
161
+ For richer test data, register a factory with custom generators:
162
+
163
+ ```ts
164
+ mock.define(User, {
165
+ name: (seq) => `User ${seq}`,
166
+ email: (seq, fields) =>
167
+ `${fields.name.toLowerCase().replace(' ', '.')}@test.com`,
168
+ });
169
+
170
+ const alice = mock.entity(User, { name: 'Alice' });
171
+ // => { __typename: 'User', id: '1', name: 'Alice', email: 'alice@test.com' }
172
+ ```
173
+
174
+ Generator functions receive a `seq` counter (auto-incrementing per typename) and a `fields` object containing previously generated field values, for derived data.
175
+
176
+ Factories are also used by `.auto()` when it generates entities for query responses.
177
+
178
+ ### Standalone usage
179
+
180
+ For Storybook or other contexts outside `MockClient`:
181
+
182
+ ```ts
183
+ import { entity, defineFactory } from 'fetchium/testing';
184
+
185
+ const user = entity(User, { name: 'Alice' });
186
+
187
+ const UserFactory = defineFactory(User, {
188
+ name: (seq) => `User ${seq}`,
189
+ });
190
+ UserFactory.build({ name: 'Alice' });
191
+ UserFactory.buildMany(5);
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Testing Components
197
+
198
+ A complete component test:
199
+
200
+ ```tsx
201
+ import { MockClient } from 'fetchium/testing';
202
+ import { renderApp } from '../test-utils';
203
+
204
+ const mock = new MockClient();
205
+ afterEach(() => mock.reset());
206
+ afterAll(() => mock.destroy());
207
+
208
+ it('renders the user name after loading', async () => {
209
+ mock.when(GetUser, { id: 1 }).respond({
210
+ user: mock.entity(User, { name: 'Alice', email: 'alice@example.com' }),
211
+ });
212
+
213
+ const { getByTestId } = renderApp(<UserProfile userId={1} />, {
214
+ client: mock.client,
215
+ });
216
+
217
+ await waitFor(() => {
218
+ expect(getByTestId('name')).toHaveTextContent('Alice');
219
+ });
220
+ });
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Testing Mutations
226
+
227
+ Mutations are mocked the same way. You can inspect what was sent using `mock.calls`:
228
+
229
+ ```ts
230
+ import { getMutation, t } from 'fetchium';
231
+ import { RESTMutation } from 'fetchium/rest';
232
+
233
+ class CreateUser extends RESTMutation {
234
+ readonly params = { name: t.string, email: t.string };
235
+ readonly path = '/users';
236
+ readonly method = 'POST' as const;
237
+ readonly body = { name: this.params.name, email: this.params.email };
238
+ readonly result = { user: t.entity(User) };
239
+ }
240
+
241
+ it('sends the correct request body', async () => {
242
+ mock.when(CreateUser).respond({
243
+ user: mock.entity(User, { name: 'Bob', email: 'bob@example.com' }),
244
+ });
245
+
246
+ // ... trigger the mutation in your component or reactive code ...
247
+
248
+ expect(mock.wasCalled(CreateUser)).toBe(true);
249
+ expect(mock.lastCall(CreateUser)?.body).toEqual({
250
+ name: 'Bob',
251
+ email: 'bob@example.com',
252
+ });
253
+ });
254
+ ```
255
+
256
+ ### Entity effects
257
+
258
+ Because Fetchium normalizes entities, a mutation that returns updated entity data automatically updates any query that references the same entity:
259
+
260
+ ```ts
261
+ it('mutation updates the entity visible in queries', async () => {
262
+ mock.when(GetUser, { id: 1 }).respond({
263
+ user: mock.entity(User, { id: '1', name: 'Alice' }),
264
+ });
265
+ mock.when(UpdateUser).respond({
266
+ user: mock.entity(User, { id: '1', name: 'Alice Updated' }),
267
+ });
268
+
269
+ // Fetch the user, then mutate.
270
+ // The query's data reflects the update automatically.
271
+ });
272
+ ```
273
+
274
+ ---
275
+
276
+ ## Summary
277
+
278
+ | What you are testing | Tools needed | Pattern |
279
+ | -------------------------------- | ------------------------------- | --------------------------------------------------- |
280
+ | React components with `useQuery` | `MockClient` + your `renderApp` | `mock.when().respond()`, render, assert |
281
+ | Mutations | `MockClient` | `mock.when(Mutation).respond()`, check `mock.calls` |
282
+ | Entity effects after mutation | `MockClient` + entity query | Mock both, verify query data reflects mutation |
283
+ | Error states | `MockClient` | `.error()` or `.networkError()` |
284
+
285
+ The core idea is always the same: create a `MockClient`, set up your mocks, plug `mock.client` into your providers, and assert.
286
+
287
+ ---
288
+
289
+ ## Next Steps
290
+
291
+ {% quick-links %}
292
+
293
+ {% quick-link title="Queries" icon="plugins" href="/core/queries" description="Learn how to define queries and use them in components" /%}
294
+
295
+ {% quick-link title="Entities" icon="theming" href="/core/entities" description="Understand normalized entities and identity-stable proxies" /%}
296
+
297
+ {% quick-link title="Mutations" icon="presets" href="/data/mutations" description="Define mutations with entity effects and optimistic updates" /%}
298
+
299
+ {% quick-link title="Error Handling" icon="warning" href="/guides/error-handling" description="Handle network failures, retries, and error boundaries" /%}
300
+
301
+ {% /quick-links %}