doxum 0.1.23 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +208 -102
  2. package/dist/advanced.cjs +237 -2
  3. package/dist/advanced.cjs.map +1 -0
  4. package/dist/advanced.d.cts +90 -2
  5. package/dist/advanced.d.ts +90 -2
  6. package/dist/advanced.js +236 -1
  7. package/dist/advanced.js.map +1 -0
  8. package/dist/{contract-ULPQmiEs.d.ts → contract-CNR6BS75.d.cts} +216 -30
  9. package/dist/{contract-SYJFP0lM.d.cts → contract-COgxO5sv.d.ts} +216 -30
  10. package/dist/definition-CDOuvb5C.d.cts +111 -0
  11. package/dist/definition-xgk3YNBp.d.ts +111 -0
  12. package/dist/{driver-yFfvDMX5.js → driver-B5vdPohL.js} +59 -3
  13. package/dist/driver-B5vdPohL.js.map +1 -0
  14. package/dist/{driver-BD3DQOW6.cjs → driver-BL91gn7_.cjs} +107 -9
  15. package/dist/driver-BL91gn7_.cjs.map +1 -0
  16. package/dist/index.cjs +1637 -703
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +65 -33
  19. package/dist/index.d.ts +65 -33
  20. package/dist/index.js +1432 -497
  21. package/dist/index.js.map +1 -1
  22. package/dist/local-sync.cjs +69 -103
  23. package/dist/local-sync.cjs.map +1 -1
  24. package/dist/local-sync.d.cts +14 -26
  25. package/dist/local-sync.d.ts +14 -26
  26. package/dist/local-sync.js +68 -96
  27. package/dist/local-sync.js.map +1 -1
  28. package/dist/react.cjs +4 -5
  29. package/dist/react.cjs.map +1 -1
  30. package/dist/react.d.cts +6 -8
  31. package/dist/react.d.ts +6 -8
  32. package/dist/react.js +4 -5
  33. package/dist/react.js.map +1 -1
  34. package/dist/record-B2yRhYsc.js +28 -0
  35. package/dist/record-B2yRhYsc.js.map +1 -0
  36. package/dist/record-ChYUi8PP.cjs +45 -0
  37. package/dist/record-ChYUi8PP.cjs.map +1 -0
  38. package/dist/scheduler--137L3j-.js +500 -0
  39. package/dist/scheduler--137L3j-.js.map +1 -0
  40. package/dist/scheduler-D4VthaaK.cjs +589 -0
  41. package/dist/scheduler-D4VthaaK.cjs.map +1 -0
  42. package/package.json +6 -3
  43. package/skills/doxum-runtime/SKILL.md +32 -7
  44. package/skills/doxum-runtime/agents/openai.yaml +1 -1
  45. package/skills/doxum-runtime/references/api.en.md +249 -0
  46. package/skills/doxum-runtime/references/api.zh-CN.md +242 -0
  47. package/skills/doxum-runtime/references/guide.en.md +18 -8
  48. package/skills/doxum-runtime/references/guide.zh-CN.md +14 -6
  49. package/skills/doxum-runtime/references/invariants.en.md +29 -5
  50. package/skills/doxum-runtime/references/invariants.zh-CN.md +24 -6
  51. package/skills/doxum-runtime/references/patterns.en.md +78 -18
  52. package/skills/doxum-runtime/references/patterns.zh-CN.md +76 -18
  53. package/skills/doxum-runtime/references/projections.en.md +209 -119
  54. package/skills/doxum-runtime/references/projections.zh-CN.md +200 -101
  55. package/dist/advanced-CLJCWkHa.d.cts +0 -176
  56. package/dist/advanced-CRNHfJBu.js +0 -1224
  57. package/dist/advanced-CRNHfJBu.js.map +0 -1
  58. package/dist/advanced-Cle5g3B4.cjs +0 -1517
  59. package/dist/advanced-Cle5g3B4.cjs.map +0 -1
  60. package/dist/advanced-d6J61er9.d.ts +0 -176
  61. package/dist/context-BcLKGfq8.cjs +0 -149
  62. package/dist/context-BcLKGfq8.cjs.map +0 -1
  63. package/dist/context-DFEaDUv5.js +0 -84
  64. package/dist/context-DFEaDUv5.js.map +0 -1
  65. package/dist/driver-BD3DQOW6.cjs.map +0 -1
  66. package/dist/driver-yFfvDMX5.js.map +0 -1
@@ -1,150 +1,240 @@
1
1
  # Projection Reference
2
2
 
3
- Root projection definitions are lazy and reusable; scoped definitions are lazy
4
- and bound to one scope. The public core consists of
5
- `Projection<T>`, `ProjectionRuntime`, `input`, `observe`, and tuple `derive`.
3
+ For exact signatures, use the [API reference](api.en.md). This document explains
4
+ ownership, invalidation and recovery.
5
+
6
+ ## Ownership and basic declarations
7
+
8
+ Projection definitions are lazy. Root definitions are reusable across runtimes;
9
+ scope definitions belong to one `ProjectionScope`. A public `Projection<T>` contains
10
+ no materialized value, state, subscription or disposal state.
11
+ `KeyedProjection<K,V>` is the corresponding public keyed handle and is safe to export
12
+ from another package's declarations.
6
13
 
7
14
  ```ts
8
15
  const tasks = observe(document, path => path.tasks);
9
16
  const filter = input<'all' | 'open'>('all');
10
- const visible = derive([tasks, filter], (tasks, filter) =>
11
- filter === 'all' ? tasks : new Map([...tasks].filter(([, task]) => !task.done))
17
+ const visible = derive({ tasks, filter }, ({ tasks, filter }) =>
18
+ filter === 'all' ? tasks : filterOpen(tasks)
12
19
  );
20
+
13
21
  const runtime = createProjectionRuntime({ onError: report });
14
- runtime.get(visible);
22
+ runtime.read(visible);
23
+ ```
24
+
25
+ `ProjectionRuntime` is the single owner of materialization, retained state, source
26
+ attachments, settlement, publication and recovery. Its stable verbs are `read`,
27
+ `select`, overloaded `update`, `batch`, `scope`, and `dispose`.
28
+
29
+ `input(initial, equality?)` is scalar Runtime-local state. `input.collection(initial?,
30
+ equality?)` is keyed Runtime-local application/UI state. Both are outside canonical
31
+ document history/persistence.
32
+
33
+ ## Source boundaries
34
+
35
+ `observe` is the one source declaration entry point. It adapts:
36
+
37
+ - a whole `ReadonlyDocument` or a schema-path selection;
38
+ - a Doxum `Readable<T>`;
39
+ - exported external value/collection source contracts.
40
+
41
+ Document `map`, `table`, and `list(field, { keyOf })` selections are keyed projections.
42
+ List identity comes from schema `keyOf`. Atomic array-valued fields remain scalar.
43
+
44
+ Trees reuse the same source model:
45
+
46
+ ```ts
47
+ const rootId = observe(document, path => path.outline.rootId);
48
+ const nodes = observe(document, path => path.outline.nodes);
49
+ const node = observe(document, path => path.outline.nodes.item(nodeId));
50
+ ```
51
+
52
+ `rootId` is scalar, `nodes` is keyed, and `item(id)` is one node value. A document
53
+ commit settles all affected source projections in one Runtime causal batch.
54
+
55
+ External collection impact is only a boundary invalidation hint. The Runtime derives
56
+ the exact `CollectionChange` before processors observe it.
57
+
58
+ ## Pure derive and keyed projection
59
+
60
+ `derive(dependencies, compute, equality?)` always takes a named dependency object.
61
+ Dependencies are explicit and fixed when the definition is created.
62
+
63
+ Use `derive.keyed` when one keyed driver owns output membership and order:
64
+
65
+ ```ts
66
+ const fieldValues = derive.keyed(records, record => record.values[fieldId]);
67
+ ```
68
+
69
+ Only affected driver entries execute the selector. Per-entry equality suppresses an
70
+ `updated` output transition when the selected value is unchanged. Added, removed and
71
+ order transitions retain their normal collection meaning.
72
+
73
+ Dynamic keyed lookups stay in the same API:
74
+
75
+ ```ts
76
+ const cardContent = derive.keyed(
77
+ items,
78
+ {
79
+ record: { source: records, key: item => item.recordId },
80
+ view: activeView,
81
+ fields: visibleFields,
82
+ },
83
+ (item, itemId, { record, view, fields }) => renderCard(itemId, item, record, view, fields)
84
+ );
85
+ ```
86
+
87
+ A plain Projection dependency invalidates the driver key set when it changes.
88
+ `{ source, key }` declares an output-key → dynamic source-key dependency. The Runtime
89
+ owns forward bindings and reverse invalidation. Missing selected entries remain bound,
90
+ so adding that source key later invalidates its dependents.
91
+
92
+ This is the keyed join protocol. Do not add a second join abstraction, wildcard
93
+ document-path grammar, or processor-side imperative dependency discovery.
94
+
95
+ ## Runtime reads, updates and scope
96
+
97
+ ```ts
98
+ const value = runtime.read(visible);
99
+ const selected = runtime.select(visible, rows => rows.get(rowId), equality);
100
+ const stop = selected.subscribe(listener);
101
+
102
+ runtime.update(filter, 'open');
103
+ runtime.update(selection, draft => {
104
+ draft.set(rowId, true);
105
+ draft.remove(previousRowId);
106
+ });
107
+
108
+ runtime.batch(run, { cause });
15
109
  ```
16
110
 
17
- `input(initial, equality?)` is Runtime-local and can be written only with
18
- `runtime.set(input, value)`. `observe(document, selector)` compiles a document
19
- boundary lazily. Collection paths publish immutable map-like snapshots; the
20
- whole-document form publishes a snapshot of the root. Existing Doxum Readables
21
- and eventful external sources can also be observed at this boundary. External
22
- sources use `kind: 'value'` or `kind: 'collection'`; the caller still uses only
23
- `observe(source)`.
24
-
25
- Schema maps, tables, and `list(field, { keyOf })` nodes are all keyed collection
26
- paths. A list uses its schema `keyOf` identity and preserves document order in
27
- iteration; array-valued fields remain scalar value observations.
28
- Tree structure reuses the same source kinds: `path.tree.rootId` is a scalar,
29
- `path.tree.nodes` is a keyed collection, and `path.tree.nodes.item(id)` is a
30
- single-node value. Committed tree-node groups route directly to affected keyed
31
- entries, and the existing `CollectionOutput` publishes ordinary `CollectionChange`
32
- entries, so unrelated node snapshots keep their identity.
33
-
34
- External events are smaller than Runtime contexts. A value event carries the new
35
- `value` and `revision`; a collection event carries a stable `previous` read,
36
- `revision`, and an optional `impact` hint. The Runtime derives the exact
37
- `CollectionChange`; processors never receive the impact directly.
38
-
39
- `derive(dependencies, compute, equality?)` takes a tuple. Dependencies are
40
- explicit and fixed when the definition is created. Processors must not discover
41
- graph dependencies by reading other projections.
42
-
43
- The Runtime API is intentionally small:
111
+ `select` returns the standard `Readable`. Keyed selectors track relevant key/structure
112
+ reads; equality filters the result only after a related invalidation.
113
+
114
+ `CollectionInputDraft` exposes `get`, `has`, `set`, `remove` and expires with the
115
+ synchronous edit callback. A throwing callback applies none of that edit. Collection
116
+ input equality is per entry and defaults to `Object.is`; it runs before accepted local
117
+ state is installed, so an equality failure also leaves the published value and next
118
+ draft unchanged. Equal sets do not replace stored data or publish.
44
119
 
45
120
  ```ts
46
- runtime.get(projection);
47
- const selected = runtime.readable(projection, value => value.get(id));
48
- selected.subscribe(listener);
49
- runtime.set(input, value);
50
- runtime.update(collectionInput, draft => draft.set(id, value));
51
- runtime.batch({ cause }, run);
52
121
  const scope = runtime.scope();
122
+ const localFilter = scope.own(input<'all' | 'open'>('all'));
123
+ const local = scope.own(
124
+ derive({ tasks, filter: localFilter }, ({ tasks, filter }) =>
125
+ filter === 'all' ? tasks : filterOpen(tasks)
126
+ )
127
+ );
128
+ scope.read(local);
129
+ scope.update(localFilter, 'open');
53
130
  scope.dispose();
54
- runtime.dispose();
55
131
  ```
56
132
 
57
- `input.collection<K, V>(initial?)` declares keyed Runtime-local state. Its
58
- `update` draft has `get`, `has`, `set`, and `remove`; edits are synchronous and
59
- atomic per callback, and a Runtime batch publishes one exact net
60
- `CollectionChange`. A scope declares local inputs, derives, and incremental
61
- processors through `scope.input`, `scope.derive`, and `scope.incremental`. They
62
- depend directly on root projections in the same Runtime. Disposing the scope
63
- releases local producers, state and subscriptions without disposing its root
64
- dependencies.
133
+ A scope shares the parent scheduler/materialization owner. `scope.own` assigns one
134
+ lifecycle to a projection definition or a static nested projection tree. Scoped
135
+ definitions may depend on root definitions; root and sibling scopes cannot depend on
136
+ scoped definitions, and one definition can belong to only one scope. Assign scope
137
+ ownership before the definition is first materialized as a root.
138
+
139
+ ## One collection change protocol
140
+
141
+ All keyed projection sources and outputs use:
142
+
143
+ ```ts
144
+ type CollectionChange<K extends string, V> =
145
+ | { kind: 'reset' }
146
+ | {
147
+ kind: 'incremental';
148
+ added: readonly { key: K; after: V }[];
149
+ updated: readonly { key: K; before: V; after: V }[];
150
+ removed: readonly { key: K; before: V }[];
151
+ order?: { before: readonly K[]; after: readonly K[] };
152
+ };
153
+ ```
154
+
155
+ Import `CollectionChange` from `doxum/advanced` when an advanced processor signature
156
+ needs it. Root projection consumers do not import this transport type.
157
+
158
+ Initial materialization and source reset report `reset` to advanced processors.
159
+ Incremental transitions are exact net changes across the settled Runtime batch.
65
160
 
66
- The Runtime exposes no scheduler/output internals, item handle, rebuild or release operation;
67
- only a `Readable`'s own publication revision is public for store integrations.
68
- Runtime materialization, keyed storage, recovery and disposal are one owner.
161
+ ## Advanced processors
69
162
 
70
- ## Incremental processors
163
+ Import `incremental` from `doxum/advanced` only when retained state, cross-key indexes,
164
+ or direct incremental output patching cannot be expressed cleanly with `derive` or
165
+ `derive.keyed`.
71
166
 
72
- Import retained-state processors from `doxum/advanced`:
167
+ All advanced processors use named dependencies plus a required `process`. Add `state()`
168
+ only when the processor needs retained state:
73
169
 
74
170
  ```ts
75
- const total = incremental([tasks], ({ sources, previous, state }) => {
76
- state.calls = Number(state.calls ?? 0) + 1;
77
- return sources[0].size + Number(state.calls) + (previous ?? 0);
78
- });
171
+ const weights = incremental.collection(
172
+ { tasks },
173
+ {
174
+ state: () => ({ initialized: false }),
175
+ process: ({ values, changes, output, state, reset }) => {
176
+ if (reset) {
177
+ for (const [id, task] of values.tasks) output.set(id, task.weight);
178
+ output.order([...values.tasks.keys()]);
179
+ state.initialized = true;
180
+ return;
181
+ }
182
+
183
+ const change = changes.tasks;
184
+ if (!change || change.kind === 'reset') return;
185
+ for (const entry of change.added) output.set(entry.key, entry.after.weight);
186
+ for (const entry of change.updated) output.set(entry.key, entry.after.weight);
187
+ for (const entry of change.removed) output.remove(entry.key);
188
+ if (change.order) output.order([...values.tasks.keys()]);
189
+ },
190
+ }
191
+ );
192
+ ```
79
193
 
80
- const doubled = incremental.collection([tasks], ({ sources, output }) => {
81
- for (const [id, task] of sources[0]) output.set(id, task.value * 2);
82
- });
194
+ `values` and `changes` are keyed by dependency name. Scalar dependencies have no
195
+ collection change metadata. Collection processors additionally expose keyed
196
+ `previous`, `next`, and a borrowed `output` draft.
197
+
198
+ Multi-output processors use one closed group definition:
83
199
 
200
+ ```ts
84
201
  const render = incremental.group(
85
- [tasks],
86
- define => ({
87
- node: {
88
- shell: define.collection<string, Shell>(),
89
- content: define.collection<string, Content>(),
202
+ { tasks },
203
+ {
204
+ output: define => ({
205
+ shell: define.collection<RowId, Shell>(),
206
+ count: define.value<number>(),
207
+ }),
208
+ state: () => ({ runs: 0 }),
209
+ process: ({ values, output, state }) => {
210
+ state.runs++;
211
+ for (const [id, task] of values.tasks) output.shell.set(id, makeShell(task));
212
+ output.count.set(values.tasks.size);
90
213
  },
91
- labels: define.collection<string, Label>(),
92
- chrome: define.value<Chrome>(),
93
- revision: define.value<number>(),
94
- }),
95
- ({ sources, outputs }) => {
96
- for (const [id, task] of sources[0]) {
97
- outputs.node.shell.set(id, makeShell(task));
98
- outputs.node.content.set(id, makeContent(task));
99
- outputs.labels.set(id, makeLabel(task));
100
- }
101
- outputs.chrome.set(makeChrome(sources[0]));
102
- outputs.revision.set(sources[0].size);
103
214
  }
104
215
  );
105
216
  ```
106
217
 
107
- Both processor contexts expose `sources`, a dependency-aligned `changes` tuple,
108
- `previous`, `reset`, `cause` and a retained `state` object. `changes[i]` belongs
109
- to dependency `i`: scalar dependencies are `undefined`, while collection
110
- dependencies receive `reset` or grouped `added`/`updated`/`removed` entries with
111
- complete `before`/`after` values and optional `order.before`/`order.after`.
112
- The initial build reports `reset` for collection dependencies. A committed batch
113
- is already coalesced into one net transition.
114
- Collection values inside `sources` are callback-local lazy `ReadonlyMap` views;
115
- do not retain or return them. Keyed `get`/`has` avoids a scan, while iteration is
116
- an explicit full-collection read.
117
-
118
- `incremental.collection(...)` uses a separate collection processor protocol and additionally exposes borrowed
119
- `previous`/`next` keyed reads and a callback-local `output` draft. Draft methods are
120
- `set`, `remove`, and `order`; the Runtime validates and seals them after
121
- the synchronous callback, computes keyed transitions and publishes one immutable
122
- map-like value. Reset or fault recovery is internal.
123
- `incremental.group(...)` is the composition boundary for several named value and
124
- keyed collection outputs. `define.value<T>(equality?)` creates a scalar leaf and
125
- `define.collection<K, V>(equality?)` creates a keyed leaf. Its nested namespace is
126
- static API organization, not another producer, runtime or scheduler concept. Every
127
- leaf is an ordinary `Projection` pointing to one output of the same processor
128
- producer; one processor execution seals and publishes all changed leaves atomically,
129
- and downstream processors depend on those leaves directly.
130
- Initial builds and rebuilds must set every value leaf; ordinary incremental runs
131
- may leave a value leaf untouched to preserve its current value and revision.
132
- Reading any leaf materializes that producer once, while scope disposal releases the
133
- producer and its retained state once.
134
-
135
- ## React selector tracking
136
-
137
- ```tsx
138
- const task = useProjection(visible, tasks => tasks.get(taskId));
139
- const [mode, setMode] = useInput(filter);
140
- ```
218
+ `define.collection<K,V>(equality?)` and `define.value<T>(equality?)` are available only
219
+ inside `output`. The returned static object tree is mirrored by ordinary Projection
220
+ leaves from the same producer: collections are `KeyedProjection<K,V>` and values are
221
+ `Projection<T>`. Value leaves must be established on initial build and
222
+ Runtime recovery; untouched value leaves keep their value on ordinary incremental runs.
223
+
224
+ Normal source resets preserve retained state when declared. A processor fault is
225
+ recovered by the Runtime: it recreates declared state and performs a reset evaluation.
226
+ Stateless processors follow the same recovery path without a state object. No public
227
+ rebuild token or manual recovery hook exists.
228
+
229
+ ## React
141
230
 
142
- Collection `get`/`has` records one key, `keys` records key/order structure, and
143
- `values`/iteration records the whole collection. Unrelated key changes do not run
144
- the selector. After a related update, the selector's equality (default
145
- `Object.is`) decides whether the readable publishes. React consumes the same
146
- `Readable` boundary; projection dependencies remain explicit.
231
+ `ProjectionProvider` supplies a Runtime or scope. `useProjection(projection, selector,
232
+ equality?)` uses the same Core selector semantics; `useInput` overloads scalar and
233
+ collection inputs. Document reads remain separate:
234
+
235
+ ```ts
236
+ const value = useDocumentSelector(document, selector, equality);
237
+ ```
147
238
 
148
- Processors settle before listeners. Writes are forbidden during notification.
149
- Listener errors do not roll back an accepted document commit; processor errors use
150
- the Runtime error callback and internal recovery.
239
+ Processors settle before projection listeners. Writes are forbidden while processing
240
+ or notifying. Listener failure does not roll back an accepted document commit.