@git-stunts/git-warp 10.1.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/LICENSE +201 -0
- package/NOTICE +16 -0
- package/README.md +480 -0
- package/SECURITY.md +30 -0
- package/bin/git-warp +24 -0
- package/bin/warp-graph.js +1574 -0
- package/index.d.ts +2366 -0
- package/index.js +180 -0
- package/package.json +129 -0
- package/scripts/install-git-warp.sh +258 -0
- package/scripts/uninstall-git-warp.sh +139 -0
- package/src/domain/WarpGraph.js +3157 -0
- package/src/domain/crdt/Dot.js +160 -0
- package/src/domain/crdt/LWW.js +154 -0
- package/src/domain/crdt/ORSet.js +371 -0
- package/src/domain/crdt/VersionVector.js +222 -0
- package/src/domain/entities/GraphNode.js +60 -0
- package/src/domain/errors/EmptyMessageError.js +47 -0
- package/src/domain/errors/ForkError.js +30 -0
- package/src/domain/errors/IndexError.js +23 -0
- package/src/domain/errors/OperationAbortedError.js +22 -0
- package/src/domain/errors/QueryError.js +39 -0
- package/src/domain/errors/SchemaUnsupportedError.js +17 -0
- package/src/domain/errors/ShardCorruptionError.js +56 -0
- package/src/domain/errors/ShardLoadError.js +57 -0
- package/src/domain/errors/ShardValidationError.js +61 -0
- package/src/domain/errors/StorageError.js +57 -0
- package/src/domain/errors/SyncError.js +30 -0
- package/src/domain/errors/TraversalError.js +23 -0
- package/src/domain/errors/WarpError.js +31 -0
- package/src/domain/errors/WormholeError.js +28 -0
- package/src/domain/errors/WriterError.js +39 -0
- package/src/domain/errors/index.js +21 -0
- package/src/domain/services/AnchorMessageCodec.js +99 -0
- package/src/domain/services/BitmapIndexBuilder.js +225 -0
- package/src/domain/services/BitmapIndexReader.js +435 -0
- package/src/domain/services/BoundaryTransitionRecord.js +463 -0
- package/src/domain/services/CheckpointMessageCodec.js +147 -0
- package/src/domain/services/CheckpointSerializerV5.js +281 -0
- package/src/domain/services/CheckpointService.js +384 -0
- package/src/domain/services/CommitDagTraversalService.js +156 -0
- package/src/domain/services/DagPathFinding.js +712 -0
- package/src/domain/services/DagTopology.js +239 -0
- package/src/domain/services/DagTraversal.js +245 -0
- package/src/domain/services/Frontier.js +108 -0
- package/src/domain/services/GCMetrics.js +101 -0
- package/src/domain/services/GCPolicy.js +122 -0
- package/src/domain/services/GitLogParser.js +205 -0
- package/src/domain/services/HealthCheckService.js +246 -0
- package/src/domain/services/HookInstaller.js +326 -0
- package/src/domain/services/HttpSyncServer.js +262 -0
- package/src/domain/services/IndexRebuildService.js +426 -0
- package/src/domain/services/IndexStalenessChecker.js +103 -0
- package/src/domain/services/JoinReducer.js +582 -0
- package/src/domain/services/KeyCodec.js +113 -0
- package/src/domain/services/LegacyAnchorDetector.js +67 -0
- package/src/domain/services/LogicalTraversal.js +351 -0
- package/src/domain/services/MessageCodecInternal.js +132 -0
- package/src/domain/services/MessageSchemaDetector.js +145 -0
- package/src/domain/services/MigrationService.js +55 -0
- package/src/domain/services/ObserverView.js +265 -0
- package/src/domain/services/PatchBuilderV2.js +669 -0
- package/src/domain/services/PatchMessageCodec.js +140 -0
- package/src/domain/services/ProvenanceIndex.js +337 -0
- package/src/domain/services/ProvenancePayload.js +242 -0
- package/src/domain/services/QueryBuilder.js +835 -0
- package/src/domain/services/StateDiff.js +300 -0
- package/src/domain/services/StateSerializerV5.js +156 -0
- package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
- package/src/domain/services/SyncProtocol.js +593 -0
- package/src/domain/services/TemporalQuery.js +201 -0
- package/src/domain/services/TranslationCost.js +221 -0
- package/src/domain/services/TraversalService.js +8 -0
- package/src/domain/services/WarpMessageCodec.js +29 -0
- package/src/domain/services/WarpStateIndexBuilder.js +127 -0
- package/src/domain/services/WormholeService.js +353 -0
- package/src/domain/types/TickReceipt.js +285 -0
- package/src/domain/types/WarpTypes.js +209 -0
- package/src/domain/types/WarpTypesV2.js +200 -0
- package/src/domain/utils/CachedValue.js +140 -0
- package/src/domain/utils/EventId.js +89 -0
- package/src/domain/utils/LRUCache.js +112 -0
- package/src/domain/utils/MinHeap.js +114 -0
- package/src/domain/utils/RefLayout.js +280 -0
- package/src/domain/utils/WriterId.js +205 -0
- package/src/domain/utils/cancellation.js +33 -0
- package/src/domain/utils/canonicalStringify.js +42 -0
- package/src/domain/utils/defaultClock.js +20 -0
- package/src/domain/utils/defaultCodec.js +51 -0
- package/src/domain/utils/nullLogger.js +21 -0
- package/src/domain/utils/roaring.js +181 -0
- package/src/domain/utils/shardVersion.js +9 -0
- package/src/domain/warp/PatchSession.js +217 -0
- package/src/domain/warp/Writer.js +181 -0
- package/src/hooks/post-merge.sh +60 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
- package/src/infrastructure/adapters/ClockAdapter.js +57 -0
- package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
- package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
- package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
- package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
- package/src/infrastructure/adapters/NoOpLogger.js +62 -0
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
- package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
- package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
- package/src/infrastructure/codecs/CborCodec.js +384 -0
- package/src/ports/BlobPort.js +30 -0
- package/src/ports/ClockPort.js +25 -0
- package/src/ports/CodecPort.js +25 -0
- package/src/ports/CommitPort.js +114 -0
- package/src/ports/ConfigPort.js +31 -0
- package/src/ports/CryptoPort.js +38 -0
- package/src/ports/GraphPersistencePort.js +57 -0
- package/src/ports/HttpServerPort.js +25 -0
- package/src/ports/IndexStoragePort.js +39 -0
- package/src/ports/LoggerPort.js +68 -0
- package/src/ports/RefPort.js +51 -0
- package/src/ports/TreePort.js +51 -0
- package/src/visualization/index.js +26 -0
- package/src/visualization/layouts/converters.js +75 -0
- package/src/visualization/layouts/elkAdapter.js +86 -0
- package/src/visualization/layouts/elkLayout.js +95 -0
- package/src/visualization/layouts/index.js +29 -0
- package/src/visualization/renderers/ascii/box.js +16 -0
- package/src/visualization/renderers/ascii/check.js +271 -0
- package/src/visualization/renderers/ascii/colors.js +13 -0
- package/src/visualization/renderers/ascii/formatters.js +73 -0
- package/src/visualization/renderers/ascii/graph.js +344 -0
- package/src/visualization/renderers/ascii/history.js +335 -0
- package/src/visualization/renderers/ascii/index.js +14 -0
- package/src/visualization/renderers/ascii/info.js +245 -0
- package/src/visualization/renderers/ascii/materialize.js +255 -0
- package/src/visualization/renderers/ascii/path.js +240 -0
- package/src/visualization/renderers/ascii/progress.js +32 -0
- package/src/visualization/renderers/ascii/symbols.js +33 -0
- package/src/visualization/renderers/ascii/table.js +19 -0
- package/src/visualization/renderers/browser/index.js +1 -0
- package/src/visualization/renderers/svg/index.js +159 -0
- package/src/visualization/utils/ansi.js +14 -0
- package/src/visualization/utils/time.js +40 -0
- package/src/visualization/utils/truncate.js +40 -0
- package/src/visualization/utils/unicode.js +52 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { ShardLoadError, ShardCorruptionError, ShardValidationError } from '../errors/index.js';
|
|
2
|
+
import nullLogger from '../utils/nullLogger.js';
|
|
3
|
+
import LRUCache from '../utils/LRUCache.js';
|
|
4
|
+
import { getRoaringBitmap32 } from '../utils/roaring.js';
|
|
5
|
+
import { canonicalStringify } from '../utils/canonicalStringify.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Supported shard format versions for backward compatibility.
|
|
9
|
+
* Version 1: Original format using JSON.stringify for checksums
|
|
10
|
+
* Version 2: Uses canonicalStringify for deterministic checksums
|
|
11
|
+
* @const {number[]}
|
|
12
|
+
*/
|
|
13
|
+
const SUPPORTED_SHARD_VERSIONS = [1, 2];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default maximum number of shards to cache.
|
|
17
|
+
* @const {number}
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_MAX_CACHED_SHARDS = 100;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Computes a SHA-256 checksum of the given data.
|
|
23
|
+
* Used to verify shard integrity on load.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} data - The data object to checksum
|
|
26
|
+
* @param {number} version - Shard version (1 uses JSON.stringify, 2+ uses canonicalStringify)
|
|
27
|
+
* @param {import('../../ports/CryptoPort.js').default} crypto - CryptoPort instance
|
|
28
|
+
* @returns {Promise<string|null>} Hex-encoded SHA-256 hash
|
|
29
|
+
*/
|
|
30
|
+
const computeChecksum = async (data, version, crypto) => {
|
|
31
|
+
if (!crypto) { return null; }
|
|
32
|
+
const json = version === 1 ? JSON.stringify(data) : canonicalStringify(data);
|
|
33
|
+
return await crypto.hash('sha256', json);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Service for querying a loaded bitmap index.
|
|
38
|
+
*
|
|
39
|
+
* This service provides O(1) lookups for parent/child relationships
|
|
40
|
+
* by lazily loading sharded bitmap data from storage. Shards are
|
|
41
|
+
* cached after first access.
|
|
42
|
+
*
|
|
43
|
+
* **Strict Mode**: When `strict: true` is passed to the constructor,
|
|
44
|
+
* the reader will throw errors on any shard validation failure:
|
|
45
|
+
* - {@link ShardCorruptionError} for invalid shard format
|
|
46
|
+
* - {@link ShardValidationError} for version or checksum mismatches
|
|
47
|
+
*
|
|
48
|
+
* In non-strict mode (default), validation failures are logged as warnings
|
|
49
|
+
* and an empty shard is returned for graceful degradation.
|
|
50
|
+
*
|
|
51
|
+
* **Note**: Storage errors (e.g., `storage.readBlob` failures) always throw
|
|
52
|
+
* {@link ShardLoadError} regardless of strict mode.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* // Non-strict mode (default) - graceful degradation on validation errors
|
|
56
|
+
* const reader = new BitmapIndexReader({ storage });
|
|
57
|
+
* reader.setup(shardOids);
|
|
58
|
+
* const parents = await reader.getParents('abc123...');
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Strict mode - throws on any validation failure
|
|
62
|
+
* const strictReader = new BitmapIndexReader({ storage, strict: true });
|
|
63
|
+
* strictReader.setup(shardOids);
|
|
64
|
+
* try {
|
|
65
|
+
* const parents = await strictReader.getParents('abc123...');
|
|
66
|
+
* } catch (err) {
|
|
67
|
+
* if (err instanceof ShardValidationError) {
|
|
68
|
+
* console.error('Shard validation failed:', err.field, err.expected, err.actual);
|
|
69
|
+
* }
|
|
70
|
+
* }
|
|
71
|
+
*
|
|
72
|
+
* @throws {ShardLoadError} When storage.readBlob fails (always, regardless of strict mode)
|
|
73
|
+
* @throws {ShardCorruptionError} When shard format is invalid (strict mode only)
|
|
74
|
+
* @throws {ShardValidationError} When version or checksum validation fails (strict mode only)
|
|
75
|
+
*/
|
|
76
|
+
export default class BitmapIndexReader {
|
|
77
|
+
/**
|
|
78
|
+
* Creates a BitmapIndexReader instance.
|
|
79
|
+
* @param {Object} options
|
|
80
|
+
* @param {import('../../ports/IndexStoragePort.js').default} options.storage - Storage adapter for reading index data
|
|
81
|
+
* @param {boolean} [options.strict=false] - If true, throw errors on validation failures; if false, log warnings and return empty shards
|
|
82
|
+
* @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger for structured logging.
|
|
83
|
+
* Defaults to NoOpLogger (no logging).
|
|
84
|
+
* @param {number} [options.maxCachedShards=100] - Maximum number of shards to keep in the LRU cache.
|
|
85
|
+
* When exceeded, least recently used shards are evicted to free memory.
|
|
86
|
+
* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort instance for checksum verification.
|
|
87
|
+
* When not provided, checksum validation is skipped.
|
|
88
|
+
*/
|
|
89
|
+
constructor({ storage, strict = false, logger = nullLogger, maxCachedShards = DEFAULT_MAX_CACHED_SHARDS, crypto } = {}) {
|
|
90
|
+
if (!storage) {
|
|
91
|
+
throw new Error('BitmapIndexReader requires a storage adapter');
|
|
92
|
+
}
|
|
93
|
+
this.storage = storage;
|
|
94
|
+
this.strict = strict;
|
|
95
|
+
this.logger = logger;
|
|
96
|
+
this.maxCachedShards = maxCachedShards;
|
|
97
|
+
/** @type {import('../../ports/CryptoPort.js').default} */
|
|
98
|
+
this._crypto = crypto;
|
|
99
|
+
this.shardOids = new Map(); // path -> OID
|
|
100
|
+
this.loadedShards = new LRUCache(maxCachedShards); // path -> Data
|
|
101
|
+
this._idToShaCache = null; // Lazy-built reverse mapping
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Configures the reader with shard OID mappings for lazy loading.
|
|
106
|
+
*
|
|
107
|
+
* The shardOids object maps shard filenames to their Git blob OIDs.
|
|
108
|
+
* Shards are organized by type and SHA prefix:
|
|
109
|
+
* - `meta_XX.json` - SHA→ID mappings for nodes with SHA prefix XX
|
|
110
|
+
* - `shards_fwd_XX.json` - Forward edge bitmaps (parent→children)
|
|
111
|
+
* - `shards_rev_XX.json` - Reverse edge bitmaps (child→parents)
|
|
112
|
+
*
|
|
113
|
+
* @param {Record<string, string>} shardOids - Map of shard path to blob OID
|
|
114
|
+
* @returns {void}
|
|
115
|
+
* @example
|
|
116
|
+
* // Typical shardOids structure from IndexRebuildService.load()
|
|
117
|
+
* reader.setup({
|
|
118
|
+
* 'meta_ab.json': 'a1b2c3d4e5f6...',
|
|
119
|
+
* 'meta_cd.json': 'f6e5d4c3b2a1...',
|
|
120
|
+
* 'shards_fwd_ab.json': '1234567890ab...',
|
|
121
|
+
* 'shards_rev_ab.json': 'abcdef123456...',
|
|
122
|
+
* 'shards_fwd_cd.json': '0987654321fe...',
|
|
123
|
+
* 'shards_rev_cd.json': 'fedcba098765...'
|
|
124
|
+
* });
|
|
125
|
+
*
|
|
126
|
+
* // After setup, queries will lazy-load only the shards needed
|
|
127
|
+
* const parents = await reader.getParents('abcd1234...'); // loads meta_ab, shards_rev_ab
|
|
128
|
+
*/
|
|
129
|
+
setup(shardOids) {
|
|
130
|
+
this.shardOids = new Map(Object.entries(shardOids));
|
|
131
|
+
this._idToShaCache = null; // Clear cache when shards change
|
|
132
|
+
this.loadedShards.clear();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Looks up the numeric ID for a SHA.
|
|
137
|
+
* @param {string} sha - The 40-character SHA
|
|
138
|
+
* @returns {Promise<number|undefined>} The numeric ID or undefined
|
|
139
|
+
*/
|
|
140
|
+
async lookupId(sha) {
|
|
141
|
+
const prefix = sha.substring(0, 2);
|
|
142
|
+
const path = `meta_${prefix}.json`;
|
|
143
|
+
const idMap = await this._getOrLoadShard(path, 'json');
|
|
144
|
+
return idMap[sha];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Gets parent SHAs for a node (O(1) via reverse bitmap).
|
|
149
|
+
* @param {string} sha - The node's SHA
|
|
150
|
+
* @returns {Promise<string[]>} Array of parent SHAs
|
|
151
|
+
*/
|
|
152
|
+
async getParents(sha) {
|
|
153
|
+
return await this._getEdges(sha, 'rev');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Gets child SHAs for a node (O(1) via forward bitmap).
|
|
158
|
+
* @param {string} sha - The node's SHA
|
|
159
|
+
* @returns {Promise<string[]>} Array of child SHAs
|
|
160
|
+
*/
|
|
161
|
+
async getChildren(sha) {
|
|
162
|
+
return await this._getEdges(sha, 'fwd');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Internal method to get edges (forward or reverse) for a node.
|
|
167
|
+
* @param {string} sha - The node's SHA
|
|
168
|
+
* @param {string} type - 'fwd' for children, 'rev' for parents
|
|
169
|
+
* @returns {Promise<string[]>} Array of connected SHAs
|
|
170
|
+
* @private
|
|
171
|
+
*/
|
|
172
|
+
async _getEdges(sha, type) {
|
|
173
|
+
const prefix = sha.substring(0, 2);
|
|
174
|
+
const shardPath = `shards_${type}_${prefix}.json`;
|
|
175
|
+
const shard = await this._getOrLoadShard(shardPath, 'json');
|
|
176
|
+
|
|
177
|
+
const encoded = shard[sha];
|
|
178
|
+
if (!encoded) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Decode base64 bitmap and extract IDs
|
|
183
|
+
const buffer = Buffer.from(encoded, 'base64');
|
|
184
|
+
let ids;
|
|
185
|
+
try {
|
|
186
|
+
const RoaringBitmap32 = getRoaringBitmap32();
|
|
187
|
+
const bitmap = RoaringBitmap32.deserialize(buffer, true);
|
|
188
|
+
ids = bitmap.toArray();
|
|
189
|
+
} catch (err) {
|
|
190
|
+
const corruptionError = new ShardCorruptionError('Failed to deserialize bitmap', {
|
|
191
|
+
shardPath,
|
|
192
|
+
oid: this.shardOids.get(shardPath),
|
|
193
|
+
reason: 'bitmap_deserialize_error',
|
|
194
|
+
originalError: err.message,
|
|
195
|
+
});
|
|
196
|
+
this._handleShardError(corruptionError, {
|
|
197
|
+
path: shardPath,
|
|
198
|
+
oid: this.shardOids.get(shardPath),
|
|
199
|
+
format: 'json',
|
|
200
|
+
});
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Convert IDs to SHAs
|
|
205
|
+
const idToSha = await this._buildIdToShaMapping();
|
|
206
|
+
return ids.map(id => idToSha[id]).filter(Boolean);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Builds the ID -> SHA reverse mapping by loading all meta shards.
|
|
211
|
+
* @returns {Promise<string[]>} Array where index is ID and value is SHA
|
|
212
|
+
* @private
|
|
213
|
+
*/
|
|
214
|
+
async _buildIdToShaMapping() {
|
|
215
|
+
if (this._idToShaCache) {
|
|
216
|
+
return this._idToShaCache;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this._idToShaCache = [];
|
|
220
|
+
|
|
221
|
+
for (const [path] of this.shardOids) {
|
|
222
|
+
if (path.startsWith('meta_') && path.endsWith('.json')) {
|
|
223
|
+
const shard = await this._getOrLoadShard(path, 'json');
|
|
224
|
+
for (const [sha, id] of Object.entries(shard)) {
|
|
225
|
+
this._idToShaCache[id] = sha;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const entryCount = this._idToShaCache.length;
|
|
231
|
+
if (entryCount > 1_000_000) {
|
|
232
|
+
this.logger.warn('ID-to-SHA cache has high memory usage', {
|
|
233
|
+
operation: '_buildIdToShaMapping',
|
|
234
|
+
entryCount,
|
|
235
|
+
estimatedMemoryBytes: entryCount * 40,
|
|
236
|
+
message: `Cache contains ${entryCount} entries (~40 bytes per entry). Consider pagination or streaming for very large graphs.`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return this._idToShaCache;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Validates a shard envelope for version and checksum integrity.
|
|
245
|
+
*
|
|
246
|
+
* @param {Object} envelope - The shard envelope to validate
|
|
247
|
+
* @param {string} path - Shard path (for error context)
|
|
248
|
+
* @param {string} oid - Object ID (for error context)
|
|
249
|
+
* @returns {Promise<Object>} The validated data from the envelope
|
|
250
|
+
* @throws {ShardCorruptionError} If envelope format is invalid
|
|
251
|
+
* @throws {ShardValidationError} If version or checksum validation fails
|
|
252
|
+
* @private
|
|
253
|
+
*/
|
|
254
|
+
async _validateShard(envelope, path, oid) {
|
|
255
|
+
if (!envelope || typeof envelope !== 'object') {
|
|
256
|
+
throw new ShardCorruptionError('Invalid shard format', {
|
|
257
|
+
shardPath: path,
|
|
258
|
+
oid,
|
|
259
|
+
reason: 'not_an_object',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
// Validate data field exists and is an object
|
|
263
|
+
if (typeof envelope.data !== 'object' || envelope.data === null || Array.isArray(envelope.data)) {
|
|
264
|
+
throw new ShardCorruptionError('Invalid or missing data field', {
|
|
265
|
+
shardPath: path,
|
|
266
|
+
oid,
|
|
267
|
+
reason: 'missing_or_invalid_data',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (!SUPPORTED_SHARD_VERSIONS.includes(envelope.version)) {
|
|
271
|
+
throw new ShardValidationError('Unsupported version', {
|
|
272
|
+
shardPath: path,
|
|
273
|
+
expected: SUPPORTED_SHARD_VERSIONS,
|
|
274
|
+
actual: envelope.version,
|
|
275
|
+
field: 'version',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
// Use version-appropriate checksum computation for backward compatibility
|
|
279
|
+
const actualChecksum = await computeChecksum(envelope.data, envelope.version, this._crypto);
|
|
280
|
+
if (actualChecksum !== null && envelope.checksum !== actualChecksum) {
|
|
281
|
+
throw new ShardValidationError('Checksum mismatch', {
|
|
282
|
+
shardPath: path,
|
|
283
|
+
expected: envelope.checksum,
|
|
284
|
+
actual: actualChecksum,
|
|
285
|
+
field: 'checksum',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return envelope.data;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Handles validation/corruption errors based on strict mode.
|
|
293
|
+
* @param {ShardCorruptionError|ShardValidationError} err - The error to handle
|
|
294
|
+
* @param {Object} context - Error context
|
|
295
|
+
* @param {string} context.path - Shard path
|
|
296
|
+
* @param {string} context.oid - Object ID
|
|
297
|
+
* @param {string} context.format - 'json' or 'bitmap'
|
|
298
|
+
* @returns {Object|RoaringBitmap32} Empty shard (non-strict mode only)
|
|
299
|
+
* @throws {ShardCorruptionError|ShardValidationError} In strict mode
|
|
300
|
+
* @private
|
|
301
|
+
*/
|
|
302
|
+
_handleShardError(err, { path, oid, format }) {
|
|
303
|
+
if (this.strict) {
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
this.logger.warn('Shard validation warning', {
|
|
307
|
+
operation: 'loadShard',
|
|
308
|
+
shardPath: path,
|
|
309
|
+
oid,
|
|
310
|
+
error: err.message,
|
|
311
|
+
code: err.code,
|
|
312
|
+
field: err.field,
|
|
313
|
+
expected: err.expected,
|
|
314
|
+
actual: err.actual,
|
|
315
|
+
});
|
|
316
|
+
const emptyShard = format === 'json' ? {} : new (getRoaringBitmap32())();
|
|
317
|
+
this.loadedShards.set(path, emptyShard);
|
|
318
|
+
return emptyShard;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Parses and validates a shard buffer.
|
|
323
|
+
* @param {Buffer} buffer - Raw shard buffer
|
|
324
|
+
* @param {string} path - Shard path (for error context)
|
|
325
|
+
* @param {string} oid - Object ID (for error context)
|
|
326
|
+
* @returns {Promise<Object>} The validated data from the shard
|
|
327
|
+
* @throws {ShardCorruptionError} If parsing fails or format is invalid
|
|
328
|
+
* @throws {ShardValidationError} If version or checksum validation fails
|
|
329
|
+
* @private
|
|
330
|
+
*/
|
|
331
|
+
async _parseAndValidateShard(buffer, path, oid) {
|
|
332
|
+
const envelope = JSON.parse(new TextDecoder().decode(buffer));
|
|
333
|
+
return await this._validateShard(envelope, path, oid);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Loads raw buffer from storage.
|
|
338
|
+
* @param {string} path - Shard path
|
|
339
|
+
* @param {string} oid - Object ID
|
|
340
|
+
* @returns {Promise<Buffer>} Raw buffer
|
|
341
|
+
* @throws {ShardLoadError} When storage.readBlob fails
|
|
342
|
+
* @private
|
|
343
|
+
*/
|
|
344
|
+
async _loadShardBuffer(path, oid) {
|
|
345
|
+
try {
|
|
346
|
+
return await this.storage.readBlob(oid);
|
|
347
|
+
} catch (cause) {
|
|
348
|
+
throw new ShardLoadError('Failed to load shard from storage', {
|
|
349
|
+
shardPath: path,
|
|
350
|
+
oid,
|
|
351
|
+
cause,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Wraps an error as a ShardCorruptionError if it's a SyntaxError.
|
|
358
|
+
* Returns the original error otherwise.
|
|
359
|
+
* @param {Error} err - The error to potentially wrap
|
|
360
|
+
* @param {string} path - Shard path
|
|
361
|
+
* @param {string} oid - Object ID
|
|
362
|
+
* @returns {Error} The wrapped or original error
|
|
363
|
+
* @private
|
|
364
|
+
*/
|
|
365
|
+
_wrapParseError(err, path, oid) {
|
|
366
|
+
if (err instanceof SyntaxError) {
|
|
367
|
+
return new ShardCorruptionError('Failed to parse shard JSON', {
|
|
368
|
+
shardPath: path,
|
|
369
|
+
oid,
|
|
370
|
+
reason: 'parse_error',
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return err;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Attempts to handle a shard error based on its type.
|
|
378
|
+
* Returns handled result for validation/corruption errors, null otherwise.
|
|
379
|
+
* @param {Error} err - The error to handle
|
|
380
|
+
* @param {Object} context - Error context
|
|
381
|
+
* @param {string} context.path - Shard path
|
|
382
|
+
* @param {string} context.oid - Object ID
|
|
383
|
+
* @param {string} context.format - 'json' or 'bitmap'
|
|
384
|
+
* @returns {Object|RoaringBitmap32|null} Handled result or null if error should be re-thrown
|
|
385
|
+
* @private
|
|
386
|
+
*/
|
|
387
|
+
_tryHandleShardError(err, context) {
|
|
388
|
+
const wrappedErr = this._wrapParseError(err, context.path, context.oid);
|
|
389
|
+
const isHandleable = wrappedErr instanceof ShardCorruptionError ||
|
|
390
|
+
wrappedErr instanceof ShardValidationError;
|
|
391
|
+
return isHandleable ? this._handleShardError(wrappedErr, context) : null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Loads a shard with validation and configurable error handling.
|
|
396
|
+
*
|
|
397
|
+
* In strict mode, throws on any validation failure.
|
|
398
|
+
* In non-strict mode, logs warnings and returns empty shards on validation failures.
|
|
399
|
+
* Storage errors always throw ShardLoadError regardless of mode.
|
|
400
|
+
*
|
|
401
|
+
* @param {string} path - Shard path
|
|
402
|
+
* @param {string} format - 'json' or 'bitmap'
|
|
403
|
+
* @returns {Promise<Object|RoaringBitmap32>}
|
|
404
|
+
* @throws {ShardLoadError} When storage.readBlob fails
|
|
405
|
+
* @throws {ShardCorruptionError} When shard format is invalid (strict mode only)
|
|
406
|
+
* @throws {ShardValidationError} When version or checksum validation fails (strict mode only)
|
|
407
|
+
* @private
|
|
408
|
+
*/
|
|
409
|
+
async _getOrLoadShard(path, format) {
|
|
410
|
+
if (this.loadedShards.has(path)) {
|
|
411
|
+
return this.loadedShards.get(path);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const oid = this.shardOids.get(path);
|
|
415
|
+
const emptyShard = format === 'json' ? {} : new (getRoaringBitmap32())();
|
|
416
|
+
if (!oid) {
|
|
417
|
+
return emptyShard;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const buffer = await this._loadShardBuffer(path, oid);
|
|
421
|
+
const context = { path, oid, format };
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
const data = await this._parseAndValidateShard(buffer, path, oid);
|
|
425
|
+
this.loadedShards.set(path, data);
|
|
426
|
+
return data;
|
|
427
|
+
} catch (err) {
|
|
428
|
+
const handled = this._tryHandleShardError(err, context);
|
|
429
|
+
if (handled !== null) {
|
|
430
|
+
return handled;
|
|
431
|
+
}
|
|
432
|
+
throw err;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|