doxum 0.1.2 → 0.1.3

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 +59 -27
  2. package/dist/{contract-DGkZcTiP.d.cts → contract-BsY1XLiG.d.ts} +2 -1
  3. package/dist/{contract-Dh3az8aN.d.ts → contract-D8kjdfqT.d.cts} +2 -1
  4. package/dist/{access-5D4KKFm7.js → dependency-C5_R1tvQ.js} +40 -262
  5. package/dist/dependency-C5_R1tvQ.js.map +1 -0
  6. package/dist/{access-z56fVqZ2.cjs → dependency-Dfzs3khk.cjs} +62 -344
  7. package/dist/dependency-Dfzs3khk.cjs.map +1 -0
  8. package/dist/driver-CNxqMVFH.cjs +69 -0
  9. package/dist/driver-CNxqMVFH.cjs.map +1 -0
  10. package/dist/driver-CbzfW5MR.js +46 -0
  11. package/dist/driver-CbzfW5MR.js.map +1 -0
  12. package/dist/index.cjs +2636 -46
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +2594 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/integration.cjs +2 -3
  19. package/dist/integration.cjs.map +1 -1
  20. package/dist/integration.d.cts +1 -1
  21. package/dist/integration.d.ts +1 -1
  22. package/dist/integration.js +1 -2
  23. package/dist/integration.js.map +1 -1
  24. package/dist/local-sync.cjs +154 -329
  25. package/dist/local-sync.cjs.map +1 -1
  26. package/dist/local-sync.d.cts +17 -22
  27. package/dist/local-sync.d.ts +17 -22
  28. package/dist/local-sync.js +149 -326
  29. package/dist/local-sync.js.map +1 -1
  30. package/dist/ownership-B-VAPcce.js +242 -0
  31. package/dist/ownership-B-VAPcce.js.map +1 -0
  32. package/dist/ownership-CU-9DKJQ.cjs +301 -0
  33. package/dist/ownership-CU-9DKJQ.cjs.map +1 -0
  34. package/package.json +1 -1
  35. package/skills/doxum-runtime/references/guide.en.md +52 -1
  36. package/skills/doxum-runtime/references/guide.zh-CN.md +46 -1
  37. package/skills/doxum-runtime/references/invariants.en.md +14 -4
  38. package/skills/doxum-runtime/references/invariants.zh-CN.md +1 -1
  39. package/dist/access-5D4KKFm7.js.map +0 -1
  40. package/dist/access-z56fVqZ2.cjs.map +0 -1
  41. package/dist/dependency-BRn1TRDi.js +0 -20
  42. package/dist/dependency-BRn1TRDi.js.map +0 -1
  43. package/dist/dependency-DmQXyj7q.cjs +0 -25
  44. package/dist/dependency-DmQXyj7q.cjs.map +0 -1
  45. package/dist/readable-B8E3FMq6.cjs +0 -2614
  46. package/dist/readable-B8E3FMq6.cjs.map +0 -1
  47. package/dist/readable-CIj02gIj.js +0 -2579
  48. package/dist/readable-CIj02gIj.js.map +0 -1
@@ -0,0 +1,301 @@
1
+ //#region core/src/profile.ts
2
+ const active = { current: void 0 };
3
+ const profile = {
4
+ clone: {
5
+ call: () => {
6
+ const value = active.current;
7
+ if (value) value.clone.calls += 1;
8
+ },
9
+ node: (reason) => {
10
+ const value = active.current;
11
+ if (!value) return;
12
+ value.clone.nodes[reason] += 1;
13
+ },
14
+ container: () => {
15
+ const value = active.current;
16
+ if (value) value.clone.containers += 1;
17
+ },
18
+ deepEqual: () => {
19
+ const value = active.current;
20
+ if (value) value.clone.deepEqual.calls += 1;
21
+ },
22
+ deepEqualContainer: () => {
23
+ const value = active.current;
24
+ if (value) value.clone.deepEqual.containers += 1;
25
+ },
26
+ initialDocument: () => {
27
+ const value = active.current;
28
+ if (value) value.clone.documents.initial += 1;
29
+ }
30
+ },
31
+ mutation: {
32
+ normalized: () => {
33
+ const value = active.current;
34
+ if (value) value.mutation.normalized += 1;
35
+ },
36
+ executed: () => {
37
+ const value = active.current;
38
+ if (value) value.mutation.executed += 1;
39
+ },
40
+ inverse: () => {
41
+ const value = active.current;
42
+ if (value) value.mutation.inverseCreated += 1;
43
+ }
44
+ },
45
+ batch: {
46
+ operation: () => {
47
+ const value = active.current;
48
+ if (value) value.batch.operations += 1;
49
+ },
50
+ entry: (amount = 1) => {
51
+ const value = active.current;
52
+ if (value) value.batch.entries += amount;
53
+ },
54
+ collectionResolved: () => {
55
+ const value = active.current;
56
+ if (value) value.batch.collectionsResolved += 1;
57
+ },
58
+ journalRecord: () => {
59
+ const value = active.current;
60
+ if (value) value.batch.journalRecords += 1;
61
+ },
62
+ rejected: () => {
63
+ const value = active.current;
64
+ if (value) value.batch.rejected += 1;
65
+ },
66
+ inverse: () => {
67
+ const value = active.current;
68
+ if (value) value.batch.inverseOperations += 1;
69
+ },
70
+ payloadTransferred: () => {
71
+ const value = active.current;
72
+ if (value) value.batch.payloadTransferred += 1;
73
+ },
74
+ payloadSnapshot: () => {
75
+ const value = active.current;
76
+ if (value) value.batch.payloadSnapshots += 1;
77
+ }
78
+ },
79
+ journal: {
80
+ subject: () => {
81
+ const value = active.current;
82
+ if (value) value.journal.subjects += 1;
83
+ },
84
+ comparison: () => {
85
+ const value = active.current;
86
+ if (value) value.journal.comparisons += 1;
87
+ },
88
+ absorbed: () => {
89
+ const value = active.current;
90
+ if (value) value.journal.absorbed += 1;
91
+ },
92
+ orderSnapshot: (items) => {
93
+ const value = active.current;
94
+ if (!value) return;
95
+ value.journal.orderSnapshots += 1;
96
+ value.journal.orderItems += items;
97
+ }
98
+ },
99
+ address: {
100
+ schemaStep: () => {
101
+ const value = active.current;
102
+ if (value) value.address.schemaSteps += 1;
103
+ },
104
+ documentStep: () => {
105
+ const value = active.current;
106
+ if (value) value.address.documentSteps += 1;
107
+ },
108
+ arrayCopied: () => {
109
+ const value = active.current;
110
+ if (value) value.address.arraysCopied += 1;
111
+ },
112
+ prefixComparison: () => {
113
+ const value = active.current;
114
+ if (value) value.address.prefixComparisons += 1;
115
+ },
116
+ segmentCompared: () => {
117
+ const value = active.current;
118
+ if (value) value.address.segmentsCompared += 1;
119
+ }
120
+ },
121
+ impact: { affects: () => {
122
+ const value = active.current;
123
+ if (value) value.impact.affectsChecks += 1;
124
+ } },
125
+ reader: {
126
+ session: () => {
127
+ const value = active.current;
128
+ if (value) value.reader.sessions += 1;
129
+ },
130
+ lookup: () => {
131
+ const value = active.current;
132
+ if (value) value.reader.nodeLookups += 1;
133
+ },
134
+ collectionIds: (amount = 1) => {
135
+ const value = active.current;
136
+ if (value) value.reader.collectionIds += amount;
137
+ },
138
+ structuralSnapshot: () => {
139
+ const value = active.current;
140
+ if (value) value.reader.structuralSnapshots += 1;
141
+ }
142
+ },
143
+ collectionView: {
144
+ mapped: () => {
145
+ const value = active.current;
146
+ if (value) value.collectionView.mappedItems += 1;
147
+ },
148
+ idsScanned: (amount = 1) => {
149
+ const value = active.current;
150
+ if (value) value.collectionView.idsScanned += amount;
151
+ },
152
+ arrayCopied: () => {
153
+ const value = active.current;
154
+ if (value) value.collectionView.arraysCopied += 1;
155
+ }
156
+ },
157
+ materialized: {
158
+ updated: () => {
159
+ const value = active.current;
160
+ if (value) value.materialized.updated += 1;
161
+ },
162
+ skipped: () => {
163
+ const value = active.current;
164
+ if (value) value.materialized.skipped += 1;
165
+ },
166
+ rebuilt: () => {
167
+ const value = active.current;
168
+ if (value) value.materialized.rebuilt += 1;
169
+ },
170
+ sourceChanged: () => {
171
+ const value = active.current;
172
+ if (value) value.materialized.sourceChanges += 1;
173
+ },
174
+ notification: () => {
175
+ const value = active.current;
176
+ if (value) value.materialized.notifications += 1;
177
+ }
178
+ }
179
+ };
180
+ //#endregion
181
+ //#region core/src/value/ownership.ts
182
+ const stablePayloads = /* @__PURE__ */ new WeakSet();
183
+ const markStablePayload = (value) => {
184
+ if (Array.isArray(value) || isPlainObject(value)) stablePayloads.add(value);
185
+ return value;
186
+ };
187
+ const isStablePayload = (value) => (Array.isArray(value) || isPlainObject(value)) && stablePayloads.has(value);
188
+ const isPlainObject = (value) => {
189
+ if (value === null || typeof value !== "object") return false;
190
+ const prototype = Object.getPrototypeOf(value);
191
+ return prototype === Object.prototype || prototype === null;
192
+ };
193
+ const countCloneReason = (reason) => {
194
+ if (reason) profile.clone.node(reason);
195
+ };
196
+ const cloneValue = (value, reason) => {
197
+ profile.clone.call();
198
+ countCloneReason(reason);
199
+ if (Array.isArray(value)) {
200
+ profile.clone.container();
201
+ const result = new Array(value.length);
202
+ for (let index = 0; index < value.length; index += 1) result[index] = cloneValue(value[index], reason);
203
+ return reason === "commit" || reason === "inverse" || reason === "snapshot" ? markStablePayload(Object.freeze(result)) : result;
204
+ }
205
+ if (isPlainObject(value)) {
206
+ profile.clone.container();
207
+ const result = Object.create(Object.getPrototypeOf(value));
208
+ for (const key in value) if (Object.prototype.hasOwnProperty.call(value, key)) result[key] = cloneValue(value[key], reason);
209
+ return reason === "commit" || reason === "inverse" || reason === "snapshot" ? markStablePayload(Object.freeze(result)) : result;
210
+ }
211
+ return value;
212
+ };
213
+ const ownPayload = (value, reason = "canonical") => Array.isArray(value) || isPlainObject(value) ? cloneValue(value, reason) : value;
214
+ const transferPayload = (value) => {
215
+ profile.batch.payloadTransferred();
216
+ return value;
217
+ };
218
+ const snapshotPayload = (value, reason = "commit") => (profile.batch.payloadSnapshot(), ownPayload(value, reason));
219
+ const deepEqual = (left, right) => {
220
+ profile.clone.deepEqual();
221
+ if (Object.is(left, right)) return true;
222
+ if (Array.isArray(left) && Array.isArray(right)) {
223
+ profile.clone.deepEqualContainer();
224
+ if (left.length !== right.length) return false;
225
+ for (let index = 0; index < left.length; index += 1) if (!deepEqual(left[index], right[index])) return false;
226
+ return true;
227
+ }
228
+ if (isPlainObject(left) && isPlainObject(right)) {
229
+ profile.clone.deepEqualContainer();
230
+ const leftKeys = Object.keys(left);
231
+ if (leftKeys.length !== Object.keys(right).length) return false;
232
+ for (const key of leftKeys) if (!Object.prototype.hasOwnProperty.call(right, key) || !deepEqual(left[key], right[key])) return false;
233
+ return true;
234
+ }
235
+ return false;
236
+ };
237
+ const sameStructuralValue = (left, right) => Object.is(left, right) || (Array.isArray(left) || isPlainObject(left)) && deepEqual(left, right);
238
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
239
+ //#endregion
240
+ Object.defineProperty(exports, "cloneValue", {
241
+ enumerable: true,
242
+ get: function() {
243
+ return cloneValue;
244
+ }
245
+ });
246
+ Object.defineProperty(exports, "deepEqual", {
247
+ enumerable: true,
248
+ get: function() {
249
+ return deepEqual;
250
+ }
251
+ });
252
+ Object.defineProperty(exports, "isPlainObject", {
253
+ enumerable: true,
254
+ get: function() {
255
+ return isPlainObject;
256
+ }
257
+ });
258
+ Object.defineProperty(exports, "isRecord", {
259
+ enumerable: true,
260
+ get: function() {
261
+ return isRecord;
262
+ }
263
+ });
264
+ Object.defineProperty(exports, "isStablePayload", {
265
+ enumerable: true,
266
+ get: function() {
267
+ return isStablePayload;
268
+ }
269
+ });
270
+ Object.defineProperty(exports, "ownPayload", {
271
+ enumerable: true,
272
+ get: function() {
273
+ return ownPayload;
274
+ }
275
+ });
276
+ Object.defineProperty(exports, "profile", {
277
+ enumerable: true,
278
+ get: function() {
279
+ return profile;
280
+ }
281
+ });
282
+ Object.defineProperty(exports, "sameStructuralValue", {
283
+ enumerable: true,
284
+ get: function() {
285
+ return sameStructuralValue;
286
+ }
287
+ });
288
+ Object.defineProperty(exports, "snapshotPayload", {
289
+ enumerable: true,
290
+ get: function() {
291
+ return snapshotPayload;
292
+ }
293
+ });
294
+ Object.defineProperty(exports, "transferPayload", {
295
+ enumerable: true,
296
+ get: function() {
297
+ return transferPayload;
298
+ }
299
+ });
300
+
301
+ //# sourceMappingURL=ownership-CU-9DKJQ.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownership-CU-9DKJQ.cjs","names":[],"sources":["../core/src/profile.ts","../core/src/value/ownership.ts"],"sourcesContent":["export type CloneReason =\n 'initial' | 'canonical' | 'commit' | 'inverse' | 'reader' | 'replace' | 'journal' | 'snapshot';\n\ntype CloneCounters = {\n calls: number;\n containers: number;\n nodes: Record<CloneReason, number>;\n deepEqual: { calls: number; containers: number };\n documents: { initial: number };\n};\ntype ProfileCounters = {\n clone: CloneCounters;\n mutation: { normalized: number; executed: number; inverseCreated: number };\n batch: {\n operations: number;\n entries: number;\n collectionsResolved: number;\n journalRecords: number;\n rejected: number;\n inverseOperations: number;\n payloadTransferred: number;\n payloadSnapshots: number;\n };\n journal: {\n subjects: number;\n comparisons: number;\n absorbed: number;\n orderSnapshots: number;\n orderItems: number;\n };\n address: {\n schemaSteps: number;\n documentSteps: number;\n prefixComparisons: number;\n segmentsCompared: number;\n arraysCopied: number;\n };\n impact: { affectsChecks: number };\n reader: {\n sessions: number;\n nodeLookups: number;\n collectionIds: number;\n structuralSnapshots: number;\n };\n collectionView: {\n mappedItems: number;\n idsScanned: number;\n arraysCopied: number;\n };\n materialized: {\n updated: number;\n skipped: number;\n rebuilt: number;\n sourceChanges: number;\n notifications: number;\n };\n};\n\nexport type ProfileSnapshot = Readonly<{\n clone: Readonly<{\n calls: number;\n containers: number;\n nodes: Readonly<Record<CloneReason, number>>;\n deepEqual: Readonly<{ calls: number; containers: number }>;\n documents: Readonly<{ initial: number }>;\n }>;\n mutation: Readonly<{\n normalized: number;\n executed: number;\n inverseCreated: number;\n }>;\n batch: Readonly<ProfileCounters['batch']>;\n journal: Readonly<ProfileCounters['journal']>;\n address: Readonly<ProfileCounters['address']>;\n impact: Readonly<ProfileCounters['impact']>;\n reader: Readonly<ProfileCounters['reader']>;\n collectionView: Readonly<ProfileCounters['collectionView']>;\n materialized: Readonly<ProfileCounters['materialized']>;\n}>;\n\nconst cloneReasons = (): Record<CloneReason, number> => ({\n initial: 0,\n canonical: 0,\n commit: 0,\n inverse: 0,\n reader: 0,\n replace: 0,\n journal: 0,\n snapshot: 0,\n});\n\nconst counters = (): ProfileCounters => ({\n clone: {\n calls: 0,\n containers: 0,\n nodes: cloneReasons(),\n deepEqual: { calls: 0, containers: 0 },\n documents: { initial: 0 },\n },\n mutation: { normalized: 0, executed: 0, inverseCreated: 0 },\n batch: {\n operations: 0,\n entries: 0,\n collectionsResolved: 0,\n journalRecords: 0,\n rejected: 0,\n inverseOperations: 0,\n payloadTransferred: 0,\n payloadSnapshots: 0,\n },\n journal: {\n subjects: 0,\n comparisons: 0,\n absorbed: 0,\n orderSnapshots: 0,\n orderItems: 0,\n },\n address: {\n schemaSteps: 0,\n documentSteps: 0,\n prefixComparisons: 0,\n segmentsCompared: 0,\n arraysCopied: 0,\n },\n impact: { affectsChecks: 0 },\n reader: {\n sessions: 0,\n nodeLookups: 0,\n collectionIds: 0,\n structuralSnapshots: 0,\n },\n collectionView: { mappedItems: 0, idsScanned: 0, arraysCopied: 0 },\n materialized: {\n updated: 0,\n skipped: 0,\n rebuilt: 0,\n sourceChanges: 0,\n notifications: 0,\n },\n});\n\n// Recorder calls are stable no-op-capable functions. Only this module owns the\n// mutable session counter tree; domain code never branches on profile state.\nconst active = { current: undefined as ProfileCounters | undefined };\n\n// Snapshot allocation is intentionally delayed until the caller asks for it.\nconst freezeSnapshot = (value: ProfileCounters): ProfileSnapshot =>\n Object.freeze({\n clone: Object.freeze({\n calls: value.clone.calls,\n containers: value.clone.containers,\n nodes: Object.freeze({ ...value.clone.nodes }),\n deepEqual: Object.freeze({ ...value.clone.deepEqual }),\n documents: Object.freeze({ ...value.clone.documents }),\n }),\n mutation: Object.freeze({ ...value.mutation }),\n batch: Object.freeze({ ...value.batch }),\n journal: Object.freeze({ ...value.journal }),\n address: Object.freeze({ ...value.address }),\n impact: Object.freeze({ ...value.impact }),\n reader: Object.freeze({ ...value.reader }),\n collectionView: Object.freeze({ ...value.collectionView }),\n materialized: Object.freeze({ ...value.materialized }),\n });\n\nexport const profile = {\n clone: {\n call: (): void => {\n const value = active.current;\n if (value) value.clone.calls += 1;\n },\n node: (reason: CloneReason): void => {\n const value = active.current;\n if (!value) return;\n value.clone.nodes[reason] += 1;\n },\n container: (): void => {\n const value = active.current;\n if (value) value.clone.containers += 1;\n },\n deepEqual: (): void => {\n const value = active.current;\n if (value) value.clone.deepEqual.calls += 1;\n },\n deepEqualContainer: (): void => {\n const value = active.current;\n if (value) value.clone.deepEqual.containers += 1;\n },\n initialDocument: (): void => {\n const value = active.current;\n if (value) value.clone.documents.initial += 1;\n },\n },\n mutation: {\n normalized: (): void => {\n const value = active.current;\n if (value) value.mutation.normalized += 1;\n },\n executed: (): void => {\n const value = active.current;\n if (value) value.mutation.executed += 1;\n },\n inverse: (): void => {\n const value = active.current;\n if (value) value.mutation.inverseCreated += 1;\n },\n },\n batch: {\n operation: (): void => {\n const value = active.current;\n if (value) value.batch.operations += 1;\n },\n entry: (amount = 1): void => {\n const value = active.current;\n if (value) value.batch.entries += amount;\n },\n collectionResolved: (): void => {\n const value = active.current;\n if (value) value.batch.collectionsResolved += 1;\n },\n journalRecord: (): void => {\n const value = active.current;\n if (value) value.batch.journalRecords += 1;\n },\n rejected: (): void => {\n const value = active.current;\n if (value) value.batch.rejected += 1;\n },\n inverse: (): void => {\n const value = active.current;\n if (value) value.batch.inverseOperations += 1;\n },\n payloadTransferred: (): void => {\n const value = active.current;\n if (value) value.batch.payloadTransferred += 1;\n },\n payloadSnapshot: (): void => {\n const value = active.current;\n if (value) value.batch.payloadSnapshots += 1;\n },\n },\n journal: {\n subject: (): void => {\n const value = active.current;\n if (value) value.journal.subjects += 1;\n },\n comparison: (): void => {\n const value = active.current;\n if (value) value.journal.comparisons += 1;\n },\n absorbed: (): void => {\n const value = active.current;\n if (value) value.journal.absorbed += 1;\n },\n orderSnapshot: (items: number): void => {\n const value = active.current;\n if (!value) return;\n value.journal.orderSnapshots += 1;\n value.journal.orderItems += items;\n },\n },\n address: {\n schemaStep: (): void => {\n const value = active.current;\n if (value) value.address.schemaSteps += 1;\n },\n documentStep: (): void => {\n const value = active.current;\n if (value) value.address.documentSteps += 1;\n },\n arrayCopied: (): void => {\n const value = active.current;\n if (value) value.address.arraysCopied += 1;\n },\n prefixComparison: (): void => {\n const value = active.current;\n if (value) value.address.prefixComparisons += 1;\n },\n segmentCompared: (): void => {\n const value = active.current;\n if (value) value.address.segmentsCompared += 1;\n },\n },\n impact: {\n affects: (): void => {\n const value = active.current;\n if (value) value.impact.affectsChecks += 1;\n },\n },\n reader: {\n session: (): void => {\n const value = active.current;\n if (value) value.reader.sessions += 1;\n },\n lookup: (): void => {\n const value = active.current;\n if (value) value.reader.nodeLookups += 1;\n },\n collectionIds: (amount = 1): void => {\n const value = active.current;\n if (value) value.reader.collectionIds += amount;\n },\n structuralSnapshot: (): void => {\n const value = active.current;\n if (value) value.reader.structuralSnapshots += 1;\n },\n },\n collectionView: {\n mapped: (): void => {\n const value = active.current;\n if (value) value.collectionView.mappedItems += 1;\n },\n idsScanned: (amount = 1): void => {\n const value = active.current;\n if (value) value.collectionView.idsScanned += amount;\n },\n arrayCopied: (): void => {\n const value = active.current;\n if (value) value.collectionView.arraysCopied += 1;\n },\n },\n materialized: {\n updated: (): void => {\n const value = active.current;\n if (value) value.materialized.updated += 1;\n },\n skipped: (): void => {\n const value = active.current;\n if (value) value.materialized.skipped += 1;\n },\n rebuilt: (): void => {\n const value = active.current;\n if (value) value.materialized.rebuilt += 1;\n },\n sourceChanged: (): void => {\n const value = active.current;\n if (value) value.materialized.sourceChanges += 1;\n },\n notification: (): void => {\n const value = active.current;\n if (value) value.materialized.notifications += 1;\n },\n },\n} as const;\n\nexport type ProfileSession = {\n readonly snapshot: () => ProfileSnapshot;\n readonly stop: () => ProfileSnapshot;\n};\n\nexport const startProfile = (): ProfileSession => {\n if (active.current) throw new Error('A document profile session is already active.');\n const value = counters();\n active.current = value;\n let stopped = false;\n const snapshot = (): ProfileSnapshot => freezeSnapshot(value);\n return {\n snapshot,\n stop: (): ProfileSnapshot => {\n if (!stopped) {\n stopped = true;\n active.current = undefined;\n }\n return snapshot();\n },\n };\n};\n\nexport const measureProfile = <T>(\n run: () => T\n): { readonly value: T; readonly profile: ProfileSnapshot } => {\n const session = startProfile();\n try {\n const value = run();\n return { value, profile: session.stop() };\n } catch (error) {\n session.stop();\n throw error;\n }\n};\n","import { profile } from '../profile';\n\nexport type CloneReason =\n 'initial' | 'canonical' | 'commit' | 'inverse' | 'reader' | 'replace' | 'journal' | 'snapshot';\n\nconst stablePayloads = new WeakSet<object>();\n\nconst markStablePayload = <T>(value: T): T => {\n if (Array.isArray(value) || isPlainObject(value)) stablePayloads.add(value as object);\n return value;\n};\n\nexport const isStablePayload = (value: unknown): boolean =>\n (Array.isArray(value) || isPlainObject(value)) && stablePayloads.has(value as object);\n\nexport const isPlainObject = (value: unknown): value is Record<string, unknown> => {\n if (value === null || typeof value !== 'object') return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n};\n\nconst countCloneReason = (reason: CloneReason | undefined): void => {\n if (reason) profile.clone.node(reason);\n};\n\nexport const cloneValue = <T>(value: T, reason?: CloneReason): T => {\n profile.clone.call();\n countCloneReason(reason);\n if (Array.isArray(value)) {\n profile.clone.container();\n const result = new Array(value.length);\n for (let index = 0; index < value.length; index += 1)\n result[index] = cloneValue(value[index], reason);\n return (\n reason === 'commit' || reason === 'inverse' || reason === 'snapshot'\n ? markStablePayload(Object.freeze(result))\n : result\n ) as T;\n }\n if (isPlainObject(value)) {\n profile.clone.container();\n const result: Record<string, unknown> = Object.create(Object.getPrototypeOf(value)) as Record<\n string,\n unknown\n >;\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key))\n result[key] = cloneValue(value[key], reason);\n }\n return (\n reason === 'commit' || reason === 'inverse' || reason === 'snapshot'\n ? markStablePayload(Object.freeze(result))\n : result\n ) as T;\n }\n return value;\n};\n\n// Structural document values cross an ownership boundary by cloning. Atomic\n// values are shared because their schema contract treats them as immutable.\nexport const ownPayload = <T>(value: T, reason: CloneReason = 'canonical'): T =>\n Array.isArray(value) || isPlainObject(value) ? cloneValue(value, reason) : value;\n\n// The caller transfers structural ownership to the mutable canonical document.\n// Public commit/history boundaries use snapshotPayload instead.\nexport const transferPayload = <T>(value: T): T => {\n profile.batch.payloadTransferred();\n return value;\n};\nexport const snapshotPayload = <T>(value: T, reason: CloneReason = 'commit'): T => (\n profile.batch.payloadSnapshot(),\n ownPayload(value, reason)\n);\n\nexport const deepEqual = (left: unknown, right: unknown): boolean => {\n profile.clone.deepEqual();\n if (Object.is(left, right)) return true;\n if (Array.isArray(left) && Array.isArray(right)) {\n profile.clone.deepEqualContainer();\n if (left.length !== right.length) return false;\n for (let index = 0; index < left.length; index += 1)\n if (!deepEqual(left[index], right[index])) return false;\n return true;\n }\n if (isPlainObject(left) && isPlainObject(right)) {\n profile.clone.deepEqualContainer();\n const leftKeys = Object.keys(left);\n if (leftKeys.length !== Object.keys(right).length) return false;\n for (const key of leftKeys) {\n if (!Object.prototype.hasOwnProperty.call(right, key) || !deepEqual(left[key], right[key]))\n return false;\n }\n return true;\n }\n return false;\n};\n\nexport const sameStructuralValue = (left: unknown, right: unknown): boolean =>\n Object.is(left, right) ||\n ((Array.isArray(left) || isPlainObject(left)) && deepEqual(left, right));\n\nexport const isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doxum",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Doxum is a typed runtime for complex mutable documents.",
@@ -17,7 +17,7 @@ resolution.
17
17
  | ------------------------------------- | -------------------------------------------------------- |
18
18
  | Define document shape | `schema`, `field`, `object`, and collection constructors |
19
19
  | Create the canonical runtime | `createDocument` |
20
- | Persist and sync one browser document | `openLocalDocument` from `doxum/local-sync` |
20
+ | Persist and sync one browser document | `attachLocalSync` from `doxum/local-sync` |
21
21
  | Read once | `select(runtime, read => ...)` |
22
22
  | Make local business changes | `runtime.update(tx => ...)` |
23
23
  | Replay persisted or remote operations | `runtime.apply(operations, options)` |
@@ -206,6 +206,57 @@ the inverse or forward operation batch through the same mutation pipeline.
206
206
  or listeners after canonical state and history settled. They are not mutation
207
207
  failures and must not cause the caller to repeat the write.
208
208
 
209
+ ## Attach local browser sync
210
+
211
+ `attachLocalSync` is an optional browser attachment. Create and keep the
212
+ runtime yourself, then await attachment before allowing the document to be used.
213
+ It hydrates that runtime from an IndexedDB checkpoint and append-only command
214
+ tail, then uses a Web Lock to choose exactly one writable tab. The `leader`
215
+ uses normal synchronous runtime writes; every other attached tab is a
216
+ `follower` read-only mirror. A direct follower write through `update`,
217
+ `prepare`, `apply`, or `replace` throws `LocalSyncReadOnlyError`.
218
+
219
+ The leader's local, system, and history commits are observed after they have
220
+ settled and persisted to IndexedDB in order. BroadcastChannel carries only a
221
+ new-head hint. Followers reload the durable tail and apply it as `remote`; this
222
+ keeps their document ordered and invalidates their in-memory history. When the
223
+ leader disposes, a caught-up follower takes the lock and becomes leader.
224
+
225
+ Local-sync persists operation commands, not arbitrary new baselines. While it
226
+ is attached, `runtime.replace()` and an externally supplied
227
+ `runtime.apply(..., { source: 'remote' })` throw
228
+ `LocalSyncUnsupportedOperationError`; internal hydration and tail replay use a
229
+ trusted attachment path instead.
230
+
231
+ ```ts
232
+ import { attachLocalSync } from 'doxum/local-sync';
233
+
234
+ const runtime = createDocument({ schema: taskSchema, initial });
235
+ const localSync = await attachLocalSync({
236
+ runtime,
237
+ database: 'my-app',
238
+ documentId: 'project-1',
239
+ });
240
+
241
+ if (localSync.state().status === 'leader') {
242
+ runtime.update(tx => tx.write.title.set('Ship Doxum'));
243
+ runtime.history.undo();
244
+ }
245
+
246
+ await localSync.flush(); // persist observed leader commands or catch up a follower
247
+ await localSync.dispose();
248
+ ```
249
+
250
+ This is synchronous visibility with asynchronous persistence, not strict
251
+ durability: a crash, storage failure, or invalid JSON payload can leave a
252
+ visible leader commit unpersisted. Use `localSync.state()` and `onError` to show
253
+ that condition. `flush()` is the explicit persistence/catch-up boundary. The
254
+ attachment does not own or dispose the runtime and it does not expose an undo
255
+ API: use `runtime.history.undo()` and `runtime.history.redo()` while the tab is
256
+ leader. Runtime history is intentionally in-memory only; attachment hydration
257
+ and remote tail replay invalidate it, so it is not transferred across reopening
258
+ or leader handoff.
259
+
209
260
  ## Subscribe through schema-owned targets
210
261
 
211
262
  Create stable selectors from the schema, then subscribe to them. This is the
@@ -10,7 +10,7 @@
10
10
  | -------------------------- | ----------------------------------------------------- |
11
11
  | 定义文档结构 | `schema`、`field`、`object` 和集合构造器 |
12
12
  | 创建 canonical runtime | `createDocument` |
13
- | 持久化并同步一个浏览器文档 | 从 `doxum/local-sync` 导入 `openLocalDocument` |
13
+ | 持久化并同步一个浏览器文档 | 从 `doxum/local-sync` 导入 `attachLocalSync` |
14
14
  | 一次性读取 | `select(runtime, read => ...)` |
15
15
  | 执行本地业务修改 | `runtime.update(tx => ...)` |
16
16
  | 回放持久化或远端 operation | `runtime.apply(operations, options)` |
@@ -167,6 +167,51 @@ if (result.status === 'rejected') {
167
167
 
168
168
  committed 结果中的 `observerErrors` 是 canonical state 和 history 已稳定后,processor、flush 或 listener 发生的失败。它们不是 mutation 失败,调用方不能因此重复写入。
169
169
 
170
+ ## 附着浏览器本地同步
171
+
172
+ `attachLocalSync` 是可选的浏览器 attachment。runtime 仍由应用创建并持有;在使用
173
+ 文档前必须等待 attach 完成。它会从 IndexedDB 的 checkpoint 与 append-only command
174
+ tail 恢复传入 runtime,随后以 Web Lock 为一个 document 选出唯一可写标签页。`leader`
175
+ 可继续使用同步的 runtime 写入 API;其他已附着标签页都是只读的 `follower` mirror。
176
+ follower 直接调用 `update`、`prepare`、`apply` 或 `replace` 会抛出
177
+ `LocalSyncReadOnlyError`。
178
+
179
+ leader 已完成的 local、system 与 history commit 会被监听,并按顺序异步写入
180
+ IndexedDB。BroadcastChannel 只传递新的 head 提示;follower 从 IndexedDB 补读 durable
181
+ tail,并以 `remote` source 顺序 apply,因此其内存 history 会失效。leader dispose
182
+ 之后,已经 catch-up 的 follower 会接管锁并成为新的 leader。
183
+
184
+ local-sync 只持久化 operation command,不接受任意的新基线。附着期间,
185
+ `runtime.replace()` 与由应用提供的 `runtime.apply(..., { source: 'remote' })` 会抛出
186
+ `LocalSyncUnsupportedOperationError`;内部 hydration 与 tail replay 使用受信任的
187
+ attachment path。
188
+
189
+ ```ts
190
+ import { attachLocalSync } from 'doxum/local-sync';
191
+
192
+ const runtime = createDocument({ schema: taskSchema, initial });
193
+ const localSync = await attachLocalSync({
194
+ runtime,
195
+ database: 'my-app',
196
+ documentId: 'project-1',
197
+ });
198
+
199
+ if (localSync.state().status === 'leader') {
200
+ runtime.update(tx => tx.write.title.set('Ship Doxum'));
201
+ runtime.history.undo();
202
+ }
203
+
204
+ await localSync.flush(); // 持久化已观察到的 leader command,或让 follower 追赶
205
+ await localSync.dispose();
206
+ ```
207
+
208
+ 开始使用前必须等待 attachment,因为它会用 IndexedDB 恢复传入 runtime。这是“同步可见、
209
+ 异步持久化”而不是严格 durable:崩溃、存储失败或非 JSON payload,都可能让已经可见的
210
+ leader commit 未持久化。用 `localSync.state()` 与 `onError` 显示该状态,`flush()` 是显式
211
+ 的持久化/追赶边界。attachment 不拥有也不会 dispose runtime,且不提供另一套 undo API:
212
+ leader 使用 `runtime.history.undo()` 与 `runtime.history.redo()`。history 有意只在内存中
213
+ 存在;attachment 恢复及 remote tail apply 都会使它失效,因此不会跨重开或 leader 交接保留。
214
+
170
215
  ## 通过 schema 所有的 target 订阅
171
216
 
172
217
  从 schema 创建稳定 selector,再订阅 selector。这是整个 runtime 共用的 address 与 impact 模型。
@@ -130,10 +130,20 @@ views and subscriptions with their owner.
130
130
  ## Framework and product boundaries
131
131
 
132
132
  `core` is framework-neutral. `doxum/react` is a one-way adapter from core to
133
- React; core must not import React or UI concepts. `doxum/local-sync` is the
134
- optional browser owner of IndexedDB durability, Web Lock serialization, and
135
- BroadcastChannel catch-up; application code must not bypass its async session
136
- by writing its internal runtime. Doxum intentionally does not decide network
133
+ React; core must not import React or UI concepts. `doxum/local-sync` is an
134
+ optional attachment to an application-owned runtime. It hydrates an IndexedDB
135
+ checkpoint/tail, holds a document Web Lock for exactly one leader, and uses a
136
+ small internal synchronous write policy to reject follower mutations without
137
+ changing the runtime API. The leader observes completed local, system, and
138
+ history commits, appends JSON operation batches asynchronously, and broadcasts
139
+ only a head hint. Followers read and apply the durable tail as `remote`; this
140
+ invalidates their history. There is no pending queue, rebase, actor history, or
141
+ attachment-specific undo API. While attached, external `replace` and external
142
+ `apply` marked `remote` are rejected because only operation commands are
143
+ appendable; the attachment's hydration and replay lease is the one trusted
144
+ exception. Local-sync does not promise strict durability;
145
+ application code uses `state()`, `onError`, and `flush()` when it needs to
146
+ observe persistence or catch-up. Doxum intentionally does not decide network
137
147
  synchronization, authorization, retry, acknowledgement, ordering, or conflict
138
148
  resolution. An application must make those decisions before applying operations
139
149
  or replacing a snapshot.
@@ -84,7 +84,7 @@ update 与 notification 窗口中禁止写入。processor、flush 和 listener e
84
84
 
85
85
  ## Framework 与产品边界
86
86
 
87
- `core` 必须保持 framework-neutral。`doxum/react` 是从 core 到 React 的单向 adapter;core 不能 import React 或 UI 概念。`doxum/local-sync` 是可选浏览器 owner,负责 IndexedDB durability、Web Lock serialization 与 BroadcastChannel catch-up;应用代码不能绕过其 async session 写入内部 runtime。Doxum 有意不决定网络同步、authorization、retry、acknowledgement、ordering 或 conflict resolution。应用必须在 apply operation 或 replace snapshot 前做出这些决策。
87
+ `core` 必须保持 framework-neutral。`doxum/react` 是从 core 到 React 的单向 adapter;core 不能 import React 或 UI 概念。`doxum/local-sync` 是附着到应用自有 runtime 的可选浏览器 attachment:它会恢复 IndexedDB checkpoint/tail,以 document Web Lock 选出唯一 leader,并通过一个很小的内部同步写入 policy 拒绝 follower mutation,而不改变 runtime API。leader 监听已完成的 local、system 与 history commit,将 JSON operation batch 异步追加到日志,并且只广播 head 提示。follower 从 durable tail 顺序读取并按 `remote` apply,因此 history 会失效。没有 pending queue、rebase、actor history 或 attachment 专属的 undo API。附着期间,外部 `replace` 与标记为 `remote` 的外部 `apply` 都会被拒绝,因为只有 operation command 能 append;attachment 的 hydration 与 replay lease 是唯一受信任例外。它不会承诺严格 durability;应用在需要观察持久化或追赶时使用 `state()`、`onError` 与 `flush()`。Doxum 有意不决定网络同步、authorization、retry、acknowledgement、ordering 或 conflict resolution。应用必须在 apply operation 或 replace snapshot 前做出这些决策。
88
88
 
89
89
  ## 改动检查清单
90
90