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,97 +1,34 @@
1
- # Doxum Runtime 不变量
2
-
3
- 修改或审查 mutation、addressing、impact、notification、history、tree 或 projection 前应阅读本页。这些是设计边界,而不是可选的代码风格。
4
-
5
- ## 唯一的 canonical 写入权威
6
-
7
- `createDocument` 拥有 canonical mutable document state。canonical write 只能通过 `runtime.update`、`runtime.apply` 或 `runtime.replace`。`runtime.prepare` 可以复用同一 mutation session 产生随后会回滚的未提交 operation batch,供 durable adapter 使用;它不是第二条写入路径。writer 只是向 mutation session 产生 operation,而不是绕过 runtime 修改对象的出口。
8
-
9
- 绝不能新增:
10
-
11
- - 并行的可写 document cache;
12
- - 在 runtime 外编辑 document state 的 reducer;
13
- - 由调用方手工同步的 view;
14
- - 绕过 decode、normalization、inverse 记录、rollback、impact、history 或 notification 的 operation 执行路径。
15
-
16
- ## Transaction 生命周期与原子性
17
-
18
- transaction callback 必须同步。reader 与 writer 只在 callback 执行期间有效。`async` callback、保存 reader/writer 供后续使用、嵌套写入,以及 notification 期间写入都会破坏 runtime 边界。
19
-
20
- 每个 session 都是原子的。若一个 operation 在之前已有状态变更后被拒绝,session 之前的全部工作都必须回滚。callback 抛错也会回滚,然后将原始错误继续抛出。预期中的拒绝应该通过返回结果表示,而不是异常协议。
21
-
22
- ## 保持引擎问题与应用问题分离
23
-
24
- | 问题 | 所有者 | 结果形状 | 正确处理方式 |
25
- | ----------------------------------------- | ---------------- | ----------------------------------------------- | -------------------------------------------- |
26
- | malformed、无法解析或语义无效的 operation | Doxum engine | `source: 'mutation'` 的 `MutationIssue` | 检查 rejected operation/transaction result。 |
27
- | 应用业务规则或校验 | Application | `source: 'application'` 的 `DocumentDiagnostic` | 使用 `tx.report` 或 `tx.reject`。 |
28
- | callback 缺陷或意外失败 | Application code | rollback 后抛出的 error | 在 transaction 外修复或处理异常。 |
29
- | processor、flush 或 listener 失败 | Observer | committed result 上的 `observerErrors` | 修复 observer;不要重放已提交的 write。 |
30
-
31
- 不要再创建笼统的 `invalid` status,不要把全部 error 字符串化为同一个形状,也不要将 notification failure 变成 mutation rejection。Mutation issue code 是封闭的公共词汇,必须保持精确。
32
-
33
- ## Schema 拥有 addressing 与 selector
34
-
35
- schema 定义 operation 与 selector 的合法语义地址。address resolution 结合 schema 结构和当前 document state,这对于 collection entry 和 variant branch 都是必要的。
36
-
37
- 长期使用的 target 应由 `schema.value(...)` 与 `schema.collection(...)` 创建。runtime 的地址域使用 `runtime.address`;target 的 identity 与 bucket 使用导出的 `target` namespace。不要新增 string-path parser、另一种 address type、自定义 selector ID 或独立的 impact-target equality helper。
38
-
39
- ## Operation 只有一条 pipeline
40
-
41
- 外部 operation 输入遵循如下顺序:
42
-
43
- ```text
44
- decode -> normalize -> resolve -> execute -> inverse + journal -> publish
45
- ```
46
-
47
- `apply` 是不可信 operation envelope 的边界。executor code 看到输入前必须完成 decode;随后要归一为唯一 canonical operation shape、按 schema resolve,并且只能整体 publish 或整体 rollback。每个 committed operation 都需要精确的 inverse data 与精确的 impact。
48
-
49
- 本地应用行为应使用 writer,而不是手工拼 operation object。直接构造 operation 适用于边界 adapter、fixture、migration 和有意的 replay。
50
-
51
- ## 所有权必须明确
52
-
53
- - `initial` 在成为 canonical state 前会被 clone。
54
- - 普通 operation 中的结构化 payload 会转移到 canonical state。提交后仍修改它,可能会修改 canonical data。
55
- - commit 与 history 的 operation payload 是不可变 snapshot。
56
- - 已发布的 diagnostic 与 selector address 会被复制并冻结。
57
- - tree replacement snapshot 会先校验并 clone。
58
-
59
- 不要承诺 Doxum 有意进行 payload transfer 的地方存在深度不可变性。调用方若要保留可变所有权,应在提交前自行 clone。
60
-
61
- ## Tree 完整性是整个 document 的完整性
62
-
63
- 每个存在的 tree 必须为空,或恰好只有一个 root;从 root 可完整到达所有节点、无环、无重复 child 引用,且 parent/child 双向一致。initial state 与 replacement snapshot 会完整检查,本地 tree operation 则增量维持它。
64
-
65
- 不能接受不连通 forest、orphan node、非 root move 到无 parent、将 root 挂到另一节点之下,或直接编辑 tree 的内部 record。tree operation 被拒绝时,整个 transaction 必须保持不变。
66
-
67
- ## Impact 与 notification 描述已提交状态
68
-
69
- 每次 commit 都发布一个 `DocumentImpact`;它不是让调用方修改的 mutable change log。value impact 使用 `affects(target)` 判断;collection impact 精确报告 added、removed、updated 和 order change,或在 replacement/subtree reset 后报告 `reset`。
70
-
71
- notification 顺序是可观察行为:
72
-
73
- ```text
74
- commit -> materialized processors -> processor flushes -> targeted listeners -> root listeners
75
- ```
76
-
77
- update 与 notification 窗口中禁止写入。processor、flush 和 listener error 会被收集,但 committed document、revision 与 history 必须保持稳定。
78
-
79
- ## 派生状态应声明,而不是由调用方同步
80
-
81
- ProjectionCollection 和 ProjectionValue 共用显式 source 图与发布机制。只有 scoped processor 写入派生输出。应用根据原生 impact 选择候选 key,Doxum 拥有最终 change、equality、revision 和 notification。不能泄漏可变私有索引,也不能保留 scoped reader/writer。update 必须同步。
82
-
83
- 受影响节点全部 settle 后再通知。失败丢弃候选输出,限一次全新 build 恢复,持续失败阻断下游;listener 错误逐个隔离。处理和通知期间禁止 source write。显式 batch 延迟派生图发布,不延迟 canonical commit 或 document listener,不承诺 source rollback。owner 负责 dispose,仍有消费者的节点不能单独释放。
84
-
85
- ## Framework 与产品边界
86
-
87
- `core` 必须保持 framework-neutral。`doxum/react` 是从 core 到 React 的单向 adapter;core 不能 import React 或 UI 概念。`doxum/local-sync` 是附着到应用自有 runtime 的可选浏览器 attachment:它会恢复 IndexedDB checkpoint/tail,以 document Web Lock 选出唯一 leader,并通过一个很小的内部同步写入 policy 拒绝 follower mutation,而不改变 runtime API。leader 监听已完成的 local、system 与 history commit,将 JSON operation batch 异步追加到日志,并且只广播 head 提示。follower 从 durable tail 顺序读取并按 `remote` apply,因此 history 会失效。没有 pending queue、rebase、actor history 或 attachment 专属的 undo API。附着期间,外部 `replace` 与标记为 `remote` 的外部 `apply` 都会被拒绝,因为只有 operation command 能 append;attachment 的 hydration 与 replay lease 是唯一受信任例外。它不会承诺严格 durability;应用在需要观察持久化或追赶时使用 `state.current()`、`onError` 与 `flush()`。Doxum 有意不决定网络同步、authorization、retry、acknowledgement、ordering 或 conflict resolution。应用必须在 apply operation 或 replace snapshot 前做出这些决策。
88
-
89
- ## 改动检查清单
90
-
91
- runtime 相关改动交付前检查:
92
-
93
- - 每一次 canonical write 是否仍经由 `createDocument`?
94
- - 适用时,是否测试了成功、rejected rollback、inverse history 与 impact/subscription 行为?
95
- - tree 与 collection 路径是否避免了意外的全量 document copy 或 traversal?
96
- - 公共生命周期语义变化时,是否更新了 README 与 architecture guide?
97
- - 过时的 protocol type、局部 helper 和重复的 address/target 解释是否被删除,而不是为了兼容继续保留?
1
+ # Runtime 不变量
2
+
3
+ 1. createDocument 唯一拥有 canonical state;Draft、apply、replace 共用 MutationSession。
4
+ runtime 统一执行、回滚、seal、发布边界,history 同样复用。
5
+ mutation/operations 按 table/list/order/tree/replay 分类完整操作,session 持有写入内核。
6
+ scope 经 session 绑定已解析事实,同 generation 复用 handle;这些均为内部实现边界。
7
+ 2. 公共读写同步且有作用域,禁止重入写入。失败恢复之前全部工作;普通异常原样抛出,
8
+ TransactionRejected 转换为 application issues。内部 readWith 返回借用 reader,
9
+ reader、子 proxy 和 collection method 不得逃逸同步回调。
10
+ 3. mutation/changes.ts 统一解析 unknown ChangeSet;schema 为寻址真值。
11
+ 拒绝父子重叠事实与同地址重复分组。逐容器安装成员及可选顺序,不接受独立 order 记录。
12
+ 4. ChangeRecorder 按所属容器分组记录首次成员旧值,唯一拥有顺序基线和触及树节点。
13
+ 有序组一次发布 members 与 order;树容器没有普通成员 layout,拒绝 members replay。
14
+ 树命令直接捕获触及节点,不经过 session 回调协议。回滚不调用用户回调或校验器,seal 只发布净变化。
15
+ capture、restore 和按域 seal 分离;当前顺序确实变化后才复制最终 order,净变化为零也保留执行期间的回滚基线。
16
+ 5. 原子值按 Object.is 比较,canonical 结构依所有权契约保留原子引用;
17
+ 快照只复制结构,payload 与 commit、history 共享且只读,不做深复制或发布冻结。
18
+ 校验器直接读取原始输入,必须纯同步,成功返回值被忽略。
19
+ 6. list 以稳定键标记身份,替换值必须保留键。anchor 拥有排序语义;
20
+ tree 拥有双向一致、连通、无环、空树或单根的拓扑约束。
21
+ 7. ObjectNode 拥有定义身份,runtime 拥有实例身份;共享路径 compiler 和
22
+ impact-target 拥有寻址、身份、相等、分桶和精确匹配,React 也遵守此边界。
23
+ 通知直接匹配分组变化,不构建 commit impact 索引。
24
+ 8. apply 要求 expectedRevision,记录本地真实旧状态。本地 reset 可撤销,
25
+ remote commit 使 history 失效;group 在单个 session 中旅行。
26
+ 9. projection 显式声明 source,先于监听结算。通知失败不撤销提交;
27
+ batch 推迟投影发布,不推迟文档提交与文档监听。
28
+ 10. local-sync 使用 Web Lock 领导权和连续 durable seq,先可见后异步持久化。
29
+ 版本 5 / 格式 3 拒绝旧数据库并保留原数据,附着期间禁止外部 replace 和 remote apply。
30
+ 11. core 框架无关,公开导出需明确用途;根 dist 为构建产物。
31
+ 12. 测试 malformed 输入、部分失败回滚、history、impact、dispose 和大集合工作量。
32
+ 删除旧 API 和平行协议。
33
+
34
+ 网络意图、鉴权、协作撤销、先持久化后可见属于独立需求,不是隐藏 runtime 能力。
@@ -1,256 +1,104 @@
1
1
  # Doxum Patterns
2
2
 
3
- Read this reference when implementing Doxum application code. It is organized by
4
- the decisions that preserve the runtime model, rather than by an exhaustive
5
- list of exported symbols.
6
-
7
- ## Model data by its mutation semantics
8
-
9
- | Need | Schema node | Canonical value | Writer behavior |
10
- | ------------------------------- | -------------------------- | ------------------- | -------------------------------------------- |
11
- | One scalar or immutable leaf | `field<T>()` | `T` | `set`, and `clear` for an optional field |
12
- | Nested named fields | `object({ ... })` | object | child writers |
13
- | Tagged structural alternatives | `variant('kind', { ... })` | tagged object | `replace` the complete branch value |
14
- | Ordered entities | `table(entity)` | `{ ids, byId }` | `create`, `item`, `remove`, `move` |
15
- | Unordered entities | `map(entity)` | id record | `create`, `item`, `remove` |
16
- | Sparse scalar dictionary | `dict<Key, T>()` | partial record | `set`, `delete`, `replace` |
17
- | Ordered scalar/structural items | `list({ keyOf })` | array | `insert`, `move`, `remove`, `replace` |
18
- | One rooted hierarchy | `tree<T>()` | `{ rootId, nodes }` | `insert`, `move`, `remove`, `set`, `replace` |
19
-
20
- Use a table when ordering is product-visible. Do not use a map plus a separate
21
- array of ids: that creates two mutation protocols and two sources of order.
22
- Use a list only when every item has a stable, unique application key; never use
23
- the current index as `keyOf`.
24
-
25
- Entry readers and writers for tables and maps are derived directly from the entry
26
- schema node. Structured entries retain their specialized tree, list, dict, or
27
- variant access instead of being reconstructed from runtime values into a broad
28
- object union. Optional fields and optional variant, dict, list, and tree leaves
29
- also expose `clear()` and can be initialized from an absent state with
30
- `replace()`; objects, tables, and maps keep their child or collection operations.
3
+ ## Payload Or Structure
31
4
 
32
5
  ```ts
33
- import { field, list, map, object, schema, table, tree } from 'doxum';
34
-
35
- type Tag = { id: string; name: string };
36
-
37
- const note = object({ body: field<string>() });
38
- const documentSchema = schema({
39
- notes: table(note),
40
- tags: list<Tag>({ keyOf: tag => tag.id }),
41
- collaborators: map(object({ name: field<string>() })),
42
- outline: tree<{ title: string }>(),
6
+ const point = object({ x: field<number>(), y: field<number>() });
7
+ const model = object({
8
+ position: point,
9
+ stroke: field<readonly { x: number; y: number }[]>(),
10
+ rows: list(field<{ id: string; label: string }>(), { keyOf: row => row.id }),
43
11
  });
44
12
  ```
45
13
 
46
- ## Local domain command: read, validate, write
47
-
48
- Keep a domain command inside one transaction when all of its changes must
49
- succeed or fail together. Read before creating operations when a business rule
50
- depends on current state; call `tx.reject` for a blocking application rule.
51
-
52
- ```ts
53
- function completeTask(id: string) {
54
- return runtime.update(tx => {
55
- const task = tx.read.tasks.get(id);
56
- if (!task) {
57
- tx.reject({
58
- code: 'task-not-found',
59
- message: `Task '${id}' does not exist.`,
60
- address: ['tasks', id],
61
- });
62
- }
63
- if (task.completed.get()) return { changed: false };
64
-
65
- tx.write.tasks.item(id).completed.set(true);
66
- tx.report({
67
- code: 'task-completed',
68
- message: 'Task marked complete.',
69
- address: ['tasks', id],
70
- });
71
- return { changed: true };
72
- });
73
- }
74
- ```
75
-
76
- `tx.report` does not reject the transaction. It is suitable for warnings,
77
- audit-oriented feedback, or application messages that should accompany a valid
78
- commit. `tx.reject` stops the transaction by returning a rejected result to the
79
- outer caller. Neither is a substitute for malformed-operation handling:
80
- engine failures are `MutationIssue` values supplied by Doxum.
81
-
82
- ## Apply external operations at one boundary
14
+ Edit position.x, replace stroke whole, use rows.set(key, value) for keyed item edits.
83
15
 
84
- Keep serialization, authorization, network ordering, and conflict policy in an
85
- application adapter. Once that adapter decides a batch may be applied, pass the
86
- whole batch to Doxum.
16
+ ## Complex Replacement
87
17
 
88
18
  ```ts
89
- async function receiveRemote(batch: unknown) {
90
- // Authenticate, order, de-duplicate, and choose conflict policy here.
91
- const result = runtime.apply(batch, {
92
- source: 'remote',
93
- history: false,
94
- });
95
-
96
- if (result.status === 'rejected') {
97
- logRejectedOperations(result.issues);
98
- return;
99
- }
100
- if (result.status === 'committed') reportObserverErrors(result.observerErrors);
101
- }
102
- ```
103
-
104
- The cast above belongs only at a dynamic boundary whose runtime input is
105
- actually unknown. Keep it there; do not loosen operation types throughout the
106
- application. Doxum still validates malformed envelopes and semantic invalidity
107
- before publishing a commit.
108
-
109
- Use `replace` only for a new, trusted canonical snapshot. It produces reset
110
- impact and invalidates local history. It is not a convenient way to express a
111
- small change.
112
-
113
- ## Place ordered entries with anchors
114
-
115
- Tables and lists use one `DocumentAnchor` vocabulary:
116
-
117
- ```ts
118
- {
119
- at: 'start';
120
- }
121
- {
122
- at: 'end';
123
- }
124
- {
125
- before: 'other-id';
126
- }
127
- {
128
- after: 'other-id';
129
- }
130
- ```
131
-
132
- Use anchors instead of calculating indices in application code. They name the
133
- domain position, let Doxum validate missing references, and keep table, list,
134
- and operation replay semantics aligned.
135
-
136
- ```ts
137
- runtime.update(tx => {
138
- tx.write.tasks.create({ id: 'review', value: newTask }, { before: 'publish' });
139
- tx.write.tags.insert({ id: 'urgent', name: 'Urgent' }, { at: 'start' });
140
- tx.write.tags.move('urgent', { after: 'planning' });
19
+ const model = object({
20
+ entries: map(object({ rows: table(object({ title: field<string>() })) })),
21
+ });
22
+ const document = createDocument({ schema: model, initial: { entries: {} } });
23
+ document.update(draft => {
24
+ assign(draft.entries, 'a', { rows: { ids: ['x'], byId: { x: { title: 'First' } } } });
25
+ draft.entries.a!.rows.get('x')!.title = 'Updated';
141
26
  });
142
27
  ```
143
28
 
144
- ## Work with trees as one invariant
29
+ TypeScript cannot express different read/write types for mapped properties.
30
+ assign checks Infer replacement data and uses the same mutation session.
145
31
 
146
- A Doxum tree is either empty or a single connected root. Its `nodes` maintain
147
- reciprocal parent/child links, unique children, full reachability, and no
148
- cycles. Root replacement validates the whole snapshot; local writers preserve
149
- the invariant incrementally.
32
+ ## Domain Keys
150
33
 
151
34
  ```ts
152
- runtime.update(tx => {
153
- tx.write.outline.insert('root', { title: 'Project' });
154
- tx.write.outline.insert('plan', { title: 'Plan' }, { parentId: 'root' });
155
- tx.write.outline.move('plan', { parentId: 'root', index: 0 });
156
- tx.write.outline.set('plan', { title: 'Plan release' });
157
- });
35
+ type PersonId = string & { readonly __person: unique symbol };
36
+ const personId = (value: unknown): PersonId => {
37
+ if (typeof value !== 'string' || !value.startsWith('person:')) throw new Error('Person ID');
38
+ return value as PersonId;
39
+ };
40
+ const text = (value: unknown): string => {
41
+ if (typeof value !== 'string') throw new Error('String required');
42
+ return value;
43
+ };
44
+ const model = object({ people: map(object({ name: field(text) }), { key: personId }) });
45
+ const initial = parse(model, { people: { 'person:1': { name: 'Ada' } } });
158
46
  ```
159
47
 
160
- Create the root with no `parentId` only when the tree is empty. Once a root
161
- exists, every inserted node needs an existing parent. Do not move a non-root
162
- node to `undefined`, re-parent the root, or directly edit `{ rootId, nodes }`
163
- outside a validated `tree.replace` or runtime `replace` snapshot.
164
-
165
- ## Select and subscribe with the domain target
48
+ Brands flow through indexed access, table methods, symbolic paths and collection impact.
166
49
 
167
- Create selectors once with their owning schema. This creates a stable public
168
- contract for subscriptions and impact interpretation.
50
+ ## Ordered Edits, History And Replay
169
51
 
170
- ```ts
171
- const notes = documentSchema.collection(path => path.notes);
172
- const body = documentSchema.value(path => path.notes.item('a').body);
173
-
174
- const unsubscribe = runtime.subscribe([notes, body], commit => {
175
- if (commit.impact.affects(body)) refreshNoteUI();
176
-
177
- const change = commit.impact.collection(notes);
178
- if (change.kind === 'incremental' && change.updated.has('a')) refreshRow('a');
179
- });
180
- ```
52
+ Use table.create/remove/move with anchors such as { at: 'start' } or { before: id }.
53
+ Tree insert/move accept { parentId, index }; index is the final position after removal.
54
+ Never directly edit topology records.
181
55
 
182
- Use `target.same(left, right)` only when you need to compare two impact targets
183
- as values. Use `target.address`, `target.id`, `target.belongs`, and
184
- `target.bucket` rather than duplicating their interpretation in a framework
185
- adapter or local helper.
56
+ A history group begins with document.history.group(); end() groups completed commits,
57
+ cancel() restores its start. undo/redo travel complete ChangeSets atomically.
58
+ Apply incoming changes with an expectedRevision and adapter-validated transport order.
59
+ Remote commits invalidate local history. Local revision is not a distributed clock.
186
60
 
187
- ## Build explicit projections
61
+ Incoming member changes share the owning container address:
188
62
 
189
63
  ```ts
190
- const projection = createProjectionRuntime({ onError: error => console.error(error) });
191
- const document = projection.document(runtime);
192
- const notes = document.collection(path => path.notes);
193
- const noteSummaries = projection.map(
194
- notes,
195
- (id, note) => ({ id, preview: note.body.get().slice(0, 80) }),
196
- { isEqual: (a, b) => a.id === b.id && a.preview === b.preview }
64
+ document.apply(
65
+ {
66
+ changes: [
67
+ {
68
+ kind: 'members',
69
+ at: ['tasks', 'a'],
70
+ members: [
71
+ { key: 'complete', kind: 'updated', before: false, after: true },
72
+ { key: 'title', kind: 'updated', before: 'First', after: 'Done' },
73
+ ],
74
+ },
75
+ ],
76
+ },
77
+ { expectedRevision: document.revision() }
197
78
  );
198
- const noteCount = projection.value({ notes }, ({ notes }) => notes.read.ids().length);
199
79
  ```
200
80
 
201
- Use map for one-to-one document or projection collection transforms. For custom indexes, use
202
- `projection.collection<Item>()(spec)` with declared sources and scoped writer.set/remove/order/replace.
203
- Its previous and next readers separate published values from staged writes.
204
- Use document collection `reset`, `candidates.keys` and `candidates.orderDirty`,
205
- or inspect native commit impacts and upstream collection changes for algorithms
206
- that need them. Candidates include net-zero changes; read final state.
207
- Dependencies are explicit and fixed, not learned
208
- from reads. Doxum determines final changes through equality.
81
+ Use one group per container. Added members carry only after, removed members only
82
+ before; present undefined is still a value. Never expand a group into legacy value
83
+ envelopes or treat its container address as whole-container invalidation.
209
84
 
210
- A failed update discards its instance and attempts one fresh build. Persistent
211
- faults block descendants; independent branches continue. Call rebuild through
212
- the node API, never manually emit. Dispose the projection with its owner.
213
- Wrap document mutation and editor cleanup in projection.batch before the first
214
- commit; reads inside the batch remain at the last published projection.
85
+ ## Projection And React
215
86
 
216
- ## Bind read models in React
217
-
218
- For a direct document read, use `useDocumentSelector`. It tracks paths and
219
- collection entries read during each selector execution, including dynamic
220
- dependencies.
221
-
222
- ```tsx
223
- function Note({ id }: { id: string }) {
224
- const body = useDocumentSelector(runtime, read => read.notes.get(id)?.body.get());
225
- return <p>{body ?? 'Missing note'}</p>;
226
- }
227
- ```
228
-
229
- For an existing read model, use its narrower hook:
230
-
231
- ```tsx
232
- function NoteRow({ id }: { id: string }) {
233
- const summary = useReadable(noteSummaries.item(id));
234
- return <p>{summary?.preview}</p>;
235
- }
236
-
237
- function UndoButton() {
238
- const history = useHistory(runtime.history);
239
- return (
240
- <button disabled={history.undoDepth === 0} onClick={() => history.undo()}>
241
- Undo
242
- </button>
243
- );
244
- }
87
+ ```ts
88
+ const titles = project(
89
+ document,
90
+ path => path.tasks,
91
+ (_id, task) => task.title
92
+ );
93
+ const total = project({ titles }, ({ titles }) => titles.ids().length);
94
+ const zoom = input(1);
95
+ const scaled = project({ total, zoom }, ({ total, zoom }) => total * zoom);
96
+ const store = createProjectionStore({ onError: console.error });
97
+ store.get(scaled);
245
98
  ```
246
99
 
247
- Keep selectors pure. They should read Doxum state and calculate a value; do not
248
- write, subscribe manually, or cause I/O while a selector is running.
249
-
250
- ## Test the behavior that changes
251
-
252
- For a mutation change, cover the successful commit, rejected partial batch
253
- rollback, inverse/history result, and relevant impact or subscription outcome.
254
- For a projection, cover unrelated commits, dynamic dependencies, stable
255
- references where expected, and disposal. For large tables, lists, and trees,
256
- add a regression test that proves unrelated data is not copied or traversed.
100
+ Use `useProjection` with a store for projection definitions; use `useReadable`
101
+ for history and other existing Readable values. Custom collection processors stage
102
+ writer.set/remove/order/replace and use scoped previous/next reads. Candidates span
103
+ the complete batch; derive output from final state. Processor dependencies remain
104
+ explicit even though React selectors track actual reads.