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
|
@@ -1,293 +1,88 @@
|
|
|
1
|
-
# Doxum
|
|
1
|
+
# Doxum 使用指南
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
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.
|