@git-stunts/git-warp 12.1.0 → 12.2.1
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 +8 -4
- package/bin/cli/commands/trust.js +37 -1
- package/bin/cli/infrastructure.js +14 -1
- package/bin/cli/schemas.js +4 -4
- package/bin/warp-graph.js +9 -2
- package/index.d.ts +18 -2
- package/package.json +1 -1
- package/src/domain/WarpGraph.js +4 -1
- package/src/domain/crdt/Dot.js +5 -0
- package/src/domain/crdt/LWW.js +3 -1
- package/src/domain/crdt/ORSet.js +63 -27
- package/src/domain/crdt/VersionVector.js +12 -0
- package/src/domain/errors/PatchError.js +27 -0
- package/src/domain/errors/StorageError.js +8 -0
- package/src/domain/errors/SyncError.js +1 -0
- package/src/domain/errors/TrustError.js +2 -0
- package/src/domain/errors/WriterError.js +5 -0
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AuditVerifierService.js +32 -2
- package/src/domain/services/BitmapIndexBuilder.js +14 -9
- package/src/domain/services/CheckpointService.js +12 -8
- package/src/domain/services/Frontier.js +18 -0
- package/src/domain/services/GCPolicy.js +25 -4
- package/src/domain/services/GraphTraversal.js +11 -50
- package/src/domain/services/HttpSyncServer.js +18 -29
- package/src/domain/services/IncrementalIndexUpdater.js +179 -36
- package/src/domain/services/JoinReducer.js +164 -31
- package/src/domain/services/MaterializedViewService.js +13 -2
- package/src/domain/services/PatchBuilderV2.js +210 -145
- package/src/domain/services/QueryBuilder.js +67 -30
- package/src/domain/services/SyncController.js +62 -18
- package/src/domain/services/SyncPayloadSchema.js +236 -0
- package/src/domain/services/SyncProtocol.js +102 -40
- package/src/domain/services/SyncTrustGate.js +146 -0
- package/src/domain/services/TranslationCost.js +2 -2
- package/src/domain/trust/TrustRecordService.js +161 -34
- package/src/domain/utils/CachedValue.js +34 -5
- package/src/domain/utils/EventId.js +4 -1
- package/src/domain/utils/LRUCache.js +3 -1
- package/src/domain/utils/RefLayout.js +4 -0
- package/src/domain/utils/canonicalStringify.js +48 -18
- package/src/domain/utils/matchGlob.js +7 -0
- package/src/domain/warp/PatchSession.js +30 -24
- package/src/domain/warp/Writer.js +12 -5
- package/src/domain/warp/_wiredMethods.d.ts +1 -1
- package/src/domain/warp/checkpoint.methods.js +102 -16
- package/src/domain/warp/materialize.methods.js +47 -5
- package/src/domain/warp/materializeAdvanced.methods.js +52 -10
- package/src/domain/warp/patch.methods.js +24 -8
- package/src/domain/warp/query.methods.js +4 -4
- package/src/domain/warp/subscribe.methods.js +11 -19
- package/src/infrastructure/adapters/GitGraphAdapter.js +57 -54
- package/src/infrastructure/codecs/CborCodec.js +2 -0
- package/src/domain/utils/fnv1a.js +0 -20
|
@@ -46,9 +46,12 @@ function findAttachedData(state, nodeId) {
|
|
|
46
46
|
const edges = [];
|
|
47
47
|
const props = [];
|
|
48
48
|
|
|
49
|
+
// Edge keys are encoded as "from\0to\0label". Check prefix for source
|
|
50
|
+
// and interior substring for target — avoids split() on every key.
|
|
51
|
+
const srcPrefix = `${nodeId}\0`;
|
|
52
|
+
const tgtInfix = `\0${nodeId}\0`;
|
|
49
53
|
for (const key of orsetElements(state.edgeAlive)) {
|
|
50
|
-
|
|
51
|
-
if (parts[0] === nodeId || parts[1] === nodeId) {
|
|
54
|
+
if (key.startsWith(srcPrefix) || key.includes(tgtInfix)) {
|
|
52
55
|
edges.push(key);
|
|
53
56
|
}
|
|
54
57
|
}
|
|
@@ -103,6 +106,15 @@ export class PatchBuilderV2 {
|
|
|
103
106
|
/** @type {Function} */
|
|
104
107
|
this._getCurrentState = getCurrentState; // Function to get current materialized state
|
|
105
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Snapshot of state captured at construction time (C4).
|
|
111
|
+
* Lazily populated on first call to _getSnapshotState().
|
|
112
|
+
* Prevents TOCTOU races where concurrent writes change state
|
|
113
|
+
* between remove operations in the same patch.
|
|
114
|
+
* @type {import('./JoinReducer.js').WarpStateV5|null}
|
|
115
|
+
*/
|
|
116
|
+
this._snapshotState = /** @type {import('./JoinReducer.js').WarpStateV5|null} */ (/** @type {unknown} */ (undefined)); // undefined = not yet captured
|
|
117
|
+
|
|
106
118
|
/** @type {string|null} */
|
|
107
119
|
this._expectedParentSha = expectedParentSha;
|
|
108
120
|
|
|
@@ -132,17 +144,23 @@ export class PatchBuilderV2 {
|
|
|
132
144
|
this._contentBlobs = [];
|
|
133
145
|
|
|
134
146
|
/**
|
|
135
|
-
*
|
|
147
|
+
* Observed operands — entities whose current state was consulted to build
|
|
148
|
+
* this patch.
|
|
149
|
+
*
|
|
150
|
+
* Semantic model per operation type:
|
|
151
|
+
* - removeNode(id): observes node `id` (reads its OR-Set dots for tombstoning)
|
|
152
|
+
* - removeEdge(from, to, label): observes the edge key
|
|
153
|
+
* - addEdge(from, to, label): observes both endpoint nodes `from` and `to`
|
|
154
|
+
* - setProperty(nodeId, key, value): observes node `nodeId`
|
|
155
|
+
* - setEdgeProperty(from, to, label, key, value): observes the edge key
|
|
156
|
+
* - cascade-generated EdgeRemove: observes the edge key
|
|
136
157
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* entity to observe its dots (OR-Set semantics). "Writes" track new data
|
|
140
|
-
* creation (adds). This distinction enables finer-grained provenance
|
|
141
|
-
* queries like "which patches wrote to X?" vs "which patches depended on X?"
|
|
158
|
+
* The public getter `.reads` and the serialized patch field `reads` retain
|
|
159
|
+
* the historical name for backward compatibility.
|
|
142
160
|
*
|
|
143
161
|
* @type {Set<string>}
|
|
144
162
|
*/
|
|
145
|
-
this.
|
|
163
|
+
this._observedOperands = new Set();
|
|
146
164
|
|
|
147
165
|
/**
|
|
148
166
|
* Nodes/edges written by this patch (for provenance tracking).
|
|
@@ -154,6 +172,42 @@ export class PatchBuilderV2 {
|
|
|
154
172
|
* @type {Set<string>}
|
|
155
173
|
*/
|
|
156
174
|
this._writes = new Set();
|
|
175
|
+
|
|
176
|
+
/** @type {boolean} Whether any edge-property ops have been added (schema 3 flag cache). */
|
|
177
|
+
this._hasEdgeProps = false;
|
|
178
|
+
|
|
179
|
+
/** @type {boolean} */
|
|
180
|
+
this._committed = false;
|
|
181
|
+
|
|
182
|
+
/** @type {boolean} */
|
|
183
|
+
this._committing = false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Returns a snapshot of the current state, captured lazily on first call (C4).
|
|
188
|
+
*
|
|
189
|
+
* All remove operations within this patch observe dots from the same
|
|
190
|
+
* state snapshot, preventing TOCTOU races where concurrent writers
|
|
191
|
+
* change state between operations.
|
|
192
|
+
*
|
|
193
|
+
* @returns {import('./JoinReducer.js').WarpStateV5|null}
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
_getSnapshotState() {
|
|
197
|
+
if (this._snapshotState === undefined) {
|
|
198
|
+
this._snapshotState = this._getCurrentState() || null;
|
|
199
|
+
}
|
|
200
|
+
return this._snapshotState;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Throws if this builder is no longer open for mutation.
|
|
205
|
+
* @private
|
|
206
|
+
*/
|
|
207
|
+
_assertNotCommitted() {
|
|
208
|
+
if (this._committed || this._committing) {
|
|
209
|
+
throw new Error('PatchBuilder already committed — create a new builder');
|
|
210
|
+
}
|
|
157
211
|
}
|
|
158
212
|
|
|
159
213
|
/**
|
|
@@ -178,6 +232,7 @@ export class PatchBuilderV2 {
|
|
|
178
232
|
* .addEdge('user:alice', 'user:bob', 'follows');
|
|
179
233
|
*/
|
|
180
234
|
addNode(nodeId) {
|
|
235
|
+
this._assertNotCommitted();
|
|
181
236
|
const dot = vvIncrement(this._vv, this._writerId);
|
|
182
237
|
this._ops.push(createNodeAddV2(nodeId, dot));
|
|
183
238
|
// Provenance: NodeAdd writes the node
|
|
@@ -212,8 +267,9 @@ export class PatchBuilderV2 {
|
|
|
212
267
|
* builder.removeNode('user:alice'); // Also removes all connected edges
|
|
213
268
|
*/
|
|
214
269
|
removeNode(nodeId) {
|
|
270
|
+
this._assertNotCommitted();
|
|
215
271
|
// Get observed dots from current state (orsetGetDots returns already-encoded dot strings)
|
|
216
|
-
const state = this.
|
|
272
|
+
const state = this._getSnapshotState();
|
|
217
273
|
|
|
218
274
|
// Cascade mode: auto-generate EdgeRemove ops for all connected edges before NodeRemove.
|
|
219
275
|
// Generated ops appear in the patch for auditability.
|
|
@@ -224,7 +280,7 @@ export class PatchBuilderV2 {
|
|
|
224
280
|
const edgeDots = [...orsetGetDots(state.edgeAlive, edgeKey)];
|
|
225
281
|
this._ops.push(createEdgeRemoveV2(from, to, label, edgeDots));
|
|
226
282
|
// Provenance: cascade-generated EdgeRemove reads the edge key (to observe its dots)
|
|
227
|
-
this.
|
|
283
|
+
this._observedOperands.add(edgeKey);
|
|
228
284
|
}
|
|
229
285
|
}
|
|
230
286
|
|
|
@@ -261,7 +317,7 @@ export class PatchBuilderV2 {
|
|
|
261
317
|
const observedDots = state ? [...orsetGetDots(state.nodeAlive, nodeId)] : [];
|
|
262
318
|
this._ops.push(createNodeRemoveV2(nodeId, observedDots));
|
|
263
319
|
// Provenance: NodeRemove reads the node (to observe its dots)
|
|
264
|
-
this.
|
|
320
|
+
this._observedOperands.add(nodeId);
|
|
265
321
|
return this;
|
|
266
322
|
}
|
|
267
323
|
|
|
@@ -292,13 +348,14 @@ export class PatchBuilderV2 {
|
|
|
292
348
|
* .addEdge('user:alice', 'user:bob', 'collaborates_with');
|
|
293
349
|
*/
|
|
294
350
|
addEdge(from, to, label) {
|
|
351
|
+
this._assertNotCommitted();
|
|
295
352
|
const dot = vvIncrement(this._vv, this._writerId);
|
|
296
353
|
this._ops.push(createEdgeAddV2(from, to, label, dot));
|
|
297
354
|
const edgeKey = encodeEdgeKey(from, to, label);
|
|
298
355
|
this._edgesAdded.add(edgeKey);
|
|
299
356
|
// Provenance: EdgeAdd reads both endpoint nodes, writes the edge key
|
|
300
|
-
this.
|
|
301
|
-
this.
|
|
357
|
+
this._observedOperands.add(from);
|
|
358
|
+
this._observedOperands.add(to);
|
|
302
359
|
this._writes.add(edgeKey);
|
|
303
360
|
return this;
|
|
304
361
|
}
|
|
@@ -329,13 +386,14 @@ export class PatchBuilderV2 {
|
|
|
329
386
|
* .removeNode('user:alice');
|
|
330
387
|
*/
|
|
331
388
|
removeEdge(from, to, label) {
|
|
389
|
+
this._assertNotCommitted();
|
|
332
390
|
// Get observed dots from current state (orsetGetDots returns already-encoded dot strings)
|
|
333
|
-
const state = this.
|
|
391
|
+
const state = this._getSnapshotState();
|
|
334
392
|
const edgeKey = encodeEdgeKey(from, to, label);
|
|
335
393
|
const observedDots = state ? [...orsetGetDots(state.edgeAlive, edgeKey)] : [];
|
|
336
394
|
this._ops.push(createEdgeRemoveV2(from, to, label, observedDots));
|
|
337
395
|
// Provenance: EdgeRemove reads the edge key (to observe its dots)
|
|
338
|
-
this.
|
|
396
|
+
this._observedOperands.add(edgeKey);
|
|
339
397
|
return this;
|
|
340
398
|
}
|
|
341
399
|
|
|
@@ -369,10 +427,11 @@ export class PatchBuilderV2 {
|
|
|
369
427
|
* .setProperty('user:alice', 'age', 30);
|
|
370
428
|
*/
|
|
371
429
|
setProperty(nodeId, key, value) {
|
|
430
|
+
this._assertNotCommitted();
|
|
372
431
|
// Props don't use dots - they use EventId from patch context
|
|
373
432
|
this._ops.push(createPropSetV2(nodeId, key, value));
|
|
374
433
|
// Provenance: PropSet reads the node (implicit existence check) and writes the node
|
|
375
|
-
this.
|
|
434
|
+
this._observedOperands.add(nodeId);
|
|
376
435
|
this._writes.add(nodeId);
|
|
377
436
|
return this;
|
|
378
437
|
}
|
|
@@ -415,10 +474,11 @@ export class PatchBuilderV2 {
|
|
|
415
474
|
* .setEdgeProperty('user:alice', 'user:bob', 'follows', 'public', true);
|
|
416
475
|
*/
|
|
417
476
|
setEdgeProperty(from, to, label, key, value) {
|
|
477
|
+
this._assertNotCommitted();
|
|
418
478
|
// Validate edge exists in this patch or in current state
|
|
419
479
|
const ek = encodeEdgeKey(from, to, label);
|
|
420
480
|
if (!this._edgesAdded.has(ek)) {
|
|
421
|
-
const state = this.
|
|
481
|
+
const state = this._getSnapshotState();
|
|
422
482
|
if (!state || !orsetContains(state.edgeAlive, ek)) {
|
|
423
483
|
throw new Error(`Cannot set property on unknown edge (${from} → ${to} [${label}]): add the edge first`);
|
|
424
484
|
}
|
|
@@ -431,8 +491,9 @@ export class PatchBuilderV2 {
|
|
|
431
491
|
// = encodeEdgePropKey(from, to, label, key)
|
|
432
492
|
const edgeNode = `${EDGE_PROP_PREFIX}${from}\0${to}\0${label}`;
|
|
433
493
|
this._ops.push(createPropSetV2(edgeNode, key, value));
|
|
494
|
+
this._hasEdgeProps = true;
|
|
434
495
|
// Provenance: setEdgeProperty reads the edge (implicit existence check) and writes the edge
|
|
435
|
-
this.
|
|
496
|
+
this._observedOperands.add(ek);
|
|
436
497
|
this._writes.add(ek);
|
|
437
498
|
return this;
|
|
438
499
|
}
|
|
@@ -453,6 +514,7 @@ export class PatchBuilderV2 {
|
|
|
453
514
|
* @returns {Promise<PatchBuilderV2>} This builder instance for method chaining
|
|
454
515
|
*/
|
|
455
516
|
async attachContent(nodeId, content) {
|
|
517
|
+
this._assertNotCommitted();
|
|
456
518
|
const oid = await this._persistence.writeBlob(content);
|
|
457
519
|
this.setProperty(nodeId, CONTENT_PROPERTY_KEY, oid);
|
|
458
520
|
this._contentBlobs.push(oid);
|
|
@@ -470,6 +532,7 @@ export class PatchBuilderV2 {
|
|
|
470
532
|
* @returns {Promise<PatchBuilderV2>} This builder instance for method chaining
|
|
471
533
|
*/
|
|
472
534
|
async attachEdgeContent(from, to, label, content) {
|
|
535
|
+
this._assertNotCommitted();
|
|
473
536
|
const oid = await this._persistence.writeBlob(content);
|
|
474
537
|
this.setEdgeProperty(from, to, label, CONTENT_PROPERTY_KEY, oid);
|
|
475
538
|
this._contentBlobs.push(oid);
|
|
@@ -495,14 +558,14 @@ export class PatchBuilderV2 {
|
|
|
495
558
|
* - `ops`: Array of operations (NodeAdd, NodeRemove, EdgeAdd, EdgeRemove, PropSet)
|
|
496
559
|
*/
|
|
497
560
|
build() {
|
|
498
|
-
const schema = this.
|
|
561
|
+
const schema = this._hasEdgeProps ? 3 : 2;
|
|
499
562
|
return createPatchV2({
|
|
500
563
|
schema,
|
|
501
564
|
writer: this._writerId,
|
|
502
565
|
lamport: this._lamport,
|
|
503
566
|
context: vvSerialize(this._vv),
|
|
504
567
|
ops: this._ops,
|
|
505
|
-
reads: [...this.
|
|
568
|
+
reads: [...this._observedOperands].sort(),
|
|
506
569
|
writes: [...this._writes].sort(),
|
|
507
570
|
});
|
|
508
571
|
}
|
|
@@ -511,20 +574,24 @@ export class PatchBuilderV2 {
|
|
|
511
574
|
* Commits the patch to the graph.
|
|
512
575
|
*
|
|
513
576
|
* This method performs the following steps atomically:
|
|
514
|
-
* 1.
|
|
515
|
-
* 2.
|
|
516
|
-
* 3.
|
|
517
|
-
* 4.
|
|
518
|
-
* 5.
|
|
519
|
-
* 6.
|
|
520
|
-
* 7. Creates a
|
|
521
|
-
* 8.
|
|
522
|
-
* 9.
|
|
577
|
+
* 1. Verifies this builder is still open (not committed and not in-flight)
|
|
578
|
+
* 2. Validates the patch is non-empty
|
|
579
|
+
* 3. Checks for concurrent modifications (compare-and-swap on writer ref)
|
|
580
|
+
* 4. Calculates the next lamport timestamp from the parent commit
|
|
581
|
+
* 5. Builds the PatchV2 structure with the resolved lamport
|
|
582
|
+
* 6. Encodes the patch as CBOR and writes it as a Git blob
|
|
583
|
+
* 7. Creates a Git tree containing the patch blob
|
|
584
|
+
* 8. Creates a commit with proper trailers linking to the parent
|
|
585
|
+
* 9. Updates the writer ref to point to the new commit
|
|
586
|
+
* 10. Invokes the success callback if provided (for eager re-materialization)
|
|
523
587
|
*
|
|
524
588
|
* The commit is written to the writer's patch chain at:
|
|
525
589
|
* `refs/warp/<graphName>/writers/<writerId>`
|
|
526
590
|
*
|
|
527
591
|
* @returns {Promise<string>} The commit SHA of the new patch commit
|
|
592
|
+
* @throws {Error} If this builder has already been committed, or a commit
|
|
593
|
+
* is currently in-flight on this builder.
|
|
594
|
+
* Message: `"PatchBuilder already committed — create a new builder"`
|
|
528
595
|
* @throws {Error} If the patch is empty (no operations were added).
|
|
529
596
|
* Message: `"Cannot commit empty patch: no operations added"`
|
|
530
597
|
* @throws {WriterError} If a concurrent commit was detected (another process
|
|
@@ -552,115 +619,122 @@ export class PatchBuilderV2 {
|
|
|
552
619
|
* }
|
|
553
620
|
*/
|
|
554
621
|
async commit() {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
622
|
+
this._assertNotCommitted();
|
|
623
|
+
this._committing = true;
|
|
624
|
+
try {
|
|
625
|
+
// 2. Reject empty patches
|
|
626
|
+
if (this._ops.length === 0) {
|
|
627
|
+
throw new Error('Cannot commit empty patch: no operations added');
|
|
628
|
+
}
|
|
559
629
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
630
|
+
// 3. Race detection: check if writer ref has advanced since builder creation
|
|
631
|
+
const writerRef = buildWriterRef(this._graphName, this._writerId);
|
|
632
|
+
const currentRefSha = await this._persistence.readRef(writerRef);
|
|
633
|
+
|
|
634
|
+
if (currentRefSha !== this._expectedParentSha) {
|
|
635
|
+
const err = /** @type {WriterError & { expectedSha: string|null, actualSha: string|null }} */ (new WriterError(
|
|
636
|
+
'WRITER_CAS_CONFLICT',
|
|
637
|
+
'Commit failed: writer ref was updated by another process. Re-materialize and retry.'
|
|
638
|
+
));
|
|
639
|
+
err.expectedSha = this._expectedParentSha;
|
|
640
|
+
err.actualSha = currentRefSha;
|
|
641
|
+
throw err;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// 4. Calculate lamport and parent from current ref state.
|
|
645
|
+
// Start from this._lamport (set by _nextLamport() in createPatch()), which already
|
|
646
|
+
// incorporates the globally-observed max Lamport tick via _maxObservedLamport.
|
|
647
|
+
// This ensures a first-time writer whose own chain is empty still commits at a tick
|
|
648
|
+
// above any previously-observed writer, winning LWW tiebreakers correctly.
|
|
649
|
+
let lamport = this._lamport;
|
|
650
|
+
let parentCommit = null;
|
|
651
|
+
|
|
652
|
+
if (currentRefSha) {
|
|
653
|
+
parentCommit = currentRefSha;
|
|
654
|
+
// Read the current patch commit to get its lamport timestamp and take the max,
|
|
655
|
+
// so the chain stays monotonic even if the ref advanced since createPatch().
|
|
656
|
+
const commitMessage = await this._persistence.showNode(currentRefSha);
|
|
657
|
+
const kind = detectMessageKind(commitMessage);
|
|
658
|
+
|
|
659
|
+
if (kind === 'patch') {
|
|
660
|
+
let patchInfo;
|
|
661
|
+
try {
|
|
662
|
+
patchInfo = decodePatchMessage(commitMessage);
|
|
663
|
+
} catch (err) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
`Failed to parse lamport from writer ref ${writerRef}: ` +
|
|
666
|
+
`commit ${currentRefSha} has invalid patch message format`,
|
|
667
|
+
{ cause: err }
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
lamport = Math.max(this._lamport, patchInfo.lamport + 1);
|
|
671
|
+
}
|
|
672
|
+
// Non-patch ref (checkpoint, etc.): keep lamport from this._lamport
|
|
673
|
+
// (already incorporates _maxObservedLamport), matching _nextLamport() behavior.
|
|
674
|
+
}
|
|
573
675
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
676
|
+
// 5. Build PatchV2 structure with correct lamport
|
|
677
|
+
// Note: Dots were assigned using constructor lamport, but commit lamport may differ.
|
|
678
|
+
// For now, we use the calculated lamport for the patch metadata.
|
|
679
|
+
// The dots themselves are independent of patch lamport (they use VV counters).
|
|
680
|
+
const schema = this._hasEdgeProps ? 3 : 2;
|
|
681
|
+
// Use createPatchV2 for consistent patch construction (DRY with build())
|
|
682
|
+
const patch = createPatchV2({
|
|
683
|
+
schema,
|
|
684
|
+
writer: this._writerId,
|
|
685
|
+
lamport,
|
|
686
|
+
context: vvSerialize(this._vv),
|
|
687
|
+
ops: this._ops,
|
|
688
|
+
reads: [...this._observedOperands].sort(),
|
|
689
|
+
writes: [...this._writes].sort(),
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// 6. Encode patch as CBOR and write as a Git blob
|
|
693
|
+
const patchCbor = this._codec.encode(patch);
|
|
694
|
+
const patchBlobOid = await this._persistence.writeBlob(/** @type {Buffer} */ (patchCbor));
|
|
695
|
+
|
|
696
|
+
// 7. Create tree with the patch blob + any content blobs (deduplicated)
|
|
697
|
+
// Format for mktree: "mode type oid\tpath"
|
|
698
|
+
const treeEntries = [`100644 blob ${patchBlobOid}\tpatch.cbor`];
|
|
699
|
+
const uniqueBlobs = [...new Set(this._contentBlobs)];
|
|
700
|
+
for (const blobOid of uniqueBlobs) {
|
|
701
|
+
treeEntries.push(`100644 blob ${blobOid}\t_content_${blobOid}`);
|
|
702
|
+
}
|
|
703
|
+
const treeOid = await this._persistence.writeTree(treeEntries);
|
|
704
|
+
|
|
705
|
+
// 8. Create commit with proper trailers linking to the parent
|
|
706
|
+
const commitMessage = encodePatchMessage({
|
|
707
|
+
graph: this._graphName,
|
|
708
|
+
writer: this._writerId,
|
|
709
|
+
lamport,
|
|
710
|
+
patchOid: patchBlobOid,
|
|
711
|
+
schema,
|
|
712
|
+
});
|
|
713
|
+
const parents = parentCommit ? [parentCommit] : [];
|
|
714
|
+
const newCommitSha = await this._persistence.commitNodeWithTree({
|
|
715
|
+
treeOid,
|
|
716
|
+
parents,
|
|
717
|
+
message: commitMessage,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// 9. Update writer ref to point to new commit
|
|
721
|
+
await this._persistence.updateRef(writerRef, newCommitSha);
|
|
722
|
+
|
|
723
|
+
// 10. Notify success callback (updates graph's version vector + eager re-materialize)
|
|
724
|
+
if (this._onCommitSuccess) {
|
|
591
725
|
try {
|
|
592
|
-
|
|
726
|
+
await this._onCommitSuccess({ patch, sha: newCommitSha });
|
|
593
727
|
} catch (err) {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
`commit ${currentRefSha} has invalid patch message format`,
|
|
597
|
-
{ cause: err }
|
|
598
|
-
);
|
|
728
|
+
// Commit is already persisted — log but don't fail the caller.
|
|
729
|
+
this._logger.warn(`[warp] onCommitSuccess callback failed (sha=${newCommitSha}):`, err);
|
|
599
730
|
}
|
|
600
|
-
lamport = Math.max(this._lamport, patchInfo.lamport + 1);
|
|
601
731
|
}
|
|
602
|
-
// Non-patch ref (checkpoint, etc.): keep lamport from this._lamport
|
|
603
|
-
// (already incorporates _maxObservedLamport), matching _nextLamport() behavior.
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// 4. Build PatchV2 structure with correct lamport
|
|
607
|
-
// Note: Dots were assigned using constructor lamport, but commit lamport may differ.
|
|
608
|
-
// For now, we use the calculated lamport for the patch metadata.
|
|
609
|
-
// The dots themselves are independent of patch lamport (they use VV counters).
|
|
610
|
-
const schema = this._ops.some(op => op.type === 'PropSet' && op.node.charCodeAt(0) === 1) ? 3 : 2;
|
|
611
|
-
// Use createPatchV2 for consistent patch construction (DRY with build())
|
|
612
|
-
const patch = createPatchV2({
|
|
613
|
-
schema,
|
|
614
|
-
writer: this._writerId,
|
|
615
|
-
lamport,
|
|
616
|
-
context: vvSerialize(this._vv),
|
|
617
|
-
ops: this._ops,
|
|
618
|
-
reads: [...this._reads].sort(),
|
|
619
|
-
writes: [...this._writes].sort(),
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
// 5. Encode patch as CBOR and write as a Git blob
|
|
623
|
-
const patchCbor = this._codec.encode(patch);
|
|
624
|
-
const patchBlobOid = await this._persistence.writeBlob(/** @type {Buffer} */ (patchCbor));
|
|
625
|
-
|
|
626
|
-
// 6. Create tree with the patch blob + any content blobs (deduplicated)
|
|
627
|
-
// Format for mktree: "mode type oid\tpath"
|
|
628
|
-
const treeEntries = [`100644 blob ${patchBlobOid}\tpatch.cbor`];
|
|
629
|
-
const uniqueBlobs = [...new Set(this._contentBlobs)];
|
|
630
|
-
for (const blobOid of uniqueBlobs) {
|
|
631
|
-
treeEntries.push(`100644 blob ${blobOid}\t_content_${blobOid}`);
|
|
632
|
-
}
|
|
633
|
-
const treeOid = await this._persistence.writeTree(treeEntries);
|
|
634
732
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
lamport,
|
|
640
|
-
patchOid: patchBlobOid,
|
|
641
|
-
schema,
|
|
642
|
-
});
|
|
643
|
-
const parents = parentCommit ? [parentCommit] : [];
|
|
644
|
-
const newCommitSha = await this._persistence.commitNodeWithTree({
|
|
645
|
-
treeOid,
|
|
646
|
-
parents,
|
|
647
|
-
message: commitMessage,
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
// 8. Update writer ref to point to new commit
|
|
651
|
-
await this._persistence.updateRef(writerRef, newCommitSha);
|
|
652
|
-
|
|
653
|
-
// 9. Notify success callback (updates graph's version vector + eager re-materialize)
|
|
654
|
-
if (this._onCommitSuccess) {
|
|
655
|
-
try {
|
|
656
|
-
await this._onCommitSuccess({ patch, sha: newCommitSha });
|
|
657
|
-
} catch (err) {
|
|
658
|
-
// Commit is already persisted — log but don't fail the caller.
|
|
659
|
-
this._logger.warn(`[warp] onCommitSuccess callback failed (sha=${newCommitSha}):`, err);
|
|
660
|
-
}
|
|
733
|
+
this._committed = true;
|
|
734
|
+
return newCommitSha;
|
|
735
|
+
} finally {
|
|
736
|
+
this._committing = false;
|
|
661
737
|
}
|
|
662
|
-
|
|
663
|
-
return newCommitSha;
|
|
664
738
|
}
|
|
665
739
|
|
|
666
740
|
/**
|
|
@@ -697,22 +771,13 @@ export class PatchBuilderV2 {
|
|
|
697
771
|
}
|
|
698
772
|
|
|
699
773
|
/**
|
|
700
|
-
* Gets the set of
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
* - Endpoint nodes read via `addEdge` (implicit existence reference)
|
|
705
|
-
* - Edge keys read via `removeEdge` (to observe dots)
|
|
706
|
-
* - Nodes read via `setProperty` (implicit existence reference)
|
|
707
|
-
* - Edge keys read via `setEdgeProperty` (implicit existence reference)
|
|
708
|
-
*
|
|
709
|
-
* Note: Returns a defensive copy to prevent external mutation of internal state.
|
|
710
|
-
* The returned Set is a copy, so mutations to it do not affect the builder.
|
|
711
|
-
*
|
|
712
|
-
* @returns {ReadonlySet<string>} Copy of node IDs and encoded edge keys that were read
|
|
774
|
+
* Gets the set of observed operands (entities whose state was consulted).
|
|
775
|
+
* Retains the `reads` name for API/serialization compatibility.
|
|
776
|
+
* Internal field is `_observedOperands`.
|
|
777
|
+
* @returns {ReadonlySet<string>}
|
|
713
778
|
*/
|
|
714
779
|
get reads() {
|
|
715
|
-
return new Set(this.
|
|
780
|
+
return new Set(this._observedOperands);
|
|
716
781
|
}
|
|
717
782
|
|
|
718
783
|
/**
|