@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 +205 -521
- package/dist/action.d.ts +22 -28
- package/dist/action.d.ts.map +1 -1
- package/dist/active-runtime.d.ts +14 -0
- package/dist/active-runtime.d.ts.map +1 -0
- package/dist/chunks/transparent-DhGE8YMn.js +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/global-controllers.d.ts +2 -0
- package/dist/global-controllers.d.ts.map +1 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +5 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -1
- package/dist/notifications.d.ts.map +1 -1
- package/dist/request.d.ts +10 -3
- package/dist/request.d.ts.map +1 -1
- package/dist/resource.d.ts +36 -5
- package/dist/resource.d.ts.map +1 -1
- package/dist/transparent-module.d.ts +60 -0
- package/dist/transparent-module.d.ts.map +1 -0
- package/dist/transparent.d.ts +83 -0
- package/dist/transparent.d.ts.map +1 -0
- package/dist/types.d.ts +126 -17
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/dist/chunks/chunk-2CT6VOBD.js +0 -1
package/README.md
CHANGED
|
@@ -1,591 +1,275 @@
|
|
|
1
1
|
# `@memoized-dom/data`
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
---
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
## 1. The Colorless Async Paradigm
|
|
14
10
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
39
|
+
---
|
|
184
40
|
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
import { Group, Pending, Error as ErrorArm } from '@memoized-dom/data';
|
|
47
|
+
import { stories, type Story } from './session';
|
|
202
48
|
|
|
203
|
-
function
|
|
204
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
115
|
+
---
|
|
277
116
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
132
|
+
---
|
|
284
133
|
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
```ts
|
|
308
|
-
// Default: share while at least one resource is active.
|
|
309
|
-
$fetch<User[]>('/api/users');
|
|
165
|
+
---
|
|
310
166
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
326
|
-
and server `Cache-Control` headers.
|
|
202
|
+
---
|
|
327
203
|
|
|
328
|
-
##
|
|
204
|
+
## 6. Runtime Response Validation (Standard Schema v1)
|
|
329
205
|
|
|
330
|
-
|
|
206
|
+
Validate server responses at runtime using Zod, Valibot, or ArkType via the Standard Schema specification:
|
|
331
207
|
|
|
332
208
|
```ts
|
|
333
|
-
|
|
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.
|
|
213
|
+
id: z.number(),
|
|
342
214
|
name: z.string(),
|
|
215
|
+
email: z.string().email(),
|
|
343
216
|
});
|
|
344
217
|
|
|
345
|
-
const
|
|
346
|
-
validate:
|
|
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
|
-
|
|
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
|
-
|
|
416
|
-
URL search parameters, array buffers, and other supported native request bodies
|
|
417
|
-
are passed through without JSON conversion.
|
|
225
|
+
---
|
|
418
226
|
|
|
419
|
-
|
|
420
|
-
`DELETE`.
|
|
227
|
+
## 7. Universal SSR & Zero-Roundtrip Payload Transport
|
|
421
228
|
|
|
422
|
-
|
|
229
|
+
During SSR, `@memoized-dom/data` coordinates request settling and serializes the state envelope into the streamed HTML:
|
|
423
230
|
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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
|
-
|
|
442
|
-
|
|
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
|
-
|
|
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
|
-
|
|
462
|
-
does not require callbacks.
|
|
256
|
+
---
|
|
463
257
|
|
|
464
|
-
##
|
|
258
|
+
## 8. Isolated Request Runtimes
|
|
465
259
|
|
|
466
|
-
|
|
260
|
+
For server request isolation or testing:
|
|
467
261
|
|
|
468
262
|
```ts
|
|
469
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
539
|
-
await
|
|
540
|
-
|
|
541
|
-
|
|
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.
|