@cloudbitmaps/core 0.18.1 → 0.18.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/dist/{chunk-K4EXZDMU.js → chunk-ML6AT53O.js} +5 -7
- package/dist/chunk-ML6AT53O.js.map +7 -0
- package/dist/driver-kit.js +6 -2
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/localfs/exact-case.d.ts +37 -0
- package/dist/drivers/localfs/fs-util.d.ts +5 -0
- package/dist/drivers/localfs/paths.d.ts +7 -0
- package/dist/drivers/localfs/registry.d.ts +1 -0
- package/dist/drivers/localfs/storage.d.ts +1 -0
- package/dist/index.js +215 -59
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-K4EXZDMU.js.map +0 -7
package/dist/index.js
CHANGED
|
@@ -70,7 +70,7 @@ import {
|
|
|
70
70
|
validateUserNamespace,
|
|
71
71
|
validateUserRef,
|
|
72
72
|
webCryptoEntropy
|
|
73
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-ML6AT53O.js";
|
|
74
74
|
|
|
75
75
|
// src/core/bit-route.ts
|
|
76
76
|
var U32_MAX = 4294967295;
|
|
@@ -78,7 +78,7 @@ var CHUNK_COUNT = 65536;
|
|
|
78
78
|
var LOW_16 = 65535;
|
|
79
79
|
function splitId(id) {
|
|
80
80
|
if (!Number.isInteger(id) || id < 0 || id > U32_MAX) {
|
|
81
|
-
throw new ValidationError(`id must be an integer
|
|
81
|
+
throw new ValidationError(`an id must be an integer from 0 to ${U32_MAX}`);
|
|
82
82
|
}
|
|
83
83
|
return { chunkKey: id >>> 16, remainder: id & LOW_16 };
|
|
84
84
|
}
|
|
@@ -428,8 +428,7 @@ function windowOf(range) {
|
|
|
428
428
|
["through", through]
|
|
429
429
|
]) {
|
|
430
430
|
if (bound !== void 0 && (!Number.isInteger(bound) || bound < 0 || bound > U32_MAX)) {
|
|
431
|
-
|
|
432
|
-
throw new ValidationError(`${name} must be an integer in 0..${U32_MAX}; got ${got}`);
|
|
431
|
+
throw new ValidationError(`${name} must be an integer from 0 to ${U32_MAX}`);
|
|
433
432
|
}
|
|
434
433
|
}
|
|
435
434
|
const lo = after === void 0 ? 0 : after + 1;
|
|
@@ -1531,10 +1530,16 @@ async function aboveEverything(ref, deps, record) {
|
|
|
1531
1530
|
for await (const key of deps.storage.list(ref)) {
|
|
1532
1531
|
if (key.generation > highest) highest = key.generation;
|
|
1533
1532
|
}
|
|
1533
|
+
return numberAbove(highest);
|
|
1534
|
+
}
|
|
1535
|
+
function numberAbove(highest) {
|
|
1536
|
+
if (highest >= Number.MAX_SAFE_INTEGER) {
|
|
1537
|
+
throw new IntegrityError("the segment has no generation number left above its highest");
|
|
1538
|
+
}
|
|
1534
1539
|
return highest + 1;
|
|
1535
1540
|
}
|
|
1536
1541
|
async function nextLoadGeneration(ref, deps, row) {
|
|
1537
|
-
const generation = (row?.currentGen ?? -1)
|
|
1542
|
+
const generation = numberAbove(row?.currentGen ?? -1);
|
|
1538
1543
|
try {
|
|
1539
1544
|
await deps.storage.getTail({ namespace: ref.namespace, segment: ref.segment, generation }, 0);
|
|
1540
1545
|
} catch (err) {
|
|
@@ -2289,7 +2294,7 @@ async function* listSegments(registry, options = {}) {
|
|
|
2289
2294
|
}
|
|
2290
2295
|
|
|
2291
2296
|
// src/drivers/backends.ts
|
|
2292
|
-
import { join as
|
|
2297
|
+
import { join as join4 } from "node:path";
|
|
2293
2298
|
|
|
2294
2299
|
// src/drivers/memory.ts
|
|
2295
2300
|
import { createHash } from "node:crypto";
|
|
@@ -2467,11 +2472,30 @@ function genObjectKey(key) {
|
|
|
2467
2472
|
// src/drivers/localfs/storage.ts
|
|
2468
2473
|
import { constants as FS2 } from "node:fs";
|
|
2469
2474
|
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
2470
|
-
import { link, mkdir, open as open2, readdir, unlink } from "node:fs/promises";
|
|
2471
|
-
import { dirname } from "node:path";
|
|
2475
|
+
import { link, mkdir, open as open2, readdir as readdir2, unlink } from "node:fs/promises";
|
|
2476
|
+
import { dirname as dirname2 } from "node:path";
|
|
2472
2477
|
|
|
2473
2478
|
// src/drivers/localfs/paths.ts
|
|
2474
2479
|
import { join } from "node:path";
|
|
2480
|
+
var MAX_COMPONENT_BYTES = 255;
|
|
2481
|
+
var TEMP_SUFFIX_BYTES = 1 + 36 + 4;
|
|
2482
|
+
function assertFits(component, extraBytes) {
|
|
2483
|
+
if (component.length + extraBytes > MAX_COMPONENT_BYTES) {
|
|
2484
|
+
throw new ValidationError(
|
|
2485
|
+
`a name is too long for a file name on this backend: the namespace, or the segment with its generation and the temp file's suffix, must fit in ${MAX_COMPONENT_BYTES} bytes once encoded`
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
function assertStorageNamesFit(key) {
|
|
2490
|
+
validateSegmentRef(key);
|
|
2491
|
+
assertFits(namespacePathPart(key.namespace), 0);
|
|
2492
|
+
assertFits(storageObjectFilename(key.segment, key.generation), TEMP_SUFFIX_BYTES);
|
|
2493
|
+
}
|
|
2494
|
+
function assertRegistryNamesFit(ref) {
|
|
2495
|
+
validateSegmentRef(ref);
|
|
2496
|
+
assertFits(namespacePathPart(ref.namespace), 0);
|
|
2497
|
+
assertFits(`${encodeNameForPath(ref.segment)}${REGISTRY_SUFFIX}`, TEMP_SUFFIX_BYTES);
|
|
2498
|
+
}
|
|
2475
2499
|
function segmentsDir(root, ref) {
|
|
2476
2500
|
validateSegmentRef(ref);
|
|
2477
2501
|
return join(root, namespacePathPart(ref.namespace), "segments");
|
|
@@ -2531,10 +2555,15 @@ function parseNamespaceDir(entry) {
|
|
|
2531
2555
|
return { namespace };
|
|
2532
2556
|
}
|
|
2533
2557
|
|
|
2558
|
+
// src/drivers/localfs/exact-case.ts
|
|
2559
|
+
import { readdir, realpath, stat } from "node:fs/promises";
|
|
2560
|
+
import { dirname, join as join2, parse, relative, resolve, sep } from "node:path";
|
|
2561
|
+
|
|
2534
2562
|
// src/drivers/localfs/fs-util.ts
|
|
2535
2563
|
import { constants as FS } from "node:fs";
|
|
2536
2564
|
import { open } from "node:fs/promises";
|
|
2537
2565
|
var isCode = (err, code) => err instanceof Error && err.code === code;
|
|
2566
|
+
var isAbsent = (err) => isCode(err, "ENOENT") || isCode(err, "ENOTDIR") || isCode(err, "ENAMETOOLONG");
|
|
2538
2567
|
var TRANSIENT_FS_CODES = /* @__PURE__ */ new Set(["EBUSY", "EAGAIN", "EMFILE", "ENFILE", "ETIMEDOUT"]);
|
|
2539
2568
|
var isFsTransient = (err) => err instanceof Error && TRANSIENT_FS_CODES.has(err.code ?? "");
|
|
2540
2569
|
var mapFsError = (err) => isFsTransient(err) ? new TransientError(`transient filesystem fault: ${err.code}`, { cause: err }) : err;
|
|
@@ -2551,18 +2580,138 @@ async function fsyncDir(dir) {
|
|
|
2551
2580
|
}
|
|
2552
2581
|
}
|
|
2553
2582
|
|
|
2583
|
+
// src/drivers/localfs/exact-case.ts
|
|
2584
|
+
function firstCaseDifference(expected, actual) {
|
|
2585
|
+
const shared = Math.min(expected.length, actual.length);
|
|
2586
|
+
for (let i = 0; i < shared; i++) {
|
|
2587
|
+
const want = expected[i];
|
|
2588
|
+
const have = actual[i];
|
|
2589
|
+
if (want !== have) return want.toLowerCase() === have.toLowerCase() ? i : -1;
|
|
2590
|
+
}
|
|
2591
|
+
return -1;
|
|
2592
|
+
}
|
|
2593
|
+
var swapCase = (s) => [...s].map((c) => c === c.toLowerCase() ? c.toUpperCase() : c.toLowerCase()).join("");
|
|
2594
|
+
var components = (path) => path.split(sep).filter((c) => c !== "");
|
|
2595
|
+
function caseVariantError() {
|
|
2596
|
+
return new ValidationError(
|
|
2597
|
+
"names differ only by case on a case-insensitive filesystem: a name that differs only by case from one already stored is the same file here, so it is refused rather than written over"
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
var ExactCase = class {
|
|
2601
|
+
constructor(root) {
|
|
2602
|
+
this.root = root;
|
|
2603
|
+
}
|
|
2604
|
+
root;
|
|
2605
|
+
/** `undefined` until the root holds an entry to try; then fixed, as a filesystem does not change its mind. */
|
|
2606
|
+
insensitive;
|
|
2607
|
+
/**
|
|
2608
|
+
* Whether the root's filesystem folds case, learned from an entry already in the root: the entry's name with its
|
|
2609
|
+
* case swapped is the same file or it is not. An empty or absent root holds nothing to alias, so it is not
|
|
2610
|
+
* insensitive yet, and is looked at again on the next call.
|
|
2611
|
+
*/
|
|
2612
|
+
async foldsCase() {
|
|
2613
|
+
if (this.insensitive !== void 0) return this.insensitive;
|
|
2614
|
+
let names;
|
|
2615
|
+
try {
|
|
2616
|
+
names = await readdir(this.root);
|
|
2617
|
+
} catch (err) {
|
|
2618
|
+
if (isAbsent(err)) return this.foldsAtAncestor();
|
|
2619
|
+
throw err;
|
|
2620
|
+
}
|
|
2621
|
+
for (const name of names) {
|
|
2622
|
+
const swapped = swapCase(name);
|
|
2623
|
+
if (swapped === name) continue;
|
|
2624
|
+
const own = await stat(join2(this.root, name), { bigint: true }).catch(() => void 0);
|
|
2625
|
+
if (own === void 0) continue;
|
|
2626
|
+
const other = await stat(join2(this.root, swapped), { bigint: true }).catch(() => void 0);
|
|
2627
|
+
this.insensitive = other !== void 0 && other.ino === own.ino && other.dev === own.dev;
|
|
2628
|
+
return this.insensitive;
|
|
2629
|
+
}
|
|
2630
|
+
return this.foldsAtAncestor();
|
|
2631
|
+
}
|
|
2632
|
+
/**
|
|
2633
|
+
* The same question asked of the root's own name, or the nearest existing directory above it, when the root holds
|
|
2634
|
+
* nothing to try: a root with no entries has none to alias yet, but two names created at once still would. Not
|
|
2635
|
+
* remembered, since it is the volume above the root that answers.
|
|
2636
|
+
*/
|
|
2637
|
+
async foldsAtAncestor() {
|
|
2638
|
+
let dir = resolve(this.root);
|
|
2639
|
+
for (; ; ) {
|
|
2640
|
+
const top = parse(dir).root;
|
|
2641
|
+
const parts = components(relative(top, dir));
|
|
2642
|
+
let at = parts.length - 1;
|
|
2643
|
+
while (at >= 0 && swapCase(parts[at]) === parts[at]) at--;
|
|
2644
|
+
if (at === -1) return false;
|
|
2645
|
+
const swapped = [...parts];
|
|
2646
|
+
swapped[at] = swapCase(parts[at]);
|
|
2647
|
+
let own;
|
|
2648
|
+
try {
|
|
2649
|
+
own = await stat(dir, { bigint: true });
|
|
2650
|
+
} catch (err) {
|
|
2651
|
+
if (!isAbsent(err)) return false;
|
|
2652
|
+
const parent = dirname(dir);
|
|
2653
|
+
if (parent === dir) return false;
|
|
2654
|
+
dir = parent;
|
|
2655
|
+
continue;
|
|
2656
|
+
}
|
|
2657
|
+
const other = await stat(join2(top, ...swapped), { bigint: true }).catch(() => void 0);
|
|
2658
|
+
return other !== void 0 && other.ino === own.ino && other.dev === own.dev;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
/** Whether the root's filesystem folds case: names that differ only by case are one file there. */
|
|
2662
|
+
folds() {
|
|
2663
|
+
return this.foldsCase();
|
|
2664
|
+
}
|
|
2665
|
+
/**
|
|
2666
|
+
* `true` when `path` (inside the root) is, on disk, a path whose name differs only by case from the one given:
|
|
2667
|
+
* the deepest part of it that exists is resolved to its real name and compared exactly. A path that does not exist
|
|
2668
|
+
* yet, or that nothing differs from, is `false`.
|
|
2669
|
+
*/
|
|
2670
|
+
async differs(path) {
|
|
2671
|
+
if (!await this.foldsCase()) return false;
|
|
2672
|
+
const rootAbs = resolve(this.root);
|
|
2673
|
+
let probe = resolve(path);
|
|
2674
|
+
let real;
|
|
2675
|
+
for (; ; ) {
|
|
2676
|
+
if (probe === rootAbs || !relative(rootAbs, probe) || relative(rootAbs, probe).startsWith("..")) {
|
|
2677
|
+
return false;
|
|
2678
|
+
}
|
|
2679
|
+
try {
|
|
2680
|
+
real = await realpath(probe);
|
|
2681
|
+
break;
|
|
2682
|
+
} catch (err) {
|
|
2683
|
+
if (!isAbsent(err)) throw err;
|
|
2684
|
+
probe = dirname(probe);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
const realRoot = await realpath(rootAbs).catch(() => void 0);
|
|
2688
|
+
if (realRoot === void 0) return false;
|
|
2689
|
+
const actual = relative(realRoot, real);
|
|
2690
|
+
if (actual.startsWith("..")) return false;
|
|
2691
|
+
return firstCaseDifference(components(relative(rootAbs, probe)), components(actual)) !== -1;
|
|
2692
|
+
}
|
|
2693
|
+
/** Refuse a write to `path` when its real name differs from it only by case. */
|
|
2694
|
+
async refuseVariant(path) {
|
|
2695
|
+
if (await this.differs(path)) throw caseVariantError();
|
|
2696
|
+
}
|
|
2697
|
+
};
|
|
2698
|
+
|
|
2554
2699
|
// src/drivers/localfs/storage.ts
|
|
2555
2700
|
var LocalFsStorageDriver = class {
|
|
2556
2701
|
constructor(root) {
|
|
2557
2702
|
this.root = root;
|
|
2703
|
+
this.exactCase = new ExactCase(root);
|
|
2558
2704
|
}
|
|
2559
2705
|
root;
|
|
2706
|
+
exactCase;
|
|
2560
2707
|
capabilities() {
|
|
2561
2708
|
return { rangeRead: true, maxObjectBytes: Number.MAX_SAFE_INTEGER, conditionalPut: false };
|
|
2562
2709
|
}
|
|
2563
2710
|
async putImmutable(key, write) {
|
|
2564
2711
|
const finalPath = storageObjectPath(this.root, key);
|
|
2565
|
-
|
|
2712
|
+
assertStorageNamesFit(key);
|
|
2713
|
+
await this.exactCase.refuseVariant(finalPath);
|
|
2714
|
+
await mkdir(dirname2(finalPath), { recursive: true });
|
|
2566
2715
|
const tmpPath = `${finalPath}.${randomUUID()}.tmp`;
|
|
2567
2716
|
const handle = await open2(tmpPath, FS2.O_CREAT | FS2.O_EXCL | FS2.O_WRONLY | O_NOFOLLOW);
|
|
2568
2717
|
const hash = createHash2("sha256");
|
|
@@ -2599,7 +2748,7 @@ var LocalFsStorageDriver = class {
|
|
|
2599
2748
|
}
|
|
2600
2749
|
await unlink(tmpPath).catch(() => {
|
|
2601
2750
|
});
|
|
2602
|
-
await fsyncDir(
|
|
2751
|
+
await fsyncDir(dirname2(finalPath));
|
|
2603
2752
|
return { size, sha256: hash.digest("hex") };
|
|
2604
2753
|
}
|
|
2605
2754
|
async getRange(key, offset, length) {
|
|
@@ -2634,17 +2783,20 @@ var LocalFsStorageDriver = class {
|
|
|
2634
2783
|
}
|
|
2635
2784
|
}
|
|
2636
2785
|
async delete(key) {
|
|
2637
|
-
|
|
2638
|
-
|
|
2786
|
+
const path = storageObjectPath(this.root, key);
|
|
2787
|
+
if (await this.exactCase.differs(path)) return;
|
|
2788
|
+
await unlink(path).catch((err) => {
|
|
2789
|
+
if (!isAbsent(err)) throw mapFsError(err);
|
|
2639
2790
|
});
|
|
2640
2791
|
}
|
|
2641
2792
|
async *list(ref) {
|
|
2642
2793
|
const dir = segmentsDir(this.root, ref);
|
|
2794
|
+
if (await this.exactCase.differs(dir)) return;
|
|
2643
2795
|
let names;
|
|
2644
2796
|
try {
|
|
2645
|
-
names = await
|
|
2797
|
+
names = await readdir2(dir);
|
|
2646
2798
|
} catch (err) {
|
|
2647
|
-
if (
|
|
2799
|
+
if (isAbsent(err)) return;
|
|
2648
2800
|
throw mapFsError(err);
|
|
2649
2801
|
}
|
|
2650
2802
|
for (const name of names) {
|
|
@@ -2655,10 +2807,14 @@ var LocalFsStorageDriver = class {
|
|
|
2655
2807
|
}
|
|
2656
2808
|
}
|
|
2657
2809
|
async openRead(key) {
|
|
2810
|
+
const path = storageObjectPath(this.root, key);
|
|
2811
|
+
if (await this.exactCase.differs(path)) {
|
|
2812
|
+
throw new NotFoundError(`no such generation: ${key.segment}.${key.generation}`);
|
|
2813
|
+
}
|
|
2658
2814
|
try {
|
|
2659
|
-
return await open2(
|
|
2815
|
+
return await open2(path, FS2.O_RDONLY | O_NOFOLLOW);
|
|
2660
2816
|
} catch (err) {
|
|
2661
|
-
if (
|
|
2817
|
+
if (isAbsent(err)) {
|
|
2662
2818
|
throw new NotFoundError(`no such generation: ${key.segment}.${key.generation}`);
|
|
2663
2819
|
}
|
|
2664
2820
|
if (isCode(err, "ELOOP")) {
|
|
@@ -2674,20 +2830,20 @@ var LocalFsStorageDriver = class {
|
|
|
2674
2830
|
// src/drivers/localfs/registry.ts
|
|
2675
2831
|
import { constants as FS3 } from "node:fs";
|
|
2676
2832
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2677
|
-
import { mkdir as mkdir2, open as open3, readdir as
|
|
2678
|
-
import { basename, dirname as
|
|
2833
|
+
import { mkdir as mkdir2, open as open3, readdir as readdir3, realpath as realpath2, rename, unlink as unlink2 } from "node:fs/promises";
|
|
2834
|
+
import { basename, dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
|
|
2679
2835
|
var DEFAULT_MAX_ROW_BYTES = 1 * 1024 * 1024;
|
|
2680
2836
|
var rowChains = /* @__PURE__ */ new Map();
|
|
2681
2837
|
async function rowLockKey(path) {
|
|
2682
|
-
const abs =
|
|
2838
|
+
const abs = resolve2(path);
|
|
2683
2839
|
const tail = [basename(abs)];
|
|
2684
|
-
let dir =
|
|
2840
|
+
let dir = dirname3(abs);
|
|
2685
2841
|
for (; ; ) {
|
|
2686
2842
|
try {
|
|
2687
|
-
return
|
|
2843
|
+
return join3(await realpath2(dir), ...tail);
|
|
2688
2844
|
} catch (err) {
|
|
2689
|
-
const parent =
|
|
2690
|
-
if (!
|
|
2845
|
+
const parent = dirname3(dir);
|
|
2846
|
+
if (!isAbsent(err) || parent === dir) throw mapFsError(err);
|
|
2691
2847
|
tail.unshift(basename(dir));
|
|
2692
2848
|
dir = parent;
|
|
2693
2849
|
}
|
|
@@ -2696,12 +2852,14 @@ async function rowLockKey(path) {
|
|
|
2696
2852
|
var LocalFsRegistryDriver = class {
|
|
2697
2853
|
constructor(root, options = {}) {
|
|
2698
2854
|
this.root = root;
|
|
2855
|
+
this.exactCase = new ExactCase(root);
|
|
2699
2856
|
this.now = options.now ?? (() => Date.now());
|
|
2700
2857
|
this.entropy = options.entropy ?? webCryptoEntropy;
|
|
2701
2858
|
}
|
|
2702
2859
|
root;
|
|
2703
2860
|
now;
|
|
2704
2861
|
entropy;
|
|
2862
|
+
exactCase;
|
|
2705
2863
|
/**
|
|
2706
2864
|
* `conditionalDelete`: a delete reads the row, checks it, and unlinks it under the row's lock, which every writer on
|
|
2707
2865
|
* the root in this process takes, so no write can land between the check and the unlink.
|
|
@@ -2716,6 +2874,7 @@ var LocalFsRegistryDriver = class {
|
|
|
2716
2874
|
async create(ref, record) {
|
|
2717
2875
|
const checked = validateNewRegistryRecord(record);
|
|
2718
2876
|
const path = registryRowPath(this.root, ref);
|
|
2877
|
+
assertRegistryNamesFit(ref);
|
|
2719
2878
|
return this.withRowLock(path, async () => {
|
|
2720
2879
|
const current = await this.readRow(path);
|
|
2721
2880
|
if (current !== null && !current.deleted) {
|
|
@@ -2733,6 +2892,7 @@ var LocalFsRegistryDriver = class {
|
|
|
2733
2892
|
async compareAndSwap(ref, expected, patch) {
|
|
2734
2893
|
const checked = validateRegistryPatch(patch);
|
|
2735
2894
|
const path = registryRowPath(this.root, ref);
|
|
2895
|
+
assertRegistryNamesFit(ref);
|
|
2736
2896
|
return this.withRowLock(path, async () => {
|
|
2737
2897
|
const current = await this.readRow(path);
|
|
2738
2898
|
if (current === null || current.deleted || current.record.token !== expected) {
|
|
@@ -2750,11 +2910,12 @@ var LocalFsRegistryDriver = class {
|
|
|
2750
2910
|
async *list(namespace) {
|
|
2751
2911
|
for (const ns of await this.namespaceDirs(namespace)) {
|
|
2752
2912
|
const dir = registryDir(this.root, ns);
|
|
2913
|
+
if (await this.exactCase.differs(dir)) continue;
|
|
2753
2914
|
let names;
|
|
2754
2915
|
try {
|
|
2755
|
-
names = await
|
|
2916
|
+
names = await readdir3(dir);
|
|
2756
2917
|
} catch (err) {
|
|
2757
|
-
if (
|
|
2918
|
+
if (isAbsent(err)) continue;
|
|
2758
2919
|
throw mapFsError(err);
|
|
2759
2920
|
}
|
|
2760
2921
|
for (const name of names) {
|
|
@@ -2791,14 +2952,14 @@ var LocalFsRegistryDriver = class {
|
|
|
2791
2952
|
} catch (err) {
|
|
2792
2953
|
if (!isCode(err, "ENOENT")) throw mapFsError(err);
|
|
2793
2954
|
}
|
|
2794
|
-
await fsyncDir(
|
|
2955
|
+
await fsyncDir(dirname3(path));
|
|
2795
2956
|
}
|
|
2796
2957
|
/** Namespaces to scan: just the one requested, or every namespace dir under the root. */
|
|
2797
2958
|
async namespaceDirs(namespace) {
|
|
2798
2959
|
if (namespace !== void 0) return [namespace];
|
|
2799
2960
|
let entries;
|
|
2800
2961
|
try {
|
|
2801
|
-
entries = await
|
|
2962
|
+
entries = await readdir3(this.root);
|
|
2802
2963
|
} catch (err) {
|
|
2803
2964
|
if (isCode(err, "ENOENT")) return [];
|
|
2804
2965
|
throw mapFsError(err);
|
|
@@ -2813,9 +2974,10 @@ var LocalFsRegistryDriver = class {
|
|
|
2813
2974
|
async readRow(path) {
|
|
2814
2975
|
let handle;
|
|
2815
2976
|
try {
|
|
2977
|
+
if (await this.exactCase.differs(path)) return null;
|
|
2816
2978
|
handle = await open3(path, FS3.O_RDONLY | O_NOFOLLOW);
|
|
2817
2979
|
} catch (err) {
|
|
2818
|
-
if (
|
|
2980
|
+
if (isAbsent(err) || isCode(err, "ELOOP")) return null;
|
|
2819
2981
|
throw mapFsError(err);
|
|
2820
2982
|
}
|
|
2821
2983
|
try {
|
|
@@ -2830,7 +2992,8 @@ var LocalFsRegistryDriver = class {
|
|
|
2830
2992
|
}
|
|
2831
2993
|
}
|
|
2832
2994
|
async writeRow(path, deleted, record) {
|
|
2833
|
-
await
|
|
2995
|
+
await this.exactCase.refuseVariant(path);
|
|
2996
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
2834
2997
|
const out = Buffer.from(serializeRegistryEnvelope({ deleted, record }), "utf8");
|
|
2835
2998
|
if (out.length > DEFAULT_MAX_ROW_BYTES) {
|
|
2836
2999
|
throw new ValidationError(
|
|
@@ -2855,11 +3018,12 @@ var LocalFsRegistryDriver = class {
|
|
|
2855
3018
|
});
|
|
2856
3019
|
throw mapFsError(err);
|
|
2857
3020
|
});
|
|
2858
|
-
await fsyncDir(
|
|
3021
|
+
await fsyncDir(dirname3(path));
|
|
2859
3022
|
}
|
|
2860
3023
|
/** Serialize callbacks for a row so read-modify-write is atomic across every instance in the process. */
|
|
2861
3024
|
async withRowLock(path, fn) {
|
|
2862
|
-
const
|
|
3025
|
+
const resolved = await rowLockKey(path);
|
|
3026
|
+
const key = await this.exactCase.folds() ? resolved.toLowerCase() : resolved;
|
|
2863
3027
|
const prev = rowChains.get(key) ?? Promise.resolve();
|
|
2864
3028
|
const result = prev.then(fn, fn);
|
|
2865
3029
|
const tail = result.then(
|
|
@@ -2887,9 +3051,9 @@ var MemoryStorage = class {
|
|
|
2887
3051
|
var LocalFsStorage = class {
|
|
2888
3052
|
constructor(root, options = {}) {
|
|
2889
3053
|
this.root = root;
|
|
2890
|
-
this.storage = new LocalFsStorageDriver(
|
|
3054
|
+
this.storage = new LocalFsStorageDriver(join4(root, "storage"));
|
|
2891
3055
|
this.registry = new LocalFsRegistryDriver(
|
|
2892
|
-
|
|
3056
|
+
join4(root, "registry"),
|
|
2893
3057
|
options.now === void 0 ? {} : { now: options.now }
|
|
2894
3058
|
);
|
|
2895
3059
|
brandAsBackend(this);
|
|
@@ -4028,9 +4192,7 @@ function overCap(what, length) {
|
|
|
4028
4192
|
}
|
|
4029
4193
|
function prepareLoadInput(input, codec, subject = "a load") {
|
|
4030
4194
|
if (typeof input !== "object" || input === null) {
|
|
4031
|
-
throw new ValidationError(
|
|
4032
|
-
`${subject} takes ids, { serialized } or { bitmap }; got ${String(input)}`
|
|
4033
|
-
);
|
|
4195
|
+
throw new ValidationError(`${subject} takes ids, { serialized } or { bitmap }`);
|
|
4034
4196
|
}
|
|
4035
4197
|
const tag = tagOf(input);
|
|
4036
4198
|
if (tag === "Uint8Array" || tag === "Uint8ClampedArray") {
|
|
@@ -5736,7 +5898,7 @@ function* rangeCheckedAtWrite(chunks) {
|
|
|
5736
5898
|
const max = chunk.bitmap.maximum?.();
|
|
5737
5899
|
if (max !== void 0 && max > MAX_REMAINDER) {
|
|
5738
5900
|
throw new ValidationError(
|
|
5739
|
-
`
|
|
5901
|
+
`a chunk holds a value outside the 16-bit range [0, ${MAX_REMAINDER}]`
|
|
5740
5902
|
);
|
|
5741
5903
|
}
|
|
5742
5904
|
yield chunk;
|
|
@@ -5748,18 +5910,14 @@ async function collectChunks(input, codec, clock) {
|
|
|
5748
5910
|
let last = -1;
|
|
5749
5911
|
for await (const chunk of input.chunks) {
|
|
5750
5912
|
if (typeof chunk !== "object" || chunk === null) {
|
|
5751
|
-
throw new ValidationError(
|
|
5752
|
-
`a chunk must be an object with a chunkKey and a bitmap; got ${String(chunk)}`
|
|
5753
|
-
);
|
|
5913
|
+
throw new ValidationError("a chunk must be an object with a chunkKey and a bitmap");
|
|
5754
5914
|
}
|
|
5755
5915
|
const { chunkKey, bitmap } = chunk;
|
|
5756
5916
|
if (codec.owns?.(bitmap) !== true) {
|
|
5757
|
-
throw new ValidationError(
|
|
5758
|
-
`chunk ${String(chunkKey)}: its bitmap is not one this load's codec made`
|
|
5759
|
-
);
|
|
5917
|
+
throw new ValidationError(`a chunk's bitmap is not one this load's codec made`);
|
|
5760
5918
|
}
|
|
5761
5919
|
if (!Number.isInteger(chunkKey) || chunkKey < 0 || chunkKey > MAX_REMAINDER) {
|
|
5762
|
-
throw new ValidationError(
|
|
5920
|
+
throw new ValidationError("a chunk's key must be an integer in [0, 65535]");
|
|
5763
5921
|
}
|
|
5764
5922
|
if (chunkKey <= last) {
|
|
5765
5923
|
throw new ValidationError(`chunk ${chunkKey} does not ascend: the last was ${last}`);
|
|
@@ -5768,7 +5926,7 @@ async function collectChunks(input, codec, clock) {
|
|
|
5768
5926
|
const max = bitmap.maximum?.();
|
|
5769
5927
|
if (max !== void 0 && max > MAX_REMAINDER) {
|
|
5770
5928
|
throw new ValidationError(
|
|
5771
|
-
`chunk
|
|
5929
|
+
`a chunk holds a value outside the 16-bit range [0, ${MAX_REMAINDER}]`
|
|
5772
5930
|
);
|
|
5773
5931
|
}
|
|
5774
5932
|
chunks.push({ chunkKey, bitmap });
|
|
@@ -6792,11 +6950,7 @@ var FeedCursor = class {
|
|
|
6792
6950
|
const key = record.key;
|
|
6793
6951
|
const operands = record.operands;
|
|
6794
6952
|
if (typeof key !== "number" || !Number.isInteger(key) || key < 0 || key >= KEY_IDS) {
|
|
6795
|
-
this.refuse(
|
|
6796
|
-
typeof key === "number" ? key : "?",
|
|
6797
|
-
void 0,
|
|
6798
|
-
"the key must be an integer from 0 to 65535"
|
|
6799
|
-
);
|
|
6953
|
+
this.refuse("?", void 0, "the key must be an integer from 0 to 65535");
|
|
6800
6954
|
}
|
|
6801
6955
|
if (key < this.prevKey) {
|
|
6802
6956
|
this.refuse(
|
|
@@ -7031,7 +7185,9 @@ function compileCombineMany(request) {
|
|
|
7031
7185
|
["after", request.after],
|
|
7032
7186
|
["through", request.through]
|
|
7033
7187
|
]) {
|
|
7034
|
-
if (bound !== void 0
|
|
7188
|
+
if (bound !== void 0 && (!Number.isInteger(bound) || bound < 0 || bound > U32_MAX)) {
|
|
7189
|
+
throw new ValidationError(`${name} must be an integer from 0 to ${U32_MAX}`);
|
|
7190
|
+
}
|
|
7035
7191
|
}
|
|
7036
7192
|
const index = /* @__PURE__ */ new Map();
|
|
7037
7193
|
operands.forEach((op, i) => index.set(op.name, i));
|
|
@@ -7170,7 +7326,7 @@ var Gate = class {
|
|
|
7170
7326
|
return this.peak;
|
|
7171
7327
|
}
|
|
7172
7328
|
async run(task) {
|
|
7173
|
-
if (this.active >= this.limit) await new Promise((
|
|
7329
|
+
if (this.active >= this.limit) await new Promise((resolve3) => this.waiting.push(resolve3));
|
|
7174
7330
|
else this.active++;
|
|
7175
7331
|
if (this.active > this.peak) this.peak = this.active;
|
|
7176
7332
|
try {
|
|
@@ -8097,7 +8253,7 @@ var Run = class {
|
|
|
8097
8253
|
const wake = () => {
|
|
8098
8254
|
const now = waiters;
|
|
8099
8255
|
waiters = [];
|
|
8100
|
-
for (const
|
|
8256
|
+
for (const resolve3 of now) resolve3();
|
|
8101
8257
|
};
|
|
8102
8258
|
const worker = async () => {
|
|
8103
8259
|
for (; ; ) {
|
|
@@ -8116,7 +8272,7 @@ var Run = class {
|
|
|
8116
8272
|
const transient = o.serialized;
|
|
8117
8273
|
let admitted = this.ledger.tryCharge(transient);
|
|
8118
8274
|
while (!admitted && inFlight > 0) {
|
|
8119
|
-
await new Promise((
|
|
8275
|
+
await new Promise((resolve3) => waiters.push(resolve3));
|
|
8120
8276
|
admitted = this.ledger.tryCharge(transient);
|
|
8121
8277
|
}
|
|
8122
8278
|
if (!admitted) {
|
|
@@ -8414,7 +8570,7 @@ var HeldChunks = class {
|
|
|
8414
8570
|
const { chunkKey, payload, cardinality } = chunk;
|
|
8415
8571
|
if (!Number.isInteger(chunkKey) || chunkKey < 0 || chunkKey > MAX_REMAINDER || chunkKey <= last) {
|
|
8416
8572
|
throw new ValidationError(
|
|
8417
|
-
`a held chunk's key must be an integer in [0, 65535] above the last
|
|
8573
|
+
`a held chunk's key must be an integer in [0, 65535] above the last`
|
|
8418
8574
|
);
|
|
8419
8575
|
}
|
|
8420
8576
|
assertChunkCardinalityInRange(cardinality);
|
|
@@ -9008,11 +9164,11 @@ var InProcessKeystore = class {
|
|
|
9008
9164
|
if (k.length !== KEY_BYTES)
|
|
9009
9165
|
throw new ValidationError(`KEK "${id}" must be ${KEY_BYTES} bytes, got ${k.length}`);
|
|
9010
9166
|
}
|
|
9011
|
-
if (!(options.activeKeyId in options.keys))
|
|
9012
|
-
throw new ValidationError(`activeKeyId "${options.activeKeyId}" is not in keys`);
|
|
9013
|
-
if (options.recoveryKeyId !== void 0 && !(options.recoveryKeyId in options.keys))
|
|
9014
|
-
throw new ValidationError(`recoveryKeyId "${options.recoveryKeyId}" is not in keys`);
|
|
9015
9167
|
this.keys = new Map(entries.map(([id, k]) => [id, Buffer.from(k)]));
|
|
9168
|
+
if (!this.keys.has(options.activeKeyId))
|
|
9169
|
+
throw new ValidationError(`activeKeyId "${String(options.activeKeyId)}" is not in keys`);
|
|
9170
|
+
if (options.recoveryKeyId !== void 0 && !this.keys.has(options.recoveryKeyId))
|
|
9171
|
+
throw new ValidationError(`recoveryKeyId "${String(options.recoveryKeyId)}" is not in keys`);
|
|
9016
9172
|
this.activeKeyId = options.activeKeyId;
|
|
9017
9173
|
this.recoveryKeyId = options.recoveryKeyId === options.activeKeyId ? void 0 : options.recoveryKeyId;
|
|
9018
9174
|
}
|