doxum 0.1.7 → 0.1.9
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 +217 -296
- package/dist/contract-BNStLbSE.d.ts +441 -0
- package/dist/contract-CIU5FCC1.d.cts +441 -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-C87tjRop.cjs} +4 -9
- package/dist/integration-C87tjRop.cjs.map +1 -0
- package/dist/{integration-B56u1l9V.js → integration-D5XCBLJ8.js} +2 -7
- package/dist/integration-D5XCBLJ8.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-1Uob0Ghk.cjs +2750 -0
- package/dist/store-1Uob0Ghk.cjs.map +1 -0
- package/dist/store-CD0KdGsq.d.cts +262 -0
- package/dist/store-D7QH6Rzw.js +2511 -0
- package/dist/store-D7QH6Rzw.js.map +1 -0
- package/dist/store-cp5CpfCy.d.ts +262 -0
- package/package.json +1 -1
- package/skills/doxum-runtime/SKILL.md +26 -44
- package/skills/doxum-runtime/references/guide.en.md +95 -359
- package/skills/doxum-runtime/references/guide.zh-CN.md +80 -285
- 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-DNZ4D53r.d.ts +0 -563
- package/dist/contract-j3SLGAwh.d.cts +0 -563
- 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-B22tuj9A.d.cts +0 -150
- package/dist/runtime-BFmhzPpZ.d.ts +0 -150
- package/dist/runtime-DF1q9Gje.js +0 -2129
- package/dist/runtime-DF1q9Gje.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,327 +1,245 @@
|
|
|
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
|
-
});
|
|
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>;
|
|
75
19
|
|
|
76
|
-
const
|
|
77
|
-
schema:
|
|
78
|
-
initial: {
|
|
79
|
-
title: 'Launch',
|
|
80
|
-
tasks: {
|
|
81
|
-
ids: ['task-1'],
|
|
82
|
-
byId: {
|
|
83
|
-
'task-1': { title: 'Write the brief', completed: false },
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
},
|
|
20
|
+
const document = createDocument({
|
|
21
|
+
schema: model,
|
|
22
|
+
initial: { title: 'Launch', tasks: { a: { title: 'Write', done: false } } },
|
|
87
23
|
});
|
|
88
24
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
25
|
+
document.update(draft => {
|
|
26
|
+
const task = draft.tasks.a;
|
|
27
|
+
if (task) task.done = true;
|
|
28
|
+
draft.tasks.b = { title: 'Review', done: false };
|
|
29
|
+
delete draft.tasks.a;
|
|
30
|
+
return { warnings: [] };
|
|
92
31
|
});
|
|
93
32
|
```
|
|
94
33
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
100
61
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
62
|
+
```ts
|
|
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
|
+
}
|
|
72
|
+
);
|
|
73
|
+
document.subscribe([path => path.title, path => path.tasks], () => {});
|
|
74
|
+
```
|
|
105
75
|
|
|
106
|
-
|
|
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.
|
|
107
84
|
|
|
108
|
-
|
|
109
|
-
when a non-React consumer needs only relevant commits.
|
|
85
|
+
React integration:
|
|
110
86
|
|
|
111
87
|
```ts
|
|
112
|
-
import {
|
|
88
|
+
import { useDocumentSelector, useHistory, useReadable } from 'doxum/react';
|
|
113
89
|
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
const tasks = taskSchema.collection(path => path.tasks);
|
|
119
|
-
const stop = runtime.subscribe(tasks, commit => {
|
|
120
|
-
const change = commit.impact.collection(tasks);
|
|
121
|
-
if (change.kind === 'incremental') console.log(change.updated);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
stop();
|
|
90
|
+
const title = useDocumentSelector(document, state => state.title);
|
|
91
|
+
const history = useHistory(document.history);
|
|
125
92
|
```
|
|
126
93
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
## History And Operations
|
|
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.
|
|
132
97
|
|
|
133
|
-
|
|
134
|
-
records inverse operations, so undo and redo follow the same mutation path as
|
|
135
|
-
ordinary updates.
|
|
98
|
+
## Containers And Parsing
|
|
136
99
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
100
|
+
- `map(field(...))`, `map(object(...))`, `map(variant(...))` use key indexing,
|
|
101
|
+
assignment and deletion. Absent differs from present `undefined`.
|
|
102
|
+
- `table(object(...))` retains `{ ids, byId }` data and `get/has/ids/create/remove/move`.
|
|
103
|
+
- `list(field(...), { keyOf })` retains a plain array and `get/has/ids/insert/set/remove/move/replace`.
|
|
104
|
+
- `tree(field(...))` retains `{ rootId?, nodes }` and explicit topology methods.
|
|
105
|
+
- `variant(tag, branches)` has a readonly discriminant and whole-value branch replacement.
|
|
106
|
+
- `optional(node)` permits absence for fields, variants, maps, lists and trees.
|
|
140
107
|
|
|
141
|
-
|
|
142
|
-
|
|
108
|
+
Map/table key validators preserve branded string types through access, paths,
|
|
109
|
+
projection keys and impact queries. Validators are pure, synchronous functions or
|
|
110
|
+
Standard Schema v1 validators. They receive the original value, must not mutate it,
|
|
111
|
+
and their successful output is ignored. Perform transformations before calling
|
|
112
|
+
Doxum; it does not detect validator mutation or conversion. `parse(model, unknown)`
|
|
113
|
+
validates and copies schema structure while sharing readonly payloads. Strict parsing
|
|
114
|
+
requires validators for atomic fields; typed in-memory fields can omit them.
|
|
143
115
|
|
|
144
|
-
|
|
145
|
-
updates, such as a drag. Keep its handle until the action ends, then call
|
|
146
|
-
`end()` to retain one undo entry or `cancel()` to apply its inverses atomically.
|
|
147
|
-
Groups cannot nest. Undo, redo, clear, remote/replace commits, and committed
|
|
148
|
-
updates with `history: false` end the current group. History implements
|
|
149
|
-
`Readable<HistoryState>` and can be used with `useReadable` or `fromReadable`.
|
|
150
|
-
|
|
151
|
-
`apply` is the boundary for replaying operations from persistence or a network
|
|
152
|
-
adapter. Doxum does not provide those adapters. A `replace` or a commit marked
|
|
153
|
-
as `remote` invalidates local history because its prior inverse sequence is no
|
|
154
|
-
longer authoritative.
|
|
155
|
-
|
|
156
|
-
The returned result distinguishes `committed`, `unchanged`, and `rejected`.
|
|
157
|
-
Observer failures do not turn a completed write into a rejection: committed
|
|
158
|
-
results expose them in `observerErrors`, after history and canonical state have
|
|
159
|
-
already settled.
|
|
160
|
-
|
|
161
|
-
## Local Persistence And Cross-Tab Sync
|
|
162
|
-
|
|
163
|
-
`doxum/local-sync` is an optional browser attachment for a document that needs
|
|
164
|
-
offline persistence and same-origin, cross-tab convergence. It uses IndexedDB
|
|
165
|
-
as the ordered checkpoint and commit log, Web Locks to serialize background
|
|
166
|
-
confirmation, and BroadcastChannel only to notify other tabs to catch up from
|
|
167
|
-
IndexedDB. It does not need a server or Yjs.
|
|
168
|
-
|
|
169
|
-
The attachment deliberately uses a simple single-writer model. It hydrates the
|
|
170
|
-
runtime from the IndexedDB checkpoint and ordered command log, then attempts to
|
|
171
|
-
hold a Web Lock for that document. The lock holder is the `leader` and may use
|
|
172
|
-
the normal synchronous runtime APIs. All other attached tabs are `follower`
|
|
173
|
-
mirrors: they read the durable tail in order after a BroadcastChannel hint, and
|
|
174
|
-
their direct writes throw `LocalSyncReadOnlyError`. When the leader disposes,
|
|
175
|
-
a follower catches up and becomes the next leader.
|
|
176
|
-
|
|
177
|
-
The leader's `runtime.update`, normal `runtime.apply`, and
|
|
178
|
-
`runtime.history.undo()` / `redo()` stay synchronous and immediately visible.
|
|
179
|
-
Local-sync observes the resulting local, system, and history commits and writes
|
|
180
|
-
their JSON operation batches to IndexedDB in the background. It deliberately
|
|
181
|
-
rejects `runtime.replace()` and an externally supplied `apply(..., {
|
|
182
|
-
source: 'remote' })` while attached: neither is a local operation command that
|
|
183
|
-
the log can faithfully append. `flush()` is the explicit point that waits for
|
|
184
|
-
commits observed before the call to persist (or, in a follower, waits to catch
|
|
185
|
-
up to the durable head). A browser crash, quota failure, or malformed JSON
|
|
186
|
-
payload can therefore leave an already visible leader commit unpersisted;
|
|
187
|
-
observe `state` or use `onError` to surface that condition.
|
|
116
|
+
For replacements containing nested collection tools, use `assign(scope, key, value)`:
|
|
188
117
|
|
|
189
118
|
```ts
|
|
190
|
-
import {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
initial: {
|
|
196
|
-
title: 'Launch',
|
|
197
|
-
tasks: { ids: [], byId: {} },
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const localSync = await attachLocalSync({
|
|
202
|
-
runtime,
|
|
203
|
-
database: 'my-app',
|
|
204
|
-
documentId: 'project-1',
|
|
119
|
+
import { assign, table } from 'doxum';
|
|
120
|
+
const boardModel = object({ entries: map(object({ rows: table(task) })) });
|
|
121
|
+
const board = createDocument({ schema: boardModel, initial: { entries: {} } });
|
|
122
|
+
board.update(draft => {
|
|
123
|
+
assign(draft.entries, 'a', { rows: { ids: [], byId: {} } });
|
|
205
124
|
});
|
|
125
|
+
```
|
|
206
126
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
127
|
+
TypeScript cannot give a mapped property a draft read type and a different plain
|
|
128
|
+
data assignment type. `assign` checks the key and its `Infer` value and calls the
|
|
129
|
+
same transaction write path. It is useful for complex map entries, variant
|
|
130
|
+
replacement and initializing optional lists/trees. Ordinary assignments remain
|
|
131
|
+
the common case. See [value boundaries](docs/value-boundaries.md).
|
|
211
132
|
|
|
212
|
-
|
|
213
|
-
}
|
|
133
|
+
## Changes And History
|
|
214
134
|
|
|
215
|
-
|
|
216
|
-
|
|
135
|
+
A commit contains `{ revision, source, changes, impact }`. Repeated writes to one
|
|
136
|
+
field produce one first-before/final-after fact. Net-zero transactions do not
|
|
137
|
+
advance revision or notify. A `members` change shares its container address across
|
|
138
|
+
member transitions: `added` carries `after`, `removed` carries `before`, and
|
|
139
|
+
`updated` carries both. `order` records key sequences, `tree` records touched nodes
|
|
140
|
+
and nullable root IDs, and `reset` records a whole-document transition. There are
|
|
141
|
+
no per-value presence wrappers or intermediate assignment logs.
|
|
217
142
|
|
|
218
|
-
|
|
143
|
+
```ts
|
|
144
|
+
const changes = {
|
|
145
|
+
changes: [
|
|
146
|
+
{
|
|
147
|
+
kind: 'members',
|
|
148
|
+
at: ['tasks', 'a'],
|
|
149
|
+
members: [{ key: 'title', kind: 'updated', before: 'A', after: 'B' }],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
};
|
|
219
153
|
```
|
|
220
154
|
|
|
221
|
-
|
|
222
|
-
`
|
|
223
|
-
|
|
155
|
+
Each container appears in at most one members group. Grouping preserves exact
|
|
156
|
+
field impact; a group at `[]` is an incremental root-member change, not a reset.
|
|
157
|
+
An ordered container's group may also contain `order: { before, after }` with its
|
|
158
|
+
complete key sequences. Order-only groups use `members: []`; a standalone `order`
|
|
159
|
+
change or a second group for the same container is rejected. Apply installs each
|
|
160
|
+
group's members and order together before moving to the next group.
|
|
224
161
|
|
|
225
|
-
`
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
runtime commit, a non-JSON payload is reported as a post-commit attachment error
|
|
232
|
-
rather than rolling back an already observed document change.
|
|
162
|
+
`document.apply(changes, { expectedRevision })` accepts unknown input. The revision
|
|
163
|
+
must match this runtime. The decoder rejects malformed and overlapping facts, then
|
|
164
|
+
the same session applies the complete transition atomically. Received `before`
|
|
165
|
+
values support reverse replay but are not trusted as local undo data; the runtime
|
|
166
|
+
captures its actual old state. Revision is a local baseline, not a distributed
|
|
167
|
+
conflict-resolution protocol.
|
|
233
168
|
|
|
234
|
-
|
|
169
|
+
Published ChangeSets are readonly in their entirety, including envelope objects,
|
|
170
|
+
addresses, transitions and order arrays. The runtime reuses validation of its own
|
|
171
|
+
publications and normalized storage input by identity; fresh unknown input still
|
|
172
|
+
passes through the decoder and every apply validates against current local state.
|
|
235
173
|
|
|
236
|
-
`
|
|
237
|
-
|
|
174
|
+
`history.undo()` and `redo()` replay the same changes by direction. A history group
|
|
175
|
+
holds complete commits and travels atomically with one notification:
|
|
238
176
|
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
177
|
+
```ts
|
|
178
|
+
const group = document.history.group();
|
|
179
|
+
document.update(draft => {
|
|
180
|
+
draft.title = 'First';
|
|
181
|
+
});
|
|
182
|
+
document.update(draft => {
|
|
183
|
+
draft.title = 'Final';
|
|
184
|
+
});
|
|
185
|
+
group.end();
|
|
186
|
+
document.history.undo();
|
|
246
187
|
```
|
|
247
188
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
## Derived Views
|
|
189
|
+
`group.cancel()` restores its start and pre-group history. Local `replace` is an
|
|
190
|
+
explicit reversible root reset. Remote commits invalidate local history. Writes
|
|
191
|
+
with `history: false` close the active group.
|
|
252
192
|
|
|
253
|
-
|
|
254
|
-
collections. It combines document runtimes and external values without owning
|
|
255
|
-
their mutations or history.
|
|
193
|
+
## Projections
|
|
256
194
|
|
|
257
195
|
```ts
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
document
|
|
262
|
-
|
|
196
|
+
import { createProjectionStore, input, project } from 'doxum';
|
|
197
|
+
|
|
198
|
+
const titles = project(
|
|
199
|
+
document,
|
|
200
|
+
path => path.tasks,
|
|
201
|
+
(id, task) => `${id}: ${task.title}`
|
|
263
202
|
);
|
|
264
|
-
const
|
|
265
|
-
const
|
|
203
|
+
const count = project({ titles }, ({ titles }) => titles.ids().length);
|
|
204
|
+
const zoom = input(1);
|
|
205
|
+
const scaled = project({ count, zoom }, ({ count, zoom }) => count * zoom);
|
|
206
|
+
|
|
207
|
+
const store = createProjectionStore({ onError: console.error });
|
|
208
|
+
store.get(scaled);
|
|
209
|
+
store.set(zoom, 2);
|
|
266
210
|
```
|
|
267
211
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
their
|
|
299
|
-
one fresh build; persistent faults block descendants, while independent branches
|
|
300
|
-
continue. Errors reach `onError` and, when inside document notification, the
|
|
301
|
-
committed result's `observerErrors`. Manual `rebuild()` uses the same graph.
|
|
302
|
-
Dispose the projection with its service; component unmount only unsubscribes.
|
|
303
|
-
Disposing a node with consumers is rejected, and disposed handles throw.
|
|
304
|
-
|
|
305
|
-
Schema access uses `object` for structured entities and `dict` for keyed values.
|
|
306
|
-
Variants expose `reader.get()` as a discriminated union and `writer.replace()`
|
|
307
|
-
for replacement. Optional presence is supported by field, variant, dict, list
|
|
308
|
-
and tree nodes. Dictionaries expose `get(key)`, `has(key)`, `keys()` and
|
|
309
|
-
`values()`; lists additionally support `get(key)` and `has(key)` using `keyOf`.
|
|
310
|
-
Tree insert/move accept `{ parentId, index }`, with index denoting the final
|
|
311
|
-
position after removing a moved node. See [API migration](docs/api-migration.md)
|
|
312
|
-
for breaking changes and the public surface.
|
|
313
|
-
|
|
314
|
-
## Data Ownership
|
|
315
|
-
|
|
316
|
-
Doxum clones the initial document. Structural values supplied through operations
|
|
317
|
-
are transferred to its mutable canonical document, while published commit and
|
|
318
|
-
history payloads are immutable snapshots. Treat data passed to an update as
|
|
319
|
-
owned by Doxum after the call unless the value is intentionally immutable.
|
|
320
|
-
|
|
321
|
-
Tree replacement snapshots are an exception: Doxum validates and clones the
|
|
322
|
-
tree structure so a caller cannot later corrupt its single-root, connected,
|
|
323
|
-
acyclic representation. Tree insert and move operations preserve the same
|
|
324
|
-
invariant.
|
|
212
|
+
Projection declarations are lazy and reusable. A `ProjectionStore` owns
|
|
213
|
+
materialized values, subscriptions, batching, processor state and disposal.
|
|
214
|
+
`project(document, path)` binds a document collection; adding a mapper performs
|
|
215
|
+
incremental keyed mapping. `project(readable)` bridges an external readable.
|
|
216
|
+
Pure computations receive current values. Advanced processors use tagged specs:
|
|
217
|
+
`project({ kind: 'value', sources, build })` and
|
|
218
|
+
`project({ kind: 'collection', sources, build })`.
|
|
219
|
+
Document collection events provide scoped `read.get/has/ids`, final candidate
|
|
220
|
+
keys, order dirtiness, commits and reset state.
|
|
221
|
+
|
|
222
|
+
Processors settle before external listeners. `store.batch` defers graph
|
|
223
|
+
settlement and projection notifications, but document commits/listeners remain
|
|
224
|
+
synchronous. Projection readers inside the batch see the last publication.
|
|
225
|
+
Dispose projection stores with their owning service.
|
|
226
|
+
See [projection contracts](docs/projections.md).
|
|
227
|
+
|
|
228
|
+
## Local Sync
|
|
229
|
+
|
|
230
|
+
`doxum/local-sync` attaches an IndexedDB timeline and Web Lock leadership to a
|
|
231
|
+
runtime. One leader writes synchronously and persists final changes asynchronously;
|
|
232
|
+
followers apply the durable sequence in order. `flush()` waits for persistence.
|
|
233
|
+
Durability errors do not roll back an already visible commit.
|
|
234
|
+
|
|
235
|
+
The storage format is IndexedDB version 5 with format version 3 records. Earlier
|
|
236
|
+
databases are rejected without upgrading, deleting or converting their data.
|
|
237
|
+
This JSON adapter rejects non-JSON atomic values. Network collaboration and
|
|
238
|
+
collaborative undo remain separate concerns; see [collaboration design](COLLABORATION_DESIGN.md).
|
|
239
|
+
Change count limits count individual members and tree nodes, not just outer groups.
|
|
240
|
+
`changeLimits` applies to newly authored local commits. Previously persisted
|
|
241
|
+
commits remain readable by followers and after reopening with smaller limits;
|
|
242
|
+
their JSON and ChangeSet structure are still validated.
|
|
325
243
|
|
|
326
244
|
## Development
|
|
327
245
|
|
|
@@ -331,29 +249,32 @@ pnpm run check
|
|
|
331
249
|
pnpm run build
|
|
332
250
|
pnpm run bench
|
|
333
251
|
pnpm run profile
|
|
252
|
+
node test.mjs
|
|
253
|
+
node core/bench/architecture.mjs --isolate
|
|
334
254
|
```
|
|
335
255
|
|
|
336
|
-
|
|
337
|
-
|
|
256
|
+
The architecture benchmark isolates each workload in a fresh process. Use
|
|
257
|
+
`--allocation` for separate V8 allocation sampling, `DOXUM_BENCH_FILTER` for a
|
|
258
|
+
comma-separated workload list, and `DOXUM_BENCH_MODULE=/absolute/path/to/index.js`
|
|
259
|
+
to compare a saved build. Sampling timings are not normal latency measurements.
|
|
260
|
+
`pnpm run profile` reports work counters and separates writes, sealing, remaining
|
|
261
|
+
runtime/publication work and explicit impact queries.
|
|
262
|
+
It also reports structural generation advances and distinguishes captured order
|
|
263
|
+
baselines from final published order copies. Architecture workloads include order
|
|
264
|
+
round trips and repeated tree edits to expose costs hidden by commit-only benchmarks.
|
|
265
|
+
|
|
266
|
+
Builds produce root `dist` ESM/CJS/declarations for `doxum`, `doxum/integration`,
|
|
267
|
+
`doxum/local-sync` and `doxum/react`. Source ownership is described in
|
|
268
|
+
[architecture](docs/architecture.md) and [AGENTS.md](AGENTS.md).
|
|
269
|
+
The runtime shares one transaction lifecycle; complete mutation operations are
|
|
270
|
+
organized by domain under `core/src/mutation/operations`, with access, first-touch
|
|
271
|
+
recording and publication retaining their own responsibilities.
|
|
272
|
+
The published [runtime skill](skills/doxum-runtime/SKILL.md) includes English and
|
|
273
|
+
Chinese application guidance.
|
|
338
274
|
|
|
339
275
|
## Release
|
|
340
276
|
|
|
341
|
-
`
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
pnpm release
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
Run it only from a clean, synchronized `main` branch. The command verifies the
|
|
349
|
-
logged-in npm account, updates both public package versions, runs the complete
|
|
350
|
-
check and build, verifies the publish tarball, publishes the package, then
|
|
351
|
-
creates and pushes a `vX.Y.Z` release commit and tag.
|
|
352
|
-
|
|
353
|
-
If npm accepts one package but the command cannot finish, it preserves the
|
|
354
|
-
release state rather than reverting a version that may already be public. Fix
|
|
355
|
-
the external failure and continue with:
|
|
356
|
-
|
|
357
|
-
```sh
|
|
358
|
-
pnpm release:resume
|
|
359
|
-
```
|
|
277
|
+
Run `pnpm release` from a clean, synchronized `main` branch to execute the release
|
|
278
|
+
checks, build, tarball validation, npm publication and release commit/tag steps.
|
|
279
|
+
Use `pnpm release:resume` to continue an interrupted release after resolving its
|
|
280
|
+
external failure. Publication is a separate maintainer action.
|