@memoized-dom/data 0.0.1 → 0.0.2

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 CHANGED
@@ -1,591 +1,275 @@
1
1
  # `@memoized-dom/data`
2
2
 
3
- Browser-first fetch resources and callable actions for memoized-dom.
3
+ `@memoized-dom/data` is Memoized DOM's data-fetching and state synchronization package. It provides **Colorless Async** transparent values, safe GET deduplication, schema validation, declarative pending/error JSX directives, request lifecycle tracking, and zero-roundtrip SSR payload transport.
4
4
 
5
- > **Current scope:** the package is usable in ordinary JavaScript and in
6
- > compiled memoized-dom components. The compiler treats imported resource and
7
- > action values like other opaque third-party state: while their getters are
8
- > rendered, it rereads them through the existing volatile frame fallback. No
9
- > `$fetch`-specific compiler adapter or method allowlist is involved.
5
+ There are no hooks, provider trees, signals, or store wrappers. Fetched data behaves as plain TypeScript values and arrays in your components.
10
6
 
11
- ## Public API
7
+ ---
12
8
 
13
- The package exposes a default runtime plus an isolated-runtime factory:
9
+ ## 1. The Colorless Async Paradigm
14
10
 
15
- ```ts
16
- import {
17
- $action,
18
- $fetch,
19
- clearDataRuntime,
20
- createDataRuntime,
21
- RequestError,
22
- } from '@memoized-dom/data';
23
- ```
24
-
25
- - `$fetch` automatically performs a read request and returns a stable resource.
26
- - `$action` creates a lazy callable operation for writes.
27
- - `createDataRuntime` creates an isolated request, cache, and action boundary.
28
- - `clearDataRuntime` clears active work and retained state in the default runtime.
29
- - `RequestError` describes network, HTTP, decoding, and validation failures.
30
-
31
- There is no provider, hook, or mutable global configuration API.
32
-
33
- ### Isolated runtimes
34
-
35
- Use a separate runtime for each server request, test, tenant, or other ownership
36
- boundary that must not share request state:
37
-
38
- ```ts
39
- const data = createDataRuntime({
40
- baseURL: 'https://api.example.test/',
41
- fetch: customFetch,
42
- });
43
-
44
- const users = data.$fetch<User[]>('users');
45
- data.clear();
46
- ```
47
-
48
- `baseURL` resolves relative targets and `fetch` injects a compatible fetch
49
- implementation. In a browser, the default base URL is `location.href`. A
50
- non-browser runtime must provide `baseURL` when it uses relative targets.
51
-
52
- `clear()` aborts active reads and actions, resets their visible pending state,
53
- detaches live read resources, and drops retained request data. Existing resource
54
- and action objects remain valid; a detached resource can be refreshed to start
55
- new work. `clearDataRuntime()` performs the same operation on the exported
56
- default `$fetch` and `$action` runtime.
57
-
58
- ### Component ownership
59
-
60
- A runtime created inside a component should be cleared with the component:
11
+ `$fetch<T>` returns a compiler-aware `ResolvedValue<T>`. In your templates and derived state, you consume it as the plain type `T`:
61
12
 
62
13
  ```tsx
63
- function Users() {
64
- const data = createDataRuntime();
65
- const users = data.$fetch<User[]>('/api/users');
66
- cleanup(data.clear);
67
-
68
- return <p>{users.pending ? 'Loading' : users.data?.length}</p>;
69
- }
70
- ```
14
+ import { $fetch } from '@memoized-dom/data';
71
15
 
72
- Memoized-dom component factories run once, so this does not recreate the
73
- runtime on every update. `cleanup(data.clear)` aborts component-owned reads and
74
- actions when the component is removed. When using the default runtime, a
75
- component can instead own one resource with `cleanup(users.abort)`.
76
-
77
- Cleanup is explicit today because ordinary third-party libraries remain usable
78
- without implementing a memoized-dom lifecycle interface.
79
-
80
- ## `$fetch`
81
-
82
- ### Basic request
83
-
84
- ```ts
85
- interface User {
86
- id: string;
16
+ export interface User {
17
+ id: number;
87
18
  name: string;
19
+ avatar: string;
88
20
  }
89
21
 
90
- const users = $fetch<User[]>('/api/users');
91
- ```
92
-
93
- The request starts automatically. `$fetch<User[]>()` tells TypeScript that a
94
- successful decoded response is expected to be `User[]`.
95
-
96
- The generic is a developer assertion. It does not validate the server response
97
- at runtime. Optional runtime validation is explained later.
98
-
99
- ### Resource state
100
-
101
- ```ts
102
- users.data; // User[] | undefined
103
- users.error; // RequestError | null
104
- users.status; // 'idle' | 'pending' | 'success' | 'error'
105
- users.pending; // boolean
106
- users.refreshing; // boolean
107
- ```
108
-
109
- The initial cold request has this state:
110
-
111
- ```ts
112
- users.status === 'pending';
113
- users.pending === true;
114
- users.refreshing === false;
115
- users.data === undefined;
116
- users.error === null;
117
- ```
118
-
119
- After success:
120
-
121
- ```ts
122
- users.status === 'success';
123
- users.pending === false;
124
- users.data !== undefined;
125
- ```
126
-
127
- After an initial failure:
128
-
129
- ```ts
130
- users.status === 'error';
131
- users.pending === false;
132
- users.data === undefined;
133
- users.error instanceof RequestError;
134
- ```
135
-
136
- During a refresh, previous data remains available:
137
-
138
- ```ts
139
- users.status === 'success';
140
- users.pending === true;
141
- users.refreshing === true;
142
- users.data; // previous successful data
143
- ```
144
-
145
- A failed refresh preserves previous data. `status` remains `success`, and
146
- `error` contains the refresh failure so the application may display a
147
- non-blocking warning.
148
-
149
- ### Query parameters
150
-
151
- ```ts
152
- const users = $fetch<User[]>('/api/users', {
153
- query: {
154
- search: 'Ada',
155
- page: 2,
156
- active: true,
157
- tag: ['compiler', 'typescript'],
158
- },
159
- });
160
- ```
161
-
162
- Query values may be strings, numbers, booleans, `null`, arrays of those values,
163
- or `undefined`. An `undefined` value is omitted. Arrays produce repeated query
164
- fields. Query keys are normalized so equivalent requests share the same
165
- identity regardless of object property order. URL fragments are removed because
166
- they are not sent in HTTP requests and must not split request identity.
167
-
168
- ### Request headers
169
-
170
- ```ts
171
- const profile = $fetch<Profile>('/api/profile', {
172
- headers: {
173
- Authorization: `Bearer ${token}`,
174
- },
175
- });
22
+ // 1. Module-scope source: lazy declaration, request-isolated during SSR
23
+ export const currentUser = $fetch<User>('/api/session');
24
+
25
+ // 2. Direct transparent reads in any component:
26
+ export function UserProfile() {
27
+ return (
28
+ <div class="user-card">
29
+ <span class="avatar">{currentUser.avatar}</span>
30
+ <h2>{currentUser.name}</h2>
31
+ </div>
32
+ );
33
+ }
176
34
  ```
177
35
 
178
- Headers participate in automatic request identity. Requests with different
179
- authorization values therefore do not share data by default. Headers are copied
180
- when the resource is created, so later mutation of a supplied `Headers` object
181
- cannot make request execution disagree with its identity.
36
+ - **Zero Boilerplate**: No `useQuery`, no `.data` access required, and no `async/await` component wrappers.
37
+ - **Push Invalidation**: The compiler links data reads to their render regions. When a fetch resolves, updates push directly to the target DOM nodes without frame polling.
182
38
 
183
- ### Paused resource
39
+ ---
184
40
 
185
- `null` is the only paused target:
186
-
187
- ```ts
188
- const user = $fetch<User>(
189
- userId ? `/api/users/${userId}` : null,
190
- );
191
- ```
41
+ ## 2. Declarative State Arms (`Group`, `Pending`, `Error`)
192
42
 
193
- A paused resource has `status: 'idle'` and performs no request. Automatic
194
- reevaluation when `userId` changes is a deferred request-argument optimization;
195
- the current call captures only the value passed at creation.
196
-
197
- When request arguments change today, replace the component-local resource
198
- explicitly and release the previous one:
43
+ Handle loading skeletons and error states declaratively without ternary clutter:
199
44
 
200
45
  ```tsx
201
- let users = loadUsers(search);
46
+ import { Group, Pending, Error as ErrorArm } from '@memoized-dom/data';
47
+ import { stories, type Story } from './session';
202
48
 
203
- function setSearch(next: string) {
204
- search = next;
205
- const previous = users;
206
- users = loadUsers(search);
207
- previous.abort();
49
+ function LoadingSkeleton() {
50
+ return <ul class="skeleton-list"><li>Loading stories…</li></ul>;
208
51
  }
209
- ```
210
-
211
- Because `users` is ordinary component `let` state, the existing compiler
212
- updates its consumers. This is explicit resource ownership, not `$fetch`
213
- recognition.
214
-
215
- ### Manual refresh
216
-
217
- ```ts
218
- const latestUsers = await users.refresh();
219
- ```
220
-
221
- `refresh()` always performs a new request and resolves with its decoded result.
222
- Existing data remains visible while it runs.
223
-
224
- ### Abort
225
-
226
- ```ts
227
- users.abort();
228
- ```
229
52
 
230
- Aborting detaches this resource from its active shared request. If it was the
231
- last consumer, the underlying request is aborted. If another resource still
232
- uses that request, the request continues for the other resource.
233
-
234
- Cancellation is not stored as `resource.error`.
235
-
236
- Cancellation is also a client-side settlement boundary. Even when an injected
237
- fetch implementation ignores `AbortSignal`, an aborted resource or action will
238
- not accept its late result.
239
-
240
- ### Replacing data
241
-
242
- `update()` requires the callback to return the new top-level value:
243
-
244
- ```ts
245
- users.update(current => [
246
- ...(current ?? []),
247
- newUser,
248
- ]);
249
- ```
250
-
251
- ### Direct mutation
252
-
253
- `mutate()` ignores the callback's result and preserves the existing top-level
254
- value:
53
+ function ErrorBanner({ error, retry }: { error: { message: string }; retry: () => void }) {
54
+ return (
55
+ <div class="error-box">
56
+ <p>{error.message}</p>
57
+ <button onClick={retry}>Try Again</button>
58
+ </div>
59
+ );
60
+ }
255
61
 
256
- ```ts
257
- users.mutate(current => {
258
- current?.push(newUser);
259
- });
62
+ export function StoriesPanel() {
63
+ return (
64
+ <section class="panel">
65
+ <h2>Top Stories</h2>
66
+
67
+ <Group>
68
+ <Pending component={() => <LoadingSkeleton />} />
69
+ <ErrorArm component={({ error, retry }) => (
70
+ <ErrorBanner error={error} retry={retry} />
71
+ )} />
72
+ {/* Resolved arm: renders automatically once data settles */}
73
+ <ul class="story-list">
74
+ {stories.map(item => (
75
+ <li key={item.id}>
76
+ <span>{item.title}</span>
77
+ <span class="votes">{item.votes}</span>
78
+ </li>
79
+ ))}
80
+ </ul>
81
+ </Group>
82
+ </section>
83
+ );
84
+ }
260
85
  ```
261
86
 
262
- These are deliberately separate. `Array.push()` returns a number, so an API
263
- that treats callback returns as optional replacements could accidentally store
264
- that number instead of the array.
265
-
266
- Use `mutate()` for direct in-place changes and `update()` when a replacement is
267
- required.
87
+ - **`Pending`**: Shown independently at source-consuming sites while their
88
+ initial requests are in flight.
89
+ - **`Error`**: Injects `{ error, retry }` into the error component when a request fails.
90
+ - **Content**: Mounts immediately by default; each dependent expression or
91
+ structural site resolves independently.
92
+ - **Dependencies**: The compiler infers exactly which colorless sources are
93
+ read by the content. `Group` does not need a `data` prop.
94
+ - **Policies**: `component` accepts either a named component or a synchronous
95
+ inline render callback. Inline callbacks may capture component-local values.
268
96
 
269
- Both operations supersede a read that was already in flight. The older request
270
- is aborted, and its result is ignored even when an injected fetch implementation
271
- does not cooperate with `AbortSignal`. This prevents a stale server snapshot
272
- from overwriting a newer local or optimistic write.
273
-
274
- ## Resource sharing
97
+ To make the first mount atomic, mark the one direct content element with the
98
+ shorthand compiler directive `suspend`. The element may be a component or a
99
+ host element:
100
+
101
+ ```tsx
102
+ <Group>
103
+ <Pending component={DashboardSkeleton} />
104
+ <ErrorArm component={ErrorBanner} />
105
+ <section suspend>
106
+ <Dashboard profile={profile} activity={activity} />
107
+ </section>
108
+ </Group>
109
+ ```
110
+
111
+ The pending arm appears once until every inferred source has its initial value.
112
+ The compiler removes `suspend` before component prop checking and emission.
113
+ Committed content remains visible during later refreshes.
275
114
 
276
- The default sharing mode is active sharing:
115
+ ---
277
116
 
278
- ```ts
279
- const first = $fetch<User[]>('/api/users');
280
- const second = $fetch<User[]>('/api/users');
281
- ```
117
+ ## 3. Direct Data Changes
118
+
119
+ Transparent values remain ordinary application data. Change the property that
120
+ actually changed; the compiler is responsible for routing that write to the
121
+ affected DOM work:
122
+
123
+ ```tsx
124
+ import { stories } from './session';
125
+
126
+ function upvote(id: number) {
127
+ const story = stories.find(item => item.id === id);
128
+ if (story !== undefined) story.votes++;
129
+ }
130
+ ```
282
131
 
283
- When both declarations describe the same normalized request:
132
+ ---
284
133
 
285
- 1. The first resource starts the request.
286
- 2. The second resource joins that request instead of sending another one.
287
- 3. Both resources observe the same decoded value.
288
- 4. A third resource created while either remains active receives that value
289
- immediately without another request.
290
- 5. After the final resource is disposed by the future integration layer, the
291
- entry is removed.
292
- 6. A later resource performs a new request.
134
+ ## 4. Fine-Grained Reactive Tracking (`$track`)
293
135
 
294
- The identity includes the normalized URL, query, headers, and validator. An
295
- explicit key can replace automatic identity when necessary:
136
+ When you need to inspect request status (e.g. showing a spinning sync icon during background refresh):
296
137
 
297
- ```ts
298
- const profile = $fetch<Profile>('/api/profile', {
299
- key: ['profile', accountId],
300
- });
138
+ ```tsx
139
+ import { $track } from '@memoized-dom/data';
140
+ import { notifications } from './session';
141
+
142
+ export function SyncButton() {
143
+ // `$track` reactively observes background refresh & pending state:
144
+ const state = $track(notifications);
145
+
146
+ return (
147
+ <button class={state.refreshing ? 'spinning' : ''}>
148
+ {state.refreshing ? 'Syncing…' : 'Refresh'}
149
+ </button>
150
+ );
151
+ }
301
152
  ```
302
153
 
303
- ### Sharing options
154
+ ### Tracked State Properties:
155
+ - `state.id`: identity of this exact request execution
156
+ - `state.status`: `'idle' | 'pending' | 'success' | 'error'`
157
+ - `state.pending`: `true` during cold initial load
158
+ - `state.refreshing`: `true` during background revalidation (previous data remains visible)
159
+ - `state.error`: `RequestError | null`
160
+ - `state.onSuccess((data, requestId) => ...)`: one-shot success observation
161
+ - `state.onError((error, requestId) => ...)`: one-shot failure observation
162
+ - `state.refresh()`: starts another execution; awaiting is optional
163
+ - `state.abort()`: explicitly cancels the represented request
304
164
 
305
- The current package implements only three clear modes:
306
-
307
- ```ts
308
- // Default: share while at least one resource is active.
309
- $fetch<User[]>('/api/users');
165
+ ---
310
166
 
311
- // Private: never share this resource's request or result.
312
- $fetch<User[]>('/api/users', {
313
- cache: false,
314
- });
315
-
316
- // Retain for this package instance until its internal store is cleared.
317
- $fetch<User[]>('/api/users', {
318
- cache: { scope: 'app' },
319
- });
320
- ```
321
-
322
- No freshness or retention duration is implemented in version `0.0.1`. Timing
323
- options remain a design discussion and are not part of the package API.
167
+ ## 5. Mutations and overlapping requests
168
+
169
+ Server-function facades and direct `$fetch` calls use the same transparent
170
+ result and `$track` lifecycle. Change application data directly, and record
171
+ the smallest inverse operation when optimistic rollback is required:
172
+
173
+ ```ts
174
+ const pendingVotes = new Set<string>();
175
+
176
+ function vote(story: Story) {
177
+ const result = postVote(story.id);
178
+ const request = $track(result);
179
+
180
+ pendingVotes.add(request.id);
181
+ story.votes++;
182
+
183
+ request.onSuccess((_data, requestId) => {
184
+ pendingVotes.delete(requestId);
185
+ });
186
+ request.onError((_error, requestId) => {
187
+ if (pendingVotes.delete(requestId)) story.votes--;
188
+ });
189
+ }
190
+ ```
191
+
192
+ Reassigning a local variable to a newer result changes what the UI displays;
193
+ it does not cancel older dispatched work. Each retained request delivers its
194
+ own callback before cleanup. Only `abort()` or an explicit `AbortSignal`
195
+ requests cancellation.
196
+
197
+ Non-GET requests are not deduplicated by default: two identical POSTs may be
198
+ two intentional operations. Disable or debounce a control to suppress rapid
199
+ client submissions. Use a domain idempotency key on the server when the
200
+ operation must be processed at most once.
324
201
 
325
- This sharing store is separate from the browser HTTP cache, service workers,
326
- and server `Cache-Control` headers.
202
+ ---
327
203
 
328
- ## Optional response validation
204
+ ## 6. Runtime Response Validation (Standard Schema v1)
329
205
 
330
- This short form trusts the developer's generic assertion:
206
+ Validate server responses at runtime using Zod, Valibot, or ArkType via the Standard Schema specification:
331
207
 
332
208
  ```ts
333
- const users = $fetch<User[]>('/api/users');
334
- ```
335
-
336
- For an external or untrusted API, a Standard Schema validator can infer and
337
- check the decoded response:
209
+ import { z } from 'zod';
210
+ import { $fetch } from '@memoized-dom/data';
338
211
 
339
- ```ts
340
212
  const UserSchema = z.object({
341
- id: z.string(),
213
+ id: z.number(),
342
214
  name: z.string(),
215
+ email: z.string().email(),
343
216
  });
344
217
 
345
- const users = $fetch('/external/users', {
346
- validate: z.array(UserSchema),
347
- });
348
- ```
349
-
350
- Here `users.data` is inferred as `User[] | undefined`. If the response does not
351
- match the schema, invalid data is not stored and `users.error.kind` is
352
- `'validation'`.
353
-
354
- `validate` is optional. It does not replace the generic form and is not required
355
- for application-owned endpoints.
356
-
357
- ## Destructuring
358
-
359
- In the standalone package, normal JavaScript destructuring is a snapshot:
360
-
361
- ```ts
362
- const { data, pending } = users;
363
- ```
364
-
365
- Those two variables do not change by themselves because this package does not
366
- rewrite JavaScript. The current compiler's opaque frame fallback also cannot
367
- replay a value that was copied out once. Keep the resource object when values
368
- must be read later:
369
-
370
- ```tsx
371
- const users = $fetch<User[]>('/api/users');
372
-
373
- function render() {
374
- return users.pending ? 'Loading' : users.data;
375
- }
376
- ```
377
-
378
- Direct resource getters used by JSX are reread while the component is mounted.
379
- Live destructuring could be added later, but it is not required for `$fetch` to
380
- render correctly and must not depend on package or method names.
381
-
382
- Methods are bound functions and are safe to extract:
383
-
384
- ```ts
385
- const { refresh } = users;
386
- await refresh();
387
- ```
388
-
389
- ## `$action`
390
-
391
- ### Creating and invoking an action
392
-
393
- ```ts
394
- interface Todo {
395
- id: string;
396
- title: string;
397
- }
398
-
399
- interface CreateTodo {
400
- title: string;
401
- }
402
-
403
- const createTodo = $action<Todo, CreateTodo>('/api/todos', {
404
- method: 'POST',
405
- });
406
-
407
- const created = await createTodo({
408
- title: 'Write documentation',
218
+ export const user = $fetch('/api/user', {
219
+ validate: UserSchema, // TypeScript infers User type automatically
409
220
  });
410
221
  ```
411
222
 
412
- Creating the action sends no request. Calling it performs one invocation and
413
- returns that invocation's promise.
223
+ If the response fails validation, `user` enters error state with `error.kind === 'validation'` and `error.issues` containing the schema breakdown.
414
224
 
415
- Plain object and array inputs are encoded as JSON. Strings, `FormData`, blobs,
416
- URL search parameters, array buffers, and other supported native request bodies
417
- are passed through without JSON conversion.
225
+ ---
418
226
 
419
- The default method is `POST`. Supported methods are `POST`, `PUT`, `PATCH`, and
420
- `DELETE`.
227
+ ## 7. Universal SSR & Zero-Roundtrip Payload Transport
421
228
 
422
- ### Action state
229
+ During SSR, `@memoized-dom/data` coordinates request settling and serializes the state envelope into the streamed HTML:
423
230
 
424
- ```ts
425
- createTodo.data; // Todo | undefined
426
- createTodo.error; // RequestError | null
427
- createTodo.status; // 'idle' | 'pending' | 'success' | 'error'
428
- createTodo.pending; // boolean
231
+ ```text
232
+ Server Stream:
233
+ HTML Markup: <!--mmd:r:App--><div class="user">Ada</div><!--/mmd-->
234
+ State Envelope: <script type="application/mmd+json" data-mmd-root="App">
235
+ {"version":1,"state":{"sources":[{"sourceId":"GET|/api/session","snapshot":{...}}]}}
236
+ </script>
429
237
  ```
430
238
 
431
- Every invocation receives its own promise. The initial implementation permits
432
- parallel calls. Visible `data`, `error`, and `status` belong to the most recently
433
- started invocation; an older request settling late cannot overwrite newer
434
- visible state.
239
+ ### Client Hydration:
240
+ 1. `hydrate()` extracts the state envelope from the embedded script tag before rendering.
241
+ 2. It restores the dormant records into the client's `DataRuntime`.
242
+ 3. Client components adopt the server DOM with **zero duplicate network fetches and zero loading flash**.
435
243
 
436
244
  ```ts
437
- createTodo.abort();
438
- createTodo.reset();
439
- ```
245
+ // main.ts (Client Bootstrap)
246
+ import { mount } from '@memoized-dom/runtime';
247
+ import { createDataRuntime, setActiveDataRuntime } from '@memoized-dom/data';
248
+ import { App } from './App';
440
249
 
441
- `abort()` stops active client requests. It cannot guarantee that a server did
442
- not already process a mutation. `reset()` aborts active work and returns visible
443
- state to `idle`. Late results from fetch implementations that ignore abort are
444
- discarded. With parallel calls, aborting one invocation does not allow it to
445
- overwrite the state of a newer invocation.
250
+ setActiveDataRuntime(createDataRuntime());
251
+ import { hydrate } from '@memoized-dom/runtime/hydrate';
446
252
 
447
- ### Success and error callbacks
448
-
449
- ```ts
450
- const createTodo = $action<Todo, CreateTodo>('/api/todos', {
451
- onSuccess(created, input) {
452
- console.log('Created', created.id, 'from', input.title);
453
- },
454
-
455
- onError(error, input) {
456
- console.error('Could not create', input.title, error);
457
- },
458
- });
253
+ hydrate('root', App, { recover: true });
459
254
  ```
460
255
 
461
- Normal `try`/`catch` around the returned invocation promise remains valid and
462
- does not require callbacks.
256
+ ---
463
257
 
464
- ## Optimistic collection changes
258
+ ## 8. Isolated Request Runtimes
465
259
 
466
- An array resource exposes three typed change constructors:
260
+ For server request isolation or testing:
467
261
 
468
262
  ```ts
469
- todos.append(temporary);
470
- todos.replace(current, temporary);
471
- todos.remove(current);
472
- ```
473
-
474
- They apply immediately and return an opaque optimistic change consumed by an
475
- action invocation. Each change is single-use; passing the same change to a
476
- second invocation throws instead of committing or rolling it back twice.
477
-
478
- ### Create
479
-
480
- ```ts
481
- const created = await createTodo(input, {
482
- optimistic: todos.append(temporary),
483
- });
484
- ```
485
-
486
- - `temporary` appears immediately.
487
- - Failure removes only that temporary item.
488
- - Success replaces that exact item with `created`, the action result.
489
- - The list is not fetched again.
490
-
491
- ### Update
492
-
493
- ```ts
494
- const saved = await updateTodo(input, {
495
- optimistic: todos.replace(existing, optimisticVersion),
496
- });
497
- ```
498
-
499
- - `existing` is replaced immediately.
500
- - Failure restores `existing`.
501
- - Success installs `saved` in place of `optimisticVersion`.
502
-
503
- `existing` should be the actual object reference obtained from `todos.data`.
504
-
505
- ### Delete
506
-
507
- ```ts
508
- await deleteTodo(existing.id, {
509
- optimistic: todos.remove<void>(existing),
510
- });
511
- ```
263
+ import { createDataRuntime, runWithDataRuntime } from '@memoized-dom/data';
512
264
 
513
- - `existing` is removed immediately.
514
- - Failure reinserts it at its previous position.
515
- - Success keeps it removed.
516
-
517
- If `existing` is not present, `replace()` and `remove()` return safe no-op
518
- changes. Duplicate object references are handled one occurrence at a time.
519
-
520
- Rollback operations target their own temporary/current item instead of
521
- restoring a complete old array. A failed older action therefore does not erase
522
- unrelated later additions.
523
-
524
- ## Optional related-resource refresh
525
-
526
- Creating an item normally returns that item, so the optimistic list should
527
- commit from the result rather than refetch itself:
528
-
529
- ```ts
530
- await createTodo(input, {
531
- optimistic: todos.append(temporary),
265
+ const requestRuntime = createDataRuntime({
266
+ fetch: customFetch,
267
+ baseURL: 'https://api.internal.service',
532
268
  });
533
- ```
534
-
535
- Refresh is only for another resource whose authoritative value cannot be
536
- derived from the returned item:
537
269
 
538
- ```ts
539
- await createTodo(input, {
540
- optimistic: todos.append(temporary),
541
- refresh: [todoStatistics],
270
+ // Run request within isolated cache boundary:
271
+ const result = await runWithDataRuntime(requestRuntime, async () => {
272
+ await requestRuntime.settle();
273
+ return requestRuntime.serializeState();
542
274
  });
543
275
  ```
544
-
545
- The action result reconciles `todos`. Only `todoStatistics` is requested again.
546
- Refresh requests start after the action succeeds and do not delay the action's
547
- returned result.
548
-
549
- ## Errors
550
-
551
- ```ts
552
- class RequestError<TData = unknown> extends Error {
553
- readonly kind: 'network' | 'http' | 'decode' | 'validation';
554
- readonly status: number | null;
555
- readonly statusText: string | null;
556
- readonly data: TData | undefined;
557
- readonly issues: readonly StandardSchemaIssue[] | undefined;
558
- }
559
- ```
560
-
561
- Automatic `$fetch` requests store failures in `resource.error` without causing
562
- an unhandled rejection. Awaited `refresh()` and action calls reject normally.
563
-
564
- ## Compiler behavior and optional future optimization
565
-
566
- The current compiler needs no data-specific integration. An imported resource
567
- whose getters participate in rendered output is an opaque value, so its owner
568
- is marked volatile and reevaluated once per visible animation frame. This also
569
- supports structural output such as loading branches and
570
- `resource.data?.map(...)` lists. Polling stops when the owner is unmounted.
571
-
572
- This is the same compatibility path used for animation engines, external
573
- stores, and other third-party objects. Those libraries do not need to implement
574
- a framework interface.
575
-
576
- The package also exposes subscribe, immutable-snapshot, and dispose hooks from
577
- `@memoized-dom/data/internal`. Generated code does not use them today. They are
578
- available if measurements later justify an optional push optimization:
579
-
580
- - notify only when resource/action state changes instead of pulling per frame;
581
- - provide more precise invalidation;
582
- - automate resource ownership and cleanup;
583
- - recreate a resource when compiled request arguments change.
584
-
585
- That optimization must preserve the opaque fallback. It must not make a
586
- special interface mandatory for third-party libraries, recognize `$fetch` by
587
- name, or approve mutation methods from a list. Direct writes to derived values
588
- remain illegal; opaque receiver calls retain ordinary JavaScript semantics.
589
-
590
- The complete evolving design and deferred server behavior live in the root
591
- `data-loading-api.md` document.