@git-stunts/git-warp 11.5.1 → 12.1.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 +137 -10
- package/bin/cli/commands/registry.js +4 -0
- package/bin/cli/commands/reindex.js +41 -0
- package/bin/cli/commands/verify-index.js +59 -0
- package/bin/cli/infrastructure.js +7 -2
- package/bin/cli/schemas.js +19 -0
- package/bin/cli/types.js +2 -0
- package/index.d.ts +52 -15
- package/package.json +3 -2
- package/src/domain/WarpGraph.js +40 -0
- package/src/domain/errors/ShardIdOverflowError.js +28 -0
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AdjacencyNeighborProvider.js +140 -0
- package/src/domain/services/BitmapNeighborProvider.js +178 -0
- package/src/domain/services/CheckpointMessageCodec.js +3 -3
- package/src/domain/services/CheckpointService.js +77 -12
- package/src/domain/services/GraphTraversal.js +1239 -0
- package/src/domain/services/IncrementalIndexUpdater.js +765 -0
- package/src/domain/services/JoinReducer.js +233 -5
- package/src/domain/services/LogicalBitmapIndexBuilder.js +323 -0
- package/src/domain/services/LogicalIndexBuildService.js +108 -0
- package/src/domain/services/LogicalIndexReader.js +315 -0
- package/src/domain/services/LogicalTraversal.js +321 -202
- package/src/domain/services/MaterializedViewService.js +379 -0
- package/src/domain/services/ObserverView.js +132 -69
- package/src/domain/services/PatchBuilderV2.js +3 -3
- package/src/domain/services/PropertyIndexBuilder.js +64 -0
- package/src/domain/services/PropertyIndexReader.js +111 -0
- package/src/domain/services/QueryBuilder.js +15 -44
- package/src/domain/services/TemporalQuery.js +128 -14
- package/src/domain/services/TranslationCost.js +8 -24
- package/src/domain/types/PatchDiff.js +90 -0
- package/src/domain/types/WarpTypesV2.js +4 -4
- package/src/domain/utils/MinHeap.js +45 -17
- package/src/domain/utils/canonicalCbor.js +36 -0
- package/src/domain/utils/fnv1a.js +20 -0
- package/src/domain/utils/matchGlob.js +51 -0
- package/src/domain/utils/roaring.js +14 -3
- package/src/domain/utils/shardKey.js +40 -0
- package/src/domain/utils/toBytes.js +17 -0
- package/src/domain/warp/_wiredMethods.d.ts +7 -1
- package/src/domain/warp/checkpoint.methods.js +21 -5
- package/src/domain/warp/materialize.methods.js +17 -5
- package/src/domain/warp/materializeAdvanced.methods.js +142 -3
- package/src/domain/warp/query.methods.js +83 -15
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +26 -5
- package/src/ports/BlobPort.js +1 -1
- package/src/ports/NeighborProviderPort.js +59 -0
- package/src/ports/SeekCachePort.js +4 -3
|
@@ -8,10 +8,18 @@
|
|
|
8
8
|
class MinHeap {
|
|
9
9
|
/**
|
|
10
10
|
* Creates an empty MinHeap.
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} [options] - Configuration options
|
|
13
|
+
* @param {((a: T, b: T) => number)} [options.tieBreaker] - Comparator invoked when two
|
|
14
|
+
* entries have equal priority. Negative return = a wins (comes out first).
|
|
15
|
+
* When omitted, equal-priority extraction order is unspecified (heap-natural).
|
|
11
16
|
*/
|
|
12
|
-
constructor() {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
const { tieBreaker } = options || {};
|
|
13
19
|
/** @type {Array<{item: T, priority: number}>} */
|
|
14
|
-
this.
|
|
20
|
+
this._heap = [];
|
|
21
|
+
/** @type {((a: T, b: T) => number) | undefined} */
|
|
22
|
+
this._tieBreaker = tieBreaker;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
/**
|
|
@@ -22,8 +30,8 @@ class MinHeap {
|
|
|
22
30
|
* @returns {void}
|
|
23
31
|
*/
|
|
24
32
|
insert(item, priority) {
|
|
25
|
-
this.
|
|
26
|
-
this._bubbleUp(this.
|
|
33
|
+
this._heap.push({ item, priority });
|
|
34
|
+
this._bubbleUp(this._heap.length - 1);
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
/**
|
|
@@ -32,11 +40,11 @@ class MinHeap {
|
|
|
32
40
|
* @returns {T | undefined} The item with lowest priority, or undefined if empty
|
|
33
41
|
*/
|
|
34
42
|
extractMin() {
|
|
35
|
-
if (this.
|
|
36
|
-
if (this.
|
|
43
|
+
if (this._heap.length === 0) { return undefined; }
|
|
44
|
+
if (this._heap.length === 1) { return /** @type {{item: T, priority: number}} */ (this._heap.pop()).item; }
|
|
37
45
|
|
|
38
|
-
const min = this.
|
|
39
|
-
this.
|
|
46
|
+
const min = this._heap[0];
|
|
47
|
+
this._heap[0] = /** @type {{item: T, priority: number}} */ (this._heap.pop());
|
|
40
48
|
this._bubbleDown(0);
|
|
41
49
|
return min.item;
|
|
42
50
|
}
|
|
@@ -47,7 +55,7 @@ class MinHeap {
|
|
|
47
55
|
* @returns {boolean} True if empty
|
|
48
56
|
*/
|
|
49
57
|
isEmpty() {
|
|
50
|
-
return this.
|
|
58
|
+
return this._heap.length === 0;
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
/**
|
|
@@ -56,7 +64,7 @@ class MinHeap {
|
|
|
56
64
|
* @returns {number} Number of items
|
|
57
65
|
*/
|
|
58
66
|
size() {
|
|
59
|
-
return this.
|
|
67
|
+
return this._heap.length;
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
/**
|
|
@@ -65,7 +73,27 @@ class MinHeap {
|
|
|
65
73
|
* @returns {number} The minimum priority value, or Infinity if empty
|
|
66
74
|
*/
|
|
67
75
|
peekPriority() {
|
|
68
|
-
return this.
|
|
76
|
+
return this._heap.length > 0 ? this._heap[0].priority : Infinity;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Compares two heap entries. Returns negative if a should come before b.
|
|
81
|
+
*
|
|
82
|
+
* @private
|
|
83
|
+
* @param {number} idxA - Index of first entry
|
|
84
|
+
* @param {number} idxB - Index of second entry
|
|
85
|
+
* @returns {number} Negative if a < b, positive if a > b, zero if equal
|
|
86
|
+
*/
|
|
87
|
+
_compare(idxA, idxB) {
|
|
88
|
+
const a = this._heap[idxA];
|
|
89
|
+
const b = this._heap[idxB];
|
|
90
|
+
if (a.priority !== b.priority) {
|
|
91
|
+
return a.priority - b.priority;
|
|
92
|
+
}
|
|
93
|
+
if (this._tieBreaker) {
|
|
94
|
+
return this._tieBreaker(a.item, b.item);
|
|
95
|
+
}
|
|
96
|
+
return 0;
|
|
69
97
|
}
|
|
70
98
|
|
|
71
99
|
/**
|
|
@@ -78,8 +106,8 @@ class MinHeap {
|
|
|
78
106
|
let current = pos;
|
|
79
107
|
while (current > 0) {
|
|
80
108
|
const parentIndex = Math.floor((current - 1) / 2);
|
|
81
|
-
if (this.
|
|
82
|
-
[this.
|
|
109
|
+
if (this._compare(parentIndex, current) <= 0) { break; }
|
|
110
|
+
[this._heap[parentIndex], this._heap[current]] = [this._heap[current], this._heap[parentIndex]];
|
|
83
111
|
current = parentIndex;
|
|
84
112
|
}
|
|
85
113
|
}
|
|
@@ -91,22 +119,22 @@ class MinHeap {
|
|
|
91
119
|
* @param {number} pos - Starting index
|
|
92
120
|
*/
|
|
93
121
|
_bubbleDown(pos) {
|
|
94
|
-
const {length} = this.
|
|
122
|
+
const {length} = this._heap;
|
|
95
123
|
let current = pos;
|
|
96
124
|
while (true) {
|
|
97
125
|
const leftChild = 2 * current + 1;
|
|
98
126
|
const rightChild = 2 * current + 2;
|
|
99
127
|
let smallest = current;
|
|
100
128
|
|
|
101
|
-
if (leftChild < length && this.
|
|
129
|
+
if (leftChild < length && this._compare(leftChild, smallest) < 0) {
|
|
102
130
|
smallest = leftChild;
|
|
103
131
|
}
|
|
104
|
-
if (rightChild < length && this.
|
|
132
|
+
if (rightChild < length && this._compare(rightChild, smallest) < 0) {
|
|
105
133
|
smallest = rightChild;
|
|
106
134
|
}
|
|
107
135
|
if (smallest === current) { break; }
|
|
108
136
|
|
|
109
|
-
[this.
|
|
137
|
+
[this._heap[current], this._heap[smallest]] = [this._heap[smallest], this._heap[current]];
|
|
110
138
|
current = smallest;
|
|
111
139
|
}
|
|
112
140
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical CBOR encoding/decoding.
|
|
3
|
+
*
|
|
4
|
+
* Delegates to defaultCodec which already sorts keys recursively
|
|
5
|
+
* and handles Maps, null-prototype objects, and arrays.
|
|
6
|
+
*
|
|
7
|
+
* Deterministic output relies on cbor-x's key-sorting behaviour,
|
|
8
|
+
* which approximates RFC 7049 Section 3.9 (Canonical CBOR) by sorting
|
|
9
|
+
* map keys in length-first lexicographic order. This is sufficient for
|
|
10
|
+
* content-addressed equality within the WARP system but should not be
|
|
11
|
+
* assumed to match other canonical CBOR implementations byte-for-byte.
|
|
12
|
+
*
|
|
13
|
+
* @module domain/utils/canonicalCbor
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import defaultCodec from './defaultCodec.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Encodes a value to canonical CBOR bytes with sorted keys.
|
|
20
|
+
*
|
|
21
|
+
* @param {unknown} value - The value to encode
|
|
22
|
+
* @returns {Uint8Array} CBOR-encoded bytes
|
|
23
|
+
*/
|
|
24
|
+
export function encodeCanonicalCbor(value) {
|
|
25
|
+
return defaultCodec.encode(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Decodes CBOR bytes to a value.
|
|
30
|
+
*
|
|
31
|
+
* @param {Buffer|Uint8Array} buffer - CBOR bytes
|
|
32
|
+
* @returns {unknown} Decoded value
|
|
33
|
+
*/
|
|
34
|
+
export function decodeCanonicalCbor(buffer) {
|
|
35
|
+
return defaultCodec.decode(buffer);
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** @type {Map<string, RegExp>} Module-level cache for compiled glob regexes. */
|
|
2
|
+
const globRegexCache = new Map();
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Escapes special regex characters in a string so it can be used as a literal match.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} value - The string to escape
|
|
8
|
+
* @returns {string} The escaped string safe for use in a RegExp
|
|
9
|
+
* @private
|
|
10
|
+
*/
|
|
11
|
+
function escapeRegex(value) {
|
|
12
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tests whether a string matches a glob-style pattern or an array of patterns.
|
|
17
|
+
*
|
|
18
|
+
* Supports:
|
|
19
|
+
* - `*` as the default pattern, matching all strings
|
|
20
|
+
* - Wildcard `*` anywhere in the pattern, matching zero or more characters
|
|
21
|
+
* - Literal match when pattern contains no wildcards
|
|
22
|
+
* - Array of patterns: returns true if ANY pattern matches (OR semantics)
|
|
23
|
+
*
|
|
24
|
+
* @param {string|string[]} pattern - The glob pattern(s) to match against
|
|
25
|
+
* @param {string} str - The string to test
|
|
26
|
+
* @returns {boolean} True if the string matches any of the patterns
|
|
27
|
+
*/
|
|
28
|
+
export function matchGlob(pattern, str) {
|
|
29
|
+
if (Array.isArray(pattern)) {
|
|
30
|
+
return pattern.some((p) => matchGlob(p, str));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (pattern === '*') {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof pattern !== 'string') {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!pattern.includes('*')) {
|
|
42
|
+
return pattern === str;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let regex = globRegexCache.get(pattern);
|
|
46
|
+
if (!regex) {
|
|
47
|
+
regex = new RegExp(`^${escapeRegex(pattern).replace(/\\\*/g, '.*')}$`);
|
|
48
|
+
globRegexCache.set(pattern, regex);
|
|
49
|
+
}
|
|
50
|
+
return regex.test(str);
|
|
51
|
+
}
|
|
@@ -50,6 +50,7 @@ const NOT_CHECKED = Symbol('NOT_CHECKED');
|
|
|
50
50
|
* @typedef {Object} RoaringBitmapSubset
|
|
51
51
|
* @property {number} size
|
|
52
52
|
* @property {function(number): void} add
|
|
53
|
+
* @property {function(number): void} remove
|
|
53
54
|
* @property {function(number): boolean} has
|
|
54
55
|
* @property {function(Iterable<number>): void} orInPlace
|
|
55
56
|
* @property {function(boolean): Uint8Array} serialize
|
|
@@ -63,6 +64,13 @@ const NOT_CHECKED = Symbol('NOT_CHECKED');
|
|
|
63
64
|
*/
|
|
64
65
|
let roaringModule = null;
|
|
65
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Captures module initialization failure so callers can see the root cause.
|
|
69
|
+
* @type {unknown}
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
let initError = null;
|
|
73
|
+
|
|
66
74
|
/**
|
|
67
75
|
* Cached result of native availability check.
|
|
68
76
|
* `NOT_CHECKED` means not yet checked, `null` means indeterminate.
|
|
@@ -83,7 +91,8 @@ let nativeAvailability = NOT_CHECKED;
|
|
|
83
91
|
*/
|
|
84
92
|
function loadRoaring() {
|
|
85
93
|
if (!roaringModule) {
|
|
86
|
-
|
|
94
|
+
const cause = initError instanceof Error ? ` Caused by: ${initError.message}` : '';
|
|
95
|
+
throw new Error(`Roaring module not loaded. Call initRoaring() first or ensure top-level await import completed.${cause}`);
|
|
87
96
|
}
|
|
88
97
|
return roaringModule;
|
|
89
98
|
}
|
|
@@ -99,6 +108,7 @@ function loadRoaring() {
|
|
|
99
108
|
export async function initRoaring(mod) {
|
|
100
109
|
if (mod) {
|
|
101
110
|
roaringModule = mod;
|
|
111
|
+
initError = null;
|
|
102
112
|
return;
|
|
103
113
|
}
|
|
104
114
|
if (!roaringModule) {
|
|
@@ -113,8 +123,9 @@ export async function initRoaring(mod) {
|
|
|
113
123
|
// Auto-initialize on module load (top-level await)
|
|
114
124
|
try {
|
|
115
125
|
await initRoaring();
|
|
116
|
-
} catch {
|
|
117
|
-
// Roaring may not be installed;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
// Roaring may not be installed; keep root cause for downstream diagnostics.
|
|
128
|
+
initError = err;
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
/**
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const HEX_RE = /^[0-9a-fA-F]{40}$|^[0-9a-fA-F]{64}$/;
|
|
2
|
+
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* FNV-1a 32-bit over raw bytes (Uint8Array).
|
|
7
|
+
*
|
|
8
|
+
* @param {Uint8Array} bytes
|
|
9
|
+
* @returns {number} Unsigned 32-bit hash
|
|
10
|
+
*/
|
|
11
|
+
function fnv1aBytes(bytes) {
|
|
12
|
+
let hash = 0x811c9dc5;
|
|
13
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
14
|
+
hash ^= bytes[i];
|
|
15
|
+
hash = Math.imul(hash, 0x01000193);
|
|
16
|
+
}
|
|
17
|
+
return hash >>> 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Computes a 2-character hex shard key for a given ID.
|
|
22
|
+
*
|
|
23
|
+
* For hex SHAs (exactly 40 or 64 hex chars), uses the first two characters (lowercased).
|
|
24
|
+
* For all other strings, computes FNV-1a hash over UTF-8 bytes and takes the low byte.
|
|
25
|
+
*
|
|
26
|
+
* Returns '00' for null, undefined, or non-string inputs (graceful fallback).
|
|
27
|
+
*
|
|
28
|
+
* @param {string} id - Node ID or SHA
|
|
29
|
+
* @returns {string} 2-character lowercase hex shard key (e.g. 'ab', '0f')
|
|
30
|
+
*/
|
|
31
|
+
export default function computeShardKey(id) {
|
|
32
|
+
if (id === null || id === undefined || typeof id !== 'string') {
|
|
33
|
+
return '00';
|
|
34
|
+
}
|
|
35
|
+
if (HEX_RE.test(id)) {
|
|
36
|
+
return id.substring(0, 2).toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
const hash = fnv1aBytes(encoder.encode(id));
|
|
39
|
+
return (hash & 0xff).toString(16).padStart(2, '0');
|
|
40
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a decoded byte payload to a Uint8Array.
|
|
3
|
+
*
|
|
4
|
+
* CBOR decoders may yield Buffer, Uint8Array, or plain number[] depending
|
|
5
|
+
* on runtime and codec implementation (e.g. cbor-x on Node vs Deno).
|
|
6
|
+
* This helper ensures Roaring bitmap deserialization and other binary APIs
|
|
7
|
+
* always receive a Uint8Array.
|
|
8
|
+
*
|
|
9
|
+
* @param {Uint8Array|ArrayLike<number>} value
|
|
10
|
+
* @returns {Uint8Array}
|
|
11
|
+
*/
|
|
12
|
+
export default function toBytes(value) {
|
|
13
|
+
if (value instanceof Uint8Array) {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
return Uint8Array.from(value);
|
|
17
|
+
}
|
|
@@ -151,6 +151,7 @@ interface CheckpointData {
|
|
|
151
151
|
stateHash: string;
|
|
152
152
|
schema: number;
|
|
153
153
|
provenanceIndex?: unknown;
|
|
154
|
+
indexShardOids?: Record<string, string>;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
export {};
|
|
@@ -247,8 +248,13 @@ declare module '../WarpGraph.js' {
|
|
|
247
248
|
// ── materializeAdvanced.methods.js ────────────────────────────────────
|
|
248
249
|
_resolveCeiling(options?: { ceiling?: number | null }): number | null;
|
|
249
250
|
_buildAdjacency(state: WarpStateV5): { outgoing: Map<string, Array<{ neighborId: string; label: string }>>; incoming: Map<string, Array<{ neighborId: string; label: string }>> };
|
|
250
|
-
|
|
251
|
+
_buildView(state: WarpStateV5, stateHash: string, diff?: import('../types/PatchDiff.js').PatchDiff): void;
|
|
252
|
+
_setMaterializedState(state: WarpStateV5, diff?: import('../types/PatchDiff.js').PatchDiff): Promise<{ state: WarpStateV5; stateHash: string; adjacency: unknown }>;
|
|
251
253
|
_materializeWithCeiling(ceiling: number, collectReceipts: boolean, t0: number): Promise<WarpStateV5 | { state: WarpStateV5; receipts: TickReceipt[] }>;
|
|
254
|
+
_persistSeekCacheEntry(cacheKey: string, buf: Buffer, state: WarpStateV5): Promise<void>;
|
|
255
|
+
_restoreIndexFromCache(indexTreeOid: string): Promise<void>;
|
|
252
256
|
materializeAt(checkpointSha: string): Promise<WarpStateV5>;
|
|
257
|
+
verifyIndex(options?: { seed?: number; sampleRate?: number }): { passed: number; failed: number; errors: Array<{ nodeId: string; direction: string; expected: string[]; actual: string[] }> };
|
|
258
|
+
invalidateIndex(): void;
|
|
253
259
|
}
|
|
254
260
|
}
|
|
@@ -60,7 +60,22 @@ export async function createCheckpoint() {
|
|
|
60
60
|
this._checkpointing = prevCheckpointing;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// 4.
|
|
63
|
+
// 4. Reuse cached index tree or rebuild from view service
|
|
64
|
+
let indexTree = this._cachedIndexTree;
|
|
65
|
+
if (!indexTree && this._viewService) {
|
|
66
|
+
try {
|
|
67
|
+
const { tree } = this._viewService.build(state);
|
|
68
|
+
indexTree = tree;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
71
|
+
this._logger?.warn('[warp] checkpoint index build failed; saving checkpoint without index', {
|
|
72
|
+
error: message,
|
|
73
|
+
});
|
|
74
|
+
indexTree = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 5. Create checkpoint commit with provenance index + index tree
|
|
64
79
|
/** @type {CorePersistence} */
|
|
65
80
|
const persistence = this._persistence;
|
|
66
81
|
const checkpointSha = await createCheckpointCommit({
|
|
@@ -72,15 +87,16 @@ export async function createCheckpoint() {
|
|
|
72
87
|
provenanceIndex: this._provenanceIndex || undefined,
|
|
73
88
|
crypto: this._crypto,
|
|
74
89
|
codec: this._codec,
|
|
90
|
+
indexTree: indexTree || undefined,
|
|
75
91
|
});
|
|
76
92
|
|
|
77
|
-
//
|
|
93
|
+
// 6. Update checkpoint ref
|
|
78
94
|
const checkpointRef = buildCheckpointRef(this._graphName);
|
|
79
95
|
await this._persistence.updateRef(checkpointRef, checkpointSha);
|
|
80
96
|
|
|
81
97
|
this._logTiming('createCheckpoint', t0);
|
|
82
98
|
|
|
83
|
-
//
|
|
99
|
+
// 7. Return checkpoint SHA
|
|
84
100
|
return checkpointSha;
|
|
85
101
|
} catch (err) {
|
|
86
102
|
this._logTiming('createCheckpoint', t0, { error: /** @type {Error} */ (err) });
|
|
@@ -137,7 +153,7 @@ export async function syncCoverage() {
|
|
|
137
153
|
* Loads the latest checkpoint for this graph.
|
|
138
154
|
*
|
|
139
155
|
* @this {import('../WarpGraph.js').default}
|
|
140
|
-
* @returns {Promise<{state: import('../services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number, provenanceIndex?: import('../services/ProvenanceIndex.js').ProvenanceIndex}|null>} The checkpoint or null
|
|
156
|
+
* @returns {Promise<{state: import('../services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number, provenanceIndex?: import('../services/ProvenanceIndex.js').ProvenanceIndex, indexShardOids?: Record<string, string>|null}|null>} The checkpoint or null
|
|
141
157
|
* @private
|
|
142
158
|
*/
|
|
143
159
|
export async function _loadLatestCheckpoint() {
|
|
@@ -197,7 +213,7 @@ export async function _loadPatchesSince(checkpoint) {
|
|
|
197
213
|
*/
|
|
198
214
|
export async function _validateMigrationBoundary() {
|
|
199
215
|
const checkpoint = await this._loadLatestCheckpoint();
|
|
200
|
-
if (checkpoint?.schema === 2 || checkpoint?.schema === 3) {
|
|
216
|
+
if (checkpoint?.schema === 2 || checkpoint?.schema === 3 || checkpoint?.schema === 4) {
|
|
201
217
|
return; // Already migrated
|
|
202
218
|
}
|
|
203
219
|
|
|
@@ -51,6 +51,7 @@ function scanPatchesForMaxLamport(graph, patches) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
|
|
54
55
|
/**
|
|
55
56
|
* Materializes the current graph state.
|
|
56
57
|
*
|
|
@@ -99,10 +100,13 @@ export async function materialize(options) {
|
|
|
99
100
|
let state;
|
|
100
101
|
/** @type {import('../types/TickReceipt.js').TickReceipt[]|undefined} */
|
|
101
102
|
let receipts;
|
|
103
|
+
/** @type {import('../types/PatchDiff.js').PatchDiff|undefined} */
|
|
104
|
+
let diff;
|
|
102
105
|
let patchCount = 0;
|
|
106
|
+
const wantDiff = !collectReceipts && !!this._cachedIndexTree;
|
|
103
107
|
|
|
104
108
|
// If checkpoint exists, use incremental materialization
|
|
105
|
-
if (checkpoint?.schema === 2 || checkpoint?.schema === 3) {
|
|
109
|
+
if (checkpoint?.schema === 2 || checkpoint?.schema === 3 || checkpoint?.schema === 4) {
|
|
106
110
|
const patches = await this._loadPatchesSince(checkpoint);
|
|
107
111
|
// Update max observed Lamport so _nextLamport() issues globally-monotonic ticks.
|
|
108
112
|
// Read the checkpoint frontier's tip commit messages to capture the pre-checkpoint max,
|
|
@@ -115,6 +119,10 @@ export async function materialize(options) {
|
|
|
115
119
|
const result = /** @type {{state: import('../services/JoinReducer.js').WarpStateV5, receipts: import('../types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(/** @type {Parameters<typeof reduceV5>[0]} */ (patches), checkpoint.state, { receipts: true }));
|
|
116
120
|
state = result.state;
|
|
117
121
|
receipts = result.receipts;
|
|
122
|
+
} else if (wantDiff) {
|
|
123
|
+
const result = /** @type {{state: import('../services/JoinReducer.js').WarpStateV5, diff: import('../types/PatchDiff.js').PatchDiff}} */ (reduceV5(/** @type {Parameters<typeof reduceV5>[0]} */ (patches), checkpoint.state, { trackDiff: true }));
|
|
124
|
+
state = result.state;
|
|
125
|
+
diff = result.diff;
|
|
118
126
|
} else {
|
|
119
127
|
state = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {Parameters<typeof reduceV5>[0]} */ (patches), checkpoint.state));
|
|
120
128
|
}
|
|
@@ -164,6 +172,10 @@ export async function materialize(options) {
|
|
|
164
172
|
const result = /** @type {{state: import('../services/JoinReducer.js').WarpStateV5, receipts: import('../types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(/** @type {Parameters<typeof reduceV5>[0]} */ (allPatches), undefined, { receipts: true }));
|
|
165
173
|
state = result.state;
|
|
166
174
|
receipts = result.receipts;
|
|
175
|
+
} else if (wantDiff) {
|
|
176
|
+
const result = /** @type {{state: import('../services/JoinReducer.js').WarpStateV5, diff: import('../types/PatchDiff.js').PatchDiff}} */ (reduceV5(/** @type {Parameters<typeof reduceV5>[0]} */ (allPatches), undefined, { trackDiff: true }));
|
|
177
|
+
state = result.state;
|
|
178
|
+
diff = result.diff;
|
|
167
179
|
} else {
|
|
168
180
|
state = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {Parameters<typeof reduceV5>[0]} */ (allPatches)));
|
|
169
181
|
}
|
|
@@ -178,7 +190,7 @@ export async function materialize(options) {
|
|
|
178
190
|
}
|
|
179
191
|
}
|
|
180
192
|
|
|
181
|
-
await this._setMaterializedState(state);
|
|
193
|
+
await this._setMaterializedState(state, diff);
|
|
182
194
|
this._provenanceDegraded = false;
|
|
183
195
|
this._cachedCeiling = null;
|
|
184
196
|
this._cachedFrontier = null;
|
|
@@ -202,9 +214,9 @@ export async function materialize(options) {
|
|
|
202
214
|
// Also handles deferred replay for subscribers added with replay: true before cached state
|
|
203
215
|
if (this._subscribers.length > 0) {
|
|
204
216
|
const hasPendingReplay = this._subscribers.some(s => s.pendingReplay);
|
|
205
|
-
const
|
|
206
|
-
if (!isEmptyDiff(
|
|
207
|
-
this._notifySubscribers(
|
|
217
|
+
const stateDelta = diffStates(this._lastNotifiedState, state);
|
|
218
|
+
if (!isEmptyDiff(stateDelta) || hasPendingReplay) {
|
|
219
|
+
this._notifySubscribers(stateDelta, state);
|
|
208
220
|
}
|
|
209
221
|
}
|
|
210
222
|
// Clone state to prevent eager path mutations from affecting the baseline
|