@git-stunts/git-warp 12.2.0 → 12.3.0
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 +9 -6
- package/bin/cli/commands/trust.js +37 -1
- package/bin/cli/infrastructure.js +14 -1
- package/bin/cli/schemas.js +4 -4
- package/bin/presenters/text.js +10 -3
- package/bin/warp-graph.js +4 -1
- package/index.d.ts +17 -1
- package/package.json +1 -1
- package/src/domain/WarpGraph.js +1 -1
- package/src/domain/crdt/Dot.js +5 -0
- package/src/domain/crdt/LWW.js +3 -1
- package/src/domain/crdt/ORSet.js +33 -23
- 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/WriterError.js +5 -0
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AuditReceiptService.js +2 -1
- package/src/domain/services/AuditVerifierService.js +33 -2
- package/src/domain/services/BitmapIndexBuilder.js +14 -9
- package/src/domain/services/BoundaryTransitionRecord.js +1 -0
- package/src/domain/services/CheckpointMessageCodec.js +5 -0
- package/src/domain/services/CheckpointService.js +29 -2
- package/src/domain/services/GCPolicy.js +25 -4
- package/src/domain/services/GraphTraversal.js +3 -1
- package/src/domain/services/IncrementalIndexUpdater.js +179 -36
- package/src/domain/services/JoinReducer.js +311 -75
- package/src/domain/services/KeyCodec.js +48 -0
- package/src/domain/services/MaterializedViewService.js +14 -3
- package/src/domain/services/MessageSchemaDetector.js +35 -5
- package/src/domain/services/OpNormalizer.js +79 -0
- package/src/domain/services/PatchBuilderV2.js +240 -160
- package/src/domain/services/QueryBuilder.js +4 -0
- package/src/domain/services/SyncAuthService.js +3 -0
- package/src/domain/services/SyncController.js +12 -31
- package/src/domain/services/SyncProtocol.js +76 -32
- package/src/domain/services/WarpMessageCodec.js +2 -0
- package/src/domain/trust/TrustCrypto.js +8 -5
- package/src/domain/trust/TrustRecordService.js +50 -36
- package/src/domain/types/TickReceipt.js +6 -4
- package/src/domain/types/WarpTypesV2.js +77 -5
- 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/defaultClock.js +1 -0
- package/src/domain/utils/matchGlob.js +7 -0
- package/src/domain/warp/PatchSession.js +30 -24
- package/src/domain/warp/Writer.js +12 -1
- package/src/domain/warp/_wiredMethods.d.ts +1 -1
- package/src/domain/warp/checkpoint.methods.js +36 -7
- package/src/domain/warp/fork.methods.js +1 -1
- package/src/domain/warp/materialize.methods.js +44 -5
- package/src/domain/warp/materializeAdvanced.methods.js +50 -10
- package/src/domain/warp/patch.methods.js +21 -11
- package/src/infrastructure/adapters/GitGraphAdapter.js +55 -52
- package/src/infrastructure/codecs/CborCodec.js +2 -0
- package/src/domain/utils/fnv1a.js +0 -20
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import { QueryError, E_NO_STATE_MSG, E_STALE_STATE_MSG } from './_internal.js';
|
|
12
12
|
import { PatchBuilderV2 } from '../services/PatchBuilderV2.js';
|
|
13
|
-
import { joinStates,
|
|
13
|
+
import { joinStates, applyWithDiff, applyWithReceipt } from '../services/JoinReducer.js';
|
|
14
14
|
import { orsetElements } from '../crdt/ORSet.js';
|
|
15
|
-
import { vvIncrement } from '../crdt/VersionVector.js';
|
|
15
|
+
import { vvIncrement, vvClone } from '../crdt/VersionVector.js';
|
|
16
16
|
import { buildWriterRef, buildWritersPrefix, parseWriterIdFromRef } from '../utils/RefLayout.js';
|
|
17
17
|
import { decodePatchMessage, detectMessageKind } from '../services/WarpMessageCodec.js';
|
|
18
18
|
import { Writer } from './Writer.js';
|
|
@@ -220,15 +220,16 @@ export async function _onPatchCommitted(writerId, { patch: committed, sha } = {}
|
|
|
220
220
|
// Only when the cache is clean — applying a patch to stale state would be incorrect
|
|
221
221
|
if (this._cachedState && !this._stateDirty && committed && sha) {
|
|
222
222
|
let tickReceipt = null;
|
|
223
|
+
/** @type {import('../types/PatchDiff.js').PatchDiff|null} */
|
|
224
|
+
let diff = null;
|
|
223
225
|
if (this._auditService) {
|
|
224
|
-
const result =
|
|
225
|
-
joinPatch(this._cachedState, /** @type {Parameters<typeof joinPatch>[1]} */ (committed), sha, true)
|
|
226
|
-
);
|
|
226
|
+
const result = applyWithReceipt(this._cachedState, committed, sha);
|
|
227
227
|
tickReceipt = result.receipt;
|
|
228
228
|
} else {
|
|
229
|
-
|
|
229
|
+
const result = applyWithDiff(this._cachedState, committed, sha);
|
|
230
|
+
diff = result.diff;
|
|
230
231
|
}
|
|
231
|
-
await this._setMaterializedState(this._cachedState);
|
|
232
|
+
await this._setMaterializedState(this._cachedState, { diff });
|
|
232
233
|
// Update provenance index with new patch
|
|
233
234
|
if (this._provenanceIndex) {
|
|
234
235
|
this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (committed.reads), /** @type {string[]|undefined} */ (committed.writes));
|
|
@@ -247,6 +248,7 @@ export async function _onPatchCommitted(writerId, { patch: committed, sha } = {}
|
|
|
247
248
|
}
|
|
248
249
|
} else {
|
|
249
250
|
this._stateDirty = true;
|
|
251
|
+
this._cachedViewHash = null;
|
|
250
252
|
if (this._auditService) {
|
|
251
253
|
this._auditSkipCount++;
|
|
252
254
|
this._logger?.warn('[warp:audit]', {
|
|
@@ -289,6 +291,7 @@ export async function writer(writerId) {
|
|
|
289
291
|
onDeleteWithData: this._onDeleteWithData,
|
|
290
292
|
onCommitSuccess: /** @type {(result: {patch: Object, sha: string}) => void} */ (/** @type {unknown} */ ((/** @type {{patch?: import('../types/WarpTypesV2.js').PatchV2, sha?: string}} */ opts) => this._onPatchCommitted(resolvedWriterId, opts))),
|
|
291
293
|
codec: this._codec,
|
|
294
|
+
logger: this._logger || undefined,
|
|
292
295
|
});
|
|
293
296
|
}
|
|
294
297
|
|
|
@@ -347,6 +350,7 @@ export async function createWriter(opts = {}) {
|
|
|
347
350
|
onDeleteWithData: this._onDeleteWithData,
|
|
348
351
|
onCommitSuccess: /** @type {(result: {patch: Object, sha: string}) => void} */ (/** @type {unknown} */ ((/** @type {{patch?: import('../types/WarpTypesV2.js').PatchV2, sha?: string}} */ commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts))),
|
|
349
352
|
codec: this._codec,
|
|
353
|
+
logger: this._logger || undefined,
|
|
350
354
|
});
|
|
351
355
|
}
|
|
352
356
|
|
|
@@ -527,16 +531,22 @@ export function join(otherState) {
|
|
|
527
531
|
!this._frontierEquals(this._cachedState.observedFrontier, mergedState.observedFrontier),
|
|
528
532
|
};
|
|
529
533
|
|
|
530
|
-
//
|
|
534
|
+
// Install merged state as canonical (B108 — cache coherence fix)
|
|
531
535
|
this._cachedState = mergedState;
|
|
536
|
+
this._versionVector = vvClone(mergedState.observedFrontier);
|
|
532
537
|
|
|
533
|
-
//
|
|
534
|
-
|
|
538
|
+
// Build adjacency synchronously (crypto hash deferred to next _buildView)
|
|
539
|
+
const adjacency = this._buildAdjacency(mergedState);
|
|
540
|
+
this._materializedGraph = { state: mergedState, stateHash: null, adjacency };
|
|
541
|
+
|
|
542
|
+
// Clear index caches — queries degrade to linear scan until next _buildView
|
|
535
543
|
this._logicalIndex = null;
|
|
536
544
|
this._propertyReader = null;
|
|
537
545
|
this._cachedViewHash = null;
|
|
538
546
|
this._cachedIndexTree = null;
|
|
539
|
-
|
|
547
|
+
|
|
548
|
+
// State IS fresh — don't force rematerialization
|
|
549
|
+
this._stateDirty = false;
|
|
540
550
|
|
|
541
551
|
return { state: mergedState, receipt };
|
|
542
552
|
}
|
|
@@ -135,29 +135,6 @@ function isDanglingObjectError(err) {
|
|
|
135
135
|
);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
/**
|
|
139
|
-
* Checks whether a Git ref exists without resolving it.
|
|
140
|
-
* @param {function(Object): Promise<string>} execute - The git command executor function
|
|
141
|
-
* @param {string} ref - The ref to check (e.g., 'refs/warp/events/writers/alice')
|
|
142
|
-
* @returns {Promise<boolean>} True if the ref exists, false otherwise
|
|
143
|
-
* @throws {Error} If the git command fails for reasons other than a missing ref
|
|
144
|
-
*/
|
|
145
|
-
async function refExists(execute, ref) {
|
|
146
|
-
try {
|
|
147
|
-
await execute({ args: ['show-ref', '--verify', '--quiet', ref] });
|
|
148
|
-
return true;
|
|
149
|
-
} catch (err) {
|
|
150
|
-
const gitErr = /** @type {GitError} */ (err);
|
|
151
|
-
if (getExitCode(gitErr) === 1) {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
if (isDanglingObjectError(gitErr)) {
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
throw err;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
138
|
/**
|
|
162
139
|
* Concrete implementation of {@link GraphPersistencePort} using Git plumbing commands.
|
|
163
140
|
*
|
|
@@ -262,26 +239,37 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
262
239
|
}
|
|
263
240
|
|
|
264
241
|
/**
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
* @param {
|
|
268
|
-
* @
|
|
269
|
-
* @
|
|
270
|
-
* @returns {Promise<string>} The SHA of the created commit
|
|
271
|
-
* @throws {Error} If any parent OID is invalid
|
|
242
|
+
* Shared helper for commit creation. Validates parents, builds args, and
|
|
243
|
+
* executes `git commit-tree` with retry.
|
|
244
|
+
* @param {{ tree: string, parents: string[], message: string, sign: boolean }} opts
|
|
245
|
+
* @returns {Promise<string>} The created commit SHA
|
|
246
|
+
* @private
|
|
272
247
|
*/
|
|
273
|
-
async
|
|
248
|
+
async _createCommit({ tree, parents, message, sign }) {
|
|
274
249
|
for (const p of parents) {
|
|
275
250
|
this._validateOid(p);
|
|
276
251
|
}
|
|
277
252
|
const parentArgs = parents.flatMap(p => ['-p', p]);
|
|
278
253
|
const signArgs = sign ? ['-S'] : [];
|
|
279
|
-
const args = ['commit-tree',
|
|
254
|
+
const args = ['commit-tree', tree, ...parentArgs, ...signArgs, '-m', message];
|
|
280
255
|
|
|
281
256
|
const oid = await this._executeWithRetry({ args });
|
|
282
257
|
return oid.trim();
|
|
283
258
|
}
|
|
284
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Creates a commit pointing to the empty tree.
|
|
262
|
+
* @param {Object} options
|
|
263
|
+
* @param {string} options.message - The commit message (typically CBOR-encoded patch data)
|
|
264
|
+
* @param {string[]} [options.parents=[]] - Parent commit SHAs
|
|
265
|
+
* @param {boolean} [options.sign=false] - Whether to GPG-sign the commit
|
|
266
|
+
* @returns {Promise<string>} The SHA of the created commit
|
|
267
|
+
* @throws {Error} If any parent OID is invalid
|
|
268
|
+
*/
|
|
269
|
+
async commitNode({ message, parents = [], sign = false }) {
|
|
270
|
+
return await this._createCommit({ tree: this.emptyTree, parents, message, sign });
|
|
271
|
+
}
|
|
272
|
+
|
|
285
273
|
/**
|
|
286
274
|
* Creates a commit pointing to a custom tree (not the empty tree).
|
|
287
275
|
* Used for WARP patch commits that have attachment trees.
|
|
@@ -294,15 +282,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
294
282
|
*/
|
|
295
283
|
async commitNodeWithTree({ treeOid, parents = [], message, sign = false }) {
|
|
296
284
|
this._validateOid(treeOid);
|
|
297
|
-
|
|
298
|
-
this._validateOid(p);
|
|
299
|
-
}
|
|
300
|
-
const parentArgs = parents.flatMap(p => ['-p', p]);
|
|
301
|
-
const signArgs = sign ? ['-S'] : [];
|
|
302
|
-
const args = ['commit-tree', treeOid, ...parentArgs, ...signArgs, '-m', message];
|
|
303
|
-
|
|
304
|
-
const oid = await this._executeWithRetry({ args });
|
|
305
|
-
return oid.trim();
|
|
285
|
+
return await this._createCommit({ tree: treeOid, parents, message, sign });
|
|
306
286
|
}
|
|
307
287
|
|
|
308
288
|
/**
|
|
@@ -402,8 +382,13 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
402
382
|
// -z flag ensures NUL-terminated output and ignores i18n.logOutputEncoding config
|
|
403
383
|
const args = ['log', '-z', `-${limit}`];
|
|
404
384
|
if (format) {
|
|
405
|
-
// Strip NUL bytes from
|
|
406
|
-
//
|
|
385
|
+
// Strip NUL (\x00) bytes from the caller-supplied format string.
|
|
386
|
+
// Why: Git's -z flag uses NUL as the record terminator in its output.
|
|
387
|
+
// If a format string contains literal NUL bytes (e.g. from %x00 expansion
|
|
388
|
+
// or caller-constructed strings), they corrupt the NUL-delimited output
|
|
389
|
+
// stream, causing downstream parsers to split records at the wrong
|
|
390
|
+
// boundaries. Additionally, Node.js child_process rejects argv entries
|
|
391
|
+
// that contain null bytes, so passing them through would throw.
|
|
407
392
|
// eslint-disable-next-line no-control-regex
|
|
408
393
|
const cleanFormat = format.replace(/\x00/g, '');
|
|
409
394
|
args.push(`--format=${cleanFormat}`);
|
|
@@ -415,6 +400,8 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
415
400
|
/**
|
|
416
401
|
* Validates that a ref is safe to use in git commands.
|
|
417
402
|
* Delegates to shared validation in adapterValidation.js.
|
|
403
|
+
*
|
|
404
|
+
* Instance method for port interface conformance and test mockability.
|
|
418
405
|
* @param {string} ref - The ref to validate
|
|
419
406
|
* @throws {Error} If ref contains invalid characters, is too long, or starts with -/--
|
|
420
407
|
* @private
|
|
@@ -451,7 +438,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
451
438
|
|
|
452
439
|
/**
|
|
453
440
|
* Reads a tree and returns a map of path to content.
|
|
454
|
-
*
|
|
441
|
+
* Reads blobs in batches of 16 to balance concurrency against fd/process limits.
|
|
455
442
|
* @param {string} treeOid - The tree OID to read
|
|
456
443
|
* @returns {Promise<Record<string, Buffer>>} Map of file path to blob content
|
|
457
444
|
*/
|
|
@@ -459,9 +446,16 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
459
446
|
const oids = await this.readTreeOids(treeOid);
|
|
460
447
|
/** @type {Record<string, Buffer>} */
|
|
461
448
|
const files = {};
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
449
|
+
const entries = Object.entries(oids);
|
|
450
|
+
const BATCH_SIZE = 16;
|
|
451
|
+
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
|
452
|
+
const batch = entries.slice(i, i + BATCH_SIZE);
|
|
453
|
+
const results = await Promise.all(
|
|
454
|
+
batch.map(([, oid]) => this.readBlob(oid))
|
|
455
|
+
);
|
|
456
|
+
for (let j = 0; j < batch.length; j++) {
|
|
457
|
+
files[batch[j][0]] = results[j];
|
|
458
|
+
}
|
|
465
459
|
}
|
|
466
460
|
return files;
|
|
467
461
|
}
|
|
@@ -539,20 +533,21 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
539
533
|
*/
|
|
540
534
|
async readRef(ref) {
|
|
541
535
|
this._validateRef(ref);
|
|
542
|
-
const exists = await refExists(this._executeWithRetry.bind(this), ref);
|
|
543
|
-
if (!exists) {
|
|
544
|
-
return null;
|
|
545
|
-
}
|
|
546
536
|
try {
|
|
537
|
+
// --verify ensures exactly one revision is resolved; --quiet suppresses
|
|
538
|
+
// error messages and makes exit code 1 (not 128) the indicator for
|
|
539
|
+
// "ref does not exist", simplifying downstream handling.
|
|
547
540
|
const oid = await this._executeWithRetry({
|
|
548
|
-
args: ['rev-parse', ref]
|
|
541
|
+
args: ['rev-parse', '--verify', '--quiet', ref]
|
|
549
542
|
});
|
|
550
543
|
return oid.trim();
|
|
551
544
|
} catch (err) {
|
|
552
545
|
const gitErr = /** @type {GitError} */ (err);
|
|
546
|
+
// Exit code 1: ref does not exist (normal with --verify --quiet)
|
|
553
547
|
if (getExitCode(gitErr) === 1) {
|
|
554
548
|
return null;
|
|
555
549
|
}
|
|
550
|
+
// Exit code 128 with dangling-object stderr: ref exists but target is missing
|
|
556
551
|
if (isDanglingObjectError(gitErr)) {
|
|
557
552
|
return null;
|
|
558
553
|
}
|
|
@@ -602,6 +597,10 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
602
597
|
/**
|
|
603
598
|
* Validates that an OID is safe to use in git commands.
|
|
604
599
|
* Delegates to shared validation in adapterValidation.js.
|
|
600
|
+
*
|
|
601
|
+
* Exists as a method (rather than inlining the import) so tests can
|
|
602
|
+
* spy/stub validation independently and so future adapters sharing
|
|
603
|
+
* the same port interface can override validation rules.
|
|
605
604
|
* @param {string} oid - The OID to validate
|
|
606
605
|
* @throws {Error} If OID is invalid
|
|
607
606
|
* @private
|
|
@@ -613,6 +612,8 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
613
612
|
/**
|
|
614
613
|
* Validates that a limit is a safe positive integer.
|
|
615
614
|
* Delegates to shared validation in adapterValidation.js.
|
|
615
|
+
*
|
|
616
|
+
* Instance method for port interface conformance and test mockability.
|
|
616
617
|
* @param {number} limit - The limit to validate
|
|
617
618
|
* @throws {Error} If limit is invalid
|
|
618
619
|
* @private
|
|
@@ -759,6 +760,8 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
759
760
|
/**
|
|
760
761
|
* Validates that a config key is safe to use in git commands.
|
|
761
762
|
* Delegates to shared validation in adapterValidation.js.
|
|
763
|
+
*
|
|
764
|
+
* Instance method for port interface conformance and test mockability.
|
|
762
765
|
* @param {string} key - The config key to validate
|
|
763
766
|
* @throws {Error} If key is invalid
|
|
764
767
|
* @private
|
|
@@ -96,6 +96,8 @@ function isPlainObject(value) {
|
|
|
96
96
|
function sortPlainObject(obj) {
|
|
97
97
|
/** @type {Record<string, unknown>} */
|
|
98
98
|
const sorted = {};
|
|
99
|
+
// Key sort ensures deterministic CBOR encoding regardless of insertion order.
|
|
100
|
+
// Required for content-addressed storage where byte-identical encoding is critical.
|
|
99
101
|
const keys = Object.keys(obj).sort();
|
|
100
102
|
for (const key of keys) {
|
|
101
103
|
sorted[key] = sortKeys(obj[key]);
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FNV-1a 32-bit hash function.
|
|
3
|
-
*
|
|
4
|
-
* Used for shard key computation when the input is not a hex SHA.
|
|
5
|
-
* Uses Math.imul for correct 32-bit multiplication semantics.
|
|
6
|
-
*
|
|
7
|
-
* @note Callers with non-ASCII node IDs should normalize to NFC before
|
|
8
|
-
* hashing to ensure consistent shard placement.
|
|
9
|
-
*
|
|
10
|
-
* @param {string} str - Input string
|
|
11
|
-
* @returns {number} Unsigned 32-bit FNV-1a hash
|
|
12
|
-
*/
|
|
13
|
-
export default function fnv1a(str) {
|
|
14
|
-
let hash = 0x811c9dc5; // FNV offset basis
|
|
15
|
-
for (let i = 0; i < str.length; i++) {
|
|
16
|
-
hash ^= str.charCodeAt(i);
|
|
17
|
-
hash = Math.imul(hash, 0x01000193); // FNV prime
|
|
18
|
-
}
|
|
19
|
-
return hash >>> 0; // Ensure unsigned
|
|
20
|
-
}
|