doxum 0.1.8 → 0.1.10
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 +233 -324
- package/dist/contract-CeAnEPBA.d.ts +458 -0
- package/dist/contract-DtGVSXSK.d.cts +458 -0
- package/dist/driver-BlR81Dqg.js +200 -0
- package/dist/driver-BlR81Dqg.js.map +1 -0
- package/dist/driver-xOIkwrB8.cjs +241 -0
- package/dist/driver-xOIkwrB8.cjs.map +1 -0
- package/dist/index.cjs +666 -2222
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -20
- package/dist/index.d.ts +10 -18
- package/dist/index.js +634 -2189
- package/dist/index.js.map +1 -1
- package/dist/{integration-CZwCwFBS.cjs → integration-BKgGuodm.cjs} +4 -9
- package/dist/integration-BKgGuodm.cjs.map +1 -0
- package/dist/{integration-B56u1l9V.js → integration-VTZ3ICsO.js} +2 -7
- package/dist/integration-VTZ3ICsO.js.map +1 -0
- package/dist/integration.cjs +11 -11
- package/dist/integration.d.cts +10 -4
- package/dist/integration.d.ts +10 -4
- package/dist/integration.js +4 -3
- package/dist/issue-DVaGQGeP.js +576 -0
- package/dist/issue-DVaGQGeP.js.map +1 -0
- package/dist/issue-DhrNdQNg.cjs +797 -0
- package/dist/issue-DhrNdQNg.cjs.map +1 -0
- package/dist/local-sync.cjs +67 -39
- package/dist/local-sync.cjs.map +1 -1
- package/dist/local-sync.d.cts +10 -10
- package/dist/local-sync.d.ts +10 -10
- package/dist/local-sync.js +66 -38
- package/dist/local-sync.js.map +1 -1
- package/dist/react.cjs +23 -4
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +8 -3
- package/dist/react.d.ts +8 -3
- package/dist/react.js +21 -6
- package/dist/react.js.map +1 -1
- package/dist/store-Bs0C0rNd.d.ts +262 -0
- package/dist/store-CJuwJl_K.d.cts +262 -0
- package/dist/store-DHFXfWSz.js +2554 -0
- package/dist/store-DHFXfWSz.js.map +1 -0
- package/dist/store-DU2u-qFH.cjs +2793 -0
- package/dist/store-DU2u-qFH.cjs.map +1 -0
- package/package.json +1 -1
- package/skills/doxum-runtime/SKILL.md +28 -44
- package/skills/doxum-runtime/references/guide.en.md +99 -366
- package/skills/doxum-runtime/references/guide.zh-CN.md +83 -290
- package/skills/doxum-runtime/references/invariants.en.md +42 -165
- package/skills/doxum-runtime/references/invariants.zh-CN.md +34 -97
- package/skills/doxum-runtime/references/patterns.en.md +73 -225
- package/skills/doxum-runtime/references/patterns.zh-CN.md +73 -179
- package/dist/chunk-pbuEa-1d.js +0 -13
- package/dist/contract-CIcftgR3.d.ts +0 -561
- package/dist/contract-b87yPRDp.d.cts +0 -561
- package/dist/driver-CNxqMVFH.cjs +0 -69
- package/dist/driver-CNxqMVFH.cjs.map +0 -1
- package/dist/driver-CbzfW5MR.js +0 -46
- package/dist/driver-CbzfW5MR.js.map +0 -1
- package/dist/integration-B56u1l9V.js.map +0 -1
- package/dist/integration-CZwCwFBS.cjs.map +0 -1
- package/dist/ownership-CY0nPXGF.cjs +0 -304
- package/dist/ownership-CY0nPXGF.cjs.map +0 -1
- package/dist/ownership-CduRygE7.js +0 -245
- package/dist/ownership-CduRygE7.js.map +0 -1
- package/dist/runtime-0mOFbe_H.cjs +0 -2439
- package/dist/runtime-0mOFbe_H.cjs.map +0 -1
- package/dist/runtime-BzxODO9Q.d.ts +0 -150
- package/dist/runtime-CRVMB9n1.d.cts +0 -150
- package/dist/runtime-DF1q9Gje.js +0 -2129
- package/dist/runtime-DF1q9Gje.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,355 +1,261 @@
|
|
|
1
1
|
# Doxum
|
|
2
2
|
|
|
3
|
-
Doxum is a
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Licensed under the [MIT License](LICENSE).
|
|
8
|
-
|
|
9
|
-
The `doxum` core entry is a local in-memory runtime. The optional
|
|
10
|
-
`doxum/local-sync` browser attachment adds IndexedDB persistence and
|
|
11
|
-
same-origin cross-tab synchronization with one writable tab at a time; network
|
|
12
|
-
collaboration, authorization, and conflict resolution remain separate
|
|
13
|
-
application concerns.
|
|
14
|
-
|
|
15
|
-
## Packages
|
|
16
|
-
|
|
17
|
-
- `doxum` defines schemas, mutations, history, subscriptions, and views.
|
|
18
|
-
- `doxum/local-sync` lets one Web-Lock leader write a runtime synchronously,
|
|
19
|
-
persists its commands asynchronously to IndexedDB, and makes other tabs
|
|
20
|
-
ordered read-only mirrors.
|
|
21
|
-
- `doxum/react` binds Doxum read models to React 18+ with fine-grained external
|
|
22
|
-
store subscriptions.
|
|
23
|
-
|
|
24
|
-
## Install
|
|
3
|
+
Doxum is a TypeScript runtime for mutable documents with synchronous atomic
|
|
4
|
+
updates, reversible final changes, history, precise subscriptions, and incremental
|
|
5
|
+
projections. Licensed under the [MIT License](LICENSE).
|
|
25
6
|
|
|
26
7
|
```sh
|
|
27
8
|
pnpm add doxum
|
|
28
9
|
```
|
|
29
10
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```sh
|
|
33
|
-
pnpm add doxum react
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Use the package manager that owns your application if it is not pnpm.
|
|
37
|
-
|
|
38
|
-
## AI Development Guide
|
|
39
|
-
|
|
40
|
-
Doxum ships a task-oriented guide for AI assistants and application developers.
|
|
41
|
-
It is the recommended reference for using `doxum` and `doxum/react` in place
|
|
42
|
-
of a generated, symbol-by-symbol API reference. The guide is included in the
|
|
43
|
-
published `doxum` package at `skills/doxum-runtime`.
|
|
44
|
-
|
|
45
|
-
- [English guide](skills/doxum-runtime/references/guide.en.md)
|
|
46
|
-
- [中文指南](skills/doxum-runtime/references/guide.zh-CN.md)
|
|
47
|
-
- [English patterns](skills/doxum-runtime/references/patterns.en.md)
|
|
48
|
-
- [中文模式参考](skills/doxum-runtime/references/patterns.zh-CN.md)
|
|
49
|
-
- [English invariants](skills/doxum-runtime/references/invariants.en.md)
|
|
50
|
-
- [中文不变量](skills/doxum-runtime/references/invariants.zh-CN.md)
|
|
51
|
-
- [AI skill instructions](skills/doxum-runtime/SKILL.md)
|
|
52
|
-
|
|
53
|
-
Tools that support `SKILL.md` can install or link the complete
|
|
54
|
-
`doxum-runtime` directory as the `$doxum-runtime` skill. The files are also
|
|
55
|
-
ordinary Markdown: read the guide matching your working language, then use the
|
|
56
|
-
patterns and invariants references for the task at hand.
|
|
57
|
-
|
|
58
|
-
## Quick Start
|
|
59
|
-
|
|
60
|
-
Define the document shape once. Doxum infers the immutable document value and
|
|
61
|
-
the reader and writer APIs from that schema.
|
|
11
|
+
## Define And Update
|
|
62
12
|
|
|
63
13
|
```ts
|
|
64
|
-
import { createDocument, field,
|
|
65
|
-
|
|
66
|
-
const task = object({
|
|
67
|
-
title: field<string>(),
|
|
68
|
-
completed: field<boolean>(),
|
|
69
|
-
});
|
|
14
|
+
import { createDocument, field, map, object, type Infer } from 'doxum';
|
|
70
15
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
type Task = Infer<typeof task>;
|
|
77
|
-
type TaskDocument = Infer<typeof taskSchema>;
|
|
78
|
-
|
|
79
|
-
const runtime = createDocument({
|
|
80
|
-
schema: taskSchema,
|
|
81
|
-
initial: {
|
|
82
|
-
title: 'Launch',
|
|
83
|
-
tasks: {
|
|
84
|
-
ids: ['task-1'],
|
|
85
|
-
byId: {
|
|
86
|
-
'task-1': { title: 'Write the brief', completed: false },
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
});
|
|
16
|
+
const task = object({ title: field<string>(), done: field<boolean>() });
|
|
17
|
+
const model = object({ tasks: map(task), title: field<string>() });
|
|
18
|
+
type TaskDocument = Infer<typeof model>;
|
|
91
19
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
20
|
+
const document = createDocument({
|
|
21
|
+
schema: model,
|
|
22
|
+
initial: { title: 'Launch', tasks: { a: { title: 'Write', done: false } } },
|
|
95
23
|
});
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
An update is synchronous and atomic. `apply` decodes untrusted operation
|
|
99
|
-
payloads before they reach mutation code. A malformed or semantically rejected
|
|
100
|
-
operation returns a typed `MutationIssue`; if an operation is rejected, if the
|
|
101
|
-
transaction calls `tx.reject`, or if user code throws, every preceding change
|
|
102
|
-
in that update is rolled back.
|
|
103
|
-
|
|
104
|
-
Mutation failure codes are a closed public `MutationIssueCode` union. For
|
|
105
|
-
application validation, use `tx.report` or `tx.reject` with a
|
|
106
|
-
diagnostic `{ code, message, address? }`; Doxum adds `source: 'application'`.
|
|
107
|
-
Published diagnostic arrays and addresses are copied and frozen.
|
|
108
|
-
|
|
109
|
-
## Infer Value Types
|
|
110
|
-
|
|
111
|
-
Use `Infer<typeof node>` or `Infer<typeof documentSchema>` for value types.
|
|
112
|
-
Schema-generated objects are flattened, with readonly properties and optional
|
|
113
|
-
presence preserved. Variants produce a discriminated union of flat branches:
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
import { field, object, variant, type Infer } from 'doxum';
|
|
117
24
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
25
|
+
document.update(draft => {
|
|
26
|
+
const task = draft.tasks.get('a');
|
|
27
|
+
if (task) task.done = true;
|
|
28
|
+
draft.tasks.put('b', { title: 'Review', done: false });
|
|
29
|
+
draft.tasks.remove('a');
|
|
30
|
+
return { warnings: [] };
|
|
121
31
|
});
|
|
122
|
-
|
|
123
|
-
type Outcome = Infer<typeof outcome>;
|
|
124
|
-
// { readonly kind: 'victory'; readonly reason: 'sealed' | 'destroyed' }
|
|
125
|
-
// | { readonly kind: 'defeat'; readonly reason: 'deadline' | 'collapse' }
|
|
126
32
|
```
|
|
127
33
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
34
|
+
The root object is the schema identity. Multiple runtimes can share its definition;
|
|
35
|
+
their data, revisions and subscriptions remain independent. Definitions are immutable.
|
|
36
|
+
|
|
37
|
+
`object()` exposes editable structure with a closed schema: undeclared own properties,
|
|
38
|
+
including symbols and non-enumerable properties, are rejected at input boundaries.
|
|
39
|
+
A variant allows its discriminant and the active branch's declared members.
|
|
40
|
+
Use `map()` for dynamic keys and `field<T>()` for arbitrary payload objects.
|
|
41
|
+
`field<T>()` is atomic: replace the whole
|
|
42
|
+
value, including objects, arrays, Maps and Dates. Atomic payloads are shared by
|
|
43
|
+
reference across inputs, reads, snapshots, commits and history. They are deeply
|
|
44
|
+
readonly in `Infer`/`Read`/`Draft`: never mutate them through any alias, even after
|
|
45
|
+
removal from the document. Published results are readonly by contract, without
|
|
46
|
+
defensive deep copying or runtime freezing. Classes and functions need no copier.
|
|
47
|
+
|
|
48
|
+
Drafts and structural reads are borrowed for their synchronous callback. Draft and
|
|
49
|
+
internal reader proxies must not escape their callback; escaping them is undefined
|
|
50
|
+
behavior. Ordinary property
|
|
51
|
+
reads see preceding writes in the same update. Same-address proxies are stable
|
|
52
|
+
within a scope and resolve against current structure after replacement.
|
|
53
|
+
|
|
54
|
+
Throw `new TransactionRejected({ code, message, address? })` for expected business
|
|
55
|
+
rejection. Schema and structural failures also return `status: 'rejected'` and
|
|
56
|
+
restore all prior work. Other thrown values roll back and are rethrown unchanged.
|
|
57
|
+
Normal callback returns, including `false`, are business results, not cancellation.
|
|
58
|
+
Committed observer failures are returned in `observerErrors` without rollback.
|
|
59
|
+
|
|
60
|
+
## Read And Observe
|
|
138
61
|
|
|
139
62
|
```ts
|
|
140
|
-
import { select } from 'doxum';
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
63
|
+
import { select, snapshot } from 'doxum';
|
|
64
|
+
|
|
65
|
+
const title = select(document, state => state.title);
|
|
66
|
+
const tasks = select(document, state => snapshot(state.tasks));
|
|
67
|
+
document.subscribe(
|
|
68
|
+
path => path.tasks.item('b').title,
|
|
69
|
+
commit => {
|
|
70
|
+
console.log(commit.revision, commit.changes);
|
|
71
|
+
}
|
|
144
72
|
);
|
|
145
|
-
|
|
146
|
-
const tasks = taskSchema.collection(path => path.tasks);
|
|
147
|
-
const stop = runtime.subscribe(tasks, commit => {
|
|
148
|
-
const change = commit.impact.collection(tasks);
|
|
149
|
-
if (change.kind === 'incremental') console.log(change.updated);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
stop();
|
|
73
|
+
document.subscribe([path => path.title, path => path.tasks], () => {});
|
|
153
74
|
```
|
|
154
75
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
76
|
+
`snapshot` exports a stable value with the subtree's `Infer` type: editable schema
|
|
77
|
+
structure is copied, while immutable atomic payloads keep their identity.
|
|
78
|
+
`snapshot(rawPayload)` returns that same readonly reference. To edit exported
|
|
79
|
+
payloads, explicitly copy them in application code; `structuredClone` works for
|
|
80
|
+
supported types, while classes/functions need application-specific handling.
|
|
81
|
+
Use ordinary properties for fine-grained reads. Data callbacks read real values;
|
|
82
|
+
path callbacks describe symbolic schema locations, including missing entries.
|
|
83
|
+
Subscription paths compile once during registration.
|
|
158
84
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
Local history is enabled by default with a capacity of 100 commits. Doxum
|
|
162
|
-
records inverse operations, so undo and redo follow the same mutation path as
|
|
163
|
-
ordinary updates.
|
|
85
|
+
React integration:
|
|
164
86
|
|
|
165
87
|
```ts
|
|
166
|
-
|
|
167
|
-
runtime.history.redo();
|
|
88
|
+
import { useDocumentSelector, useHistory, useReadable } from 'doxum/react';
|
|
168
89
|
|
|
169
|
-
|
|
90
|
+
const title = useDocumentSelector(document, state => state.title);
|
|
91
|
+
const history = useHistory(document.history);
|
|
170
92
|
```
|
|
171
93
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
`
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
mirrors: they read the durable tail in order after a BroadcastChannel hint, and
|
|
202
|
-
their direct writes throw `LocalSyncReadOnlyError`. When the leader disposes,
|
|
203
|
-
a follower catches up and becomes the next leader.
|
|
204
|
-
|
|
205
|
-
The leader's `runtime.update`, normal `runtime.apply`, and
|
|
206
|
-
`runtime.history.undo()` / `redo()` stay synchronous and immediately visible.
|
|
207
|
-
Local-sync observes the resulting local, system, and history commits and writes
|
|
208
|
-
their JSON operation batches to IndexedDB in the background. It deliberately
|
|
209
|
-
rejects `runtime.replace()` and an externally supplied `apply(..., {
|
|
210
|
-
source: 'remote' })` while attached: neither is a local operation command that
|
|
211
|
-
the log can faithfully append. `flush()` is the explicit point that waits for
|
|
212
|
-
commits observed before the call to persist (or, in a follower, waits to catch
|
|
213
|
-
up to the durable head). A browser crash, quota failure, or malformed JSON
|
|
214
|
-
payload can therefore leave an already visible leader commit unpersisted;
|
|
215
|
-
observe `state` or use `onError` to surface that condition.
|
|
94
|
+
React tracks fields actually read and updates dependencies when the selector
|
|
95
|
+
branches. The core has no React dependency. `asReadable(document)` removes write
|
|
96
|
+
capabilities while retaining selection, subscription and projection support.
|
|
97
|
+
|
|
98
|
+
## Containers And Parsing
|
|
99
|
+
|
|
100
|
+
- `map(field(...))`, `map(object(...))`, `map(variant(...))` expose
|
|
101
|
+
`get/has/ids/put/remove/replace`. `put` is an upsert; removing a missing key is a no-op.
|
|
102
|
+
- `table(object(...))` retains `{ ids, byId }` data and exposes
|
|
103
|
+
`get/has/ids/create/remove/move/replace`.
|
|
104
|
+
- `list(field(...), { keyOf })` retains a plain array and exposes
|
|
105
|
+
`get/has/ids/insert/remove/move/replace`.
|
|
106
|
+
- `tree(field(...))` retains `{ rootId?, nodes }` and exposes topology reads plus
|
|
107
|
+
`insert/remove/move/replace`.
|
|
108
|
+
- `variant(tag, branches)` has a readonly discriminant and whole-value branch replacement.
|
|
109
|
+
- `optional(node)` permits absence for fields, variants, maps, lists and trees.
|
|
110
|
+
|
|
111
|
+
Map/table key validators preserve branded string types through access, paths,
|
|
112
|
+
projection keys and impact queries. Validators are pure, synchronous functions or
|
|
113
|
+
Standard Schema v1 validators. They receive the original value, must not mutate it,
|
|
114
|
+
and their successful output is ignored. Perform transformations before calling
|
|
115
|
+
Doxum; it does not detect validator mutation or conversion. `parse(model, unknown)`
|
|
116
|
+
validates and copies schema structure while sharing readonly payloads. Strict parsing
|
|
117
|
+
requires validators for atomic fields; typed in-memory fields can omit them.
|
|
118
|
+
|
|
119
|
+
Collection `replace` has two forms: `replace(id, value)` replaces one existing
|
|
120
|
+
table/list/tree member without changing order or topology, while `replace(value)`
|
|
121
|
+
replaces the entire map/table/list/tree. Use top-level `replace(parent, key, value)`
|
|
122
|
+
when replacing an object or variant member whose draft type exposes collection tools:
|
|
216
123
|
|
|
217
124
|
```ts
|
|
218
|
-
import {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
tasks: { ids: [], byId: {} },
|
|
226
|
-
},
|
|
125
|
+
import { replace, table, variant } from 'doxum';
|
|
126
|
+
const boardModel = object({
|
|
127
|
+
entries: map(object({ rows: table(task) })),
|
|
128
|
+
view: variant('kind', {
|
|
129
|
+
empty: object({}),
|
|
130
|
+
tasks: object({ rows: table(task) }),
|
|
131
|
+
}),
|
|
227
132
|
});
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
database: 'my-app',
|
|
232
|
-
documentId: 'project-1',
|
|
133
|
+
const board = createDocument({
|
|
134
|
+
schema: boardModel,
|
|
135
|
+
initial: { entries: {}, view: { kind: 'empty' } },
|
|
233
136
|
});
|
|
137
|
+
board.update(draft => {
|
|
138
|
+
draft.entries.put('a', { rows: { ids: [], byId: {} } });
|
|
139
|
+
replace(draft, 'view', { kind: 'tasks', rows: { ids: [], byId: {} } });
|
|
140
|
+
});
|
|
141
|
+
```
|
|
234
142
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
143
|
+
Top-level `replace` checks the parent key and its plain `Infer` value, then enters the
|
|
144
|
+
same transaction write path. It is useful for variant replacement and initializing
|
|
145
|
+
optional collections. Map entries use `put`; collection-wide replacements use the
|
|
146
|
+
collection's own `replace`. Ordinary field and object-member assignments remain the
|
|
147
|
+
common case. See [value boundaries](docs/value-boundaries.md).
|
|
239
148
|
|
|
240
|
-
|
|
241
|
-
}
|
|
149
|
+
## Changes And History
|
|
242
150
|
|
|
243
|
-
|
|
244
|
-
|
|
151
|
+
A commit contains `{ revision, source, changes, impact }`. Repeated writes to one
|
|
152
|
+
field produce one first-before/final-after fact. Net-zero transactions do not
|
|
153
|
+
advance revision or notify. A `members` change shares its container address across
|
|
154
|
+
member transitions: `added` carries `after`, `removed` carries `before`, and
|
|
155
|
+
`updated` carries both. `order` records key sequences, `tree` records touched nodes
|
|
156
|
+
and nullable root IDs, and `reset` records a whole-document transition. There are
|
|
157
|
+
no per-value presence wrappers or intermediate assignment logs.
|
|
245
158
|
|
|
246
|
-
|
|
159
|
+
```ts
|
|
160
|
+
const changes = {
|
|
161
|
+
changes: [
|
|
162
|
+
{
|
|
163
|
+
kind: 'members',
|
|
164
|
+
at: ['tasks', 'a'],
|
|
165
|
+
members: [{ key: 'title', kind: 'updated', before: 'A', after: 'B' }],
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
247
169
|
```
|
|
248
170
|
|
|
249
|
-
|
|
250
|
-
`
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
does not own `runtime.dispose()`, and does not expose a second undo API. Runtime
|
|
256
|
-
history is in-memory only: hydration and remote tail application invalidate it,
|
|
257
|
-
so an undo stack never transfers to a new leader or survives reopening a tab.
|
|
258
|
-
Local-sync data must be JSON. Because command validation happens after the
|
|
259
|
-
runtime commit, a non-JSON payload is reported as a post-commit attachment error
|
|
260
|
-
rather than rolling back an already observed document change.
|
|
171
|
+
Each container appears in at most one members group. Grouping preserves exact
|
|
172
|
+
field impact; a group at `[]` is an incremental root-member change, not a reset.
|
|
173
|
+
An ordered container's group may also contain `order: { before, after }` with its
|
|
174
|
+
complete key sequences. Order-only groups use `members: []`; a standalone `order`
|
|
175
|
+
change or a second group for the same container is rejected. Apply installs each
|
|
176
|
+
group's members and order together before moving to the next group.
|
|
261
177
|
|
|
262
|
-
|
|
178
|
+
`document.apply(changes, { expectedRevision })` accepts unknown input. The revision
|
|
179
|
+
must match this runtime. The decoder rejects malformed and overlapping facts, then
|
|
180
|
+
the same session applies the complete transition atomically. Received `before`
|
|
181
|
+
values support reverse replay but are not trusted as local undo data; the runtime
|
|
182
|
+
captures its actual old state. Revision is a local baseline, not a distributed
|
|
183
|
+
conflict-resolution protocol.
|
|
263
184
|
|
|
264
|
-
|
|
265
|
-
|
|
185
|
+
Published ChangeSets are readonly in their entirety, including envelope objects,
|
|
186
|
+
addresses, transitions and order arrays. The runtime reuses validation of its own
|
|
187
|
+
publications and normalized storage input by identity; fresh unknown input still
|
|
188
|
+
passes through the decoder and every apply validates against current local state.
|
|
266
189
|
|
|
267
|
-
|
|
268
|
-
|
|
190
|
+
`history.undo()` and `redo()` replay the same changes by direction. A history group
|
|
191
|
+
holds complete commits and travels atomically with one notification:
|
|
269
192
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
193
|
+
```ts
|
|
194
|
+
const group = document.history.group();
|
|
195
|
+
document.update(draft => {
|
|
196
|
+
draft.title = 'First';
|
|
197
|
+
});
|
|
198
|
+
document.update(draft => {
|
|
199
|
+
draft.title = 'Final';
|
|
200
|
+
});
|
|
201
|
+
group.end();
|
|
202
|
+
document.history.undo();
|
|
274
203
|
```
|
|
275
204
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
## Derived Views
|
|
205
|
+
`group.cancel()` restores its start and pre-group history. Local `replace` is an
|
|
206
|
+
explicit reversible root reset. Remote commits invalidate local history. Writes
|
|
207
|
+
with `history: false` close the active group.
|
|
280
208
|
|
|
281
|
-
|
|
282
|
-
collections. It combines document runtimes and external values without owning
|
|
283
|
-
their mutations or history.
|
|
209
|
+
## Projections
|
|
284
210
|
|
|
285
211
|
```ts
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
document
|
|
290
|
-
|
|
212
|
+
import { createProjectionStore, input, project } from 'doxum';
|
|
213
|
+
|
|
214
|
+
const titles = project(
|
|
215
|
+
document,
|
|
216
|
+
path => path.tasks,
|
|
217
|
+
(id, task) => `${id}: ${task.title}`
|
|
291
218
|
);
|
|
292
|
-
const
|
|
293
|
-
const
|
|
219
|
+
const count = project({ titles }, ({ titles }) => titles.ids().length);
|
|
220
|
+
const zoom = input(1);
|
|
221
|
+
const scaled = project({ count, zoom }, ({ count, zoom }) => count * zoom);
|
|
222
|
+
|
|
223
|
+
const store = createProjectionStore({ onError: console.error });
|
|
224
|
+
store.get(scaled);
|
|
225
|
+
store.set(zoom, 2);
|
|
294
226
|
```
|
|
295
227
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
their
|
|
327
|
-
one fresh build; persistent faults block descendants, while independent branches
|
|
328
|
-
continue. Errors reach `onError` and, when inside document notification, the
|
|
329
|
-
committed result's `observerErrors`. Manual `rebuild()` uses the same graph.
|
|
330
|
-
Dispose the projection with its service; component unmount only unsubscribes.
|
|
331
|
-
Disposing a node with consumers is rejected, and disposed handles throw.
|
|
332
|
-
|
|
333
|
-
Schema access uses `object` for structured entities and `dict` for keyed values.
|
|
334
|
-
Variants expose `reader.get()` as a discriminated union and `writer.replace()`
|
|
335
|
-
for replacement. Optional presence is supported by field, variant, dict, list
|
|
336
|
-
and tree nodes. Dictionaries expose `get(key)`, `has(key)`, `keys()` and
|
|
337
|
-
`values()`; lists additionally support `get(key)` and `has(key)` using `keyOf`.
|
|
338
|
-
Tree insert/move accept `{ parentId, index }`, with index denoting the final
|
|
339
|
-
position after removing a moved node. See [API migration](docs/api-migration.md)
|
|
340
|
-
for breaking changes and the public surface.
|
|
341
|
-
|
|
342
|
-
## Data Ownership
|
|
343
|
-
|
|
344
|
-
Doxum clones the initial document. Structural values supplied through operations
|
|
345
|
-
are transferred to its mutable canonical document, while published commit and
|
|
346
|
-
history payloads are immutable snapshots. Treat data passed to an update as
|
|
347
|
-
owned by Doxum after the call unless the value is intentionally immutable.
|
|
348
|
-
|
|
349
|
-
Tree replacement snapshots are an exception: Doxum validates and clones the
|
|
350
|
-
tree structure so a caller cannot later corrupt its single-root, connected,
|
|
351
|
-
acyclic representation. Tree insert and move operations preserve the same
|
|
352
|
-
invariant.
|
|
228
|
+
Projection declarations are lazy and reusable. A `ProjectionStore` owns
|
|
229
|
+
materialized values, subscriptions, batching, processor state and disposal.
|
|
230
|
+
`project(document, path)` binds a document collection; adding a mapper performs
|
|
231
|
+
incremental keyed mapping. `project(readable)` bridges an external readable.
|
|
232
|
+
Pure computations receive current values. Advanced processors use tagged specs:
|
|
233
|
+
`project({ kind: 'value', sources, build })` and
|
|
234
|
+
`project({ kind: 'collection', sources, build })`.
|
|
235
|
+
Document collection events provide scoped `read.get/has/ids`, final candidate
|
|
236
|
+
keys, order dirtiness, commits and reset state.
|
|
237
|
+
|
|
238
|
+
Processors settle before external listeners. `store.batch` defers graph
|
|
239
|
+
settlement and projection notifications, but document commits/listeners remain
|
|
240
|
+
synchronous. Projection readers inside the batch see the last publication.
|
|
241
|
+
Dispose projection stores with their owning service.
|
|
242
|
+
See [projection contracts](docs/projections.md).
|
|
243
|
+
|
|
244
|
+
## Local Sync
|
|
245
|
+
|
|
246
|
+
`doxum/local-sync` attaches an IndexedDB timeline and Web Lock leadership to a
|
|
247
|
+
runtime. One leader writes synchronously and persists final changes asynchronously;
|
|
248
|
+
followers apply the durable sequence in order. `flush()` waits for persistence.
|
|
249
|
+
Durability errors do not roll back an already visible commit.
|
|
250
|
+
|
|
251
|
+
The storage format is IndexedDB version 5 with format version 3 records. Earlier
|
|
252
|
+
databases are rejected without upgrading, deleting or converting their data.
|
|
253
|
+
This JSON adapter rejects non-JSON atomic values. Network collaboration and
|
|
254
|
+
collaborative undo remain separate concerns; see [collaboration design](COLLABORATION_DESIGN.md).
|
|
255
|
+
Change count limits count individual members and tree nodes, not just outer groups.
|
|
256
|
+
`changeLimits` applies to newly authored local commits. Previously persisted
|
|
257
|
+
commits remain readable by followers and after reopening with smaller limits;
|
|
258
|
+
their JSON and ChangeSet structure are still validated.
|
|
353
259
|
|
|
354
260
|
## Development
|
|
355
261
|
|
|
@@ -359,29 +265,32 @@ pnpm run check
|
|
|
359
265
|
pnpm run build
|
|
360
266
|
pnpm run bench
|
|
361
267
|
pnpm run profile
|
|
268
|
+
node test.mjs
|
|
269
|
+
node core/bench/architecture.mjs --isolate
|
|
362
270
|
```
|
|
363
271
|
|
|
364
|
-
|
|
365
|
-
|
|
272
|
+
The architecture benchmark isolates each workload in a fresh process. Use
|
|
273
|
+
`--allocation` for separate V8 allocation sampling, `DOXUM_BENCH_FILTER` for a
|
|
274
|
+
comma-separated workload list, and `DOXUM_BENCH_MODULE=/absolute/path/to/index.js`
|
|
275
|
+
to compare a saved build. Sampling timings are not normal latency measurements.
|
|
276
|
+
`pnpm run profile` reports work counters and separates writes, sealing, remaining
|
|
277
|
+
runtime/publication work and explicit impact queries.
|
|
278
|
+
It also reports structural generation advances and distinguishes captured order
|
|
279
|
+
baselines from final published order copies. Architecture workloads include order
|
|
280
|
+
round trips and repeated tree edits to expose costs hidden by commit-only benchmarks.
|
|
281
|
+
|
|
282
|
+
Builds produce root `dist` ESM/CJS/declarations for `doxum`, `doxum/integration`,
|
|
283
|
+
`doxum/local-sync` and `doxum/react`. Source ownership is described in
|
|
284
|
+
[architecture](docs/architecture.md) and [AGENTS.md](AGENTS.md).
|
|
285
|
+
The runtime shares one transaction lifecycle; complete mutation operations are
|
|
286
|
+
organized by domain under `core/src/mutation/operations`, with access, first-touch
|
|
287
|
+
recording and publication retaining their own responsibilities.
|
|
288
|
+
The published [runtime skill](skills/doxum-runtime/SKILL.md) includes English and
|
|
289
|
+
Chinese application guidance.
|
|
366
290
|
|
|
367
291
|
## Release
|
|
368
292
|
|
|
369
|
-
`
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
pnpm release
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
Run it only from a clean, synchronized `main` branch. The command verifies the
|
|
377
|
-
logged-in npm account, updates both public package versions, runs the complete
|
|
378
|
-
check and build, verifies the publish tarball, publishes the package, then
|
|
379
|
-
creates and pushes a `vX.Y.Z` release commit and tag.
|
|
380
|
-
|
|
381
|
-
If npm accepts one package but the command cannot finish, it preserves the
|
|
382
|
-
release state rather than reverting a version that may already be public. Fix
|
|
383
|
-
the external failure and continue with:
|
|
384
|
-
|
|
385
|
-
```sh
|
|
386
|
-
pnpm release:resume
|
|
387
|
-
```
|
|
293
|
+
Run `pnpm release` from a clean, synchronized `main` branch to execute the release
|
|
294
|
+
checks, build, tarball validation, npm publication and release commit/tag steps.
|
|
295
|
+
Use `pnpm release:resume` to continue an interrupted release after resolving its
|
|
296
|
+
external failure. Publication is a separate maintainer action.
|