doxum 0.1.12 → 0.1.13

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 (64) hide show
  1. package/README.md +44 -39
  2. package/dist/advanced.cjs +116 -0
  3. package/dist/advanced.cjs.map +1 -0
  4. package/dist/advanced.d.cts +42 -0
  5. package/dist/advanced.d.ts +42 -0
  6. package/dist/advanced.js +115 -0
  7. package/dist/advanced.js.map +1 -0
  8. package/dist/{contract-CeAnEPBA.d.ts → contract-BT53Gg94.d.ts} +5 -4
  9. package/dist/{contract-DtGVSXSK.d.cts → contract-XmKnroJq.d.cts} +5 -4
  10. package/dist/definition-177L4Amk.cjs +898 -0
  11. package/dist/definition-177L4Amk.cjs.map +1 -0
  12. package/dist/definition-BlMeHwdz.d.ts +98 -0
  13. package/dist/definition-DXrKMVs7.js +785 -0
  14. package/dist/definition-DXrKMVs7.js.map +1 -0
  15. package/dist/definition-kxnWUvX9.d.cts +98 -0
  16. package/dist/{driver-xOIkwrB8.cjs → driver-BCyfqxsl.cjs} +1 -1
  17. package/dist/{driver-xOIkwrB8.cjs.map → driver-BCyfqxsl.cjs.map} +1 -1
  18. package/dist/{driver-BlR81Dqg.js → driver-CSnhMjbP.js} +1 -1
  19. package/dist/{driver-BlR81Dqg.js.map → driver-CSnhMjbP.js.map} +1 -1
  20. package/dist/index.cjs +31 -29
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +5 -4
  23. package/dist/index.d.ts +5 -4
  24. package/dist/index.js +7 -6
  25. package/dist/index.js.map +1 -1
  26. package/dist/{integration-CWjttg7H.js → integration-Bco8ZB16.js} +7 -3
  27. package/dist/integration-Bco8ZB16.js.map +1 -0
  28. package/dist/integration-CEUn96tx.cjs +52 -0
  29. package/dist/integration-CEUn96tx.cjs.map +1 -0
  30. package/dist/integration.cjs +7 -3
  31. package/dist/integration.d.cts +7 -3
  32. package/dist/integration.d.ts +7 -3
  33. package/dist/integration.js +3 -3
  34. package/dist/local-sync.cjs +1 -1
  35. package/dist/local-sync.d.cts +1 -1
  36. package/dist/local-sync.d.ts +1 -1
  37. package/dist/local-sync.js +1 -1
  38. package/dist/react.cjs +63 -36
  39. package/dist/react.cjs.map +1 -1
  40. package/dist/react.d.cts +8 -7
  41. package/dist/react.d.ts +8 -7
  42. package/dist/react.js +63 -35
  43. package/dist/react.js.map +1 -1
  44. package/dist/store-CDeqZEE3.d.cts +28 -0
  45. package/dist/{store-7XBEcR8s.js → store-CtgvCVFg.js} +585 -922
  46. package/dist/store-CtgvCVFg.js.map +1 -0
  47. package/dist/{store-DBv58dns.cjs → store-DcDpDgjk.cjs} +595 -992
  48. package/dist/store-DcDpDgjk.cjs.map +1 -0
  49. package/dist/store-P5dJ7TVe.d.ts +28 -0
  50. package/package.json +6 -1
  51. package/skills/doxum-runtime/SKILL.md +5 -7
  52. package/skills/doxum-runtime/references/guide.en.md +12 -16
  53. package/skills/doxum-runtime/references/guide.zh-CN.md +9 -14
  54. package/skills/doxum-runtime/references/patterns.en.md +12 -20
  55. package/skills/doxum-runtime/references/patterns.zh-CN.md +12 -18
  56. package/skills/doxum-runtime/references/projections.en.md +56 -199
  57. package/skills/doxum-runtime/references/projections.zh-CN.md +50 -180
  58. package/dist/integration-BShL68Nc.cjs +0 -30
  59. package/dist/integration-BShL68Nc.cjs.map +0 -1
  60. package/dist/integration-CWjttg7H.js.map +0 -1
  61. package/dist/store-2i8pYXqa.d.ts +0 -263
  62. package/dist/store-7XBEcR8s.js.map +0 -1
  63. package/dist/store-DBv58dns.cjs.map +0 -1
  64. package/dist/store-Ks80xWS_.d.cts +0 -263
@@ -84,24 +84,18 @@ document.apply(
84
84
  ## 投影与 React
85
85
 
86
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);
87
+ const tasks = observe(document, path => path.tasks);
88
+ const titles = derive([tasks], tasks => new Map([...tasks].map(([id, task]) => [id, task.title])));
89
+ const total = derive([titles], titles => titles.size);
93
90
  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);
97
- const title = store.item(titles, taskId);
91
+ const scaled = derive([total, zoom], (total, zoom) => total * zoom);
92
+ const runtime = createProjectionRuntime({ onError: console.error });
93
+ runtime.get(scaled);
94
+ const title = runtime.get(tasks).get(taskId);
98
95
  ```
99
96
 
100
- 投影定义使用 `useProjection` 和一个 store,document.history 等既有 Readable
101
- 使用 `useReadable` `useHistory`。观察一个物化集合键时使用
102
- `useProjectionItem(titles, taskId)`,它忽略其他键和纯顺序变化。自定义集合 processor 通过
103
- writer.set/remove/order/replace 暂存输出,
104
- previous/next 读取只在作用域内有效。candidates 汇总整个 batch,以最终状态派生输出。
105
- React 追踪实际读取,但 processor 依赖仍显式声明。只有 source 键影响同名输出键时
106
- 才使用 mapper。跨集合 join 应声明全部 source,并按
107
- [Projection 参考](projections.zh-CN.md)维护领域反向依赖索引。
97
+ React 使用 Runtime 上下文中的 `useProjection` 读取 Projection,单键读取写成
98
+ `useProjection(projection, selector)`;`useInput` 返回值和 setter。高级集合 processor
99
+ `doxum/advanced` 引入,在同步 callback 中使用
100
+ `output.set/remove/order/replace` 与 previous/next。Processor 依赖仍显式声明,
101
+ React selector 追踪只属于消费端。
@@ -1,220 +1,77 @@
1
- # Doxum Projection Reference
2
-
3
- Use this reference when choosing, implementing, or reviewing a projection. The
4
- [public long-form contract](../../../docs/projections.md) provides a complete join
5
- example; source code inspection should not be necessary for normal application work.
6
-
7
- ## Choose The Form
8
-
9
- | Relationship | API |
10
- | --------------------------------------------- | ------------------------------------------------- |
11
- | Whole document source | `project(document)` |
12
- | Target-limited document source | `project(document, [pick, ...])` |
13
- | Document collection source | `project(document, pick)` |
14
- | One source key to the same output key | `project(collection, mapper)` |
15
- | Pure current values to one value | `project(sources, compute)` |
16
- | Stateful incremental value | `project({ kind: 'value', sources, build })` |
17
- | Multiple sources or cross-key collection work | `project({ kind: 'collection', sources, build })` |
18
- | Application input | `input(initial)` and `store.set(input, value)` |
19
- | Existing `Readable` | `project(readable)` |
20
-
21
- Definitions are lazy and reusable. `createProjectionStore({ onError })` owns one
22
- materialization graph. Every store has independent published values, revisions,
23
- inputs, subscriptions, processor instances, and closure indexes.
24
-
25
- An ordinary mapper is deliberately limited to:
26
-
27
- ```text
28
- source key K changed -> output key K may change
29
- ```
30
-
31
- It rebuilds on reset and follows source order, but does not track reads performed
32
- inside the mapper. Never read another collection in a mapper and assume it becomes
33
- a dependency. Declare all sources in an advanced processor instead.
1
+ # Projection Reference
34
2
 
35
- ## Source Event Contract
36
-
37
- Advanced processors receive events, not just current values:
3
+ Projection definitions are lazy and reusable. The public core consists of
4
+ `Projection<T>`, `ProjectionRuntime`, `input`, `observe`, and tuple `derive`.
38
5
 
39
6
  ```ts
40
- type ValueEvent<T> = {
41
- value: T;
42
- previous: T;
43
- changed: boolean;
44
- revision: number;
45
- reset: boolean;
46
- };
47
-
48
- type CollectionEvent<K extends string, V> = {
49
- get(key: K): V | undefined;
50
- has(key: K): boolean;
51
- ids(): readonly K[];
52
- change: CollectionImpact<K> | undefined;
53
- revision: number;
54
- reset: boolean;
55
- };
56
-
57
- type DocumentEvent<S> = {
58
- read: Read<S>;
59
- revision: number;
60
- commits: readonly DocumentCommit<S>[];
61
- reset: boolean;
62
- };
63
-
64
- type DocumentCollectionEvent<S, N, K extends string> = {
65
- read: CollectionAccess<K, N>;
66
- revision: number;
67
- commits: readonly DocumentCommit<S>[];
68
- reset: boolean;
69
- candidates: { keys: readonly K[]; orderDirty: boolean };
70
- };
7
+ const tasks = observe(document, path => path.tasks);
8
+ const filter = input<'all' | 'open'>('all');
9
+ const visible = derive([tasks, filter], (tasks, filter) =>
10
+ filter === 'all' ? tasks : new Map([...tasks].filter(([, task]) => !task.done))
11
+ );
12
+ const runtime = createProjectionRuntime({ onError: report });
13
+ runtime.get(visible);
71
14
  ```
72
15
 
73
- Projected collection `change` is incremental (`added`, `removed`, `updated`,
74
- `orderChanged`), reset, or undefined. Document collection candidates are the
75
- union across all relevant commits in a store batch. A later commit may cancel an
76
- earlier one without removing its candidate key. Always derive from final `read`
77
- state. Use `commit.impact.collection(pick)` for per-commit relationship changes.
16
+ `input(initial, equality?)` is Runtime-local and can be written only with
17
+ `runtime.set(input, value)`. `observe(document, selector)` compiles a document
18
+ boundary lazily. Collection paths publish immutable map-like snapshots; the
19
+ whole-document form publishes a snapshot of the root. Existing Doxum Readables
20
+ can also be observed at this boundary.
78
21
 
79
- Across a batch, a value event's `previous` is the pre-batch value and `value` is
80
- the final value. A net-zero document transaction produces no commit or candidates.
22
+ `derive(dependencies, compute, equality?)` takes a tuple. Dependencies are
23
+ explicit and fixed when the definition is created. Processors must not discover
24
+ graph dependencies by reading other projections.
81
25
 
82
- ## Advanced Value Processor
26
+ The Runtime API is intentionally small:
83
27
 
84
28
  ```ts
85
- const value = project({
86
- kind: 'value',
87
- sources,
88
- build: events => ({
89
- value: buildValue(events),
90
- update: events =>
91
- needsRebuild(events)
92
- ? { kind: 'rebuild' }
93
- : changed(events)
94
- ? { kind: 'changed', value: updateValue(events) }
95
- : { kind: 'unchanged' },
96
- }),
97
- });
29
+ runtime.get(projection);
30
+ runtime.subscribe(projection, listener);
31
+ runtime.set(input, value);
32
+ runtime.batch({ cause }, run);
33
+ runtime.dispose();
98
34
  ```
99
35
 
100
- Prefer `project(sources, compute)` unless retained state prevents meaningful work.
36
+ There is no public revision, item handle, rebuild or release operation. Runtime
37
+ materialization, keyed storage, recovery and disposal are one owner.
101
38
 
102
- ## Advanced Collection Processor
39
+ ## Incremental processors
40
+
41
+ Import retained-state processors from `doxum/advanced`:
103
42
 
104
43
  ```ts
105
- const result = project({
106
- kind: 'collection',
107
- sources,
108
- name: 'optional diagnostic name',
109
- isEqual: Object.is,
110
- build: ({ sources, previous, next, writer }) => {
111
- // Build the complete output and any store-local indexes.
112
- return {
113
- update: ({ sources, previous, next, writer }) => {
114
- // Stage only affected keys, or return { kind: 'rebuild' }.
115
- },
116
- };
117
- },
44
+ const total = incremental([tasks], ({ sources, previous, state }) => {
45
+ state.calls = Number(state.calls ?? 0) + 1;
46
+ return sources[0].size + Number(state.calls) + (previous ?? 0);
118
47
  });
119
- ```
120
-
121
- `build` runs on first materialization, source reset, explicit `store.rebuild`, an
122
- update-requested rebuild, and fault recovery. Build starts from a cleared output
123
- and must stage the complete result. Its closure state is local to one store and
124
- must be reconstructible from sources.
125
48
 
126
- `update` runs when a declared source participates in settlement. Returning
127
- `{ kind: 'rebuild' }` discards staged update writes and performs a fresh build.
128
- All callbacks are synchronous; Promises and thenables are rejected. Reads and
129
- writes during processing or notification must not reenter a source or document.
130
-
131
- `previous`, `next`, source readers, and `writer` are borrowed for the current
132
- synchronous callback only:
133
-
134
- - `previous` sees only published output.
135
- - `next` immediately sees current staged writes.
136
- - `writer.set(key, value)` stages presence.
137
- - `writer.remove(key)` stages absence; a missing removal is a no-op.
138
- - Repeated operations on one key use the last operation.
139
- - `writer.order(ids)` supplies the entire next order, with every key exactly once.
140
- - `writer.replace(entries)` clears and replaces all output in entry order; keys
141
- must be unique.
142
-
143
- Staged output publishes atomically. Equality defaults to `Object.is`; equal sets
144
- keep the old reference. Net-zero output does not increment revision or notify.
145
-
146
- ## Cross-Collection Join Pattern
49
+ const doubled = incremental.collection([tasks], ({ sources, output }) => {
50
+ for (const [id, task] of sources[0]) output.set(id, task.value * 2);
51
+ });
52
+ ```
147
53
 
148
- Do not add a generic join abstraction. The domain owns missing references,
149
- cardinality, reconnect, delete, ordering, and result-key policy. Model the join as
150
- an advanced collection with explicit sources and indexes.
54
+ Value contexts expose `sources`, `previous`, `reset`, `change`, `cause` and a
55
+ retained `state` object. `incremental.collection(...)` uses a separate
56
+ collection processor protocol and additionally exposes borrowed
57
+ `previous`/`next` keyed reads and a callback-local `output` draft. Draft methods are
58
+ `set`, `remove`, `order`, and `replace`; the Runtime validates and seals them after
59
+ the synchronous callback, computes keyed transitions and publishes one immutable
60
+ map-like value. Reset or fault recovery is internal.
151
61
 
152
- For routes derived from `edges(from, to)` and node geometry, maintain:
62
+ ## React selector tracking
153
63
 
154
- ```text
155
- endpoints: edgeId -> [fromNodeId, toNodeId]
156
- adjacency: nodeId -> Set<edgeId>
64
+ ```tsx
65
+ const task = useProjection(visible, tasks => tasks.get(taskId));
66
+ const [mode, setMode] = useInput(filter);
157
67
  ```
158
68
 
159
- On build, scan edges once, populate both indexes, and write the complete routes.
160
- On an edge event:
161
-
162
- 1. Read exact changed edge ids from each commit's collection impact.
163
- 2. Detach every changed id from its old endpoints.
164
- 3. Read final edge state; attach its final endpoints, or delete its forward entry.
165
- 4. Add that edge id to the affected output set.
166
-
167
- On a node collection event, map added/updated/removed node ids through `adjacency`
168
- and add only adjacent edges to the affected set. Finally read current endpoint
169
- geometry and set or remove each affected route. Rebuild on either source reset.
170
-
171
- After an edge reconnects, the old node must no longer select it. Deleting an edge
172
- must remove both forward and reverse entries. Missing endpoint behavior is an
173
- explicit domain decision. Do not scan all edges for every node change unless the
174
- known collection size makes that tradeoff intentional.
175
-
176
- Closure indexes are not part of the writer's staged transaction. An uncaught
177
- processor failure triggers fresh-build recovery, but a processor that catches a
178
- failure and continues keeps its own mutations. Perform fallible work before
179
- mutating live indexes, stage temporary index changes and commit them on success,
180
- or request a complete rebuild.
181
-
182
- ## Batching, Faults, And Lifetime
183
-
184
- `store.batch` defers projection settlement and projection listeners only. Document
185
- commits and document listeners remain synchronous. Reads inside the batch return
186
- the last publication; settlement after the outer batch uses final source state.
187
- There is no cross-document rollback.
188
-
189
- Processor or writer validation failure publishes no partial output. The runtime
190
- may retry with a fresh build; persistent failure faults that projection and blocks
191
- its descendants while independent branches continue. Reads then throw
192
- `ProjectionError`. A later source update or `store.rebuild(projection)` can recover.
193
- `onError` also receives source, blocked, and listener failures. Listener failures
194
- never undo published projection state or accepted document commits.
195
-
196
- `store.release(projection)` releases an independently releasable materialization;
197
- do not release a node still used by a materialized downstream projection.
198
- `store.dispose()` invalidates the complete graph and releases its subscriptions.
199
-
200
- For one materialized collection entry, `store.item(collection, key)` returns a
201
- stable `Readable<V | undefined>`. It follows the key through add, update, remove,
202
- and recreation; other keys and order-only changes do not notify it. React uses
203
- `useProjectionItem(collection, key, store?)`. Key or store changes replace the
204
- subscription. The hook internally observes membership even when both absent and
205
- present values read as `undefined`. Raw document collection sources are not valid
206
- inputs; use `useDocumentSelector` for canonical document items.
207
-
208
- ## Review Checklist
209
-
210
- - All result-changing sources are declared.
211
- - Candidate selection covers add, update, remove, reconnect, and missing reference.
212
- - Batch logic reads final state instead of replaying imagined intermediate states.
213
- - Reset and explicit rebuild reconstruct complete output, order, and indexes.
214
- - Build never depends on prior output or a previous processor closure.
215
- - `writer.order` is complete and valid whenever used.
216
- - Equal values preserve references; unrelated changes produce no output revision.
217
- - Item consumers use keyed readables/hooks instead of whole-collection value projections.
218
- - A thrown update cannot silently corrupt closure indexes.
219
- - Tests cover unrelated commits, dynamic dependencies, stable references, reset,
220
- batch behavior, recovery, and disposal.
69
+ Collection `get`/`has` records one key, `keys` records key/order structure, and
70
+ `values`/iteration records the whole collection. Unrelated key changes do not run
71
+ the selector. After a related update, the selector's equality (default
72
+ `Object.is`) decides whether React re-renders. This tracking exists only at the
73
+ consumer boundary; projection dependencies remain explicit.
74
+
75
+ Processors settle before listeners. Writes are forbidden during notification.
76
+ Listener errors do not roll back an accepted document commit; processor errors use
77
+ the Runtime error callback and internal recovery.
@@ -1,203 +1,73 @@
1
- # Doxum Projection 参考
1
+ # Projection 参考
2
2
 
3
- 设计、实现或审查 projection 时使用本文。[公开完整契约](../../../docs/projections.md)
4
- 提供完整 join 示例;正常应用开发不应再要求阅读运行时源码。
3
+ Projection 定义是惰性、可复用的。默认入口只保留 `Projection<T>`、
4
+ `ProjectionRuntime`、`input`、`observe` tuple 形式的 `derive`。
5
5
 
6
- ## 选择定义形式
7
-
8
- | 依赖关系 | API |
9
- | ------------------------------ | ------------------------------------------------- |
10
- | 整个文档作为 source | `project(document)` |
11
- | 只关注指定文档路径 | `project(document, [pick, ...])` |
12
- | 一个文档集合 source | `project(document, pick)` |
13
- | 一个 source 键映射到同名输出键 | `project(collection, mapper)` |
14
- | 当前 source 值纯计算出一个值 | `project(sources, compute)` |
15
- | 有状态增量 value | `project({ kind: 'value', sources, build })` |
16
- | 多 source 或跨键集合计算 | `project({ kind: 'collection', sources, build })` |
17
- | 应用输入 | `input(initial)` 与 `store.set(input, value)` |
18
- | 既有 `Readable` | `project(readable)` |
19
-
20
- 定义是惰性、可复用的描述。`createProjectionStore({ onError })` 拥有一个物化图。
21
- 同一定义在不同 store 内分别拥有发布值、revision、input、订阅、processor 实例和闭包索引。
22
-
23
- 普通 mapper 的契约有意限制为:
24
-
25
- ```text
26
- source 键 K 变化 -> 输出键 K 可能变化
6
+ ```ts
7
+ const tasks = observe(document, path => path.tasks);
8
+ const filter = input<'all' | 'open'>('all');
9
+ const visible = derive([tasks, filter], (tasks, filter) =>
10
+ filter === 'all' ? tasks : new Map([...tasks].filter(([, task]) => !task.done))
11
+ );
12
+ const runtime = createProjectionRuntime({ onError: report });
13
+ runtime.get(visible);
27
14
  ```
28
15
 
29
- 它在 reset 时重建并跟随 source 顺序,但不会追踪 mapper 内的读取。不要在 mapper
30
- 里读取另一个集合并假定它自动成为依赖;应改用 advanced processor,并显式声明所有 source。
16
+ `input(initial, equality?)` 属于每个 Runtime,只能通过
17
+ `runtime.set(input, value)` 写入。`observe(document, selector)` 延迟编译文档
18
+ 边界;集合路径发布只读 map-like 快照,省略 selector 时发布整个文档快照。
19
+ 现有 Doxum `Readable` 也可以在这个边界接入。
31
20
 
32
- ## Source Event 契约
21
+ `derive(dependencies, compute, equality?)` 使用 tuple,依赖在定义创建时固定。
22
+ Processor 不能通过读取另一个 Projection 隐式建立图依赖。
33
23
 
34
- advanced processor 接收 event,而不只是当前值:
24
+ Runtime 的公开脊柱只有:
35
25
 
36
26
  ```ts
37
- type ValueEvent<T> = {
38
- value: T;
39
- previous: T;
40
- changed: boolean;
41
- revision: number;
42
- reset: boolean;
43
- };
44
-
45
- type CollectionEvent<K extends string, V> = {
46
- get(key: K): V | undefined;
47
- has(key: K): boolean;
48
- ids(): readonly K[];
49
- change: CollectionImpact<K> | undefined;
50
- revision: number;
51
- reset: boolean;
52
- };
53
-
54
- type DocumentEvent<S> = {
55
- read: Read<S>;
56
- revision: number;
57
- commits: readonly DocumentCommit<S>[];
58
- reset: boolean;
59
- };
60
-
61
- type DocumentCollectionEvent<S, N, K extends string> = {
62
- read: CollectionAccess<K, N>;
63
- revision: number;
64
- commits: readonly DocumentCommit<S>[];
65
- reset: boolean;
66
- candidates: { keys: readonly K[]; orderDirty: boolean };
67
- };
27
+ runtime.get(projection);
28
+ runtime.subscribe(projection, listener);
29
+ runtime.set(input, value);
30
+ runtime.batch({ cause }, run);
31
+ runtime.dispose();
68
32
  ```
69
33
 
70
- 投影集合的 `change` 可能是 incremental(`added`、`removed`、`updated`、
71
- `orderChanged`)、reset undefined。文档集合的 candidates 是一个 store batch
72
- 内所有相关 commit 的并集。后续 commit 即使抵消早先变化,也不会移除候选键。
73
- 必须用最终 `read` 状态计算输出。需要维护关系索引时,用
74
- `commit.impact.collection(pick)` 取得每次 commit 的精确变化。
34
+ revision、item handle、rebuild、release 都是 Runtime 内部事实,不是应用层操作。
35
+ Materialization、keyed storage、故障恢复和释放只有一个 owner。
75
36
 
76
- batch value event 的 `previous` 是 batch 前的值,`value` 是最终值。
77
- 单个净零文档事务不会产生 commit 或 candidates。
37
+ ## 增量 Processor
78
38
 
79
- ## Advanced Value Processor
39
+ 保留状态和 keyed patch 从 `doxum/advanced` 引入:
80
40
 
81
41
  ```ts
82
- const value = project({
83
- kind: 'value',
84
- sources,
85
- build: events => ({
86
- value: buildValue(events),
87
- update: events =>
88
- needsRebuild(events)
89
- ? { kind: 'rebuild' }
90
- : changed(events)
91
- ? { kind: 'changed', value: updateValue(events) }
92
- : { kind: 'unchanged' },
93
- }),
42
+ const total = incremental([tasks], ({ sources, previous, state }) => {
43
+ state.calls = Number(state.calls ?? 0) + 1;
44
+ return sources[0].size + Number(state.calls) + (previous ?? 0);
94
45
  });
95
- ```
96
-
97
- 除非保留状态能避免有意义的工作,否则优先使用 `project(sources, compute)`。
98
-
99
- ## Advanced Collection Processor
100
46
 
101
- ```ts
102
- const result = project({
103
- kind: 'collection',
104
- sources,
105
- name: 'optional diagnostic name',
106
- isEqual: Object.is,
107
- build: ({ sources, previous, next, writer }) => {
108
- // 构建完整输出和 store 局部索引。
109
- return {
110
- update: ({ sources, previous, next, writer }) => {
111
- // 只暂存受影响的键,或返回 { kind: 'rebuild' }。
112
- },
113
- };
114
- },
47
+ const doubled = incremental.collection([tasks], ({ sources, output }) => {
48
+ for (const [id, task] of sources[0]) output.set(id, task.value * 2);
115
49
  });
116
50
  ```
117
51
 
118
- 首次物化、source reset、显式 `store.rebuild`、update 请求 rebuild 以及 fault
119
- 恢复时会调用 `build`。build 从已清空的输出开始,必须写出完整结果。
120
- 其闭包状态只属于一个 store,并且必须能完全由 sources 重建。
121
-
122
- 任一已声明 source 参与结算时调用 `update`。返回 `{ kind: 'rebuild' }` 会丢弃
123
- 本次 update 暂存的写入,并执行全新 build。所有回调同步执行;Promise thenable
124
- 会被拒绝。processing 或 notification 中的读取和写入不得重入 source 或 document。
125
-
126
- `previous`、`next`、source reader 和 `writer` 都只在当前同步回调内有效:
127
-
128
- - `previous` 只看到已发布输出。
129
- - `next` 立即看到当前暂存的写入。
130
- - `writer.set(key, value)` 暂存存在值。
131
- - `writer.remove(key)` 暂存缺失;删除不存在的键是 no-op。
132
- - 同一键多次操作以最后一次为准。
133
- - `writer.order(ids)` 提供完整 next 顺序,每个输出键必须恰好出现一次。
134
- - `writer.replace(entries)` 清空并按 entries 顺序替换全部输出,键不能重复。
52
+ Value context 包含 `sources`、`previous`、`reset`、`change`、`cause` 和持久
53
+ `state`。`incremental.collection(...)` 使用独立的 collection processor 协议,
54
+ 另外提供借用的 `previous`/`next` keyed read,以及
55
+ 只在同步 callback 内有效的 `output` draft。Draft 只有 `set`、`remove`、`order`、
56
+ `replace`;Runtime callback 返回后校验并 seal,计算 keyed transitions,发布
57
+ 一个不可变 map-like 值。reset 或故障恢复由 Runtime 内部完成。
135
58
 
136
- 暂存输出原子发布。相等判断默认使用 `Object.is`,相等的 set 保留旧引用。
137
- 净零输出不增加 revision,也不通知 listener。
59
+ ## React selector 追踪
138
60
 
139
- ## 跨集合 Join 模式
140
-
141
- 不要增加通用 join 抽象。缺失引用、基数、重连、删除、顺序和结果键策略都属于领域逻辑。
142
- 应使用带显式 sources 和索引的 advanced collection 表达 join。
143
-
144
- 例如 route 由 `edges(from, to)` 和 node 几何信息计算,维护:
145
-
146
- ```text
147
- endpoints: edgeId -> [fromNodeId, toNodeId]
148
- adjacency: nodeId -> Set<edgeId>
61
+ ```tsx
62
+ const task = useProjection(visible, tasks => tasks.get(taskId));
63
+ const [mode, setMode] = useInput(filter);
149
64
  ```
150
65
 
151
- build 时扫描一次 edges,填充两个索引并写出全部 routes。edge 变化时:
152
-
153
- 1. 从每个 commit 的 collection impact 读取精确变化的 edge id。
154
- 2. 将每个变化 id 从旧 endpoints 中解绑。
155
- 3. 读取最终 edge 状态;绑定其最终 endpoints,或删除其正向索引。
156
- 4. 将该 edge id 加入受影响输出集合。
157
-
158
- node 集合变化时,把 added/updated/removed node id 通过 `adjacency` 映射为相邻
159
- edge,并只将这些 edge 加入受影响集合。最后读取当前 endpoint 几何信息,对每个
160
- 受影响 route 执行 set 或 remove。任一 source reset 都应 rebuild。
161
-
162
- edge 重连后,旧 node 不得再选中它;删除 edge 必须同时移除正向和反向索引。
163
- endpoint 缺失时的行为由领域显式定义。除非集合规模已知且权衡明确,不要在每次
164
- node 变化时扫描全部 edges。
165
-
166
- 闭包索引不属于 writer 的 staged transaction。未捕获的 processor 失败会触发 fresh
167
- build 恢复,但 processor 如果自行捕获失败后继续执行,其索引修改会保留。先完成可能
168
- 失败的工作再修改活动索引,或在临时状态上暂存索引变化并在成功后提交,或请求完整 rebuild。
169
-
170
- ## Batch、Fault 与生命周期
171
-
172
- `store.batch` 只推迟 projection 结算和 projection listener;文档 commit 和文档
173
- listener 仍同步执行。batch 内读取返回上次发布值,最外层 batch 结束后根据最终
174
- source 状态结算。不提供跨文档回滚。
175
-
176
- processor 或 writer 校验失败不会发布部分输出。runtime 可以用新 build 重试;
177
- 持续失败会使该 projection fault,并阻塞下游,独立分支仍继续工作。此时读取抛
178
- `ProjectionError`。后续 source 更新或 `store.rebuild(projection)` 可以恢复。
179
- `onError` 还接收 source、blocked 和 listener 失败。listener 失败不会撤销已发布
180
- projection 状态或已接受的文档 commit。
181
-
182
- `store.release(projection)` 释放可独立释放的物化结果;仍被已物化下游使用的节点
183
- 不能释放。`store.dispose()` 使整个图失效并解除其全部订阅。
184
-
185
- 观察一个物化集合项时,`store.item(collection, key)` 返回稳定的
186
- `Readable<V | undefined>`。它贯穿 add、update、remove 和 recreate;其他键和纯顺序
187
- 变化不会通知它。React 使用 `useProjectionItem(collection, key, store?)`,key 或 store
188
- 变化时替换订阅。hook 内部会观察 membership,即使 absent 与 present 值都读作
189
- `undefined`。原始文档集合 source 不能作为输入;canonical 文档项使用
190
- `useDocumentSelector`。
191
-
192
- ## 审查清单
193
-
194
- - 所有能改变结果的 source 都已声明。
195
- - 候选选择覆盖 add、update、remove、reconnect 和缺失引用。
196
- - batch 逻辑读取最终状态,而不是模拟中间状态。
197
- - reset 和显式 rebuild 能重建完整输出、顺序和索引。
198
- - build 不依赖旧输出或旧 processor 闭包。
199
- - 使用 `writer.order` 时提供完整且有效的顺序。
200
- - 相等结果保留引用;无关变化不产生输出 revision。
201
- - 单项消费者使用 keyed readable/hook,而不是整个集合的 value projection。
202
- - update 抛错不会悄悄破坏闭包索引。
203
- - 测试覆盖无关 commit、动态依赖、稳定引用、reset、batch、恢复和 disposal。
66
+ `get`/`has` 记录单 key,`keys` 记录 key/order 结构,`values` 或迭代记录整个
67
+ 集合。无关 key 的更新不会执行 selector;相关更新后才执行 selector,并由
68
+ `equality`(默认 `Object.is`)决定是否重渲染。这是消费端优化,不会反向构建
69
+ Projection processor 依赖。
70
+
71
+ Processor 先于 listener settle。通知期间禁止写入。listener 错误不会回滚已经
72
+ 接受的文档 commit;processor 错误交给 Runtime error callback,并由 Runtime
73
+ 内部恢复。
@@ -1,30 +0,0 @@
1
- const require_store = require("./store-DBv58dns.cjs");
2
- //#region core/src/access/dependency.ts
3
- const createDependencyTracker = () => {
4
- const targets = [];
5
- return {
6
- record: (value) => {
7
- if (!targets.some((entry) => require_store.same(entry, value))) targets.push(value);
8
- },
9
- snapshot: () => Object.freeze(targets.slice())
10
- };
11
- };
12
- //#endregion
13
- //#region core/src/integration.ts
14
- const track = (runtime, selector) => {
15
- const dependencies = createDependencyTracker();
16
- const value = require_store.readWith(runtime, selector, dependencies);
17
- return Object.freeze({
18
- value,
19
- targets: dependencies.snapshot()
20
- });
21
- };
22
- //#endregion
23
- Object.defineProperty(exports, "track", {
24
- enumerable: true,
25
- get: function() {
26
- return track;
27
- }
28
- });
29
-
30
- //# sourceMappingURL=integration-BShL68Nc.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"integration-BShL68Nc.cjs","names":["readWith"],"sources":["../core/src/access/dependency.ts","../core/src/integration.ts"],"sourcesContent":["import type { ImpactTarget } from '../schema';\nimport * as impactTarget from '../impact-target';\n\nexport type DependencyTracker = {\n readonly record: (target: ImpactTarget<unknown>) => void;\n readonly snapshot: () => readonly ImpactTarget<unknown>[];\n};\n\nexport const createDependencyTracker = (): DependencyTracker => {\n const targets: ImpactTarget<unknown>[] = [];\n return {\n record: value => {\n if (!targets.some(entry => impactTarget.same(entry, value))) targets.push(value);\n },\n snapshot: () => Object.freeze(targets.slice()),\n };\n};\n","import type { ObjectNode, ImpactTarget } from './schema';\nimport type { Read } from './access/scope';\nimport type { DocumentReadable } from './runtime/contract';\nimport { createDependencyTracker } from './access/dependency';\nimport { readWith } from './runtime/access';\nexport { projectionStoreDebug } from './projection/store';\nexport type { AddressRef } from './address';\nexport { contains, debugKey, overlaps, read as readAddress, resolveAddress } from './address';\nexport { subscribeDependencies } from './runtime/notification';\nexport { same as sameTarget } from './impact-target';\nexport type { ImpactTarget } from './schema';\n\nexport type TrackedSelection<TValue> = {\n readonly value: TValue;\n readonly targets: readonly ImpactTarget<unknown>[];\n};\n\n// Framework adapters receive one immutable result instead of coordinating a\n// mutable collector with the reader's scoped lifetime themselves.\nexport const track = <TSchema extends ObjectNode, TValue>(\n runtime: DocumentReadable<TSchema>,\n selector: (read: Read<TSchema>) => TValue\n): TrackedSelection<TValue> => {\n const dependencies = createDependencyTracker();\n const value = readWith(runtime, selector, dependencies);\n return Object.freeze({ value, targets: dependencies.snapshot() });\n};\n"],"mappings":";;AAQA,MAAa,gCAAmD;CAC9D,MAAM,UAAmC,EAAE;AAC3C,QAAO;EACL,SAAQ,UAAS;AACf,OAAI,CAAC,QAAQ,MAAK,UAAA,cAAA,KAA2B,OAAO,MAAM,CAAC,CAAE,SAAQ,KAAK,MAAM;;EAElF,gBAAgB,OAAO,OAAO,QAAQ,OAAO,CAAC;EAC/C;;;;ACIH,MAAa,SACX,SACA,aAC6B;CAC7B,MAAM,eAAe,yBAAyB;CAC9C,MAAM,QAAQA,cAAAA,SAAS,SAAS,UAAU,aAAa;AACvD,QAAO,OAAO,OAAO;EAAE;EAAO,SAAS,aAAa,UAAU;EAAE,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"integration-CWjttg7H.js","names":["impactTarget.same"],"sources":["../core/src/access/dependency.ts","../core/src/integration.ts"],"sourcesContent":["import type { ImpactTarget } from '../schema';\nimport * as impactTarget from '../impact-target';\n\nexport type DependencyTracker = {\n readonly record: (target: ImpactTarget<unknown>) => void;\n readonly snapshot: () => readonly ImpactTarget<unknown>[];\n};\n\nexport const createDependencyTracker = (): DependencyTracker => {\n const targets: ImpactTarget<unknown>[] = [];\n return {\n record: value => {\n if (!targets.some(entry => impactTarget.same(entry, value))) targets.push(value);\n },\n snapshot: () => Object.freeze(targets.slice()),\n };\n};\n","import type { ObjectNode, ImpactTarget } from './schema';\nimport type { Read } from './access/scope';\nimport type { DocumentReadable } from './runtime/contract';\nimport { createDependencyTracker } from './access/dependency';\nimport { readWith } from './runtime/access';\nexport { projectionStoreDebug } from './projection/store';\nexport type { AddressRef } from './address';\nexport { contains, debugKey, overlaps, read as readAddress, resolveAddress } from './address';\nexport { subscribeDependencies } from './runtime/notification';\nexport { same as sameTarget } from './impact-target';\nexport type { ImpactTarget } from './schema';\n\nexport type TrackedSelection<TValue> = {\n readonly value: TValue;\n readonly targets: readonly ImpactTarget<unknown>[];\n};\n\n// Framework adapters receive one immutable result instead of coordinating a\n// mutable collector with the reader's scoped lifetime themselves.\nexport const track = <TSchema extends ObjectNode, TValue>(\n runtime: DocumentReadable<TSchema>,\n selector: (read: Read<TSchema>) => TValue\n): TrackedSelection<TValue> => {\n const dependencies = createDependencyTracker();\n const value = readWith(runtime, selector, dependencies);\n return Object.freeze({ value, targets: dependencies.snapshot() });\n};\n"],"mappings":";;AAQA,MAAa,gCAAmD;CAC9D,MAAM,UAAmC,EAAE;AAC3C,QAAO;EACL,SAAQ,UAAS;AACf,OAAI,CAAC,QAAQ,MAAK,UAASA,KAAkB,OAAO,MAAM,CAAC,CAAE,SAAQ,KAAK,MAAM;;EAElF,gBAAgB,OAAO,OAAO,QAAQ,OAAO,CAAC;EAC/C;;;;ACIH,MAAa,SACX,SACA,aAC6B;CAC7B,MAAM,eAAe,yBAAyB;CAC9C,MAAM,QAAQ,SAAS,SAAS,UAAU,aAAa;AACvD,QAAO,OAAO,OAAO;EAAE;EAAO,SAAS,aAAa,UAAU;EAAE,CAAC"}