doxum 0.1.1
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/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/chunk-pbuEa-1d.js +13 -0
- package/dist/contract-DwNiKioc.d.ts +480 -0
- package/dist/contract-Otb5W6cQ.d.cts +480 -0
- package/dist/dependency-BdEMyquf.js +735 -0
- package/dist/dependency-BdEMyquf.js.map +1 -0
- package/dist/dependency-DLcCvNKq.cjs +1015 -0
- package/dist/dependency-DLcCvNKq.cjs.map +1 -0
- package/dist/index.cjs +2718 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +89 -0
- package/dist/index.d.ts +87 -0
- package/dist/index.js +2689 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.cjs +15 -0
- package/dist/integration.cjs.map +1 -0
- package/dist/integration.d.cts +11 -0
- package/dist/integration.d.ts +11 -0
- package/dist/integration.js +14 -0
- package/dist/integration.js.map +1 -0
- package/dist/react.cjs +99 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +16 -0
- package/dist/react.d.ts +16 -0
- package/dist/react.js +96 -0
- package/dist/react.js.map +1 -0
- package/package.json +89 -0
- package/skills/doxum-runtime/SKILL.md +51 -0
- package/skills/doxum-runtime/agents/openai.yaml +4 -0
- package/skills/doxum-runtime/references/guide.en.md +295 -0
- package/skills/doxum-runtime/references/guide.zh-CN.md +240 -0
- package/skills/doxum-runtime/references/invariants.en.md +147 -0
- package/skills/doxum-runtime/references/invariants.zh-CN.md +97 -0
- package/skills/doxum-runtime/references/patterns.en.md +287 -0
- package/skills/doxum-runtime/references/patterns.zh-CN.md +240 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Doxum Runtime Guide
|
|
2
|
+
|
|
3
|
+
This is the task-oriented public guide for Doxum. It describes how to model,
|
|
4
|
+
read, mutate, observe, and project one in-memory document with
|
|
5
|
+
`doxum`, and how to bind those read models with `doxum/react`.
|
|
6
|
+
|
|
7
|
+
Doxum is deliberately local. It owns typed document state, atomic mutation,
|
|
8
|
+
history, impact, subscriptions, and derived views. Your application owns
|
|
9
|
+
persistence, network ordering, authorization, and conflict resolution.
|
|
10
|
+
|
|
11
|
+
## Choose the right entry point
|
|
12
|
+
|
|
13
|
+
| Goal | Use |
|
|
14
|
+
| ------------------------------------- | -------------------------------------------------------- |
|
|
15
|
+
| Define document shape | `schema`, `field`, `object`, and collection constructors |
|
|
16
|
+
| Create the canonical runtime | `createDocument` |
|
|
17
|
+
| Read once | `select(runtime, read => ...)` |
|
|
18
|
+
| Make local business changes | `runtime.update(tx => ...)` |
|
|
19
|
+
| Replay persisted or remote operations | `runtime.apply(operations, options)` |
|
|
20
|
+
| Replace an entire trusted snapshot | `runtime.replace(document, options)` |
|
|
21
|
+
| Observe one schema location | `schema.value` plus `runtime.subscribe` |
|
|
22
|
+
| Observe a table or map | `schema.collection` plus `runtime.subscribe` |
|
|
23
|
+
| Maintain mapped collection data | `createCollectionView` |
|
|
24
|
+
| Maintain an aggregate or index | `createMaterializedView` |
|
|
25
|
+
| Read in React | `useDocumentSelector`, `useReadable`, or `useReadable` |
|
|
26
|
+
|
|
27
|
+
Do not write a second mutable copy of the document. `createDocument` is the
|
|
28
|
+
only owner of canonical state.
|
|
29
|
+
|
|
30
|
+
## Start with a schema
|
|
31
|
+
|
|
32
|
+
A schema is both the TypeScript shape of the document and the authoritative
|
|
33
|
+
address model for writers, operations, selectors, and subscriptions. Define it
|
|
34
|
+
once and keep it close to the domain it describes.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { createDocument, field, object, schema, table } from 'doxum';
|
|
38
|
+
|
|
39
|
+
const task = object({
|
|
40
|
+
title: field<string>(),
|
|
41
|
+
completed: field<boolean>(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const taskSchema = schema({
|
|
45
|
+
title: field<string>(),
|
|
46
|
+
tasks: table(task),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const runtime = createDocument({
|
|
50
|
+
schema: taskSchema,
|
|
51
|
+
initial: {
|
|
52
|
+
title: 'Launch Doxum',
|
|
53
|
+
tasks: {
|
|
54
|
+
ids: ['write-guide'],
|
|
55
|
+
byId: {
|
|
56
|
+
'write-guide': { title: 'Write the guide', completed: false },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`table` preserves application-visible order through `{ ids, byId }`. `map`
|
|
64
|
+
stores id-indexed entities without order. Use `single` for one structured
|
|
65
|
+
entity, `dict` or `record` for scalar key/value data, `list` for an ordered
|
|
66
|
+
sequence with an application-supplied stable key, and `tree` for a validated
|
|
67
|
+
single-root hierarchy. See [patterns.en.md](patterns.en.md) for modelling
|
|
68
|
+
guidance.
|
|
69
|
+
|
|
70
|
+
## Read through readers
|
|
71
|
+
|
|
72
|
+
The runtime does not expose its mutable document. Read through a callback:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { select } from 'doxum';
|
|
76
|
+
|
|
77
|
+
const openTitles = select(runtime, read =>
|
|
78
|
+
read.tasks.ids().flatMap(id => {
|
|
79
|
+
const task = read.tasks.get(id);
|
|
80
|
+
return task && !task.completed.get() ? [task.title.get()] : [];
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Readers are intentionally shaped by the schema:
|
|
86
|
+
|
|
87
|
+
- A field has `get()`.
|
|
88
|
+
- A table or map has `ids()`, `has(id)`, and `get(id)`.
|
|
89
|
+
- A list has `values()`, `length()`, and `at(index)`.
|
|
90
|
+
- A tree has `rootId()`, `has(id)`, `value(id)`, `parent(id)`, and
|
|
91
|
+
`children(id)`.
|
|
92
|
+
|
|
93
|
+
Reader values for structural data are snapshots. Do not retain a transaction
|
|
94
|
+
reader after `runtime.update` returns; it is only valid during that callback.
|
|
95
|
+
|
|
96
|
+
## Mutate atomically
|
|
97
|
+
|
|
98
|
+
Use `runtime.update` for local, typed domain behavior. Its callback receives a
|
|
99
|
+
short-lived `tx.read` and `tx.write`. Writers create operations for one atomic
|
|
100
|
+
session; they never expose direct canonical mutation.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const result = runtime.update(tx => {
|
|
104
|
+
const task = tx.read.tasks.get('write-guide');
|
|
105
|
+
if (!task) {
|
|
106
|
+
tx.reject({
|
|
107
|
+
source: 'application',
|
|
108
|
+
code: 'task-not-found',
|
|
109
|
+
message: 'The requested task no longer exists.',
|
|
110
|
+
address: ['tasks', 'write-guide'],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
tx.write.tasks.item('write-guide').completed.set(true);
|
|
115
|
+
return task.title.get();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (result.status === 'committed') {
|
|
119
|
+
console.log(result.value, result.commit.revision);
|
|
120
|
+
} else if (result.status === 'rejected') {
|
|
121
|
+
console.error(result.issues);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
An update is synchronous and atomic:
|
|
126
|
+
|
|
127
|
+
- If a writer emits a semantically invalid operation, Doxum rolls back the
|
|
128
|
+
entire session and returns `status: 'rejected'` with `MutationIssue` values.
|
|
129
|
+
- `tx.reject(...)` rolls back and returns your application
|
|
130
|
+
`DocumentDiagnostic` values.
|
|
131
|
+
- A normal thrown error also rolls back, then is rethrown to the caller.
|
|
132
|
+
- Net-zero work returns `status: 'unchanged'` and publishes no commit.
|
|
133
|
+
|
|
134
|
+
Use `tx.report(...)` for non-blocking application diagnostics. Committed and
|
|
135
|
+
unchanged transaction results expose them as `reports`; reports and diagnostic
|
|
136
|
+
addresses are copied and frozen before they are published.
|
|
137
|
+
|
|
138
|
+
## Use writers instead of constructing local operations
|
|
139
|
+
|
|
140
|
+
For normal application behavior, writer APIs are clearer and preserve the
|
|
141
|
+
schema domain:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
runtime.update(tx => {
|
|
145
|
+
tx.write.title.set('Ship Doxum');
|
|
146
|
+
tx.write.tasks.create(
|
|
147
|
+
{ id: 'release', value: { title: 'Publish the package', completed: false } },
|
|
148
|
+
{ after: 'write-guide' }
|
|
149
|
+
);
|
|
150
|
+
tx.write.tasks.item('release').title.set('Publish doxum');
|
|
151
|
+
tx.write.tasks.move('release', { at: 'start' });
|
|
152
|
+
tx.write.tasks.remove('write-guide');
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
For a table, `create`, `item`, `remove`, and `move` are available. A map has
|
|
157
|
+
the same API except `move`, because it is unordered. Lists offer `insert`,
|
|
158
|
+
`move`, `remove`, and `replace`; list identity comes from the `keyOf` function
|
|
159
|
+
specified in the schema. See [patterns.en.md](patterns.en.md) for complete
|
|
160
|
+
collection and tree examples.
|
|
161
|
+
|
|
162
|
+
## Replay operations at the boundary
|
|
163
|
+
|
|
164
|
+
Use `apply` for operation batches that came from persistence, a network
|
|
165
|
+
adapter, or another external boundary. Doxum decodes unknown operation payloads
|
|
166
|
+
before mutation code observes them, resolves every address against the schema,
|
|
167
|
+
and applies the batch atomically.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const result = runtime.apply([{ type: 'field.set', at: ['title'], value: 'Restored title' }], {
|
|
171
|
+
source: 'remote',
|
|
172
|
+
history: false,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (result.status === 'rejected') {
|
|
176
|
+
// The document and revision remain unchanged.
|
|
177
|
+
console.error(result.issues);
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Treat external operation input as untrusted, even if TypeScript types make it
|
|
182
|
+
look valid. Do not write a second path parser or validate operations by
|
|
183
|
+
partially replaying them outside Doxum. A remote commit and every `replace`
|
|
184
|
+
establish a new baseline, so they invalidate local undo/redo history.
|
|
185
|
+
|
|
186
|
+
## Interpret results and history
|
|
187
|
+
|
|
188
|
+
Every mutation entry point returns one of three states:
|
|
189
|
+
|
|
190
|
+
| Status | Meaning |
|
|
191
|
+
| ----------- | -------------------------------------------------------- |
|
|
192
|
+
| `committed` | Canonical state changed; the result contains a commit. |
|
|
193
|
+
| `unchanged` | The net state did not change; the revision is unchanged. |
|
|
194
|
+
| `rejected` | The whole batch rolled back; inspect `issues`. |
|
|
195
|
+
|
|
196
|
+
Committed operations carry forward operations, inverse operations, a revision,
|
|
197
|
+
and a `DocumentImpact`. Local history records local and system commits by
|
|
198
|
+
default. Use `runtime.history.undo()` and `runtime.history.redo()`; they replay
|
|
199
|
+
the inverse or forward operation batch through the same mutation pipeline.
|
|
200
|
+
|
|
201
|
+
`observerErrors` on a committed result are failures from processors, flushes,
|
|
202
|
+
or listeners after canonical state and history settled. They are not mutation
|
|
203
|
+
failures and must not cause the caller to repeat the write.
|
|
204
|
+
|
|
205
|
+
## Subscribe through schema-owned targets
|
|
206
|
+
|
|
207
|
+
Create stable selectors from the schema, then subscribe to them. This is the
|
|
208
|
+
shared address and impact model for the whole runtime.
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
const title = taskSchema.value(path => path.title);
|
|
212
|
+
const tasks = taskSchema.collection(path => path.tasks);
|
|
213
|
+
|
|
214
|
+
const stopTitle = runtime.subscribe(title, commit => {
|
|
215
|
+
console.log('title changed at revision', commit.revision);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const stopTasks = runtime.subscribe(tasks, commit => {
|
|
219
|
+
const change = commit.impact.collection(tasks);
|
|
220
|
+
if (change.kind === 'incremental') {
|
|
221
|
+
console.log(change.added, change.removed, change.updated, change.orderChanged);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
stopTitle();
|
|
226
|
+
stopTasks();
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
For value selectors, use `commit.impact.affects(target)`. For table or map
|
|
230
|
+
selectors, use `commit.impact.collection(selector)`, which returns either a
|
|
231
|
+
precise incremental change or `reset` after replacement. Do not recreate path
|
|
232
|
+
comparison helpers in application modules.
|
|
233
|
+
|
|
234
|
+
## Build derived read models
|
|
235
|
+
|
|
236
|
+
`createCollectionView` maps one table or map into stable ids, cached `item(id)`
|
|
237
|
+
readables, and a lazy `all` array. It updates only affected entries where possible.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { createCollectionView } from 'doxum';
|
|
241
|
+
|
|
242
|
+
const taskTitles = createCollectionView({
|
|
243
|
+
runtime,
|
|
244
|
+
source: taskSchema.collection(path => path.tasks),
|
|
245
|
+
map: (_id, task) => task.title.get(),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
taskTitles.item('write-guide').current();
|
|
249
|
+
taskTitles.all.current();
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Use `createMaterializedView` for an index or aggregate with custom incremental
|
|
253
|
+
logic. A materialized view is derived state, not a caller-maintained cache. It
|
|
254
|
+
may depend only on materialized views created earlier from the same runtime.
|
|
255
|
+
Dispose every view when its owning feature is disposed.
|
|
256
|
+
|
|
257
|
+
## React integration
|
|
258
|
+
|
|
259
|
+
`doxum/react` uses `useSyncExternalStore` and tracked Doxum dependencies. A
|
|
260
|
+
component re-renders only for commits that can affect the selector it read.
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
import { useDocumentSelector } from 'doxum/react';
|
|
264
|
+
|
|
265
|
+
function OpenTaskCount() {
|
|
266
|
+
const count = useDocumentSelector(
|
|
267
|
+
runtime,
|
|
268
|
+
read => read.tasks.ids().filter(id => !read.tasks.get(id)?.completed.get()).length
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
return <output>{count}</output>;
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Use `useReadable(view.all)` for a `Readable`, `useReadable(view.item(id))`
|
|
276
|
+
for one keyed value, and `useHistory(runtime.history)` for undo/redo state and
|
|
277
|
+
actions. Keep `core` free of React imports; React-specific code belongs in the
|
|
278
|
+
adapter or application layer.
|
|
279
|
+
|
|
280
|
+
## Lifecycle and ownership
|
|
281
|
+
|
|
282
|
+
- The initial document is cloned when `createDocument` starts.
|
|
283
|
+
- Structural payloads passed through operations are transferred into canonical
|
|
284
|
+
state. Do not mutate them afterwards unless you intentionally want to mutate
|
|
285
|
+
the canonical document.
|
|
286
|
+
- Published commits, history payloads, diagnostics, and selector addresses are
|
|
287
|
+
immutable snapshots.
|
|
288
|
+
- Tree replacement snapshots are validated and cloned to preserve structural
|
|
289
|
+
integrity.
|
|
290
|
+
- Call `runtime.dispose()` when the runtime is no longer usable. Existing
|
|
291
|
+
subscriptions, history state, and views should be disposed with their owners.
|
|
292
|
+
|
|
293
|
+
For decision rules and anti-patterns, read
|
|
294
|
+
[invariants.en.md](invariants.en.md). For copyable implementation patterns,
|
|
295
|
+
read [patterns.en.md](patterns.en.md).
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Doxum Runtime 使用指南
|
|
2
|
+
|
|
3
|
+
这是 Doxum 面向任务的公开使用指南。它说明如何借助 `doxum` 建模、读取、修改、观察和派生一个内存文档,以及如何通过 `doxum/react` 将这些读模型接入 React。
|
|
4
|
+
|
|
5
|
+
Doxum 有意保持本地化:它负责类型化文档状态、原子修改、历史记录、影响范围、订阅和派生视图。持久化、网络顺序、授权和冲突解决由应用负责。
|
|
6
|
+
|
|
7
|
+
## 先选择正确入口
|
|
8
|
+
|
|
9
|
+
| 目标 | 使用方式 |
|
|
10
|
+
| -------------------------- | ----------------------------------------------------- |
|
|
11
|
+
| 定义文档结构 | `schema`、`field`、`object` 和集合构造器 |
|
|
12
|
+
| 创建 canonical runtime | `createDocument` |
|
|
13
|
+
| 一次性读取 | `select(runtime, read => ...)` |
|
|
14
|
+
| 执行本地业务修改 | `runtime.update(tx => ...)` |
|
|
15
|
+
| 回放持久化或远端 operation | `runtime.apply(operations, options)` |
|
|
16
|
+
| 整体替换可信快照 | `runtime.replace(document, options)` |
|
|
17
|
+
| 监听一个 schema 位置 | `schema.value` + `runtime.subscribe` |
|
|
18
|
+
| 监听 table 或 map | `schema.collection` + `runtime.subscribe` |
|
|
19
|
+
| 维护映射后的集合数据 | `createCollectionView` |
|
|
20
|
+
| 维护聚合或索引 | `createMaterializedView` |
|
|
21
|
+
| 在 React 中读取 | `useDocumentSelector`、`useReadable` 或 `useReadable` |
|
|
22
|
+
|
|
23
|
+
不要再维护一份可写的文档副本。`createDocument` 是 canonical state 的唯一所有者。
|
|
24
|
+
|
|
25
|
+
## 从 schema 开始
|
|
26
|
+
|
|
27
|
+
schema 同时定义文档的 TypeScript 结构,以及 writer、operation、selector 和 subscription 共用的权威地址模型。只定义一次,并让它靠近所描述的领域。
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createDocument, field, object, schema, table } from 'doxum';
|
|
31
|
+
|
|
32
|
+
const task = object({
|
|
33
|
+
title: field<string>(),
|
|
34
|
+
completed: field<boolean>(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const taskSchema = schema({
|
|
38
|
+
title: field<string>(),
|
|
39
|
+
tasks: table(task),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const runtime = createDocument({
|
|
43
|
+
schema: taskSchema,
|
|
44
|
+
initial: {
|
|
45
|
+
title: 'Launch Doxum',
|
|
46
|
+
tasks: {
|
|
47
|
+
ids: ['write-guide'],
|
|
48
|
+
byId: {
|
|
49
|
+
'write-guide': { title: 'Write the guide', completed: false },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`table` 通过 `{ ids, byId }` 保留应用可见的顺序;`map` 存储无顺序的 id 索引实体。单个结构化实体用 `single`,标量键值数据用 `dict` 或 `record`,带应用稳定 key 的有序序列用 `list`,经过校验的单根层级结构用 `tree`。建模取舍见 [patterns.zh-CN.md](patterns.zh-CN.md)。
|
|
57
|
+
|
|
58
|
+
## 通过 reader 读取
|
|
59
|
+
|
|
60
|
+
runtime 不会暴露可变的 canonical document,应通过回调读取:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { select } from 'doxum';
|
|
64
|
+
|
|
65
|
+
const openTitles = select(runtime, read =>
|
|
66
|
+
read.tasks.ids().flatMap(id => {
|
|
67
|
+
const task = read.tasks.get(id);
|
|
68
|
+
return task && !task.completed.get() ? [task.title.get()] : [];
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
reader 的形状由 schema 决定:
|
|
74
|
+
|
|
75
|
+
- field 使用 `get()`。
|
|
76
|
+
- table 或 map 使用 `ids()`、`has(id)` 和 `get(id)`。
|
|
77
|
+
- list 使用 `values()`、`length()` 和 `at(index)`。
|
|
78
|
+
- tree 使用 `rootId()`、`has(id)`、`value(id)`、`parent(id)` 和 `children(id)`。
|
|
79
|
+
|
|
80
|
+
结构化 reader 返回 snapshot。不要在 `runtime.update` 返回后保留 transaction reader;它只在该回调期间有效。
|
|
81
|
+
|
|
82
|
+
## 原子地修改
|
|
83
|
+
|
|
84
|
+
本地、类型化的领域行为使用 `runtime.update`。回调获得短生命周期的 `tx.read` 和 `tx.write`。writer 在一个原子 session 中产生 operation,而不是直接暴露 canonical state 的写入。
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const result = runtime.update(tx => {
|
|
88
|
+
const task = tx.read.tasks.get('write-guide');
|
|
89
|
+
if (!task) {
|
|
90
|
+
tx.reject({
|
|
91
|
+
source: 'application',
|
|
92
|
+
code: 'task-not-found',
|
|
93
|
+
message: 'The requested task no longer exists.',
|
|
94
|
+
address: ['tasks', 'write-guide'],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
tx.write.tasks.item('write-guide').completed.set(true);
|
|
99
|
+
return task.title.get();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (result.status === 'committed') {
|
|
103
|
+
console.log(result.value, result.commit.revision);
|
|
104
|
+
} else if (result.status === 'rejected') {
|
|
105
|
+
console.error(result.issues);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
一次 update 是同步且原子的:
|
|
110
|
+
|
|
111
|
+
- writer 产生语义上无效的 operation 时,Doxum 回滚整个 session,并以 `MutationIssue` 返回 `status: 'rejected'`。
|
|
112
|
+
- `tx.reject(...)` 回滚并返回应用层的 `DocumentDiagnostic`。
|
|
113
|
+
- 普通 `throw` 同样回滚,但错误会继续抛给调用方。
|
|
114
|
+
- 净变化为零时,返回 `status: 'unchanged'`,且不会发布 commit。
|
|
115
|
+
|
|
116
|
+
非阻断的应用诊断使用 `tx.report(...)`。committed 与 unchanged 结果通过 `reports` 暴露它们;report 与诊断地址在发布前会被复制并冻结。
|
|
117
|
+
|
|
118
|
+
## 用 writer,而不是局部拼 operation
|
|
119
|
+
|
|
120
|
+
普通应用行为优先调用 writer API;它更清晰,也能保留 schema 的领域语义:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
runtime.update(tx => {
|
|
124
|
+
tx.write.title.set('Ship Doxum');
|
|
125
|
+
tx.write.tasks.create(
|
|
126
|
+
{ id: 'release', value: { title: 'Publish the package', completed: false } },
|
|
127
|
+
{ after: 'write-guide' }
|
|
128
|
+
);
|
|
129
|
+
tx.write.tasks.item('release').title.set('Publish doxum');
|
|
130
|
+
tx.write.tasks.move('release', { at: 'start' });
|
|
131
|
+
tx.write.tasks.remove('write-guide');
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
table 支持 `create`、`item`、`remove` 和 `move`。map 与之相同,但没有 `move`,因为它无顺序。list 支持 `insert`、`move`、`remove` 与 `replace`;其身份来自 schema 中的 `keyOf`。完整的集合与 tree 模式见 [patterns.zh-CN.md](patterns.zh-CN.md)。
|
|
136
|
+
|
|
137
|
+
## 在边界回放 operation
|
|
138
|
+
|
|
139
|
+
来自持久化、网络适配器或其他外部边界的 operation batch 应通过 `apply` 回放。Doxum 会在 mutation code 看到数据前解码未知 operation payload,按 schema 解析每个地址,并原子地执行整个 batch。
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const result = runtime.apply([{ type: 'field.set', at: ['title'], value: 'Restored title' }], {
|
|
143
|
+
source: 'remote',
|
|
144
|
+
history: false,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (result.status === 'rejected') {
|
|
148
|
+
// 文档与 revision 均保持不变。
|
|
149
|
+
console.error(result.issues);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
外部 operation 输入即便在 TypeScript 中看似合法,也应视作不可信。不要在 Doxum 之外再写一套 path parser,也不要以局部回放方式自行校验 operation。远端 commit 与每次 `replace` 都会建立新的基线,因此会使本地 undo/redo history 失效。
|
|
154
|
+
|
|
155
|
+
## 理解结果与 history
|
|
156
|
+
|
|
157
|
+
所有 mutation 入口都会返回三种状态之一:
|
|
158
|
+
|
|
159
|
+
| 状态 | 含义 |
|
|
160
|
+
| ----------- | ----------------------------------------- |
|
|
161
|
+
| `committed` | canonical state 已变化;结果含有 commit。 |
|
|
162
|
+
| `unchanged` | 净状态未变化;revision 不变。 |
|
|
163
|
+
| `rejected` | 整个 batch 已回滚;检查 `issues`。 |
|
|
164
|
+
|
|
165
|
+
已提交的 operation 包含 forward operation、inverse operation、revision 和 `DocumentImpact`。本地与 system commit 默认会记录到 local history。使用 `runtime.history.undo()` 与 `runtime.history.redo()`;它们仍通过同一 mutation pipeline 回放 inverse 或 forward batch。
|
|
166
|
+
|
|
167
|
+
committed 结果中的 `observerErrors` 是 canonical state 和 history 已稳定后,processor、flush 或 listener 发生的失败。它们不是 mutation 失败,调用方不能因此重复写入。
|
|
168
|
+
|
|
169
|
+
## 通过 schema 所有的 target 订阅
|
|
170
|
+
|
|
171
|
+
从 schema 创建稳定 selector,再订阅 selector。这是整个 runtime 共用的 address 与 impact 模型。
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const title = taskSchema.value(path => path.title);
|
|
175
|
+
const tasks = taskSchema.collection(path => path.tasks);
|
|
176
|
+
|
|
177
|
+
const stopTitle = runtime.subscribe(title, commit => {
|
|
178
|
+
console.log('title changed at revision', commit.revision);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const stopTasks = runtime.subscribe(tasks, commit => {
|
|
182
|
+
const change = commit.impact.collection(tasks);
|
|
183
|
+
if (change.kind === 'incremental') {
|
|
184
|
+
console.log(change.added, change.removed, change.updated, change.orderChanged);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
stopTitle();
|
|
189
|
+
stopTasks();
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
value selector 使用 `commit.impact.affects(target)`;table 或 map selector 使用 `commit.impact.collection(selector)`,它在增量更新时返回精确变更,在 replace 后返回 `reset`。不要在应用模块中重写 path 比较 helper。
|
|
193
|
+
|
|
194
|
+
## 构建派生读模型
|
|
195
|
+
|
|
196
|
+
`createCollectionView` 将一个 table 或 map 映射为稳定的 ids、缓存的 `item(id)` readable 和惰性 `all` 数组;在可能的情况下只更新受影响的条目。
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { createCollectionView } from 'doxum';
|
|
200
|
+
|
|
201
|
+
const taskTitles = createCollectionView({
|
|
202
|
+
runtime,
|
|
203
|
+
source: taskSchema.collection(path => path.tasks),
|
|
204
|
+
map: (_id, task) => task.title.get(),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
taskTitles.item('write-guide').current();
|
|
208
|
+
taskTitles.all.current();
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
需要自定义增量逻辑的索引或聚合使用 `createMaterializedView`。materialized view 是派生状态,不是由调用方手工同步的 cache;它只能依赖同一 runtime 中更早创建的 materialized view。每个 view 都要由所属功能在销毁时释放。
|
|
212
|
+
|
|
213
|
+
## React 集成
|
|
214
|
+
|
|
215
|
+
`doxum/react` 基于 `useSyncExternalStore` 与 Doxum 读取依赖。组件只会在 commit 可能影响其 selector 读取结果时重新渲染。
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
import { useDocumentSelector } from 'doxum/react';
|
|
219
|
+
|
|
220
|
+
function OpenTaskCount() {
|
|
221
|
+
const count = useDocumentSelector(
|
|
222
|
+
runtime,
|
|
223
|
+
read => read.tasks.ids().filter(id => !read.tasks.get(id)?.completed.get()).length
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return <output>{count}</output>;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`Readable` 使用 `useReadable(view.all)`,单个 keyed value 使用 `useReadable(view.item(id))`,undo/redo state 与 action 使用 `useHistory(runtime.history)`。`core` 必须保持不导入 React;React 相关代码应属于 adapter 或应用层。
|
|
231
|
+
|
|
232
|
+
## 生命周期与所有权
|
|
233
|
+
|
|
234
|
+
- `createDocument` 启动时会克隆 initial document。
|
|
235
|
+
- 经由 operation 传入的结构化 payload 会转移到 canonical state。除非有意修改 canonical state,否则调用后不要再修改它。
|
|
236
|
+
- 已发布的 commit、history payload、diagnostic 和 selector address 都是不可变 snapshot。
|
|
237
|
+
- tree 的 replace snapshot 会经过校验并克隆,以维持结构完整性。
|
|
238
|
+
- runtime 不再使用时调用 `runtime.dispose()`;现有 subscription、history state 和 view 应随其所属对象一同 dispose。
|
|
239
|
+
|
|
240
|
+
决策规则与反模式见 [invariants.zh-CN.md](invariants.zh-CN.md),可直接复用的实现模式见 [patterns.zh-CN.md](patterns.zh-CN.md)。
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Doxum Runtime Invariants
|
|
2
|
+
|
|
3
|
+
Read this reference before changing or reviewing mutation, addressing, impact,
|
|
4
|
+
notifications, history, trees, or projections. These are design boundaries,
|
|
5
|
+
not optional style preferences.
|
|
6
|
+
|
|
7
|
+
## One canonical write authority
|
|
8
|
+
|
|
9
|
+
`createDocument` owns canonical mutable document state. New mutation behavior
|
|
10
|
+
must pass through `runtime.update`, `runtime.apply`, or `runtime.replace`.
|
|
11
|
+
Writers emit operations into a mutation session; they are not a public escape
|
|
12
|
+
hatch to mutate an object.
|
|
13
|
+
|
|
14
|
+
Never add:
|
|
15
|
+
|
|
16
|
+
- a parallel writable document cache;
|
|
17
|
+
- a reducer that edits document state outside the runtime;
|
|
18
|
+
- a view that callers manually keep synchronized; or
|
|
19
|
+
- an operation execution path that bypasses decode, normalization, inverse
|
|
20
|
+
recording, rollback, impact, history, or notification.
|
|
21
|
+
|
|
22
|
+
## Transaction lifetime and atomicity
|
|
23
|
+
|
|
24
|
+
A transaction callback is synchronous. Its reader and writer objects are valid
|
|
25
|
+
only while it executes. An `async` callback, a retained reader/writer, nested
|
|
26
|
+
write, or write during notification violates the runtime boundary.
|
|
27
|
+
|
|
28
|
+
Every session is atomic. If one operation is rejected after prior operations
|
|
29
|
+
have changed state, all earlier work in that session must roll back. A callback
|
|
30
|
+
throw also rolls back, then the original error is rethrown. Expected rejection
|
|
31
|
+
is a returned result, not an exception protocol.
|
|
32
|
+
|
|
33
|
+
## Keep engine and application problems distinct
|
|
34
|
+
|
|
35
|
+
| Problem | Owner | Result shape | Correct response |
|
|
36
|
+
| -------------------------------------------------------- | ---------------- | ------------------------------------------------- | ------------------------------------------------------- |
|
|
37
|
+
| Malformed, unresolved, or semantically invalid operation | Doxum engine | `MutationIssue` with `source: 'mutation'` | Inspect a `rejected` operation/transaction result. |
|
|
38
|
+
| Application business rule or validation | Application | `DocumentDiagnostic` with `source: 'application'` | Use `tx.report` or `tx.reject`. |
|
|
39
|
+
| Callback defect or unexpected failure | Application code | thrown error after rollback | Fix or handle the exception outside the transaction. |
|
|
40
|
+
| Processor, flush, or listener failure | Observer | `observerErrors` on a committed result | Repair the observer; do not replay the committed write. |
|
|
41
|
+
|
|
42
|
+
Do not create another generic `invalid` status, stringify every error into one
|
|
43
|
+
shape, or turn notification failures into mutation rejection. Mutation issue
|
|
44
|
+
codes are a closed public vocabulary and must remain exact.
|
|
45
|
+
|
|
46
|
+
## Schema owns addressing and selectors
|
|
47
|
+
|
|
48
|
+
The schema defines legal semantic addresses for operations and selectors.
|
|
49
|
+
Address resolution combines schema structure and current document state, which
|
|
50
|
+
is necessary for collection entries and variant branches.
|
|
51
|
+
|
|
52
|
+
Create long-lived targets with `schema.value(...)` and
|
|
53
|
+
`schema.collection(...)`. Use `runtime.address` for the runtime's address
|
|
54
|
+
domain. Use the exported `target` namespace for target identity and bucketing.
|
|
55
|
+
Do not add string-path parsers, another address type, custom selector IDs, or
|
|
56
|
+
separate impact-target equality helpers.
|
|
57
|
+
|
|
58
|
+
## Operations follow one pipeline
|
|
59
|
+
|
|
60
|
+
External operation input follows this order:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
decode -> normalize -> resolve -> execute -> inverse + journal -> publish
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`apply` is the boundary for untrusted operation envelopes. The batch must be
|
|
67
|
+
decoded before executor code sees it, normalized to one canonical operation
|
|
68
|
+
shape, resolved against the schema, and either fully published or fully rolled
|
|
69
|
+
back. All committed operations need exact inverse data and exact impact.
|
|
70
|
+
|
|
71
|
+
Local application behavior should use writers, not manually assembled operation
|
|
72
|
+
objects. Direct operation construction is appropriate for boundary adapters,
|
|
73
|
+
fixtures, migrations, and intentional replay.
|
|
74
|
+
|
|
75
|
+
## Ownership is explicit
|
|
76
|
+
|
|
77
|
+
- `initial` is cloned before it becomes canonical state.
|
|
78
|
+
- Structural payloads in ordinary operations are transferred into canonical
|
|
79
|
+
state. A caller that mutates one after submission may mutate canonical data.
|
|
80
|
+
- Commit and history operation payloads are immutable snapshots.
|
|
81
|
+
- Published diagnostics and selector addresses are copied and frozen.
|
|
82
|
+
- Tree replacement snapshots are validated and cloned.
|
|
83
|
+
|
|
84
|
+
Do not promise deep immutability where Doxum intentionally transfers a payload.
|
|
85
|
+
When a caller needs to retain mutable ownership, clone it before submission.
|
|
86
|
+
|
|
87
|
+
## Tree integrity is whole-document integrity
|
|
88
|
+
|
|
89
|
+
Every present tree must be empty or have exactly one root, complete
|
|
90
|
+
reachability from that root, no cycles, no duplicate child references, and
|
|
91
|
+
reciprocal parent/child relationships. This is checked for initial state and
|
|
92
|
+
replacement snapshots. Local tree operations preserve it incrementally.
|
|
93
|
+
|
|
94
|
+
Do not accept disconnected forests, orphan nodes, a non-root moved to no
|
|
95
|
+
parent, a root re-parented below another node, or direct edits to the tree's
|
|
96
|
+
internal records. A rejected tree operation leaves the entire transaction
|
|
97
|
+
unchanged.
|
|
98
|
+
|
|
99
|
+
## Impact and notification describe committed state
|
|
100
|
+
|
|
101
|
+
Every commit publishes a `DocumentImpact`; it is not a mutable change log for
|
|
102
|
+
callers to edit. Value impact answers `affects(target)`. Collection impact
|
|
103
|
+
reports exact added, removed, updated, and ordering changes, or `reset` after a
|
|
104
|
+
replacement/subtree reset.
|
|
105
|
+
|
|
106
|
+
Notification order is observable behavior:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
commit -> materialized processors -> processor flushes -> targeted listeners -> root listeners
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Writes are forbidden during the update and notification windows. Processor,
|
|
113
|
+
flush, and listener errors are captured while the committed document, revision,
|
|
114
|
+
and history stay settled.
|
|
115
|
+
|
|
116
|
+
## Derived state is declared, not synchronized by callers
|
|
117
|
+
|
|
118
|
+
`CollectionView` derives one declared collection and incrementally maintains
|
|
119
|
+
ids, keyed values, and a lazy aggregate array. `MaterializedView` derives one
|
|
120
|
+
value from document reads, tracked impact dependencies, and optional earlier
|
|
121
|
+
materialized sources. A materialized view can only depend on views of the same
|
|
122
|
+
runtime that were created before it.
|
|
123
|
+
|
|
124
|
+
Do not cache derived values inside canonical document state unless they are
|
|
125
|
+
real domain data. Do not have UI code manually feed changes into a view. Dispose
|
|
126
|
+
views and subscriptions with their owner.
|
|
127
|
+
|
|
128
|
+
## Framework and product boundaries
|
|
129
|
+
|
|
130
|
+
`core` is framework-neutral. `doxum/react` is a one-way adapter from core to
|
|
131
|
+
React; core must not import React or UI concepts. Doxum intentionally does not
|
|
132
|
+
decide persistence formats, network synchronization, authorization, retry,
|
|
133
|
+
acknowledgement, ordering, or conflict resolution. An application must make
|
|
134
|
+
those decisions before applying operations or replacing a snapshot.
|
|
135
|
+
|
|
136
|
+
## Change checklist
|
|
137
|
+
|
|
138
|
+
Before handing off a change that touches the runtime:
|
|
139
|
+
|
|
140
|
+
- Does every canonical write still flow through `createDocument`?
|
|
141
|
+
- Are success, rejected rollback, inverse history, and impact/subscription
|
|
142
|
+
behavior tested where applicable?
|
|
143
|
+
- Are tree and collection paths free of accidental whole-document copying or
|
|
144
|
+
traversal?
|
|
145
|
+
- Do public lifecycle changes update the README and architecture guide?
|
|
146
|
+
- Are obsolete protocol types, local helpers, and duplicate address/target
|
|
147
|
+
interpretations removed rather than preserved for compatibility?
|