doxum 0.1.4 → 0.1.6

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 (48) hide show
  1. package/README.md +46 -5
  2. package/dist/{contract-BsY1XLiG.d.ts → contract-I99DUMbN.d.ts} +53 -40
  3. package/dist/{contract-D8kjdfqT.d.cts → contract-uYJTmT8o.d.cts} +53 -40
  4. package/dist/index.cjs +244 -630
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +3 -64
  7. package/dist/index.d.ts +3 -64
  8. package/dist/index.js +182 -569
  9. package/dist/index.js.map +1 -1
  10. package/dist/integration-Bs3pOk65.cjs +35 -0
  11. package/dist/integration-Bs3pOk65.cjs.map +1 -0
  12. package/dist/integration-xupBIKjU.js +30 -0
  13. package/dist/integration-xupBIKjU.js.map +1 -0
  14. package/dist/integration.cjs +4 -14
  15. package/dist/integration.d.cts +3 -2
  16. package/dist/integration.d.ts +3 -2
  17. package/dist/integration.js +3 -14
  18. package/dist/local-sync.cjs +1 -1
  19. package/dist/local-sync.d.cts +1 -1
  20. package/dist/local-sync.d.ts +1 -1
  21. package/dist/local-sync.js +1 -1
  22. package/dist/{ownership-CU-9DKJQ.cjs → ownership-CY0nPXGF.cjs} +4 -1
  23. package/dist/ownership-CY0nPXGF.cjs.map +1 -0
  24. package/dist/{ownership-B-VAPcce.js → ownership-CduRygE7.js} +4 -1
  25. package/dist/ownership-CduRygE7.js.map +1 -0
  26. package/dist/react.cjs +1 -1
  27. package/dist/react.js +1 -1
  28. package/dist/runtime-BJRXF0gq.js +1735 -0
  29. package/dist/runtime-BJRXF0gq.js.map +1 -0
  30. package/dist/runtime-CRijXjYp.cjs +1991 -0
  31. package/dist/runtime-CRijXjYp.cjs.map +1 -0
  32. package/dist/runtime-CUcvhFNP.d.ts +152 -0
  33. package/dist/runtime-C_vKHmhy.d.cts +152 -0
  34. package/package.json +1 -1
  35. package/skills/doxum-runtime/references/guide.en.md +35 -18
  36. package/skills/doxum-runtime/references/guide.zh-CN.md +26 -14
  37. package/skills/doxum-runtime/references/invariants.en.md +12 -9
  38. package/skills/doxum-runtime/references/invariants.zh-CN.md +2 -2
  39. package/skills/doxum-runtime/references/patterns.en.md +36 -56
  40. package/skills/doxum-runtime/references/patterns.zh-CN.md +27 -45
  41. package/dist/dependency-C5_R1tvQ.js +0 -496
  42. package/dist/dependency-C5_R1tvQ.js.map +0 -1
  43. package/dist/dependency-Dfzs3khk.cjs +0 -722
  44. package/dist/dependency-Dfzs3khk.cjs.map +0 -1
  45. package/dist/integration.cjs.map +0 -1
  46. package/dist/integration.js.map +0 -1
  47. package/dist/ownership-B-VAPcce.js.map +0 -1
  48. package/dist/ownership-CU-9DKJQ.cjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doxum",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Doxum is a typed runtime for complex mutable documents.",
@@ -24,8 +24,8 @@ resolution.
24
24
  | Replace an entire trusted snapshot | `runtime.replace(document, options)` |
25
25
  | Observe one schema location | `schema.value` plus `runtime.subscribe` |
26
26
  | Observe a table or map | `schema.collection` plus `runtime.subscribe` |
27
- | Maintain mapped collection data | `createCollectionView` |
28
- | Maintain an aggregate or index | `createMaterializedView` |
27
+ | Maintain mapped collection data | `projection.map` |
28
+ | Maintain an aggregate or index | `projection.value / projection.collection` |
29
29
  | Read in React | `useDocumentSelector`, `useReadable`, or `useReadable` |
30
30
 
31
31
  Do not write a second mutable copy of the document. `createDocument` is the
@@ -163,6 +163,11 @@ the same API except `move`, because it is unordered. Lists offer `insert`,
163
163
  specified in the schema. See [patterns.en.md](patterns.en.md) for complete
164
164
  collection and tree examples.
165
165
 
166
+ Optional field, variant, dict, list, and tree writers expose `clear()`. Optional
167
+ structured leaves can also be initialized from an absent state with `replace()`.
168
+ Optional objects, tables, and maps have no whole-value clear; modify them through
169
+ their child writers or collection operations.
170
+
166
171
  ## Replay operations at the boundary
167
172
 
168
173
  Use `apply` for operation batches that came from persistence, a network
@@ -288,26 +293,38 @@ comparison helpers in application modules.
288
293
 
289
294
  ## Build derived read models
290
295
 
291
- `createCollectionView` maps one table or map into stable ids, cached `item(id)`
292
- readables, and a lazy `all` array. It updates only affected entries where possible.
293
-
294
296
  ```ts
295
- import { createCollectionView } from 'doxum';
296
-
297
- const taskTitles = createCollectionView({
298
- runtime,
299
- source: taskSchema.collection(path => path.tasks),
300
- map: (_id, task) => task.title.get(),
297
+ const projection = createProjectionRuntime({ onError: error => console.error(error) });
298
+ const document = projection.document(runtime);
299
+ const notes = document.collection(path => path.notes);
300
+ const noteSummaries = projection.map(
301
+ notes,
302
+ (id, note) => ({ id, preview: note.body.get().slice(0, 80) }),
303
+ { isEqual: (a, b) => a.id === b.id && a.preview === b.preview }
304
+ );
305
+ const noteCount = projection.value({
306
+ sources: { notes },
307
+ build: ({ notes }) => ({
308
+ value: notes.read.ids().length,
309
+ update: ({ notes }) => ({
310
+ kind: 'changed',
311
+ value: notes.read.ids().length,
312
+ }),
313
+ }),
301
314
  });
302
-
303
- taskTitles.item('write-guide').current();
304
- taskTitles.all.current();
305
315
  ```
306
316
 
307
- Use `createMaterializedView` for an index or aggregate with custom incremental
308
- logic. A materialized view is derived state, not a caller-maintained cache. It
309
- may depend only on materialized views created earlier from the same runtime.
310
- Dispose every view when its owning feature is disposed.
317
+ projection.map produces stable ids/item readables and lazy all. Declare a
318
+ DocumentCollectionSource directly in any processor's sources, or use
319
+ projection.value and projection.collection for explicit incremental logic.
320
+ There is no automatic keyed dependency tracking. All sources belong to the
321
+ same projection owner, but may refer to different document runtimes.
322
+
323
+ Use projection.input(initial, { isEqual }) for boundary values; give processors
324
+ its source and keep set at the application boundary. fromReadable attaches an
325
+ existing external source without taking ownership of it. Batch synchronous
326
+ multi-source changes before the first commit. Dispose the projection at service
327
+ shutdown; React unmount only unsubscribes. Disposed handles throw.
311
328
 
312
329
  ## React integration
313
330
 
@@ -17,8 +17,8 @@
17
17
  | 整体替换可信快照 | `runtime.replace(document, options)` |
18
18
  | 监听一个 schema 位置 | `schema.value` + `runtime.subscribe` |
19
19
  | 监听 table 或 map | `schema.collection` + `runtime.subscribe` |
20
- | 维护映射后的集合数据 | `createCollectionView` |
21
- | 维护聚合或索引 | `createMaterializedView` |
20
+ | 维护映射后的集合数据 | `projection.map` |
21
+ | 维护聚合或索引 | `projection.value / projection.collection` |
22
22
  | 在 React 中读取 | `useDocumentSelector`、`useReadable` 或 `useReadable` |
23
23
 
24
24
  不要再维护一份可写的文档副本。`createDocument` 是 canonical state 的唯一所有者。
@@ -135,6 +135,10 @@ runtime.update(tx => {
135
135
 
136
136
  table 支持 `create`、`item`、`remove` 和 `move`。map 与之相同,但没有 `move`,因为它无顺序。list 支持 `insert`、`move`、`remove` 与 `replace`;其身份来自 schema 中的 `keyOf`。完整的集合与 tree 模式见 [patterns.zh-CN.md](patterns.zh-CN.md)。
137
137
 
138
+ optional 的 field、variant、dict、list 和 tree writer 提供 `clear()`;其中结构化叶子也可
139
+ 在当前缺失时直接调用 `replace()` 建立值。optional object、table 和 map 没有整体 clear,
140
+ 应通过子 writer 或集合操作修改。
141
+
138
142
  ## 在边界回放 operation
139
143
 
140
144
  来自持久化、网络适配器或其他外部边界的 operation batch 应通过 `apply` 回放。Doxum 会在 mutation code 看到数据前解码未知 operation payload,按 schema 解析每个地址,并原子地执行整个 batch。
@@ -239,22 +243,30 @@ value selector 使用 `commit.impact.affects(target)`;table 或 map selector
239
243
 
240
244
  ## 构建派生读模型
241
245
 
242
- `createCollectionView` 将一个 table 或 map 映射为稳定的 ids、缓存的 `item(id)` readable 和惰性 `all` 数组;在可能的情况下只更新受影响的条目。
243
-
244
246
  ```ts
245
- import { createCollectionView } from 'doxum';
246
-
247
- const taskTitles = createCollectionView({
248
- runtime,
249
- source: taskSchema.collection(path => path.tasks),
250
- map: (_id, task) => task.title.get(),
247
+ const projection = createProjectionRuntime({ onError: error => console.error(error) });
248
+ const document = projection.document(runtime);
249
+ const notes = document.collection(path => path.notes);
250
+ const noteSummaries = projection.map(
251
+ notes,
252
+ (id, note) => ({ id, preview: note.body.get().slice(0, 80) }),
253
+ { isEqual: (a, b) => a.id === b.id && a.preview === b.preview }
254
+ );
255
+ const noteCount = projection.value({
256
+ sources: { notes },
257
+ build: ({ notes }) => ({
258
+ value: notes.read.ids().length,
259
+ update: ({ notes }) => ({
260
+ kind: 'changed',
261
+ value: notes.read.ids().length,
262
+ }),
263
+ }),
251
264
  });
252
-
253
- taskTitles.item('write-guide').current();
254
- taskTitles.all.current();
255
265
  ```
256
266
 
257
- 需要自定义增量逻辑的索引或聚合使用 `createMaterializedView`。materialized view 是派生状态,不是由调用方手工同步的 cache;它只能依赖同一 runtime 中更早创建的 materialized view。每个 view 都要由所属功能在销毁时释放。
267
+ projection.map 提供稳定的 ids/item readable 和惰性 all。DocumentCollectionSource 可直接用于自定义 processor 的 sources;projection.value 和 projection.collection 承载显式增量计算。不自动跟踪 keyed 依赖。sources 属于同一 projection owner,但可接入不同 document runtime。
268
+
269
+ projection.input(initial, { isEqual }) 用于边界值;应用保留 set,processor 只接收 source。fromReadable 接入已有外部 source,不取得其生命周期所有权。多 source 同步更新应在首次 commit 前进入 batch。projection 随 service 销毁,React unmount 只取消订阅,disposed handle 明确抛错。
258
270
 
259
271
  ## React 集成
260
272
 
@@ -117,15 +117,18 @@ and history stay settled.
117
117
 
118
118
  ## Derived state is declared, not synchronized by callers
119
119
 
120
- `CollectionView` derives one declared collection and incrementally maintains
121
- ids, keyed values, and a lazy aggregate array. `MaterializedView` derives one
122
- value from document reads, tracked impact dependencies, and optional earlier
123
- materialized sources. A materialized view can only depend on views of the same
124
- runtime that were created before it.
125
-
126
- Do not cache derived values inside canonical document state unless they are
127
- real domain data. Do not have UI code manually feed changes into a view. Dispose
128
- views and subscriptions with their owner.
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.
129
132
 
130
133
  ## Framework and product boundaries
131
134
 
@@ -78,9 +78,9 @@ update 与 notification 窗口中禁止写入。processor、flush 和 listener e
78
78
 
79
79
  ## 派生状态应声明,而不是由调用方同步
80
80
 
81
- `CollectionView` 从一个已声明的 collection 派生,并增量维护 ids、keyed values 和惰性 aggregate array。`MaterializedView` 从 document read、跟踪到的 impact dependency 和可选的更早 materialized source 派生一个值。materialized view 只能依赖同一 runtime 中比它更早创建的 view。
81
+ ProjectionCollection 和 ProjectionValue 共用显式 source 图与发布机制。只有 scoped processor 写入派生输出。应用根据原生 impact 选择候选 key,Doxum 拥有最终 change、equality、revision 和 notification。不能泄漏可变私有索引,也不能保留 scoped reader/writer。update 必须同步。
82
82
 
83
- 不要把 derived value 缓存在 canonical document state 中,除非它本来就是领域数据。不要让 UI 手工将变更推入 view。view 与 subscription 都应随 owner dispose。
83
+ 受影响节点全部 settle 后再通知。失败丢弃候选输出,限一次全新 build 恢复,持续失败阻断下游;listener 错误逐个隔离。处理和通知期间禁止 source write。显式 batch 延迟派生图发布,不延迟 canonical commit 或 document listener,不承诺 source rollback。owner 负责 dispose,仍有消费者的节点不能单独释放。
84
84
 
85
85
  ## Framework 与产品边界
86
86
 
@@ -24,6 +24,13 @@ array of ids: that creates two mutation protocols and two sources of order.
24
24
  Use a list only when every item has a stable, unique application key; never use
25
25
  the current index as `keyOf`.
26
26
 
27
+ Entry readers and writers for tables and maps are derived directly from the entry
28
+ schema node. Structured entries retain their specialized tree, list, dict, or
29
+ variant access instead of being reconstructed from runtime values into a broad
30
+ object union. Optional fields and optional variant, dict, list, and tree leaves
31
+ also expose `clear()` and can be initialized from an absent state with
32
+ `replace()`; objects, tables, and maps keep their child or collection operations.
33
+
27
34
  ```ts
28
35
  import { field, list, map, object, schema, table, tree } from 'doxum';
29
36
 
@@ -181,68 +188,41 @@ as values. Use `target.address`, `target.id`, `target.belongs`, and
181
188
  `target.bucket` rather than duplicating their interpretation in a framework
182
189
  adapter or local helper.
183
190
 
184
- ## Build a collection view for mapped rows
185
-
186
- Use a collection view when a table or map needs a reusable read model. Its
187
- mapping callback can read the entry's typed fields; `isEqual` lets the view
188
- retain a previous mapped value when a remapped row is semantically unchanged.
189
-
190
- ```ts
191
- const notes = documentSchema.collection(path => path.notes);
192
-
193
- const noteSummaries = createCollectionView({
194
- runtime,
195
- source: notes,
196
- map: (id, note) => ({ id, preview: note.body.get().slice(0, 80) }),
197
- isEqual: (left, right) => left.id === right.id && left.preview === right.preview,
198
- });
199
-
200
- const stop = noteSummaries.item('a').subscribe(() => rerenderRow('a'));
201
- const all = noteSummaries.all.current();
202
- ```
203
-
204
- Do not make callers push changes into the view. It listens to its declared
205
- source and recomputes from runtime state. Dispose `noteSummaries` and `stop`
206
- with the UI or service that owns them.
207
-
208
- ## Build a materialized aggregate or index
209
-
210
- Use a materialized view when you own an aggregate that can update from a
211
- commit. During `update`, inspect the impact through the provided `impact`
212
- object. That records the actual dependencies, so future unrelated commits can
213
- skip this update.
191
+ ## Build explicit projections
214
192
 
215
193
  ```ts
216
- const notes = documentSchema.collection(path => path.notes);
217
-
218
- const noteCount = createMaterializedView(runtime, {
219
- build: ({ read }) => ({
220
- value: read.notes.ids().length,
221
- update: ({ impact, read }) => {
222
- const change = impact.collection(notes);
223
- if (
224
- change.kind === 'incremental' &&
225
- change.added.size === 0 &&
226
- change.removed.size === 0 &&
227
- change.updated.size === 0 &&
228
- !change.orderChanged
229
- ) {
230
- return { kind: 'unchanged' };
231
- }
232
- return {
233
- kind: 'changed',
234
- value: read.notes.ids().length,
235
- change,
236
- };
237
- },
194
+ const projection = createProjectionRuntime({ onError: error => console.error(error) });
195
+ const document = projection.document(runtime);
196
+ const notes = document.collection(path => path.notes);
197
+ const noteSummaries = projection.map(
198
+ notes,
199
+ (id, note) => ({ id, preview: note.body.get().slice(0, 80) }),
200
+ { isEqual: (a, b) => a.id === b.id && a.preview === b.preview }
201
+ );
202
+ const noteCount = projection.value({
203
+ sources: { notes },
204
+ build: ({ notes }) => ({
205
+ value: notes.read.ids().length,
206
+ update: ({ notes }) => ({
207
+ kind: 'changed',
208
+ value: notes.read.ids().length,
209
+ }),
238
210
  }),
239
211
  });
240
212
  ```
241
213
 
242
- Return `rebuild` if local incremental logic cannot safely handle a commit.
243
- Views created later can name earlier views in `sources`; Doxum processes that
244
- graph in creation order and flushes it before external listeners observe the
245
- commit.
214
+ Use map for one-to-one collection transforms. For custom indexes, use
215
+ projection.collection with declared sources and scoped writer.set/remove/order/replace.
216
+ Its previous and next readers separate published values from staged writes.
217
+ Inspect each document source commit.impact, or an upstream collection change,
218
+ to determine candidate keys. Dependencies are explicit and fixed, not learned
219
+ from reads. Doxum determines final changes through equality.
220
+
221
+ A failed update discards its instance and attempts one fresh build. Persistent
222
+ faults block descendants; independent branches continue. Call rebuild through
223
+ the node API, never manually emit. Dispose the projection with its owner.
224
+ Wrap document mutation and editor cleanup in projection.batch before the first
225
+ commit; reads inside the batch remain at the last published projection.
246
226
 
247
227
  ## Bind read models in React
248
228
 
@@ -19,6 +19,12 @@
19
19
 
20
20
  当顺序对产品可见时使用 table。不要用 map 加另一份 ids array 表示顺序:这会产生两套 mutation protocol 和两个顺序来源。list 只适合每个条目都有稳定、唯一应用 key 的情形;不能把当前 index 当作 `keyOf`。
21
21
 
22
+ table/map 的 entry reader 与 writer 直接由 entry schema node 推导。结构化 entry 会保留
23
+ 自己的 tree、list、dict、variant 等专用 access,而不会从运行时 value 反推成普通对象或
24
+ 产生 value union。optional field 以及 optional 的 variant、dict、list、tree 结构叶子额外
25
+ 提供 `clear()`,并可在缺失状态通过 `replace()` 初始化;object、table、map 仍使用各自的
26
+ 子 writer 或集合操作。
27
+
22
28
  ```ts
23
29
  import { field, list, map, object, schema, table, tree } from 'doxum';
24
30
 
@@ -151,58 +157,34 @@ const unsubscribe = runtime.subscribe([notes, body], commit => {
151
157
 
152
158
  只有需要将两个 impact target 当作值比较时,使用 `target.same(left, right)`。应使用 `target.address`、`target.id`、`target.belongs` 与 `target.bucket`,不要在 framework adapter 或局部 helper 中复制它们的解释逻辑。
153
159
 
154
- ## 用 CollectionView 构建映射行
155
-
156
- 当一个 table 或 map 需要可复用 read model 时,使用 collection view。mapping callback 可以读取 entry 的类型化字段;`isEqual` 允许 view 在重新映射出的 row 语义不变时保留旧值。
160
+ ## 构建显式派生图
157
161
 
158
162
  ```ts
159
- const notes = documentSchema.collection(path => path.notes);
160
-
161
- const noteSummaries = createCollectionView({
162
- runtime,
163
- source: notes,
164
- map: (id, note) => ({ id, preview: note.body.get().slice(0, 80) }),
165
- isEqual: (left, right) => left.id === right.id && left.preview === right.preview,
163
+ const projection = createProjectionRuntime({ onError: error => console.error(error) });
164
+ const document = projection.document(runtime);
165
+ const notes = document.collection(path => path.notes);
166
+ const noteSummaries = projection.map(
167
+ notes,
168
+ (id, note) => ({ id, preview: note.body.get().slice(0, 80) }),
169
+ { isEqual: (a, b) => a.id === b.id && a.preview === b.preview }
170
+ );
171
+ const noteCount = projection.value({
172
+ sources: { notes },
173
+ build: ({ notes }) => ({
174
+ value: notes.read.ids().length,
175
+ update: ({ notes }) => ({
176
+ kind: 'changed',
177
+ value: notes.read.ids().length,
178
+ }),
179
+ }),
166
180
  });
167
-
168
- const stop = noteSummaries.item('a').subscribe(() => rerenderRow('a'));
169
- const all = noteSummaries.all.current();
170
181
  ```
171
182
 
172
- 不要让调用方把变更推入 view。它会监听声明的 source 并从 runtime state 重新计算。`noteSummaries` 和 `stop` 应随所属 UI 或 service 一起 dispose。
173
-
174
- ## 用 MaterializedView 构建聚合或索引
183
+ map 用于保留 key 和顺序的一对一映射。自定义索引使用 projection.collection,声明 sources,并通过 scoped writer.set/remove/order/replace 更新。previous 与 next 分别读取上次输出和本轮暂存结果。
175
184
 
176
- 当你拥有可以根据 commit 增量更新的 aggregate 时,使用 materialized view。`update` 期间通过传入的 `impact` 对象检查影响范围;这会记录真实依赖,使之后无关的 commit 可以跳过此次 update。
177
-
178
- ```ts
179
- const notes = documentSchema.collection(path => path.notes);
180
-
181
- const noteCount = createMaterializedView(runtime, {
182
- build: ({ read }) => ({
183
- value: read.notes.ids().length,
184
- update: ({ impact, read }) => {
185
- const change = impact.collection(notes);
186
- if (
187
- change.kind === 'incremental' &&
188
- change.added.size === 0 &&
189
- change.removed.size === 0 &&
190
- change.updated.size === 0 &&
191
- !change.orderChanged
192
- ) {
193
- return { kind: 'unchanged' };
194
- }
195
- return {
196
- kind: 'changed',
197
- value: read.notes.ids().length,
198
- change,
199
- };
200
- },
201
- }),
202
- });
203
- ```
185
+ 根据每个 document source 的 commits 中的 impact,或上游 collection change,决定候选 key。依赖显式固定,不通过 reader 自动学习。Doxum 根据 equality 决定最终 change。
204
186
 
205
- 局部增量逻辑无法安全处理一个 commit 时,返回 `rebuild`。后创建的 view 可以在 `sources` 中声明更早创建的 view;Doxum 会按创建顺序处理该图,并在外部 listener 看到 commit 前将其 flush 完成。
187
+ update 失败后丢弃实例并限一次全新 build 恢复;持续失败阻断下游,独立分支继续。rebuild 经过同一调度图,不手动 emit。projection 随 owner dispose。使用 projection.batch 包住首次 document commit 之前到 editor cleanup 结束的完整同步动作;batch 内派生读取保持上次发布状态。
206
188
 
207
189
  ## 在 React 中绑定读模型
208
190