@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.
Files changed (143) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +16 -0
  3. package/README.md +480 -0
  4. package/SECURITY.md +30 -0
  5. package/bin/git-warp +24 -0
  6. package/bin/warp-graph.js +1574 -0
  7. package/index.d.ts +2366 -0
  8. package/index.js +180 -0
  9. package/package.json +129 -0
  10. package/scripts/install-git-warp.sh +258 -0
  11. package/scripts/uninstall-git-warp.sh +139 -0
  12. package/src/domain/WarpGraph.js +3157 -0
  13. package/src/domain/crdt/Dot.js +160 -0
  14. package/src/domain/crdt/LWW.js +154 -0
  15. package/src/domain/crdt/ORSet.js +371 -0
  16. package/src/domain/crdt/VersionVector.js +222 -0
  17. package/src/domain/entities/GraphNode.js +60 -0
  18. package/src/domain/errors/EmptyMessageError.js +47 -0
  19. package/src/domain/errors/ForkError.js +30 -0
  20. package/src/domain/errors/IndexError.js +23 -0
  21. package/src/domain/errors/OperationAbortedError.js +22 -0
  22. package/src/domain/errors/QueryError.js +39 -0
  23. package/src/domain/errors/SchemaUnsupportedError.js +17 -0
  24. package/src/domain/errors/ShardCorruptionError.js +56 -0
  25. package/src/domain/errors/ShardLoadError.js +57 -0
  26. package/src/domain/errors/ShardValidationError.js +61 -0
  27. package/src/domain/errors/StorageError.js +57 -0
  28. package/src/domain/errors/SyncError.js +30 -0
  29. package/src/domain/errors/TraversalError.js +23 -0
  30. package/src/domain/errors/WarpError.js +31 -0
  31. package/src/domain/errors/WormholeError.js +28 -0
  32. package/src/domain/errors/WriterError.js +39 -0
  33. package/src/domain/errors/index.js +21 -0
  34. package/src/domain/services/AnchorMessageCodec.js +99 -0
  35. package/src/domain/services/BitmapIndexBuilder.js +225 -0
  36. package/src/domain/services/BitmapIndexReader.js +435 -0
  37. package/src/domain/services/BoundaryTransitionRecord.js +463 -0
  38. package/src/domain/services/CheckpointMessageCodec.js +147 -0
  39. package/src/domain/services/CheckpointSerializerV5.js +281 -0
  40. package/src/domain/services/CheckpointService.js +384 -0
  41. package/src/domain/services/CommitDagTraversalService.js +156 -0
  42. package/src/domain/services/DagPathFinding.js +712 -0
  43. package/src/domain/services/DagTopology.js +239 -0
  44. package/src/domain/services/DagTraversal.js +245 -0
  45. package/src/domain/services/Frontier.js +108 -0
  46. package/src/domain/services/GCMetrics.js +101 -0
  47. package/src/domain/services/GCPolicy.js +122 -0
  48. package/src/domain/services/GitLogParser.js +205 -0
  49. package/src/domain/services/HealthCheckService.js +246 -0
  50. package/src/domain/services/HookInstaller.js +326 -0
  51. package/src/domain/services/HttpSyncServer.js +262 -0
  52. package/src/domain/services/IndexRebuildService.js +426 -0
  53. package/src/domain/services/IndexStalenessChecker.js +103 -0
  54. package/src/domain/services/JoinReducer.js +582 -0
  55. package/src/domain/services/KeyCodec.js +113 -0
  56. package/src/domain/services/LegacyAnchorDetector.js +67 -0
  57. package/src/domain/services/LogicalTraversal.js +351 -0
  58. package/src/domain/services/MessageCodecInternal.js +132 -0
  59. package/src/domain/services/MessageSchemaDetector.js +145 -0
  60. package/src/domain/services/MigrationService.js +55 -0
  61. package/src/domain/services/ObserverView.js +265 -0
  62. package/src/domain/services/PatchBuilderV2.js +669 -0
  63. package/src/domain/services/PatchMessageCodec.js +140 -0
  64. package/src/domain/services/ProvenanceIndex.js +337 -0
  65. package/src/domain/services/ProvenancePayload.js +242 -0
  66. package/src/domain/services/QueryBuilder.js +835 -0
  67. package/src/domain/services/StateDiff.js +300 -0
  68. package/src/domain/services/StateSerializerV5.js +156 -0
  69. package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
  70. package/src/domain/services/SyncProtocol.js +593 -0
  71. package/src/domain/services/TemporalQuery.js +201 -0
  72. package/src/domain/services/TranslationCost.js +221 -0
  73. package/src/domain/services/TraversalService.js +8 -0
  74. package/src/domain/services/WarpMessageCodec.js +29 -0
  75. package/src/domain/services/WarpStateIndexBuilder.js +127 -0
  76. package/src/domain/services/WormholeService.js +353 -0
  77. package/src/domain/types/TickReceipt.js +285 -0
  78. package/src/domain/types/WarpTypes.js +209 -0
  79. package/src/domain/types/WarpTypesV2.js +200 -0
  80. package/src/domain/utils/CachedValue.js +140 -0
  81. package/src/domain/utils/EventId.js +89 -0
  82. package/src/domain/utils/LRUCache.js +112 -0
  83. package/src/domain/utils/MinHeap.js +114 -0
  84. package/src/domain/utils/RefLayout.js +280 -0
  85. package/src/domain/utils/WriterId.js +205 -0
  86. package/src/domain/utils/cancellation.js +33 -0
  87. package/src/domain/utils/canonicalStringify.js +42 -0
  88. package/src/domain/utils/defaultClock.js +20 -0
  89. package/src/domain/utils/defaultCodec.js +51 -0
  90. package/src/domain/utils/nullLogger.js +21 -0
  91. package/src/domain/utils/roaring.js +181 -0
  92. package/src/domain/utils/shardVersion.js +9 -0
  93. package/src/domain/warp/PatchSession.js +217 -0
  94. package/src/domain/warp/Writer.js +181 -0
  95. package/src/hooks/post-merge.sh +60 -0
  96. package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
  97. package/src/infrastructure/adapters/ClockAdapter.js +57 -0
  98. package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
  99. package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
  100. package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
  101. package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
  102. package/src/infrastructure/adapters/NoOpLogger.js +62 -0
  103. package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
  104. package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
  105. package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
  106. package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
  107. package/src/infrastructure/codecs/CborCodec.js +384 -0
  108. package/src/ports/BlobPort.js +30 -0
  109. package/src/ports/ClockPort.js +25 -0
  110. package/src/ports/CodecPort.js +25 -0
  111. package/src/ports/CommitPort.js +114 -0
  112. package/src/ports/ConfigPort.js +31 -0
  113. package/src/ports/CryptoPort.js +38 -0
  114. package/src/ports/GraphPersistencePort.js +57 -0
  115. package/src/ports/HttpServerPort.js +25 -0
  116. package/src/ports/IndexStoragePort.js +39 -0
  117. package/src/ports/LoggerPort.js +68 -0
  118. package/src/ports/RefPort.js +51 -0
  119. package/src/ports/TreePort.js +51 -0
  120. package/src/visualization/index.js +26 -0
  121. package/src/visualization/layouts/converters.js +75 -0
  122. package/src/visualization/layouts/elkAdapter.js +86 -0
  123. package/src/visualization/layouts/elkLayout.js +95 -0
  124. package/src/visualization/layouts/index.js +29 -0
  125. package/src/visualization/renderers/ascii/box.js +16 -0
  126. package/src/visualization/renderers/ascii/check.js +271 -0
  127. package/src/visualization/renderers/ascii/colors.js +13 -0
  128. package/src/visualization/renderers/ascii/formatters.js +73 -0
  129. package/src/visualization/renderers/ascii/graph.js +344 -0
  130. package/src/visualization/renderers/ascii/history.js +335 -0
  131. package/src/visualization/renderers/ascii/index.js +14 -0
  132. package/src/visualization/renderers/ascii/info.js +245 -0
  133. package/src/visualization/renderers/ascii/materialize.js +255 -0
  134. package/src/visualization/renderers/ascii/path.js +240 -0
  135. package/src/visualization/renderers/ascii/progress.js +32 -0
  136. package/src/visualization/renderers/ascii/symbols.js +33 -0
  137. package/src/visualization/renderers/ascii/table.js +19 -0
  138. package/src/visualization/renderers/browser/index.js +1 -0
  139. package/src/visualization/renderers/svg/index.js +159 -0
  140. package/src/visualization/utils/ansi.js +14 -0
  141. package/src/visualization/utils/time.js +40 -0
  142. package/src/visualization/utils/truncate.js +40 -0
  143. 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
+ }