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,209 +1,103 @@
|
|
|
1
|
-
# Doxum
|
|
1
|
+
# Doxum 常见模式
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## 按修改语义建模数据
|
|
6
|
-
|
|
7
|
-
| 需求 | Schema node | Canonical value | Writer 行为 |
|
|
8
|
-
| -------------------- | -------------------------- | ------------------- | -------------------------------------------- |
|
|
9
|
-
| 一个标量或不可变叶子 | `field<T>()` | `T` | `set`;optional field 还可 `clear` |
|
|
10
|
-
| 命名的嵌套字段 | `object({ ... })` | object | 子 writer |
|
|
11
|
-
| 带 tag 的结构分支 | `variant('kind', { ... })` | tagged object | `replace` 完整分支值 |
|
|
12
|
-
| 有序实体 | `table(entity)` | `{ ids, byId }` | `create`、`item`、`remove`、`move` |
|
|
13
|
-
| 无序实体 | `map(entity)` | id record | `create`、`item`、`remove` |
|
|
14
|
-
| 稀疏标量字典 | `dict<Key, T>()` | partial record | `set`、`delete`、`replace` |
|
|
15
|
-
| 有序标量/结构条目 | `list({ keyOf })` | array | `insert`、`move`、`remove`、`replace` |
|
|
16
|
-
| 单根层级 | `tree<T>()` | `{ rootId, nodes }` | `insert`、`move`、`remove`、`set`、`replace` |
|
|
17
|
-
|
|
18
|
-
当顺序对产品可见时使用 table。不要用 map 加另一份 ids array 表示顺序:这会产生两套 mutation protocol 和两个顺序来源。list 只适合每个条目都有稳定、唯一应用 key 的情形;不能把当前 index 当作 `keyOf`。
|
|
19
|
-
|
|
20
|
-
table/map 的 entry reader 与 writer 直接由 entry schema node 推导。结构化 entry 会保留
|
|
21
|
-
自己的 tree、list、dict、variant 等专用 access,而不会从运行时 value 反推成普通对象或
|
|
22
|
-
产生 value union。optional field 以及 optional 的 variant、dict、list、tree 结构叶子额外
|
|
23
|
-
提供 `clear()`,并可在缺失状态通过 `replace()` 初始化;object、table、map 仍使用各自的
|
|
24
|
-
子 writer 或集合操作。
|
|
3
|
+
## 原子值与结构
|
|
25
4
|
|
|
26
5
|
```ts
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const documentSchema = schema({
|
|
33
|
-
notes: table(note),
|
|
34
|
-
tags: list<Tag>({ keyOf: tag => tag.id }),
|
|
35
|
-
collaborators: map(object({ name: field<string>() })),
|
|
36
|
-
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 }),
|
|
37
11
|
});
|
|
38
12
|
```
|
|
39
13
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
所有修改要么全部成功、要么全部失败时,将领域命令放进同一个 transaction。业务规则依赖当前状态时,先读取再产生 operation;阻断型业务规则调用 `tx.reject`。
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
function completeTask(id: string) {
|
|
46
|
-
return runtime.update(tx => {
|
|
47
|
-
const task = tx.read.tasks.get(id);
|
|
48
|
-
if (!task) {
|
|
49
|
-
tx.reject({
|
|
50
|
-
code: 'task-not-found',
|
|
51
|
-
message: `Task '${id}' does not exist.`,
|
|
52
|
-
address: ['tasks', id],
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
if (task.completed.get()) return { changed: false };
|
|
56
|
-
|
|
57
|
-
tx.write.tasks.item(id).completed.set(true);
|
|
58
|
-
tx.report({
|
|
59
|
-
code: 'task-completed',
|
|
60
|
-
message: 'Task marked complete.',
|
|
61
|
-
address: ['tasks', id],
|
|
62
|
-
});
|
|
63
|
-
return { changed: true };
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
`tx.report` 不会拒绝 transaction,适合 warning、审计反馈或伴随合法 commit 的应用消息。`tx.reject` 会中止 transaction,并让外层调用者得到 rejected result。两者都不能替代 malformed operation 的处理:引擎失败由 Doxum 提供的 `MutationIssue` 表示。
|
|
69
|
-
|
|
70
|
-
## 在一个边界应用外部 operation
|
|
71
|
-
|
|
72
|
-
将序列化、授权、网络排序和冲突策略集中在应用 adapter 中。adapter 判断一个 batch 可以应用后,再把完整 batch 交给 Doxum。
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
async function receiveRemote(batch: unknown) {
|
|
76
|
-
// 在这里执行认证、排序、去重和冲突策略。
|
|
77
|
-
const result = runtime.apply(batch, {
|
|
78
|
-
source: 'remote',
|
|
79
|
-
history: false,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
if (result.status === 'rejected') {
|
|
83
|
-
logRejectedOperations(result.issues);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (result.status === 'committed') reportObserverErrors(result.observerErrors);
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
上面的断言只应存在于真正接受未知运行时输入的动态边界;不要把 operation type 放宽到整个应用。Doxum 仍会在发布 commit 前校验 malformed envelope 与语义错误。
|
|
91
|
-
|
|
92
|
-
`replace` 只用于新的、可信的 canonical snapshot。它会产生 reset impact 并使 local history 失效,不是表达微小变更的便捷写法。
|
|
93
|
-
|
|
94
|
-
## 用 Anchor 放置有序条目
|
|
95
|
-
|
|
96
|
-
table 与 list 共用同一套 `DocumentAnchor` 词汇:
|
|
14
|
+
position.x 细粒度赋值;stroke 整体替换;rows.set(key, value) 按稳定键替换项。
|
|
97
15
|
|
|
98
|
-
|
|
99
|
-
{
|
|
100
|
-
at: 'start';
|
|
101
|
-
}
|
|
102
|
-
{
|
|
103
|
-
at: 'end';
|
|
104
|
-
}
|
|
105
|
-
{
|
|
106
|
-
before: 'other-id';
|
|
107
|
-
}
|
|
108
|
-
{
|
|
109
|
-
after: 'other-id';
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
不要在应用代码中计算 index,而应使用 anchor。它表达领域位置,让 Doxum 校验缺失的引用,并让 table、list 与 operation replay 语义保持一致。
|
|
16
|
+
## 含集合的整体替换
|
|
114
17
|
|
|
115
18
|
```ts
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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';
|
|
120
26
|
});
|
|
121
27
|
```
|
|
122
28
|
|
|
123
|
-
|
|
29
|
+
TypeScript 映射属性不能分别指定读写类型。assign 校验对应 Infer 数据,
|
|
30
|
+
经同一 mutation session 修改。
|
|
124
31
|
|
|
125
|
-
|
|
32
|
+
## 领域键
|
|
126
33
|
|
|
127
34
|
```ts
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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' } } });
|
|
134
46
|
```
|
|
135
47
|
|
|
136
|
-
|
|
48
|
+
品牌类型贯穿索引、table 方法、符号路径和 collection impact。
|
|
137
49
|
|
|
138
|
-
##
|
|
50
|
+
## 顺序、History 与重放
|
|
139
51
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const notes = documentSchema.collection(path => path.notes);
|
|
144
|
-
const body = documentSchema.value(path => path.notes.item('a').body);
|
|
145
|
-
|
|
146
|
-
const unsubscribe = runtime.subscribe([notes, body], commit => {
|
|
147
|
-
if (commit.impact.affects(body)) refreshTitleUI();
|
|
148
|
-
|
|
149
|
-
const change = commit.impact.collection(notes);
|
|
150
|
-
if (change.kind === 'incremental' && change.updated.has('a')) refreshRow('a');
|
|
151
|
-
});
|
|
152
|
-
```
|
|
52
|
+
table.create/remove/move 使用 { at: 'start' } 或 { before: id } 等 anchor。
|
|
53
|
+
tree.insert/move 接收 { parentId, index },index 表示移除后的最终位置,
|
|
54
|
+
不能直接修改拓扑记录。
|
|
153
55
|
|
|
154
|
-
|
|
56
|
+
document.history.group() 开始分组,end() 分组已完成提交,cancel() 恢复起点;
|
|
57
|
+
undo/redo 原子重放完整 ChangeSet。外部 apply 必须提供 expectedRevision,
|
|
58
|
+
适配器还须校验传输顺序。remote commit 使本地 history 失效;
|
|
59
|
+
本地 revision 不是分布式时钟。
|
|
155
60
|
|
|
156
|
-
|
|
61
|
+
输入的成员变化共享所属容器地址:
|
|
157
62
|
|
|
158
63
|
```ts
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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() }
|
|
166
78
|
);
|
|
167
|
-
const noteCount = projection.value({ notes }, ({ notes }) => notes.read.ids().length);
|
|
168
79
|
```
|
|
169
80
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
document collection 提供整个批次的 reset、candidates.keys 和 candidates.orderDirty;需要更多细节时仍可读取原生 commits/impact 或上游 collection change。候选 key 包括净零变化,必须读取最终状态决定输出。依赖显式固定,不通过 reader 自动学习。Doxum 根据 equality 决定最终 change。
|
|
173
|
-
|
|
174
|
-
update 失败后丢弃实例并限一次全新 build 恢复;持续失败阻断下游,独立分支继续。rebuild 经过同一调度图,不手动 emit。projection 随 owner dispose。使用 projection.batch 包住首次 document commit 之前到 editor cleanup 结束的完整同步动作;batch 内派生读取保持上次发布状态。
|
|
175
|
-
|
|
176
|
-
## 在 React 中绑定读模型
|
|
177
|
-
|
|
178
|
-
直接读取 document 时使用 `useDocumentSelector`。它会跟踪每次 selector 执行中读取的 path 和 collection entry,包括动态依赖。
|
|
81
|
+
每个容器只能有一组。added 仅携带 after,removed 仅携带 before;存在的 undefined
|
|
82
|
+
仍是一个值。不要展开成旧 value envelope,也不要将分组地址当作整个容器失效。
|
|
179
83
|
|
|
180
|
-
|
|
181
|
-
function Note({ id }: { id: string }) {
|
|
182
|
-
const body = useDocumentSelector(runtime, read => read.notes.get(id)?.body.get());
|
|
183
|
-
return <p>{body ?? 'Missing note'}</p>;
|
|
184
|
-
}
|
|
185
|
-
```
|
|
84
|
+
## 投影与 React
|
|
186
85
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
<button disabled={history.undoDepth === 0} onClick={() => history.undo()}>
|
|
199
|
-
Undo
|
|
200
|
-
</button>
|
|
201
|
-
);
|
|
202
|
-
}
|
|
86
|
+
```ts
|
|
87
|
+
const titles = project(
|
|
88
|
+
document,
|
|
89
|
+
path => path.tasks,
|
|
90
|
+
(_id, task) => task.title
|
|
91
|
+
);
|
|
92
|
+
const total = project({ titles }, ({ titles }) => titles.ids().length);
|
|
93
|
+
const zoom = input(1);
|
|
94
|
+
const scaled = project({ total, zoom }, ({ total, zoom }) => total * zoom);
|
|
95
|
+
const store = createProjectionStore({ onError: console.error });
|
|
96
|
+
store.get(scaled);
|
|
203
97
|
```
|
|
204
98
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
99
|
+
投影定义使用 `useProjection` 和一个 store,document.history 等既有 Readable
|
|
100
|
+
使用 `useReadable` 或 `useHistory`。自定义集合 processor 通过
|
|
101
|
+
writer.set/remove/order/replace 暂存输出,
|
|
102
|
+
previous/next 读取只在作用域内有效。candidates 汇总整个 batch,以最终状态派生输出。
|
|
103
|
+
React 追踪实际读取,但 processor 依赖仍显式声明。
|
package/dist/chunk-pbuEa-1d.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
//#region \0rolldown/runtime.js
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __exportAll = (all, no_symbols) => {
|
|
4
|
-
let target = {};
|
|
5
|
-
for (var name in all) __defProp(target, name, {
|
|
6
|
-
get: all[name],
|
|
7
|
-
enumerable: true
|
|
8
|
-
});
|
|
9
|
-
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
-
return target;
|
|
11
|
-
};
|
|
12
|
-
//#endregion
|
|
13
|
-
export { __exportAll as t };
|