@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
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/core/errors.ts", "../src/core/name-codec.ts", "../src/core/validate.ts", "../src/core/concurrency.ts", "../src/drivers/_shared/keys.ts", "../src/core/ports.ts", "../src/drivers/_shared/registry.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Typed errors \u2014 callers learn *why* something failed, never by parsing strings. Retry is the driver\n * decorators' job, not the engine's: a driver classifies its backend's failures into this vocabulary, and\n * `withRetry` decides what is transient.\n */\n\n/**\n * Registry-symbol brands, so an error stays classifiable when the class object is not shared.\n *\n * In an ordinary install there is ONE copy of these classes and `instanceof` holds everywhere \u2014 across the\n * flavor, the driver packages and core itself \u2014 because every package leaves `@cloudbitmaps/*` external and\n * resolves core at runtime rather than inlining it.\n *\n * What still breaks it is a SECOND copy: a version skew between our packages that the installer could not\n * dedupe, or a bundler that emits core twice. Then a driver throws a class the catching side's `instanceof`\n * does not match \u2014 and silently, because a `catch` that stops matching just falls through, defeating\n * transient retry and the publish path's conflict handling with no error of its own.\n *\n * `Symbol.for` is identity-stable across copies, bundles and realms, so classify errors with the exported\n * predicates below (never `instanceof`) anywhere an error may cross that boundary.\n */\nconst ERROR_BRAND: unique symbol = Symbol.for('cloud-roaring.error');\nconst TRANSIENT_BRAND: unique symbol = Symbol.for('cloud-roaring.error.transient');\n\n/** Base class for every error CloudRoaring throws. */\nexport class CloudRoaringError extends Error {\n /** Cross-bundle brand \u2014 see the predicates ({@link isCloudRoaringError}, \u2026). Non-enumerable-ish (symbol key \u21D2 not in JSON). */\n readonly [ERROR_BRAND] = true as const;\n constructor(message: string) {\n super(message);\n // Subclass name (works under transpilation since we set it explicitly). Also the discriminator the\n // predicates match on \u2014 a runtime string, so it survives bundling where the class identity does not.\n this.name = new.target.name;\n }\n}\n\n/** Invalid caller input (bad id, segment name, options). Raised before any storage call. */\nexport class ValidationError extends CloudRoaringError {}\n\n/** An OCC conditional write/delete lost the race \u2014 the row changed since it was read. */\nexport class WriteConflictError extends CloudRoaringError {}\n\n/** Bytes from a tier are corrupt, oversized, or fail a checksum/format check. */\nexport class IntegrityError extends CloudRoaringError {}\n\n/**\n * A requested object/row does not exist. Part of the driver error vocabulary; thrown by\n * the persistent drivers (S3, GCS, Azure Blob, local filesystem) \u2014 the in-memory drivers return\n * `null` instead.\n */\nexport class NotFoundError extends CloudRoaringError {}\n\n/**\n * This build/configuration cannot perform the requested operation, though nothing is malformed. Two uses:\n * (1) **format** \u2014 the bytes are well-formed but unreadable here (an unknown `.crbm` major version, an\n * encrypted file before the crypto path exists) \u2014 distinct from `IntegrityError` (corruption); and (2)\n * **store configuration** \u2014 an operation this store's wiring doesn't support (e.g. a lifecycle helper like\n * `eraseSubject`/`retireExpired` called on a store built without a storage backend). Raised at\n * operation time, before any mutation.\n */\nexport class UnsupportedError extends CloudRoaringError {}\n\n/**\n * A driver cannot meet a capability the chosen topology requires (e.g. a Storage driver without range\n * reads). Raised fail-fast at wiring time, never mid-operation.\n */\nexport class CapabilityError extends CloudRoaringError {}\n\n/**\n * An operation would exceed its per-op **denial-of-wallet budget** \u2014 too many backend requests for a single\n * `count`/`iterate`/`intersect`/`subjectReport`/`eraseSubject` call \u2014 so it is refused **before** fanning out\n * (hard invariant 6). Default-on but generous (normal ops never hit it); tune it\n * per store (`budget`) or per op, or disable with `budget: false`. Deterministic (never retried): the op is too\n * big by policy, not by luck. Each request's bytes are separately capped (the safe-deserialize ceiling), so\n * bounding the request count transitively bounds bytes. Carries the projected count + the limit, never data.\n */\nexport class BudgetExceededError extends CloudRoaringError {}\n\n/**\n * An encrypted segment's data key (DEK) cannot be unwrapped because the keystore holds none of the\n * key-encryption-keys (KEKs) its wrappings reference \u2014 the KEK was never configured, rotated away without\n * keeping the old key, or lost. Deterministic (never retried): without a KEK the ciphertext is unreadable by\n * design. The flip side of crypto-shred \u2014 when this is *intended* (a destroyed segment) the registry row is\n * already a `destroyed` tombstone; when it's *not*, restore the missing KEK (or its recovery KEK). Carries no\n * key material.\n */\nexport class KeyUnavailableError extends CloudRoaringError {}\n\n/**\n * A **transient** infrastructure fault that is safe to retry \u2014 throttling, a 5xx, a dropped connection,\n * a client-side request timeout. Drivers classify their backend's retryable faults and raise this (the\n * SDK-specific knowledge stays in the SDK-specific driver); the retry layer (`core/retry`) retries **only**\n * this class, never a deterministic error like {@link ValidationError}, {@link IntegrityError},\n * {@link NotFoundError}, or {@link WriteConflictError} (retrying those is pointless or wrong). The original\n * error is preserved in `cause` so callers can still inspect it.\n *\n * Note for logging hygiene: `cause` is the **raw SDK error**, which may carry operational\n * metadata (endpoint host, request IDs, `$metadata`). The library's own `message` is identifier-only and safe\n * to log; if you serialize the whole error *chain*, be aware you're including that metadata.\n */\nexport class TransientError extends CloudRoaringError {\n /** A second brand so the whole transient subtree (incl. {@link TimeoutError}) is classifiable cross-bundle. */\n readonly [TRANSIENT_BRAND] = true as const;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message);\n if (options && 'cause' in options) this.cause = options.cause;\n }\n}\n\n/**\n * A single attempt exceeded its time budget. Subclass of {@link TransientError} so the retry layer treats a\n * timeout as retryable by default \u2014 a stalled request often succeeds on a fresh connection. Raised by a\n * driver whose injected client reports a request timeout \u2014 setting one on your injected client is the\n * recommended way to bound a hang.\n */\nexport class TimeoutError extends TransientError {}\n\n/**\n * Bundle-safe error predicates \u2014 use these, not `instanceof`, wherever an error may cross the core\u2194driver\n * (`./s3` / `./gcs` / `./azure`) boundary (and prefer them in consumer `catch` blocks too, for the same reason). They\n * match the {@link ERROR_BRAND} registry brand + the runtime `name`, both of which survive separate bundling.\n */\nfunction hasBrand(err: unknown, brand: symbol): boolean {\n return (\n typeof err === 'object' && err !== null && (err as Record<symbol, unknown>)[brand] === true\n );\n}\n\n/** Any error thrown by CloudRoaring (any tier, any bundle). */\nexport function isCloudRoaringError(err: unknown): err is CloudRoaringError {\n return hasBrand(err, ERROR_BRAND);\n}\n\n/** An OCC conditional write/delete lost the race \u2014 retry the read-modify-write, don't fail. */\nexport function isWriteConflictError(err: unknown): err is WriteConflictError {\n return isCloudRoaringError(err) && err.name === 'WriteConflictError';\n}\n\n/** A retryable transient infrastructure fault (incl. {@link TimeoutError}). The retry layer keys on this. */\nexport function isTransientError(err: unknown): err is TransientError {\n return hasBrand(err, TRANSIENT_BRAND);\n}\n\n/** A requested object/row does not exist. */\nexport function isNotFoundError(err: unknown): err is NotFoundError {\n return isCloudRoaringError(err) && err.name === 'NotFoundError';\n}\n\n/** Corrupt/oversized/failed-checksum bytes from a tier. */\nexport function isIntegrityError(err: unknown): err is IntegrityError {\n return isCloudRoaringError(err) && err.name === 'IntegrityError';\n}\n\n/** Invalid caller input. */\nexport function isValidationError(err: unknown): err is ValidationError {\n return isCloudRoaringError(err) && err.name === 'ValidationError';\n}\n", "/**\n * Name \u2192 physical-key encoding. The library validates *length*, and encodes everything else.\n *\n * Lives in `core/` rather than beside the drivers because it is pure string logic \u2014 no I/O, no SDK, no\n * `node:` builtin \u2014 and because `validate.ts` needs it to measure a name against the key budget. Core may\n * not import a driver (invariant 7), and a driver may import core, so this is the only layer both can see.\n *\n * The old grammar was an allowlist \u2014 `[A-Za-z0-9._:-]`. Widening it one character at a time is what exposed the\n * real problem: the colon had been missing from it by accident rather than by decision, and nothing in the\n * process would have caught the next omission either. An allowlist rejects names for the storage layer's\n * convenience, and the storage layer's convenience is the library's job, not the caller's. So a name is **any\n * non-empty string**, and each physical boundary escapes what *it* cannot take literally.\n *\n * It also fixes a hazard the allowlist *permitted*: `con`, `nul` and `com1` are Windows device names that\n * validated cleanly and failed only on a user's machine. A grammar can be both too narrow and too permissive\n * at once, which is the argument against having one.\n *\n * Percent-encoding, because it is the one escape everybody already reads. `%` escapes itself as `%25` and is\n * encoded **first**, which is what makes the transform injective: every `%` in an encoded string starts an\n * escape, so decoding is unambiguous and two distinct names can never collide on one key.\n *\n * There are two alphabets, because the boundaries differ:\n *\n * - **Object keys** (S3, GCS, Azure Blob) take almost anything. They need `/` escaped so a name cannot\n * invent hierarchy or break a key parser that splits on it, `#` and `|` (see below), and control characters\n * because they are not legal in the XML an S3 LIST returns.\n * - **Filesystem paths** need all of that plus `:` (an NTFS alternate-data-stream separator, where a write can\n * *succeed* while `readdir` never lists the result), plus three hazards that are about the component as a\n * whole rather than its characters \u2014 see {@link encodeNameForPath}.\n *\n * **`#` and `|` are escaped for compatibility, not for any backend still here.** They were reserved because\n * the DynamoDB registry composed its partition key from `<namespace>#<segment>|\u2026`, and that driver is gone.\n * The escaping stays anyway: it is baked into every key already written, and un-reserving the two characters\n * would change what `encodeNameForKey` emits for a name containing either \u2014 silently moving those segments\n * to a key nothing looks up. A cheap two-character reservation is the right price for not migrating a\n * bucket; the property test below pins it.\n *\n * **Every name that was legal before encodes to itself on the OBJECT-KEY alphabet** \u2014 so S3, GCS and Azure\n * keys are byte-identical and there is nothing to migrate there. That is the property to preserve if this\n * file is ever edited, and a property test asserts it over the whole old grammar.\n *\n * The **path** alphabet is not identical, and the difference is a breaking change for an existing LocalFs\n * store. Two classes move: a Windows device-name stem (`con`, `nul`, `com1`, `con.backup`) and a trailing dot\n * (`a.`). Both were legal before and are escaped now \u2014 deliberately, since both silently alias or fail on\n * Windows \u2014 but a 0.9 store holding one will not find it after the upgrade, because the readers' round-trip\n * guard skips a spelling the driver would never have written. See the upgrade note in `CHANGELOG.md`.\n */\n\n/** Characters safe in an object key, left literal so existing keys are byte-identical. */\nconst KEY_SAFE = /[A-Za-z0-9._:-]/;\n/** Characters safe in a path component. As {@link KEY_SAFE} minus `:`. */\nconst PATH_SAFE = /[A-Za-z0-9._-]/;\n\nconst UTF8 = new TextEncoder();\n\n/**\n * Percent-encode one character's UTF-8 bytes: `\u00E9` \u2192 `%C3%A9`.\n *\n * Only ever called with a WELL-FORMED code point. `TextEncoder` maps an unpaired surrogate to U+FFFD, which\n * would make every surrogate \u2014 and U+FFFD itself \u2014 encode identically and silently break injectivity, so\n * `validate.ts` refuses a name that is not well-formed UTF-16 before any of this runs.\n */\nfunction escapeChar(ch: string): string {\n let out = '';\n for (const byte of UTF8.encode(ch)) {\n out += `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;\n }\n return out;\n}\n\nfunction encodeWith(name: string, safe: RegExp): string {\n let out = '';\n // Iterate by code POINT, not code unit: a surrogate pair is one character and must encode as one UTF-8\n // sequence. Indexing by `charAt` would split an emoji into two unpaired halves and produce garbage bytes.\n for (const ch of name) {\n out += ch !== '%' && safe.test(ch) ? ch : escapeChar(ch);\n }\n // A LEADING underscore is escaped, which is what keeps the reserved namespaces reachable only by us.\n //\n // `_default` is the physical stand-in for an absent namespace. Under the old grammar a name could not begin\n // with `_`, so the sentinel was unreachable for free \u2014 and dropping the grammar would have handed any caller\n // a way to name a namespace `_default` and have it resolve to everyone else's un-namespaced data. Escaping\n // the first `_` restores that guarantee by construction rather than by a blocklist that has to be maintained.\n //\n // It costs nothing and migrates nothing: no previously legal name began with `_`, so no stored key moves.\n return out.startsWith('_') ? `%5F${out.slice(1)}` : out;\n}\n\nfunction decodePercent(encoded: string): string {\n const bytes: number[] = [];\n for (let i = 0; i < encoded.length;) {\n if (encoded[i] === '%' && i + 2 < encoded.length + 1) {\n const hex = encoded.slice(i + 1, i + 3);\n if (!/^[0-9A-Fa-f]{2}$/.test(hex)) return encoded; // not an escape we wrote\n bytes.push(parseInt(hex, 16));\n i += 3;\n } else {\n for (const b of new TextEncoder().encode(encoded[i])) bytes.push(b);\n i += 1;\n }\n }\n // `ignoreBOM: true` is load-bearing, not a tidy-up. Without it `TextDecoder` STRIPS a leading U+FEFF, so\n // `%EF%BB%BForders` decodes to `orders` \u2014 a different, probably existing, segment. That is a collision in\n // the one direction everything else here rests on, and it is reachable: `validatePart` accepts U+FEFF, and\n // a BOM-prefixed name is what any spreadsheet export or CSV read without BOM stripping hands you.\n //\n // The visible symptom was worse than a failed round-trip. Each parser (`parseRegistryRow`,\n // `parseNamespaceDir`, `parseRegistryKey`) re-encodes what it decoded and refuses a spelling it cannot\n // reproduce, so a BOM-prefixed row could be WRITTEN and then silently skipped by `list()`,\n // `checkConsistency`, `exportSegments`, the retention sweep and subject erasure \u2014 present in the bucket and\n // invisible to every sweep over it.\n //\n // Decode-only, so nothing that works today changes: `encode` is untouched, no stored key moves, and the\n // only names whose decoding changes are ones that are currently rejected by that round-trip guard anyway.\n return new TextDecoder('utf-8', { fatal: false, ignoreBOM: true }).decode(new Uint8Array(bytes));\n}\n\n/**\n * Encode a name for use inside an **object key** (S3, GCS, Azure Blob).\n *\n * Leaves the historically-legal alphabet literal so existing keys do not move, and escapes everything else \u2014\n * including `/` (which would invent hierarchy and break a parser that splits on it), `#` and `|` (reserved by\n * the former DynamoDB registry and kept reserved so existing keys stay valid), and control characters (not\n * legal in S3's XML responses).\n */\nexport function encodeNameForKey(name: string): string {\n return defuseDotComponent(encodeWith(name, KEY_SAFE));\n}\n\n/**\n * Escape a component that is exactly `.` or `..`.\n *\n * Applied to **both** alphabets. It is obviously needed for a filesystem path, and it is needed for an object\n * key too: the library's own S3/GCS/Azure calls treat the key space as flat, but the moment those keys touch\n * something hierarchical the component resolves. `aws s3 sync`, `gsutil -m cp -r` and `azcopy` all write\n * `dest/tenant/../segments/\u2026`, which lands *outside* `tenant/`; s3fs and gcsfuse mounts do the same; and on\n * ADLS Gen2, where the namespace really is hierarchical, `prefix/../x` and `x` are the same object. Those are\n * the DR-restore and eject paths this library documents.\n *\n * Note the asymmetry it removes: `normalizeS3Prefix` already refuses `.`/`..` segments in a **prefix**, which\n * is trusted config, while the name \u2014 the attacker-influenced input \u2014 was left unguarded.\n *\n * Every dot is escaped rather than just the first, so the result cannot end in one either. Zero migration: `.`\n * failed the old leading-alphanumeric rule and `..` was rejected outright, so no previously legal name is\n * touched.\n */\nfunction defuseDotComponent(encoded: string): string {\n return encoded === '.' || encoded === '..' ? encoded.replaceAll('.', '%2E') : encoded;\n}\n\n/** Inverse of {@link encodeNameForKey}. */\nexport function decodeNameFromKey(encoded: string): string {\n return decodePercent(encoded);\n}\n\n/**\n * Encode a name for use as a **filesystem path component**.\n *\n * Everything {@link encodeNameForKey} escapes, plus `:`, plus three hazards that are properties of the whole\n * component rather than of any character in it. All three are silent corruption rather than errors, which is\n * why they are handled here instead of being left to the OS:\n *\n * 1. **`.` and `..`** are traversal. `join(root, '..')` leaves the storage root.\n * 2. **Windows reserved device names** \u2014 `CON`, `PRN`, `AUX`, `NUL`, `COM1`\u2013`COM9`, `LPT1`\u2013`LPT9`, case\n * insensitive and *with or without an extension*, so `con.0.crbm` is reserved too. Opening one addresses a\n * device rather than a file. Note the old grammar **permitted** these: `store.segment('con')` validated\n * cleanly and broke only on a user's Windows box.\n * 3. **A trailing `.` or space**, which Windows silently strips \u2014 so `a.` and `a` would become the same\n * directory, quietly merging two segments.\n *\n * Each is fixed by escaping the one character that defuses it, which keeps the encoding injective: a name that\n * needed defusing produces a `%XX` no other name produces.\n */\nexport function encodeNameForPath(name: string): string {\n let encoded = encodeWith(name, PATH_SAFE);\n\n // The fixes COMPOSE rather than short-circuit. An earlier draft returned straight out of the traversal case,\n // and `..` became `%2E.` \u2014 which ends in a dot, i.e. hazard 3, reintroduced by the fix for hazard 1.\n const dotted = defuseDotComponent(encoded);\n if (dotted !== encoded) return dotted;\n\n // A Windows device name is reserved with OR without an extension, so the test is on the stem.\n const stem = encoded.split('.')[0] ?? '';\n if (/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(stem)) {\n encoded = escapeChar(encoded[0] as string) + encoded.slice(1);\n }\n\n // Windows strips a trailing dot, which would alias two distinct names onto one path. A trailing SPACE is the\n // same hazard but needs no arm here: space is not in `PATH_SAFE`, so the charset already escaped it to `%20`.\n if (encoded.endsWith('.')) encoded = `${encoded.slice(0, -1)}%2E`;\n\n return encoded;\n}\n\n/** Inverse of {@link encodeNameForPath}. */\nexport function decodeNameFromPath(encoded: string): string {\n return decodePercent(encoded);\n}\n", "/**\n * Boundary validation for segment / namespace names.\n *\n * **A name is any non-empty string.** There is no character allowlist, because an allowlist rejects names for\n * the storage layer's convenience \u2014 and the storage layer's convenience is this library's problem, not the\n * caller's. Every physical boundary escapes what *it* cannot take literally (`core/name-codec.ts`),\n * so `dedup:2026-08-01`, `user@example.com`, `orders/2026`, `\u65E5\u672C\u8A9E` and `100%` are all ordinary names.\n *\n * That replaced a grammar which had banned the colon *by accident rather than by decision*: every dated-bucket\n * example the retention docs published threw, because prose in a fenced block is not run by anything. The\n * grammar also **permitted** names that were genuinely broken \u2014 `con`, `nul` and `com1` are Windows device\n * names, and `store.segment('con')` validated cleanly here and failed only on a user's machine. Encoding\n * handles both directions: it stops rejecting what is merely unfamiliar, and starts defusing what is actually\n * dangerous.\n *\n * What remains is **size** and **representability**, both real constraints rather than tastes.\n *\n * Size: S3 caps an object key at 1024 bytes and a name is only one part of it. The cap is applied to the\n * **encoded** length \u2014 and to the longer of the two encodings, since a path escapes `:` three-for-one where a\n * key leaves it literal \u2014 because a limit measured on the input would let a key exceed the backend's.\n *\n * Representability: a name must be well-formed UTF-16, because an unpaired surrogate has no UTF-8 encoding at\n * all. Letting one through would not merely store oddly; every lone surrogate encodes to the same replacement\n * bytes, so four distinct names would claim one key.\n */\nimport { ValidationError } from './errors';\nimport { encodeNameForKey, encodeNameForPath } from './name-codec';\nimport type { ChunkRef, SegmentRef } from './ports';\n\nconst CHUNK_KEY_MAX = 0xffff;\n\n/**\n * The most an encoded name may occupy in a physical key.\n *\n * S3's limit is 1024 bytes for the WHOLE key, which also carries the caller's prefix, the namespace, a fixed\n * infix (`/segments/`) and the `.<generation>.crbm` suffix. 256 leaves generous room for all of that with both\n * a namespace and a segment at the ceiling, and matches the limit the previous grammar advertised, so no name\n * that was legal before becomes illegal now.\n */\nexport const MAX_NAME_LENGTH = 256;\n\n/**\n * A high surrogate not followed by a low one, or a low surrogate not preceded by a high one.\n *\n * Deliberately NOT `/u` and not `String.prototype.isWellFormed()`: the flag would make the engine read\n * code points, which is the very distinction being tested, and the method needs an ES2024 lib target\n * this package does not set. Matching on code units is what sees a half of a pair.\n */\nconst LONE_SURROGATE = /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/;\n\n/**\n * How long a name is once storage has it \u2014 the longer of the two encodings.\n *\n * Exported because anything that composes a name out of other names (the due index does) has to measure it the\n * same way, and a second copy of that rule is how the two drifted apart before.\n */\nexport function encodedNameLength(value: string): number {\n return Math.max(encodeNameForKey(value).length, encodeNameForPath(value).length);\n}\n\nfunction validatePart(value: string, field: string): void {\n if (typeof value !== 'string') {\n throw new ValidationError(`${field} must be a string; got ${typeof value}`);\n }\n if (value.length === 0) {\n throw new ValidationError(`${field} must not be empty`);\n }\n // Cheap bound BEFORE encoding. Encoding never shrinks a string, so a name already over the cap in raw units\n // is over it encoded \u2014 and rejecting first means an attacker-supplied name cannot make us build a 60 MB\n // string to find out. (Measured: a 10-million-unit name cost 4.2 s of blocking CPU without this.)\n if (value.length > MAX_NAME_LENGTH) {\n throw new ValidationError(\n `${field} is too long: ${value.length} characters (limit ${MAX_NAME_LENGTH} once encoded for a storage key)`,\n );\n }\n // A name has to survive the round trip to UTF-8 and back. An unpaired surrogate does not: `TextEncoder`\n // replaces it with U+FFFD, so every lone surrogate AND U+FFFD itself would encode to the same bytes \u2014 four\n // distinct names collapsing onto one key, which is the one property everything else here rests on. There is\n // no UTF-8 for a lone surrogate, so this is a limit of the medium rather than a rule we chose.\n if (LONE_SURROGATE.test(value)) {\n throw new ValidationError(\n `${field} contains an unpaired surrogate, so it has no UTF-8 encoding and cannot be stored. This is ` +\n `usually a string sliced through an astral character (an emoji, say) \u2014 slice by code point instead.`,\n );\n }\n // Measured on the ENCODED form, because that is what a bucket has to hold \u2014 and on the LONGER of the two\n // encodings. A path escapes `:` three-for-one where a key leaves it literal, so measuring the key form alone\n // let `'a' + ':'.repeat(255)` pass the boundary and then fail inside the driver with a raw ENAMETOOLONG\n // rather than a typed error at the edge.\n const encoded = encodedNameLength(value);\n if (encoded > MAX_NAME_LENGTH) {\n throw new ValidationError(\n `${field} is too long: ${encoded} characters once encoded for storage (limit ${MAX_NAME_LENGTH}). The name ` +\n `itself is ${value.length} characters \u2014 encoding expands anything outside [A-Za-z0-9._-], so a name of ` +\n `mostly non-ASCII text, or one full of colons, reaches the limit sooner than its length suggests.`,\n );\n }\n}\n\nexport function validateSegmentRef(ref: SegmentRef): void {\n validatePart(ref.segment, 'segment');\n if (ref.namespace !== undefined) validatePart(ref.namespace, 'namespace');\n}\n\n/** Validate a chunk ref: the segment/namespace rules plus `chunkKey` \u2208 `[0, 65535]` (a u16). */\nexport function validateChunkRef(ref: ChunkRef): void {\n validateSegmentRef(ref);\n if (!Number.isInteger(ref.chunkKey) || ref.chunkKey < 0 || ref.chunkKey > CHUNK_KEY_MAX) {\n throw new ValidationError(\n `chunkKey must be an integer in [0, ${CHUNK_KEY_MAX}]; got ${ref.chunkKey}`,\n );\n }\n}\n", "/**\n * Bounded-concurrency fan-out \u2014 the missing primitive for running async work over many items without either\n * a thundering herd (unbounded `Promise.all` \u2192 N simultaneous object-store calls) or full serial latency\n * (`for await`). Pure + I/O-free, so it lives in `core/`; callers inject the async work.\n */\n\nimport { ValidationError } from './errors';\n\n/**\n * Run `fn` over `items` with at most `limit` promises in flight at once, returning results in **input order**\n * (regardless of completion order). If any `fn` rejects, the pool stops scheduling new work, lets the in-flight\n * tasks settle, and rejects with the **first** error \u2014 callers that must not abort on one failure (e.g. an\n * erasure ledger that records per-item faults) should catch inside `fn` so it never rejects.\n *\n * `items` is a materialized array on purpose: every caller here already enumerates its source (registry list,\n * S3 page, chunk map), so an array keeps the pool trivial and race-free (no shared-iterator hazard).\n */\nexport async function mapWithConcurrency<T, R>(\n items: readonly T[],\n limit: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n if (!Number.isInteger(limit) || limit < 1) {\n throw new ValidationError(`concurrency limit must be a positive integer; got ${limit}`);\n }\n const results = new Array<R>(items.length);\n let next = 0;\n let failed = false;\n let firstError: unknown;\n const worker = async (): Promise<void> => {\n while (!failed) {\n const i = next++;\n if (i >= items.length) return;\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n if (!failed) {\n failed = true; // first failure wins; stop scheduling new items (in-flight ones still settle)\n firstError = err;\n }\n return;\n }\n }\n };\n const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());\n await Promise.all(workers);\n if (failed) throw firstError;\n return results;\n}\n", "/**\n * Shared key-grammar fragments used by every driver's logical-ref \u2192 physical-key mapping.\n *\n * Extracted once the registry drivers became the third consumer of the same `_default` namespace sentinel\n * (it lived copy-pasted in `s3/keys` and `localfs/paths`). Pure string logic, no SDK, no I/O \u2014\n * lives in the SDK-free `_shared` bundle so any driver may import it.\n */\n\n/**\n * The physical stand-in for an **absent** namespace. A caller MAY name a namespace `_default`; it simply does not collide, because\n * `namespaceKeyPart`/`namespacePathPart` encode the caller's namespace (to `%5Fdefault`) and emit this\n * sentinel literally. The separation is a property of the encoding, not of a grammar \u2014 an earlier version of\n * this comment claimed the latter, and that claim is exactly what made the collision easy to reintroduce.\n */\nexport const DEFAULT_NAMESPACE = '_default';\n\n/**\n * The physical namespace component of an **object key**: the caller's namespace encoded, or the sentinel.\n *\n * The sentinel is emitted **literally, never encoded**, and that asymmetry is the whole point. Encoding it too\n * would send an absent namespace to `%5Fdefault` \u2014 exactly where a caller who names their namespace `_default`\n * already goes, since the encoder escapes their leading underscore. Both would land in one place and read each\n * other's data. Encoding only the caller's side means `_default` (ours) and `%5Fdefault` (theirs) are two\n * different strings, and no caller can produce the first.\n */\nexport function namespaceKeyPart(namespace: string | undefined): string {\n return namespace === undefined ? DEFAULT_NAMESPACE : encodeNameForKey(namespace);\n}\n\n/** As {@link namespaceKeyPart}, for a filesystem path component. */\nexport function namespacePathPart(namespace: string | undefined): string {\n return namespace === undefined ? DEFAULT_NAMESPACE : encodeNameForPath(namespace);\n}\n\n// The name codec lives in `core/` (pure string logic, and `validate.ts` needs it); imported here so the\n// drivers keep one import for key shaping, and re-exported for the same reason.\nimport { encodeNameForKey, encodeNameForPath } from '@/core/name-codec';\n\nexport {\n encodeNameForKey,\n decodeNameFromKey,\n encodeNameForPath,\n decodeNameFromPath,\n} from '@/core/name-codec';\n", "/**\n * Storage-driver contracts the engine depends on.\n *\n * Two tiers and one pointer. **Storage** is immutable object storage holding `.crbm` generations (`IStorageDriver`,\n * read by the engine through the per-chunk `StorageChunkSource` view the `.crbm` reader implements); the\n * **registry** is the authoritative per-segment record \u2014 which generation is current, the wrapped DEKs, the\n * governance metadata \u2014 under optimistic concurrency (`IRegistryDriver`). Drivers move opaque bytes and OCC\n * tokens; they never understand roaring or the `.crbm` layout.\n */\n\nimport { ValidationError } from './errors';\nimport type { BlobSink } from './blob';\nimport type { WrappedDek } from './crypto';\n\n/** Opaque optimistic-concurrency token \u2014 unique per write, compared by equality only. */\nexport type Token = string;\n\nexport interface SegmentRef {\n readonly namespace?: string;\n readonly segment: string;\n}\n\nexport interface ChunkRef extends SegmentRef {\n readonly chunkKey: number;\n}\n\n/** Identifies one immutable `.crbm` object \u2014 a single generation of a segment. */\nexport interface GenKey extends SegmentRef {\n readonly generation: number;\n}\n\n/**\n * A segment's grounded on-disk footprint \u2014 the current generation's Storage object bytes, read cheaply from the\n * `.crbm` footer/index (no payload reads). Powers the grounded `costReport()`.\n */\nexport interface SegmentSize {\n readonly sizeBytes: number;\n}\n\n/** Per-chunk read view of the immutable Storage tier (implemented by the `.crbm` reader). */\nexport interface StorageChunkSource {\n /** Read-only bytes for the chunk, or `null` if absent. Callers must not mutate the buffer. */\n getChunk(ref: ChunkRef): Promise<Uint8Array | null>;\n listChunkKeys(ref: SegmentRef): Promise<number[]>;\n /**\n * Optional: the current generation's grounded size, cheaply (from the already-parsed `.crbm` index \u2014 no\n * payload reads), or `null` if the segment has no Storage generation. Powers the grounded `costReport()`.\n */\n sizeOf?(ref: SegmentRef): Promise<SegmentSize | null>;\n /**\n * Optional: the current generation's **per-chunk cardinality** (`chunkKey \u2192 count`), read from the\n * already-parsed `.crbm` index with **no payload reads**, or `null` if the segment has no Storage generation.\n * Powers the free `count()` \u2014 the engine sums the index instead of fetching a single chunk. A source with no\n * index (e.g. the in-memory source) omits this, and `count()` falls back to fetching every chunk.\n */\n cardinalities?(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;\n /**\n * Optional: the segment's **current generation number** as this source resolves it right now (registry\n * `currentGen`, or the highest storage generation), or `null` if the segment has no Storage generation. The engine\n * keys its chunk cache by this so a generation bump (a load's publish) is observed \u2014 a new generation\n * misses the cache instead of serving a stale decoded chunk, and an erased id can't resurrect from a cached\n * superseded chunk. Cheap: served from the source's own (short-TTL-refreshed) snapshot, **not** a fresh\n * backend read per call. A source that pins one immutable generation for its whole lifetime and never\n * refreshes may omit this \u2014 the engine then keys the cache without a generation.\n */\n currentGeneration?(ref: SegmentRef): Promise<number | null>;\n /**\n * Optional: forget everything this source has derived from `ref` \u2014 its resolved snapshot, its open reader,\n * and any key material that reader captured \u2014 so the next read resolves from the registry again.\n *\n * The TTL refresh and the generation-keyed chunk cache are both built for **a publish that advances\n * `currentGen`**. Neither covers an event that *destroys* what the cache was derived from: an erasure that\n * deletes the generation holding the bit, a `dropSegment`, a crypto-shred, a retirement. After one of those\n * a source that had already resolved the segment keeps answering from memory \u2014 with no backend read at all,\n * so no storage-side control can close the window \u2014 until its TTL lapses, and **never** if it has no clock\n * or `cache.genTtlMs: 0` (\"pin forever\").\n *\n * Callers that destroy or retire a segment must call this. It is synchronous and best-effort: dropping\n * memoized state cannot fail, and a source that memoizes nothing may omit the method entirely.\n */\n invalidate?(ref: SegmentRef): void;\n /**\n * Optional: does this segment **exist as a registered name**, as distinct from resolving to no generation?\n *\n * `currentGeneration` collapses two very different states into `null`: a name nobody ever created, and a\n * segment that exists and is legitimately empty. For a *read* that distinction does not matter \u2014 both answer\n * empty. For an **operand of a combine** it is the difference between a suppression list with nobody on it\n * yet and a suppression list you misspelled, and those must not look alike: the first correctly suppresses\n * nothing, the second silently suppresses nothing while you believe it is working.\n *\n * Consulted **only** when an operand resolved to no chunks at all, so a normal combine never calls it.\n */\n exists?(ref: SegmentRef): Promise<boolean>;\n /**\n * Optional: an opaque token identifying **which bytes** a read of this segment will see right now \u2014\n * the generation *and* the incarnation of the name it belongs to.\n *\n * {@link StorageChunkSource.currentGeneration} is not enough to key a decoded-chunk cache, and the gap is not\n * theoretical: `nextGeneration` returns `max(currentGen, highest object) + 1`, so it **restarts at 0** once\n * a registry row is purged and the bucket emptied. A retired, re-created name therefore serves different\n * data at the same `currentGen`, and a cache keyed on `(segment, chunk, generation)` hands back the previous\n * incarnation's ids \u2014 an erased id reappearing, with no read to intercept.\n *\n * Note that putting the incarnation in the *object key* would not fix this: the new incarnation still starts\n * at generation 0, so the cache key collides either way. The identity has to reach the cache.\n *\n * Returns `null` when the segment resolves to no generation. A source that pins one immutable generation for\n * its lifetime may omit this, and the engine falls back to the generation alone.\n */\n currentVersion?(ref: SegmentRef): Promise<string | null>;\n}\n\n/** Capabilities a Storage driver advertises; validated at wiring time, fail-fast. */\nexport interface StorageCaps {\n /** REQUIRED \u2014 the format relies on byte-range reads. */\n readonly rangeRead: true;\n /** Largest single object the backend accepts (informs single-object-vs-shard \u2014 future). */\n readonly maxObjectBytes: number;\n /** Optional: enables the pure-object `LATEST`-pointer registry variant (S3 conditional put). */\n readonly conditionalPut?: boolean;\n}\n\n/**\n * Immutable object storage for `.crbm` generations. A \"dumb byte mover\": it\n * understands neither roaring nor the `.crbm` layout, only opaque bytes addressed by a {@link GenKey}.\n * The core never reuses a key, so puts are write-once.\n */\nexport interface IStorageDriver {\n capabilities(): StorageCaps;\n /**\n * Stream a new immutable generation. The driver opens a destination, hands `write` a {@link BlobSink},\n * then atomically commits (and computes the content hash). Throws if the key already exists (write-once).\n */\n putImmutable(\n key: GenKey,\n write: (sink: BlobSink) => Promise<void>,\n ): Promise<{ size: number; sha256: string }>;\n /** Range read; the caller bounds-checks. Out-of-range is rejected, never a short/adjacent read. */\n getRange(key: GenKey, offset: number, length: number): Promise<Uint8Array>;\n /** Speculative tail read: the last `min(maxBytes, size)` bytes + the total object size. */\n getTail(key: GenKey, maxBytes: number): Promise<{ bytes: Uint8Array; size: number }>;\n delete(key: GenKey): Promise<void>;\n /** Enumerate the generations present for a segment (orphan sweep / latest-gen resolution). */\n list(ref: SegmentRef): AsyncIterable<GenKey>;\n}\n\n/**\n * Lifecycle status of a segment. `active` is the steady state; `destroyed` is the post-crypto-shred tombstone\n * (the row is kept for audit but the segment is logically gone). `compacting`/`erasing` are **reserved** \u2014\n * stored and round-tripped so a row written by an earlier build still reads, but no writer in this build sets\n * them.\n */\nexport type RegistryStatus = 'active' | 'compacting' | 'erasing' | 'destroyed';\n\n/**\n * Free-form, JSON-serializable governance metadata \u2014 a **plain object** (both registry boundaries reject `null`,\n * an array or a primitive, because a reader that tests `'key' in meta` would throw an untyped `TypeError`).\n *\n * `retention` has one **reserved key with defined semantics**: `expiresAt`, an absolute epoch-ms after which a\n * retention sweep may retire the segment \u2014 written by `setSegmentRetention`, parsed by `readRetentionPolicy`. Any\n * other key is yours (a legal hold, a note, an owner) and is preserved across policy writes. `residency` is still\n * shape-later: stored and round-tripped, no semantics attached.\n */\nexport type GovernanceMeta = Record<string, unknown>;\n\n/**\n * One registry row \u2014 the authoritative per-segment record. Exactly one per segment.\n */\nexport interface RegistryRecord extends SegmentRef {\n /**\n * **The** authoritative LATEST pointer: which immutable Storage generation is current \u2014 or **`null` for a segment\n * that has no Storage generation yet.**\n *\n * `null` is not \"unknown\", it is a positive statement: *this segment exists and has no Storage data.* It is what\n * lets a segment have a registry row **before its first load** \u2014 `setSegmentRetention` mints one so a policy\n * can be recorded ahead of the data, and so the segment is reachable by `registry.list()` and therefore by\n * retention sweeps, `checkConsistency` and every other fleet-wide operation. The alternative \u2014 a row with\n * `currentGen: 0` and no object behind it \u2014 is the forbidden `missing-storage-generation` state, which fails per\n * read with `NotFoundError`. Generation resolution maps `null` onto the same path a segment with **no row**\n * takes: every read answers empty, and the first publish advances the pointer.\n */\n readonly currentGen: number | null;\n /**\n * Per-segment data-key (DEK) wrappings for encryption-at-rest: the DEK envelope-wrapped under one or more KEKs\n * (active + optional recovery). Reading unwraps with any held KEK; **crypto-shred deletes this whole list**,\n * making the segment's at-rest bytes permanently unrecoverable. Absent \u21D2 the segment is cleartext.\n */\n readonly wrappedDeks?: readonly WrappedDek[];\n /**\n * Optional **external** key reference (reserved) \u2014 e.g. a KMS key ARN for a future KMS keystore adapter that\n * keeps wrapped material in the KMS rather than in-band {@link wrappedDeks}. Unused by the in-process BYOK\n * keystore. Clearable on crypto-shred.\n */\n readonly keyId?: string;\n readonly status: RegistryStatus;\n /**\n * Governance policy. `retention.expiresAt` drives the retention sweep (see {@link GovernanceMeta}); `residency`\n * is stored and round-tripped with no semantics yet. Both must be plain objects, and both must survive a\n * `list()` projection \u2014 a fleet sweep reads the policy from the enumeration rather than per-segment.\n */\n readonly retention?: GovernanceMeta;\n readonly residency?: GovernanceMeta;\n /** Epoch-ms of creation / last mutation (from the driver's injected clock). */\n readonly createdAt: number;\n readonly updatedAt: number;\n /** Opaque OCC token \u2014 compare-by-equality, never reused (ABA-safe). */\n readonly token: Token;\n}\n\n/** The caller-settable fields at {@link IRegistryDriver.create} (audit + token are driver-managed). */\nexport interface NewRegistryRecord {\n /** `null` \u21D2 the segment has no Storage generation yet \u2014 see {@link RegistryRecord.currentGen}. */\n readonly currentGen: number | null;\n readonly wrappedDeks?: readonly WrappedDek[];\n readonly keyId?: string;\n /** Defaults to `'active'`. */\n readonly status?: RegistryStatus;\n readonly retention?: GovernanceMeta;\n readonly residency?: GovernanceMeta;\n}\n\n/** Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity + audit + token are off-limits). */\nexport type RegistryPatch = Partial<\n Pick<\n RegistryRecord,\n 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency'\n >\n>;\n\n/** Capabilities a registry driver advertises; validated fail-fast at wiring time. */\nexport interface RegCaps {\n /** REQUIRED \u2014 `currentGen` feeds read correctness + the publish CAS, so reads must be strongly consistent. */\n readonly strongRead: true;\n}\n\n/**\n * Brand for {@link StorageBackend}.\n *\n * `Symbol.for` rather than a class check, for the same reason the error brands are: a class object is only\n * shared inside one package's bundle. `@cloudbitmaps/roaring` carries its own copy of core, so a backend\n * built from one package and checked by the other would fail an `instanceof` on an ordinary install \u2014 no\n * version skew required \u2014 as would a genuine second copy from a skew or a duplicating bundler. A registered\n * symbol is identity-stable across all of them, and across realms.\n *\n * The string says `cloudbitmaps` while the error brands still say `cloud-roaring`: those are locked, because\n * changing one would break error identity against already-published bundles. A new brand has no such\n * constraint, so it uses the current name.\n */\nconst STORAGE_BACKEND_BRAND: unique symbol = Symbol.for('cloudbitmaps.storage-backend');\n\n/**\n * Where a store keeps everything: the generations, and the pointer saying which one is current.\n *\n * **Branded on purpose.** The two fields alone are not enough to qualify, and that is the whole point. This\n * shape is also {@link LoadDeps}/{@link GenerationDeps}, so before the brand any `{ storage, registry }`\n * object literal satisfied it \u2014 including one assembling halves from two *unrelated* stores, which the store\n * accepted and then answered **empty** for a segment that holds data, because it read a pointer from a place\n * nothing had ever been written. That is the exact silent-empty failure one-class-per-backend exists to\n * remove, and it was reachable in five lines.\n *\n * So a backend comes from `MemoryStorage`, `LocalFsStorage`, `S3Storage`, `GcsStorage` or\n * `AzureBlobStorage` \u2014 each deriving both halves from one bucket and one prefix \u2014 or from\n * {@link createBackend}, which is the same thing said on purpose. The halves stay readable as `.storage` /\n * `.registry`, because the free functions genuinely need them.\n *\n * **What this stops is the accident, not a determined caller.** The brand is a registered symbol, so it can\n * be written by hand; nothing here is a security boundary. It stops the two spellings people reach for\n * without deciding to \u2014 an object literal, and a spread of a real backend.\n */\nexport interface StorageBackend {\n /** Cross-bundle brand. Set by the backend classes; not part of the public surface. */\n readonly [STORAGE_BACKEND_BRAND]: true;\n /** Where the immutable `.crbm` generations live. */\n readonly storage: IStorageDriver;\n /** Where the `currentGen` pointer, the discovery index and the wrapped DEKs live. */\n readonly registry: IRegistryDriver;\n}\n\n/**\n * Build a backend from two halves you supply yourself.\n *\n * **Reach for a backend class first** \u2014 `MemoryStorage`, `LocalFsStorage`, `S3Storage`, `GcsStorage`,\n * `AzureBlobStorage`. Each derives both halves from one bucket and one prefix, so they cannot disagree, and\n * that is the whole reason the classes exist.\n *\n * This is the door for the cases a class cannot express, and they are real: a driver wrapped for auditing,\n * metrics, tenant scoping or client-side encryption; a registry in a database you already run; a\n * fault-injecting double in a test. What it is NOT is a shortcut \u2014 naming it is the point. The plain\n * `{ storage, registry }` literal is refused precisely because it let the two halves come from unrelated\n * places by accident, and answer **empty** rather than fail.\n *\n * It cannot check that the halves agree: `IStorageDriver` and `IRegistryDriver` do not expose a location, so\n * nothing here can compare one. Calling this is you taking that on.\n */\nexport function createBackend(halves: {\n readonly storage: IStorageDriver;\n readonly registry: IRegistryDriver;\n}): StorageBackend {\n if (halves === null || typeof halves !== 'object') {\n throw new ValidationError('createBackend needs `{ storage, registry }`');\n }\n const { storage, registry } = halves;\n if (\n storage === null ||\n typeof storage !== 'object' ||\n typeof storage.putImmutable !== 'function'\n ) {\n throw new ValidationError(\n 'createBackend: `storage` must be an IStorageDriver (it has no `putImmutable`)',\n );\n }\n if (\n registry === null ||\n typeof registry !== 'object' ||\n typeof registry.compareAndSwap !== 'function'\n ) {\n throw new ValidationError(\n 'createBackend: `registry` must be an IRegistryDriver (it has no `compareAndSwap`)',\n );\n }\n return brandAsBackend({ storage, registry });\n}\n\n/**\n * Stamp the brand on a backend, **non-enumerably**.\n *\n * Non-enumerable is load-bearing, not tidiness. A class field (`readonly [BRAND] = true`) emits an\n * *enumerable* own property, and spread and `Object.assign` copy exactly those \u2014 so\n * `{ ...backend, registry: other.registry }` carried the brand and was accepted, reproducing the silent-empty\n * bug this whole thing exists to close. That spelling is also the most idiomatic way a JS developer swaps one\n * half, which means the audience `createBackend` was added for walked straight past it.\n *\n * This does not make the brand unforgeable \u2014 the symbol is registered, so anyone who wants it can write\n * `Symbol.for('cloudbitmaps.storage-backend')`. That is deliberate effort equivalent to calling\n * `createBackend`, and it is not what the check is for. The check is for the accident.\n */\n/**\n * The brand key, for the type-only `declare` field a backend class carries.\n *\n * Exported from `@cloudbitmaps/core/driver-kit` and part of the driver contract: a third-party storage\n * package needs it to declare the field that {@link brandAsBackend} then stamps at runtime.\n */\nexport const STORAGE_BACKEND: typeof STORAGE_BACKEND_BRAND = STORAGE_BACKEND_BRAND;\n\nexport function brandAsBackend<T extends { storage: IStorageDriver; registry: IRegistryDriver }>(\n target: T,\n): T & StorageBackend {\n Object.defineProperty(target, STORAGE_BACKEND_BRAND, {\n value: true,\n enumerable: false,\n writable: false,\n configurable: false,\n });\n return target as T & StorageBackend;\n}\n\n/**\n * Is this a branded backend \u2014 from a backend class or from {@link createBackend}?\n *\n * Checks the brand, not the shape \u2014 see {@link StorageBackend} for why the shape alone is not enough.\n */\nexport function isStorageBackend(value: unknown): value is StorageBackend {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<symbol, unknown>)[STORAGE_BACKEND_BRAND] === true\n );\n}\n\n/**\n * Per-segment registry \u2014 the authoritative source of `currentGen`, the discovery index, and the wrapped-DEK\n * holder. One row per segment under OCC: a never-reused, equality-compared {@link Token} (ABA-safe across\n * delete\u2192recreate).\n */\nexport interface IRegistryDriver {\n capabilities(): RegCaps;\n /** The segment's record, or `null` if it doesn't exist (or was deleted). */\n get(ref: SegmentRef): Promise<RegistryRecord | null>;\n /** Create the row; throws {@link WriteConflictError} if it already exists (use CAS to mutate). */\n create(ref: SegmentRef, record: NewRegistryRecord): Promise<{ token: Token }>;\n /** Server-side compare-and-set: apply `patch` iff the stored token equals `expected`, else `WriteConflictError`. */\n compareAndSwap(ref: SegmentRef, expected: Token, patch: RegistryPatch): Promise<{ token: Token }>;\n /**\n * Discovery: every **existing** record, optionally scoped to one namespace. Order is unspecified.\n *\n * \"Existing\" means not `delete`d. A **`destroyed` tombstone is still a record and must be yielded** \u2014 a driver\n * that filters by `status` breaks callers silently, and two already depend on seeing them: `runConsistencyCheck`\n * skips them itself, and the retention sweep can only clean up a tombstone row it can see (filtering it makes\n * the cleanup a permanent no-op, indistinguishable from having nothing to do, while dead rows accumulate). Same\n * rule for every other field: a row with **`currentGen: null`** must be yielded like any other, and `retention`\n * must survive the projection \u2014 a fleet sweep reads the policy straight out of this enumeration rather than\n * paying a `get()` per segment, so a `list()` that drops the field means nothing ever expires, silently.\n */\n list(namespace?: string): AsyncIterable<RegistryRecord>;\n /** Remove the row (tombstoned for ABA-safety \u2014 a later `create` still gets a fresh, greater token). */\n delete(ref: SegmentRef): Promise<void>;\n}\n", "/**\n * Shared, SDK-free helpers for assembling + validating {@link RegistryRecord}s.\n *\n * The registry's record-construction, patch-application, and field-validation are identical across every\n * backend (memory / LocalFs / object store) \u2014 only the *storage* + OCC mechanics differ. This is the one home\n * for that logic so the three drivers can't drift. Pure: no I/O, no SDK, no clock (the caller passes `now`).\n */\nimport { IntegrityError, UnsupportedError, ValidationError } from '@/core/errors';\nimport type {\n NewRegistryRecord,\n RegistryPatch,\n RegistryRecord,\n SegmentRef,\n Token,\n} from '@/core/ports';\n\n/** The valid {@link RegistryStatus} values \u2014 used to validate both caller input and stored bytes. `compacting` and\n * `erasing` are reserved (no writer in this build sets them) but stay valid so an older row still reads. */\nconst STATUSES: readonly string[] = ['active', 'compacting', 'erasing', 'destroyed'];\n/** Cap on a serialized governance blob (retention/residency) \u2014 bounds row size so a row can't be bricked. */\nconst MAX_GOVERNANCE_BYTES = 64 * 1024;\n/** Bounds on the wrapped-DEK list (one DEK wrapped under a few KEKs) \u2014 keeps the row small + rejects abuse. */\nconst MAX_WRAPPED_DEKS = 8;\nconst MAX_WRAPPED_DEK_BYTES = 4 * 1024;\n\n/** `null` is legal and meaningful: the segment has no Storage generation yet (see `RegistryRecord.currentGen`). */\nfunction validateGeneration(gen: number | null): void {\n if (gen === null) return;\n if (!Number.isInteger(gen) || gen < 0) {\n throw new ValidationError(\n `currentGen must be a non-negative integer or null (no Storage generation yet); got ${gen}`,\n );\n }\n}\n\n/**\n * A patch that *mentions* `currentGen` must give it a real value \u2014 `null` to clear the pointer, or a generation.\n * `{ currentGen: undefined }` is refused rather than coerced, and the reason is a regression this check exists to\n * prevent: before `currentGen` was nullable, that patch was a **no-op** (the merge used `??`), so\n * `{ currentGen: maybeUndefined }` \u2014 which `strict` alone permits, since `exactOptionalPropertyTypes` is off \u2014\n * left the pointer alone. Under presence-based merging the same call would silently *un-publish* a live segment:\n * every Storage generation goes invisible, and `gcOrphanGenerations` then refuses to collect the objects (no pointer\n * \u21D2 nothing to compare against), so they are stranded and billed forever. Fail fast at the boundary instead.\n */\nfunction validatePatchGeneration(patch: RegistryPatch): void {\n if (!('currentGen' in patch)) return;\n // Dropping the old `?? null` is what actually restores the rejection (`validateGeneration(undefined)` fails the\n // integer test); this branch exists to make the *message* say which value to pass instead, because the generic\n // \"must be a non-negative integer or null\" reads like a type error rather than the trap it is.\n if (patch.currentGen === undefined) {\n throw new ValidationError(\n `currentGen was present in the patch but undefined. Pass \\`null\\` to clear the pointer (the segment has no ` +\n `Storage generation), or omit the key to leave it unchanged \u2014 an undefined value is refused because it used ` +\n `to be a no-op and would now un-publish the segment's Storage data.`,\n );\n }\n validateGeneration(patch.currentGen);\n}\n\nfunction validateStatus(status: string): void {\n if (!STATUSES.includes(status)) {\n throw new ValidationError(`status must be one of ${STATUSES.join('/')}; got ${status}`);\n }\n}\n\n/**\n * Validate a governance blob (retention/residency) at the write boundary: it must be JSON-serializable\n * (a `BigInt`/function/circular value fails fast with a typed error instead of a cryptic one deep in a\n * driver) and within a size cap (so an oversized blob can't write a row that's later unreadable \u2014 a brick).\n */\nfunction validateGovernance(meta: unknown, field: string): void {\n if (meta === undefined) return;\n // A plain object, not `null`/an array/a primitive. `JSON.stringify(null)` is 4 valid bytes, so without this the\n // size + serializability checks below happily store `\"retention\": null` \u2014 and `retention.expiresAt` is read with\n // an `in` test, which throws an untyped `TypeError` on a non-object and would abort a whole fleet retention\n // sweep rather than becoming one ledger entry. Reject at the write boundary; the read boundary\n // ({@link assertStoredRecordShape}) rejects it too, for rows written before this check or edited by hand.\n if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {\n throw new ValidationError(\n `${field} must be a plain object (got ${meta === null ? 'null' : typeof meta})`,\n );\n }\n let json: string;\n try {\n json = JSON.stringify(meta);\n } catch {\n throw new ValidationError(`${field} must be JSON-serializable`);\n }\n if (json.length > MAX_GOVERNANCE_BYTES) {\n throw new ValidationError(`${field} is ${json.length}B, exceeds cap ${MAX_GOVERNANCE_BYTES}B`);\n }\n}\n\n/**\n * Validate the wrapped-DEK list at the write/read boundary: a bounded array of `{ keyId, wrapped }` with\n * non-empty string fields and a per-entry size cap (so a row can't be bricked, and corrupt bytes are rejected\n * rather than reaching the keystore). `isStored` toggles the error class (write = ValidationError; read =\n * IntegrityError, invariant 5).\n */\nfunction validateWrappedDeks(value: unknown, isStored: boolean): void {\n if (value === undefined) return;\n const fail = (msg: string): never => {\n throw isStored ? new IntegrityError(msg) : new ValidationError(msg);\n };\n if (!Array.isArray(value)) fail('wrappedDeks must be an array');\n const list = value as unknown[];\n if (list.length === 0) fail('wrappedDeks must be non-empty when present');\n if (list.length > MAX_WRAPPED_DEKS)\n fail(`wrappedDeks has ${list.length} entries, cap ${MAX_WRAPPED_DEKS}`);\n for (const w of list) {\n if (w === null || typeof w !== 'object') fail('wrappedDeks entry must be an object');\n const { keyId, wrapped } = w as { keyId?: unknown; wrapped?: unknown };\n if (typeof keyId !== 'string' || keyId.length === 0)\n fail('wrappedDeks entry needs a non-empty keyId');\n if (typeof wrapped !== 'string' || wrapped.length === 0)\n fail('wrappedDeks entry needs a non-empty wrapped blob');\n if ((wrapped as string).length > MAX_WRAPPED_DEK_BYTES)\n fail(`wrappedDeks entry is ${(wrapped as string).length}B, cap ${MAX_WRAPPED_DEK_BYTES}B`);\n }\n}\n\n/** Validate the caller-settable fields at `create`. */\nexport function validateNewRegistryRecord(rec: NewRegistryRecord): void {\n validateGeneration(rec.currentGen);\n if (rec.status !== undefined) validateStatus(rec.status);\n validateWrappedDeks(rec.wrappedDeks, false);\n validateGovernance(rec.retention, 'retention');\n validateGovernance(rec.residency, 'residency');\n}\n\n/** Validate the caller-settable fields in a `compareAndSwap` patch. */\nexport function validateRegistryPatch(patch: RegistryPatch): void {\n validatePatchGeneration(patch);\n if (patch.status !== undefined) validateStatus(patch.status);\n if ('wrappedDeks' in patch) validateWrappedDeks(patch.wrappedDeks, false);\n if ('retention' in patch) validateGovernance(patch.retention, 'retention');\n if ('residency' in patch) validateGovernance(patch.residency, 'residency');\n}\n\n/**\n * The persisted envelope for a registry row: the record plus a tombstone flag. A **deleted** row keeps its\n * record (with an advanced counter) rather than being removed, so the monotonic token survives a\n * delete\u2192recreate \u2014 ABA-safety. Shared by the persistent drivers (LocalFs file, S3 object).\n *\n * `schemaVersion` is a wire-only concern \u2014 it stamps the persisted bytes, not the in-memory domain object \u2014\n * so it lives on the serialize/parse boundary ({@link serializeRegistryEnvelope} /\n * {@link parseRegistryEnvelope}), never on this interface.\n */\nexport interface RegistryEnvelope {\n readonly deleted: boolean;\n readonly record: RegistryRecord;\n}\n\n/**\n * Current registry-row schema version \u2014 a pre-1.0 format-freeze prerequisite. Every persistent\n * registry driver \u2014 every one of which persists the `{ deleted, record }` envelope \u2014 stamps its rows\n * with this so a reader can fail-closed on a future, incompatible layout instead of misparsing it. Bump only\n * on a backward-incompatible change. Policy: a **higher** stamp than this build knows \u2192 `UnsupportedError`\n * (fail-closed); an **absent** stamp \u2192 legacy pre-freeze row, tolerated as v1.\n */\nexport const REGISTRY_SCHEMA_VERSION = 1;\n\n/**\n * Validate a persisted registry row's `schemaVersion` (untrusted bytes, invariant 5): absent \u2192 legacy v1\n * (tolerated); a malformed value \u2192 `IntegrityError`; a version newer than this build \u2192 `UnsupportedError`\n * (fail-closed rather than misread a format we don't understand).\n */\nexport function assertRegistrySchemaVersion(raw: unknown, ctx: string): void {\n if (raw === undefined) return; // pre-stamp rows are v1\n if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1) {\n throw new IntegrityError(`registry row has a malformed schemaVersion (${String(raw)}): ${ctx}`);\n }\n if (raw > REGISTRY_SCHEMA_VERSION) {\n throw new UnsupportedError(\n `registry row schemaVersion ${raw} is newer than this build reads (v${REGISTRY_SCHEMA_VERSION}): ${ctx}`,\n );\n }\n}\n\n/** Serialize a registry envelope for persistence, stamping the current {@link REGISTRY_SCHEMA_VERSION}. */\nexport function serializeRegistryEnvelope(env: RegistryEnvelope): string {\n return JSON.stringify({\n schemaVersion: REGISTRY_SCHEMA_VERSION,\n deleted: env.deleted,\n record: env.record,\n });\n}\n\n/**\n * The OCC counter encoded in a record's token. A published row always has a **canonical decimal** token\n * (`String(counter)`), so anything else (`\"1e3\"`, `\"0x10\"`, `\" 5 \"`, `\"\"`, a >2^53 value) is corruption /\n * tampering \u2014 reject it (invariant 5) rather than let `Number()` coerce it and silently break monotonicity.\n */\nexport function registryCounterOf(record: RegistryRecord): number {\n if (!/^(0|[1-9]\\d*)$/.test(record.token)) {\n throw new IntegrityError(`registry row token is not a canonical counter: ${record.token}`);\n }\n const n = Number(record.token);\n if (!Number.isSafeInteger(n)) {\n throw new IntegrityError(`registry row token is out of safe-integer range: ${record.token}`);\n }\n return n;\n}\n\n/**\n * Parse + structurally validate a persisted `{ deleted, record }` envelope from stored bytes. A published row\n * is always whole (atomic write), so a parse failure or a missing/mistyped field means corruption/tampering \u2014\n * fail fast (invariant 5), never silently report \"absent\". `ctx` names the source (path/key) for the message.\n */\nexport function parseRegistryEnvelope(text: string, ctx: string): RegistryEnvelope {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n throw new IntegrityError(`registry row is not valid JSON: ${ctx}`);\n }\n // Guard before any property access: `JSON.parse` yields `null`/primitives for hostile bytes (e.g. `\"null\"`,\n // `\"5\"`), which would throw an *untyped* TypeError on the dereference below (invariant 5 wants a typed error).\n if (parsed === null || typeof parsed !== 'object') {\n throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);\n }\n const env = parsed as { schemaVersion?: unknown; deleted?: unknown; record?: unknown };\n assertRegistrySchemaVersion(env.schemaVersion, ctx);\n if (typeof env.deleted !== 'boolean' || env.record === null || typeof env.record !== 'object') {\n throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);\n }\n const r = env.record as Record<string, unknown>;\n assertStoredRecordShape(r, ctx);\n if (typeof r.token !== 'string') {\n throw new IntegrityError(`registry row is missing its token: ${ctx}`);\n }\n return { deleted: env.deleted, record: env.record as RegistryRecord };\n}\n\n/** Build a full record from a {@link NewRegistryRecord} plus identity, audit timestamps, and an OCC token. */\nexport function recordFromNew(\n ref: SegmentRef,\n rec: NewRegistryRecord,\n now: number,\n token: Token,\n): RegistryRecord {\n return {\n namespace: ref.namespace,\n segment: ref.segment,\n currentGen: rec.currentGen,\n wrappedDeks: rec.wrappedDeks,\n keyId: rec.keyId,\n status: rec.status ?? 'active',\n retention: rec.retention,\n residency: rec.residency,\n createdAt: now,\n updatedAt: now,\n token,\n };\n}\n\n/**\n * Fail-fast structural check on a record read back from a persistent tier (untrusted bytes, invariant 5):\n * a published row is always whole, so a missing/mistyped required field means corruption/tampering \u2014 reject\n * it rather than silently report \"absent\". `token` is checked by the caller (its source differs per backend:\n * an in-body field vs a derived OCC counter).\n */\nexport function assertStoredRecordShape(r: Record<string, unknown>, ctx: string): void {\n if (\n typeof r.segment !== 'string' ||\n (r.namespace !== undefined && typeof r.namespace !== 'string') ||\n (r.currentGen !== null && typeof r.currentGen !== 'number') ||\n typeof r.status !== 'string' ||\n typeof r.createdAt !== 'number' ||\n typeof r.updatedAt !== 'number'\n ) {\n throw new IntegrityError(`registry record is missing required fields: ${ctx}`);\n }\n // Enforce the same value invariants the write path checks, so corrupt/tampered bytes are rejected at the\n // read boundary (invariant 5) rather than leaking a bad currentGen/status downstream.\n if (r.currentGen !== null && (!Number.isInteger(r.currentGen) || r.currentGen < 0)) {\n throw new IntegrityError(`registry record has an invalid currentGen (${r.currentGen}): ${ctx}`);\n }\n if (!STATUSES.includes(r.status)) {\n throw new IntegrityError(`registry record has an unknown status (${r.status}): ${ctx}`);\n }\n // Rows written by earlier builds may carry the retired compaction bookkeeping (`dirtyChunkCount`, the lease\n // fields, `lastCompactedAt`, `consecutiveFailures`). They are ignored on read and dropped on the next write \u2014\n // `applyRegistryPatch` rebuilds the record from the fields this build knows.\n if (r.keyId !== undefined && typeof r.keyId !== 'string') {\n throw new IntegrityError(`registry record has an invalid keyId: ${ctx}`);\n }\n validateWrappedDeks(r.wrappedDeks, true); // invariant 5: reject a corrupt wrapped-DEK list on read-back\n // The governance blobs are the only fields whose SHAPE was never checked on read-back, and it matters now that\n // one of them carries semantics: `retention.expiresAt` is read with an `in` test, which throws an untyped\n // `TypeError` on a stored `null`/string/number. That is reachable \u2014 the write boundary only checks\n // JSON-serializability and size, so `\"retention\": null` round-trips through every driver from a hand-edit, an\n // older writer, or a restore \u2014 and it would abort a whole fleet retention sweep rather than becoming one\n // ledger entry. Reject it here, typed, at the trust boundary (invariant 5).\n for (const field of ['retention', 'residency'] as const) {\n const meta = r[field];\n if (meta !== undefined && (meta === null || typeof meta !== 'object' || Array.isArray(meta))) {\n throw new IntegrityError(`registry record has a non-object ${field}: ${ctx}`);\n }\n }\n}\n\n/**\n * Apply a patch to an existing record, returning a new one with a fresh `updatedAt` + `token` (identity and\n * `createdAt` are preserved). Optional fields use `'k' in patch` so a patch can *clear* them (set to\n * `undefined`, e.g. dropping `keyId` on crypto-shred); required fields use `??` (they always have a value).\n * Fields this build does not know (an older row's compaction bookkeeping) are not carried over.\n */\nexport function applyRegistryPatch(\n prev: RegistryRecord,\n patch: RegistryPatch,\n now: number,\n token: Token,\n): RegistryRecord {\n return {\n namespace: prev.namespace,\n segment: prev.segment,\n // `'currentGen' in patch`, NOT `patch.currentGen ?? prev.currentGen`. `null` is a legal, meaningful value\n // here (no Storage generation yet), and `??` treats it as absent \u2014 so the nullish form would silently ignore a\n // patch that clears the pointer and leave the old generation in place. The same reason `wrappedDeks` and\n // `keyId` below use presence: any field whose null is a *value* cannot be merged with `??`.\n currentGen: 'currentGen' in patch ? (patch.currentGen ?? null) : prev.currentGen,\n wrappedDeks: 'wrappedDeks' in patch ? patch.wrappedDeks : prev.wrappedDeks,\n keyId: 'keyId' in patch ? patch.keyId : prev.keyId,\n status: patch.status ?? prev.status,\n retention: 'retention' in patch ? patch.retention : prev.retention,\n residency: 'residency' in patch ? patch.residency : prev.residency,\n createdAt: prev.createdAt,\n updatedAt: now,\n token,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit sink — an injected, no-op-by-default seam for **security/compliance** events, distinct
|
|
3
|
+
* from the metrics sink (5a). Different audience (an audit log / SIEM, not a dashboard), different retention,
|
|
4
|
+
* and only the compliance-relevant *state changes* — never routine reads/writes (that's the metrics sink).
|
|
5
|
+
* It doubles as the GDPR Art. 30 "record of processing" surface: publishes, rewrites, and erasures.
|
|
6
|
+
*
|
|
7
|
+
* Like `Clock`/`Rng`/`IMetricsSink`, it's injected (into the operations that emit — bulk-load, the `*Into`
|
|
8
|
+
* verbs, the erasure rewrite, crypto-shred, disposal and the retention sweep) and wrapped exception-safe, so a
|
|
9
|
+
* buggy sink can never break the operation it observes.
|
|
10
|
+
* Events are vendor-neutral and carry no timestamp/actor — the sink runs synchronously at the event, so it
|
|
11
|
+
* stamps its own time / attaches the caller identity (keeps `core/` free of ambient time). As with metrics,
|
|
12
|
+
* `segment`/`namespace` are caller-controlled strings that may be PII — treat them accordingly when routing.
|
|
13
|
+
*
|
|
14
|
+
* Not (yet) emitted: **KEK rotation** — rotation here is operator-side keystore reconfiguration (key-id-tagged
|
|
15
|
+
* wrappings, no data re-encryption), so there is no library call to hook. A future `rewrapSegment()` op would
|
|
16
|
+
* add a `kek.rotate` variant; until then, audit key changes at your keystore-config layer.
|
|
17
|
+
*/
|
|
18
|
+
/** A security/compliance-relevant state change. Vendor-neutral; the sink adds its own timestamp/actor. */
|
|
19
|
+
export type AuditEvent = {
|
|
20
|
+
/** A new immutable Storage generation *became the segment's current generation* (via bulk-load publish). */
|
|
21
|
+
readonly kind: 'segment.publish';
|
|
22
|
+
readonly namespace?: string;
|
|
23
|
+
readonly segment: string;
|
|
24
|
+
readonly generation: number;
|
|
25
|
+
} | {
|
|
26
|
+
/**
|
|
27
|
+
* A segment's pointer was moved **backwards**, to a generation still in the bucket.
|
|
28
|
+
*
|
|
29
|
+
* The only non-forward-only pointer move in the library, and the only one no automatic path can perform —
|
|
30
|
+
* a human decided the current generation was wrong and named the one they wanted. Every other pointer move
|
|
31
|
+
* can be reconstructed from "a load happened"; this one is someone overriding the ordering rule the rest of
|
|
32
|
+
* the system relies on, which is precisely what an incident review or an Art. 30 record wants to find.
|
|
33
|
+
*
|
|
34
|
+
* `fromGeneration` is `null` when the segment had a row but no current generation.
|
|
35
|
+
*/
|
|
36
|
+
readonly kind: 'segment.rollback';
|
|
37
|
+
readonly namespace?: string;
|
|
38
|
+
readonly segment: string;
|
|
39
|
+
readonly fromGeneration: number | null;
|
|
40
|
+
readonly generation: number;
|
|
41
|
+
} | {
|
|
42
|
+
/**
|
|
43
|
+
* A load was **refused** — the generation was written, failed a guard, and was deleted again rather than
|
|
44
|
+
* published. The security-relevant fact is that a replacement the caller asked for did NOT happen, which a
|
|
45
|
+
* downstream system reconciling "the segment should now contain X" needs as much as it needs the publish.
|
|
46
|
+
*
|
|
47
|
+
* `reason` is `'empty'` (an empty result over a non-empty segment, with no `allowEmpty`),
|
|
48
|
+
* `'min-cardinality'`, `'min-retained'`, or `'superseded'` (another writer published a higher generation
|
|
49
|
+
* first). `cardinality` is what the refused generation would have contained.
|
|
50
|
+
*/
|
|
51
|
+
readonly kind: 'segment.load-refused';
|
|
52
|
+
readonly namespace?: string;
|
|
53
|
+
readonly segment: string;
|
|
54
|
+
readonly generation: number;
|
|
55
|
+
readonly reason: 'empty' | 'min-cardinality' | 'min-retained' | 'superseded';
|
|
56
|
+
readonly cardinality: number;
|
|
57
|
+
} | {
|
|
58
|
+
/**
|
|
59
|
+
* A generation was **rewritten**: a new generation derived from `fromGeneration` became current in its
|
|
60
|
+
* place. Today the one emitter is `eraseIdFromSegment` (a subject erasure clearing one id), and the event is
|
|
61
|
+
* emitted at the publish — before the superseded generation is collected — so the record exists the moment
|
|
62
|
+
* the generation without the id is authoritative. A `segment.publish` is NOT also emitted for a rewrite: a
|
|
63
|
+
* rewrite derives its content from the segment itself, a publish brings content in from outside.
|
|
64
|
+
*/
|
|
65
|
+
readonly kind: 'segment.rewrite';
|
|
66
|
+
readonly namespace?: string;
|
|
67
|
+
readonly segment: string;
|
|
68
|
+
readonly fromGeneration: number;
|
|
69
|
+
readonly generation: number;
|
|
70
|
+
} | {
|
|
71
|
+
/**
|
|
72
|
+
* A segment was **crypto-shredded** — its wrapped DEK(s) are gone, so its at-rest Storage bytes are now
|
|
73
|
+
* permanently unreadable. Emitted only for a genuine key shred, never for a cleartext tombstone (which
|
|
74
|
+
* leaves the Storage bytes readable) or an idempotent re-run.
|
|
75
|
+
*/
|
|
76
|
+
readonly kind: 'segment.erase';
|
|
77
|
+
readonly namespace?: string;
|
|
78
|
+
readonly segment: string;
|
|
79
|
+
} | {
|
|
80
|
+
/**
|
|
81
|
+
* A segment was **disposed of** — tombstoned and its storage reclaimed (its Storage generations deleted) by
|
|
82
|
+
* `dropSegment`, *without* a key shred.
|
|
83
|
+
*
|
|
84
|
+
* Deliberately a separate kind from {@link AuditEvent} `segment.erase`, and the distinction is the point.
|
|
85
|
+
* `segment.erase` attests that bytes are unreadable **everywhere, backups included** — the only claim that
|
|
86
|
+
* survives WORM. Deleting an object is strictly weaker: a noncurrent version, a cross-region replica or a
|
|
87
|
+
* PITR snapshot can still hold the cleartext. Emitting one kind for both would make a compliance dashboard
|
|
88
|
+
* over-attest, so a cleartext drop gets this instead.
|
|
89
|
+
*
|
|
90
|
+
* An **encrypted** segment dropped via `dropSegment` emits **both** — `segment.erase` for the key shred and
|
|
91
|
+
* this for the storage reclamation — because both things genuinely happened.
|
|
92
|
+
*
|
|
93
|
+
* `generationsDeleted` is how many Storage generations went. It can be 0 (a segment whose bytes were already
|
|
94
|
+
* gone), and it does not promise the storage is now fully reclaimed: check `DropResult.generationsRemaining`
|
|
95
|
+
* for that.
|
|
96
|
+
*/
|
|
97
|
+
readonly kind: 'segment.dispose';
|
|
98
|
+
readonly namespace?: string;
|
|
99
|
+
readonly segment: string;
|
|
100
|
+
readonly generationsDeleted: number;
|
|
101
|
+
} | {
|
|
102
|
+
/** A namespace erasure was executed. `segmentsShredded` is the count actually crypto-shredded (may be 0). */
|
|
103
|
+
readonly kind: 'namespace.erase';
|
|
104
|
+
readonly namespace: string;
|
|
105
|
+
readonly segmentsShredded: number;
|
|
106
|
+
};
|
|
107
|
+
/** The `kind` discriminant of an {@link AuditEvent}. */
|
|
108
|
+
export type AuditEventKind = AuditEvent['kind'];
|
|
109
|
+
/** Sink for {@link AuditEvent}s. Injected via the lifecycle options; omit it and nothing is recorded. */
|
|
110
|
+
export interface IAuditSink {
|
|
111
|
+
onEvent(event: AuditEvent): void;
|
|
112
|
+
}
|
|
113
|
+
/** The default sink: records nothing. */
|
|
114
|
+
export declare const NOOP_AUDIT: IAuditSink;
|
|
115
|
+
/**
|
|
116
|
+
* Wrap a sink so a throwing/buggy `onEvent` can never break the lifecycle operation it observes — audit is
|
|
117
|
+
* strictly observation. Returns {@link NOOP_AUDIT} unchanged (so the no-op case skips even the try/catch alloc).
|
|
118
|
+
*/
|
|
119
|
+
export declare function safeAudit(sink: IAuditSink): IAuditSink;
|
|
120
|
+
/** A ready-made sink that records events into an in-memory list — handy for tests + simple audit trails. */
|
|
121
|
+
export declare class RecordingAuditSink implements IAuditSink {
|
|
122
|
+
private readonly recorded;
|
|
123
|
+
onEvent(event: AuditEvent): void;
|
|
124
|
+
/** An independent copy of the recorded events, in emission order. */
|
|
125
|
+
snapshot(): AuditEvent[];
|
|
126
|
+
reset(): void;
|
|
127
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const U32_MAX = 4294967295;
|
|
2
|
+
export declare const CHUNK_COUNT = 65536;
|
|
3
|
+
export interface BitRoute {
|
|
4
|
+
/** Top 16 bits — the chunk key (0..65535). */
|
|
5
|
+
readonly chunkKey: number;
|
|
6
|
+
/** Bottom 16 bits — the value within the chunk (0..65535). */
|
|
7
|
+
readonly remainder: number;
|
|
8
|
+
}
|
|
9
|
+
/** Split a u32 id into `{ chunkKey, remainder }`. Throws `ValidationError` for non-u32 input. */
|
|
10
|
+
export declare function splitId(id: number): BitRoute;
|
|
11
|
+
/**
|
|
12
|
+
* Recombine a chunk key + remainder back into the u32 id. Precondition: `chunkKey` is a validated
|
|
13
|
+
* 16-bit value (tier-derived keys are validated upstream in the engine before reaching here, per
|
|
14
|
+
* invariant 5); `remainder` is masked to 16 bits.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Largest legal value inside a chunk payload. A chunk covers `CHUNK_COUNT` ids, so remainders run
|
|
18
|
+
* `[0, CHUNK_COUNT - 1]`. Exported so the engine can assert it on untrusted tier bytes.
|
|
19
|
+
*/
|
|
20
|
+
export declare const MAX_REMAINDER: number;
|
|
21
|
+
export declare function joinId(chunkKey: number, remainder: number): number;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Streaming append target for a single object being written. */
|
|
2
|
+
export interface BlobSink {
|
|
3
|
+
write(bytes: Uint8Array): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Random/tail read access to a single finished object. The total size comes back from `getTail` (the
|
|
7
|
+
* codec's entry point), so there's no separate `size()` — keeping the seam minimal for driver authors.
|
|
8
|
+
*/
|
|
9
|
+
export interface BlobReader {
|
|
10
|
+
/** Exactly `length` bytes starting at `offset`. Out-of-range is a `ValidationError`, never a short read. */
|
|
11
|
+
getRange(offset: number, length: number): Promise<Uint8Array>;
|
|
12
|
+
/** The last `min(maxBytes, size)` bytes plus the total size — the one-GET footer+index path. */
|
|
13
|
+
getTail(maxBytes: number): Promise<{
|
|
14
|
+
bytes: Uint8Array;
|
|
15
|
+
size: number;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
/** In-memory `BlobSink` that accumulates written bytes; `bytes()` returns the concatenation. */
|
|
19
|
+
export declare class BufferSink implements BlobSink {
|
|
20
|
+
private readonly chunks;
|
|
21
|
+
private length;
|
|
22
|
+
write(bytes: Uint8Array): Promise<void>;
|
|
23
|
+
bytes(): Uint8Array;
|
|
24
|
+
}
|
|
25
|
+
/** In-memory `BlobReader` over a fixed byte buffer. */
|
|
26
|
+
export declare class BufferReader implements BlobReader {
|
|
27
|
+
private readonly buffer;
|
|
28
|
+
constructor(buffer: Uint8Array);
|
|
29
|
+
getRange(offset: number, length: number): Promise<Uint8Array>;
|
|
30
|
+
getTail(maxBytes: number): Promise<{
|
|
31
|
+
bytes: Uint8Array;
|
|
32
|
+
size: number;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-op denial-of-wallet budget — hard invariant 6 (bounded memory and cost). A read/admin op is refused
|
|
3
|
+
* **before it fans out** if its projected fan-out would exceed `maxRequests` — so one runaway
|
|
4
|
+
* `count`/`iterate`/`intersect`/`subjectReport`/`eraseSubject` can't drive unbounded RCU/GET cost on a shared
|
|
5
|
+
* backend. The check is O(1) (a single comparison against the already-known fan-out size), so the hot path is
|
|
6
|
+
* untouched. Each request's payload is separately size-capped (the safe-deserialize ceiling), so bounding the
|
|
7
|
+
* fan-out count transitively bounds bytes — the budget is intentionally count-based, not a second byte
|
|
8
|
+
* accumulator that would add per-chunk work.
|
|
9
|
+
*
|
|
10
|
+
* **What the count is.** For the read ops it is the number of Storage **chunk fetches** (`count`/`iterate`: one per
|
|
11
|
+
* effective chunk; `intersect`: surviving keys × operands). For the admin scans it is the number of **segments
|
|
12
|
+
* fanned out to** (`subjectReport`: one `has()` each; `eraseSubject`: one generation rewrite each) — i.e. it
|
|
13
|
+
* bounds the *breadth* of the fan-out, not the request total of each segment's own rewrite. That's
|
|
14
|
+
* the right lever for a runaway (a million-segment sweep); it is not a byte/request meter.
|
|
15
|
+
*/
|
|
16
|
+
export interface Budget {
|
|
17
|
+
/** Max units of fan-out (Storage chunk fetches, or segments scanned) one op may issue before it is refused. */
|
|
18
|
+
readonly maxRequests: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Default: **on but generous** — even a full single-segment scan (≤ 65 536 chunks) or a large single-namespace
|
|
22
|
+
* admin scan stays well under it; it exists to catch a true runaway (a million-segment scan, a pathological
|
|
23
|
+
* intersection). Tune it down for tighter multi-tenant control, or set `budget: false` to disable.
|
|
24
|
+
*/
|
|
25
|
+
export declare const DEFAULT_BUDGET: Budget;
|
|
26
|
+
/** A store/per-op budget option: partial overrides, or `false` to disable entirely. */
|
|
27
|
+
export type BudgetOption = Partial<Budget> | false;
|
|
28
|
+
/** Resolve a budget option against a fallback to a concrete `Budget`, or `null` when disabled. Validates. */
|
|
29
|
+
export declare function resolveBudget(opt: BudgetOption | undefined, fallback: Budget): Budget | null;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a **per-op** budget override against the store's own resolved budget (never the raw global default):
|
|
32
|
+
* `undefined` inherits the store budget as-is (including a disabled `null`); a partial `{}` / omitted
|
|
33
|
+
* `maxRequests` inherits the store's *tightening* (so a per-op passthrough of an absent config value can't
|
|
34
|
+
* silently lift a tight tenant ceiling back to the generous default); `{ maxRequests }` replaces it; `false`
|
|
35
|
+
* disables. Falls back to {@link DEFAULT_BUDGET} only when the store itself has no budget (disabled).
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolvePerOpBudget(opt: BudgetOption | undefined, storeBudget: Budget | null): Budget | null;
|
|
38
|
+
/**
|
|
39
|
+
* Refuse an op whose projected fan-out exceeds the budget. **Call before fan-out**, with the exact fan-out size
|
|
40
|
+
* the op is about to issue (Storage chunk fetches, or segments to scan). O(1). No-op when `budget` is `null`
|
|
41
|
+
* (disabled).
|
|
42
|
+
*/
|
|
43
|
+
export declare function checkBudget(budget: Budget | null, projectedRequests: number, op: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* Drain an async iterable into an array, refusing **as soon as** the budget is exceeded.
|
|
46
|
+
*
|
|
47
|
+
* This exists because the obvious spelling is wrong in a way that is easy to miss:
|
|
48
|
+
*
|
|
49
|
+
* ```ts
|
|
50
|
+
* for await (const rec of registry.list(ns)) recs.push(rec);
|
|
51
|
+
* checkBudget(budget, recs.length, op); // ← too late; the memory is already spent
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* That pattern bounds the **fan-out** but not the **enumeration**, so a tight budget refuses the work only
|
|
55
|
+
* after paying for the list — which defeats the purpose. It was measured buffering 20,000 registry records
|
|
56
|
+
* under `budget: { maxRequests: 2 }` before throwing. Draining incrementally bounds resident memory to
|
|
57
|
+
* `O(budget)` instead of `O(fleet)`.
|
|
58
|
+
*
|
|
59
|
+
* **The error message is deliberately vaguer than `checkBudget`'s.** It reports "more than N" rather than an
|
|
60
|
+
* exact total, because knowing the exact total requires finishing the scan — which is precisely the cost being
|
|
61
|
+
* refused. An honest approximation beats an expensive exact number.
|
|
62
|
+
*
|
|
63
|
+
* A `null` budget (`budget: false`) means the caller has explicitly opted out of fan-out limits, so this
|
|
64
|
+
* degrades to a plain drain.
|
|
65
|
+
*/
|
|
66
|
+
export declare function collectWithinBudget<T>(source: AsyncIterable<T>, budget: Budget | null, op: string): Promise<T[]>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **bitmap-codec seam**.
|
|
3
|
+
*
|
|
4
|
+
* `core/` is **codec-agnostic**: `SegmentEngine`, the erasure rewrite, and the `.crbm` read/write helpers only ever
|
|
5
|
+
* construct and combine bitmaps through the {@link CodecInterface} factory + the {@link CodecBitmap} value type
|
|
6
|
+
* defined here — never a concrete implementation. The flagship codec is roaring (`roaringCodec`, in
|
|
7
|
+
* `@cloudbitmaps/roaring`), and another codec would plug in behind the same seam with zero engine or driver
|
|
8
|
+
* changes. That is the seam's value even with one codec shipped: a plain-bitset flavor was measured and
|
|
9
|
+
* **decided against** (above ~6% density a Roaring chunk already *is* an uncompressed bitset), so the seam is
|
|
10
|
+
* what let that question be answered by measurement rather than by a rewrite.
|
|
11
|
+
*
|
|
12
|
+
* **Homogeneity contract:** a single store uses a single codec, so every {@link CodecBitmap} an operation sees
|
|
13
|
+
* was produced by the same {@link CodecInterface}. The binary set ops ({@link CodecBitmap.orInPlace} etc.) may
|
|
14
|
+
* therefore assume `other` is the same concrete type and are not required to interoperate across codecs.
|
|
15
|
+
* (Corollary: a decoded-chunk cache is codec-specific — never share one `cache` across engines built with
|
|
16
|
+
* different codecs, or a cached bitmap from codec A could reach codec B's in-place op. The `CloudRoaring` facade
|
|
17
|
+
* mints the cache per store, so this cannot arise in normal use.)
|
|
18
|
+
*
|
|
19
|
+
* **Interface surface = exactly what the engine needs**, kept general enough for the known fast-follow codecs
|
|
20
|
+
* (positional/rank-select access + raw-bitset interop that `@cloudbitmaps/bitset` will add live on that
|
|
21
|
+
* package's own extended value type, not here — the engine never calls them). The one codec-linked concern
|
|
22
|
+
* this seam intentionally leaves in the format layer is the `.crbm` **payload codec id**
|
|
23
|
+
* (`PAYLOAD_CODEC_ROARING_PORTABLE` / `KNOWN_PAYLOAD_CODEC_IDS` in `crbm/format.ts`): it is stamped in the footer
|
|
24
|
+
* and validated on read. That generalization is **done** as of 0.7.0 — the field was `roaringSerializationId`
|
|
25
|
+
* and validated by equality against a single constant, which would have frozen a codec-specific name into the
|
|
26
|
+
* format at `1.0`. It is now a registry the reader checks membership against, so a future codec is a one-line
|
|
27
|
+
* registration rather than a major format version. Still deliberately outside this interface: a codec declares
|
|
28
|
+
* its bytes, the seam does not.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The value type a codec produces — a mutable set of `u32` with set algebra and portable (de)serialization.
|
|
32
|
+
* This is the shape `SafeBitmap` already has; the engine holds these, caches them, and merges tiers with them.
|
|
33
|
+
*/
|
|
34
|
+
export interface CodecBitmap {
|
|
35
|
+
/** Serialize with the codec's **stable, portable** format (never a frozen/unsafe variant). */
|
|
36
|
+
serialize(): Uint8Array;
|
|
37
|
+
add(value: number): void;
|
|
38
|
+
addMany(values: Iterable<number>): void;
|
|
39
|
+
remove(value: number): void;
|
|
40
|
+
removeMany(values: Iterable<number>): void;
|
|
41
|
+
has(value: number): boolean;
|
|
42
|
+
/** Cardinality. */
|
|
43
|
+
readonly size: number;
|
|
44
|
+
readonly isEmpty: boolean;
|
|
45
|
+
/** A deep copy — mutating the clone must not touch the original (the cache relies on this). */
|
|
46
|
+
clone(): CodecBitmap;
|
|
47
|
+
/** In-place union `this = this ∪ other`. `other` is from the same codec (see the homogeneity contract). */
|
|
48
|
+
orInPlace(other: CodecBitmap): void;
|
|
49
|
+
/** In-place difference `this = this \ other`. */
|
|
50
|
+
andNotInPlace(other: CodecBitmap): void;
|
|
51
|
+
/** In-place intersection `this = this ∩ other`. */
|
|
52
|
+
andInPlace(other: CodecBitmap): void;
|
|
53
|
+
/** Ascending iterator over the set values. */
|
|
54
|
+
[Symbol.iterator](): IterableIterator<number>;
|
|
55
|
+
toArray(): number[];
|
|
56
|
+
/**
|
|
57
|
+
* The largest value in the set, or `undefined` when empty.
|
|
58
|
+
*
|
|
59
|
+
* **Optional, and deliberately so.** The engine uses it for one thing: asserting that a payload it is about
|
|
60
|
+
* to interpret **as a chunk** holds only 16-bit remainders (see {@link assertChunkPayload}). A codec that
|
|
61
|
+
* cannot answer this in better than O(n) should simply omit it — the engine then skips the check rather
|
|
62
|
+
* than walking every value on the read path, which is the one thing this must never cost.
|
|
63
|
+
*
|
|
64
|
+
* Roaring answers it in O(1) from its container index, so the flagship codec implements it.
|
|
65
|
+
*/
|
|
66
|
+
maximum?(): number | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Re-encode for size, in place, immediately before a **storage** write. Representation only — this must never
|
|
69
|
+
* change membership, and `serialize()` afterwards must decode back to exactly the same set.
|
|
70
|
+
*
|
|
71
|
+
* **Optional, like {@link maximum}.** A codec whose encoding has no size decision to make (a plain bitset has
|
|
72
|
+
* one representation and nothing to choose) simply omits it, and the engine skips the call.
|
|
73
|
+
*
|
|
74
|
+
* WHY THIS EXISTS. Roaring picks per container between an array, a bitset and a RUN — but the run choice is
|
|
75
|
+
* not automatic in any implementation: it is a `runOptimize()` pass you have to ask for, and nothing here was
|
|
76
|
+
* asking. So two of the three container types were ever used, and run-shaped data paid list or bitmap prices
|
|
77
|
+
* for a run. Measured on the shipped codec: a contiguous 1,000,000-id range serialized to 128.1 KiB where
|
|
78
|
+
* run-encoding needs **0.2 KiB** (570×), and a 2,000-run shape 536.5 KiB against **8.5 KiB** (63×). Sparse
|
|
79
|
+
* data is unchanged, because there are no runs to find — the pass is not a gamble.
|
|
80
|
+
*
|
|
81
|
+
* WHY ONLY AT WRITE TIME. This is called where a whole immutable generation is written, so its cost amortizes
|
|
82
|
+
* over a write that is already serializing and checksumming every chunk — and a generation is read many times
|
|
83
|
+
* after. It is deliberately NOT called on the read path (a clone made for a combine is transient): the hot path
|
|
84
|
+
* must not pay for a win that only the stored bytes collect, per KISS/YAGNI.
|
|
85
|
+
*/
|
|
86
|
+
optimize?(): void;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A pluggable bitmap codec — the factory the codec-agnostic engine constructs {@link CodecBitmap}s through.
|
|
90
|
+
* Implementations **must** size-cap before handing untrusted bytes to any native decoder and use a safe
|
|
91
|
+
* (never a trusting/frozen) deserializer — hard invariant 5.
|
|
92
|
+
*/
|
|
93
|
+
export interface CodecInterface {
|
|
94
|
+
/** An empty set. */
|
|
95
|
+
empty(): CodecBitmap;
|
|
96
|
+
/** A set seeded from an iterable of `u32` values. */
|
|
97
|
+
fromValues(values: Iterable<number>): CodecBitmap;
|
|
98
|
+
/**
|
|
99
|
+
* Size-cap (`bytes.length <= maxBytes`) then portable-deserialize. Throws `IntegrityError` when the input
|
|
100
|
+
* exceeds the cap or fails to decode — the native decoder is never handed unbounded or unsafe-format input.
|
|
101
|
+
*/
|
|
102
|
+
safeDeserialize(bytes: Uint8Array, maxBytes: number): CodecBitmap;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a codec that a **public core entry point** was given, failing fast when it is missing.
|
|
106
|
+
*
|
|
107
|
+
* Why these entry points take `codec?` rather than a required field: `bulkLoadCrbmGeneration` /
|
|
108
|
+
* `eraseIdFromSegment` / `runExport` are call-compatible public API, and core cannot supply a default (the concrete
|
|
109
|
+
* codec lives in a *flavor* package that depends on core — a default here would invert that arrow). A **flavor**
|
|
110
|
+
* package binds the codec for its users (`@cloudbitmaps/roaring` re-exports codec-bound wrappers), so an
|
|
111
|
+
* application never reaches this throw; only someone calling `@cloudbitmaps/core` directly — i.e. a flavor or
|
|
112
|
+
* driver author — can, and for them the typed error names exactly what to pass.
|
|
113
|
+
*/
|
|
114
|
+
export declare function requireCodec(codec: CodecInterface | undefined, api: string): CodecInterface;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded-concurrency fan-out — the missing primitive for running async work over many items without either
|
|
3
|
+
* a thundering herd (unbounded `Promise.all` → N simultaneous object-store calls) or full serial latency
|
|
4
|
+
* (`for await`). Pure + I/O-free, so it lives in `core/`; callers inject the async work.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Run `fn` over `items` with at most `limit` promises in flight at once, returning results in **input order**
|
|
8
|
+
* (regardless of completion order). If any `fn` rejects, the pool stops scheduling new work, lets the in-flight
|
|
9
|
+
* tasks settle, and rejects with the **first** error — callers that must not abort on one failure (e.g. an
|
|
10
|
+
* erasure ledger that records per-item faults) should catch inside `fn` so it never rejects.
|
|
11
|
+
*
|
|
12
|
+
* `items` is a materialized array on purpose: every caller here already enumerates its source (registry list,
|
|
13
|
+
* S3 page, chunk map), so an array keeps the pool trivial and race-free (no shared-iterator hazard).
|
|
14
|
+
*/
|
|
15
|
+
export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|