@cloudbitmaps/core 0.8.2 → 0.10.0
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/NOTICE +4 -4
- package/README.md +28 -6
- package/dist/chunk-BUPDGZRR.js +461 -0
- package/dist/chunk-BUPDGZRR.js.map +7 -0
- package/dist/core/audit.d.ts +127 -0
- package/dist/core/bit-route.d.ts +21 -0
- package/dist/core/blob.d.ts +34 -0
- package/dist/core/budget.d.ts +66 -0
- package/dist/core/codec.d.ts +114 -0
- package/dist/core/concurrency.d.ts +15 -0
- package/dist/core/consistency.d.ts +73 -0
- package/dist/core/cooperative.d.ts +78 -0
- package/dist/core/cost.d.ts +102 -0
- package/dist/core/crbm/crc32c.d.ts +10 -0
- package/dist/core/crbm/format.d.ts +116 -0
- package/dist/core/crbm/reader.d.ts +69 -0
- package/dist/core/crbm/varint.d.ts +13 -0
- package/dist/core/crbm/writer.d.ts +39 -0
- package/dist/core/crbm-storage-source.d.ts +432 -0
- package/dist/core/crypto.d.ts +80 -0
- package/dist/core/determinism.d.ts +42 -0
- package/dist/core/discover.d.ts +75 -0
- package/dist/core/due-index.d.ts +55 -0
- package/dist/core/engine.d.ts +204 -0
- package/dist/core/erase-id.d.ts +141 -0
- package/dist/core/erasure.d.ts +179 -0
- package/dist/core/errors.d.ts +120 -0
- package/dist/core/generation-gc.d.ts +52 -0
- package/dist/core/keys.d.ts +23 -0
- package/dist/core/load.d.ts +102 -0
- package/dist/core/lru.d.ts +69 -0
- package/dist/core/metrics.d.ts +132 -0
- package/dist/core/name-codec.d.ts +79 -0
- package/dist/core/pinned-storage-source.d.ts +69 -0
- package/dist/core/ports.d.ts +340 -0
- package/dist/core/registry-scan.d.ts +38 -0
- package/dist/core/retention-sweep.d.ts +190 -0
- package/dist/core/retention.d.ts +67 -0
- package/dist/core/retry.d.ts +65 -0
- package/dist/core/rollback.d.ts +71 -0
- package/dist/core/validate.d.ts +20 -0
- package/dist/driver-kit.d.ts +10 -0
- package/dist/driver-kit.js +314 -0
- package/dist/driver-kit.js.map +7 -0
- package/dist/drivers/_shared/aws-errors.d.ts +18 -0
- package/dist/drivers/_shared/keys.d.ts +27 -0
- package/dist/drivers/_shared/object-registry-keys.d.ts +28 -0
- package/dist/drivers/_shared/object-registry.d.ts +84 -0
- package/dist/drivers/_shared/registry.d.ts +62 -0
- package/dist/drivers/backends.d.ts +49 -0
- package/dist/drivers/crypto.d.ts +38 -0
- package/dist/drivers/localfs/fs-util.d.ts +20 -0
- package/dist/drivers/localfs/paths.d.ts +33 -0
- package/dist/drivers/localfs/registry.d.ts +28 -0
- package/dist/drivers/localfs/storage.d.ts +19 -0
- package/dist/drivers/memory.d.ts +57 -0
- package/dist/drivers/retry/retrying-drivers.d.ts +90 -0
- package/dist/export/index.d.ts +92 -0
- package/dist/index.d.ts +85 -2534
- package/dist/index.js +3076 -2978
- package/dist/index.js.map +7 -1
- package/dist/testing/fuzz-core.d.ts +14 -0
- package/package.json +19 -115
- package/dist/azure/index.cjs +0 -368
- package/dist/azure/index.cjs.map +0 -1
- package/dist/azure/index.d.cts +0 -48
- package/dist/azure/index.d.ts +0 -48
- package/dist/azure/index.js +0 -304
- package/dist/azure/index.js.map +0 -1
- package/dist/cassandra/index.cjs +0 -265
- package/dist/cassandra/index.cjs.map +0 -1
- package/dist/cassandra/index.d.cts +0 -52
- package/dist/cassandra/index.d.ts +0 -52
- package/dist/cassandra/index.js +0 -204
- package/dist/cassandra/index.js.map +0 -1
- package/dist/chunk-7LMLYSVJ.js +0 -43
- package/dist/chunk-7LMLYSVJ.js.map +0 -1
- package/dist/chunk-L54FL5DW.js +0 -203
- package/dist/chunk-L54FL5DW.js.map +0 -1
- package/dist/chunk-NUIDEEFZ.js +0 -91
- package/dist/chunk-NUIDEEFZ.js.map +0 -1
- package/dist/chunk-SNJVZ227.js +0 -35
- package/dist/chunk-SNJVZ227.js.map +0 -1
- package/dist/chunk-YEHMG6W2.js +0 -6
- package/dist/chunk-YEHMG6W2.js.map +0 -1
- package/dist/dynamodb/index.cjs +0 -740
- package/dist/dynamodb/index.cjs.map +0 -1
- package/dist/dynamodb/index.d.cts +0 -106
- package/dist/dynamodb/index.d.ts +0 -106
- package/dist/dynamodb/index.js +0 -469
- package/dist/dynamodb/index.js.map +0 -1
- package/dist/gcs/index.cjs +0 -343
- package/dist/gcs/index.cjs.map +0 -1
- package/dist/gcs/index.d.cts +0 -46
- package/dist/gcs/index.d.ts +0 -46
- package/dist/gcs/index.js +0 -279
- package/dist/gcs/index.js.map +0 -1
- package/dist/index.cjs +0 -4608
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -2535
- package/dist/mongodb/index.cjs +0 -260
- package/dist/mongodb/index.cjs.map +0 -1
- package/dist/mongodb/index.d.cts +0 -45
- package/dist/mongodb/index.d.ts +0 -45
- package/dist/mongodb/index.js +0 -199
- package/dist/mongodb/index.js.map +0 -1
- package/dist/mysql/index.cjs +0 -281
- package/dist/mysql/index.cjs.map +0 -1
- package/dist/mysql/index.d.cts +0 -56
- package/dist/mysql/index.d.ts +0 -56
- package/dist/mysql/index.js +0 -211
- package/dist/mysql/index.js.map +0 -1
- package/dist/ports-5BQTGKze.d.cts +0 -356
- package/dist/ports-5BQTGKze.d.ts +0 -356
- package/dist/postgres/index.cjs +0 -273
- package/dist/postgres/index.cjs.map +0 -1
- package/dist/postgres/index.d.cts +0 -54
- package/dist/postgres/index.d.ts +0 -54
- package/dist/postgres/index.js +0 -203
- package/dist/postgres/index.js.map +0 -1
- package/dist/redis/index.cjs +0 -257
- package/dist/redis/index.cjs.map +0 -1
- package/dist/redis/index.d.cts +0 -42
- package/dist/redis/index.d.ts +0 -42
- package/dist/redis/index.js +0 -197
- package/dist/redis/index.js.map +0 -1
- package/dist/s3/index.cjs +0 -891
- package/dist/s3/index.cjs.map +0 -1
- package/dist/s3/index.d.cts +0 -112
- package/dist/s3/index.d.ts +0 -112
- package/dist/s3/index.js +0 -555
- package/dist/s3/index.js.map +0 -1
package/NOTICE
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
@cloudbitmaps/core
|
|
2
2
|
Copyright 2026 Sharvil Kadam
|
|
3
3
|
|
|
4
4
|
This product includes software developed as an open-source project.
|
|
@@ -7,6 +7,6 @@ file. You may obtain a copy of the License at:
|
|
|
7
7
|
|
|
8
8
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
@cloudbitmaps/core is the codec-agnostic engine of CloudBitmaps. It contains no bitmap
|
|
11
|
+
codec and no cloud SDK, and has zero runtime dependencies; a flavor package supplies the
|
|
12
|
+
codec through the CodecInterface seam.
|
package/README.md
CHANGED
|
@@ -1,20 +1,42 @@
|
|
|
1
1
|
# @cloudbitmaps/core
|
|
2
2
|
|
|
3
|
+
> **ESM-only, Node ≥ 22.12.** This package ships as ES modules; there is no CommonJS bundle.
|
|
4
|
+
> `require()` works on Node 22.12+ through `require(esm)`, but a runner with its own CommonJS loader
|
|
5
|
+
> (notably Jest in its default configuration) does not get that and needs `import` instead. On TypeScript,
|
|
6
|
+
> a CommonJS project needs `"module": "nodenext"` or `"node20"`. See the
|
|
7
|
+
> [repository README](https://github.com/cloudbitmaps/cloudbitmaps#install--entry-points) for the details.
|
|
8
|
+
|
|
9
|
+
|
|
3
10
|
The **codec-agnostic cloud engine** behind the [CloudBitmaps](https://github.com/cloudbitmaps/cloudbitmaps) family:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
a bounded RAM cache over immutable `.crbm` objects in STORAGE, serverless chunk-skipping intersection, the segment
|
|
12
|
+
registry, the write-once load-and-publish write path, generation GC, encryption-at-rest + crypto-shred, subject
|
|
13
|
+
erasure by generation rewrite, segment lifecycle (disposal, and a per-segment retention policy with the sweep
|
|
14
|
+
that enforces it), and the **in-memory and local-filesystem drivers**. It contains **no cloud SDK at all**: the
|
|
15
|
+
cloud drivers are their own packages (`@cloudbitmaps/s3` · `/gcs` · `/azure-blob`), each hosting both the
|
|
16
|
+
generations and the registry, and each built against `@cloudbitmaps/core/driver-kit` — the declared contract a
|
|
17
|
+
driver package depends on, which a third-party driver can use too.
|
|
8
18
|
|
|
9
19
|
## You probably want a flavor, not this package
|
|
10
20
|
|
|
11
21
|
This package holds no bitmap codec — that lives in a *flavor* package which depends on this one and supplies it
|
|
12
|
-
through the `CodecInterface` seam
|
|
22
|
+
through the `CodecInterface` seam, and it holds no cloud driver either — those are packages of their own.
|
|
23
|
+
Install a codec and a storage; `@cloudbitmaps/core` is a dependency of both and never named by you:
|
|
13
24
|
|
|
14
25
|
```bash
|
|
15
|
-
|
|
26
|
+
pnpm add @cloudbitmaps/roaring @cloudbitmaps/s3 # the roaring flavor (flagship), and the storage you have
|
|
27
|
+
# npm i @cloudbitmaps/roaring @cloudbitmaps/s3 # the same, with npm
|
|
16
28
|
```
|
|
17
29
|
|
|
30
|
+
> **On pnpm 10+, allow the one build script.** pnpm 10 skips dependency build scripts by default, so
|
|
31
|
+
> the `roaring` native addon never downloads and the package throws at `import` — while the install
|
|
32
|
+
> itself prints a warning and **exits 0**. Add this to your `package.json`, then install:
|
|
33
|
+
>
|
|
34
|
+
> ```json
|
|
35
|
+
> { "pnpm": { "onlyBuiltDependencies": ["roaring"] } }
|
|
36
|
+
> ```
|
|
37
|
+
>
|
|
38
|
+
> pnpm 9 and npm run it already. [Full symptoms and fixes](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
|
|
39
|
+
|
|
18
40
|
Depend on `@cloudbitmaps/core` directly only to **author a flavor or a driver**. It has **zero runtime
|
|
19
41
|
dependencies** of its own.
|
|
20
42
|
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var ERROR_BRAND = /* @__PURE__ */ Symbol.for("cloud-roaring.error");
|
|
3
|
+
var TRANSIENT_BRAND = /* @__PURE__ */ Symbol.for("cloud-roaring.error.transient");
|
|
4
|
+
var CloudRoaringError = class extends Error {
|
|
5
|
+
/** Cross-bundle brand — see the predicates ({@link isCloudRoaringError}, …). Non-enumerable-ish (symbol key ⇒ not in JSON). */
|
|
6
|
+
[ERROR_BRAND] = true;
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = new.target.name;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var ValidationError = class extends CloudRoaringError {
|
|
13
|
+
};
|
|
14
|
+
var WriteConflictError = class extends CloudRoaringError {
|
|
15
|
+
};
|
|
16
|
+
var IntegrityError = class extends CloudRoaringError {
|
|
17
|
+
};
|
|
18
|
+
var NotFoundError = class extends CloudRoaringError {
|
|
19
|
+
};
|
|
20
|
+
var UnsupportedError = class extends CloudRoaringError {
|
|
21
|
+
};
|
|
22
|
+
var CapabilityError = class extends CloudRoaringError {
|
|
23
|
+
};
|
|
24
|
+
var BudgetExceededError = class extends CloudRoaringError {
|
|
25
|
+
};
|
|
26
|
+
var KeyUnavailableError = class extends CloudRoaringError {
|
|
27
|
+
};
|
|
28
|
+
var TransientError = class extends CloudRoaringError {
|
|
29
|
+
/** A second brand so the whole transient subtree (incl. {@link TimeoutError}) is classifiable cross-bundle. */
|
|
30
|
+
[TRANSIENT_BRAND] = true;
|
|
31
|
+
constructor(message, options) {
|
|
32
|
+
super(message);
|
|
33
|
+
if (options && "cause" in options) this.cause = options.cause;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var TimeoutError = class extends TransientError {
|
|
37
|
+
};
|
|
38
|
+
function hasBrand(err, brand) {
|
|
39
|
+
return typeof err === "object" && err !== null && err[brand] === true;
|
|
40
|
+
}
|
|
41
|
+
function isCloudRoaringError(err) {
|
|
42
|
+
return hasBrand(err, ERROR_BRAND);
|
|
43
|
+
}
|
|
44
|
+
function isWriteConflictError(err) {
|
|
45
|
+
return isCloudRoaringError(err) && err.name === "WriteConflictError";
|
|
46
|
+
}
|
|
47
|
+
function isTransientError(err) {
|
|
48
|
+
return hasBrand(err, TRANSIENT_BRAND);
|
|
49
|
+
}
|
|
50
|
+
function isNotFoundError(err) {
|
|
51
|
+
return isCloudRoaringError(err) && err.name === "NotFoundError";
|
|
52
|
+
}
|
|
53
|
+
function isIntegrityError(err) {
|
|
54
|
+
return isCloudRoaringError(err) && err.name === "IntegrityError";
|
|
55
|
+
}
|
|
56
|
+
function isValidationError(err) {
|
|
57
|
+
return isCloudRoaringError(err) && err.name === "ValidationError";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/core/name-codec.ts
|
|
61
|
+
var KEY_SAFE = /[A-Za-z0-9._:-]/;
|
|
62
|
+
var PATH_SAFE = /[A-Za-z0-9._-]/;
|
|
63
|
+
var UTF8 = new TextEncoder();
|
|
64
|
+
function escapeChar(ch) {
|
|
65
|
+
let out = "";
|
|
66
|
+
for (const byte of UTF8.encode(ch)) {
|
|
67
|
+
out += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function encodeWith(name, safe) {
|
|
72
|
+
let out = "";
|
|
73
|
+
for (const ch of name) {
|
|
74
|
+
out += ch !== "%" && safe.test(ch) ? ch : escapeChar(ch);
|
|
75
|
+
}
|
|
76
|
+
return out.startsWith("_") ? `%5F${out.slice(1)}` : out;
|
|
77
|
+
}
|
|
78
|
+
function decodePercent(encoded) {
|
|
79
|
+
const bytes = [];
|
|
80
|
+
for (let i = 0; i < encoded.length; ) {
|
|
81
|
+
if (encoded[i] === "%" && i + 2 < encoded.length + 1) {
|
|
82
|
+
const hex = encoded.slice(i + 1, i + 3);
|
|
83
|
+
if (!/^[0-9A-Fa-f]{2}$/.test(hex)) return encoded;
|
|
84
|
+
bytes.push(parseInt(hex, 16));
|
|
85
|
+
i += 3;
|
|
86
|
+
} else {
|
|
87
|
+
for (const b of new TextEncoder().encode(encoded[i])) bytes.push(b);
|
|
88
|
+
i += 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return new TextDecoder("utf-8", { fatal: false, ignoreBOM: true }).decode(new Uint8Array(bytes));
|
|
92
|
+
}
|
|
93
|
+
function encodeNameForKey(name) {
|
|
94
|
+
return defuseDotComponent(encodeWith(name, KEY_SAFE));
|
|
95
|
+
}
|
|
96
|
+
function defuseDotComponent(encoded) {
|
|
97
|
+
return encoded === "." || encoded === ".." ? encoded.replaceAll(".", "%2E") : encoded;
|
|
98
|
+
}
|
|
99
|
+
function decodeNameFromKey(encoded) {
|
|
100
|
+
return decodePercent(encoded);
|
|
101
|
+
}
|
|
102
|
+
function encodeNameForPath(name) {
|
|
103
|
+
let encoded = encodeWith(name, PATH_SAFE);
|
|
104
|
+
const dotted = defuseDotComponent(encoded);
|
|
105
|
+
if (dotted !== encoded) return dotted;
|
|
106
|
+
const stem = encoded.split(".")[0] ?? "";
|
|
107
|
+
if (/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(stem)) {
|
|
108
|
+
encoded = escapeChar(encoded[0]) + encoded.slice(1);
|
|
109
|
+
}
|
|
110
|
+
if (encoded.endsWith(".")) encoded = `${encoded.slice(0, -1)}%2E`;
|
|
111
|
+
return encoded;
|
|
112
|
+
}
|
|
113
|
+
function decodeNameFromPath(encoded) {
|
|
114
|
+
return decodePercent(encoded);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/core/validate.ts
|
|
118
|
+
var CHUNK_KEY_MAX = 65535;
|
|
119
|
+
var MAX_NAME_LENGTH = 256;
|
|
120
|
+
var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
121
|
+
function encodedNameLength(value) {
|
|
122
|
+
return Math.max(encodeNameForKey(value).length, encodeNameForPath(value).length);
|
|
123
|
+
}
|
|
124
|
+
function validatePart(value, field) {
|
|
125
|
+
if (typeof value !== "string") {
|
|
126
|
+
throw new ValidationError(`${field} must be a string; got ${typeof value}`);
|
|
127
|
+
}
|
|
128
|
+
if (value.length === 0) {
|
|
129
|
+
throw new ValidationError(`${field} must not be empty`);
|
|
130
|
+
}
|
|
131
|
+
if (value.length > MAX_NAME_LENGTH) {
|
|
132
|
+
throw new ValidationError(
|
|
133
|
+
`${field} is too long: ${value.length} characters (limit ${MAX_NAME_LENGTH} once encoded for a storage key)`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (LONE_SURROGATE.test(value)) {
|
|
137
|
+
throw new ValidationError(
|
|
138
|
+
`${field} contains an unpaired surrogate, so it has no UTF-8 encoding and cannot be stored. This is usually a string sliced through an astral character (an emoji, say) \u2014 slice by code point instead.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const encoded = encodedNameLength(value);
|
|
142
|
+
if (encoded > MAX_NAME_LENGTH) {
|
|
143
|
+
throw new ValidationError(
|
|
144
|
+
`${field} is too long: ${encoded} characters once encoded for storage (limit ${MAX_NAME_LENGTH}). The name itself is ${value.length} characters \u2014 encoding expands anything outside [A-Za-z0-9._-], so a name of mostly non-ASCII text, or one full of colons, reaches the limit sooner than its length suggests.`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function validateSegmentRef(ref) {
|
|
149
|
+
validatePart(ref.segment, "segment");
|
|
150
|
+
if (ref.namespace !== void 0) validatePart(ref.namespace, "namespace");
|
|
151
|
+
}
|
|
152
|
+
function validateChunkRef(ref) {
|
|
153
|
+
validateSegmentRef(ref);
|
|
154
|
+
if (!Number.isInteger(ref.chunkKey) || ref.chunkKey < 0 || ref.chunkKey > CHUNK_KEY_MAX) {
|
|
155
|
+
throw new ValidationError(
|
|
156
|
+
`chunkKey must be an integer in [0, ${CHUNK_KEY_MAX}]; got ${ref.chunkKey}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/core/concurrency.ts
|
|
162
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
163
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
164
|
+
throw new ValidationError(`concurrency limit must be a positive integer; got ${limit}`);
|
|
165
|
+
}
|
|
166
|
+
const results = new Array(items.length);
|
|
167
|
+
let next = 0;
|
|
168
|
+
let failed = false;
|
|
169
|
+
let firstError;
|
|
170
|
+
const worker = async () => {
|
|
171
|
+
while (!failed) {
|
|
172
|
+
const i = next++;
|
|
173
|
+
if (i >= items.length) return;
|
|
174
|
+
try {
|
|
175
|
+
results[i] = await fn(items[i], i);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (!failed) {
|
|
178
|
+
failed = true;
|
|
179
|
+
firstError = err;
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
186
|
+
await Promise.all(workers);
|
|
187
|
+
if (failed) throw firstError;
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/drivers/_shared/keys.ts
|
|
192
|
+
var DEFAULT_NAMESPACE = "_default";
|
|
193
|
+
function namespaceKeyPart(namespace) {
|
|
194
|
+
return namespace === void 0 ? DEFAULT_NAMESPACE : encodeNameForKey(namespace);
|
|
195
|
+
}
|
|
196
|
+
function namespacePathPart(namespace) {
|
|
197
|
+
return namespace === void 0 ? DEFAULT_NAMESPACE : encodeNameForPath(namespace);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/core/ports.ts
|
|
201
|
+
var STORAGE_BACKEND_BRAND = /* @__PURE__ */ Symbol.for("cloudbitmaps.storage-backend");
|
|
202
|
+
function createBackend(halves) {
|
|
203
|
+
if (halves === null || typeof halves !== "object") {
|
|
204
|
+
throw new ValidationError("createBackend needs `{ storage, registry }`");
|
|
205
|
+
}
|
|
206
|
+
const { storage, registry } = halves;
|
|
207
|
+
if (storage === null || typeof storage !== "object" || typeof storage.putImmutable !== "function") {
|
|
208
|
+
throw new ValidationError(
|
|
209
|
+
"createBackend: `storage` must be an IStorageDriver (it has no `putImmutable`)"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (registry === null || typeof registry !== "object" || typeof registry.compareAndSwap !== "function") {
|
|
213
|
+
throw new ValidationError(
|
|
214
|
+
"createBackend: `registry` must be an IRegistryDriver (it has no `compareAndSwap`)"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return brandAsBackend({ storage, registry });
|
|
218
|
+
}
|
|
219
|
+
var STORAGE_BACKEND = STORAGE_BACKEND_BRAND;
|
|
220
|
+
function brandAsBackend(target) {
|
|
221
|
+
Object.defineProperty(target, STORAGE_BACKEND_BRAND, {
|
|
222
|
+
value: true,
|
|
223
|
+
enumerable: false,
|
|
224
|
+
writable: false,
|
|
225
|
+
configurable: false
|
|
226
|
+
});
|
|
227
|
+
return target;
|
|
228
|
+
}
|
|
229
|
+
function isStorageBackend(value) {
|
|
230
|
+
return typeof value === "object" && value !== null && value[STORAGE_BACKEND_BRAND] === true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/drivers/_shared/registry.ts
|
|
234
|
+
var STATUSES = ["active", "compacting", "erasing", "destroyed"];
|
|
235
|
+
var MAX_GOVERNANCE_BYTES = 64 * 1024;
|
|
236
|
+
var MAX_WRAPPED_DEKS = 8;
|
|
237
|
+
var MAX_WRAPPED_DEK_BYTES = 4 * 1024;
|
|
238
|
+
function validateGeneration(gen) {
|
|
239
|
+
if (gen === null) return;
|
|
240
|
+
if (!Number.isInteger(gen) || gen < 0) {
|
|
241
|
+
throw new ValidationError(
|
|
242
|
+
`currentGen must be a non-negative integer or null (no Storage generation yet); got ${gen}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function validatePatchGeneration(patch) {
|
|
247
|
+
if (!("currentGen" in patch)) return;
|
|
248
|
+
if (patch.currentGen === void 0) {
|
|
249
|
+
throw new ValidationError(
|
|
250
|
+
`currentGen was present in the patch but undefined. Pass \`null\` to clear the pointer (the segment has no Storage generation), or omit the key to leave it unchanged \u2014 an undefined value is refused because it used to be a no-op and would now un-publish the segment's Storage data.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
validateGeneration(patch.currentGen);
|
|
254
|
+
}
|
|
255
|
+
function validateStatus(status) {
|
|
256
|
+
if (!STATUSES.includes(status)) {
|
|
257
|
+
throw new ValidationError(`status must be one of ${STATUSES.join("/")}; got ${status}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function validateGovernance(meta, field) {
|
|
261
|
+
if (meta === void 0) return;
|
|
262
|
+
if (meta === null || typeof meta !== "object" || Array.isArray(meta)) {
|
|
263
|
+
throw new ValidationError(
|
|
264
|
+
`${field} must be a plain object (got ${meta === null ? "null" : typeof meta})`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
let json;
|
|
268
|
+
try {
|
|
269
|
+
json = JSON.stringify(meta);
|
|
270
|
+
} catch {
|
|
271
|
+
throw new ValidationError(`${field} must be JSON-serializable`);
|
|
272
|
+
}
|
|
273
|
+
if (json.length > MAX_GOVERNANCE_BYTES) {
|
|
274
|
+
throw new ValidationError(`${field} is ${json.length}B, exceeds cap ${MAX_GOVERNANCE_BYTES}B`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function validateWrappedDeks(value, isStored) {
|
|
278
|
+
if (value === void 0) return;
|
|
279
|
+
const fail = (msg) => {
|
|
280
|
+
throw isStored ? new IntegrityError(msg) : new ValidationError(msg);
|
|
281
|
+
};
|
|
282
|
+
if (!Array.isArray(value)) fail("wrappedDeks must be an array");
|
|
283
|
+
const list = value;
|
|
284
|
+
if (list.length === 0) fail("wrappedDeks must be non-empty when present");
|
|
285
|
+
if (list.length > MAX_WRAPPED_DEKS)
|
|
286
|
+
fail(`wrappedDeks has ${list.length} entries, cap ${MAX_WRAPPED_DEKS}`);
|
|
287
|
+
for (const w of list) {
|
|
288
|
+
if (w === null || typeof w !== "object") fail("wrappedDeks entry must be an object");
|
|
289
|
+
const { keyId, wrapped } = w;
|
|
290
|
+
if (typeof keyId !== "string" || keyId.length === 0)
|
|
291
|
+
fail("wrappedDeks entry needs a non-empty keyId");
|
|
292
|
+
if (typeof wrapped !== "string" || wrapped.length === 0)
|
|
293
|
+
fail("wrappedDeks entry needs a non-empty wrapped blob");
|
|
294
|
+
if (wrapped.length > MAX_WRAPPED_DEK_BYTES)
|
|
295
|
+
fail(`wrappedDeks entry is ${wrapped.length}B, cap ${MAX_WRAPPED_DEK_BYTES}B`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function validateNewRegistryRecord(rec) {
|
|
299
|
+
validateGeneration(rec.currentGen);
|
|
300
|
+
if (rec.status !== void 0) validateStatus(rec.status);
|
|
301
|
+
validateWrappedDeks(rec.wrappedDeks, false);
|
|
302
|
+
validateGovernance(rec.retention, "retention");
|
|
303
|
+
validateGovernance(rec.residency, "residency");
|
|
304
|
+
}
|
|
305
|
+
function validateRegistryPatch(patch) {
|
|
306
|
+
validatePatchGeneration(patch);
|
|
307
|
+
if (patch.status !== void 0) validateStatus(patch.status);
|
|
308
|
+
if ("wrappedDeks" in patch) validateWrappedDeks(patch.wrappedDeks, false);
|
|
309
|
+
if ("retention" in patch) validateGovernance(patch.retention, "retention");
|
|
310
|
+
if ("residency" in patch) validateGovernance(patch.residency, "residency");
|
|
311
|
+
}
|
|
312
|
+
var REGISTRY_SCHEMA_VERSION = 1;
|
|
313
|
+
function assertRegistrySchemaVersion(raw, ctx) {
|
|
314
|
+
if (raw === void 0) return;
|
|
315
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) {
|
|
316
|
+
throw new IntegrityError(`registry row has a malformed schemaVersion (${String(raw)}): ${ctx}`);
|
|
317
|
+
}
|
|
318
|
+
if (raw > REGISTRY_SCHEMA_VERSION) {
|
|
319
|
+
throw new UnsupportedError(
|
|
320
|
+
`registry row schemaVersion ${raw} is newer than this build reads (v${REGISTRY_SCHEMA_VERSION}): ${ctx}`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function serializeRegistryEnvelope(env) {
|
|
325
|
+
return JSON.stringify({
|
|
326
|
+
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
327
|
+
deleted: env.deleted,
|
|
328
|
+
record: env.record
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function registryCounterOf(record) {
|
|
332
|
+
if (!/^(0|[1-9]\d*)$/.test(record.token)) {
|
|
333
|
+
throw new IntegrityError(`registry row token is not a canonical counter: ${record.token}`);
|
|
334
|
+
}
|
|
335
|
+
const n = Number(record.token);
|
|
336
|
+
if (!Number.isSafeInteger(n)) {
|
|
337
|
+
throw new IntegrityError(`registry row token is out of safe-integer range: ${record.token}`);
|
|
338
|
+
}
|
|
339
|
+
return n;
|
|
340
|
+
}
|
|
341
|
+
function parseRegistryEnvelope(text, ctx) {
|
|
342
|
+
let parsed;
|
|
343
|
+
try {
|
|
344
|
+
parsed = JSON.parse(text);
|
|
345
|
+
} catch {
|
|
346
|
+
throw new IntegrityError(`registry row is not valid JSON: ${ctx}`);
|
|
347
|
+
}
|
|
348
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
349
|
+
throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
|
|
350
|
+
}
|
|
351
|
+
const env = parsed;
|
|
352
|
+
assertRegistrySchemaVersion(env.schemaVersion, ctx);
|
|
353
|
+
if (typeof env.deleted !== "boolean" || env.record === null || typeof env.record !== "object") {
|
|
354
|
+
throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
|
|
355
|
+
}
|
|
356
|
+
const r = env.record;
|
|
357
|
+
assertStoredRecordShape(r, ctx);
|
|
358
|
+
if (typeof r.token !== "string") {
|
|
359
|
+
throw new IntegrityError(`registry row is missing its token: ${ctx}`);
|
|
360
|
+
}
|
|
361
|
+
return { deleted: env.deleted, record: env.record };
|
|
362
|
+
}
|
|
363
|
+
function recordFromNew(ref, rec, now, token) {
|
|
364
|
+
return {
|
|
365
|
+
namespace: ref.namespace,
|
|
366
|
+
segment: ref.segment,
|
|
367
|
+
currentGen: rec.currentGen,
|
|
368
|
+
wrappedDeks: rec.wrappedDeks,
|
|
369
|
+
keyId: rec.keyId,
|
|
370
|
+
status: rec.status ?? "active",
|
|
371
|
+
retention: rec.retention,
|
|
372
|
+
residency: rec.residency,
|
|
373
|
+
createdAt: now,
|
|
374
|
+
updatedAt: now,
|
|
375
|
+
token
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function assertStoredRecordShape(r, ctx) {
|
|
379
|
+
if (typeof r.segment !== "string" || r.namespace !== void 0 && typeof r.namespace !== "string" || r.currentGen !== null && typeof r.currentGen !== "number" || typeof r.status !== "string" || typeof r.createdAt !== "number" || typeof r.updatedAt !== "number") {
|
|
380
|
+
throw new IntegrityError(`registry record is missing required fields: ${ctx}`);
|
|
381
|
+
}
|
|
382
|
+
if (r.currentGen !== null && (!Number.isInteger(r.currentGen) || r.currentGen < 0)) {
|
|
383
|
+
throw new IntegrityError(`registry record has an invalid currentGen (${r.currentGen}): ${ctx}`);
|
|
384
|
+
}
|
|
385
|
+
if (!STATUSES.includes(r.status)) {
|
|
386
|
+
throw new IntegrityError(`registry record has an unknown status (${r.status}): ${ctx}`);
|
|
387
|
+
}
|
|
388
|
+
if (r.keyId !== void 0 && typeof r.keyId !== "string") {
|
|
389
|
+
throw new IntegrityError(`registry record has an invalid keyId: ${ctx}`);
|
|
390
|
+
}
|
|
391
|
+
validateWrappedDeks(r.wrappedDeks, true);
|
|
392
|
+
for (const field of ["retention", "residency"]) {
|
|
393
|
+
const meta = r[field];
|
|
394
|
+
if (meta !== void 0 && (meta === null || typeof meta !== "object" || Array.isArray(meta))) {
|
|
395
|
+
throw new IntegrityError(`registry record has a non-object ${field}: ${ctx}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function applyRegistryPatch(prev, patch, now, token) {
|
|
400
|
+
return {
|
|
401
|
+
namespace: prev.namespace,
|
|
402
|
+
segment: prev.segment,
|
|
403
|
+
// `'currentGen' in patch`, NOT `patch.currentGen ?? prev.currentGen`. `null` is a legal, meaningful value
|
|
404
|
+
// here (no Storage generation yet), and `??` treats it as absent — so the nullish form would silently ignore a
|
|
405
|
+
// patch that clears the pointer and leave the old generation in place. The same reason `wrappedDeks` and
|
|
406
|
+
// `keyId` below use presence: any field whose null is a *value* cannot be merged with `??`.
|
|
407
|
+
currentGen: "currentGen" in patch ? patch.currentGen ?? null : prev.currentGen,
|
|
408
|
+
wrappedDeks: "wrappedDeks" in patch ? patch.wrappedDeks : prev.wrappedDeks,
|
|
409
|
+
keyId: "keyId" in patch ? patch.keyId : prev.keyId,
|
|
410
|
+
status: patch.status ?? prev.status,
|
|
411
|
+
retention: "retention" in patch ? patch.retention : prev.retention,
|
|
412
|
+
residency: "residency" in patch ? patch.residency : prev.residency,
|
|
413
|
+
createdAt: prev.createdAt,
|
|
414
|
+
updatedAt: now,
|
|
415
|
+
token
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export {
|
|
420
|
+
CloudRoaringError,
|
|
421
|
+
ValidationError,
|
|
422
|
+
WriteConflictError,
|
|
423
|
+
IntegrityError,
|
|
424
|
+
NotFoundError,
|
|
425
|
+
UnsupportedError,
|
|
426
|
+
CapabilityError,
|
|
427
|
+
BudgetExceededError,
|
|
428
|
+
KeyUnavailableError,
|
|
429
|
+
TransientError,
|
|
430
|
+
TimeoutError,
|
|
431
|
+
isCloudRoaringError,
|
|
432
|
+
isWriteConflictError,
|
|
433
|
+
isTransientError,
|
|
434
|
+
isNotFoundError,
|
|
435
|
+
isIntegrityError,
|
|
436
|
+
isValidationError,
|
|
437
|
+
encodeNameForKey,
|
|
438
|
+
decodeNameFromKey,
|
|
439
|
+
encodeNameForPath,
|
|
440
|
+
decodeNameFromPath,
|
|
441
|
+
MAX_NAME_LENGTH,
|
|
442
|
+
encodedNameLength,
|
|
443
|
+
validateSegmentRef,
|
|
444
|
+
validateChunkRef,
|
|
445
|
+
mapWithConcurrency,
|
|
446
|
+
DEFAULT_NAMESPACE,
|
|
447
|
+
namespaceKeyPart,
|
|
448
|
+
namespacePathPart,
|
|
449
|
+
createBackend,
|
|
450
|
+
STORAGE_BACKEND,
|
|
451
|
+
brandAsBackend,
|
|
452
|
+
isStorageBackend,
|
|
453
|
+
validateNewRegistryRecord,
|
|
454
|
+
validateRegistryPatch,
|
|
455
|
+
serializeRegistryEnvelope,
|
|
456
|
+
registryCounterOf,
|
|
457
|
+
parseRegistryEnvelope,
|
|
458
|
+
recordFromNew,
|
|
459
|
+
applyRegistryPatch
|
|
460
|
+
};
|
|
461
|
+
//# sourceMappingURL=chunk-BUPDGZRR.js.map
|