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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doxum",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Doxum is a typed runtime for complex mutable documents.",
|
|
@@ -82,7 +82,8 @@
|
|
|
82
82
|
"build": "tsdown --config tsdown.config.ts && node scripts/check-build-exports.mjs && pnpm run check:declaration-portability",
|
|
83
83
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
84
84
|
"test": "vitest run --config vitest.config.ts",
|
|
85
|
-
"check": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm run check:public-types && pnpm run test && pnpm run check:projection-surface && pnpm run check:runtime-imports",
|
|
85
|
+
"check": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm run check:public-types && pnpm run test && pnpm run check:runtime-skill && pnpm run check:projection-surface && pnpm run check:runtime-imports",
|
|
86
|
+
"check:runtime-skill": "node scripts/check-runtime-skill.mjs",
|
|
86
87
|
"check:projection-surface": "node scripts/check-projection-surface.mjs",
|
|
87
88
|
"check:runtime-imports": "node scripts/check-runtime-imports.mjs",
|
|
88
89
|
"check:public-types": "tsc -p tsconfig.public-api.json --noEmit",
|
|
@@ -1,88 +1,70 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: doxum-runtime
|
|
3
|
-
description:
|
|
3
|
+
description: Use Doxum public APIs to design, implement, and review schemas, document mutations, projections, React bindings, local sync, and advanced processors without reading implementation source. Read maintainer invariants only when changing Doxum itself.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Doxum Runtime
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
same dynamic keyed dependency routing. `collectionChange.keys` is the
|
|
72
|
-
transport-level iterable for incremental added/updated/removed keys; callers handle
|
|
73
|
-
reset and order semantics themselves. React selector tracking remains a consumer
|
|
74
|
-
concern through `ProjectionProvider` and `useProjection(projection, selector)`.
|
|
75
|
-
- Advanced incremental definitions always declare `process`; `state()` is present only
|
|
76
|
-
when retained state is actually needed. `incremental.group` declares its static
|
|
77
|
-
output tree through the synchronous `output` callback. `define.value` and
|
|
78
|
-
`define.collection` are callback methods, not separate imports; every returned leaf
|
|
79
|
-
is a `Projection<T>` or `KeyedProjection<K,V>` from the same producer.
|
|
80
|
-
- Observer errors leave commits accepted. Do not retry as if they rolled back.
|
|
81
|
-
- Core stays framework-neutral; adapters consume standard `Readable`,
|
|
82
|
-
document `select`, and projection readables without internal target/address protocols.
|
|
83
|
-
- Local sync owns browser persistence/leadership. Network conflict policy and
|
|
84
|
-
collaborative undo belong at a separate boundary.
|
|
85
|
-
|
|
86
|
-
Update examples and tests when behavior changes. Never add compatibility APIs,
|
|
87
|
-
a second path grammar, a parallel join/dependency protocol, or another writable
|
|
88
|
-
derived cache.
|
|
8
|
+
Use this skill as the public contract for Doxum. When Doxum is a dependency, work from the public API and these references. Do not inspect `core/src`, `react/src`, tests, source maps, or built JavaScript to discover how application code should use Doxum. If a public behavior is missing here while working in the Doxum repository, treat that as a skill documentation defect and update the skill together with the API change.
|
|
9
|
+
|
|
10
|
+
Answer and write examples in the user's language. The canonical references are intentionally single-source so API semantics cannot drift between translations.
|
|
11
|
+
|
|
12
|
+
## Ownership spine
|
|
13
|
+
|
|
14
|
+
Choose the owner before choosing an API:
|
|
15
|
+
|
|
16
|
+
- **Canonical application data** belongs to one `DocumentRuntime` created by `createDocument`.
|
|
17
|
+
- **Pure derived data** belongs to lazy `Projection` definitions and one materializing `ProjectionRuntime`.
|
|
18
|
+
- **Runtime-local UI/application state** belongs to `input` or `input.collection`, not to the canonical document unless it must participate in document history, persistence, replay, or impact.
|
|
19
|
+
- **Framework state** stays behind adapters. React consumes `Readable`, document selectors, projections, and projection inputs.
|
|
20
|
+
- **Browser durability/leadership** belongs to `doxum/local-sync`; network collaboration and distributed conflict policy are separate boundaries.
|
|
21
|
+
- **Retained incremental state** belongs to `doxum/advanced` only when ordinary `derive` / `derive.keyed` cannot express the work clearly.
|
|
22
|
+
|
|
23
|
+
## Reference routing
|
|
24
|
+
|
|
25
|
+
Read the smallest complete reference for the task before implementing it.
|
|
26
|
+
|
|
27
|
+
| Task | Read |
|
|
28
|
+
| ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- |
|
|
29
|
+
| Exact imports, signatures, callback fields, result types, errors, and all public exports | [Public API](references/public-api.md) |
|
|
30
|
+
| Schema modeling, validators, Draft operations, reads, snapshots, commits, history, apply, impact, trees | [Document runtime](references/document-runtime.md) |
|
|
31
|
+
| `observe`, `derive`, the `derive.keyed` family, dynamic keyed dependencies, Runtime/scope/items, batching, advanced processors | [Projections](references/projections.md) |
|
|
32
|
+
| React, external sources, `Readable`, and local-sync lifecycle | [Integrations](references/integrations.md) |
|
|
33
|
+
| End-to-end usage patterns that can be adapted directly | [Recipes](references/recipes.md) |
|
|
34
|
+
| Editing or reviewing Doxum internals, ownership boundaries, scheduler/mutation architecture, release-facing invariants | [Maintainer invariants](references/invariants.md) |
|
|
35
|
+
|
|
36
|
+
For application work, do not read the maintainer reference unless the task is actually changing Doxum itself.
|
|
37
|
+
|
|
38
|
+
## Default API decisions
|
|
39
|
+
|
|
40
|
+
Follow these choices unless the requirement says otherwise:
|
|
41
|
+
|
|
42
|
+
1. Model stable document structure with `object`, `variant`, `map`, `table`, `list`, and `tree`; use `field` for atomic payloads that are replaced as a whole.
|
|
43
|
+
2. Mutate canonical state only through `document.update`, `document.apply`, `document.replace`, or history operations. Draft/read proxies are synchronous borrowed views and must not escape their callback.
|
|
44
|
+
3. Use `read` for one synchronous document read and `select` for a document `Readable` with dynamic dependency tracking.
|
|
45
|
+
4. Use `observe` to bring a document/readable/external source into the projection graph.
|
|
46
|
+
5. Use named-object `derive` for pure aggregate values.
|
|
47
|
+
6. Use `derive.keyed` when one keyed source owns output membership/order. Prefer its built-in `keys`, `values`, `entries`, `get`, `subset`, `filter`, `compact`, `groupBy`, and `singleton` primitives over hand-written incremental collection patches.
|
|
48
|
+
7. Express per-output-key joins through `{ source, key }` or `{ source, keys }` dependencies. Let the Runtime own binding and reverse invalidation; do not maintain an application `Map` just to route dependency changes.
|
|
49
|
+
8. Use `runtime.items(keyedProjection)` or `scope.items(...)` for stable per-membership item `Readable`s.
|
|
50
|
+
9. Use `incremental.keyed` for independent retained state per driver key. Use `incremental.collection` or `incremental.group` only when direct incremental output patching or shared retained state is required.
|
|
51
|
+
10. Use `ProjectionScope` only for lifecycle ownership. Define projections normally, then `scope.own(...)` before that definition is first materialized as a root.
|
|
52
|
+
|
|
53
|
+
## Failure and lifecycle rules
|
|
54
|
+
|
|
55
|
+
- Doxum callbacks are synchronous unless the public API explicitly returns a Promise.
|
|
56
|
+
- `TransactionRejected` expresses expected application rejection; ordinary thrown exceptions roll back and rethrow unchanged.
|
|
57
|
+
- A committed document write remains committed even if projection or listener notification later fails; observer errors are reported on the result or through projection error handling.
|
|
58
|
+
- `CollectionChange` is an advanced processor transport contract. Reset, membership transitions, value updates, and order are distinct facts.
|
|
59
|
+
- `input.collection` has a strong exception boundary: callback or equality failure installs no partial next state.
|
|
60
|
+
- Dispose the owning `ProjectionRuntime`, `ProjectionScope`, and `LocalSync` with the application/service lifecycle that created them.
|
|
61
|
+
|
|
62
|
+
## Completion contract
|
|
63
|
+
|
|
64
|
+
Before finishing Doxum application work, verify that:
|
|
65
|
+
|
|
66
|
+
- every import comes from `doxum`, `doxum/advanced`, `doxum/react`, or `doxum/local-sync`;
|
|
67
|
+
- no application behavior depends on an internal path, private brand, generated chunk, or source implementation detail;
|
|
68
|
+
- canonical, projection, input, adapter, and persistence responsibilities have one owner each;
|
|
69
|
+
- advanced APIs are used only for requirements that the ordinary public projection family cannot express;
|
|
70
|
+
- examples follow the documented public failure and lifecycle semantics.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: 'Doxum Runtime'
|
|
3
|
-
short_description: '
|
|
4
|
-
default_prompt: 'Use $doxum-runtime to implement this
|
|
3
|
+
short_description: 'Use Doxum public APIs without reading implementation source.'
|
|
4
|
+
default_prompt: 'Use $doxum-runtime to implement this with the documented public Doxum APIs and lifecycle semantics.'
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# Doxum Document Runtime
|
|
2
|
+
|
|
3
|
+
Use this reference for schema design, canonical document mutation, reads, subscriptions, history, replay, impact, validation, and tree behavior. Exact signatures and exported types are in [Public API](public-api.md).
|
|
4
|
+
|
|
5
|
+
## Mental model
|
|
6
|
+
|
|
7
|
+
A Doxum document has one canonical owner: `DocumentRuntime`. The schema defines structure and identity; the runtime owns canonical data, revision, mutation, ChangeSets, history, and commit publication.
|
|
8
|
+
|
|
9
|
+
Use these representation choices consistently:
|
|
10
|
+
|
|
11
|
+
| Requirement | Model |
|
|
12
|
+
| ------------------------------------------------------- | ----------------------------- |
|
|
13
|
+
| Fixed named structure | `object({...})` |
|
|
14
|
+
| Tagged union of object branches | `variant(tag, variants)` |
|
|
15
|
+
| Arbitrary payload replaced as one value | `field<T>()` |
|
|
16
|
+
| Dynamic keyed values without explicit order | `map(valueSchema)` |
|
|
17
|
+
| Ordered keyed object/variant entities | `table(entitySchema)` |
|
|
18
|
+
| Ordered atomic items with stable identity from the item | `list(field<T>(), { keyOf })` |
|
|
19
|
+
| Parent/child topology | `tree(field<T>())` |
|
|
20
|
+
| Optional member | `optional(schema)` |
|
|
21
|
+
|
|
22
|
+
Do not model dynamic keys as undeclared object properties. Object/variant input is closed to declared own members; dynamic domains belong in maps or fields.
|
|
23
|
+
|
|
24
|
+
## Atomic payloads and structure
|
|
25
|
+
|
|
26
|
+
`field<T>` is atomic. If `T` is an object, array, `Map`, `Set`, or `Date`, Doxum still treats the whole payload as one value. Access is deeply readonly through `ReadonlyValue<T>` and mutation replaces the payload whole.
|
|
27
|
+
|
|
28
|
+
Use structure when Doxum must understand and address nested edits. For example:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const point = object({
|
|
32
|
+
x: field<number>(),
|
|
33
|
+
y: field<number>(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const stroke = field<readonly { x: number; y: number }[]>();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`point.x` can change independently and participates in path impact. `stroke` changes as one atomic payload.
|
|
40
|
+
|
|
41
|
+
Published atomic payload references are shared across snapshots, commits, and history. Treat them as immutable through every alias. Doxum does not deep-freeze application payloads and does not clone them merely to defend against mutation.
|
|
42
|
+
|
|
43
|
+
## Schema handles and inferred types
|
|
44
|
+
|
|
45
|
+
Use `Schema<T>` / `ObjectSchema<T>` as the portable schema boundary and `Infer<typeof schema>` for the canonical value type.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const task = object({
|
|
49
|
+
title: field<string>(),
|
|
50
|
+
done: field<boolean>(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const model = object({
|
|
54
|
+
tasks: map(task),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
type DocumentValue = Infer<typeof model>;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Export inferred schema constants directly. Do not expose internal node types or wrap public schemas in `ReturnType<typeof object>` to make declarations portable.
|
|
61
|
+
|
|
62
|
+
## Optionality
|
|
63
|
+
|
|
64
|
+
Optionality is structural presence, not an ordinary value convention.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const model = object({
|
|
68
|
+
nickname: optional(field<string>()),
|
|
69
|
+
settings: optional(map(field<string>())),
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Absent and present-with-`undefined` are distinct where the schema permits an atomic `undefined` payload. Use the schema's optional shape rather than inventing sentinel members.
|
|
74
|
+
|
|
75
|
+
Tree optionality has two separate meanings:
|
|
76
|
+
|
|
77
|
+
- `tree(optional(field<T>()))`: each existing node may omit `value`.
|
|
78
|
+
- `optional(tree(field<T>()))`: the whole tree member may be absent.
|
|
79
|
+
|
|
80
|
+
## Validators and parse
|
|
81
|
+
|
|
82
|
+
Validators are synchronous and pure. A function validator may be a predicate or assertion:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const taskId = (value: unknown): value is TaskId =>
|
|
86
|
+
typeof value === 'string' && value.startsWith('task:');
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Returning `true` or `undefined` means success; returning `false` rejects. An assertion may throw. Standard Schema v1 validators are also accepted, but successful validation must return the same input by identity. Doxum does not accept validator transforms.
|
|
90
|
+
|
|
91
|
+
Transform external values before they enter Doxum. Use `parse(schema, unknownValue)` to validate unknown input and build schema-owned structure while sharing readonly atomic payloads.
|
|
92
|
+
|
|
93
|
+
Map/table key validators define the public key type and apply at parse/mutation boundaries:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const model = object({
|
|
97
|
+
tasks: map(task, { key: taskId }),
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Branded key types then flow through collection methods, typed paths, impact, and keyed projections.
|
|
102
|
+
|
|
103
|
+
## Creating the canonical runtime
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const document = createDocument({
|
|
107
|
+
schema: model,
|
|
108
|
+
initial,
|
|
109
|
+
history: { capacity: 100 },
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`createDocument` is the only canonical state owner. Do not mirror canonical document state in a second writable store and synchronize both manually.
|
|
114
|
+
|
|
115
|
+
`document.readonly()` returns a read/subscription-only alias bound to the same runtime identity.
|
|
116
|
+
|
|
117
|
+
## Update lifecycle
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const result = document.update(draft => {
|
|
121
|
+
const task = draft.tasks.get(taskId);
|
|
122
|
+
if (!task) throw new TransactionRejected({ code: 'missing-task', message: 'Task missing' });
|
|
123
|
+
task.done = true;
|
|
124
|
+
return taskId;
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A transaction is synchronous and atomic:
|
|
129
|
+
|
|
130
|
+
- reads inside the callback see preceding writes in the same transaction;
|
|
131
|
+
- all accepted writes publish together as one commit;
|
|
132
|
+
- an ordinary thrown value rolls back all work and is rethrown unchanged;
|
|
133
|
+
- `TransactionRejected` rolls back and returns `status: 'rejected'` with application diagnostics;
|
|
134
|
+
- returning `false`, `undefined`, or any other ordinary value is just a callback result, not a rejection signal;
|
|
135
|
+
- a net-zero transaction returns `status: 'unchanged'` and publishes no commit.
|
|
136
|
+
|
|
137
|
+
The callback may not return a Promise.
|
|
138
|
+
|
|
139
|
+
### Source and history options
|
|
140
|
+
|
|
141
|
+
`update` accepts `{ source?: 'local' | 'system', history?: boolean }`.
|
|
142
|
+
|
|
143
|
+
- `local` is the default application-authored commit source.
|
|
144
|
+
- `system` is for application/system work that is still local canonical mutation.
|
|
145
|
+
- `history: false` prevents the commit from entering local undo history.
|
|
146
|
+
|
|
147
|
+
## Borrowed Draft/Read lifetime
|
|
148
|
+
|
|
149
|
+
Draft/read access objects are synchronous borrowed capabilities. Do not retain any Draft/Read object, collection accessor, or method past its callback.
|
|
150
|
+
|
|
151
|
+
Wrong:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
let tasks;
|
|
155
|
+
document.update(draft => {
|
|
156
|
+
tasks = draft.tasks;
|
|
157
|
+
});
|
|
158
|
+
tasks.put('later', value);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Use a durable value or a fresh callback instead. When a read must outlive the callback, use `snapshot`:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const tasks = read(document, state => snapshot(state.tasks));
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`snapshot` copies schema structure and shares atomic payload references under the readonly ownership contract.
|
|
168
|
+
|
|
169
|
+
## Collection mutation rules
|
|
170
|
+
|
|
171
|
+
### Map
|
|
172
|
+
|
|
173
|
+
A map is unordered dynamic membership.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
draft.tasks.put(id, value); // upsert
|
|
177
|
+
draft.tasks.remove(id); // missing is a no-op
|
|
178
|
+
draft.tasks.replace(nextRecord);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Table
|
|
182
|
+
|
|
183
|
+
A table is ordered membership plus object/variant entities.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
draft.rows.create(id, value, { at: 'start' });
|
|
187
|
+
draft.rows.create([{ id, value }, ...], { after: existingId });
|
|
188
|
+
draft.rows.remove(idOrIds);
|
|
189
|
+
draft.rows.move(idOrIds, { before: anchorId });
|
|
190
|
+
draft.rows.reorder(exactCurrentMembership);
|
|
191
|
+
draft.rows.replace(id, value);
|
|
192
|
+
draft.rows.replace({ ids, byId });
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`replace(id,value)` preserves that member's order position. `reorder` requires an exact permutation of current membership.
|
|
196
|
+
|
|
197
|
+
### List
|
|
198
|
+
|
|
199
|
+
A list stores atomic items and derives stable identity from `keyOf`.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
draft.rows.insert(value, { after: id });
|
|
203
|
+
draft.rows.remove(id);
|
|
204
|
+
draft.rows.move(idOrIds, { at: 'end' });
|
|
205
|
+
draft.rows.reorder(exactIds);
|
|
206
|
+
draft.rows.replace(id, replacementWithSameKey);
|
|
207
|
+
draft.rows.replace(nextArray);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Member replacement must retain the addressed stable key.
|
|
211
|
+
|
|
212
|
+
### Move semantics
|
|
213
|
+
|
|
214
|
+
For table/list `move(selection, anchor?)`, Doxum first removes the selection, preserves the selected members' current relative order, then resolves the anchor against the remaining sequence. Omitting the anchor means end.
|
|
215
|
+
|
|
216
|
+
### Tree
|
|
217
|
+
|
|
218
|
+
Trees are empty or one connected single-root tree. Nodes have reciprocal parent/children topology and no cycles.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
draft.outline.insert(id, value, { parentId, index });
|
|
222
|
+
draft.outline.move(id, { parentId, index });
|
|
223
|
+
draft.outline.remove(id);
|
|
224
|
+
draft.outline.replace(id, newPayload);
|
|
225
|
+
draft.outline.replace(wholeTree);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`index` is the final child position after removal from the old location. A tree member replacement must satisfy full topology invariants at the boundary.
|
|
229
|
+
|
|
230
|
+
Do not edit `rootId`, `parentId`, `children`, or `nodes` as raw records through application code; use tree operations.
|
|
231
|
+
|
|
232
|
+
## Replacing complex object/variant members
|
|
233
|
+
|
|
234
|
+
Draft object/variant members that contain collection nodes expose collection capabilities rather than plain assignable data. To replace one such member from a plain `Infer` value, use top-level `replace`:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
document.update(draft => {
|
|
238
|
+
replace(draft.settings, 'rows', {
|
|
239
|
+
ids: ['a'],
|
|
240
|
+
byId: { a: { title: 'A' } },
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
A collection's own `replace(next)` remains the normal whole-collection replacement when the collection itself is the mutation target.
|
|
246
|
+
|
|
247
|
+
## Reading canonical state
|
|
248
|
+
|
|
249
|
+
Use `document.snapshot()` for a durable whole-document value.
|
|
250
|
+
|
|
251
|
+
Use `read` for one synchronous selector read:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const done = read(document, state => state.tasks.get(taskId)?.done);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Use `select` for a revisioned `Readable`:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
const selected = select(document, state => state.tasks.get(taskId)?.done);
|
|
261
|
+
const stop = selected.subscribe(() => render(selected.current()));
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`select` tracks actual reads, including branching, and rebinds dependencies when the selector's read set changes. Equality defaults to `Object.is`; supply custom equality only for the projected result, not to redefine document semantics.
|
|
265
|
+
|
|
266
|
+
## Typed paths and subscriptions
|
|
267
|
+
|
|
268
|
+
Path callbacks describe schema locations without reading current data:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
const stop = document.subscribe(
|
|
272
|
+
path => path.tasks.item(taskId).done,
|
|
273
|
+
commit => handle(commit)
|
|
274
|
+
);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
You can subscribe to one path or a non-empty array of paths. Paths compile when the subscription/impact query is registered. They remain meaningful even when an optional/member path is currently absent.
|
|
278
|
+
|
|
279
|
+
Tree paths expose:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
path.outline.rootId;
|
|
283
|
+
path.outline.nodes;
|
|
284
|
+
path.outline.nodes.item(nodeId);
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Commits and observer errors
|
|
288
|
+
|
|
289
|
+
An accepted commit contains:
|
|
290
|
+
|
|
291
|
+
- monotonic local `revision`;
|
|
292
|
+
- `source` (`local`, `system`, `history`, `remote`);
|
|
293
|
+
- exact normalized reversible `changes`;
|
|
294
|
+
- exact `impact` queries.
|
|
295
|
+
|
|
296
|
+
Commit listeners and projection processing occur after canonical acceptance. Their failures do not turn an accepted mutation into a rejected one. `TransactionResult` / `OperationResult` report post-commit observer failures in `observerErrors`.
|
|
297
|
+
|
|
298
|
+
## ChangeSet semantics
|
|
299
|
+
|
|
300
|
+
A member group owns one container address:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
{
|
|
304
|
+
kind: 'members',
|
|
305
|
+
at: ['tasks', taskId],
|
|
306
|
+
members: [
|
|
307
|
+
{ key: 'title', kind: 'updated', before: 'A', after: 'B' },
|
|
308
|
+
{ key: 'done', kind: 'updated', before: false, after: true },
|
|
309
|
+
],
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
An ordered container may include `order: { before, after }` in the same group. Order-only groups have an empty `members` array. There is no standalone order change kind.
|
|
314
|
+
|
|
315
|
+
Tree changes use one tree-container address plus changed node transitions and before/after root id. Whole-document reset is explicit.
|
|
316
|
+
|
|
317
|
+
ChangeSets are final net changes, not an operation log. Multiple writes that cancel out do not publish a change.
|
|
318
|
+
|
|
319
|
+
## Applying external ChangeSets
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
const result = document.apply(changes, {
|
|
323
|
+
expectedRevision: document.revision(),
|
|
324
|
+
source: 'remote',
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
`expectedRevision` is mandatory. External `before` data is transport input, not authority: Doxum checks local baseline/state and records actual local before values for reversible local history.
|
|
329
|
+
|
|
330
|
+
A remote commit invalidates local history. Treat local revision as a local concurrency baseline, not as a globally distributed clock.
|
|
331
|
+
|
|
332
|
+
## History
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
const group = document.history.group();
|
|
336
|
+
try {
|
|
337
|
+
document.update(...);
|
|
338
|
+
document.update(...);
|
|
339
|
+
group.end();
|
|
340
|
+
} catch (error) {
|
|
341
|
+
group.cancel();
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`undo()` / `redo()` travel complete ChangeSets atomically. `group().end()` groups completed history entries; `cancel()` restores the group's start through history travel.
|
|
347
|
+
|
|
348
|
+
A local whole-document replace is reversible. Remote commits clear/invalidate local history because the old local history baseline no longer describes the canonical branch.
|
|
349
|
+
|
|
350
|
+
## Impact
|
|
351
|
+
|
|
352
|
+
Use commit impact instead of manually parsing ChangeSets for ordinary subscription/application invalidation questions:
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
if (commit.impact.affects(path => path.tasks.item(id).done)) { ... }
|
|
356
|
+
|
|
357
|
+
const impact = commit.impact.collection(path => path.tasks);
|
|
358
|
+
if (impact.kind === 'incremental') {
|
|
359
|
+
impact.added;
|
|
360
|
+
impact.removed;
|
|
361
|
+
impact.updated;
|
|
362
|
+
impact.orderChanged;
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Use raw `ChangeSet` only when the exact reversible transport is itself the requirement.
|
|
367
|
+
|
|
368
|
+
## Disposal
|
|
369
|
+
|
|
370
|
+
`document.dispose()` ends the runtime. Later access throws `DocumentDisposedError`. Own the document runtime at the application/service boundary that owns its canonical state.
|