doxum 0.1.29 → 0.1.30
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 +3 -2
- package/dist/index.cjs +6 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/skills/doxum-runtime/SKILL.md +64 -82
- package/skills/doxum-runtime/agents/openai.yaml +2 -2
- package/skills/doxum-runtime/references/document-runtime.md +370 -0
- package/skills/doxum-runtime/references/integrations.md +215 -0
- package/skills/doxum-runtime/references/{invariants.en.md → invariants.md} +5 -2
- package/skills/doxum-runtime/references/projections.md +467 -0
- package/skills/doxum-runtime/references/public-api.md +623 -0
- package/skills/doxum-runtime/references/recipes.md +493 -0
- package/skills/doxum-runtime/references/api.en.md +0 -283
- package/skills/doxum-runtime/references/api.zh-CN.md +0 -276
- package/skills/doxum-runtime/references/guide.en.md +0 -138
- package/skills/doxum-runtime/references/guide.zh-CN.md +0 -116
- package/skills/doxum-runtime/references/invariants.zh-CN.md +0 -53
- package/skills/doxum-runtime/references/patterns.en.md +0 -162
- package/skills/doxum-runtime/references/patterns.zh-CN.md +0 -159
- package/skills/doxum-runtime/references/projections.en.md +0 -313
- package/skills/doxum-runtime/references/projections.zh-CN.md +0 -306
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Doxum Integrations
|
|
2
|
+
|
|
3
|
+
Use this reference for standard `Readable` integration, external projection sources, React bindings, and browser local-sync. Core document/projection semantics live in [Document runtime](document-runtime.md) and [Projections](projections.md); exact signatures are in [Public API](public-api.md).
|
|
4
|
+
|
|
5
|
+
## Standard `Readable`
|
|
6
|
+
|
|
7
|
+
Doxum exposes one small observable contract:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
type Readable<T> = {
|
|
11
|
+
current(): T;
|
|
12
|
+
revision(): number;
|
|
13
|
+
subscribe(listener: () => void): () => void;
|
|
14
|
+
};
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`select(document, ...)`, `runtime.select(...)`, `runtime.items(...).keys`, item readables, history, and local-sync state all use this contract. Subscription listeners receive no payload; read the latest published value with `current()`.
|
|
18
|
+
|
|
19
|
+
Consumer listener failure does not retroactively roll back the source mutation that caused the notification.
|
|
20
|
+
|
|
21
|
+
## External value sources
|
|
22
|
+
|
|
23
|
+
Adapt a non-Doxum scalar source through `ExternalValueSource<T>` and then call `observe(source)`:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
const source: ExternalValueSource<Theme> = {
|
|
27
|
+
kind: 'value',
|
|
28
|
+
current: () => currentTheme,
|
|
29
|
+
revision: () => revision,
|
|
30
|
+
subscribe(listener) {
|
|
31
|
+
listeners.add(listener);
|
|
32
|
+
return () => listeners.delete(listener);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const theme = observe(source);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Publish events with `{ value, revision, reset?, cause? }`. The source owns its revision and stable current value. `reset` marks a reset boundary rather than ordinary incremental continuation.
|
|
40
|
+
|
|
41
|
+
External source callbacks are synchronous from Doxum's perspective. Do not expose an async `current()` or Promise-valued event.
|
|
42
|
+
|
|
43
|
+
## External keyed collection sources
|
|
44
|
+
|
|
45
|
+
Use the external collection contract when the source itself has keyed membership/order and can provide stable snapshots:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const source: ExternalCollectionSource<RowId, Row> = {
|
|
49
|
+
kind: 'collection',
|
|
50
|
+
current: () => ({
|
|
51
|
+
get: id => rows.get(id),
|
|
52
|
+
has: id => rows.has(id),
|
|
53
|
+
ids: () => orderedIds,
|
|
54
|
+
}),
|
|
55
|
+
revision: () => revision,
|
|
56
|
+
subscribe(listener) {
|
|
57
|
+
listeners.add(listener);
|
|
58
|
+
return () => listeners.delete(listener);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
An event carries the stable read from before the external update/batch:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
listener({
|
|
67
|
+
previous,
|
|
68
|
+
revision: nextRevision,
|
|
69
|
+
impact?: {
|
|
70
|
+
kind: 'incremental',
|
|
71
|
+
added: new Set<RowId>(),
|
|
72
|
+
removed: new Set<RowId>(),
|
|
73
|
+
updated: new Set<RowId>(),
|
|
74
|
+
orderChanged: false,
|
|
75
|
+
},
|
|
76
|
+
cause,
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`impact` is an invalidation hint, not the processor-facing final change. Doxum reads the current source and normalizes source state into exact `CollectionChange` semantics before processors consume it. Use reset impact when the adapter cannot provide a trustworthy incremental hint.
|
|
81
|
+
|
|
82
|
+
The `previous` read must remain stable for that event/external batch. Do not back it with mutable data that changes underneath the event.
|
|
83
|
+
|
|
84
|
+
## React ownership
|
|
85
|
+
|
|
86
|
+
React consumes Doxum through `doxum/react` only. Do not duplicate Runtime subscriptions or reach into projection internals from components.
|
|
87
|
+
|
|
88
|
+
### Projection provider
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
const runtime = useMemo(() => createProjectionRuntime({ onError: report }), []);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<ProjectionProvider value={runtime}>
|
|
95
|
+
<App />
|
|
96
|
+
</ProjectionProvider>
|
|
97
|
+
);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The provider accepts a root `ProjectionRuntime` or `ProjectionScope`. The component/service that creates it owns disposal. Do not create a new Runtime on every render.
|
|
101
|
+
|
|
102
|
+
### Reading projections
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
const rows = useProjection(visibleRows);
|
|
106
|
+
const active = useProjection(rowsProjection, rows => rows.get(activeId));
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Selector form reuses Core projection selector semantics, including keyed read tracking and optional result equality.
|
|
110
|
+
|
|
111
|
+
For stable per-membership item `Readable`s, use `runtime.items` / `scope.items` outside ad-hoc component caches and consume individual readables with `useReadable`.
|
|
112
|
+
|
|
113
|
+
### Projection inputs
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
const [filter, setFilter] = useInput(filterInput);
|
|
117
|
+
|
|
118
|
+
const [selection, editSelection] = useInput(selectionInput);
|
|
119
|
+
editSelection(draft => {
|
|
120
|
+
draft.set(rowId, true);
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`useInput` writes through the provided Runtime/scope. The input definition must belong to that owner.
|
|
125
|
+
|
|
126
|
+
### Document selectors
|
|
127
|
+
|
|
128
|
+
Document state does not require a `ProjectionProvider`:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
const title = useDocumentSelector(document, read => read.title);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This is the React adapter for Core document `select`; dependency tracking remains owned by Core.
|
|
135
|
+
|
|
136
|
+
### Generic Readable and history
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
const value = useReadable(readable);
|
|
140
|
+
const history = useHistory(document.history);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`useHistory` returns current `undoDepth` / `redoDepth` plus `undo()` and `redo()` callbacks.
|
|
144
|
+
|
|
145
|
+
## Local sync
|
|
146
|
+
|
|
147
|
+
`doxum/local-sync` owns browser-local durability and leader/follower coordination for one `DocumentRuntime`:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const sync = await attachLocalSync({
|
|
151
|
+
runtime: document,
|
|
152
|
+
database: 'my-app',
|
|
153
|
+
documentId: 'doc:123',
|
|
154
|
+
schemaVersion: 1,
|
|
155
|
+
changeLimits,
|
|
156
|
+
onError: reportSyncError,
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This boundary is intentionally local-browser only. It does not define network transport, authorization, server conflict resolution, distributed clocks, or collaborative undo.
|
|
161
|
+
|
|
162
|
+
### Leader/follower behavior
|
|
163
|
+
|
|
164
|
+
`sync.state` is a `Readable<LocalSyncState>`. Normal states are `{ status: 'leader' | 'follower', headSeq, checkpointSeq }`. Only the leader may author document writes while attached; followers replay durable contiguous sequence.
|
|
165
|
+
|
|
166
|
+
Error state carries `LocalSyncError`; disposal publishes `{ status: 'disposed' }`.
|
|
167
|
+
|
|
168
|
+
### Visibility and durability
|
|
169
|
+
|
|
170
|
+
Local writes become visible in the document runtime before asynchronous persistence completes. `await sync.flush()` waits until currently accepted local work is durable according to the local-sync contract. It is not a network acknowledgement.
|
|
171
|
+
|
|
172
|
+
### Unsupported attached operations
|
|
173
|
+
|
|
174
|
+
While local-sync owns the attached runtime boundary, whole-document external replacement and external remote-marked apply are unsupported. Keep collaboration/network ingestion at a separate owner.
|
|
175
|
+
|
|
176
|
+
### Storage compatibility
|
|
177
|
+
|
|
178
|
+
`schemaVersion` identifies the application storage schema version used for the attached document. A schema mismatch is an operational error; local-sync does not silently migrate unknown old storage. Perform migration explicitly before attaching the new runtime format.
|
|
179
|
+
|
|
180
|
+
### Admission limits
|
|
181
|
+
|
|
182
|
+
`JsonChangeLimits` controls new local commit admission:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
{
|
|
186
|
+
maxChanges?: number;
|
|
187
|
+
maxBytes?: number;
|
|
188
|
+
maxDepth?: number;
|
|
189
|
+
maxStringLength?: number;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`defaultJsonChangeLimits` provides resolved defaults. These limits govern newly authored local commits. Previously admitted durable records remain replayable under smaller current limits while their JSON/ChangeSet structure is still validated.
|
|
194
|
+
|
|
195
|
+
### Local-sync errors
|
|
196
|
+
|
|
197
|
+
`LocalSyncErrorCode` values are:
|
|
198
|
+
|
|
199
|
+
- `unavailable`
|
|
200
|
+
- `schema-mismatch`
|
|
201
|
+
- `consistency`
|
|
202
|
+
- `read-only`
|
|
203
|
+
- `unsupported-operation`
|
|
204
|
+
- `disposed`
|
|
205
|
+
- `invalid-data`
|
|
206
|
+
|
|
207
|
+
`onError` receives the same `LocalSyncError` type used by the error state. A consumer exception thrown while observing `sync.state` is a consumer failure; it does not become local-sync fault state.
|
|
208
|
+
|
|
209
|
+
### Disposal
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
await sync.dispose();
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Dispose local-sync before abandoning the attached application/service lifecycle. Dispose document/projection owners according to the application ownership order after the sync boundary no longer needs them.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Runtime Invariants
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
implementation invariants that affect design and review decisions.
|
|
3
|
+
This file is maintainer-only. Application/library consumers should use [Public API](public-api.md), [Document runtime](document-runtime.md), [Projections](projections.md), and [Integrations](integrations.md) without reading implementation source. Keep this file limited to implementation invariants that affect Doxum design and review decisions.
|
|
5
4
|
|
|
6
5
|
1. createDocument owns canonical state; Draft, apply and replace share MutationSession.
|
|
7
6
|
runtime owns one execution/rollback/seal/publish boundary, also used by history.
|
|
@@ -66,3 +65,7 @@ implementation invariants that affect design and review decisions.
|
|
|
66
65
|
|
|
67
66
|
Network intent, authorization, collaborative undo and persist-before-visible acceptance
|
|
68
67
|
are separate requirements, not hidden runtime capabilities.
|
|
68
|
+
|
|
69
|
+
## Skill contract
|
|
70
|
+
|
|
71
|
+
The published `skills/doxum-runtime` directory is part of the consumer contract. When a public export, call shape, lifecycle rule, failure mode, or recommended primitive changes, update the canonical skill reference in the same change. Consumer references must not require internal source paths to explain ordinary usage. The repository skill check owns export coverage and source-free consumer-reference validation.
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# Doxum Projections
|
|
2
|
+
|
|
3
|
+
Use this reference for projection ownership, source boundaries, pure derive, keyed derivation, dynamic keyed dependencies, Runtime/scope lifecycle, selector tracking, batching, advanced processors, and recovery. Exact signatures are in [Public API](public-api.md).
|
|
4
|
+
|
|
5
|
+
## What a Projection is
|
|
6
|
+
|
|
7
|
+
`Projection<T>` and `KeyedProjection<K,V>` are lazy definitions. They do not themselves own current materialized values, subscriptions, retained state, source attachments, or disposal state.
|
|
8
|
+
|
|
9
|
+
One `ProjectionRuntime` materializes definitions and owns:
|
|
10
|
+
|
|
11
|
+
- producer/output state;
|
|
12
|
+
- dependency graph settlement;
|
|
13
|
+
- source subscriptions;
|
|
14
|
+
- projection revisions/listeners;
|
|
15
|
+
- retained advanced state;
|
|
16
|
+
- dynamic keyed dependency bindings;
|
|
17
|
+
- `runtime.items(...)` caches;
|
|
18
|
+
- error recovery and disposal.
|
|
19
|
+
|
|
20
|
+
Root projection definitions can be reused by multiple runtimes. Runtime-local state lives in each materialization, not on the definition object.
|
|
21
|
+
|
|
22
|
+
## Choosing the projection shape
|
|
23
|
+
|
|
24
|
+
Use the simplest public primitive that owns the required semantics:
|
|
25
|
+
|
|
26
|
+
| Requirement | Primitive |
|
|
27
|
+
| --------------------------------------------------- | ---------------------------------------------- |
|
|
28
|
+
| Pure scalar/aggregate result from named projections | `derive({...}, compute)` |
|
|
29
|
+
| One keyed source owns output keys/order | `derive.keyed(source, selector)` |
|
|
30
|
+
| Same keyed driver plus global/per-key joins | `derive.keyed(source, dependencies, selector)` |
|
|
31
|
+
| Ordered keys as scalar array | `derive.keyed.keys(source)` |
|
|
32
|
+
| Ordered values as scalar array | `derive.keyed.values(source)` |
|
|
33
|
+
| Ordered `[key,value]` tuples as scalar array | `derive.keyed.entries(source)` |
|
|
34
|
+
| Current scalar key selects one keyed entry | `derive.keyed.get(source, keyProjection)` |
|
|
35
|
+
| Select requested ordered subset | `derive.keyed.subset(source, orderedKeys)` |
|
|
36
|
+
| Preserve source values but filter membership | `derive.keyed.filter(source, predicate)` |
|
|
37
|
+
| Map entry and drop `undefined` results | `derive.keyed.compact(source, selector)` |
|
|
38
|
+
| Reverse index/group membership | `derive.keyed.groupBy(source, selector)` |
|
|
39
|
+
| Optional scalar becomes 0/1 keyed collection | `derive.keyed.singleton(source, keyOf)` |
|
|
40
|
+
| Runtime-local scalar state | `input(initial)` |
|
|
41
|
+
| Runtime-local keyed state | `input.collection(initial?)` |
|
|
42
|
+
| Stable per-membership item readables | `runtime.items(keyedProjection)` |
|
|
43
|
+
| Retained state per driver key | `incremental.keyed` |
|
|
44
|
+
| Custom collection patching/shared retained state | `incremental.collection` / `incremental.group` |
|
|
45
|
+
|
|
46
|
+
Do not implement a second join/cache/invalidation layer in application code when one of these primitives already owns it.
|
|
47
|
+
|
|
48
|
+
## Source declarations
|
|
49
|
+
|
|
50
|
+
`observe` is the single projection source declaration family.
|
|
51
|
+
|
|
52
|
+
### Document sources
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const whole = observe(document);
|
|
56
|
+
const rows = observe(document, path => path.rows);
|
|
57
|
+
const title = observe(document, path => path.title);
|
|
58
|
+
const row = observe(document, path => path.rows.item(rowId));
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Document map/table/list selections become keyed projections. Atomic array-valued fields remain scalar projections.
|
|
62
|
+
|
|
63
|
+
Tree paths reuse the same model:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const rootId = observe(document, path => path.outline.rootId);
|
|
67
|
+
const nodes = observe(document, path => path.outline.nodes);
|
|
68
|
+
const node = observe(document, path => path.outline.nodes.item(nodeId));
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`rootId` is scalar, `nodes` is keyed, and `item(id)` is scalar `node | undefined`.
|
|
72
|
+
|
|
73
|
+
### Readable and external sources
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const fromReadable = observe(readable);
|
|
77
|
+
const fromExternalValue = observe(externalValueSource);
|
|
78
|
+
const fromExternalCollection = observe(externalCollectionSource);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
External source contracts are documented in [Integrations](integrations.md). Source events are normalized before processors run.
|
|
82
|
+
|
|
83
|
+
## Pure scalar derive
|
|
84
|
+
|
|
85
|
+
Use named dependency objects:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const summary = derive(
|
|
89
|
+
{ rows, filter },
|
|
90
|
+
({ rows, filter }) => summarize(rows, filter),
|
|
91
|
+
optionalEquality
|
|
92
|
+
);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Dependencies are explicit and static. The compute callback may not return a Promise. Equality defaults to `Object.is` and compares the generated scalar result.
|
|
96
|
+
|
|
97
|
+
Do not call `runtime.read(...)` from a derive callback to discover dependencies. That would create an imperative dependency protocol outside the producer graph.
|
|
98
|
+
|
|
99
|
+
## `derive.keyed`: driver-owned membership/order
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const labels = derive.keyed(rows, (row, rowId) => format(rowId, row));
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The driver keyed projection owns output membership and order. Only invalidated driver keys execute the selector. Per-entry equality suppresses `updated` publication when the selected value is equal, while driver add/remove/order semantics remain intact.
|
|
106
|
+
|
|
107
|
+
The selector receives `(value, key)` and returns the output value for the same key.
|
|
108
|
+
|
|
109
|
+
### Global and dynamic keyed dependencies
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const cards = derive.keyed(
|
|
113
|
+
items,
|
|
114
|
+
{
|
|
115
|
+
density,
|
|
116
|
+
record: { source: records, key: item => item.recordId },
|
|
117
|
+
related: { source: records, keys: item => item.relatedRecordIds },
|
|
118
|
+
},
|
|
119
|
+
(item, itemId, { density, record, related }) => renderCard(itemId, item, density, record, related)
|
|
120
|
+
);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Dependency forms:
|
|
124
|
+
|
|
125
|
+
- ordinary `Projection<T>`: global dependency; when it changes, relevant driver work is invalidated as a whole;
|
|
126
|
+
- `{ source, key }`: singular driver-key → source-key binding; resolved value is `V | undefined`;
|
|
127
|
+
- `{ source, keys }`: ordered duplicate-free driver-key → source-keys binding; resolved value is `ReadonlyMap<K,V>` in requested-key order, containing currently present entries only.
|
|
128
|
+
|
|
129
|
+
The Runtime owns forward binding and reverse invalidation. A missing selected source entry still keeps the binding, so a later source add invalidates the dependent driver key correctly.
|
|
130
|
+
|
|
131
|
+
Source-only order change does not invalidate singular/plural keyed lookup because dependency identity is by selected key, not by source-global order.
|
|
132
|
+
|
|
133
|
+
`keys(...)` selectors must return duplicate-free key arrays. Treat duplicate keys as invalid input rather than as a hidden deduplication feature.
|
|
134
|
+
|
|
135
|
+
## Structural keyed reads
|
|
136
|
+
|
|
137
|
+
### Keys
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const ids = derive.keyed.keys(rows);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The scalar result follows formal keyed membership order. Value-only updates do not change the published array reference or projection revision. Add/remove/order/reset can publish a new array.
|
|
144
|
+
|
|
145
|
+
### Values
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const values = derive.keyed.values(rows);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Values follow formal keyed order. Value-only updates publish when the corresponding entry value changes.
|
|
152
|
+
|
|
153
|
+
### Entries
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const entries = derive.keyed.entries(rows);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Produces ordered readonly `[key,value]` tuples. Value-only updates publish correctly. Unchanged tuple objects are reused where possible, so consumers do not need a manual `derive.keyed(... => [key,value])` plus values stage.
|
|
160
|
+
|
|
161
|
+
### Precise scalar lookup
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const active = derive.keyed.get(rows, activeId);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`activeId` is a scalar `Projection<K | undefined>`. The derive binds exactly the selected key. If the key is absent, the result is `undefined` while the binding remains live for later add.
|
|
168
|
+
|
|
169
|
+
Use this for active entity, editor target, selected record, or current-cell lookups.
|
|
170
|
+
|
|
171
|
+
## Membership-changing keyed primitives
|
|
172
|
+
|
|
173
|
+
### `subset`
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const visibleRows = derive.keyed.subset(rows, orderedVisibleIds);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Output order is `orderedVisibleIds ∩ source membership`. Missing requested ids remain latent and appear later if the source adds them. Duplicate requested keys are invalid. Values are source values and are not re-derived.
|
|
180
|
+
|
|
181
|
+
Use `subset` when another projection or static array already owns the requested order.
|
|
182
|
+
|
|
183
|
+
### `filter`
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const visible = derive.keyed.filter(rows, row => row.visible);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Preserves source values and source-relative order. Because values are reused directly, there is no output-value equality parameter. Dynamic dependencies can be supplied with the same named keyed-dependency protocol as ordinary `derive.keyed`.
|
|
190
|
+
|
|
191
|
+
### `compact`
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const content = derive.keyed.compact(cards, card => card.content || undefined);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`undefined` means output absence. Present selected values can use optional per-entry equality. Use when membership depends on a mapped optional result rather than a boolean predicate.
|
|
198
|
+
|
|
199
|
+
### `groupBy`
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const recordsBySection = derive.keyed.groupBy(records, record => record.sectionIds);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Selector returns one group key or a readonly group-key array. Output is `KeyedProjection<GroupKey, readonly SourceKey[]>`. Each group's members follow source formal order. The Runtime incrementally maintains reverse membership for added/updated/removed/order/reset.
|
|
206
|
+
|
|
207
|
+
Use this for domain-neutral reverse indexing such as record→sections, node→edges, group→item ids. Keep domain names outside Doxum itself.
|
|
208
|
+
|
|
209
|
+
### `singleton`
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const activeEntity = derive.keyed.singleton(activeRecord, record => record.id);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Input is `Projection<V | undefined>`. `undefined` becomes empty membership; a present value becomes exactly one keyed entry.
|
|
216
|
+
|
|
217
|
+
## Runtime read/select
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const current = runtime.read(projection);
|
|
221
|
+
const readable = runtime.select(projection);
|
|
222
|
+
const selected = runtime.select(projection, value => selectPart(value), equality);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`read` returns the current published projection value and materializes the producer if needed.
|
|
226
|
+
|
|
227
|
+
`select(projection)` returns a `Readable` for the whole value. Selector form tracks the relevant keyed reads/structure when the source is keyed, so unrelated keyed updates need not rerun the selector. If an invalidation is relevant, the selector reruns and result equality decides publication.
|
|
228
|
+
|
|
229
|
+
## Runtime-owned keyed item family
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
const family = runtime.items(rows);
|
|
233
|
+
const orderedKeys = family.keys;
|
|
234
|
+
const rowReadable = family.get(rowId);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Semantics:
|
|
238
|
+
|
|
239
|
+
- `keys` is a `Readable<readonly K[]>` for formal ordered membership;
|
|
240
|
+
- `get(key)` returns `Readable<V | undefined>`;
|
|
241
|
+
- while a key continuously exists, repeated `get(key)` returns stable readable identity;
|
|
242
|
+
- value-only updates notify the current item's readable without creating a new membership lifecycle;
|
|
243
|
+
- removal ends the current membership lifecycle and releases its cached item readable when no longer retained by the family;
|
|
244
|
+
- re-adding the same key creates a new membership lifecycle and may return a new readable identity;
|
|
245
|
+
- a requested missing key can remain latent and activate when the source later adds it.
|
|
246
|
+
|
|
247
|
+
Use this instead of adapter/business code subscribing to keys and maintaining its own `Map<K,Readable<V>>`.
|
|
248
|
+
|
|
249
|
+
## Projection inputs
|
|
250
|
+
|
|
251
|
+
Scalar local state:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const filter = input<'all' | 'open'>('all');
|
|
255
|
+
runtime.update(filter, 'open');
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Keyed local state:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
const selection = input.collection<RowId, boolean>();
|
|
262
|
+
runtime.update(selection, draft => {
|
|
263
|
+
draft.set(rowId, true);
|
|
264
|
+
draft.remove(previousId);
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Inputs belong to one materializing Runtime/scope. They are not canonical document state and do not participate in document history, ChangeSets, local-sync durability, or document impact.
|
|
269
|
+
|
|
270
|
+
`input.collection` edit callbacks are synchronous borrowed sessions. If the callback throws, nothing is installed. Entry equality is also evaluated before the next local state is formally installed; if equality throws, published state and the next edit baseline remain unchanged.
|
|
271
|
+
|
|
272
|
+
An equality-equivalent `set` retains the previously published value reference and publishes no entry update.
|
|
273
|
+
|
|
274
|
+
## ProjectionScope
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
const scope = runtime.scope();
|
|
278
|
+
const localFilter = scope.own(input('all'));
|
|
279
|
+
const visible = scope.own(derive({ rows, localFilter }, ...));
|
|
280
|
+
scope.read(visible);
|
|
281
|
+
scope.dispose();
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A scope adds lifecycle ownership only. It shares the parent's scheduler/materialization system.
|
|
285
|
+
|
|
286
|
+
Rules:
|
|
287
|
+
|
|
288
|
+
- define projections through the ordinary root declaration APIs;
|
|
289
|
+
- call `scope.own(projectionOrStaticTree)` before that definition is first materialized as a root;
|
|
290
|
+
- a scoped definition may depend on root definitions;
|
|
291
|
+
- root definitions and sibling scopes may not depend on a scoped definition;
|
|
292
|
+
- the same definition cannot belong to two scopes;
|
|
293
|
+
- `scope.items` has the same membership/readable semantics as `runtime.items` and ends with scope disposal.
|
|
294
|
+
|
|
295
|
+
## Batching
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
runtime.batch(() => {
|
|
299
|
+
runtime.update(filter, 'open');
|
|
300
|
+
document.update(...);
|
|
301
|
+
}, { cause });
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Projection batching defers projection graph settlement/listeners until the batch completes. It does not delay or roll back canonical document commits or document listeners.
|
|
305
|
+
|
|
306
|
+
Reads of projections inside the batch see the last published projection state, not an eagerly settled draft. Batch the whole application action before its first source update when one settled projection publication is required.
|
|
307
|
+
|
|
308
|
+
There is no cross-document transaction or rollback implied by `runtime.batch`.
|
|
309
|
+
|
|
310
|
+
## Collection change protocol
|
|
311
|
+
|
|
312
|
+
Advanced keyed processors receive one transport shape:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
type CollectionChange<K, V> =
|
|
316
|
+
| { kind: 'reset' }
|
|
317
|
+
| {
|
|
318
|
+
kind: 'incremental';
|
|
319
|
+
added: readonly { key: K; after: V }[];
|
|
320
|
+
updated: readonly { key: K; before: V; after: V }[];
|
|
321
|
+
removed: readonly { key: K; before: V }[];
|
|
322
|
+
order?: { before: readonly K[]; after: readonly K[] };
|
|
323
|
+
};
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Interpret facts separately:
|
|
327
|
+
|
|
328
|
+
- `reset`: rebuild semantic baseline from current dependency value;
|
|
329
|
+
- `added` / `removed`: membership lifecycle changes;
|
|
330
|
+
- `updated`: same key/membership, new value;
|
|
331
|
+
- `order`: common-member relative order changed.
|
|
332
|
+
|
|
333
|
+
Do not treat order as “all entries updated”. Do not infer reset from a large incremental change.
|
|
334
|
+
|
|
335
|
+
`collectionChange.keys(change)` from `doxum/advanced` iterates added→updated→removed entry-transition keys only. It deliberately does not interpret reset or order.
|
|
336
|
+
|
|
337
|
+
## When to use `doxum/advanced`
|
|
338
|
+
|
|
339
|
+
Use advanced only when a public pure primitive cannot express the requirement without manual retained state or direct incremental patching.
|
|
340
|
+
|
|
341
|
+
Good reasons:
|
|
342
|
+
|
|
343
|
+
- retained state across evaluations;
|
|
344
|
+
- independent retained state per driver key;
|
|
345
|
+
- a specialized incremental algorithm that updates only affected output keys;
|
|
346
|
+
- several output leaves that share retained state/work;
|
|
347
|
+
- cross-key coordination that is not a pure keyed selector/reverse index.
|
|
348
|
+
|
|
349
|
+
Bad reasons:
|
|
350
|
+
|
|
351
|
+
- mapping one keyed source;
|
|
352
|
+
- filtering/compacting/subsetting;
|
|
353
|
+
- active keyed lookup;
|
|
354
|
+
- reverse group index;
|
|
355
|
+
- stable item readables;
|
|
356
|
+
- scalar aggregation without retained state.
|
|
357
|
+
|
|
358
|
+
Those already have first-class public primitives.
|
|
359
|
+
|
|
360
|
+
## `incremental.keyed`
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
const totals = incremental.keyed(
|
|
364
|
+
sections,
|
|
365
|
+
{
|
|
366
|
+
records: { source: records, keys: section => section.recordIds },
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
state: (_section, _sectionId) => ({ runs: 0 }),
|
|
370
|
+
process: ({ dependencies, state }) => {
|
|
371
|
+
state.runs++;
|
|
372
|
+
return [...dependencies.records.values()].reduce((sum, record) => sum + record.score, 0);
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
);
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The driver owns output membership/order. Each current driver key owns independent optional state.
|
|
379
|
+
|
|
380
|
+
Lifecycle:
|
|
381
|
+
|
|
382
|
+
- driver add creates a new state lifecycle and runs that key;
|
|
383
|
+
- driver value update reruns that key;
|
|
384
|
+
- driver removal releases the key state and dependency bindings;
|
|
385
|
+
- driver order-only change updates output order without running `process`;
|
|
386
|
+
- dynamic source update reruns only driver keys whose current binding includes the changed source key;
|
|
387
|
+
- source order-only change does not invalidate keyed bindings;
|
|
388
|
+
- driver reset preserves retained state for keys that remain members;
|
|
389
|
+
- processor fault recovery recreates all per-key retained state before reset evaluation.
|
|
390
|
+
|
|
391
|
+
`process` returns only its own output key value and does not expose collection draft methods.
|
|
392
|
+
|
|
393
|
+
## `incremental.collection`
|
|
394
|
+
|
|
395
|
+
Use when one processor owns a keyed output and must apply direct incremental patches:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
const weights = incremental.collection(
|
|
399
|
+
{ tasks },
|
|
400
|
+
{
|
|
401
|
+
state: () => ({ ready: false }),
|
|
402
|
+
process: ({ values, changes, output, reset, state }) => {
|
|
403
|
+
if (reset) {
|
|
404
|
+
for (const [id, task] of values.tasks) output.set(id, task.weight);
|
|
405
|
+
output.order([...values.tasks.keys()]);
|
|
406
|
+
state.ready = true;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const change = changes.tasks;
|
|
411
|
+
if (!change || change.kind === 'reset') return;
|
|
412
|
+
for (const entry of change.added) output.set(entry.key, entry.after.weight);
|
|
413
|
+
for (const entry of change.updated) output.set(entry.key, entry.after.weight);
|
|
414
|
+
for (const entry of change.removed) output.remove(entry.key);
|
|
415
|
+
if (change.order) output.order([...values.tasks.keys()]);
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
);
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Context includes `values`, `changes`, `previous`, `next`, borrowed `output`, `reset`, `cause`, and optional `state`.
|
|
422
|
+
|
|
423
|
+
Declare `state()` only when retained state is actually needed.
|
|
424
|
+
|
|
425
|
+
## `incremental.group`
|
|
426
|
+
|
|
427
|
+
Use one group when several projection leaves share retained state or incremental work:
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
const result = incremental.group(
|
|
431
|
+
{ scene },
|
|
432
|
+
{
|
|
433
|
+
output: define => ({
|
|
434
|
+
cards: define.collection<CardId, Card>(),
|
|
435
|
+
count: define.value<number>(),
|
|
436
|
+
}),
|
|
437
|
+
state: () => ({ runs: 0 }),
|
|
438
|
+
process: ({ values, output, state }) => {
|
|
439
|
+
state.runs++;
|
|
440
|
+
const cards = buildCards(values.scene);
|
|
441
|
+
for (const [id, card] of cards) output.cards.set(id, card);
|
|
442
|
+
output.cards.order([...cards.keys()]);
|
|
443
|
+
output.count.set(cards.size);
|
|
444
|
+
},
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
`define.collection` and `define.value` exist only inside the synchronous static output declaration. Every declared descriptor must be returned exactly once from the output tree. The returned tree has ordinary `Projection` / `KeyedProjection` leaves.
|
|
450
|
+
|
|
451
|
+
Initial build/recovery must establish each scalar value output. During ordinary incremental runs, untouched scalar leaves retain their published value.
|
|
452
|
+
|
|
453
|
+
## Processor state and recovery
|
|
454
|
+
|
|
455
|
+
For advanced processors, `state()` creates retained Runtime-owned state. Source reset does not automatically discard declared retained state. A processor fault is different: Runtime recovery recreates declared retained state and performs a reset evaluation.
|
|
456
|
+
|
|
457
|
+
Stateless processors use the same recovery lifecycle without a state object.
|
|
458
|
+
|
|
459
|
+
Do not build application recovery tokens or hidden rebuild channels around processors.
|
|
460
|
+
|
|
461
|
+
## Error and notification ordering
|
|
462
|
+
|
|
463
|
+
Materialized processors settle before external projection listeners. Projection writes are not allowed while the graph is processing/notifying.
|
|
464
|
+
|
|
465
|
+
A projection source/processor/listener failure is reported as `ProjectionError` through Runtime error handling and may block dependents until recovery. It does not roll back an already accepted document commit.
|
|
466
|
+
|
|
467
|
+
Dispose subscriptions and Runtime/scope ownership with the lifecycle that created them.
|