@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,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CBOR Codec with Canonical/Deterministic Encoding.
|
|
3
|
+
*
|
|
4
|
+
* This module wraps the `cbor-x` library to provide **canonical CBOR encoding**
|
|
5
|
+
* for WARP graph patches. Canonical encoding is critical for WARP's content-addressed
|
|
6
|
+
* storage model: the same logical patch must always produce identical bytes so that
|
|
7
|
+
* Git SHA comparisons work correctly.
|
|
8
|
+
*
|
|
9
|
+
* ## Why Canonical Encoding Matters
|
|
10
|
+
*
|
|
11
|
+
* WARP stores patches as Git commits where the commit message contains CBOR-encoded
|
|
12
|
+
* patch data. Git identifies commits by SHA-256 hash of their content. If the same
|
|
13
|
+
* logical patch could encode to different byte sequences (e.g., due to non-deterministic
|
|
14
|
+
* object key ordering), then:
|
|
15
|
+
*
|
|
16
|
+
* 1. **Duplicate patches** could exist with different SHAs
|
|
17
|
+
* 2. **Deduplication** would fail - the same operation stored multiple times
|
|
18
|
+
* 3. **Causality tracking** would break - version vectors rely on exact SHA matching
|
|
19
|
+
* 4. **Sync protocols** would transfer redundant data
|
|
20
|
+
*
|
|
21
|
+
* ## The cbor-x Limitation
|
|
22
|
+
*
|
|
23
|
+
* Unlike some CBOR libraries, `cbor-x` does not provide built-in canonical/deterministic
|
|
24
|
+
* encoding. JavaScript object key enumeration order is insertion-order (per ES2015+),
|
|
25
|
+
* which means `{ b: 1, a: 2 }` and `{ a: 2, b: 1 }` would encode differently despite
|
|
26
|
+
* being logically equivalent.
|
|
27
|
+
*
|
|
28
|
+
* ## Our Solution: Pre-encoding Key Sorting
|
|
29
|
+
*
|
|
30
|
+
* Before encoding, we recursively sort all object keys using JavaScript's default
|
|
31
|
+
* lexicographic sort (via {@link sortKeys}). This produces deterministic,
|
|
32
|
+
* lexicographically-sorted CBOR for WARP patches.
|
|
33
|
+
*
|
|
34
|
+
* **Important**: This is NOT RFC 7049 Section 3.9 canonical CBOR, which requires
|
|
35
|
+
* byte-length-first ordering (shorter keys before longer keys, then lexicographic
|
|
36
|
+
* within same length). Our approach uses simple lexicographic ordering, which is
|
|
37
|
+
* sufficient for WARP's content-addressing needs but not interoperable with systems
|
|
38
|
+
* expecting strict RFC 7049 canonical form.
|
|
39
|
+
*
|
|
40
|
+
* This ensures:
|
|
41
|
+
*
|
|
42
|
+
* - **Determinism**: Same input always produces identical bytes
|
|
43
|
+
* - **Verifiability**: Patches can be re-encoded and compared byte-for-byte
|
|
44
|
+
*
|
|
45
|
+
* ## Performance Considerations
|
|
46
|
+
*
|
|
47
|
+
* The key sorting step adds O(n log n) overhead per encoding operation where n is
|
|
48
|
+
* the total number of keys across all nested objects. For typical WARP patches
|
|
49
|
+
* (tens to hundreds of keys), this overhead is negligible compared to I/O costs.
|
|
50
|
+
*
|
|
51
|
+
* @module infrastructure/codecs/CborCodec
|
|
52
|
+
* @see {@link https://cbor.io/} for CBOR specification
|
|
53
|
+
* @see {@link https://tools.ietf.org/html/rfc7049#section-3.9} for Canonical CBOR
|
|
54
|
+
* @see {@link https://github.com/kriszyp/cbor-x} for cbor-x library
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import { Encoder, decode as cborDecode } from 'cbor-x';
|
|
58
|
+
import CodecPort from '../../ports/CodecPort.js';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pre-configured cbor-x encoder instance.
|
|
62
|
+
*
|
|
63
|
+
* Configuration options:
|
|
64
|
+
* - `useRecords: false` - Disables cbor-x's record extension (which uses a custom
|
|
65
|
+
* CBOR tag for repeated object structures). We use standard CBOR maps for maximum
|
|
66
|
+
* interoperability with other CBOR implementations.
|
|
67
|
+
* - `mapsAsObjects: true` - Decodes CBOR maps as JavaScript plain objects rather than
|
|
68
|
+
* Map instances. This matches the expected input format for WARP patches.
|
|
69
|
+
*
|
|
70
|
+
* @type {Encoder}
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
const encoder = new Encoder({
|
|
74
|
+
useRecords: false,
|
|
75
|
+
mapsAsObjects: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Checks if a value is a plain object (constructed via Object or Object.create(null)).
|
|
80
|
+
*
|
|
81
|
+
* @param {unknown} value - The value to check
|
|
82
|
+
* @returns {boolean} True if value is a plain object
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
function isPlainObject(value) {
|
|
86
|
+
return typeof value === 'object' && (value.constructor === Object || value.constructor === undefined);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Sorts the keys of a plain object and recursively processes values.
|
|
91
|
+
*
|
|
92
|
+
* @param {Object} obj - The plain object to process
|
|
93
|
+
* @returns {Object} A new object with sorted keys
|
|
94
|
+
* @private
|
|
95
|
+
*/
|
|
96
|
+
function sortPlainObject(obj) {
|
|
97
|
+
const sorted = {};
|
|
98
|
+
const keys = Object.keys(obj).sort();
|
|
99
|
+
for (const key of keys) {
|
|
100
|
+
sorted[key] = sortKeys(obj[key]);
|
|
101
|
+
}
|
|
102
|
+
return sorted;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Converts a Map to a sorted plain object with recursive value processing.
|
|
107
|
+
* Validates that all Map keys are strings (required for CBOR encoding).
|
|
108
|
+
*
|
|
109
|
+
* @param {Map} map - The Map instance to convert
|
|
110
|
+
* @returns {Object} A plain object with sorted keys
|
|
111
|
+
* @throws {TypeError} If any Map key is not a string
|
|
112
|
+
* @private
|
|
113
|
+
*/
|
|
114
|
+
function sortMapToObject(map) {
|
|
115
|
+
const keys = Array.from(map.keys());
|
|
116
|
+
for (const key of keys) {
|
|
117
|
+
if (typeof key !== 'string') {
|
|
118
|
+
throw new TypeError(`Map keys must be strings for CBOR encoding, got ${typeof key}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const sorted = {};
|
|
122
|
+
keys.sort();
|
|
123
|
+
for (const key of keys) {
|
|
124
|
+
sorted[key] = sortKeys(map.get(key));
|
|
125
|
+
}
|
|
126
|
+
return sorted;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Recursively sorts object keys to ensure deterministic/canonical encoding.
|
|
131
|
+
*
|
|
132
|
+
* This function transforms any JavaScript value into an equivalent structure
|
|
133
|
+
* where all object keys are sorted lexicographically. This is necessary because
|
|
134
|
+
* cbor-x encodes object keys in enumeration order, and JavaScript object key
|
|
135
|
+
* enumeration follows insertion order.
|
|
136
|
+
*
|
|
137
|
+
* ## Transformation Rules
|
|
138
|
+
*
|
|
139
|
+
* | Input Type | Output Type | Transformation |
|
|
140
|
+
* |----------------|----------------|-----------------------------------------|
|
|
141
|
+
* | `null` | `null` | Pass through |
|
|
142
|
+
* | `undefined` | `undefined` | Pass through |
|
|
143
|
+
* | Primitive | Primitive | Pass through (number, string, boolean, bigint) |
|
|
144
|
+
* | Array | Array | Elements recursively sorted |
|
|
145
|
+
* | Plain Object | Plain Object | Keys sorted, values recursively sorted |
|
|
146
|
+
* | Map | Plain Object | Converted to object with sorted keys (keys must be strings) |
|
|
147
|
+
* | Other objects | Same object | Pass through (Date, Buffer, etc.) |
|
|
148
|
+
*
|
|
149
|
+
* ## Why Only Plain Objects?
|
|
150
|
+
*
|
|
151
|
+
* We only sort keys for plain objects (`value.constructor === Object`) because:
|
|
152
|
+
* 1. Plain objects are the standard structure for WARP patches
|
|
153
|
+
* 2. Special objects (Date, Buffer, TypedArray) have specific CBOR encodings
|
|
154
|
+
* 3. Class instances may have non-enumerable or symbol keys that shouldn't be modified
|
|
155
|
+
*
|
|
156
|
+
* ## Complexity
|
|
157
|
+
*
|
|
158
|
+
* Time: O(n log n) where n is total number of keys across all nested objects
|
|
159
|
+
* Space: O(d) where d is maximum nesting depth (recursive call stack)
|
|
160
|
+
*
|
|
161
|
+
* @param {unknown} value - The value to transform. Can be any JavaScript value.
|
|
162
|
+
* @returns {unknown} A new value with all object keys sorted. Primitives are
|
|
163
|
+
* returned as-is. Objects are shallow-copied with sorted keys. Arrays are
|
|
164
|
+
* shallow-copied with transformed elements.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* // Object keys are sorted lexicographically
|
|
168
|
+
* sortKeys({ z: 1, a: 2 })
|
|
169
|
+
* // Returns: { a: 2, z: 1 }
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* // Nested objects are recursively sorted
|
|
173
|
+
* sortKeys({ outer: { z: 1, a: 2 }, b: 3 })
|
|
174
|
+
* // Returns: { b: 3, outer: { a: 2, z: 1 } }
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* // Arrays preserve order but sort object elements
|
|
178
|
+
* sortKeys([{ b: 1, a: 2 }, { d: 3, c: 4 }])
|
|
179
|
+
* // Returns: [{ a: 2, b: 1 }, { c: 4, d: 3 }]
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* // Map instances are converted to sorted objects
|
|
183
|
+
* sortKeys(new Map([['z', 1], ['a', 2]]))
|
|
184
|
+
* // Returns: { a: 2, z: 1 }
|
|
185
|
+
*
|
|
186
|
+
* @private
|
|
187
|
+
*/
|
|
188
|
+
function sortKeys(value) {
|
|
189
|
+
// Nullish values pass through
|
|
190
|
+
if (value === null || value === undefined) {
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Arrays: recursively sort elements
|
|
195
|
+
if (Array.isArray(value)) {
|
|
196
|
+
return value.map(sortKeys);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Plain objects: sort keys and recursively process values
|
|
200
|
+
if (isPlainObject(value)) {
|
|
201
|
+
return sortPlainObject(value);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Map instances: convert to sorted object
|
|
205
|
+
if (value instanceof Map) {
|
|
206
|
+
return sortMapToObject(value);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Primitive values (number, string, boolean, bigint) and other objects pass through
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Encodes data to canonical CBOR bytes with deterministic output.
|
|
215
|
+
*
|
|
216
|
+
* This function guarantees that logically equivalent inputs always produce
|
|
217
|
+
* byte-identical outputs. It achieves this by recursively sorting all object
|
|
218
|
+
* keys before encoding, ensuring consistent key ordering regardless of how
|
|
219
|
+
* the input object was constructed.
|
|
220
|
+
*
|
|
221
|
+
* ## Determinism Guarantee
|
|
222
|
+
*
|
|
223
|
+
* The following invariant always holds:
|
|
224
|
+
* ```javascript
|
|
225
|
+
* // Logically equivalent inputs produce identical bytes
|
|
226
|
+
* encode({ b: 1, a: 2 }).equals(encode({ a: 2, b: 1 })) // true
|
|
227
|
+
*
|
|
228
|
+
* // Re-encoding decoded data produces identical bytes
|
|
229
|
+
* const original = encode(data);
|
|
230
|
+
* const roundTrip = encode(decode(original));
|
|
231
|
+
* original.equals(roundTrip) // true
|
|
232
|
+
* ```
|
|
233
|
+
*
|
|
234
|
+
* ## CBOR Type Mappings
|
|
235
|
+
*
|
|
236
|
+
* | JavaScript Type | CBOR Major Type | Notes |
|
|
237
|
+
* |-----------------|-----------------|----------------------------------|
|
|
238
|
+
* | number (int) | 0 or 1 | Positive or negative integer |
|
|
239
|
+
* | number (float) | 7 | IEEE 754 float |
|
|
240
|
+
* | string | 3 | UTF-8 text string |
|
|
241
|
+
* | boolean | 7 | Simple value (true=21, false=20)|
|
|
242
|
+
* | null | 7 | Simple value (null=22) |
|
|
243
|
+
* | undefined | 7 | Simple value (undefined=23) |
|
|
244
|
+
* | Array | 4 | Array of data items |
|
|
245
|
+
* | Object | 5 | Map of pairs (keys sorted) |
|
|
246
|
+
* | Buffer | 2 | Byte string |
|
|
247
|
+
* | BigInt | 0, 1, or 6 | Integer or tagged bignum |
|
|
248
|
+
*
|
|
249
|
+
* @param {unknown} data - The data to encode. Can be any JSON-serializable value,
|
|
250
|
+
* plus Buffer, BigInt, and other types supported by cbor-x. Objects have their
|
|
251
|
+
* keys sorted before encoding.
|
|
252
|
+
* @returns {Buffer} CBOR-encoded bytes. The buffer is a Node.js Buffer instance
|
|
253
|
+
* that can be written directly to files, network sockets, or Git objects.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* // Encode a simple patch operation
|
|
257
|
+
* const patch = {
|
|
258
|
+
* schema: 2,
|
|
259
|
+
* ops: [
|
|
260
|
+
* { op: 'NodeAdd', id: 'user:alice', dot: ['w1', 1] },
|
|
261
|
+
* { op: 'PropSet', target: 'user:alice', key: 'name', value: 'Alice' }
|
|
262
|
+
* ]
|
|
263
|
+
* };
|
|
264
|
+
* const bytes = encode(patch);
|
|
265
|
+
* console.log(bytes.length); // ~80 bytes (much smaller than JSON)
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* // Demonstrating determinism - key order doesn't matter
|
|
269
|
+
* const bytes1 = encode({ z: 1, a: 2, m: 3 });
|
|
270
|
+
* const bytes2 = encode({ a: 2, m: 3, z: 1 });
|
|
271
|
+
* const bytes3 = encode({ m: 3, z: 1, a: 2 });
|
|
272
|
+
* console.log(bytes1.equals(bytes2)); // true
|
|
273
|
+
* console.log(bytes2.equals(bytes3)); // true
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* // Canonical encoding enables content-addressing
|
|
277
|
+
* const sha = crypto.createHash('sha256').update(encode(data)).digest('hex');
|
|
278
|
+
* // Same data always produces the same SHA
|
|
279
|
+
*/
|
|
280
|
+
export function encode(data) {
|
|
281
|
+
const sorted = sortKeys(data);
|
|
282
|
+
return encoder.encode(sorted);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Decodes CBOR bytes to a JavaScript value.
|
|
287
|
+
*
|
|
288
|
+
* This function deserializes CBOR-encoded data back into JavaScript values.
|
|
289
|
+
* It uses cbor-x's native decoder which is optimized for performance.
|
|
290
|
+
*
|
|
291
|
+
* ## CBOR to JavaScript Type Mappings
|
|
292
|
+
*
|
|
293
|
+
* | CBOR Major Type | JavaScript Type | Notes |
|
|
294
|
+
* |-----------------|-----------------|----------------------------------|
|
|
295
|
+
* | 0 (pos int) | number | Up to Number.MAX_SAFE_INTEGER |
|
|
296
|
+
* | 1 (neg int) | number | Down to Number.MIN_SAFE_INTEGER |
|
|
297
|
+
* | 2 (byte string) | Buffer | Node.js Buffer |
|
|
298
|
+
* | 3 (text string) | string | UTF-8 decoded |
|
|
299
|
+
* | 4 (array) | Array | Recursive decode |
|
|
300
|
+
* | 5 (map) | Object | Due to mapsAsObjects: true |
|
|
301
|
+
* | 6 (tagged) | varies | Depends on tag number |
|
|
302
|
+
* | 7 (simple) | varies | true, false, null, undefined |
|
|
303
|
+
*
|
|
304
|
+
* ## Large Integers
|
|
305
|
+
*
|
|
306
|
+
* CBOR integers outside JavaScript's safe integer range (2^53 - 1) are decoded
|
|
307
|
+
* as BigInt values. Ensure your code handles both number and BigInt if you
|
|
308
|
+
* expect large values.
|
|
309
|
+
*
|
|
310
|
+
* ## Round-Trip Safety
|
|
311
|
+
*
|
|
312
|
+
* Values encoded with {@link encode} can be round-tripped through decode without
|
|
313
|
+
* data loss, and re-encoding will produce byte-identical output due to our
|
|
314
|
+
* canonical encoding strategy:
|
|
315
|
+
*
|
|
316
|
+
* ```javascript
|
|
317
|
+
* const original = { foo: 'bar', count: 42 };
|
|
318
|
+
* const bytes = encode(original);
|
|
319
|
+
* const decoded = decode(bytes);
|
|
320
|
+
* const reEncoded = encode(decoded);
|
|
321
|
+
* bytes.equals(reEncoded); // true - canonical encoding is idempotent
|
|
322
|
+
* ```
|
|
323
|
+
*
|
|
324
|
+
* @param {Buffer|Uint8Array} buffer - CBOR-encoded bytes to decode. Accepts
|
|
325
|
+
* Node.js Buffer, Uint8Array, or any ArrayBufferView.
|
|
326
|
+
* @returns {unknown} The decoded JavaScript value. Type depends on the encoded
|
|
327
|
+
* CBOR data - could be a primitive, array, or plain object.
|
|
328
|
+
* @throws {Error} If the buffer contains invalid CBOR data or is truncated.
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* // Decode a CBOR-encoded patch from a Git commit message
|
|
332
|
+
* const commitMessage = await adapter.showNode(sha);
|
|
333
|
+
* const patchData = decode(Buffer.from(commitMessage, 'binary'));
|
|
334
|
+
* console.log(patchData.schema); // 2
|
|
335
|
+
* console.log(patchData.ops.length); // number of operations
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* // Error handling for invalid data
|
|
339
|
+
* try {
|
|
340
|
+
* const data = decode(Buffer.from([0xff, 0xff])); // Invalid CBOR
|
|
341
|
+
* } catch (err) {
|
|
342
|
+
* console.error('Invalid CBOR:', err.message);
|
|
343
|
+
* }
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* // Handling large integers
|
|
347
|
+
* const bytes = encode({ bigNum: BigInt('9007199254740993') }); // > MAX_SAFE_INTEGER
|
|
348
|
+
* const decoded = decode(bytes);
|
|
349
|
+
* console.log(typeof decoded.bigNum); // 'bigint'
|
|
350
|
+
*/
|
|
351
|
+
export function decode(buffer) {
|
|
352
|
+
return cborDecode(buffer);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Default export providing both encode and decode functions.
|
|
357
|
+
*
|
|
358
|
+
* @type {{ encode: typeof encode, decode: typeof decode }}
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* import cbor from './CborCodec.js';
|
|
362
|
+
*
|
|
363
|
+
* const bytes = cbor.encode({ key: 'value' });
|
|
364
|
+
* const data = cbor.decode(bytes);
|
|
365
|
+
*/
|
|
366
|
+
/**
|
|
367
|
+
* CBOR codec implementing CodecPort with canonical/deterministic encoding.
|
|
368
|
+
*
|
|
369
|
+
* @class CborCodec
|
|
370
|
+
* @extends CodecPort
|
|
371
|
+
*/
|
|
372
|
+
export class CborCodec extends CodecPort {
|
|
373
|
+
/** @inheritdoc */
|
|
374
|
+
encode(data) {
|
|
375
|
+
return encode(data);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** @inheritdoc */
|
|
379
|
+
decode(buffer) {
|
|
380
|
+
return decode(buffer);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export default new CborCodec();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port for Git blob operations.
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract for writing and reading Git blob objects.
|
|
5
|
+
* This is one of five focused ports extracted from GraphPersistencePort.
|
|
6
|
+
*
|
|
7
|
+
* @abstract
|
|
8
|
+
* @see GraphPersistencePort - Composite port implementing all five focused ports
|
|
9
|
+
*/
|
|
10
|
+
export default class BlobPort {
|
|
11
|
+
/**
|
|
12
|
+
* Writes content as a Git blob and returns its OID.
|
|
13
|
+
* @param {Buffer|string} content - The blob content to write
|
|
14
|
+
* @returns {Promise<string>} The Git OID of the created blob
|
|
15
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
16
|
+
*/
|
|
17
|
+
async writeBlob(_content) {
|
|
18
|
+
throw new Error('BlobPort.writeBlob() not implemented');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reads the content of a Git blob.
|
|
23
|
+
* @param {string} oid - The blob OID to read
|
|
24
|
+
* @returns {Promise<Buffer>} The blob content
|
|
25
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
26
|
+
*/
|
|
27
|
+
async readBlob(_oid) {
|
|
28
|
+
throw new Error('BlobPort.readBlob() not implemented');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port for time-related operations.
|
|
3
|
+
*
|
|
4
|
+
* Abstracts platform-specific timing APIs to keep domain services pure.
|
|
5
|
+
* Implementations can use performance.now(), Date.now(), or test doubles.
|
|
6
|
+
*/
|
|
7
|
+
export default class ClockPort {
|
|
8
|
+
/**
|
|
9
|
+
* Returns a high-resolution timestamp in milliseconds.
|
|
10
|
+
* Used for measuring durations (latency, elapsed time).
|
|
11
|
+
* @returns {number} Timestamp in milliseconds
|
|
12
|
+
*/
|
|
13
|
+
now() {
|
|
14
|
+
throw new Error('Not implemented');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns the current wall-clock time as an ISO string.
|
|
19
|
+
* Used for timestamps in logs and cached results.
|
|
20
|
+
* @returns {string} ISO 8601 timestamp
|
|
21
|
+
*/
|
|
22
|
+
timestamp() {
|
|
23
|
+
throw new Error('Not implemented');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port for serialization/deserialization operations.
|
|
3
|
+
*
|
|
4
|
+
* Abstracts codec implementations to allow substitution of
|
|
5
|
+
* CBOR, JSON, MessagePack, or other formats.
|
|
6
|
+
*/
|
|
7
|
+
export default class CodecPort {
|
|
8
|
+
/**
|
|
9
|
+
* Encodes data to binary format.
|
|
10
|
+
* @param {unknown} data - Data to encode
|
|
11
|
+
* @returns {Buffer|Uint8Array} Encoded bytes
|
|
12
|
+
*/
|
|
13
|
+
encode(_data) {
|
|
14
|
+
throw new Error('CodecPort.encode() not implemented');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Decodes binary data back to a JavaScript value.
|
|
19
|
+
* @param {Buffer|Uint8Array} bytes - Encoded bytes to decode
|
|
20
|
+
* @returns {unknown} Decoded value
|
|
21
|
+
*/
|
|
22
|
+
decode(_bytes) {
|
|
23
|
+
throw new Error('CodecPort.decode() not implemented');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port for Git commit operations.
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract for creating, reading, and querying Git commits.
|
|
5
|
+
* This is one of five focused ports extracted from GraphPersistencePort.
|
|
6
|
+
*
|
|
7
|
+
* @abstract
|
|
8
|
+
* @see GraphPersistencePort - Composite port implementing all five focused ports
|
|
9
|
+
*/
|
|
10
|
+
export default class CommitPort {
|
|
11
|
+
/**
|
|
12
|
+
* Creates a commit pointing to the empty tree.
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {string} options.message - The commit message (typically CBOR-encoded patch data)
|
|
15
|
+
* @param {string[]} [options.parents=[]] - Parent commit SHAs for the commit graph
|
|
16
|
+
* @param {boolean} [options.sign=false] - Whether to GPG-sign the commit
|
|
17
|
+
* @returns {Promise<string>} The SHA of the created commit
|
|
18
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
19
|
+
*/
|
|
20
|
+
async commitNode(_options) {
|
|
21
|
+
throw new Error('CommitPort.commitNode() not implemented');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Retrieves the raw commit message for a given SHA.
|
|
26
|
+
* @param {string} sha - The commit SHA to read
|
|
27
|
+
* @returns {Promise<string>} The raw commit message content
|
|
28
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
29
|
+
*/
|
|
30
|
+
async showNode(_sha) {
|
|
31
|
+
throw new Error('CommitPort.showNode() not implemented');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Gets full commit metadata for a node.
|
|
36
|
+
* @param {string} sha - The commit SHA to retrieve
|
|
37
|
+
* @returns {Promise<{sha: string, message: string, author: string, date: string, parents: string[]}>}
|
|
38
|
+
* Full commit metadata including SHA, message, author, date, and parent SHAs
|
|
39
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
40
|
+
*/
|
|
41
|
+
async getNodeInfo(_sha) {
|
|
42
|
+
throw new Error('CommitPort.getNodeInfo() not implemented');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns raw git log output for a ref.
|
|
47
|
+
* @param {Object} options
|
|
48
|
+
* @param {string} options.ref - The Git ref to log from
|
|
49
|
+
* @param {number} [options.limit=50] - Maximum number of commits to return
|
|
50
|
+
* @param {string} [options.format] - Custom format string for git log
|
|
51
|
+
* @returns {Promise<string>} The raw log output
|
|
52
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
53
|
+
*/
|
|
54
|
+
async logNodes(_options) {
|
|
55
|
+
throw new Error('CommitPort.logNodes() not implemented');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Streams git log output for a ref.
|
|
60
|
+
* @param {Object} options
|
|
61
|
+
* @param {string} options.ref - The Git ref to log from
|
|
62
|
+
* @param {number} [options.limit=1000000] - Maximum number of commits to return
|
|
63
|
+
* @param {string} [options.format] - Custom format string for git log
|
|
64
|
+
* @returns {Promise<import('node:stream').Readable>} A readable stream of log output
|
|
65
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
66
|
+
*/
|
|
67
|
+
async logNodesStream(_options) {
|
|
68
|
+
throw new Error('CommitPort.logNodesStream() not implemented');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Counts nodes reachable from a ref without loading them into memory.
|
|
73
|
+
* @param {string} ref - Git ref to count from (e.g., 'HEAD', 'main', or a SHA)
|
|
74
|
+
* @returns {Promise<number>} The count of reachable nodes
|
|
75
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
76
|
+
*/
|
|
77
|
+
async countNodes(_ref) {
|
|
78
|
+
throw new Error('CommitPort.countNodes() not implemented');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates a commit pointing to a specified tree (not the empty tree).
|
|
83
|
+
* Used by CheckpointService and PatchBuilderV2 for tree-backed commits.
|
|
84
|
+
* @param {Object} options
|
|
85
|
+
* @param {string} options.treeOid - The tree OID to commit
|
|
86
|
+
* @param {string[]} [options.parents=[]] - Parent commit SHAs
|
|
87
|
+
* @param {string} options.message - The commit message
|
|
88
|
+
* @param {boolean} [options.sign=false] - Whether to GPG-sign the commit
|
|
89
|
+
* @returns {Promise<string>} The SHA of the created commit
|
|
90
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
91
|
+
*/
|
|
92
|
+
async commitNodeWithTree(_options) {
|
|
93
|
+
throw new Error('CommitPort.commitNodeWithTree() not implemented');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Checks whether a commit exists in the repository.
|
|
98
|
+
* @param {string} sha - The commit SHA to check
|
|
99
|
+
* @returns {Promise<boolean>} True if the commit exists
|
|
100
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
101
|
+
*/
|
|
102
|
+
async nodeExists(_sha) {
|
|
103
|
+
throw new Error('CommitPort.nodeExists() not implemented');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Pings the repository to verify accessibility.
|
|
108
|
+
* @returns {Promise<{ok: boolean, latencyMs: number}>} Health check result with latency
|
|
109
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
110
|
+
*/
|
|
111
|
+
async ping() {
|
|
112
|
+
throw new Error('CommitPort.ping() not implemented');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port for Git config operations.
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract for reading and writing Git configuration values.
|
|
5
|
+
* This is one of five focused ports extracted from GraphPersistencePort.
|
|
6
|
+
*
|
|
7
|
+
* @abstract
|
|
8
|
+
* @see GraphPersistencePort - Composite port implementing all five focused ports
|
|
9
|
+
*/
|
|
10
|
+
export default class ConfigPort {
|
|
11
|
+
/**
|
|
12
|
+
* Reads a git config value.
|
|
13
|
+
* @param {string} key - The config key to read (e.g., 'warp.writerId.events')
|
|
14
|
+
* @returns {Promise<string|null>} The config value, or null if not set
|
|
15
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
16
|
+
*/
|
|
17
|
+
async configGet(_key) {
|
|
18
|
+
throw new Error('ConfigPort.configGet() not implemented');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sets a git config value.
|
|
23
|
+
* @param {string} key - The config key to set (e.g., 'warp.writerId.events')
|
|
24
|
+
* @param {string} value - The value to set
|
|
25
|
+
* @returns {Promise<void>}
|
|
26
|
+
* @throws {Error} If not implemented by a concrete adapter
|
|
27
|
+
*/
|
|
28
|
+
async configSet(_key, _value) {
|
|
29
|
+
throw new Error('ConfigPort.configSet() not implemented');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port for cryptographic operations.
|
|
3
|
+
*
|
|
4
|
+
* Abstracts platform-specific crypto APIs to keep domain services pure.
|
|
5
|
+
* Implementations can use Node.js crypto, Web Crypto API, or test doubles.
|
|
6
|
+
*/
|
|
7
|
+
export default class CryptoPort {
|
|
8
|
+
/**
|
|
9
|
+
* Computes a hash digest of the given data.
|
|
10
|
+
* @param {string} algorithm - Hash algorithm (e.g. 'sha1', 'sha256')
|
|
11
|
+
* @param {string|Buffer|Uint8Array} data - Data to hash
|
|
12
|
+
* @returns {Promise<string>} Hex-encoded digest
|
|
13
|
+
*/
|
|
14
|
+
async hash(_algorithm, _data) {
|
|
15
|
+
throw new Error('CryptoPort.hash() not implemented');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Computes an HMAC of the given data.
|
|
20
|
+
* @param {string} algorithm - Hash algorithm (e.g. 'sha256')
|
|
21
|
+
* @param {string|Buffer|Uint8Array} key - HMAC key
|
|
22
|
+
* @param {string|Buffer|Uint8Array} data - Data to authenticate
|
|
23
|
+
* @returns {Promise<Buffer|Uint8Array>} Raw HMAC digest
|
|
24
|
+
*/
|
|
25
|
+
async hmac(_algorithm, _key, _data) {
|
|
26
|
+
throw new Error('CryptoPort.hmac() not implemented');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Constant-time comparison of two buffers.
|
|
31
|
+
* @param {Buffer|Uint8Array} a - First buffer
|
|
32
|
+
* @param {Buffer|Uint8Array} b - Second buffer
|
|
33
|
+
* @returns {boolean} True if buffers are equal
|
|
34
|
+
*/
|
|
35
|
+
timingSafeEqual(_a, _b) {
|
|
36
|
+
throw new Error('CryptoPort.timingSafeEqual() not implemented');
|
|
37
|
+
}
|
|
38
|
+
}
|