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,97 +1,34 @@
|
|
|
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
|
-
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
contract for subscriptions and impact interpretation.
|
|
50
|
+
## Ordered Edits, History And Replay
|
|
169
51
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
61
|
+
Incoming member changes share the owning container address:
|
|
188
62
|
|
|
189
63
|
```ts
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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.
|