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.
Files changed (70) hide show
  1. package/README.md +217 -296
  2. package/dist/contract-BNStLbSE.d.ts +441 -0
  3. package/dist/contract-CIU5FCC1.d.cts +441 -0
  4. package/dist/driver-BlR81Dqg.js +200 -0
  5. package/dist/driver-BlR81Dqg.js.map +1 -0
  6. package/dist/driver-xOIkwrB8.cjs +241 -0
  7. package/dist/driver-xOIkwrB8.cjs.map +1 -0
  8. package/dist/index.cjs +666 -2222
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +10 -20
  11. package/dist/index.d.ts +10 -18
  12. package/dist/index.js +634 -2189
  13. package/dist/index.js.map +1 -1
  14. package/dist/{integration-CZwCwFBS.cjs → integration-C87tjRop.cjs} +4 -9
  15. package/dist/integration-C87tjRop.cjs.map +1 -0
  16. package/dist/{integration-B56u1l9V.js → integration-D5XCBLJ8.js} +2 -7
  17. package/dist/integration-D5XCBLJ8.js.map +1 -0
  18. package/dist/integration.cjs +11 -11
  19. package/dist/integration.d.cts +10 -4
  20. package/dist/integration.d.ts +10 -4
  21. package/dist/integration.js +4 -3
  22. package/dist/issue-DVaGQGeP.js +576 -0
  23. package/dist/issue-DVaGQGeP.js.map +1 -0
  24. package/dist/issue-DhrNdQNg.cjs +797 -0
  25. package/dist/issue-DhrNdQNg.cjs.map +1 -0
  26. package/dist/local-sync.cjs +67 -39
  27. package/dist/local-sync.cjs.map +1 -1
  28. package/dist/local-sync.d.cts +10 -10
  29. package/dist/local-sync.d.ts +10 -10
  30. package/dist/local-sync.js +66 -38
  31. package/dist/local-sync.js.map +1 -1
  32. package/dist/react.cjs +23 -4
  33. package/dist/react.cjs.map +1 -1
  34. package/dist/react.d.cts +8 -3
  35. package/dist/react.d.ts +8 -3
  36. package/dist/react.js +21 -6
  37. package/dist/react.js.map +1 -1
  38. package/dist/store-1Uob0Ghk.cjs +2750 -0
  39. package/dist/store-1Uob0Ghk.cjs.map +1 -0
  40. package/dist/store-CD0KdGsq.d.cts +262 -0
  41. package/dist/store-D7QH6Rzw.js +2511 -0
  42. package/dist/store-D7QH6Rzw.js.map +1 -0
  43. package/dist/store-cp5CpfCy.d.ts +262 -0
  44. package/package.json +1 -1
  45. package/skills/doxum-runtime/SKILL.md +26 -44
  46. package/skills/doxum-runtime/references/guide.en.md +95 -359
  47. package/skills/doxum-runtime/references/guide.zh-CN.md +80 -285
  48. package/skills/doxum-runtime/references/invariants.en.md +42 -165
  49. package/skills/doxum-runtime/references/invariants.zh-CN.md +34 -97
  50. package/skills/doxum-runtime/references/patterns.en.md +73 -225
  51. package/skills/doxum-runtime/references/patterns.zh-CN.md +73 -179
  52. package/dist/chunk-pbuEa-1d.js +0 -13
  53. package/dist/contract-DNZ4D53r.d.ts +0 -563
  54. package/dist/contract-j3SLGAwh.d.cts +0 -563
  55. package/dist/driver-CNxqMVFH.cjs +0 -69
  56. package/dist/driver-CNxqMVFH.cjs.map +0 -1
  57. package/dist/driver-CbzfW5MR.js +0 -46
  58. package/dist/driver-CbzfW5MR.js.map +0 -1
  59. package/dist/integration-B56u1l9V.js.map +0 -1
  60. package/dist/integration-CZwCwFBS.cjs.map +0 -1
  61. package/dist/ownership-CY0nPXGF.cjs +0 -304
  62. package/dist/ownership-CY0nPXGF.cjs.map +0 -1
  63. package/dist/ownership-CduRygE7.js +0 -245
  64. package/dist/ownership-CduRygE7.js.map +0 -1
  65. package/dist/runtime-0mOFbe_H.cjs +0 -2439
  66. package/dist/runtime-0mOFbe_H.cjs.map +0 -1
  67. package/dist/runtime-B22tuj9A.d.cts +0 -150
  68. package/dist/runtime-BFmhzPpZ.d.ts +0 -150
  69. package/dist/runtime-DF1q9Gje.js +0 -2129
  70. package/dist/runtime-DF1q9Gje.js.map +0 -1
@@ -1,293 +1,88 @@
1
- # Doxum Runtime 使用指南
1
+ # Doxum 使用指南
2
2
 
3
- 这是 Doxum 面向任务的公开使用指南。它说明如何借助 `doxum` 建模、读取、修改、观察和派生一个内存文档,如何通过 `doxum/local-sync` 持久化并同步同一浏览器 origin 的文档,以及如何通过 `doxum/react` 将读模型接入 React。
4
-
5
- `doxum` core 负责类型化文档状态、原子修改、历史记录、影响范围、订阅和派生视图。`doxum/local-sync` 是可选的浏览器适配器,提供 IndexedDB 离线持久化和同一 origin 的跨标签页同步。网络顺序、授权和冲突解决仍由应用负责。
6
-
7
- ## 先选择正确入口
8
-
9
- | 目标 | 使用方式 |
10
- | -------------------------- | ----------------------------------------------------- |
11
- | 定义文档结构 | `schema`、`field`、`object` 和集合构造器 |
12
- | 创建 canonical runtime | `createDocument` |
13
- | 持久化并同步一个浏览器文档 | 从 `doxum/local-sync` 导入 `attachLocalSync` |
14
- | 一次性读取 | `select(runtime, read => ...)` |
15
- | 执行本地业务修改 | `runtime.update(tx => ...)` |
16
- | 回放持久化或远端 operation | `runtime.apply(operations, options)` |
17
- | 整体替换可信快照 | `runtime.replace(document, options)` |
18
- | 监听一个 schema 位置 | `schema.value` + `runtime.subscribe` |
19
- | 监听 table 或 map | `schema.collection` + `runtime.subscribe` |
20
- | 维护映射后的集合数据 | `projection.map` |
21
- | 维护聚合或索引 | `projection.value / projection.collection` |
22
- | 在 React 中读取 | `useDocumentSelector`、`useReadable` 或 `useReadable` |
23
-
24
- 不要再维护一份可写的文档副本。`createDocument` 是 canonical state 的唯一所有者。
25
-
26
- ## 从 schema 开始
27
-
28
- schema 同时定义文档的 TypeScript 结构,以及 writer、operation、selector 和 subscription 共用的权威地址模型。只定义一次,并让它靠近所描述的领域。
29
-
30
- ```ts
31
- import { createDocument, field, object, schema, table } from 'doxum';
32
-
33
- const task = object({
34
- title: field<string>(),
35
- completed: field<boolean>(),
36
- });
37
-
38
- const taskSchema = schema({
39
- title: field<string>(),
40
- tasks: table(task),
41
- });
42
-
43
- const runtime = createDocument({
44
- schema: taskSchema,
45
- initial: {
46
- title: 'Launch Doxum',
47
- tasks: {
48
- ids: ['write-guide'],
49
- byId: {
50
- 'write-guide': { title: 'Write the guide', completed: false },
51
- },
52
- },
53
- },
54
- });
55
- ```
56
-
57
- `table` 通过 `{ ids, byId }` 保留应用可见的顺序;`map` 存储无顺序的 id 索引实体。单个结构化实体用 `object`,标量键值数据用 `dict`,带应用稳定 key 的有序序列用 `list`,经过校验的单根层级结构用 `tree`。建模取舍见 [patterns.zh-CN.md](patterns.zh-CN.md)。
58
-
59
- ## 通过 reader 读取
60
-
61
- runtime 不会暴露可变的 canonical document,应通过回调读取:
62
-
63
- ```ts
64
- import { select } from 'doxum';
65
-
66
- const openTitles = select(runtime, read =>
67
- read.tasks.ids().flatMap(id => {
68
- const task = read.tasks.get(id);
69
- return task && !task.completed.get() ? [task.title.get()] : [];
70
- })
71
- );
72
- ```
73
-
74
- reader 的形状由 schema 决定:
75
-
76
- - field 使用 `get()`。
77
- - table 或 map 使用 `ids()`、`has(id)` 和 `get(id)`。
78
- - list 使用 `values()`、`length()` 和 `at(index)`。
79
- - tree 使用 `rootId()`、`has(id)`、`value(id)`、`parent(id)` 和 `children(id)`。
80
-
81
- 结构化 reader 返回 snapshot。不要在 `runtime.update` 返回后保留 transaction reader;它只在该回调期间有效。
82
-
83
- ## 原子地修改
84
-
85
- 本地、类型化的领域行为使用 `runtime.update`。回调获得短生命周期的 `tx.read` 和 `tx.write`。writer 在一个原子 session 中产生 operation,而不是直接暴露 canonical state 的写入。
86
-
87
- ```ts
88
- const result = runtime.update(tx => {
89
- const task = tx.read.tasks.get('write-guide');
90
- if (!task) {
91
- tx.reject({
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
- optional 的 field、variant、dict、list 和 tree writer 提供 `clear()`;其中结构化叶子也可
138
- 在当前缺失时直接调用 `replace()` 建立值。schema 构造器和类型均拒绝 optional object、table 和 map,
139
- 这些容器应保持存在,通过子 writer 或集合操作修改。variant reader 的 get() 返回判别联合;
140
- dict reader 提供 get(key)、has(key)、keys() 和 values(),list 也支持 get(key)/has(key)。
141
-
142
- 一次动作跨越多个 update 时,打开 `runtime.history.group()`,结束后调用 end() 保留一个撤销项,
143
- 或调用 cancel() 撤销整次动作。group 不能嵌套。history 实现 Readable,localSync.state 也是
144
- Readable,两者都可以直接接入 useReadable 和 projection.fromReadable。
145
-
146
- ## 在边界回放 operation
147
-
148
- 来自持久化、网络适配器或其他外部边界的 operation batch 应通过 `apply` 回放。Doxum 会在 mutation code 看到数据前解码未知 operation payload,按 schema 解析每个地址,并原子地执行整个 batch。
149
-
150
- ```ts
151
- const result = runtime.apply([{ type: 'field.set', at: ['title'], value: 'Restored title' }], {
152
- source: 'remote',
153
- history: false,
154
- });
155
-
156
- if (result.status === 'rejected') {
157
- // 文档与 revision 均保持不变。
158
- console.error(result.issues);
159
- }
160
- ```
161
-
162
- 外部 operation 输入即便在 TypeScript 中看似合法,也应视作不可信。不要在 Doxum 之外再写一套 path parser,也不要以局部回放方式自行校验 operation。远端 commit 与每次 `replace` 都会建立新的基线,因此会使本地 undo/redo history 失效。
163
-
164
- ## 理解结果与 history
165
-
166
- 所有 mutation 入口都会返回三种状态之一:
167
-
168
- | 状态 | 含义 |
169
- | ----------- | ----------------------------------------- |
170
- | `committed` | canonical state 已变化;结果含有 commit。 |
171
- | `unchanged` | 净状态未变化;revision 不变。 |
172
- | `rejected` | 整个 batch 已回滚;检查 `issues`。 |
173
-
174
- 已提交的 operation 包含 forward operation、inverse operation、revision 和 `DocumentImpact`。本地与 system commit 默认会记录到 local history。使用 `runtime.history.undo()` 与 `runtime.history.redo()`;它们仍通过同一 mutation pipeline 回放 inverse 或 forward batch。
175
-
176
- committed 结果中的 `observerErrors` 是 canonical state 和 history 已稳定后,processor、flush 或 listener 发生的失败。它们不是 mutation 失败,调用方不能因此重复写入。
177
-
178
- ## 附着浏览器本地同步
179
-
180
- `attachLocalSync` 是可选的浏览器 attachment。runtime 仍由应用创建并持有;在使用
181
- 文档前必须等待 attach 完成。它会从 IndexedDB 的 checkpoint 与 append-only command
182
- tail 恢复传入 runtime,随后以 Web Lock 为一个 document 选出唯一可写标签页。`leader`
183
- 可继续使用同步的 runtime 写入 API;其他已附着标签页都是只读的 `follower` mirror。
184
- follower 直接调用 `update`、`prepare`、`apply` 或 `replace` 会抛出
185
- `LocalSyncReadOnlyError`。
186
-
187
- leader 已完成的 local、system 与 history commit 会被监听,并按顺序异步写入
188
- IndexedDB。BroadcastChannel 只传递新的 head 提示;follower 从 IndexedDB 补读 durable
189
- tail,并以 `remote` source 顺序 apply,因此其内存 history 会失效。leader dispose
190
- 之后,已经 catch-up 的 follower 会接管锁并成为新的 leader。
191
-
192
- local-sync 只持久化 operation command,不接受任意的新基线。附着期间,
193
- `runtime.replace()` 与由应用提供的 `runtime.apply(..., { source: 'remote' })` 会抛出
194
- `LocalSyncUnsupportedOperationError`;内部 hydration 与 tail replay 使用受信任的
195
- attachment path。
196
-
197
- ```ts
198
- import { attachLocalSync } from 'doxum/local-sync';
199
-
200
- const runtime = createDocument({ schema: taskSchema, initial });
201
- const localSync = await attachLocalSync({
202
- runtime,
203
- database: 'my-app',
204
- documentId: 'project-1',
205
- });
206
-
207
- if (localSync.state.current().status === 'leader') {
208
- runtime.update(tx => tx.write.title.set('Ship Doxum'));
209
- runtime.history.undo();
210
- }
211
-
212
- await localSync.flush(); // 持久化已观察到的 leader command,或让 follower 追赶
213
- await localSync.dispose();
214
- ```
215
-
216
- 开始使用前必须等待 attachment,因为它会用 IndexedDB 恢复传入 runtime。这是“同步可见、
217
- 异步持久化”而不是严格 durable:崩溃、存储失败或非 JSON payload,都可能让已经可见的
218
- leader commit 未持久化。用 `localSync.state.current()` 与 `onError` 显示该状态,`flush()` 是显式
219
- 的持久化/追赶边界。attachment 不拥有也不会 dispose runtime,且不提供另一套 undo API:
220
- leader 使用 `runtime.history.undo()` 与 `runtime.history.redo()`。history 有意只在内存中
221
- 存在;attachment 恢复及 remote tail apply 都会使它失效,因此不会跨重开或 leader 交接保留。
222
-
223
- ## 通过 schema 所有的 target 订阅
224
-
225
- 从 schema 创建稳定 selector,再订阅 selector。这是整个 runtime 共用的 address 与 impact 模型。
3
+ ## 定义、修改与读取
226
4
 
227
5
  ```ts
228
- const title = taskSchema.value(path => path.title);
229
- const tasks = taskSchema.collection(path => path.tasks);
230
-
231
- const stopTitle = runtime.subscribe(title, commit => {
232
- console.log('title changed at revision', commit.revision);
6
+ import { createDocument, field, map, object, select, snapshot, type Infer } from 'doxum';
7
+
8
+ const task = object({ title: field<string>(), done: field<boolean>() });
9
+ const model = object({ tasks: map(task) });
10
+ type DocumentValue = Infer<typeof model>;
11
+ const document = createDocument({
12
+ schema: model,
13
+ initial: { tasks: { a: { title: 'Write', done: false } } },
233
14
  });
234
-
235
- const stopTasks = runtime.subscribe(tasks, commit => {
236
- const change = commit.impact.collection(tasks);
237
- if (change.kind === 'incremental') {
238
- console.log(change.added, change.removed, change.updated, change.orderChanged);
239
- }
15
+ document.update(draft => {
16
+ const task = draft.tasks.a;
17
+ if (task) task.done = !task.done;
18
+ draft.tasks.b = { title: 'Review', done: false };
19
+ return { warnings: [] };
240
20
  });
241
-
242
- stopTitle();
243
- stopTasks();
244
- ```
245
-
246
- value selector 使用 `commit.impact.affects(target)`;table 或 map selector 使用 `commit.impact.collection(selector)`,它在增量更新时返回精确变更,在 replace 后返回 `reset`。不要在应用模块中重写 path 比较 helper。
247
-
248
- ## 构建派生读模型
249
-
250
- ```ts
251
- const projection = createProjectionRuntime({ onError: error => console.error(error) });
252
- const document = projection.document(runtime);
253
- const notes = document.collection(path => path.notes);
254
- const noteSummaries = projection.map(
255
- notes,
256
- (id, note) => ({ id, preview: note.body.get().slice(0, 80) }),
257
- { isEqual: (a, b) => a.id === b.id && a.preview === b.preview }
21
+ const done = select(document, state => state.tasks.a?.done);
22
+ const tasks = select(document, state => snapshot(state.tasks));
23
+ document.subscribe(
24
+ path => path.tasks.item('a').done,
25
+ commit => console.log(commit.changes)
258
26
  );
259
- const noteCount = projection.value({ notes }, ({ notes }) => notes.read.ids().length);
260
27
  ```
261
28
 
262
- projection.map 提供稳定的 ids/item readable 和惰性 all,也能接入上游投影集合。DocumentCollectionSource 可直接用于自定义 processor 的 sources;有状态算法使用 `projection.value({ sources, build }, { isEqual }?)` 或 `projection.collection<Item>()(spec)`。document collection context 提供整个批次的 `candidates.keys`、`candidates.orderDirty` 和 `reset`。不自动跟踪 keyed 依赖。sources 属于同一 projection owner,但可接入不同 document runtime。
263
-
264
- projection.input(initial, { isEqual }) 用于边界值;应用保留 set,processor 只接收 source。fromReadable 接入已有外部 source,不取得其生命周期所有权。多 source 同步更新应在首次 commit 前进入 batch。projection 随 service 销毁,React unmount 只取消订阅,disposed handle 明确抛错。
265
-
266
- ## React 集成
267
-
268
- `doxum/react` 基于 `useSyncExternalStore` 与 Doxum 读取依赖。组件只会在 commit 可能影响其 selector 读取结果时重新渲染。
269
-
270
- ```tsx
271
- import { useDocumentSelector } from 'doxum/react';
272
-
273
- function OpenTaskCount() {
274
- const count = useDocumentSelector(
275
- runtime,
276
- read => read.tasks.ids().filter(id => !read.tasks.get(id)?.completed.get()).length
277
- );
278
-
279
- return <output>{count}</output>;
280
- }
281
- ```
282
-
283
- `Readable` 使用 `useReadable(view.all)`,单个 keyed value 使用 `useReadable(view.item(id))`,undo/redo state 与 action 使用 `useHistory(runtime.history)`。`core` 必须保持不导入 React;React 相关代码应属于 adapter 或应用层。
284
-
285
- ## 生命周期与所有权
286
-
287
- - `createDocument` 启动时会克隆 initial document。
288
- - 经由 operation 传入的结构化 payload 会转移到 canonical state。除非有意修改 canonical state,否则调用后不要再修改它。
289
- - 已发布的 commit、history payload、diagnostic 和 selector address 都是不可变 snapshot。
290
- - tree 的 replace snapshot 会经过校验并克隆,以维持结构完整性。
291
- - runtime 不再使用时调用 `runtime.dispose()`;现有 subscription、history state 和 view 应随其所属对象一同 dispose。
292
-
293
- 决策规则与反模式见 [invariants.zh-CN.md](invariants.zh-CN.md),可直接复用的实现模式见 [patterns.zh-CN.md](patterns.zh-CN.md)。
29
+ 根 object 是定义身份,runtime 拥有状态与 revision。事务同步且原子,修改立即可读。
30
+ Draft 和内部 readWith 是同步回调内的借用视图,不得逃逸。预期拒绝抛 TransactionRejected;普通异常完整恢复后
31
+ 原样抛出。正常返回 false/undefined 是业务结果,不表示拒绝。
32
+
33
+ object 暴露可编辑成员;field 是原子值,包括对象和数组。Infer 与作用域内原子值深只读;
34
+ 输入、快照、commit 和 history 共享 payload 引用,删除后也不能通过任何别名修改它。
35
+ object/variant 结构只接受 schema 声明的自身属性及 variant 判别字段;额外的字符串、Symbol 和不可枚举属性都会被拒绝。
36
+ 动态键使用 map,任意对象内部数据使用 field;这个限制不检查 field 的 payload 内部。
37
+ snapshot 只复制 schema 结构;snapshot(rawPayload) 返回原始只读引用。
38
+ 发布数据不做运行时冻结,不再支持字段 copier。需要可修改副本或序列化时由应用边界显式处理。
39
+
40
+ Infer 保留 optional 属性和扁平 variant 联合。Read/Draft 带集合方法;
41
+ 含 table/list/tree 数据的整体替换使用 assign(scope, key, inferValue)。
42
+
43
+ ## 容器与校验
44
+
45
+ | 声明 | 数据 | Draft 方法 |
46
+ | -------------------------------- | ------------- | ------------------------------------------------------------- |
47
+ | map(valueSchema, { key }?) | record | 索引、赋值、delete |
48
+ | table(objectOrVariant, { key }?) | ids/byId | get/has/ids/create/remove/move |
49
+ | list(field, { keyOf }) | 普通数组 | get/has/ids/insert/set/remove/move/replace |
50
+ | tree(field) | rootId?/nodes | get/has/rootId/parent/children/insert/set/remove/move/replace |
51
+
52
+ Read 只暴露读取方法。map 支持 field/object/variant。list 替换项必须保持寻址键。
53
+ 简单数组与笔画可作为一个原子 field。optional 支持 field/variant/map/list/tree,
54
+ 缺失与存在的 undefined 不同。variant tag 只读,通过整体替换切换分支。
55
+
56
+ 校验器是纯同步函数或 Standard Schema v1,直接接收原始引用且不得修改它。
57
+ 成功返回值被忽略,不复制输入,也不深度检查转换;数据转换在进入 Doxum 前完成。
58
+ parse(model, unknown) 复制校验后的结构并共享只读 payload;严格解析要求原子字段具备校验器。品牌键贯穿 map/table 访问、
59
+ 符号路径和 impact。路径回调描述地址,包括缺失键,订阅注册时解析。
60
+ React useDocumentSelector 追踪实际读取,并在选择分支改变时更新依赖。
61
+
62
+ ## 变化与消费者
63
+
64
+ commit 包含 revision/source/changes/impact。ChangeSet 按所属容器分组,成员以
65
+ added/removed/updated 携带直接 before/after,不使用 Presence 包装。
66
+ 顺序变化放在同一 members 分组的可选 order.before/after 中;纯排序的 members 为空。
67
+ 不接受独立 order 记录或同地址重复分组,apply 逐组完成成员与顺序安装。
68
+ tree 保留拓扑语义,reset 显式表达整体根过渡;根成员分组仍是增量变化。
69
+ 净零事务不发布。
70
+ apply(changes, { expectedRevision }) 拒绝缺失或不匹配的本地基线;来包 before
71
+ 不作为本地 undo 真值,记录真实旧状态。history 重放完整 ChangeSet,分组原子旅行。
72
+ 本地 replace 是可撤销根重置,remote commit 使 history 失效。
73
+ observerErrors 属于已提交结果。
74
+
75
+ 集合增量映射使用 `project(document, path => path.tasks, mapper)`。
76
+ 纯派生值使用 `project(sources, compute)`;有状态算法使用带 kind 的 value
77
+ 或 collection spec。定义是惰性的,由 `createProjectionStore({ onError })`
78
+ 实例物化和管理。sources 仍然显式声明。
79
+ `input` 与 `project(readable)` 接入外部边界值。随所属服务 dispose store。
80
+ `store.batch` 推迟投影结算与通知,但不推迟文档提交和文档通知;内部读取上次发布值,
81
+ 不提供跨文档回滚。
82
+
83
+ doxum/local-sync 附着 IndexedDB 和 Web Lock 领导权,只有 leader 写入,
84
+ follower 连续重放 durable seq。先可见后异步落盘,flush 等待持久化。
85
+ 附着期间禁止外部 replace 和外部标记 remote 的 apply。版本 5 / 格式 3 拒绝旧存储,
86
+ 保留数据且不迁移。适配器仅接收 JSON 值,变化数量限制统计逻辑成员和树节点,不能按外层分组数绕过。
87
+ 限额仅约束新产生的本地提交;减小当前限额不影响既有持久化提交的读取。
88
+ 整个已发布 ChangeSet 都必须只读,其身份可复用结构校验结果,但不跳过本地 revision、schema 和真实 before 检查。
@@ -1,165 +1,42 @@
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. Canonical writes must
10
- pass through `runtime.update`, `runtime.apply`, or `runtime.replace`.
11
- `runtime.prepare` may use the same mutation session to produce a rolled-back,
12
- uncommitted operation batch for a durable adapter; it is not a second write
13
- path. Writers emit operations into a mutation session; they are not a public
14
- escape hatch to mutate an object.
15
-
16
- Never add:
17
-
18
- - a parallel writable document cache;
19
- - a reducer that edits document state outside the runtime;
20
- - a view that callers manually keep synchronized; or
21
- - an operation execution path that bypasses decode, normalization, inverse
22
- recording, rollback, impact, history, or notification.
23
-
24
- ## Transaction lifetime and atomicity
25
-
26
- A transaction callback is synchronous. Its reader and writer objects are valid
27
- only while it executes. An `async` callback, a retained reader/writer, nested
28
- write, or write during notification violates the runtime boundary.
29
-
30
- Every session is atomic. If one operation is rejected after prior operations
31
- have changed state, all earlier work in that session must roll back. A callback
32
- throw also rolls back, then the original error is rethrown. Expected rejection
33
- is a returned result, not an exception protocol.
34
-
35
- ## Keep engine and application problems distinct
36
-
37
- | Problem | Owner | Result shape | Correct response |
38
- | -------------------------------------------------------- | ---------------- | ------------------------------------------------- | ------------------------------------------------------- |
39
- | Malformed, unresolved, or semantically invalid operation | Doxum engine | `MutationIssue` with `source: 'mutation'` | Inspect a `rejected` operation/transaction result. |
40
- | Application business rule or validation | Application | `DocumentDiagnostic` with `source: 'application'` | Use `tx.report` or `tx.reject`. |
41
- | Callback defect or unexpected failure | Application code | thrown error after rollback | Fix or handle the exception outside the transaction. |
42
- | Processor, flush, or listener failure | Observer | `observerErrors` on a committed result | Repair the observer; do not replay the committed write. |
43
-
44
- Do not create another generic `invalid` status, stringify every error into one
45
- shape, or turn notification failures into mutation rejection. Mutation issue
46
- codes are a closed public vocabulary and must remain exact.
47
-
48
- ## Schema owns addressing and selectors
49
-
50
- The schema defines legal semantic addresses for operations and selectors.
51
- Address resolution combines schema structure and current document state, which
52
- is necessary for collection entries and variant branches.
53
-
54
- Create long-lived targets with `schema.value(...)` and
55
- `schema.collection(...)`. Use `runtime.address` for the runtime's address
56
- domain. Use the exported `target` namespace for target identity and bucketing.
57
- Do not add string-path parsers, another address type, custom selector IDs, or
58
- separate impact-target equality helpers.
59
-
60
- ## Operations follow one pipeline
61
-
62
- External operation input follows this order:
63
-
64
- ```text
65
- decode -> normalize -> resolve -> execute -> inverse + journal -> publish
66
- ```
67
-
68
- `apply` is the boundary for untrusted operation envelopes. The batch must be
69
- decoded before executor code sees it, normalized to one canonical operation
70
- shape, resolved against the schema, and either fully published or fully rolled
71
- back. All committed operations need exact inverse data and exact impact.
72
-
73
- Local application behavior should use writers, not manually assembled operation
74
- objects. Direct operation construction is appropriate for boundary adapters,
75
- fixtures, migrations, and intentional replay.
76
-
77
- ## Ownership is explicit
78
-
79
- - `initial` is cloned before it becomes canonical state.
80
- - Structural payloads in ordinary operations are transferred into canonical
81
- state. A caller that mutates one after submission may mutate canonical data.
82
- - Commit and history operation payloads are immutable snapshots.
83
- - Published diagnostics and selector addresses are copied and frozen.
84
- - Tree replacement snapshots are validated and cloned.
85
-
86
- Do not promise deep immutability where Doxum intentionally transfers a payload.
87
- When a caller needs to retain mutable ownership, clone it before submission.
88
-
89
- ## Tree integrity is whole-document integrity
90
-
91
- Every present tree must be empty or have exactly one root, complete
92
- reachability from that root, no cycles, no duplicate child references, and
93
- reciprocal parent/child relationships. This is checked for initial state and
94
- replacement snapshots. Local tree operations preserve it incrementally.
95
-
96
- Do not accept disconnected forests, orphan nodes, a non-root moved to no
97
- parent, a root re-parented below another node, or direct edits to the tree's
98
- internal records. A rejected tree operation leaves the entire transaction
99
- unchanged.
100
-
101
- ## Impact and notification describe committed state
102
-
103
- Every commit publishes a `DocumentImpact`; it is not a mutable change log for
104
- callers to edit. Value impact answers `affects(target)`. Collection impact
105
- reports exact added, removed, updated, and ordering changes, or `reset` after a
106
- replacement/subtree reset.
107
-
108
- Notification order is observable behavior:
109
-
110
- ```text
111
- commit -> materialized processors -> processor flushes -> targeted listeners -> root listeners
112
- ```
113
-
114
- Writes are forbidden during the update and notification windows. Processor,
115
- flush, and listener errors are captured while the committed document, revision,
116
- and history stay settled.
117
-
118
- ## Derived state is declared, not synchronized by callers
119
-
120
- ProjectionCollection and ProjectionValue share one explicit source graph and
121
- publication mechanism. Only scoped processors write derived output. Application
122
- code selects candidate keys from native impacts; Doxum owns final change,
123
- equality, revision and notification. Never expose mutable private indexes or
124
- retain scoped readers/writers. Updates must be synchronous.
125
-
126
- All affected nodes settle before listeners; failures discard staged output and
127
- attempt one fresh build before faulting. Faulted ancestors block descendants.
128
- Listener exceptions are isolated. Source writes are forbidden during graph
129
- processing and notification. Explicit batch defers graph publication, not
130
- canonical commits or document listeners, and never promises source rollback.
131
- Dispose the owner; do not dispose a node while it still has consumers.
132
-
133
- ## Framework and product boundaries
134
-
135
- `core` is framework-neutral. `doxum/react` is a one-way adapter from core to
136
- React; core must not import React or UI concepts. `doxum/local-sync` is an
137
- optional attachment to an application-owned runtime. It hydrates an IndexedDB
138
- checkpoint/tail, holds a document Web Lock for exactly one leader, and uses a
139
- small internal synchronous write policy to reject follower mutations without
140
- changing the runtime API. The leader observes completed local, system, and
141
- history commits, appends JSON operation batches asynchronously, and broadcasts
142
- only a head hint. Followers read and apply the durable tail as `remote`; this
143
- invalidates their history. There is no pending queue, rebase, actor history, or
144
- attachment-specific undo API. While attached, external `replace` and external
145
- `apply` marked `remote` are rejected because only operation commands are
146
- appendable; the attachment's hydration and replay lease is the one trusted
147
- exception. Local-sync does not promise strict durability;
148
- application code uses `state.current()`, `onError`, and `flush()` when it needs to
149
- observe persistence or catch-up. Doxum intentionally does not decide network
150
- synchronization, authorization, retry, acknowledgement, ordering, or conflict
151
- resolution. An application must make those decisions before applying operations
152
- or replacing a snapshot.
153
-
154
- ## Change checklist
155
-
156
- Before handing off a change that touches the runtime:
157
-
158
- - Does every canonical write still flow through `createDocument`?
159
- - Are success, rejected rollback, inverse history, and impact/subscription
160
- behavior tested where applicable?
161
- - Are tree and collection paths free of accidental whole-document copying or
162
- traversal?
163
- - Do public lifecycle changes update the README and architecture guide?
164
- - Are obsolete protocol types, local helpers, and duplicate address/target
165
- interpretations removed rather than preserved for compatibility?
1
+ # Runtime Invariants
2
+
3
+ 1. createDocument owns canonical state; Draft, apply and replace share MutationSession.
4
+ runtime owns one execution/rollback/seal/publish boundary, also used by history.
5
+ mutation/operations groups table/list/order/tree/replay commands by domain; session
6
+ owns the write kernel. Scope binds already-resolved facts through session and
7
+ reuses handles within a generation. These are internal implementation boundaries.
8
+ 2. Updates and public reads are synchronous and scoped. Reentrant writes are forbidden.
9
+ Failure restores all earlier work. Ordinary exceptions rethrow; TransactionRejected
10
+ returns application issues. Internal readWith creates a borrowed reader; its
11
+ proxies and collection methods must not escape the synchronous callback.
12
+ 3. mutation/changes.ts decodes unknown ChangeSets once. Schema resolution is authoritative.
13
+ Reject overlapping parent/child facts and duplicate groups. Each container group
14
+ installs its members then its optional order; no standalone order records.
15
+ 4. ChangeRecorder groups first-touch members by container and owns order baselines and touched tree nodes.
16
+ Ordered groups publish members and order together; tree containers have no member
17
+ layout and cannot accept ordinary members replay. Tree commands capture nodes directly.
18
+ Rollback runs no user callbacks or validators. Seal publishes net differences.
19
+ Capture, restoration and per-domain sealing stay separate. Copy final order only
20
+ after comparing current keys; keep the rollback baseline even for a net-zero edit.
21
+ 5. Atomic equality is Object.is; canonical structure retains atomic references under
22
+ ownership contract. Snapshots copy structure and share readonly payloads, as do
23
+ commits and history. No payload cloning or publication freezing. Validators receive
24
+ original inputs and must be pure; successful output is ignored.
25
+ 6. List identity is a stable key; replacement retains it. Anchor owns ordering.
26
+ Tree owns reciprocal, connected, acyclic, empty-or-single-root topology.
27
+ 7. ObjectNode owns schema identity; runtime owns instance identity. Shared path compiler
28
+ and impact-target own addressing/identity/equality/bucketing and exact matching, including React.
29
+ Notification matches grouped changes directly without building commit impact indexes.
30
+ 8. Apply requires expectedRevision and captures actual local old state. Local reset is
31
+ reversible; remote commits invalidate history. Groups travel in one session.
32
+ 9. Projections declare sources and settle before listeners. Notification failures leave
33
+ commits accepted. Batch defers projection publication, not document commits/listeners.
34
+ 10. Local sync uses Web Lock leadership and contiguous durable sequence. Writes precede
35
+ asynchronous persistence. Version 5 / format 3 rejects old databases without editing
36
+ them. External replace and remote apply are prohibited while attached.
37
+ 11. Core is framework-neutral. Public exports are deliberate; root dist is generated.
38
+ 12. Test malformed input, partial rollback, history, impact, disposal and bounded work.
39
+ Delete obsolete APIs and parallel protocols.
40
+
41
+ Network intent, authorization, collaborative undo and persist-before-visible acceptance
42
+ are separate requirements, not hidden runtime capabilities.