@coderook/cli 0.46.1 → 0.46.3
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/.claude-plugin/plugin.json +1 -1
- package/dist/cbx/src/core/upload.js +583 -70
- package/dist/cbx/src/local/history.js +27 -4
- package/dist/cbx/src/local/remote.js +15 -0
- package/dist/cbx/src/local/repository.js +90 -11
- package/dist/cbx/src/local/snapshot.js +110 -9
- package/dist/cbx/src/local/store.js +365 -9
- package/dist/cbx/src/local/store_v2.js +269 -0
- package/dist/cbx/src/shared/delta.js +109 -4
- package/dist/cbx/src/shared/native_engine.js +23 -0
- package/dist/cli/src/bin.js +21 -0
- package/dist/cli/src/cli.js +13 -0
- package/dist/cli/src/local_status.js +18 -33
- package/dist/cli/src/paint.js +16 -0
- package/dist/cli/src/status_lines.js +26 -0
- package/engine-bin/darwin-arm64/cbx-engine +0 -0
- package/engine-bin/darwin-x64/cbx-engine +0 -0
- package/engine-bin/linux-arm64/cbx-engine +0 -0
- package/engine-bin/linux-x64/cbx-engine +0 -0
- package/engine-bin/win32-x64/cbx-engine.exe +0 -0
- package/package.json +1 -1
|
@@ -30,6 +30,8 @@ const node_crypto_1 = require("node:crypto");
|
|
|
30
30
|
const promises_1 = require("node:fs/promises");
|
|
31
31
|
const node_path_1 = __importDefault(require("node:path"));
|
|
32
32
|
const cbx_js_1 = require("../core/cbx.js");
|
|
33
|
+
const delta_js_1 = require("../shared/delta.js");
|
|
34
|
+
const store_v2_js_1 = require("./store_v2.js");
|
|
33
35
|
exports.DIGEST = /^[0-9a-f]{64}$/;
|
|
34
36
|
/** Objects at most this big are packed rather than written on their own. */
|
|
35
37
|
exports.PACKED_OBJECT_LIMIT = 256 * 1024;
|
|
@@ -38,6 +40,58 @@ const PACK_BYTES = 32 * 1024 * 1024;
|
|
|
38
40
|
function digestOf(bytes) {
|
|
39
41
|
return (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex");
|
|
40
42
|
}
|
|
43
|
+
/*
|
|
44
|
+
Every v2 index, searched in place. A lookup is a binary search per pack,
|
|
45
|
+
newest first, which is nothing for the few a status makes; a save or a
|
|
46
|
+
restore makes thousands, and once the searches would cost more than
|
|
47
|
+
decoding everything once, everything is decoded into one map.
|
|
48
|
+
*/
|
|
49
|
+
class V2Packs {
|
|
50
|
+
files = [];
|
|
51
|
+
map = null;
|
|
52
|
+
searches = 0;
|
|
53
|
+
add(file) {
|
|
54
|
+
this.files.push(file);
|
|
55
|
+
if (this.map)
|
|
56
|
+
for (const name of file.names())
|
|
57
|
+
this.map.set(name, { ...file.find(name), pack: file.pack });
|
|
58
|
+
}
|
|
59
|
+
get(digest) {
|
|
60
|
+
if (this.map)
|
|
61
|
+
return this.map.get(digest);
|
|
62
|
+
this.searches += this.files.length;
|
|
63
|
+
if (this.searches > 200_000) {
|
|
64
|
+
this.map = new Map();
|
|
65
|
+
for (const file of this.files) {
|
|
66
|
+
for (const name of file.names())
|
|
67
|
+
this.map.set(name, { ...file.find(name), pack: file.pack });
|
|
68
|
+
}
|
|
69
|
+
return this.map.get(digest);
|
|
70
|
+
}
|
|
71
|
+
for (let index = this.files.length - 1; index >= 0; index -= 1) {
|
|
72
|
+
const found = this.files[index].find(digest);
|
|
73
|
+
if (found)
|
|
74
|
+
return { ...found, pack: this.files[index].pack };
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
has(digest) {
|
|
79
|
+
return this.get(digest) !== undefined;
|
|
80
|
+
}
|
|
81
|
+
*keys() {
|
|
82
|
+
for (const file of this.files)
|
|
83
|
+
yield* file.names();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/*
|
|
87
|
+
The largest object kept as a delta: the biggest piece any chunk profile
|
|
88
|
+
cuts (32 MiB). The transport stops at 8 MiB, and using its limit here kept
|
|
89
|
+
every edited piece of a large file whole again: JDK's 130 MB module image
|
|
90
|
+
is cut into pieces of up to 16 MiB.
|
|
91
|
+
*/
|
|
92
|
+
const LOCAL_DELTA_MAX_BYTES = 32 * 1024 * 1024;
|
|
93
|
+
/* Solid blocks kept decompressed, so restoring the files of one block reads it once. */
|
|
94
|
+
const BLOCKS_KEPT_BYTES = 160 * 1024 * 1024;
|
|
41
95
|
class ObjectStore {
|
|
42
96
|
directory;
|
|
43
97
|
/* Fan-out folders already known to exist, so each is made once, not per object. */
|
|
@@ -57,8 +111,20 @@ class ObjectStore {
|
|
|
57
111
|
because Windows will not delete a folder holding an open file.
|
|
58
112
|
*/
|
|
59
113
|
handles = new Map();
|
|
60
|
-
|
|
114
|
+
/*
|
|
115
|
+
A cbx-local 2 history (store_v2.ts): the same objects, kept as solid
|
|
116
|
+
blocks and deltas in v2 packs. A v1 history never writes them, but every
|
|
117
|
+
store reads both, so upgrading changes nothing already written.
|
|
118
|
+
*/
|
|
119
|
+
v2;
|
|
120
|
+
v2Located = null;
|
|
121
|
+
pendingV2 = new Map();
|
|
122
|
+
writingV2 = new Map();
|
|
123
|
+
pendingV2Bytes = 0;
|
|
124
|
+
blocks = new Map();
|
|
125
|
+
constructor(directory, options = {}) {
|
|
61
126
|
this.directory = directory;
|
|
127
|
+
this.v2 = options.v2 === true;
|
|
62
128
|
}
|
|
63
129
|
get packDirectory() {
|
|
64
130
|
return node_path_1.default.join(this.directory, "packs");
|
|
@@ -68,9 +134,21 @@ class ObjectStore {
|
|
|
68
134
|
throw new Error(`Not an object name: ${digest}`);
|
|
69
135
|
return node_path_1.default.join(this.directory, digest.slice(0, 2), digest.slice(2));
|
|
70
136
|
}
|
|
137
|
+
/*
|
|
138
|
+
Read once, by one caller: the lanes of a save ask at the same moment, and
|
|
139
|
+
a second caller that found the v1 list set while the v2 list was still
|
|
140
|
+
loading went on with no v2 list at all.
|
|
141
|
+
*/
|
|
142
|
+
loadingPacks = null;
|
|
71
143
|
async packs() {
|
|
72
|
-
if (this.packed)
|
|
144
|
+
if (this.packed && this.v2Located)
|
|
73
145
|
return this.packed;
|
|
146
|
+
this.loadingPacks ??= this.loadPacks().finally(() => {
|
|
147
|
+
this.loadingPacks = null;
|
|
148
|
+
});
|
|
149
|
+
return this.loadingPacks;
|
|
150
|
+
}
|
|
151
|
+
async loadPacks() {
|
|
74
152
|
const found = new Map();
|
|
75
153
|
let names = [];
|
|
76
154
|
try {
|
|
@@ -86,9 +164,33 @@ class ObjectStore {
|
|
|
86
164
|
found.set(digest, { pack, offset, length });
|
|
87
165
|
}
|
|
88
166
|
}
|
|
167
|
+
const v2 = await this.readV2Indexes();
|
|
89
168
|
this.packed = found;
|
|
169
|
+
this.v2Located = v2;
|
|
170
|
+
return found;
|
|
171
|
+
}
|
|
172
|
+
get v2Directory() {
|
|
173
|
+
return node_path_1.default.join(this.directory, "v2");
|
|
174
|
+
}
|
|
175
|
+
async readV2Indexes() {
|
|
176
|
+
const found = new V2Packs();
|
|
177
|
+
let names = [];
|
|
178
|
+
try {
|
|
179
|
+
names = await (0, promises_1.readdir)(this.v2Directory);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
/* No v2 packs. */
|
|
183
|
+
}
|
|
184
|
+
for (const name of names.filter((one) => one.endsWith(".idx")).sort()) {
|
|
185
|
+
const pack = node_path_1.default.join(this.v2Directory, name.replace(/\.idx$/, ".pack"));
|
|
186
|
+
found.add(new store_v2_js_1.V2IndexFile(pack, await (0, promises_1.readFile)(node_path_1.default.join(this.v2Directory, name))));
|
|
187
|
+
}
|
|
90
188
|
return found;
|
|
91
189
|
}
|
|
190
|
+
async v2Index() {
|
|
191
|
+
await this.packs();
|
|
192
|
+
return this.v2Located;
|
|
193
|
+
}
|
|
92
194
|
async loose(digest) {
|
|
93
195
|
try {
|
|
94
196
|
await (0, promises_1.stat)(this.where(digest));
|
|
@@ -101,10 +203,59 @@ class ObjectStore {
|
|
|
101
203
|
async has(digest) {
|
|
102
204
|
if (this.pending.has(digest) || this.writing.has(digest))
|
|
103
205
|
return true;
|
|
206
|
+
if (this.pendingV2.has(digest) || this.writingV2.has(digest))
|
|
207
|
+
return true;
|
|
104
208
|
if ((await this.packs()).has(digest))
|
|
105
209
|
return true;
|
|
210
|
+
if (this.v2Located.has(digest))
|
|
211
|
+
return true;
|
|
106
212
|
return this.loose(digest);
|
|
107
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* How many deltas stand under an object, or null when it is not held.
|
|
216
|
+
* Anything kept whole, in a v1 store or loose, stands on nothing.
|
|
217
|
+
*/
|
|
218
|
+
async chainOf(digest) {
|
|
219
|
+
const waiting = this.pendingV2.get(digest) ?? this.writingV2.get(digest);
|
|
220
|
+
if (waiting)
|
|
221
|
+
return waiting.solid ? 0 : waiting.chain;
|
|
222
|
+
const entry = (await this.v2Index()).get(digest);
|
|
223
|
+
if (entry)
|
|
224
|
+
return entry.chain;
|
|
225
|
+
return (await this.has(digest)) ? 0 : null;
|
|
226
|
+
}
|
|
227
|
+
/** An object's own length without rebuilding it, or null when it is not held. */
|
|
228
|
+
async rawSize(digest) {
|
|
229
|
+
const waiting = this.pendingV2.get(digest) ?? this.writingV2.get(digest);
|
|
230
|
+
if (waiting)
|
|
231
|
+
return waiting.raw.length;
|
|
232
|
+
const entry = (await this.v2Index()).get(digest);
|
|
233
|
+
if (entry)
|
|
234
|
+
return entry.raw;
|
|
235
|
+
const frame = this.pending.get(digest) ?? this.writing.get(digest);
|
|
236
|
+
if (frame)
|
|
237
|
+
return Number(frame.readBigUInt64LE(8));
|
|
238
|
+
const located = this.packed.get(digest);
|
|
239
|
+
try {
|
|
240
|
+
const head = Buffer.alloc(16);
|
|
241
|
+
if (located) {
|
|
242
|
+
await (await this.handleFor(located.pack)).read(head, 0, 16, located.offset);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const handle = await (0, promises_1.open)(this.where(digest), "r");
|
|
246
|
+
try {
|
|
247
|
+
await handle.read(head, 0, 16, 0);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
await handle.close();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return Number(head.readBigUInt64LE(8));
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
108
259
|
/**
|
|
109
260
|
* Keep `raw`, returning its name.
|
|
110
261
|
*
|
|
@@ -118,6 +269,10 @@ class ObjectStore {
|
|
|
118
269
|
const digest = digestOf(raw);
|
|
119
270
|
if (await this.has(digest))
|
|
120
271
|
return digest;
|
|
272
|
+
if (this.v2 && !options.loose) {
|
|
273
|
+
await this.putV2(raw, digest, options.base, options.path, options.maxChain ?? store_v2_js_1.MAX_CHAIN);
|
|
274
|
+
return digest;
|
|
275
|
+
}
|
|
121
276
|
const { frame } = (0, cbx_js_1.chunkFrame)(raw);
|
|
122
277
|
if (!options.loose && raw.length <= exports.PACKED_OBJECT_LIMIT) {
|
|
123
278
|
this.pending.set(digest, frame);
|
|
@@ -150,6 +305,17 @@ class ObjectStore {
|
|
|
150
305
|
/* One pack at a time; a second caller waits for the first and goes again. */
|
|
151
306
|
while (this.flushing)
|
|
152
307
|
await this.flushing;
|
|
308
|
+
if (this.pendingV2.size) {
|
|
309
|
+
const taken = this.pendingV2;
|
|
310
|
+
this.pendingV2 = new Map();
|
|
311
|
+
this.pendingV2Bytes = 0;
|
|
312
|
+
this.writingV2 = taken;
|
|
313
|
+
this.flushing = this.writeV2Pack(taken).finally(() => {
|
|
314
|
+
this.writingV2 = new Map();
|
|
315
|
+
this.flushing = null;
|
|
316
|
+
});
|
|
317
|
+
await this.flushing;
|
|
318
|
+
}
|
|
153
319
|
if (!this.pending.size)
|
|
154
320
|
return;
|
|
155
321
|
const taken = this.pending;
|
|
@@ -162,6 +328,190 @@ class ObjectStore {
|
|
|
162
328
|
});
|
|
163
329
|
await this.flushing;
|
|
164
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Keep one object in a v2 store: as a delta against `base` when that is
|
|
333
|
+
* clearly smaller than the object compressed on its own and the base does
|
|
334
|
+
* not already stand on too many others; otherwise joined into a solid
|
|
335
|
+
* block when small, or whole.
|
|
336
|
+
*/
|
|
337
|
+
async putV2(raw, digest, base, filePath, maxChain) {
|
|
338
|
+
if (base && base !== digest && raw.length >= 64 && raw.length <= LOCAL_DELTA_MAX_BYTES) {
|
|
339
|
+
const depth = await this.chainOf(base);
|
|
340
|
+
if (depth !== null && depth + 1 < Math.min(maxChain, store_v2_js_1.MAX_CHAIN) + 1) {
|
|
341
|
+
let from = null;
|
|
342
|
+
try {
|
|
343
|
+
from = await this.get(base);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
from = null;
|
|
347
|
+
}
|
|
348
|
+
if (from && from.length <= LOCAL_DELTA_MAX_BYTES) {
|
|
349
|
+
const patch = Buffer.from((0, delta_js_1.createExactDeltaPatch)(from, raw, LOCAL_DELTA_MAX_BYTES));
|
|
350
|
+
const squeezed = patch.length > 64 ? (0, store_v2_js_1.zstdAt)(patch, 3) : patch;
|
|
351
|
+
const packed = squeezed.length < patch.length;
|
|
352
|
+
const record = packed ? squeezed : patch;
|
|
353
|
+
/* Against what the object costs compressed on its own, not its raw size. */
|
|
354
|
+
const alone = Math.min(raw.length, (0, store_v2_js_1.zstdAt)(raw, 1).length);
|
|
355
|
+
if (record.length < alone / 2 && alone - record.length >= 96) {
|
|
356
|
+
await this.holdV2(digest, {
|
|
357
|
+
solid: false,
|
|
358
|
+
raw,
|
|
359
|
+
record,
|
|
360
|
+
kind: packed ? store_v2_js_1.KIND_DELTA_ZSTD : store_v2_js_1.KIND_DELTA,
|
|
361
|
+
chain: depth + 1,
|
|
362
|
+
base,
|
|
363
|
+
});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (raw.length <= store_v2_js_1.SOLID_OBJECT_LIMIT) {
|
|
370
|
+
await this.holdV2(digest, { solid: true, raw, key: (0, store_v2_js_1.solidKey)(filePath), large: false });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
/* Larger and compressible: joined with others of its kind. Otherwise kept as it is. */
|
|
374
|
+
if ((0, store_v2_js_1.compresses)(raw)) {
|
|
375
|
+
await this.holdV2(digest, { solid: true, raw, key: (0, store_v2_js_1.solidKey)(filePath), large: true });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
await this.holdV2(digest, { solid: false, raw, record: raw, kind: store_v2_js_1.KIND_WHOLE, chain: 0 });
|
|
379
|
+
}
|
|
380
|
+
async holdV2(digest, held) {
|
|
381
|
+
this.pendingV2.set(digest, held);
|
|
382
|
+
this.pendingV2Bytes += held.raw.length;
|
|
383
|
+
/* Written when there is a pack's worth; callers flush at the end of a save either way. */
|
|
384
|
+
if (this.pendingV2Bytes >= store_v2_js_1.V2_PACK_BYTES)
|
|
385
|
+
await this.flush();
|
|
386
|
+
}
|
|
387
|
+
async writeV2Pack(objects) {
|
|
388
|
+
await (0, promises_1.mkdir)(this.v2Directory, { recursive: true });
|
|
389
|
+
const entries = new Map();
|
|
390
|
+
const records = [];
|
|
391
|
+
let offset = 0;
|
|
392
|
+
const solid = [...objects]
|
|
393
|
+
.filter((pair) => pair[1].solid)
|
|
394
|
+
.sort(([leftName, left], [rightName, right]) => left.key < right.key ? -1 : left.key > right.key ? 1 : leftName < rightName ? -1 : 1);
|
|
395
|
+
let block = [];
|
|
396
|
+
let blockBytes = 0;
|
|
397
|
+
let blockLarge = false;
|
|
398
|
+
const closeBlock = () => {
|
|
399
|
+
if (!block.length)
|
|
400
|
+
return;
|
|
401
|
+
const joined = Buffer.concat(block.map(([, raw]) => raw));
|
|
402
|
+
const stored = (0, store_v2_js_1.zstdAt)(joined, 9, true, blockLarge ? store_v2_js_1.LARGE_WINDOW_LOG : undefined, true);
|
|
403
|
+
let inner = 0;
|
|
404
|
+
for (const [digest, raw] of block) {
|
|
405
|
+
entries.set(digest, { kind: store_v2_js_1.KIND_SOLID, chain: 0, offset, length: stored.length, inner, raw: raw.length });
|
|
406
|
+
inner += raw.length;
|
|
407
|
+
}
|
|
408
|
+
records.push(stored);
|
|
409
|
+
offset += stored.length;
|
|
410
|
+
block = [];
|
|
411
|
+
blockBytes = 0;
|
|
412
|
+
};
|
|
413
|
+
/* Small objects first, then large: the two tiers never share a block. */
|
|
414
|
+
for (const large of [false, true]) {
|
|
415
|
+
const limit = large ? store_v2_js_1.LARGE_BLOCK_BYTES : store_v2_js_1.SOLID_BLOCK_BYTES;
|
|
416
|
+
blockLarge = large;
|
|
417
|
+
for (const [digest, held] of solid) {
|
|
418
|
+
if (held.large !== large)
|
|
419
|
+
continue;
|
|
420
|
+
if (blockBytes && blockBytes + held.raw.length > limit)
|
|
421
|
+
closeBlock();
|
|
422
|
+
block.push([digest, held.raw]);
|
|
423
|
+
blockBytes += held.raw.length;
|
|
424
|
+
}
|
|
425
|
+
closeBlock();
|
|
426
|
+
}
|
|
427
|
+
for (const [digest, held] of objects) {
|
|
428
|
+
if (held.solid)
|
|
429
|
+
continue;
|
|
430
|
+
entries.set(digest, {
|
|
431
|
+
kind: held.kind,
|
|
432
|
+
chain: held.chain,
|
|
433
|
+
offset,
|
|
434
|
+
length: held.record.length,
|
|
435
|
+
inner: 0,
|
|
436
|
+
raw: held.raw.length,
|
|
437
|
+
...(held.base ? { base: held.base } : {}),
|
|
438
|
+
});
|
|
439
|
+
records.push(held.record);
|
|
440
|
+
offset += held.record.length;
|
|
441
|
+
}
|
|
442
|
+
const body = Buffer.concat(records);
|
|
443
|
+
const id = digestOf(body);
|
|
444
|
+
const pack = node_path_1.default.join(this.v2Directory, `${id}.pack`);
|
|
445
|
+
const idx = node_path_1.default.join(this.v2Directory, `${id}.idx`);
|
|
446
|
+
await (0, promises_1.writeFile)(`${pack}.partial`, body);
|
|
447
|
+
await (0, promises_1.rename)(`${pack}.partial`, pack);
|
|
448
|
+
const encoded = (0, store_v2_js_1.encodeIndex)(entries);
|
|
449
|
+
await (0, promises_1.writeFile)(`${idx}.partial`, encoded);
|
|
450
|
+
await (0, promises_1.rename)(`${idx}.partial`, idx);
|
|
451
|
+
(await this.v2Index()).add(new store_v2_js_1.V2IndexFile(pack, encoded));
|
|
452
|
+
}
|
|
453
|
+
handleFor(pack) {
|
|
454
|
+
let opening = this.handles.get(pack);
|
|
455
|
+
if (!opening) {
|
|
456
|
+
opening = (0, promises_1.open)(pack, "r");
|
|
457
|
+
this.handles.set(pack, opening);
|
|
458
|
+
}
|
|
459
|
+
return opening;
|
|
460
|
+
}
|
|
461
|
+
async readRecord(entry) {
|
|
462
|
+
const record = Buffer.alloc(entry.length);
|
|
463
|
+
const { bytesRead } = await (await this.handleFor(entry.pack)).read(record, 0, entry.length, entry.offset);
|
|
464
|
+
if (bytesRead !== entry.length)
|
|
465
|
+
throw new Error("A v2 pack ends before one of its records does");
|
|
466
|
+
return record;
|
|
467
|
+
}
|
|
468
|
+
/** Rebuild one object from a v2 pack and check it against its name. */
|
|
469
|
+
async getV2(digest, entry) {
|
|
470
|
+
let raw;
|
|
471
|
+
try {
|
|
472
|
+
if (entry.kind === store_v2_js_1.KIND_SOLID) {
|
|
473
|
+
const key = `${entry.pack}:${entry.offset}`;
|
|
474
|
+
let block = this.blocks.get(key);
|
|
475
|
+
if (block) {
|
|
476
|
+
this.blocks.delete(key);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
block = (0, store_v2_js_1.unzstd)(await this.readRecord(entry));
|
|
480
|
+
}
|
|
481
|
+
this.blocks.set(key, block);
|
|
482
|
+
let kept = 0;
|
|
483
|
+
for (const one of this.blocks.values())
|
|
484
|
+
kept += one.length;
|
|
485
|
+
while (kept > BLOCKS_KEPT_BYTES && this.blocks.size > 1) {
|
|
486
|
+
const oldest = this.blocks.keys().next().value;
|
|
487
|
+
kept -= this.blocks.get(oldest).length;
|
|
488
|
+
this.blocks.delete(oldest);
|
|
489
|
+
}
|
|
490
|
+
if (entry.inner + entry.raw > block.length)
|
|
491
|
+
throw new Error("it reaches outside its block");
|
|
492
|
+
raw = Buffer.from(block.subarray(entry.inner, entry.inner + entry.raw));
|
|
493
|
+
}
|
|
494
|
+
else if (entry.kind === store_v2_js_1.KIND_WHOLE || entry.kind === store_v2_js_1.KIND_WHOLE_ZSTD) {
|
|
495
|
+
const record = await this.readRecord(entry);
|
|
496
|
+
raw = entry.kind === store_v2_js_1.KIND_WHOLE ? record : (0, store_v2_js_1.unzstd)(record);
|
|
497
|
+
}
|
|
498
|
+
else if (entry.kind === store_v2_js_1.KIND_DELTA || entry.kind === store_v2_js_1.KIND_DELTA_ZSTD) {
|
|
499
|
+
const record = await this.readRecord(entry);
|
|
500
|
+
const patch = entry.kind === store_v2_js_1.KIND_DELTA ? record : (0, store_v2_js_1.unzstd)(record);
|
|
501
|
+
raw = Buffer.from((0, delta_js_1.applyDeltaPatch)(await this.get(entry.base), patch, LOCAL_DELTA_MAX_BYTES));
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
throw new Error("it is of a kind this cbx does not read");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
throw new Error(`Object ${digest.slice(0, 12)} could not be rebuilt: ${error instanceof Error ? error.message : String(error)}`);
|
|
509
|
+
}
|
|
510
|
+
if (digestOf(raw) !== digest) {
|
|
511
|
+
throw new Error(`Object ${digest.slice(0, 12)} is damaged: its bytes do not match its name`);
|
|
512
|
+
}
|
|
513
|
+
return raw;
|
|
514
|
+
}
|
|
165
515
|
async writePack(objects) {
|
|
166
516
|
await (0, promises_1.mkdir)(this.packDirectory, { recursive: true });
|
|
167
517
|
const index = {};
|
|
@@ -189,14 +539,15 @@ class ObjectStore {
|
|
|
189
539
|
const waiting = this.pending.get(digest) ?? this.writing.get(digest);
|
|
190
540
|
if (waiting)
|
|
191
541
|
return (0, cbx_js_1.openChunkFrame)(waiting, digest);
|
|
542
|
+
const held = this.pendingV2.get(digest) ?? this.writingV2.get(digest);
|
|
543
|
+
if (held)
|
|
544
|
+
return Buffer.from(held.raw);
|
|
192
545
|
const located = (await this.packs()).get(digest);
|
|
546
|
+
const inV2 = !located ? this.v2Located.get(digest) : undefined;
|
|
547
|
+
if (inV2)
|
|
548
|
+
return this.getV2(digest, inV2);
|
|
193
549
|
if (located) {
|
|
194
|
-
|
|
195
|
-
if (!opening) {
|
|
196
|
-
opening = (0, promises_1.open)(located.pack, "r");
|
|
197
|
-
this.handles.set(located.pack, opening);
|
|
198
|
-
}
|
|
199
|
-
const handle = await opening;
|
|
550
|
+
const handle = await this.handleFor(located.pack);
|
|
200
551
|
const frame = Buffer.alloc(located.length);
|
|
201
552
|
const { bytesRead } = await handle.read(frame, 0, located.length, located.offset);
|
|
202
553
|
if (bytesRead !== located.length) {
|
|
@@ -221,6 +572,7 @@ class ObjectStore {
|
|
|
221
572
|
*/
|
|
222
573
|
rescan() {
|
|
223
574
|
this.packed = null;
|
|
575
|
+
this.v2Located = null;
|
|
224
576
|
}
|
|
225
577
|
/** Close any packs held open for reading. Safe to call at any time. */
|
|
226
578
|
async release() {
|
|
@@ -246,7 +598,7 @@ class ObjectStore {
|
|
|
246
598
|
find them, which grew with the history: the same object either way,
|
|
247
599
|
checked the same way.
|
|
248
600
|
*/
|
|
249
|
-
if (!this.pending.has(digest) && !this.writing.has(digest)) {
|
|
601
|
+
if (!this.pending.has(digest) && !this.writing.has(digest) && !this.pendingV2.has(digest) && !this.writingV2.has(digest)) {
|
|
250
602
|
let frame = null;
|
|
251
603
|
try {
|
|
252
604
|
frame = await (0, promises_1.readFile)(this.where(digest));
|
|
@@ -278,6 +630,10 @@ class ObjectStore {
|
|
|
278
630
|
if (digest.startsWith(prefix))
|
|
279
631
|
found.add(digest);
|
|
280
632
|
}
|
|
633
|
+
for (const digest of this.v2Located.keys()) {
|
|
634
|
+
if (digest.startsWith(prefix))
|
|
635
|
+
found.add(digest);
|
|
636
|
+
}
|
|
281
637
|
return [...found];
|
|
282
638
|
}
|
|
283
639
|
}
|