@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,57 @@
|
|
|
1
|
+
import IndexError from './IndexError.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error thrown when a storage operation fails.
|
|
5
|
+
*
|
|
6
|
+
* This error indicates that a read or write operation to storage failed,
|
|
7
|
+
* typically due to I/O errors, permission issues, or storage unavailability.
|
|
8
|
+
*
|
|
9
|
+
* @class StorageError
|
|
10
|
+
* @extends IndexError
|
|
11
|
+
*
|
|
12
|
+
* @property {string} name - The error name ('StorageError')
|
|
13
|
+
* @property {string} code - Error code ('STORAGE_ERROR')
|
|
14
|
+
* @property {string} operation - The operation that failed ('read' or 'write')
|
|
15
|
+
* @property {string} oid - Object ID associated with the operation
|
|
16
|
+
* @property {Error} cause - The original error that caused the failure
|
|
17
|
+
* @property {Object} context - Serializable context object for debugging
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* try {
|
|
21
|
+
* await storage.write(oid, data);
|
|
22
|
+
* } catch (err) {
|
|
23
|
+
* throw new StorageError('Failed to write to storage', {
|
|
24
|
+
* operation: 'write',
|
|
25
|
+
* oid: 'abc123',
|
|
26
|
+
* cause: err
|
|
27
|
+
* });
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
export default class StorageError extends IndexError {
|
|
31
|
+
/**
|
|
32
|
+
* Creates a new StorageError.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} message - Human-readable error message
|
|
35
|
+
* @param {Object} [options={}] - Error options
|
|
36
|
+
* @param {string} [options.operation] - The operation that failed ('read' or 'write')
|
|
37
|
+
* @param {string} [options.oid] - Object ID associated with the operation
|
|
38
|
+
* @param {Error} [options.cause] - The original error that caused the failure
|
|
39
|
+
* @param {Object} [options.context={}] - Additional context for debugging
|
|
40
|
+
*/
|
|
41
|
+
constructor(message, options = {}) {
|
|
42
|
+
const context = {
|
|
43
|
+
...options.context,
|
|
44
|
+
operation: options.operation,
|
|
45
|
+
oid: options.oid,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
super(message, {
|
|
49
|
+
code: 'STORAGE_ERROR',
|
|
50
|
+
context,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.operation = options.operation;
|
|
54
|
+
this.oid = options.oid;
|
|
55
|
+
this.cause = options.cause;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import WarpError from './WarpError.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error class for sync transport and replication operations.
|
|
5
|
+
*
|
|
6
|
+
* SyncError is thrown when synchronization between WARP graph instances fails.
|
|
7
|
+
*
|
|
8
|
+
* ## Error Codes
|
|
9
|
+
*
|
|
10
|
+
* | Code | Description |
|
|
11
|
+
* |------|-------------|
|
|
12
|
+
* | `E_SYNC_REMOTE_URL` | Invalid or unsupported remote URL |
|
|
13
|
+
* | `E_SYNC_NETWORK` | Network-level failure |
|
|
14
|
+
* | `E_SYNC_TIMEOUT` | Sync request exceeded timeout |
|
|
15
|
+
* | `E_SYNC_REMOTE` | Remote server returned a 5xx error |
|
|
16
|
+
* | `E_SYNC_PROTOCOL` | Protocol violation: 4xx, invalid JSON, or malformed response |
|
|
17
|
+
* | `SYNC_ERROR` | Generic/default sync error |
|
|
18
|
+
*
|
|
19
|
+
* @class SyncError
|
|
20
|
+
* @extends WarpError
|
|
21
|
+
*
|
|
22
|
+
* @property {string} name - Always 'SyncError' for instanceof checks
|
|
23
|
+
* @property {string} code - Machine-readable error code for programmatic handling
|
|
24
|
+
* @property {Object} context - Serializable context object with error details
|
|
25
|
+
*/
|
|
26
|
+
export default class SyncError extends WarpError {
|
|
27
|
+
constructor(message, options = {}) {
|
|
28
|
+
super(message, 'SYNC_ERROR', options);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import WarpError from './WarpError.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error class for graph traversal operations.
|
|
5
|
+
*
|
|
6
|
+
* @class TraversalError
|
|
7
|
+
* @extends WarpError
|
|
8
|
+
*
|
|
9
|
+
* @property {string} name - The error name ('TraversalError')
|
|
10
|
+
* @property {string} code - Error code for programmatic handling (default: 'TRAVERSAL_ERROR')
|
|
11
|
+
* @property {Object} context - Serializable context object for debugging
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* throw new TraversalError('Node not found in index', {
|
|
15
|
+
* code: 'NODE_NOT_FOUND',
|
|
16
|
+
* context: { sha: 'abc123', operation: 'bfs' }
|
|
17
|
+
* });
|
|
18
|
+
*/
|
|
19
|
+
export default class TraversalError extends WarpError {
|
|
20
|
+
constructor(message, options = {}) {
|
|
21
|
+
super(message, 'TRAVERSAL_ERROR', options);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for all WARP domain errors.
|
|
3
|
+
*
|
|
4
|
+
* Provides shared constructor logic: name (from constructor), code,
|
|
5
|
+
* context, and stack trace capture. Subclasses reduce to a one-line
|
|
6
|
+
* constructor calling super(message, defaultCode, options).
|
|
7
|
+
*
|
|
8
|
+
* @class WarpError
|
|
9
|
+
* @extends Error
|
|
10
|
+
*
|
|
11
|
+
* @property {string} name - Error name (set from constructor.name)
|
|
12
|
+
* @property {string} code - Machine-readable error code
|
|
13
|
+
* @property {Object} context - Serializable context for debugging
|
|
14
|
+
*/
|
|
15
|
+
export default class WarpError extends Error {
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} message - Human-readable error message
|
|
18
|
+
* @param {string} defaultCode - Default error code if not overridden by options
|
|
19
|
+
* @param {Object} [options={}] - Error options
|
|
20
|
+
* @param {string} [options.code] - Override error code
|
|
21
|
+
* @param {Object} [options.context={}] - Serializable context for debugging
|
|
22
|
+
*/
|
|
23
|
+
constructor(message, defaultCode, options = {}) {
|
|
24
|
+
super(message);
|
|
25
|
+
const opts = options || {};
|
|
26
|
+
this.name = this.constructor.name;
|
|
27
|
+
this.code = opts.code || defaultCode;
|
|
28
|
+
this.context = opts.context || {};
|
|
29
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import WarpError from './WarpError.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error class for wormhole compression operations.
|
|
5
|
+
*
|
|
6
|
+
* ## Error Codes
|
|
7
|
+
*
|
|
8
|
+
* | Code | Description |
|
|
9
|
+
* |------|-------------|
|
|
10
|
+
* | `E_WORMHOLE_SHA_NOT_FOUND` | A specified SHA does not exist |
|
|
11
|
+
* | `E_WORMHOLE_INVALID_RANGE` | The from SHA is not an ancestor of to SHA |
|
|
12
|
+
* | `E_WORMHOLE_MULTI_WRITER` | The range spans multiple writers |
|
|
13
|
+
* | `E_WORMHOLE_EMPTY_RANGE` | No patches found in the specified range |
|
|
14
|
+
* | `E_WORMHOLE_NOT_PATCH` | A commit in the range is not a patch commit |
|
|
15
|
+
* | `WORMHOLE_ERROR` | Generic/default wormhole error |
|
|
16
|
+
*
|
|
17
|
+
* @class WormholeError
|
|
18
|
+
* @extends WarpError
|
|
19
|
+
*
|
|
20
|
+
* @property {string} name - Always 'WormholeError' for instanceof checks
|
|
21
|
+
* @property {string} code - Machine-readable error code for programmatic handling
|
|
22
|
+
* @property {Object} context - Serializable context object with error details
|
|
23
|
+
*/
|
|
24
|
+
export default class WormholeError extends WarpError {
|
|
25
|
+
constructor(message, options = {}) {
|
|
26
|
+
super(message, 'WORMHOLE_ERROR', options);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import WarpError from './WarpError.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error class for Writer operations.
|
|
5
|
+
*
|
|
6
|
+
* Preserves the existing (code, message, cause) positional constructor
|
|
7
|
+
* signature used throughout PatchSession and PatchBuilderV2, while
|
|
8
|
+
* inheriting from WarpError for unified error hierarchy.
|
|
9
|
+
*
|
|
10
|
+
* ## Error Codes
|
|
11
|
+
*
|
|
12
|
+
* | Code | Description |
|
|
13
|
+
* |------|-------------|
|
|
14
|
+
* | `EMPTY_PATCH` | Patch commit attempted with zero operations |
|
|
15
|
+
* | `WRITER_REF_ADVANCED` | Writer ref moved since beginPatch() |
|
|
16
|
+
* | `WRITER_CAS_CONFLICT` | Compare-and-swap failure during commit |
|
|
17
|
+
* | `PERSIST_WRITE_FAILED` | Git persistence operation failed |
|
|
18
|
+
* | `WRITER_ERROR` | Generic/default writer error |
|
|
19
|
+
*
|
|
20
|
+
* @class WriterError
|
|
21
|
+
* @extends WarpError
|
|
22
|
+
*
|
|
23
|
+
* @property {string} name - Always 'WriterError'
|
|
24
|
+
* @property {string} code - Machine-readable error code
|
|
25
|
+
* @property {Error} [cause] - Original error that caused this error
|
|
26
|
+
*/
|
|
27
|
+
export default class WriterError extends WarpError {
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} code - Error code
|
|
30
|
+
* @param {string} message - Human-readable error message
|
|
31
|
+
* @param {Error} [cause] - Original error that caused this error
|
|
32
|
+
*/
|
|
33
|
+
constructor(code, message, cause) {
|
|
34
|
+
super(message, 'WRITER_ERROR', { code });
|
|
35
|
+
if (cause !== undefined) {
|
|
36
|
+
this.cause = cause;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error classes for domain operations.
|
|
3
|
+
*
|
|
4
|
+
* @module domain/errors
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { default as EmptyMessageError } from './EmptyMessageError.js';
|
|
8
|
+
export { default as WarpError } from './WarpError.js';
|
|
9
|
+
export { default as ForkError } from './ForkError.js';
|
|
10
|
+
export { default as IndexError } from './IndexError.js';
|
|
11
|
+
export { default as OperationAbortedError } from './OperationAbortedError.js';
|
|
12
|
+
export { default as QueryError } from './QueryError.js';
|
|
13
|
+
export { default as SyncError } from './SyncError.js';
|
|
14
|
+
export { default as ShardCorruptionError } from './ShardCorruptionError.js';
|
|
15
|
+
export { default as ShardLoadError } from './ShardLoadError.js';
|
|
16
|
+
export { default as ShardValidationError } from './ShardValidationError.js';
|
|
17
|
+
export { default as StorageError } from './StorageError.js';
|
|
18
|
+
export { default as SchemaUnsupportedError } from './SchemaUnsupportedError.js';
|
|
19
|
+
export { default as TraversalError } from './TraversalError.js';
|
|
20
|
+
export { default as WriterError } from './WriterError.js';
|
|
21
|
+
export { default as WormholeError } from './WormholeError.js';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anchor message encoding and decoding for WARP commit messages.
|
|
3
|
+
*
|
|
4
|
+
* Handles the 'anchor' message type which marks a merge point in the WARP
|
|
5
|
+
* DAG. See {@link module:domain/services/WarpMessageCodec} for the facade
|
|
6
|
+
* that re-exports all codec functions.
|
|
7
|
+
*
|
|
8
|
+
* @module domain/services/AnchorMessageCodec
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { validateGraphName } from '../utils/RefLayout.js';
|
|
12
|
+
import {
|
|
13
|
+
getCodec,
|
|
14
|
+
MESSAGE_TITLES,
|
|
15
|
+
TRAILER_KEYS,
|
|
16
|
+
validateSchema,
|
|
17
|
+
} from './MessageCodecInternal.js';
|
|
18
|
+
|
|
19
|
+
// -----------------------------------------------------------------------------
|
|
20
|
+
// Encoder
|
|
21
|
+
// -----------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Encodes an anchor commit message.
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} options - The anchor message options
|
|
27
|
+
* @param {string} options.graph - The graph name
|
|
28
|
+
* @param {number} [options.schema=2] - The schema version (defaults to 2 for new messages)
|
|
29
|
+
* @returns {string} The encoded commit message
|
|
30
|
+
* @throws {Error} If any validation fails
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const message = encodeAnchorMessage({ graph: 'events' });
|
|
34
|
+
*/
|
|
35
|
+
export function encodeAnchorMessage({ graph, schema = 2 }) {
|
|
36
|
+
// Validate inputs
|
|
37
|
+
validateGraphName(graph);
|
|
38
|
+
validateSchema(schema);
|
|
39
|
+
|
|
40
|
+
const codec = getCodec();
|
|
41
|
+
return codec.encode({
|
|
42
|
+
title: MESSAGE_TITLES.anchor,
|
|
43
|
+
trailers: {
|
|
44
|
+
[TRAILER_KEYS.kind]: 'anchor',
|
|
45
|
+
[TRAILER_KEYS.graph]: graph,
|
|
46
|
+
[TRAILER_KEYS.schema]: String(schema),
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// -----------------------------------------------------------------------------
|
|
52
|
+
// Decoder
|
|
53
|
+
// -----------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Decodes an anchor commit message.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} message - The raw commit message
|
|
59
|
+
* @returns {Object} The decoded anchor message
|
|
60
|
+
* @returns {string} return.kind - Always 'anchor'
|
|
61
|
+
* @returns {string} return.graph - The graph name
|
|
62
|
+
* @returns {number} return.schema - The schema version
|
|
63
|
+
* @throws {Error} If the message is not a valid anchor message
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* const { kind, graph, schema } = decodeAnchorMessage(message);
|
|
67
|
+
*/
|
|
68
|
+
export function decodeAnchorMessage(message) {
|
|
69
|
+
const codec = getCodec();
|
|
70
|
+
const decoded = codec.decode(message);
|
|
71
|
+
const { trailers } = decoded;
|
|
72
|
+
|
|
73
|
+
// Validate kind discriminator
|
|
74
|
+
const kind = trailers[TRAILER_KEYS.kind];
|
|
75
|
+
if (kind !== 'anchor') {
|
|
76
|
+
throw new Error(`Invalid anchor message: eg-kind must be 'anchor', got '${kind}'`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Extract and validate required fields
|
|
80
|
+
const graph = trailers[TRAILER_KEYS.graph];
|
|
81
|
+
if (!graph) {
|
|
82
|
+
throw new Error('Invalid anchor message: missing required trailer eg-graph');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const schemaStr = trailers[TRAILER_KEYS.schema];
|
|
86
|
+
if (!schemaStr) {
|
|
87
|
+
throw new Error('Invalid anchor message: missing required trailer eg-schema');
|
|
88
|
+
}
|
|
89
|
+
const schema = parseInt(schemaStr, 10);
|
|
90
|
+
if (!Number.isInteger(schema) || schema < 1) {
|
|
91
|
+
throw new Error(`Invalid anchor message: eg-schema must be a positive integer, got '${schemaStr}'`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
kind: 'anchor',
|
|
96
|
+
graph,
|
|
97
|
+
schema,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import defaultCodec from '../utils/defaultCodec.js';
|
|
2
|
+
import { getRoaringBitmap32, getNativeRoaringAvailable } from '../utils/roaring.js';
|
|
3
|
+
import { canonicalStringify } from '../utils/canonicalStringify.js';
|
|
4
|
+
import { SHARD_VERSION } from '../utils/shardVersion.js';
|
|
5
|
+
|
|
6
|
+
// Re-export for backwards compatibility
|
|
7
|
+
export { SHARD_VERSION };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Computes a SHA-256 checksum of the given data.
|
|
11
|
+
* Uses canonical JSON stringification for deterministic output
|
|
12
|
+
* across different JavaScript engines.
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} data - The data object to checksum
|
|
15
|
+
* @param {import('../../ports/CryptoPort.js').default} crypto - CryptoPort instance
|
|
16
|
+
* @returns {Promise<string|null>} Hex-encoded SHA-256 hash
|
|
17
|
+
*/
|
|
18
|
+
const computeChecksum = async (data, crypto) => {
|
|
19
|
+
if (!crypto) { return null; }
|
|
20
|
+
const json = canonicalStringify(data);
|
|
21
|
+
return await crypto.hash('sha256', json);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** @type {boolean|null} Whether native Roaring bindings are available (null = unknown until first use) */
|
|
25
|
+
export let NATIVE_ROARING_AVAILABLE = null;
|
|
26
|
+
|
|
27
|
+
const ensureRoaringBitmap32 = () => {
|
|
28
|
+
const RoaringBitmap32 = getRoaringBitmap32();
|
|
29
|
+
if (NATIVE_ROARING_AVAILABLE === null) {
|
|
30
|
+
NATIVE_ROARING_AVAILABLE = getNativeRoaringAvailable();
|
|
31
|
+
}
|
|
32
|
+
return RoaringBitmap32;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Wraps data in a version/checksum envelope.
|
|
37
|
+
* @param {Object} data - The data to wrap
|
|
38
|
+
* @param {import('../../ports/CryptoPort.js').default} crypto - CryptoPort instance
|
|
39
|
+
* @returns {Promise<Object>} Envelope with version, checksum, and data
|
|
40
|
+
*/
|
|
41
|
+
const wrapShard = async (data, crypto) => ({
|
|
42
|
+
version: SHARD_VERSION,
|
|
43
|
+
checksum: await computeChecksum(data, crypto),
|
|
44
|
+
data,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Serializes a frontier Map into CBOR and JSON blobs in the given tree.
|
|
49
|
+
* @param {Map<string, string>} frontier - Writer→tip SHA map
|
|
50
|
+
* @param {Record<string, Buffer>} tree - Target tree to add entries to
|
|
51
|
+
* @param {import('../../ports/CodecPort.js').default} codec - Codec for CBOR serialization
|
|
52
|
+
*/
|
|
53
|
+
function serializeFrontierToTree(frontier, tree, codec) {
|
|
54
|
+
const sorted = {};
|
|
55
|
+
for (const key of Array.from(frontier.keys()).sort()) {
|
|
56
|
+
sorted[key] = frontier.get(key);
|
|
57
|
+
}
|
|
58
|
+
const envelope = { version: 1, writerCount: frontier.size, frontier: sorted };
|
|
59
|
+
tree['frontier.cbor'] = Buffer.from(codec.encode(envelope));
|
|
60
|
+
tree['frontier.json'] = Buffer.from(canonicalStringify(envelope));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Builder for constructing bitmap indexes in memory.
|
|
65
|
+
*
|
|
66
|
+
* This is a pure domain class with no infrastructure dependencies.
|
|
67
|
+
* Create an instance, add nodes and edges, then serialize to persist.
|
|
68
|
+
*
|
|
69
|
+
* Callers that persist the serialized output typically need
|
|
70
|
+
* BlobPort + TreePort + RefPort from the persistence layer.
|
|
71
|
+
*
|
|
72
|
+
* **Performance Note**: Uses Roaring Bitmaps for compression. Native bindings
|
|
73
|
+
* provide best performance. Check `NATIVE_ROARING_AVAILABLE` export if
|
|
74
|
+
* performance is critical.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* import BitmapIndexBuilder, { NATIVE_ROARING_AVAILABLE } from './BitmapIndexBuilder.js';
|
|
78
|
+
* if (NATIVE_ROARING_AVAILABLE === false) {
|
|
79
|
+
* console.warn('Consider installing native Roaring bindings for better performance');
|
|
80
|
+
* }
|
|
81
|
+
* const builder = new BitmapIndexBuilder();
|
|
82
|
+
*/
|
|
83
|
+
export default class BitmapIndexBuilder {
|
|
84
|
+
/**
|
|
85
|
+
* Creates a new BitmapIndexBuilder instance.
|
|
86
|
+
*
|
|
87
|
+
* The builder tracks:
|
|
88
|
+
* - SHA to numeric ID mappings (for compact bitmap storage)
|
|
89
|
+
* - Forward edge bitmaps (parent → children)
|
|
90
|
+
* - Reverse edge bitmaps (child → parents)
|
|
91
|
+
*
|
|
92
|
+
* @param {Object} [options] - Configuration options
|
|
93
|
+
* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort instance for hashing
|
|
94
|
+
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
95
|
+
*/
|
|
96
|
+
constructor({ crypto, codec } = {}) {
|
|
97
|
+
/** @type {import('../../ports/CryptoPort.js').default} */
|
|
98
|
+
this._crypto = crypto;
|
|
99
|
+
/** @type {import('../../ports/CodecPort.js').default|undefined} */
|
|
100
|
+
this._codec = codec || defaultCodec;
|
|
101
|
+
/** @type {Map<string, number>} */
|
|
102
|
+
this.shaToId = new Map();
|
|
103
|
+
/** @type {string[]} */
|
|
104
|
+
this.idToSha = [];
|
|
105
|
+
/** @type {Map<string, RoaringBitmap32>} */
|
|
106
|
+
this.bitmaps = new Map();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Registers a node without adding edges.
|
|
111
|
+
* Useful for root nodes with no parents.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} sha - The node's SHA
|
|
114
|
+
* @returns {number} The assigned numeric ID
|
|
115
|
+
*/
|
|
116
|
+
registerNode(sha) {
|
|
117
|
+
return this._getOrCreateId(sha);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Adds a directed edge from source to target node.
|
|
122
|
+
*
|
|
123
|
+
* Updates both forward (src → tgt) and reverse (tgt → src) bitmaps.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} srcSha - Source node SHA (parent)
|
|
126
|
+
* @param {string} tgtSha - Target node SHA (child)
|
|
127
|
+
* @returns {void}
|
|
128
|
+
*/
|
|
129
|
+
addEdge(srcSha, tgtSha) {
|
|
130
|
+
const srcId = this._getOrCreateId(srcSha);
|
|
131
|
+
const tgtId = this._getOrCreateId(tgtSha);
|
|
132
|
+
this._addToBitmap({ sha: srcSha, id: tgtId, type: 'fwd' });
|
|
133
|
+
this._addToBitmap({ sha: tgtSha, id: srcId, type: 'rev' });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Serializes the index to a tree structure of buffers.
|
|
138
|
+
*
|
|
139
|
+
* Output structure (sharded by SHA prefix for lazy loading):
|
|
140
|
+
* - `meta_XX.json`: {version, checksum, data: {sha: id, ...}} for SHAs with prefix XX
|
|
141
|
+
* - `shards_fwd_XX.json`: {version, checksum, data: {sha: base64Bitmap, ...}} for forward edges
|
|
142
|
+
* - `shards_rev_XX.json`: {version, checksum, data: {sha: base64Bitmap, ...}} for reverse edges
|
|
143
|
+
*
|
|
144
|
+
* Each shard is wrapped in a version/checksum envelope for integrity verification.
|
|
145
|
+
*
|
|
146
|
+
* @param {Object} [options] - Serialization options
|
|
147
|
+
* @param {Map<string, string>} [options.frontier] - Writer→tip SHA map to include in the tree
|
|
148
|
+
* @returns {Promise<Record<string, Buffer>>} Map of path → serialized content
|
|
149
|
+
*/
|
|
150
|
+
async serialize({ frontier } = {}) {
|
|
151
|
+
const tree = {};
|
|
152
|
+
|
|
153
|
+
// Serialize ID mappings (sharded by prefix)
|
|
154
|
+
const idShards = {};
|
|
155
|
+
for (const [sha, id] of this.shaToId) {
|
|
156
|
+
const prefix = sha.substring(0, 2);
|
|
157
|
+
if (!idShards[prefix]) {
|
|
158
|
+
idShards[prefix] = {};
|
|
159
|
+
}
|
|
160
|
+
idShards[prefix][sha] = id;
|
|
161
|
+
}
|
|
162
|
+
for (const [prefix, map] of Object.entries(idShards)) {
|
|
163
|
+
tree[`meta_${prefix}.json`] = Buffer.from(JSON.stringify(await wrapShard(map, this._crypto)));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Serialize bitmaps (sharded by prefix, per-node within shard)
|
|
167
|
+
// Keys are constructed as '${type}_${sha}' by _addToBitmap (e.g., 'fwd_abc123', 'rev_def456')
|
|
168
|
+
const bitmapShards = { fwd: {}, rev: {} };
|
|
169
|
+
for (const [key, bitmap] of this.bitmaps) {
|
|
170
|
+
const [type, sha] = [key.substring(0, 3), key.substring(4)];
|
|
171
|
+
const prefix = sha.substring(0, 2);
|
|
172
|
+
|
|
173
|
+
if (!bitmapShards[type][prefix]) {
|
|
174
|
+
bitmapShards[type][prefix] = {};
|
|
175
|
+
}
|
|
176
|
+
// Encode bitmap as base64 for JSON storage
|
|
177
|
+
bitmapShards[type][prefix][sha] = bitmap.serialize(true).toString('base64');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const type of ['fwd', 'rev']) {
|
|
181
|
+
for (const [prefix, shardData] of Object.entries(bitmapShards[type])) {
|
|
182
|
+
tree[`shards_${type}_${prefix}.json`] = Buffer.from(JSON.stringify(await wrapShard(shardData, this._crypto)));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (frontier) {
|
|
187
|
+
serializeFrontierToTree(frontier, tree, this._codec);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return tree;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Gets or creates a numeric ID for a SHA.
|
|
195
|
+
* @param {string} sha - The SHA to look up or register
|
|
196
|
+
* @returns {number} The numeric ID
|
|
197
|
+
* @private
|
|
198
|
+
*/
|
|
199
|
+
_getOrCreateId(sha) {
|
|
200
|
+
if (this.shaToId.has(sha)) {
|
|
201
|
+
return this.shaToId.get(sha);
|
|
202
|
+
}
|
|
203
|
+
const id = this.idToSha.length;
|
|
204
|
+
this.idToSha.push(sha);
|
|
205
|
+
this.shaToId.set(sha, id);
|
|
206
|
+
return id;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Adds an ID to a node's bitmap.
|
|
211
|
+
* @param {Object} opts - Options
|
|
212
|
+
* @param {string} opts.sha - The SHA to use as key
|
|
213
|
+
* @param {number} opts.id - The ID to add to the bitmap
|
|
214
|
+
* @param {string} opts.type - 'fwd' or 'rev'
|
|
215
|
+
* @private
|
|
216
|
+
*/
|
|
217
|
+
_addToBitmap({ sha, id, type }) {
|
|
218
|
+
const key = `${type}_${sha}`;
|
|
219
|
+
if (!this.bitmaps.has(key)) {
|
|
220
|
+
const RoaringBitmap32 = ensureRoaringBitmap32();
|
|
221
|
+
this.bitmaps.set(key, new RoaringBitmap32());
|
|
222
|
+
}
|
|
223
|
+
this.bitmaps.get(key).add(id);
|
|
224
|
+
}
|
|
225
|
+
}
|