doxum 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +217 -296
  2. package/dist/contract-BNStLbSE.d.ts +441 -0
  3. package/dist/contract-CIU5FCC1.d.cts +441 -0
  4. package/dist/driver-BlR81Dqg.js +200 -0
  5. package/dist/driver-BlR81Dqg.js.map +1 -0
  6. package/dist/driver-xOIkwrB8.cjs +241 -0
  7. package/dist/driver-xOIkwrB8.cjs.map +1 -0
  8. package/dist/index.cjs +666 -2222
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +10 -20
  11. package/dist/index.d.ts +10 -18
  12. package/dist/index.js +634 -2189
  13. package/dist/index.js.map +1 -1
  14. package/dist/{integration-CZwCwFBS.cjs → integration-C87tjRop.cjs} +4 -9
  15. package/dist/integration-C87tjRop.cjs.map +1 -0
  16. package/dist/{integration-B56u1l9V.js → integration-D5XCBLJ8.js} +2 -7
  17. package/dist/integration-D5XCBLJ8.js.map +1 -0
  18. package/dist/integration.cjs +11 -11
  19. package/dist/integration.d.cts +10 -4
  20. package/dist/integration.d.ts +10 -4
  21. package/dist/integration.js +4 -3
  22. package/dist/issue-DVaGQGeP.js +576 -0
  23. package/dist/issue-DVaGQGeP.js.map +1 -0
  24. package/dist/issue-DhrNdQNg.cjs +797 -0
  25. package/dist/issue-DhrNdQNg.cjs.map +1 -0
  26. package/dist/local-sync.cjs +67 -39
  27. package/dist/local-sync.cjs.map +1 -1
  28. package/dist/local-sync.d.cts +10 -10
  29. package/dist/local-sync.d.ts +10 -10
  30. package/dist/local-sync.js +66 -38
  31. package/dist/local-sync.js.map +1 -1
  32. package/dist/react.cjs +23 -4
  33. package/dist/react.cjs.map +1 -1
  34. package/dist/react.d.cts +8 -3
  35. package/dist/react.d.ts +8 -3
  36. package/dist/react.js +21 -6
  37. package/dist/react.js.map +1 -1
  38. package/dist/store-1Uob0Ghk.cjs +2750 -0
  39. package/dist/store-1Uob0Ghk.cjs.map +1 -0
  40. package/dist/store-CD0KdGsq.d.cts +262 -0
  41. package/dist/store-D7QH6Rzw.js +2511 -0
  42. package/dist/store-D7QH6Rzw.js.map +1 -0
  43. package/dist/store-cp5CpfCy.d.ts +262 -0
  44. package/package.json +1 -1
  45. package/skills/doxum-runtime/SKILL.md +26 -44
  46. package/skills/doxum-runtime/references/guide.en.md +95 -359
  47. package/skills/doxum-runtime/references/guide.zh-CN.md +80 -285
  48. package/skills/doxum-runtime/references/invariants.en.md +42 -165
  49. package/skills/doxum-runtime/references/invariants.zh-CN.md +34 -97
  50. package/skills/doxum-runtime/references/patterns.en.md +73 -225
  51. package/skills/doxum-runtime/references/patterns.zh-CN.md +73 -179
  52. package/dist/chunk-pbuEa-1d.js +0 -13
  53. package/dist/contract-DNZ4D53r.d.ts +0 -563
  54. package/dist/contract-j3SLGAwh.d.cts +0 -563
  55. package/dist/driver-CNxqMVFH.cjs +0 -69
  56. package/dist/driver-CNxqMVFH.cjs.map +0 -1
  57. package/dist/driver-CbzfW5MR.js +0 -46
  58. package/dist/driver-CbzfW5MR.js.map +0 -1
  59. package/dist/integration-B56u1l9V.js.map +0 -1
  60. package/dist/integration-CZwCwFBS.cjs.map +0 -1
  61. package/dist/ownership-CY0nPXGF.cjs +0 -304
  62. package/dist/ownership-CY0nPXGF.cjs.map +0 -1
  63. package/dist/ownership-CduRygE7.js +0 -245
  64. package/dist/ownership-CduRygE7.js.map +0 -1
  65. package/dist/runtime-0mOFbe_H.cjs +0 -2439
  66. package/dist/runtime-0mOFbe_H.cjs.map +0 -1
  67. package/dist/runtime-B22tuj9A.d.cts +0 -150
  68. package/dist/runtime-BFmhzPpZ.d.ts +0 -150
  69. package/dist/runtime-DF1q9Gje.js +0 -2129
  70. package/dist/runtime-DF1q9Gje.js.map +0 -1
@@ -1,367 +1,103 @@
1
- # Doxum Runtime Guide
1
+ # Doxum Guide
2
2
 
3
- This is the task-oriented public guide for Doxum. It describes how to model,
4
- read, mutate, observe, and project one in-memory document with `doxum`, persist
5
- one browser-origin document with `doxum/local-sync`, and bind read models with
6
- `doxum/react`.
7
-
8
- The `doxum` core entry owns typed document state, atomic mutation, history,
9
- impact, subscriptions, and derived views. `doxum/local-sync` is the optional
10
- browser adapter for IndexedDB-backed offline editing and same-origin cross-tab
11
- sync. Your application still owns network ordering, authorization, and conflict
12
- resolution.
13
-
14
- ## Choose the right entry point
15
-
16
- | Goal | Use |
17
- | ------------------------------------- | -------------------------------------------------------- |
18
- | Define document shape | `schema`, `field`, `object`, and collection constructors |
19
- | Create the canonical runtime | `createDocument` |
20
- | Persist and sync one browser document | `attachLocalSync` from `doxum/local-sync` |
21
- | Read once | `select(runtime, read => ...)` |
22
- | Make local business changes | `runtime.update(tx => ...)` |
23
- | Replay persisted or remote operations | `runtime.apply(operations, options)` |
24
- | Replace an entire trusted snapshot | `runtime.replace(document, options)` |
25
- | Observe one schema location | `schema.value` plus `runtime.subscribe` |
26
- | Observe a table or map | `schema.collection` plus `runtime.subscribe` |
27
- | Maintain mapped collection data | `projection.map` |
28
- | Maintain an aggregate or index | `projection.value / projection.collection` |
29
- | Read in React | `useDocumentSelector`, `useReadable`, or `useReadable` |
30
-
31
- Do not write a second mutable copy of the document. `createDocument` is the
32
- only owner of canonical state.
33
-
34
- ## Start with a schema
35
-
36
- A schema is both the TypeScript shape of the document and the authoritative
37
- address model for writers, operations, selectors, and subscriptions. Define it
38
- once and keep it close to the domain it describes.
39
-
40
- ```ts
41
- import { createDocument, field, object, schema, table } from 'doxum';
42
-
43
- const task = object({
44
- title: field<string>(),
45
- completed: field<boolean>(),
46
- });
47
-
48
- const taskSchema = schema({
49
- title: field<string>(),
50
- tasks: table(task),
51
- });
52
-
53
- const runtime = createDocument({
54
- schema: taskSchema,
55
- initial: {
56
- title: 'Launch Doxum',
57
- tasks: {
58
- ids: ['write-guide'],
59
- byId: {
60
- 'write-guide': { title: 'Write the guide', completed: false },
61
- },
62
- },
63
- },
64
- });
65
- ```
66
-
67
- `table` preserves application-visible order through `{ ids, byId }`. `map`
68
- stores id-indexed entities without order. Use `object` for one structured
69
- entity, `dict` for scalar key/value data, `list` for an ordered
70
- sequence with an application-supplied stable key, and `tree` for a validated
71
- single-root hierarchy. See [patterns.en.md](patterns.en.md) for modelling
72
- guidance.
73
-
74
- ## Read through readers
75
-
76
- The runtime does not expose its mutable document. Read through a callback:
77
-
78
- ```ts
79
- import { select } from 'doxum';
80
-
81
- const openTitles = select(runtime, read =>
82
- read.tasks.ids().flatMap(id => {
83
- const task = read.tasks.get(id);
84
- return task && !task.completed.get() ? [task.title.get()] : [];
85
- })
86
- );
87
- ```
88
-
89
- Readers are intentionally shaped by the schema:
90
-
91
- - A field has `get()`.
92
- - A table or map has `ids()`, `has(id)`, and `get(id)`.
93
- - A list has `values()`, `length()`, and `at(index)`.
94
- - A tree has `rootId()`, `has(id)`, `value(id)`, `parent(id)`, and
95
- `children(id)`.
96
-
97
- Reader values for structural data are snapshots. Do not retain a transaction
98
- reader after `runtime.update` returns; it is only valid during that callback.
99
-
100
- ## Mutate atomically
101
-
102
- Use `runtime.update` for local, typed domain behavior. Its callback receives a
103
- short-lived `tx.read` and `tx.write`. Writers create operations for one atomic
104
- session; they never expose direct canonical mutation.
105
-
106
- ```ts
107
- const result = runtime.update(tx => {
108
- const task = tx.read.tasks.get('write-guide');
109
- if (!task) {
110
- tx.reject({
111
- code: 'task-not-found',
112
- message: 'The requested task no longer exists.',
113
- address: ['tasks', 'write-guide'],
114
- });
115
- }
116
-
117
- tx.write.tasks.item('write-guide').completed.set(true);
118
- return task.title.get();
119
- });
120
-
121
- if (result.status === 'committed') {
122
- console.log(result.value, result.commit.revision);
123
- } else if (result.status === 'rejected') {
124
- console.error(result.issues);
125
- }
126
- ```
127
-
128
- An update is synchronous and atomic:
129
-
130
- - If a writer emits a semantically invalid operation, Doxum rolls back the
131
- entire session and returns `status: 'rejected'` with `MutationIssue` values.
132
- - `tx.reject(...)` rolls back and returns your application
133
- `DocumentDiagnostic` values.
134
- - A normal thrown error also rolls back, then is rethrown to the caller.
135
- - Net-zero work returns `status: 'unchanged'` and publishes no commit.
136
-
137
- Use `tx.report(...)` for non-blocking application diagnostics. Committed and
138
- unchanged transaction results expose them as `reports`; reports and diagnostic
139
- addresses are copied and frozen before they are published.
140
-
141
- ## Use writers instead of constructing local operations
142
-
143
- For normal application behavior, writer APIs are clearer and preserve the
144
- schema domain:
145
-
146
- ```ts
147
- runtime.update(tx => {
148
- tx.write.title.set('Ship Doxum');
149
- tx.write.tasks.create(
150
- { id: 'release', value: { title: 'Publish the package', completed: false } },
151
- { after: 'write-guide' }
152
- );
153
- tx.write.tasks.item('release').title.set('Publish doxum');
154
- tx.write.tasks.move('release', { at: 'start' });
155
- tx.write.tasks.remove('write-guide');
156
- });
157
- ```
158
-
159
- For a table, `create`, `item`, `remove`, and `move` are available. A map has
160
- the same API except `move`, because it is unordered. Lists offer `insert`,
161
- `move`, `remove`, and `replace`; list identity comes from the `keyOf` function
162
- specified in the schema. See [patterns.en.md](patterns.en.md) for complete
163
- collection and tree examples.
164
-
165
- Optional field, variant, dict, list, and tree writers expose `clear()`. Optional
166
- structured leaves can also be initialized from an absent state with `replace()`.
167
- Optional objects, tables, and maps are rejected by the schema constructor and
168
- types. Model those containers as present and use their child or collection operations.
169
- Variant readers expose get() as a discriminated union. Dictionary readers use
170
- get(key), has(key), keys() and values(); lists also provide get(key)/has(key).
171
-
172
- For one action spanning several updates, open `runtime.history.group()` and
173
- call its `end()` to retain one undo entry or `cancel()` to revert the action.
174
- Groups cannot nest. History implements Readable, and `localSync.state` is also
175
- a Readable: both work with useReadable and projection.fromReadable.
176
-
177
- ## Replay operations at the boundary
178
-
179
- Use `apply` for operation batches that came from persistence, a network
180
- adapter, or another external boundary. Doxum decodes unknown operation payloads
181
- before mutation code observes them, resolves every address against the schema,
182
- and applies the batch atomically.
183
-
184
- ```ts
185
- const result = runtime.apply([{ type: 'field.set', at: ['title'], value: 'Restored title' }], {
186
- source: 'remote',
187
- history: false,
188
- });
189
-
190
- if (result.status === 'rejected') {
191
- // The document and revision remain unchanged.
192
- console.error(result.issues);
193
- }
194
- ```
195
-
196
- Treat external operation input as untrusted, even if TypeScript types make it
197
- look valid. Do not write a second path parser or validate operations by
198
- partially replaying them outside Doxum. A remote commit and every `replace`
199
- establish a new baseline, so they invalidate local undo/redo history.
200
-
201
- ## Interpret results and history
202
-
203
- Every mutation entry point returns one of three states:
204
-
205
- | Status | Meaning |
206
- | ----------- | -------------------------------------------------------- |
207
- | `committed` | Canonical state changed; the result contains a commit. |
208
- | `unchanged` | The net state did not change; the revision is unchanged. |
209
- | `rejected` | The whole batch rolled back; inspect `issues`. |
210
-
211
- Committed operations carry forward operations, inverse operations, a revision,
212
- and a `DocumentImpact`. Local history records local and system commits by
213
- default. Use `runtime.history.undo()` and `runtime.history.redo()`; they replay
214
- the inverse or forward operation batch through the same mutation pipeline.
215
-
216
- `observerErrors` on a committed result are failures from processors, flushes,
217
- or listeners after canonical state and history settled. They are not mutation
218
- failures and must not cause the caller to repeat the write.
219
-
220
- ## Attach local browser sync
221
-
222
- `attachLocalSync` is an optional browser attachment. Create and keep the
223
- runtime yourself, then await attachment before allowing the document to be used.
224
- It hydrates that runtime from an IndexedDB checkpoint and append-only command
225
- tail, then uses a Web Lock to choose exactly one writable tab. The `leader`
226
- uses normal synchronous runtime writes; every other attached tab is a
227
- `follower` read-only mirror. A direct follower write through `update`,
228
- `prepare`, `apply`, or `replace` throws `LocalSyncReadOnlyError`.
229
-
230
- The leader's local, system, and history commits are observed after they have
231
- settled and persisted to IndexedDB in order. BroadcastChannel carries only a
232
- new-head hint. Followers reload the durable tail and apply it as `remote`; this
233
- keeps their document ordered and invalidates their in-memory history. When the
234
- leader disposes, a caught-up follower takes the lock and becomes leader.
235
-
236
- Local-sync persists operation commands, not arbitrary new baselines. While it
237
- is attached, `runtime.replace()` and an externally supplied
238
- `runtime.apply(..., { source: 'remote' })` throw
239
- `LocalSyncUnsupportedOperationError`; internal hydration and tail replay use a
240
- trusted attachment path instead.
241
-
242
- ```ts
243
- import { attachLocalSync } from 'doxum/local-sync';
244
-
245
- const runtime = createDocument({ schema: taskSchema, initial });
246
- const localSync = await attachLocalSync({
247
- runtime,
248
- database: 'my-app',
249
- documentId: 'project-1',
250
- });
251
-
252
- if (localSync.state.current().status === 'leader') {
253
- runtime.update(tx => tx.write.title.set('Ship Doxum'));
254
- runtime.history.undo();
255
- }
256
-
257
- await localSync.flush(); // persist observed leader commands or catch up a follower
258
- await localSync.dispose();
259
- ```
260
-
261
- This is synchronous visibility with asynchronous persistence, not strict
262
- durability: a crash, storage failure, or invalid JSON payload can leave a
263
- visible leader commit unpersisted. Use `localSync.state.current()` and `onError` to show
264
- that condition. `flush()` is the explicit persistence/catch-up boundary. The
265
- attachment does not own or dispose the runtime and it does not expose an undo
266
- API: use `runtime.history.undo()` and `runtime.history.redo()` while the tab is
267
- leader. Runtime history is intentionally in-memory only; attachment hydration
268
- and remote tail replay invalidate it, so it is not transferred across reopening
269
- or leader handoff.
270
-
271
- ## Subscribe through schema-owned targets
272
-
273
- Create stable selectors from the schema, then subscribe to them. This is the
274
- shared address and impact model for the whole runtime.
3
+ ## Define, Update And Read
275
4
 
276
5
  ```ts
277
- const title = taskSchema.value(path => path.title);
278
- const tasks = taskSchema.collection(path => path.tasks);
279
-
280
- const stopTitle = runtime.subscribe(title, commit => {
281
- console.log('title changed at revision', commit.revision);
6
+ import { createDocument, field, map, object, select, snapshot, type Infer } from 'doxum';
7
+
8
+ const task = object({ title: field<string>(), done: field<boolean>() });
9
+ const model = object({ tasks: map(task) });
10
+ type DocumentValue = Infer<typeof model>;
11
+ const document = createDocument({
12
+ schema: model,
13
+ initial: { tasks: { a: { title: 'Write', done: false } } },
282
14
  });
283
-
284
- const stopTasks = runtime.subscribe(tasks, commit => {
285
- const change = commit.impact.collection(tasks);
286
- if (change.kind === 'incremental') {
287
- console.log(change.added, change.removed, change.updated, change.orderChanged);
288
- }
15
+ document.update(draft => {
16
+ const task = draft.tasks.a;
17
+ if (task) task.done = !task.done;
18
+ draft.tasks.b = { title: 'Review', done: false };
19
+ return { warnings: [] };
289
20
  });
290
-
291
- stopTitle();
292
- stopTasks();
293
- ```
294
-
295
- For value selectors, use `commit.impact.affects(target)`. For table or map
296
- selectors, use `commit.impact.collection(selector)`, which returns either a
297
- precise incremental change or `reset` after replacement. Do not recreate path
298
- comparison helpers in application modules.
299
-
300
- ## Build derived read models
301
-
302
- ```ts
303
- const projection = createProjectionRuntime({ onError: error => console.error(error) });
304
- const document = projection.document(runtime);
305
- const notes = document.collection(path => path.notes);
306
- const noteSummaries = projection.map(
307
- notes,
308
- (id, note) => ({ id, preview: note.body.get().slice(0, 80) }),
309
- { isEqual: (a, b) => a.id === b.id && a.preview === b.preview }
21
+ const done = select(document, state => state.tasks.a?.done);
22
+ const tasks = select(document, state => snapshot(state.tasks));
23
+ document.subscribe(
24
+ path => path.tasks.item('a').done,
25
+ commit => console.log(commit.changes)
310
26
  );
311
- const noteCount = projection.value({ notes }, ({ notes }) => notes.read.ids().length);
312
27
  ```
313
28
 
314
- projection.map produces stable ids/item readables and lazy all. Declare a
315
- DocumentCollectionSource directly in any processor's sources, or use
316
- `projection.value({ sources, build }, { isEqual }?)` and
317
- `projection.collection<Item>()(spec)` for explicit incremental logic.
318
- Map also accepts upstream projection collections. Document collection contexts
319
- provide batch-wide `candidates.keys`, `candidates.orderDirty` and `reset`.
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.
328
-
329
- ## React integration
330
-
331
- `doxum/react` uses `useSyncExternalStore` and tracked Doxum dependencies. A
332
- component re-renders only for commits that can affect the selector it read.
333
-
334
- ```tsx
335
- import { useDocumentSelector } from 'doxum/react';
336
-
337
- function OpenTaskCount() {
338
- const count = useDocumentSelector(
339
- runtime,
340
- read => read.tasks.ids().filter(id => !read.tasks.get(id)?.completed.get()).length
341
- );
342
-
343
- return <output>{count}</output>;
344
- }
345
- ```
346
-
347
- Use `useReadable(view.all)` for a `Readable`, `useReadable(view.item(id))`
348
- for one keyed value, and `useHistory(runtime.history)` for undo/redo state and
349
- actions. Keep `core` free of React imports; React-specific code belongs in the
350
- adapter or application layer.
351
-
352
- ## Lifecycle and ownership
353
-
354
- - The initial document is cloned when `createDocument` starts.
355
- - Structural payloads passed through operations are transferred into canonical
356
- state. Do not mutate them afterwards unless you intentionally want to mutate
357
- the canonical document.
358
- - Published commits, history payloads, diagnostics, and selector addresses are
359
- immutable snapshots.
360
- - Tree replacement snapshots are validated and cloned to preserve structural
361
- integrity.
362
- - Call `runtime.dispose()` when the runtime is no longer usable. Existing
363
- subscriptions, history state, and views should be disposed with their owners.
364
-
365
- For decision rules and anti-patterns, read
366
- [invariants.en.md](invariants.en.md). For copyable implementation patterns,
367
- read [patterns.en.md](patterns.en.md).
29
+ Root object is definition identity; runtime owns its data/revision. Updates are
30
+ synchronous and atomic. Reads see preceding writes. Draft and trusted internal
31
+ readWith scopes are borrowed for the synchronous callback and must not escape.
32
+ Throw TransactionRejected for expected rejection; ordinary throws
33
+ restore all work and rethrow unchanged. False/undefined returns are business values.
34
+
35
+ Object exposes editable members; field is atomic, including arrays and objects.
36
+ Atomic values are deeply readonly in Infer and scoped access. Inputs, snapshots,
37
+ commits and history share payload references. Object/variant structure accepts only
38
+ declared own properties (plus the variant discriminant); extra string, symbol and
39
+ non-enumerable properties are rejected. Use maps for dynamic keys and fields for
40
+ arbitrary payload objects. Never mutate payloads through any alias,
41
+ even after removal. Snapshot copies schema structure only; snapshot(rawPayload)
42
+ returns its original readonly reference. Published data is not frozen. Field copiers
43
+ are not supported; mutable exports and serialization belong to application boundaries.
44
+
45
+ Infer preserves optional properties and flat variant unions. Read/Draft contain
46
+ collection tools; assign(scope, key, inferValue) handles plain replacements containing
47
+ nested table/list/tree data.
48
+
49
+ ## Containers And Validation
50
+
51
+ | Definition | Data | Draft methods |
52
+ | -------------------------------- | ------------- | ------------------------------------------------------------- |
53
+ | map(valueSchema, { key }?) | record | indexing, assignment, delete |
54
+ | table(objectOrVariant, { key }?) | ids/byId | get/has/ids/create/remove/move |
55
+ | list(field, { keyOf }) | array | get/has/ids/insert/set/remove/move/replace |
56
+ | tree(field) | rootId?/nodes | get/has/rootId/parent/children/insert/set/remove/move/replace |
57
+
58
+ Read scopes expose only read methods. Map supports field/object/variant values.
59
+ List replacement retains the addressed key. Simple arrays and strokes can be one
60
+ atomic field. Optional supports field/variant/map/list/tree. Absent differs from
61
+ present undefined. Variant tags are readonly; change branch by whole replacement.
62
+
63
+ Pure synchronous functions and Standard Schema v1 validators receive original input.
64
+ They must not mutate it; successful output is ignored, with no copy or deep conversion
65
+ check. Transform values before entering Doxum. parse(model, unknown) copies validated
66
+ structure and shares readonly payloads; strict parse requires atomic validators.
67
+ Branded map/table keys flow through access, symbolic paths and impact.
68
+ Path callbacks describe locations, including absent entries, and compile at registration.
69
+ React useDocumentSelector tracks actual reads and changes dependencies when branching.
70
+
71
+ ## Changes And Consumers
72
+
73
+ Commits contain revision/source/changes/impact. ChangeSet groups members by owning
74
+ container, with direct added/removed/updated transitions and optional before/after
75
+ order in the same group. Order-only groups have empty members; standalone order
76
+ changes and duplicate groups are invalid. Each group is applied completely before
77
+ the next. Touched tree
78
+ nodes retain structural semantics; reset is an explicit whole-document transition.
79
+ Root member groups remain incremental. Net-zero changes do not publish.
80
+ apply(changes, { expectedRevision }) rejects a missing or mismatched local baseline.
81
+ Received before values are untrusted; local undo records actual old state.
82
+ History travels complete ChangeSets; grouped travel is atomic. Local replace is a
83
+ reversible root reset; remote commits invalidate local history. Observer errors occur
84
+ after acceptance.
85
+
86
+ Use `project(document, path => path.tasks, mapper)` for incremental mapping.
87
+ Pure values use `project(sources, compute)`; stateful algorithms use tagged value
88
+ or collection specs. Definitions are lazy and are materialized by a
89
+ `createProjectionStore({ onError })` instance. Sources remain explicit.
90
+ `input` and `project(readable)` connect external boundary values. Dispose the store
91
+ with the owning service. `store.batch` defers projection settlement/listeners, not document commits/listeners. Reads
92
+ inside a batch see the last publication; no cross-document rollback is provided.
93
+
94
+ doxum/local-sync attaches IndexedDB and Web Lock leadership. Only the leader writes;
95
+ followers replay contiguous durable sequence. Writes become visible before async
96
+ persistence; flush waits for durability. External replace and external remote-marked
97
+ apply are forbidden while attached. Version 5 / format 3 rejects old storage without
98
+ deleting or migrating it. The adapter accepts JSON values only. Change limits count
99
+ logical members and tree nodes, not just outer groups.
100
+ Limits govern new local commits only; existing durable commits remain readable
101
+ under smaller current limits. Treat the whole published ChangeSet as readonly:
102
+ its identity carries reusable structural validation, not authority to skip local
103
+ revision, schema or actual-before checks.