doxum 0.1.22 → 0.1.25
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 +210 -105
- package/dist/advanced.cjs +237 -2
- package/dist/advanced.cjs.map +1 -0
- package/dist/advanced.d.cts +98 -2
- package/dist/advanced.d.ts +98 -2
- package/dist/advanced.js +236 -1
- package/dist/advanced.js.map +1 -0
- package/dist/{contract-CRrZ-3sF.d.cts → contract-DB8XvNt0.d.cts} +102 -99
- package/dist/{contract-CxiNcqMn.d.ts → contract-Dawq6LKS.d.ts} +102 -99
- package/dist/definition-CHVS-RDY.d.cts +110 -0
- package/dist/definition-DT1NpMul.d.ts +110 -0
- package/dist/driver-B5vdPohL.js +385 -0
- package/dist/driver-B5vdPohL.js.map +1 -0
- package/dist/driver-BL91gn7_.cjs +480 -0
- package/dist/driver-BL91gn7_.cjs.map +1 -0
- package/dist/index.cjs +3489 -954
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -32
- package/dist/index.d.ts +66 -32
- package/dist/index.js +4114 -1579
- package/dist/index.js.map +1 -1
- package/dist/local-sync.cjs +69 -103
- package/dist/local-sync.cjs.map +1 -1
- package/dist/local-sync.d.cts +14 -26
- package/dist/local-sync.d.ts +14 -26
- package/dist/local-sync.js +68 -96
- package/dist/local-sync.js.map +1 -1
- package/dist/react.cjs +6 -75
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +6 -9
- package/dist/react.d.ts +6 -9
- package/dist/react.js +7 -76
- package/dist/react.js.map +1 -1
- package/dist/record-B2yRhYsc.js +28 -0
- package/dist/record-B2yRhYsc.js.map +1 -0
- package/dist/record-ChYUi8PP.cjs +45 -0
- package/dist/record-ChYUi8PP.cjs.map +1 -0
- package/dist/scheduler--137L3j-.js +500 -0
- package/dist/scheduler--137L3j-.js.map +1 -0
- package/dist/scheduler-D4VthaaK.cjs +589 -0
- package/dist/scheduler-D4VthaaK.cjs.map +1 -0
- package/package.json +5 -8
- package/skills/doxum-runtime/SKILL.md +28 -8
- package/skills/doxum-runtime/agents/openai.yaml +1 -1
- package/skills/doxum-runtime/references/api.en.md +243 -0
- package/skills/doxum-runtime/references/api.zh-CN.md +236 -0
- package/skills/doxum-runtime/references/guide.en.md +26 -14
- package/skills/doxum-runtime/references/guide.zh-CN.md +21 -11
- package/skills/doxum-runtime/references/invariants.en.md +32 -6
- package/skills/doxum-runtime/references/invariants.zh-CN.md +26 -7
- package/skills/doxum-runtime/references/patterns.en.md +78 -18
- package/skills/doxum-runtime/references/patterns.zh-CN.md +76 -18
- package/skills/doxum-runtime/references/projections.en.md +206 -119
- package/skills/doxum-runtime/references/projections.zh-CN.md +197 -101
- package/dist/access-CZXv_DIF.js +0 -958
- package/dist/access-CZXv_DIF.js.map +0 -1
- package/dist/access-D64MFRSm.cjs +0 -1101
- package/dist/access-D64MFRSm.cjs.map +0 -1
- package/dist/advanced-BTStm2cW.js +0 -1192
- package/dist/advanced-BTStm2cW.js.map +0 -1
- package/dist/advanced-CqrR_4dh.d.ts +0 -176
- package/dist/advanced-CyR5o3Xr.d.cts +0 -176
- package/dist/advanced-GKZ-CZ6a.cjs +0 -1365
- package/dist/advanced-GKZ-CZ6a.cjs.map +0 -1
- package/dist/driver-BC06FEMF.cjs +0 -241
- package/dist/driver-BC06FEMF.cjs.map +0 -1
- package/dist/driver-BqRjwFmU.js +0 -200
- package/dist/driver-BqRjwFmU.js.map +0 -1
- package/dist/integration-BqDw7e2w.cjs +0 -30
- package/dist/integration-BqDw7e2w.cjs.map +0 -1
- package/dist/integration-CSXaF8HP.js +0 -25
- package/dist/integration-CSXaF8HP.js.map +0 -1
- package/dist/integration.cjs +0 -13
- package/dist/integration.d.cts +0 -17
- package/dist/integration.d.ts +0 -17
- package/dist/integration.js +0 -5
- package/dist/issue-9U6YLYz3.js +0 -647
- package/dist/issue-9U6YLYz3.js.map +0 -1
- package/dist/issue-Bo0FIO3b.cjs +0 -886
- package/dist/issue-Bo0FIO3b.cjs.map +0 -1
- package/dist/notification-CrZHVx3a.js +0 -176
- package/dist/notification-CrZHVx3a.js.map +0 -1
- package/dist/notification-DFoA9bQY.cjs +0 -235
- package/dist/notification-DFoA9bQY.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -60,10 +60,14 @@ Committed observer failures are returned in `observerErrors` without rollback.
|
|
|
60
60
|
## Read And Observe
|
|
61
61
|
|
|
62
62
|
```ts
|
|
63
|
-
import { read, snapshot } from 'doxum';
|
|
63
|
+
import { read, select, snapshot } from 'doxum';
|
|
64
64
|
|
|
65
65
|
const title = read(document, state => state.title);
|
|
66
66
|
const tasks = read(document, state => snapshot(state.tasks));
|
|
67
|
+
const selectedTitle = select(document, state => state.title);
|
|
68
|
+
const stop = selectedTitle.subscribe(() => {
|
|
69
|
+
console.log(selectedTitle.current());
|
|
70
|
+
});
|
|
67
71
|
document.subscribe(
|
|
68
72
|
path => path.tasks.item('b').title,
|
|
69
73
|
commit => {
|
|
@@ -81,6 +85,10 @@ supported types, while classes/functions need application-specific handling.
|
|
|
81
85
|
Use ordinary properties for fine-grained reads. Data callbacks read real values;
|
|
82
86
|
path callbacks describe symbolic schema locations, including missing entries.
|
|
83
87
|
Subscription paths compile once during registration.
|
|
88
|
+
`select(document, selector, equality?)` returns a standard `Readable`. Core
|
|
89
|
+
tracks exactly the document locations read by the selector, rebinds dependencies
|
|
90
|
+
when selector branches change, and preserves the previous selected reference and
|
|
91
|
+
publication revision when `equality` reports no result change.
|
|
84
92
|
|
|
85
93
|
React integration:
|
|
86
94
|
|
|
@@ -91,9 +99,10 @@ const title = useDocumentSelector(document, state => state.title);
|
|
|
91
99
|
const history = useHistory(document.history);
|
|
92
100
|
```
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
Document dependency tracking belongs to Core's `select`; the React adapter only
|
|
103
|
+
subscribes to the resulting `Readable`. The core has no React dependency.
|
|
104
|
+
`document.readonly()` returns a capability-stripped `ReadonlyDocument` alias for
|
|
105
|
+
boundaries that must not receive document write APIs.
|
|
97
106
|
|
|
98
107
|
## Containers And Parsing
|
|
99
108
|
|
|
@@ -110,9 +119,11 @@ capabilities while retaining selection, subscription and projection support.
|
|
|
110
119
|
|
|
111
120
|
Map/table key validators preserve branded string types through access, paths,
|
|
112
121
|
projection keys and impact queries. Validators are pure, synchronous functions or
|
|
113
|
-
Standard Schema v1 validators.
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
Standard Schema v1 validators. Function validators are predicates/assertions: return
|
|
123
|
+
`true` or `undefined` for success, `false` for rejection, or throw from an assertion.
|
|
124
|
+
Standard Schema validation must return the original value by identity; transformed
|
|
125
|
+
outputs are rejected. Validators must not mutate their input. Perform transformations
|
|
126
|
+
before calling Doxum. `parse(model, unknown)`
|
|
116
127
|
validates and copies schema structure while sharing readonly payloads. Strict parsing
|
|
117
128
|
requires validators for atomic fields; typed in-memory fields can omit them.
|
|
118
129
|
|
|
@@ -196,6 +207,10 @@ values support reverse replay but are not trusted as local undo data; the runtim
|
|
|
196
207
|
captures its actual old state. Revision is a local baseline, not a distributed
|
|
197
208
|
conflict-resolution protocol.
|
|
198
209
|
|
|
210
|
+
Local/system apply may opt out of history with `history: false`. Remote apply uses
|
|
211
|
+
`{ expectedRevision, source: 'remote' }` and always invalidates local history; it has
|
|
212
|
+
no independent history option.
|
|
213
|
+
|
|
199
214
|
Published ChangeSets are readonly in their entirety, including envelope objects,
|
|
200
215
|
addresses, transitions and order arrays. The runtime reuses validation of its own
|
|
201
216
|
publications and normalized storage input by identity; fresh unknown input still
|
|
@@ -222,138 +237,223 @@ with `history: false` close the active group.
|
|
|
222
237
|
|
|
223
238
|
## Projections
|
|
224
239
|
|
|
240
|
+
Projection definitions are lazy and reusable. A `ProjectionRuntime` owns materialization,
|
|
241
|
+
incremental state, scheduling, publication, batching, recovery and Runtime-local inputs.
|
|
242
|
+
|
|
225
243
|
```ts
|
|
226
244
|
import { createProjectionRuntime, derive, input, observe } from 'doxum';
|
|
227
245
|
|
|
228
246
|
const tasks = observe(document, path => path.tasks);
|
|
229
247
|
const filter = input<'all' | 'open'>('all');
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
248
|
+
|
|
249
|
+
const visible = derive({ tasks, filter }, ({ tasks, filter }) =>
|
|
250
|
+
filter === 'all' ? tasks : new Map([...tasks].filter(([, task]) => !task.done))
|
|
251
|
+
);
|
|
234
252
|
|
|
235
253
|
const runtime = createProjectionRuntime({ onError: console.error });
|
|
236
|
-
|
|
237
|
-
const
|
|
254
|
+
|
|
255
|
+
const current = runtime.read(visible);
|
|
256
|
+
const selected = runtime.select(visible, tasks => tasks.get('a'));
|
|
238
257
|
const stop = selected.subscribe(() => selected.current());
|
|
239
|
-
|
|
240
|
-
runtime.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
258
|
+
|
|
259
|
+
runtime.update(filter, 'open');
|
|
260
|
+
runtime.batch(
|
|
261
|
+
() => {
|
|
262
|
+
runtime.update(filter, 'all');
|
|
263
|
+
document.update(draft => {
|
|
264
|
+
draft.title = 'Updated';
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
{ cause: { action: 'refresh' } }
|
|
268
|
+
);
|
|
269
|
+
|
|
246
270
|
stop();
|
|
271
|
+
runtime.dispose();
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The stable vocabulary is `read` for a synchronous current value, `select` for a
|
|
275
|
+
`Readable`, and `observe` for declaring a lazy source boundary. `ProjectionRuntime`
|
|
276
|
+
exposes `read`, `select`, `update`, `batch`, `scope` and `dispose`.
|
|
277
|
+
`ProjectionScope` mirrors `read`, `select`, `update`, `batch` and `dispose`, plus
|
|
278
|
+
`own(definitionOrTree)` for lifecycle ownership of lazy projection definitions.
|
|
279
|
+
|
|
280
|
+
### Keyed projection
|
|
281
|
+
|
|
282
|
+
`derive.keyed` preserves a keyed driver's membership and order while deriving entries
|
|
283
|
+
independently. The simple form is a per-entry selector:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const fieldValues = derive.keyed(records, record => record.values[fieldId]);
|
|
247
287
|
```
|
|
248
288
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
materialization, incremental state, publication and recovery stay inside that
|
|
253
|
-
one Runtime. Collection values are
|
|
254
|
-
immutable `ReadonlyMap`-like snapshots, while a `Readable` owns selector
|
|
255
|
-
tracking, equality and subscription lifecycle.
|
|
289
|
+
The output keeps one key per source entry. The selector runs only for affected keys,
|
|
290
|
+
and the optional equality function suppresses `updated` output transitions when the
|
|
291
|
+
derived value is equal.
|
|
256
292
|
|
|
257
|
-
|
|
293
|
+
Dynamic joins stay explicit in the dependency graph. Declare keyed lookups and ordinary
|
|
294
|
+
projection dependencies by name:
|
|
258
295
|
|
|
259
296
|
```ts
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
297
|
+
const cardContent = derive.keyed(
|
|
298
|
+
items,
|
|
299
|
+
{
|
|
300
|
+
record: { source: records, key: item => item.recordId },
|
|
301
|
+
view: activeView,
|
|
302
|
+
fields: visibleFields,
|
|
303
|
+
},
|
|
304
|
+
(item, itemId, { record, view, fields }) => renderCard(itemId, item, record, view, fields)
|
|
264
305
|
);
|
|
265
|
-
scope.get(localVisible);
|
|
266
|
-
scope.dispose(); // Releases local producers, state and subscriptions; parent tasks remain.
|
|
267
306
|
```
|
|
268
307
|
|
|
269
|
-
For
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
308
|
+
For `{ source, key }` dependencies, the Runtime owns output-key → source-key bindings
|
|
309
|
+
and reverse invalidation. A missing source entry resolves to `undefined` but remains
|
|
310
|
+
bound, so adding it later invalidates the dependent output keys. An ordinary projection
|
|
311
|
+
dependency invalidates the driver key set when its value changes. Processor code never
|
|
312
|
+
performs imperative Runtime reads to discover dependencies.
|
|
313
|
+
|
|
314
|
+
### Runtime-local keyed state
|
|
315
|
+
|
|
316
|
+
Use `input.collection` for selection, expanded state, local overrides and other
|
|
317
|
+
Runtime-local keyed application/UI state:
|
|
273
318
|
|
|
274
319
|
```ts
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
320
|
+
const selection = input.collection<RowId, SelectionState>(
|
|
321
|
+
new Map(),
|
|
322
|
+
(previous, next) => previous.selected === next.selected
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
runtime.update(selection, draft => {
|
|
326
|
+
draft.set(rowId, { selected: true });
|
|
327
|
+
draft.remove(previousRowId);
|
|
279
328
|
});
|
|
280
329
|
```
|
|
281
330
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
`
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
331
|
+
Its state is owned by the `ProjectionRuntime`, not the document, history or persistence.
|
|
332
|
+
Updates are synchronous and atomic. It publishes exact keyed `CollectionChange`
|
|
333
|
+
transitions and preserves order. The optional per-entry equality defaults to `Object.is`
|
|
334
|
+
and suppresses equivalent `set` operations. Callback and equality failures leave both
|
|
335
|
+
the published value and the next draft unchanged.
|
|
336
|
+
|
|
337
|
+
The processor-facing `CollectionChange` type is exported by `doxum/advanced`.
|
|
338
|
+
|
|
339
|
+
A root input definition can be materialized independently by several runtimes. Wrap any
|
|
340
|
+
definition in `scope.own(...)` when its lifecycle belongs to one scope; scalar inputs,
|
|
341
|
+
keyed inputs, derives and advanced group output trees all use the same ownership API.
|
|
342
|
+
Ownership must be assigned before that definition is first materialized as a root.
|
|
343
|
+
|
|
344
|
+
### Sources and scopes
|
|
345
|
+
|
|
346
|
+
`observe` adapts documents, Doxum `Readable` values and exported external source
|
|
347
|
+
contracts. Document `map`, `table` and `list(field, { keyOf })` paths become keyed
|
|
348
|
+
collection projections. Trees reuse the same model: `path.tree.rootId` is scalar,
|
|
349
|
+
`path.tree.nodes` is keyed and `path.tree.nodes.item(id)` is a single-node value source.
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
const scope = runtime.scope();
|
|
353
|
+
const localFilter = scope.own(input<'all' | 'open'>('all'));
|
|
354
|
+
const localVisible = scope.own(
|
|
355
|
+
derive({ tasks, filter: localFilter }, ({ tasks, filter }) =>
|
|
356
|
+
filter === 'all' ? tasks : new Map([...tasks].filter(([, task]) => !task.done))
|
|
357
|
+
)
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
scope.read(localVisible);
|
|
361
|
+
scope.dispose();
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
A scope shares its parent Runtime's scheduler and source materialization but owns the
|
|
365
|
+
lifetime of definitions passed to `scope.own`, their materialized producers, state and
|
|
366
|
+
subscriptions. Scoped definitions may depend on root definitions; root definitions and
|
|
367
|
+
sibling scopes cannot depend on scoped definitions. A materialized root definition cannot
|
|
368
|
+
later be converted into a scoped definition.
|
|
369
|
+
|
|
370
|
+
### Advanced processors
|
|
371
|
+
|
|
372
|
+
Use `doxum/advanced` only when pure `derive` / `derive.keyed` cannot express retained
|
|
373
|
+
state, cross-key indexes, or direct incremental patches. Dependencies are always named.
|
|
374
|
+
The processor definition is closed and always owns `process()`. Add `state()` only when
|
|
375
|
+
the processor actually needs Runtime-owned retained state.
|
|
305
376
|
|
|
306
377
|
```ts
|
|
307
378
|
import { incremental } from 'doxum/advanced';
|
|
308
379
|
|
|
309
|
-
const doubled = incremental.collection(
|
|
310
|
-
|
|
311
|
-
|
|
380
|
+
const doubled = incremental.collection(
|
|
381
|
+
{ tasks },
|
|
382
|
+
{
|
|
383
|
+
state: () => ({ initialized: false }),
|
|
384
|
+
process: ({ values, changes, output, state, reset }) => {
|
|
385
|
+
if (reset) {
|
|
386
|
+
for (const [id, task] of values.tasks) {
|
|
387
|
+
output.set(id, task.value * 2);
|
|
388
|
+
}
|
|
389
|
+
output.order([...values.tasks.keys()]);
|
|
390
|
+
state.initialized = true;
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const change = changes.tasks;
|
|
395
|
+
if (!change || change.kind === 'reset') return;
|
|
396
|
+
|
|
397
|
+
for (const entry of change.added) output.set(entry.key, entry.after.value * 2);
|
|
398
|
+
for (const entry of change.updated) output.set(entry.key, entry.after.value * 2);
|
|
399
|
+
for (const entry of change.removed) output.remove(entry.key);
|
|
400
|
+
if (change.order) output.order([...values.tasks.keys()]);
|
|
401
|
+
},
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`incremental(...)` produces one scalar value projection.
|
|
407
|
+
`incremental.collection(...)` exposes keyed `previous`, `next` and `output`.
|
|
408
|
+
`incremental.group(...)` runs one processor for several atomic output leaves:
|
|
312
409
|
|
|
410
|
+
```ts
|
|
313
411
|
const render = incremental.group(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
412
|
+
{ tasks },
|
|
413
|
+
{
|
|
414
|
+
output: define => ({
|
|
317
415
|
shell: define.collection<string, { readonly title: string }>(),
|
|
318
416
|
content: define.collection<string, string>(),
|
|
417
|
+
count: define.value<number>(),
|
|
418
|
+
}),
|
|
419
|
+
state: () => ({ renders: 0 }),
|
|
420
|
+
process: ({ values, output, state }) => {
|
|
421
|
+
state.renders++;
|
|
422
|
+
for (const [id, task] of values.tasks) {
|
|
423
|
+
output.shell.set(id, { title: task.title });
|
|
424
|
+
output.content.set(id, task.title);
|
|
425
|
+
}
|
|
426
|
+
output.count.set(values.tasks.size);
|
|
319
427
|
},
|
|
320
|
-
labels: define.collection<string, string>(),
|
|
321
|
-
chrome: define.value<{ readonly count: number }>(),
|
|
322
|
-
revision: define.value<number>(),
|
|
323
|
-
}),
|
|
324
|
-
({ sources, outputs }) => {
|
|
325
|
-
for (const [id, task] of sources[0]) {
|
|
326
|
-
outputs.node.shell.set(id, { title: task.title });
|
|
327
|
-
outputs.node.content.set(id, task.title);
|
|
328
|
-
outputs.labels.set(id, task.title);
|
|
329
|
-
}
|
|
330
|
-
outputs.chrome.set({ count: sources[0].size });
|
|
331
|
-
outputs.revision.set(sources[0].size);
|
|
332
428
|
}
|
|
333
429
|
);
|
|
334
430
|
```
|
|
335
431
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
`
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
432
|
+
`define.collection<K,V>(equality?)` and `define.value<T>(equality?)` are output
|
|
433
|
+
declaration methods available only inside `output`. The returned object tree keeps the
|
|
434
|
+
same shape with ordinary `Projection` leaves. Value leaves must be initialized on
|
|
435
|
+
initial build or Runtime recovery; untouched value leaves retain their value during a
|
|
436
|
+
normal incremental run.
|
|
437
|
+
|
|
438
|
+
`values` and `changes` use the dependency names. Collection changes are exact
|
|
439
|
+
`added` / `updated` / `removed` transitions plus optional `order`; scalar dependency
|
|
440
|
+
changes are `undefined`. `reset` means the processor must reconcile from current source
|
|
441
|
+
values. Ordinary source resets preserve retained state when one exists. If a processor
|
|
442
|
+
faults, the Runtime owns recovery, recreates declared state, and performs a reset
|
|
443
|
+
evaluation. Stateless processors use the same recovery path without a state object.
|
|
444
|
+
|
|
445
|
+
### React
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
const task = useProjection(tasks, tasks => tasks.get(taskId), equality);
|
|
449
|
+
const [mode, setMode] = useInput(filter);
|
|
450
|
+
const [selectedRows, updateSelectedRows] = useInput(selection);
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`useProjection(projection, selector, equality?)` uses the same keyed selector semantics
|
|
454
|
+
as Core. `useInput` handles both scalar `Input<T>` and `CollectionInput<K,V>`; collection
|
|
455
|
+
updates receive a keyed draft callback. `ProjectionProvider` accepts either a
|
|
456
|
+
`ProjectionRuntime` or `ProjectionScope`.
|
|
357
457
|
|
|
358
458
|
See [projection contracts](docs/projections.md).
|
|
359
459
|
|
|
@@ -364,6 +464,11 @@ runtime. One leader writes synchronously and persists final changes asynchronous
|
|
|
364
464
|
followers apply the durable sequence in order. `flush()` waits for persistence.
|
|
365
465
|
Durability errors do not roll back an already visible commit.
|
|
366
466
|
|
|
467
|
+
Operational failures use one `LocalSyncError` contract. Error states and `onError`
|
|
468
|
+
receive that same typed error and preserve an underlying `cause` when one exists.
|
|
469
|
+
Exceptions thrown by consumers of `state.subscribe` stay outside synchronization
|
|
470
|
+
fault state and are not reported through operational `onError`.
|
|
471
|
+
|
|
367
472
|
The storage format is IndexedDB version 5 with format version 3 records. Earlier
|
|
368
473
|
databases are rejected without upgrading, deleting or converting their data.
|
|
369
474
|
This JSON adapter rejects non-JSON atomic values. Network collaboration and
|
|
@@ -395,7 +500,7 @@ It also reports structural generation advances and distinguishes captured order
|
|
|
395
500
|
baselines from final published order copies. Architecture workloads include order
|
|
396
501
|
round trips and repeated tree edits to expose costs hidden by commit-only benchmarks.
|
|
397
502
|
|
|
398
|
-
Builds produce root `dist` ESM/CJS/declarations for `doxum`,
|
|
503
|
+
Builds produce root `dist` ESM/CJS/declarations for `doxum`,
|
|
399
504
|
`doxum/local-sync`, `doxum/react` and `doxum/advanced`. Source ownership is described in
|
|
400
505
|
[architecture](docs/architecture.md) and [AGENTS.md](AGENTS.md).
|
|
401
506
|
The runtime shares one transaction lifecycle; complete mutation operations are
|
package/dist/advanced.cjs
CHANGED
|
@@ -1,3 +1,238 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
3
|
-
|
|
2
|
+
const require_scheduler = require("./scheduler-D4VthaaK.cjs");
|
|
3
|
+
const require_record = require("./record-ChYUi8PP.cjs");
|
|
4
|
+
//#region core/src/projection/advanced.ts
|
|
5
|
+
const resetCollectionChange = Object.freeze({ kind: "reset" });
|
|
6
|
+
const publicValue = (source) => source.kind === "value" ? source.value : require_scheduler.collectionView(source.read);
|
|
7
|
+
const compileDependencies = (dependencies) => {
|
|
8
|
+
if (!require_record.isPlainObject(dependencies) || require_scheduler.isProjection(dependencies)) throw new TypeError("Incremental dependencies must be a plain object.");
|
|
9
|
+
const names = [];
|
|
10
|
+
const projections = [];
|
|
11
|
+
for (const key of Reflect.ownKeys(dependencies)) {
|
|
12
|
+
if (typeof key !== "string") throw new TypeError("Incremental dependency names must be strings.");
|
|
13
|
+
const descriptor = Object.getOwnPropertyDescriptor(dependencies, key);
|
|
14
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) throw new TypeError("Incremental dependencies must be enumerable data properties.");
|
|
15
|
+
require_scheduler.projectionRef(descriptor.value);
|
|
16
|
+
names.push(key);
|
|
17
|
+
projections.push(descriptor.value);
|
|
18
|
+
}
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
names: Object.freeze(names),
|
|
21
|
+
projections: Object.freeze(projections)
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const publicInputs = (sources, names, reset) => {
|
|
25
|
+
const values = Object.create(null);
|
|
26
|
+
const changes = Object.create(null);
|
|
27
|
+
for (let index = 0; index < names.length; index++) {
|
|
28
|
+
const source = sources[index];
|
|
29
|
+
values[names[index]] = publicValue(source);
|
|
30
|
+
changes[names[index]] = source.kind === "collection" ? reset ? resetCollectionChange : source.change : void 0;
|
|
31
|
+
}
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
values: Object.freeze(values),
|
|
34
|
+
changes: Object.freeze(changes)
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
function validateDefinition(definition, allowed, label) {
|
|
38
|
+
if (!require_record.isPlainObject(definition)) throw new TypeError(`${label} definition must be a plain object.`);
|
|
39
|
+
const allowedKeys = new Set(allowed);
|
|
40
|
+
for (const key of Reflect.ownKeys(definition)) {
|
|
41
|
+
if (typeof key !== "string" || !allowedKeys.has(key)) throw new TypeError(`${label} definition contains an unknown property.`);
|
|
42
|
+
const descriptor = Object.getOwnPropertyDescriptor(definition, key);
|
|
43
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) throw new TypeError(`${label} definition must contain enumerable data properties.`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const validateRetainedState = (definition, label) => {
|
|
47
|
+
if (Object.prototype.hasOwnProperty.call(definition, "state") && typeof definition.state !== "function") throw new TypeError(`${label} state must be a function when provided.`);
|
|
48
|
+
};
|
|
49
|
+
const initializeRetainedState = (definition) => {
|
|
50
|
+
if (definition.state === void 0) return Object.freeze({});
|
|
51
|
+
const state = definition.state();
|
|
52
|
+
require_scheduler.assertSynchronous(state);
|
|
53
|
+
return Object.freeze({ state });
|
|
54
|
+
};
|
|
55
|
+
function createIncrementalValue(dependencies, definition) {
|
|
56
|
+
const compiled = compileDependencies(dependencies);
|
|
57
|
+
validateDefinition(definition, ["state", "process"], "Incremental value");
|
|
58
|
+
validateRetainedState(definition, "Incremental value");
|
|
59
|
+
if (typeof definition.process !== "function") throw new TypeError("Incremental value definition requires a process function.");
|
|
60
|
+
const [projection] = require_scheduler.defineProcessor({
|
|
61
|
+
dependencies: compiled.projections,
|
|
62
|
+
outputs: [{
|
|
63
|
+
kind: "value",
|
|
64
|
+
equality: Object.is
|
|
65
|
+
}],
|
|
66
|
+
create: () => {
|
|
67
|
+
const state = initializeRetainedState(definition);
|
|
68
|
+
return { evaluate: (evaluation) => {
|
|
69
|
+
const output = evaluation.outputs[0];
|
|
70
|
+
if (output.kind !== "value") throw new Error("Incremental value output is invalid.");
|
|
71
|
+
const publicInput = publicInputs(evaluation.sources, compiled.names, evaluation.reset);
|
|
72
|
+
const result = definition.process({
|
|
73
|
+
values: publicInput.values,
|
|
74
|
+
changes: publicInput.changes,
|
|
75
|
+
previous: output.previous,
|
|
76
|
+
reset: evaluation.reset,
|
|
77
|
+
cause: evaluation.cause,
|
|
78
|
+
...state
|
|
79
|
+
});
|
|
80
|
+
require_scheduler.assertSynchronous(result);
|
|
81
|
+
output.output.set(result);
|
|
82
|
+
} };
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return projection;
|
|
86
|
+
}
|
|
87
|
+
function createIncrementalCollection(dependencies, definition) {
|
|
88
|
+
const compiled = compileDependencies(dependencies);
|
|
89
|
+
validateDefinition(definition, ["state", "process"], "Incremental collection");
|
|
90
|
+
validateRetainedState(definition, "Incremental collection");
|
|
91
|
+
if (typeof definition.process !== "function") throw new TypeError("Incremental collection definition requires a process function.");
|
|
92
|
+
const [projection] = require_scheduler.defineProcessor({
|
|
93
|
+
dependencies: compiled.projections,
|
|
94
|
+
outputs: [{
|
|
95
|
+
kind: "collection",
|
|
96
|
+
equality: Object.is
|
|
97
|
+
}],
|
|
98
|
+
create: () => {
|
|
99
|
+
const state = initializeRetainedState(definition);
|
|
100
|
+
return { evaluate: (evaluation) => {
|
|
101
|
+
const output = evaluation.outputs[0];
|
|
102
|
+
if (output.kind !== "collection") throw new Error("Incremental collection output is invalid.");
|
|
103
|
+
const publicInput = publicInputs(evaluation.sources, compiled.names, evaluation.reset);
|
|
104
|
+
require_scheduler.assertSynchronous(definition.process({
|
|
105
|
+
values: publicInput.values,
|
|
106
|
+
changes: publicInput.changes,
|
|
107
|
+
previous: output.previous,
|
|
108
|
+
next: output.next,
|
|
109
|
+
reset: evaluation.reset,
|
|
110
|
+
cause: evaluation.cause,
|
|
111
|
+
output: output.output,
|
|
112
|
+
...state
|
|
113
|
+
}));
|
|
114
|
+
} };
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return projection;
|
|
118
|
+
}
|
|
119
|
+
const isPlainGroupNamespace = (value, metadata) => require_record.isPlainObject(value) && !metadata.has(value);
|
|
120
|
+
const compileGroupShape = (value, path, metadata, used, outputs) => {
|
|
121
|
+
if (value !== null && typeof value === "object") {
|
|
122
|
+
const output = metadata.get(value);
|
|
123
|
+
if (output) {
|
|
124
|
+
if (used.has(value)) throw new TypeError("An incremental group output cannot be reused.");
|
|
125
|
+
used.add(value);
|
|
126
|
+
const index = outputs.length;
|
|
127
|
+
outputs.push({
|
|
128
|
+
kind: output.kind,
|
|
129
|
+
equality: output.equality,
|
|
130
|
+
path: Object.freeze([...path])
|
|
131
|
+
});
|
|
132
|
+
return index;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!isPlainGroupNamespace(value, metadata)) throw new TypeError("Incremental group outputs must be a static object tree.");
|
|
136
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
137
|
+
if (typeof key !== "string") throw new TypeError("Incremental group output names must be strings.");
|
|
138
|
+
if (!Object.prototype.propertyIsEnumerable.call(value, key)) throw new TypeError("Incremental group namespaces must contain enumerable properties.");
|
|
139
|
+
}
|
|
140
|
+
const keys = Object.keys(value);
|
|
141
|
+
if (!keys.length) throw new TypeError("Incremental group namespaces cannot be empty.");
|
|
142
|
+
const result = Object.create(null);
|
|
143
|
+
for (const key of keys) {
|
|
144
|
+
if (key === "__proto__" || key === "prototype" || key === "constructor") throw new TypeError(`Invalid incremental group output name: ${key}`);
|
|
145
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
146
|
+
if (!descriptor || !("value" in descriptor)) throw new TypeError("Incremental group namespaces cannot contain accessors.");
|
|
147
|
+
result[key] = compileGroupShape(descriptor.value, [...path, key], metadata, used, outputs);
|
|
148
|
+
}
|
|
149
|
+
return Object.freeze(result);
|
|
150
|
+
};
|
|
151
|
+
const hydrateGroupShape = (shape, values) => {
|
|
152
|
+
if (typeof shape === "number") return values[shape];
|
|
153
|
+
const result = {};
|
|
154
|
+
for (const [key, child] of Object.entries(shape)) result[key] = hydrateGroupShape(child, values);
|
|
155
|
+
return Object.freeze(result);
|
|
156
|
+
};
|
|
157
|
+
const hydrateGroupReads = (shape, outputs, next) => {
|
|
158
|
+
if (typeof shape === "number") {
|
|
159
|
+
const output = outputs[shape];
|
|
160
|
+
return next && output.kind === "value" ? output.next() : next ? output.next : output.previous;
|
|
161
|
+
}
|
|
162
|
+
const result = {};
|
|
163
|
+
for (const [key, child] of Object.entries(shape)) if (typeof child === "number" && next && outputs[child].kind === "value") Object.defineProperty(result, key, {
|
|
164
|
+
enumerable: true,
|
|
165
|
+
configurable: false,
|
|
166
|
+
get: () => outputs[child].next()
|
|
167
|
+
});
|
|
168
|
+
else result[key] = hydrateGroupReads(child, outputs, next);
|
|
169
|
+
return Object.freeze(result);
|
|
170
|
+
};
|
|
171
|
+
function createIncrementalGroup(dependencies, definition) {
|
|
172
|
+
const compiled = compileDependencies(dependencies);
|
|
173
|
+
validateDefinition(definition, [
|
|
174
|
+
"output",
|
|
175
|
+
"state",
|
|
176
|
+
"process"
|
|
177
|
+
], "Incremental group");
|
|
178
|
+
validateRetainedState(definition, "Incremental group");
|
|
179
|
+
if (typeof definition.output !== "function" || typeof definition.process !== "function") throw new TypeError("Incremental group definition requires output and process functions.");
|
|
180
|
+
let active = true;
|
|
181
|
+
const metadata = /* @__PURE__ */ new Map();
|
|
182
|
+
const declared = definition.output({
|
|
183
|
+
collection: (equality = Object.is) => {
|
|
184
|
+
if (!active) throw new TypeError("Incremental group output declarations are synchronous.");
|
|
185
|
+
const descriptor = Object.freeze({});
|
|
186
|
+
metadata.set(descriptor, {
|
|
187
|
+
kind: "collection",
|
|
188
|
+
equality
|
|
189
|
+
});
|
|
190
|
+
return descriptor;
|
|
191
|
+
},
|
|
192
|
+
value: (equality = Object.is) => {
|
|
193
|
+
if (!active) throw new TypeError("Incremental group output declarations are synchronous.");
|
|
194
|
+
const descriptor = Object.freeze({});
|
|
195
|
+
metadata.set(descriptor, {
|
|
196
|
+
kind: "value",
|
|
197
|
+
equality
|
|
198
|
+
});
|
|
199
|
+
return descriptor;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
require_scheduler.assertSynchronous(declared);
|
|
203
|
+
active = false;
|
|
204
|
+
const outputs = [];
|
|
205
|
+
const used = /* @__PURE__ */ new Set();
|
|
206
|
+
const shape = compileGroupShape(declared, [], metadata, used, outputs);
|
|
207
|
+
if (typeof shape === "number") throw new TypeError("Incremental group declarations must return an output namespace.");
|
|
208
|
+
if (used.size === 0 || used.size !== metadata.size) throw new TypeError("Every incremental group output must be returned by the declaration.");
|
|
209
|
+
return hydrateGroupShape(shape, require_scheduler.defineProcessor({
|
|
210
|
+
dependencies: compiled.projections,
|
|
211
|
+
outputs,
|
|
212
|
+
create: () => {
|
|
213
|
+
const state = initializeRetainedState(definition);
|
|
214
|
+
return { evaluate: (evaluation) => {
|
|
215
|
+
const publicInput = publicInputs(evaluation.sources, compiled.names, evaluation.reset);
|
|
216
|
+
require_scheduler.assertSynchronous(definition.process({
|
|
217
|
+
values: publicInput.values,
|
|
218
|
+
changes: publicInput.changes,
|
|
219
|
+
previous: hydrateGroupReads(shape, evaluation.outputs, false),
|
|
220
|
+
next: hydrateGroupReads(shape, evaluation.outputs, true),
|
|
221
|
+
output: hydrateGroupShape(shape, evaluation.outputs.map((output) => output.output)),
|
|
222
|
+
reset: evaluation.reset,
|
|
223
|
+
cause: evaluation.cause,
|
|
224
|
+
...state
|
|
225
|
+
}));
|
|
226
|
+
} };
|
|
227
|
+
},
|
|
228
|
+
name: `processor-group:${outputs.map((output) => output.path?.join(".") ?? "").join(",")}`
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
const incremental = Object.assign(createIncrementalValue, {
|
|
232
|
+
collection: createIncrementalCollection,
|
|
233
|
+
group: createIncrementalGroup
|
|
234
|
+
});
|
|
235
|
+
//#endregion
|
|
236
|
+
exports.incremental = incremental;
|
|
237
|
+
|
|
238
|
+
//# sourceMappingURL=advanced.cjs.map
|