@git-stunts/git-warp 11.5.1 → 12.0.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 +142 -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 +49 -12
- package/package.json +2 -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 +138 -47
- 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/TemporalQuery.js +128 -14
- 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/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 +78 -12
- 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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NeighborProvider backed by in-memory adjacency maps.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the { outgoing, incoming } Maps produced by _buildAdjacency().
|
|
5
|
+
* Adjacency lists are pre-sorted at construction by (neighborId, label)
|
|
6
|
+
* using strict codepoint comparison. Label filtering via Set.has() in-memory.
|
|
7
|
+
*
|
|
8
|
+
* @module domain/services/AdjacencyNeighborProvider
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import NeighborProviderPort from '../../ports/NeighborProviderPort.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Comparator for (neighborId, label) sorting.
|
|
15
|
+
* Strict codepoint comparison — never localeCompare.
|
|
16
|
+
*
|
|
17
|
+
* @param {{ neighborId: string, label: string }} a
|
|
18
|
+
* @param {{ neighborId: string, label: string }} b
|
|
19
|
+
* @returns {number}
|
|
20
|
+
*/
|
|
21
|
+
function edgeCmp(a, b) {
|
|
22
|
+
if (a.neighborId < b.neighborId) { return -1; }
|
|
23
|
+
if (a.neighborId > b.neighborId) { return 1; }
|
|
24
|
+
if (a.label < b.label) { return -1; }
|
|
25
|
+
if (a.label > b.label) { return 1; }
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pre-sorts all adjacency lists and freezes the result.
|
|
31
|
+
*
|
|
32
|
+
* @param {Map<string, Array<{neighborId: string, label: string}>>} adjMap
|
|
33
|
+
* @returns {Map<string, Array<{neighborId: string, label: string}>>}
|
|
34
|
+
*/
|
|
35
|
+
function sortAdjacencyMap(adjMap) {
|
|
36
|
+
const result = new Map();
|
|
37
|
+
for (const [nodeId, edges] of adjMap) {
|
|
38
|
+
const sorted = edges.slice().sort(edgeCmp);
|
|
39
|
+
result.set(nodeId, sorted);
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Filters an edge list by a label set. Returns the original array when
|
|
46
|
+
* no filter is provided.
|
|
47
|
+
*
|
|
48
|
+
* @param {Array<{neighborId: string, label: string}>} edges
|
|
49
|
+
* @param {Set<string>|undefined} labels
|
|
50
|
+
* @returns {Array<{neighborId: string, label: string}>}
|
|
51
|
+
*/
|
|
52
|
+
function filterByLabels(edges, labels) {
|
|
53
|
+
if (!labels) {
|
|
54
|
+
return edges;
|
|
55
|
+
}
|
|
56
|
+
return edges.filter((e) => labels.has(e.label));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Merges two pre-sorted edge lists, deduplicating by (neighborId, label).
|
|
61
|
+
*
|
|
62
|
+
* @param {Array<{neighborId: string, label: string}>} a
|
|
63
|
+
* @param {Array<{neighborId: string, label: string}>} b
|
|
64
|
+
* @returns {Array<{neighborId: string, label: string}>}
|
|
65
|
+
*/
|
|
66
|
+
function mergeSorted(a, b) {
|
|
67
|
+
const result = [];
|
|
68
|
+
let i = 0;
|
|
69
|
+
let j = 0;
|
|
70
|
+
while (i < a.length && j < b.length) {
|
|
71
|
+
const cmp = edgeCmp(a[i], b[j]);
|
|
72
|
+
if (cmp < 0) {
|
|
73
|
+
result.push(a[i++]);
|
|
74
|
+
} else if (cmp > 0) {
|
|
75
|
+
result.push(b[j++]);
|
|
76
|
+
} else {
|
|
77
|
+
// Duplicate — take one, skip both
|
|
78
|
+
result.push(a[i++]);
|
|
79
|
+
j++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
while (i < a.length) { result.push(a[i++]); }
|
|
83
|
+
while (j < b.length) { result.push(b[j++]); }
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default class AdjacencyNeighborProvider extends NeighborProviderPort {
|
|
88
|
+
/**
|
|
89
|
+
* @param {Object} params
|
|
90
|
+
* @param {Map<string, Array<{neighborId: string, label: string}>>} params.outgoing
|
|
91
|
+
* @param {Map<string, Array<{neighborId: string, label: string}>>} params.incoming
|
|
92
|
+
* @param {Set<string>} params.aliveNodes - Set of alive nodeIds for hasNode()
|
|
93
|
+
*/
|
|
94
|
+
constructor({ outgoing, incoming, aliveNodes }) {
|
|
95
|
+
super();
|
|
96
|
+
if (!aliveNodes) {
|
|
97
|
+
throw new Error('AdjacencyNeighborProvider: aliveNodes is required');
|
|
98
|
+
}
|
|
99
|
+
/** @type {Map<string, Array<{neighborId: string, label: string}>>} */
|
|
100
|
+
this._outgoing = sortAdjacencyMap(outgoing);
|
|
101
|
+
/** @type {Map<string, Array<{neighborId: string, label: string}>>} */
|
|
102
|
+
this._incoming = sortAdjacencyMap(incoming);
|
|
103
|
+
/** @type {Set<string>} */
|
|
104
|
+
this._aliveNodes = aliveNodes;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {string} nodeId
|
|
109
|
+
* @param {import('../../ports/NeighborProviderPort.js').Direction} direction
|
|
110
|
+
* @param {import('../../ports/NeighborProviderPort.js').NeighborOptions} [options]
|
|
111
|
+
* @returns {Promise<import('../../ports/NeighborProviderPort.js').NeighborEdge[]>}
|
|
112
|
+
*/
|
|
113
|
+
getNeighbors(nodeId, direction, options) {
|
|
114
|
+
const labels = options?.labels;
|
|
115
|
+
const outEdges = filterByLabels(this._outgoing.get(nodeId) || [], labels);
|
|
116
|
+
const inEdges = filterByLabels(this._incoming.get(nodeId) || [], labels);
|
|
117
|
+
|
|
118
|
+
if (direction === 'out') {
|
|
119
|
+
return Promise.resolve(outEdges);
|
|
120
|
+
}
|
|
121
|
+
if (direction === 'in') {
|
|
122
|
+
return Promise.resolve(inEdges);
|
|
123
|
+
}
|
|
124
|
+
// 'both': merge two pre-sorted lists, dedup by (neighborId, label)
|
|
125
|
+
return Promise.resolve(mergeSorted(outEdges, inEdges));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {string} nodeId
|
|
130
|
+
* @returns {Promise<boolean>}
|
|
131
|
+
*/
|
|
132
|
+
hasNode(nodeId) {
|
|
133
|
+
return Promise.resolve(this._aliveNodes.has(nodeId));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @returns {'sync'} */
|
|
137
|
+
get latencyClass() {
|
|
138
|
+
return 'sync';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NeighborProvider backed by bitmap indexes.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* 1. **Commit DAG** (`indexReader`): Wraps BitmapIndexReader for parent/child
|
|
6
|
+
* relationships. Edges use label = '' (empty string sentinel).
|
|
7
|
+
* 2. **Logical graph** (`logicalIndex`): Wraps CBOR-based logical bitmap index
|
|
8
|
+
* with labeled edges, per-label bitmap filtering, and alive bitmap checks.
|
|
9
|
+
*
|
|
10
|
+
* @module domain/services/BitmapNeighborProvider
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import NeighborProviderPort from '../../ports/NeighborProviderPort.js';
|
|
14
|
+
|
|
15
|
+
/** @typedef {import('./BitmapIndexReader.js').default} BitmapIndexReader */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} LogicalIndex
|
|
19
|
+
* @property {(nodeId: string) => number|undefined} getGlobalId
|
|
20
|
+
* @property {(nodeId: string) => boolean} isAlive
|
|
21
|
+
* @property {(globalId: number) => string|undefined} getNodeId
|
|
22
|
+
* @property {(nodeId: string, direction: string, labelIds?: number[]) => Array<{neighborId: string, label: string}>} getEdges
|
|
23
|
+
* @property {() => Map<string, number>} getLabelRegistry
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sorts edges by (neighborId, label) using strict codepoint comparison.
|
|
28
|
+
*
|
|
29
|
+
* @param {Array<{neighborId: string, label: string}>} edges
|
|
30
|
+
* @returns {Array<{neighborId: string, label: string}>}
|
|
31
|
+
*/
|
|
32
|
+
function sortEdges(edges) {
|
|
33
|
+
return edges.sort((a, b) => {
|
|
34
|
+
if (a.neighborId < b.neighborId) { return -1; }
|
|
35
|
+
if (a.neighborId > b.neighborId) { return 1; }
|
|
36
|
+
if (a.label < b.label) { return -1; }
|
|
37
|
+
if (a.label > b.label) { return 1; }
|
|
38
|
+
return 0;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Deduplicates a sorted edge list by (neighborId, label).
|
|
44
|
+
*
|
|
45
|
+
* @param {Array<{neighborId: string, label: string}>} edges
|
|
46
|
+
* @returns {Array<{neighborId: string, label: string}>}
|
|
47
|
+
*/
|
|
48
|
+
function dedupSorted(edges) {
|
|
49
|
+
if (edges.length <= 1) { return edges; }
|
|
50
|
+
const result = [edges[0]];
|
|
51
|
+
for (let i = 1; i < edges.length; i++) {
|
|
52
|
+
const prev = result[result.length - 1];
|
|
53
|
+
if (edges[i].neighborId !== prev.neighborId || edges[i].label !== prev.label) {
|
|
54
|
+
result.push(edges[i]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default class BitmapNeighborProvider extends NeighborProviderPort {
|
|
61
|
+
/**
|
|
62
|
+
* @param {Object} params
|
|
63
|
+
* @param {BitmapIndexReader} [params.indexReader] - For commit DAG mode
|
|
64
|
+
* @param {LogicalIndex} [params.logicalIndex] - For logical graph mode
|
|
65
|
+
*/
|
|
66
|
+
constructor({ indexReader, logicalIndex }) {
|
|
67
|
+
super();
|
|
68
|
+
if (!indexReader && !logicalIndex) {
|
|
69
|
+
throw new Error('BitmapNeighborProvider requires either indexReader or logicalIndex');
|
|
70
|
+
}
|
|
71
|
+
this._reader = indexReader ?? null;
|
|
72
|
+
this._logical = logicalIndex ?? null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} nodeId
|
|
77
|
+
* @param {import('../../ports/NeighborProviderPort.js').Direction} direction
|
|
78
|
+
* @param {import('../../ports/NeighborProviderPort.js').NeighborOptions} [options]
|
|
79
|
+
* @returns {Promise<import('../../ports/NeighborProviderPort.js').NeighborEdge[]>}
|
|
80
|
+
*/
|
|
81
|
+
async getNeighbors(nodeId, direction, options) {
|
|
82
|
+
if (this._logical) {
|
|
83
|
+
return this._getLogicalNeighbors(nodeId, direction, options);
|
|
84
|
+
}
|
|
85
|
+
return await this._getDagNeighbors(nodeId, direction, options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} nodeId
|
|
90
|
+
* @returns {Promise<boolean>}
|
|
91
|
+
*/
|
|
92
|
+
async hasNode(nodeId) {
|
|
93
|
+
if (this._logical) {
|
|
94
|
+
return this._logical.isAlive(nodeId);
|
|
95
|
+
}
|
|
96
|
+
if (this._reader) {
|
|
97
|
+
const id = await this._reader.lookupId(nodeId);
|
|
98
|
+
return id !== undefined;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @returns {'async-local'} */
|
|
104
|
+
get latencyClass() {
|
|
105
|
+
return 'async-local';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Commit DAG mode ─────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} nodeId
|
|
112
|
+
* @param {import('../../ports/NeighborProviderPort.js').Direction} direction
|
|
113
|
+
* @param {import('../../ports/NeighborProviderPort.js').NeighborOptions} [options]
|
|
114
|
+
* @returns {Promise<import('../../ports/NeighborProviderPort.js').NeighborEdge[]>}
|
|
115
|
+
* @private
|
|
116
|
+
*/
|
|
117
|
+
async _getDagNeighbors(nodeId, direction, options) {
|
|
118
|
+
if (!this._reader) { return []; }
|
|
119
|
+
|
|
120
|
+
if (options?.labels) {
|
|
121
|
+
if (!options.labels.has('')) { return []; }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (direction === 'out') {
|
|
125
|
+
const children = await this._reader.getChildren(nodeId);
|
|
126
|
+
return sortEdges(children.map((id) => ({ neighborId: id, label: '' })));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (direction === 'in') {
|
|
130
|
+
const parents = await this._reader.getParents(nodeId);
|
|
131
|
+
return sortEdges(parents.map((id) => ({ neighborId: id, label: '' })));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const [children, parents] = await Promise.all([
|
|
135
|
+
this._reader.getChildren(nodeId),
|
|
136
|
+
this._reader.getParents(nodeId),
|
|
137
|
+
]);
|
|
138
|
+
const all = children.map((id) => ({ neighborId: id, label: '' }))
|
|
139
|
+
.concat(parents.map((id) => ({ neighborId: id, label: '' })));
|
|
140
|
+
return dedupSorted(sortEdges(all));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Logical graph mode ──────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {string} nodeId
|
|
147
|
+
* @param {import('../../ports/NeighborProviderPort.js').Direction} direction
|
|
148
|
+
* @param {import('../../ports/NeighborProviderPort.js').NeighborOptions} [options]
|
|
149
|
+
* @returns {import('../../ports/NeighborProviderPort.js').NeighborEdge[]}
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
152
|
+
_getLogicalNeighbors(nodeId, direction, options) {
|
|
153
|
+
const logical = /** @type {LogicalIndex} */ (this._logical);
|
|
154
|
+
|
|
155
|
+
// Resolve label filter to labelIds
|
|
156
|
+
/** @type {number[]|undefined} */
|
|
157
|
+
let labelIds;
|
|
158
|
+
if (options?.labels) {
|
|
159
|
+
const registry = logical.getLabelRegistry();
|
|
160
|
+
labelIds = [];
|
|
161
|
+
for (const label of options.labels) {
|
|
162
|
+
const id = registry.get(label);
|
|
163
|
+
if (id !== undefined) {
|
|
164
|
+
labelIds.push(id);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (labelIds.length === 0) { return []; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (direction === 'both') {
|
|
171
|
+
const outEdges = logical.getEdges(nodeId, 'out', labelIds);
|
|
172
|
+
const inEdges = logical.getEdges(nodeId, 'in', labelIds);
|
|
173
|
+
return dedupSorted(sortEdges([...outEdges, ...inEdges]));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return sortEdges(logical.getEdges(nodeId, direction, labelIds));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -60,8 +60,8 @@ export function encodeCheckpointMessage({ graph, stateHash, frontierOid, indexOi
|
|
|
60
60
|
[TRAILER_KEYS.schema]: String(schema),
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
// Add checkpoint version marker for V5 format (schema:2
|
|
64
|
-
if (schema === 2 || schema === 3) {
|
|
63
|
+
// Add checkpoint version marker for V5 format (schema:2, schema:3, schema:4)
|
|
64
|
+
if (schema === 2 || schema === 3 || schema === 4) {
|
|
65
65
|
trailers[TRAILER_KEYS.checkpointVersion] = 'v5';
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -126,7 +126,7 @@ export function decodeCheckpointMessage(message) {
|
|
|
126
126
|
throw new Error(`Invalid checkpoint message: eg-schema must be a positive integer, got '${schemaStr}'`);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
// Extract optional checkpoint version (v5 for schema:2)
|
|
129
|
+
// Extract optional checkpoint version (v5 for schema:2/3/4)
|
|
130
130
|
const checkpointVersion = trailers[TRAILER_KEYS.checkpointVersion] || null;
|
|
131
131
|
|
|
132
132
|
return {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Checkpoint Service for WARP multi-writer graph database.
|
|
3
3
|
*
|
|
4
|
-
* Provides functionality for creating and loading schema:2 and schema:
|
|
4
|
+
* Provides functionality for creating and loading schema:2, schema:3, and schema:4
|
|
5
5
|
* checkpoints, as well as incremental state materialization from checkpoints.
|
|
6
6
|
*
|
|
7
|
-
* This service supports schema:2 and schema:
|
|
7
|
+
* This service supports schema:2, schema:3, and schema:4 (V5) checkpoints. Schema:1 (V4)
|
|
8
8
|
* checkpoints must be migrated before use.
|
|
9
9
|
*
|
|
10
10
|
* @module CheckpointService
|
|
@@ -28,6 +28,53 @@ import { cloneStateV5, reduceV5 } from './JoinReducer.js';
|
|
|
28
28
|
import { encodeEdgeKey, encodePropKey, CONTENT_PROPERTY_KEY, decodePropKey, isEdgePropKey, decodeEdgePropKey } from './KeyCodec.js';
|
|
29
29
|
import { ProvenanceIndex } from './ProvenanceIndex.js';
|
|
30
30
|
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Internal Helpers
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Writes index tree shards as blobs and creates a subtree.
|
|
37
|
+
*
|
|
38
|
+
* @param {Record<string, Uint8Array>} indexTree - path → buffer mapping
|
|
39
|
+
* @param {{ writeBlob(buf: Uint8Array): Promise<string>, writeTree(entries: string[]): Promise<string> }} persistence
|
|
40
|
+
* @returns {Promise<string>} subtree OID
|
|
41
|
+
*/
|
|
42
|
+
async function writeIndexSubtree(indexTree, persistence) {
|
|
43
|
+
const paths = Object.keys(indexTree).sort();
|
|
44
|
+
const oids = await Promise.all(
|
|
45
|
+
paths.map((p) => persistence.writeBlob(indexTree[p]))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const entries = paths.map(
|
|
49
|
+
(path, i) => `100644 blob ${oids[i]}\t${path}`
|
|
50
|
+
);
|
|
51
|
+
return await persistence.writeTree(entries);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Partitions readTreeOids output into core entries and index shard OIDs.
|
|
56
|
+
*
|
|
57
|
+
* Entries prefixed with `index/` are stripped and collected separately.
|
|
58
|
+
*
|
|
59
|
+
* @param {Record<string, string>} rawOids
|
|
60
|
+
* @returns {{ treeOids: Record<string, string>, indexShardOids: Record<string, string> }}
|
|
61
|
+
*/
|
|
62
|
+
function partitionTreeOids(rawOids) {
|
|
63
|
+
/** @type {Record<string, string>} */
|
|
64
|
+
const treeOids = {};
|
|
65
|
+
/** @type {Record<string, string>} */
|
|
66
|
+
const indexShardOids = {};
|
|
67
|
+
|
|
68
|
+
for (const [path, oid] of Object.entries(rawOids)) {
|
|
69
|
+
if (path.startsWith('index/')) {
|
|
70
|
+
indexShardOids[path.slice(6)] = oid;
|
|
71
|
+
} else {
|
|
72
|
+
treeOids[path] = oid;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { treeOids, indexShardOids };
|
|
76
|
+
}
|
|
77
|
+
|
|
31
78
|
// ============================================================================
|
|
32
79
|
// Checkpoint Creation (WARP spec Section 10)
|
|
33
80
|
// ============================================================================
|
|
@@ -55,10 +102,11 @@ import { ProvenanceIndex } from './ProvenanceIndex.js';
|
|
|
55
102
|
* @param {import('./ProvenanceIndex.js').ProvenanceIndex} [options.provenanceIndex] - Optional provenance index to persist
|
|
56
103
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for CBOR serialization
|
|
57
104
|
* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort for state hash computation
|
|
105
|
+
* @param {Record<string, Uint8Array>} [options.indexTree] - Optional materialized view index tree (triggers schema 4)
|
|
58
106
|
* @returns {Promise<string>} The checkpoint commit SHA
|
|
59
107
|
*/
|
|
60
|
-
export async function create({ persistence, graphName, state, frontier, parents = [], compact = true, provenanceIndex, codec, crypto }) {
|
|
61
|
-
return await createV5({ persistence, graphName, state, frontier, parents, compact, provenanceIndex, codec, crypto });
|
|
108
|
+
export async function create({ persistence, graphName, state, frontier, parents = [], compact = true, provenanceIndex, codec, crypto, indexTree }) {
|
|
109
|
+
return await createV5({ persistence, graphName, state, frontier, parents, compact, provenanceIndex, codec, crypto, indexTree });
|
|
62
110
|
}
|
|
63
111
|
|
|
64
112
|
/**
|
|
@@ -84,6 +132,7 @@ export async function create({ persistence, graphName, state, frontier, parents
|
|
|
84
132
|
* @param {import('./ProvenanceIndex.js').ProvenanceIndex} [options.provenanceIndex] - Optional provenance index to persist
|
|
85
133
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for CBOR serialization
|
|
86
134
|
* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort for state hash computation
|
|
135
|
+
* @param {Record<string, Uint8Array>} [options.indexTree] - Optional materialized view index tree (triggers schema 4)
|
|
87
136
|
* @returns {Promise<string>} The checkpoint commit SHA
|
|
88
137
|
*/
|
|
89
138
|
export async function createV5({
|
|
@@ -96,6 +145,7 @@ export async function createV5({
|
|
|
96
145
|
provenanceIndex,
|
|
97
146
|
codec,
|
|
98
147
|
crypto,
|
|
148
|
+
indexTree,
|
|
99
149
|
}) {
|
|
100
150
|
// 1. Compute appliedVV from actual state dots
|
|
101
151
|
const appliedVV = computeAppliedVV(state);
|
|
@@ -132,7 +182,13 @@ export async function createV5({
|
|
|
132
182
|
provenanceIndexBlobOid = await persistence.writeBlob(/** @type {Buffer} */ (provenanceIndexBuffer));
|
|
133
183
|
}
|
|
134
184
|
|
|
135
|
-
// 6c.
|
|
185
|
+
// 6c. Optionally write index subtree (schema 4)
|
|
186
|
+
let indexSubtreeOid = null;
|
|
187
|
+
if (indexTree) {
|
|
188
|
+
indexSubtreeOid = await writeIndexSubtree(indexTree, persistence);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 6d. Collect content blob OIDs from state properties for GC anchoring.
|
|
136
192
|
// If patch commits are ever pruned, content blobs remain reachable via
|
|
137
193
|
// the checkpoint tree. Without this, git gc would nuke content blobs
|
|
138
194
|
// whose only anchor was the (now-pruned) patch commit tree.
|
|
@@ -159,6 +215,11 @@ export async function createV5({
|
|
|
159
215
|
treeEntries.push(`100644 blob ${provenanceIndexBlobOid}\tprovenanceIndex.cbor`);
|
|
160
216
|
}
|
|
161
217
|
|
|
218
|
+
// Add index subtree if present (schema 4)
|
|
219
|
+
if (indexSubtreeOid) {
|
|
220
|
+
treeEntries.push(`040000 tree ${indexSubtreeOid}\tindex`);
|
|
221
|
+
}
|
|
222
|
+
|
|
162
223
|
// Add content blob anchors
|
|
163
224
|
for (const oid of contentOids) {
|
|
164
225
|
treeEntries.push(`100644 blob ${oid}\t_content_${oid}`);
|
|
@@ -168,7 +229,7 @@ export async function createV5({
|
|
|
168
229
|
treeEntries.sort((a, b) => {
|
|
169
230
|
const filenameA = a.split('\t')[1];
|
|
170
231
|
const filenameB = b.split('\t')[1];
|
|
171
|
-
return filenameA
|
|
232
|
+
return filenameA < filenameB ? -1 : filenameA > filenameB ? 1 : 0;
|
|
172
233
|
});
|
|
173
234
|
|
|
174
235
|
const treeOid = await persistence.writeTree(treeEntries);
|
|
@@ -179,7 +240,7 @@ export async function createV5({
|
|
|
179
240
|
stateHash,
|
|
180
241
|
frontierOid: frontierBlobOid,
|
|
181
242
|
indexOid: treeOid,
|
|
182
|
-
schema: 2,
|
|
243
|
+
schema: indexTree ? 4 : 2,
|
|
183
244
|
});
|
|
184
245
|
|
|
185
246
|
// 9. Create the checkpoint commit
|
|
@@ -212,7 +273,7 @@ export async function createV5({
|
|
|
212
273
|
* @param {string} checkpointSha - The checkpoint commit SHA to load
|
|
213
274
|
* @param {Object} [options] - Load options
|
|
214
275
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for CBOR deserialization
|
|
215
|
-
* @returns {Promise<{state: import('./JoinReducer.js').WarpStateV5, frontier: import('./Frontier.js').Frontier, stateHash: string, schema: number, appliedVV: Map<string, number>|null, provenanceIndex?: import('./ProvenanceIndex.js').ProvenanceIndex}>} The loaded checkpoint data
|
|
276
|
+
* @returns {Promise<{state: import('./JoinReducer.js').WarpStateV5, frontier: import('./Frontier.js').Frontier, stateHash: string, schema: number, appliedVV: Map<string, number>|null, provenanceIndex?: import('./ProvenanceIndex.js').ProvenanceIndex, indexShardOids: Record<string, string>|null}>} The loaded checkpoint data
|
|
216
277
|
* @throws {Error} If checkpoint is schema:1 (migration required)
|
|
217
278
|
*/
|
|
218
279
|
export async function loadCheckpoint(persistence, checkpointSha, { codec } = {}) {
|
|
@@ -220,16 +281,19 @@ export async function loadCheckpoint(persistence, checkpointSha, { codec } = {})
|
|
|
220
281
|
const message = await persistence.showNode(checkpointSha);
|
|
221
282
|
const decoded = /** @type {{ schema: number, stateHash: string, indexOid: string }} */ (decodeCheckpointMessage(message));
|
|
222
283
|
|
|
223
|
-
// 2. Reject
|
|
224
|
-
if (decoded.schema !== 2 && decoded.schema !== 3) {
|
|
284
|
+
// 2. Reject unsupported schemas - migration required for schema:1
|
|
285
|
+
if (decoded.schema !== 2 && decoded.schema !== 3 && decoded.schema !== 4) {
|
|
225
286
|
throw new Error(
|
|
226
287
|
`Checkpoint ${checkpointSha} is schema:${decoded.schema}. ` +
|
|
227
|
-
`Only schema:2 and schema:
|
|
288
|
+
`Only schema:2, schema:3, and schema:4 checkpoints are supported. Please migrate using MigrationService.`
|
|
228
289
|
);
|
|
229
290
|
}
|
|
230
291
|
|
|
231
292
|
// 3. Read tree entries via the indexOid from the message (points to the tree)
|
|
232
|
-
const
|
|
293
|
+
const rawTreeOids = await persistence.readTreeOids(decoded.indexOid);
|
|
294
|
+
|
|
295
|
+
// 3b. Partition: entries with 'index/' prefix are bitmap index shards
|
|
296
|
+
const { treeOids, indexShardOids } = partitionTreeOids(rawTreeOids);
|
|
233
297
|
|
|
234
298
|
// 4. Read frontier.cbor blob
|
|
235
299
|
const frontierOid = treeOids['frontier.cbor'];
|
|
@@ -272,6 +336,7 @@ export async function loadCheckpoint(persistence, checkpointSha, { codec } = {})
|
|
|
272
336
|
schema: decoded.schema,
|
|
273
337
|
appliedVV,
|
|
274
338
|
provenanceIndex: provenanceIndex || undefined,
|
|
339
|
+
indexShardOids: Object.keys(indexShardOids).length > 0 ? indexShardOids : null,
|
|
275
340
|
};
|
|
276
341
|
}
|
|
277
342
|
|