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.
- package/README.md +59 -27
- package/dist/{contract-DGkZcTiP.d.cts → contract-BsY1XLiG.d.ts} +2 -1
- package/dist/{contract-Dh3az8aN.d.ts → contract-D8kjdfqT.d.cts} +2 -1
- package/dist/{access-5D4KKFm7.js → dependency-C5_R1tvQ.js} +40 -262
- package/dist/dependency-C5_R1tvQ.js.map +1 -0
- package/dist/{access-z56fVqZ2.cjs → dependency-Dfzs3khk.cjs} +62 -344
- package/dist/dependency-Dfzs3khk.cjs.map +1 -0
- package/dist/driver-CNxqMVFH.cjs +69 -0
- package/dist/driver-CNxqMVFH.cjs.map +1 -0
- package/dist/driver-CbzfW5MR.js +46 -0
- package/dist/driver-CbzfW5MR.js.map +1 -0
- package/dist/index.cjs +2636 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2594 -4
- package/dist/index.js.map +1 -1
- package/dist/integration.cjs +2 -3
- package/dist/integration.cjs.map +1 -1
- package/dist/integration.d.cts +1 -1
- package/dist/integration.d.ts +1 -1
- package/dist/integration.js +1 -2
- package/dist/integration.js.map +1 -1
- package/dist/local-sync.cjs +154 -329
- package/dist/local-sync.cjs.map +1 -1
- package/dist/local-sync.d.cts +17 -22
- package/dist/local-sync.d.ts +17 -22
- package/dist/local-sync.js +149 -326
- package/dist/local-sync.js.map +1 -1
- package/dist/ownership-B-VAPcce.js +242 -0
- package/dist/ownership-B-VAPcce.js.map +1 -0
- package/dist/ownership-CU-9DKJQ.cjs +301 -0
- package/dist/ownership-CU-9DKJQ.cjs.map +1 -0
- package/package.json +1 -1
- package/skills/doxum-runtime/references/guide.en.md +52 -1
- package/skills/doxum-runtime/references/guide.zh-CN.md +46 -1
- package/skills/doxum-runtime/references/invariants.en.md +14 -4
- package/skills/doxum-runtime/references/invariants.zh-CN.md +1 -1
- package/dist/access-5D4KKFm7.js.map +0 -1
- package/dist/access-z56fVqZ2.cjs.map +0 -1
- package/dist/dependency-BRn1TRDi.js +0 -20
- package/dist/dependency-BRn1TRDi.js.map +0 -1
- package/dist/dependency-DmQXyj7q.cjs +0 -25
- package/dist/dependency-DmQXyj7q.cjs.map +0 -1
- package/dist/readable-B8E3FMq6.cjs +0 -2614
- package/dist/readable-B8E3FMq6.cjs.map +0 -1
- package/dist/readable-CIj02gIj.js +0 -2579
- 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
|
@@ -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 | `
|
|
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` 导入 `
|
|
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
|
|
134
|
-
optional
|
|
135
|
-
|
|
136
|
-
|
|
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`
|
|
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
|
|