@cloudbitmaps/core 0.8.2 → 0.9.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.
Files changed (50) hide show
  1. package/README.md +2 -1
  2. package/dist/azure/index.d.cts +1 -1
  3. package/dist/azure/index.d.ts +1 -1
  4. package/dist/cassandra/index.cjs.map +1 -1
  5. package/dist/cassandra/index.d.cts +1 -1
  6. package/dist/cassandra/index.d.ts +1 -1
  7. package/dist/cassandra/index.js +1 -1
  8. package/dist/{chunk-L54FL5DW.js → chunk-BD22WDAN.js} +34 -7
  9. package/dist/chunk-BD22WDAN.js.map +1 -0
  10. package/dist/{chunk-YEHMG6W2.js → chunk-OZ6LAFV4.js} +2 -2
  11. package/dist/chunk-OZ6LAFV4.js.map +1 -0
  12. package/dist/dynamodb/index.cjs +32 -5
  13. package/dist/dynamodb/index.cjs.map +1 -1
  14. package/dist/dynamodb/index.d.cts +1 -1
  15. package/dist/dynamodb/index.d.ts +1 -1
  16. package/dist/dynamodb/index.js +2 -2
  17. package/dist/gcs/index.d.cts +1 -1
  18. package/dist/gcs/index.d.ts +1 -1
  19. package/dist/index.cjs +417 -35
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +244 -18
  22. package/dist/index.d.ts +244 -18
  23. package/dist/index.js +379 -34
  24. package/dist/index.js.map +1 -1
  25. package/dist/mongodb/index.cjs.map +1 -1
  26. package/dist/mongodb/index.d.cts +1 -1
  27. package/dist/mongodb/index.d.ts +1 -1
  28. package/dist/mongodb/index.js +1 -1
  29. package/dist/mysql/index.cjs.map +1 -1
  30. package/dist/mysql/index.d.cts +1 -1
  31. package/dist/mysql/index.d.ts +1 -1
  32. package/dist/mysql/index.js +1 -1
  33. package/dist/{ports-5BQTGKze.d.cts → ports-CvcvJWf6.d.cts} +46 -7
  34. package/dist/{ports-5BQTGKze.d.ts → ports-CvcvJWf6.d.ts} +46 -7
  35. package/dist/postgres/index.cjs.map +1 -1
  36. package/dist/postgres/index.d.cts +1 -1
  37. package/dist/postgres/index.d.ts +1 -1
  38. package/dist/postgres/index.js +1 -1
  39. package/dist/redis/index.cjs.map +1 -1
  40. package/dist/redis/index.d.cts +1 -1
  41. package/dist/redis/index.d.ts +1 -1
  42. package/dist/redis/index.js +1 -1
  43. package/dist/s3/index.cjs +32 -5
  44. package/dist/s3/index.cjs.map +1 -1
  45. package/dist/s3/index.d.cts +1 -1
  46. package/dist/s3/index.d.ts +1 -1
  47. package/dist/s3/index.js +1 -1
  48. package/package.json +1 -1
  49. package/dist/chunk-L54FL5DW.js.map +0 -1
  50. package/dist/chunk-YEHMG6W2.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/errors.ts","../../src/core/ports.ts","../../src/core/validate.ts","../../src/drivers/_shared/keys.ts","../../src/drivers/redis/keys.ts","../../src/drivers/redis/redis-errors.ts","../../src/drivers/redis/warm.ts"],"names":["randomUUID"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Typed errors — 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. The package ships as multiple bundles — the core entry and the `./s3` / `./dynamodb`\n * subpaths — and the builder inlines `core/errors` into each. A driver in a subpath bundle therefore throws a\n * *different* class object than the one the core engine/retry code would `instanceof`-check, so `instanceof`\n * silently returns false across that boundary in the published CJS package (defeating OCC/transient retry and\n * compaction race-handling). These `Symbol.for` brands are identity-stable across bundles/realms; classify\n * errors with the exported predicates below (never `instanceof`) anywhere an error may cross the 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 — see the predicates ({@link isCloudRoaringError}, …). Non-enumerable-ish (symbol key ⇒ 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 — 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 — 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 * persistent drivers from Phase 2 — the Phase-1 engine + in-memory drivers return `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** — the bytes are well-formed but unreadable here (an unknown `.crbm` major version, an\n * encrypted file before the crypto path exists) — distinct from `IntegrityError` (corruption); and (2)\n * **store configuration** — an operation this store's wiring doesn't support (e.g. a lifecycle helper like\n * `compact`/`eraseSubject` called on a store built without a raw cold driver + registry). 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 Cold 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** — too many backend requests for a single\n * `count`/`iterate`/`intersect`/`subjectReport`/`eraseSubject` call — so it is refused **before** fanning out\n * (Decision #3 / invariant T3). 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 — 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 — 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 — 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 (threat-model S12): `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 — a stalled request often succeeds on a fresh connection. Raised by a\n * driver whose injected client reports a request timeout — 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 — use these, not `instanceof`, wherever an error may cross the core↔driver\n * (`./s3` / `./dynamodb`) 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 — 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 * Storage-driver contracts the engine depends on.\n *\n * Phase 1 uses the subset needed by the in-memory engine: the per-chunk Warm store (under OCC) and a\n * per-chunk read view of Cold. Drivers move opaque bytes + an OCC token; they never understand roaring,\n * `adds`/`removes`, or the `.crbm` layout. The full `IColdDriver` (blob/range) + `IRegistryDriver` arrive\n * with generations in Phase 2/3 — the `.crbm` reader will implement `ColdChunkSource`.\n */\n\nimport type { BlobSink } from './blob';\nimport type { WrappedDek } from './crypto';\n\n/** Opaque optimistic-concurrency token — unique per write, compared by equality only. */\nexport type Token = string;\n\n/** Sentinel for `putConditional` meaning \"the row must not exist yet\" (create). */\n// `Symbol.for` (the global registry), NOT `Symbol()`: the package ships multiple bundles (the core entry and\n// the `./dynamodb` / `./s3` subpaths), and tsup inlines `core/ports` into each. A plain `Symbol('no-row')`\n// would be a DISTINCT instance per bundle, so the engine's `NO_ROW` wouldn't `===` the one the DynamoDb warm\n// driver compares against — every create would misroute to the token-fenced path and fail. A registry symbol\n// is identity-stable across bundles/realms. (Invisible in tests, which share one source module graph.)\nexport const NO_ROW: unique symbol = Symbol.for('cloud-roaring.no-row');\nexport type NoRow = typeof NO_ROW;\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 — a single generation of a segment. */\nexport interface GenKey extends SegmentRef {\n readonly generation: number;\n}\n\nexport interface WarmRow {\n readonly token: Token;\n /** Read-only: owned by the driver. Callers must NOT mutate the buffer (re-encode to write). */\n readonly bytes: Uint8Array;\n}\n\n/**\n * Read consistency for a Warm fetch (gap #9). `consistent: true` (the default — omitted ⇒ true) is a\n * strong, read-your-writes read, as the OCC read-modify-write path requires. `consistent: false` requests an\n * **eventually-consistent** read — on DynamoDB that is ~½ the RCU cost; a driver that is always strongly\n * consistent (in-memory, LocalFs) ignores it. The engine uses it only on read paths (`has`/`count`/`iterate`/\n * `intersect`) when the store opts into `warmReadConsistency: 'eventual'`, trading read-after-write for cost.\n */\nexport interface WarmReadOptions {\n readonly consistent?: boolean;\n}\n\n/** Per-chunk Warm store under optimistic concurrency. Returned byte buffers are read-only. */\nexport interface IWarmDriver {\n get(ref: ChunkRef, opts?: WarmReadOptions): Promise<WarmRow | null>;\n /** OCC write. `expected` = `NO_ROW` to create, else the token previously read. Throws `WriteConflictError` on mismatch. */\n putConditional(\n ref: ChunkRef,\n bytes: Uint8Array,\n expected: Token | NoRow,\n ): Promise<{ token: Token }>;\n /**\n * Fenced delete — only if the stored token still equals `expected`; throws `WriteConflictError`\n * otherwise. **Used by compaction (Phase 4); the Phase-1 engine never calls it** — drivers still\n * implement it to satisfy the conformance suite.\n */\n deleteConditional(ref: ChunkRef, expected: Token): Promise<void>;\n /** All dirty chunks of a segment, ascending `chunkKey`. Yielded `bytes` are read-only. */\n listChunks(\n ref: SegmentRef,\n opts?: WarmReadOptions,\n ): AsyncIterable<{ chunkKey: number } & WarmRow>;\n}\n\n/**\n * A segment's grounded on-disk footprint (Phase 5b) — the current generation's Cold object bytes, read\n * cheaply from the `.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 Cold tier (implemented by the `.crbm` reader from Phase 2). */\nexport interface ColdChunkSource {\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 (Phase 5b): the current generation's grounded size, cheaply (from the already-parsed\n * `.crbm` index — no payload reads), or `null` if the segment has no Cold generation. Powers the\n * grounded `costReport()`.\n */\n sizeOf?(ref: SegmentRef): Promise<SegmentSize | null>;\n /**\n * Optional (Phase 5c): the current generation's **per-chunk cardinality** (`chunkKey → count`), read\n * from the already-parsed `.crbm` index with **no payload reads**, or `null` if the segment has no Cold\n * generation. Powers the cheap `count()` — a warm-delta-free chunk is counted straight from the index\n * instead of fetching + deserializing it. A source with no index (e.g. the in-memory source) omits this,\n * and `count()` falls back to fetching + merging every chunk.\n */\n cardinalities?(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;\n /**\n * Optional (Phase B, gap #4): the segment's **current generation number** as this source resolves it right now\n * (registry `currentGen`, or the highest cold generation), or `null` if the segment has no Cold generation. The\n * engine keys its HOT chunk cache by this so a generation bump (a compaction commit) is observed — a new\n * generation misses the cache instead of serving a stale decoded chunk, and an erased id can't resurrect from a\n * cached pre-compaction chunk. Cheap: served from the source's own (short-TTL-refreshed) snapshot, **not** a\n * fresh backend read per call. A source that pins one immutable generation for its whole lifetime and never\n * refreshes may omit this — the engine then keys the cache without a generation, exactly as before.\n */\n currentGeneration?(ref: SegmentRef): Promise<number | null>;\n}\n\n/** Capabilities a Cold driver advertises; validated at wiring time, fail-fast. */\nexport interface ColdCaps {\n /** REQUIRED — the format relies on byte-range reads. */\n readonly rangeRead: true;\n /** Largest single object the backend accepts (informs single-object-vs-shard, B7 — 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 IColdDriver {\n capabilities(): ColdCaps;\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;\n * `compacting`/`erasing` are transient flags a daemon sets while it works (Phase 4d/4e); `destroyed` is the\n * post-crypto-shred tombstone (the row is kept for audit but the segment is logically gone). The 4c registry\n * stores and round-trips the field; the *transitions* are driven by their owning features.\n */\nexport type RegistryStatus = 'active' | 'compacting' | 'erasing' | 'destroyed';\n\n/** Free-form, JSON-serializable governance metadata (retention/residency policy); shape lands in Phase 6. */\nexport type GovernanceMeta = Record<string, unknown>;\n\n/**\n * One registry row — the authoritative per-segment record. Exactly one per segment.\n */\nexport interface RegistryRecord extends SegmentRef {\n /** **The** authoritative LATEST pointer: which immutable Cold generation is current. */\n readonly currentGen: number;\n /**\n * Per-segment data-key (DEK) wrappings for encryption-at-rest (Phase 4e): the DEK envelope-wrapped under one\n * or more KEKs (active + optional recovery). Reading unwraps with any held KEK; **crypto-shred deletes this\n * whole list**, making the segment's at-rest bytes permanently unrecoverable. Absent ⇒ the segment is\n * cleartext.\n */\n readonly wrappedDeks?: readonly WrappedDek[];\n /**\n * Optional **external** key reference (reserved) — 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 /**\n * Dirty-row hint written by `findCompactable`. Discovery still recomputes the live dirty count by scanning\n * Warm each cycle (the count that decides candidacy), but it now **reads** this hint to gate a change-guarded\n * CAS — the hint is only rewritten when the count actually moved, so an all-idle fleet issues no per-segment\n * registry writes. It does not yet drive candidacy — turning the Warm scan itself into O(dirty) is a\n * deferred cheap-enumeration fix.\n */\n readonly dirtyChunkCount: number;\n readonly status: RegistryStatus;\n /**\n * Daemon health (Phase D, gap #2): epoch-ms of the last **successfully committed** compaction, and the\n * count of **consecutive** compaction failures (reset to 0 by a successful commit). `lastCompactedAt`\n * powers a dead-man's-switch / staleness alarm; `consecutiveFailures` drives **poison-segment quarantine** —\n * discovery skips a segment past a failure threshold so one corrupt Warm row can't freeze the compaction of a\n * segment **that has a committed generation** forever (with no alarm) while its Warm backlog grows unbounded.\n * (A segment whose *very first* compaction keeps failing has no registry row to count against yet —\n * bootstrap faults are surfaced as `error` results but not yet quarantined; gap #2.) Both\n * **optional** (absent on rows written before Phase D, and on a segment never compacted) — read them as\n * `lastCompactedAt ?? undefined`, `consecutiveFailures ?? 0`.\n */\n readonly lastCompactedAt?: number;\n readonly consecutiveFailures?: number;\n /**\n * Compaction lease (Phase 4d): the opaque id of the worker currently compacting this segment, and the\n * epoch-ms at which its lease expires. `status === 'compacting'` with `leaseExpiresAt` in the future means\n * a live daemon owns it; once `leaseExpiresAt` is past, another worker may **steal** the lease (the prior\n * holder crashed). Both absent in the steady (`active`) state. The lease is an efficiency guard only — the\n * 2-phase commit is correct without it (OCC-fenced swap + write-once generations + version-fenced purge).\n */\n readonly leaseOwner?: string;\n readonly leaseExpiresAt?: number;\n /** Governance policy (Phase 6); stored + round-tripped now, semantics later. */\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 — compare-by-equality, never reused (ABA-safe), exactly like {@link WarmRow.token}. */\n readonly token: Token;\n}\n\n/** The caller-settable fields at {@link IRegistryDriver.create} (audit + token are driver-managed). */\nexport interface NewRegistryRecord {\n readonly currentGen: number;\n readonly wrappedDeks?: readonly WrappedDek[];\n readonly keyId?: string;\n /** Defaults to 0. */\n readonly dirtyChunkCount?: number;\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'\n | 'wrappedDeks'\n | 'keyId'\n | 'dirtyChunkCount'\n | 'status'\n | 'leaseOwner'\n | 'leaseExpiresAt'\n | 'lastCompactedAt'\n | 'consecutiveFailures'\n | 'retention'\n | 'residency'\n >\n>;\n\n/** Capabilities a registry driver advertises; validated fail-fast at wiring time. */\nexport interface RegCaps {\n /** REQUIRED — `currentGen` feeds read correctness + compaction CAS, so reads must be strongly consistent. */\n readonly strongRead: true;\n}\n\n/**\n * Per-segment registry — the authoritative source of `currentGen`, the discovery index, and (Phase 4e) the\n * wrapped-DEK holder. One row per segment under OCC: a\n * never-reused, equality-compared {@link Token} (ABA-safe across delete→recreate, like the Warm tier).\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 /** Discovery: every live record, optionally scoped to one namespace. Order is unspecified. */\n list(namespace?: string): AsyncIterable<RegistryRecord>;\n /** Remove the row (tombstoned for ABA-safety — a later `create` still gets a fresh, greater token). */\n delete(ref: SegmentRef): Promise<void>;\n}\n","/**\n * Boundary validation for segment / namespace names (finding S2).\n * Names become object keys, partition keys, and file paths — so they're validated before they\n * ever reach storage: strict charset, bounded length, no path traversal.\n */\nimport { ValidationError } from './errors';\nimport type { ChunkRef, SegmentRef } from './ports';\n\nconst CHUNK_KEY_MAX = 0xffff;\n\n// The locked name grammar: 1 leading alphanumeric + up to 255 more (max 256 chars).\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;\n\nfunction validatePart(value: string, field: string): void {\n if (typeof value !== 'string' || !NAME.test(value) || value.includes('..')) {\n throw new ValidationError(\n `${field} must match ${String(NAME)} and contain no \"..\": got ${JSON.stringify(value)}`,\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 grammar plus `chunkKey` ∈ `[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 * Shared key-grammar fragments used by every driver's logical-ref → physical-key mapping (Phase 4c).\n *\n * Extracted once the registry drivers became the 4th/5th consumer of the same `_default` namespace sentinel\n * (it lived copy-pasted in `s3/keys`, `dynamodb/keys`, `localfs/paths`). Pure string logic, no SDK, no I/O —\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. The name grammar forbids a leading underscore\n * so `_default` can never collide with a real\n * namespace — `segment(\"s\")` and `segment(\"s\", { namespace: \"_default\" })` would be a grammar error, not an\n * aliasing hazard.\n */\nexport const DEFAULT_NAMESPACE = '_default';\n\n/** Map an optional namespace to its physical part: the namespace itself, or {@link DEFAULT_NAMESPACE}. */\nexport function namespacePart(namespace: string | undefined): string {\n return namespace ?? DEFAULT_NAMESPACE;\n}\n","/**\n * Logical-ref → Redis key mapping for {@link RedisWarmDriver} (Phase 7).\n *\n * Pure, SDK-free string logic — unit-testable without a live Redis. Each chunk is one hash (`t` = token,\n * `b` = payload); each segment has one sorted-set index of its live chunk keys (Redis has no range scan, so\n * `listChunks` reads the index). Both keys for a segment share a **hash tag** `{<prefix>|<ns>|<seg>}` so they\n * always land in the same Redis Cluster slot — the OCC Lua script touches both atomically, which Cluster only\n * allows for same-slot keys. The name grammar (validateSegmentRef) forbids `|`/`{`/`}`/`:`/control chars in\n * ns/seg, and {@link normalizeRedisPrefix} forbids them in the prefix, so the tag can't be broken or aliased.\n */\nimport { ValidationError } from '@/core/errors';\nimport { validateChunkRef, validateSegmentRef } from '@/core/validate';\nimport type { ChunkRef, SegmentRef } from '@/core/ports';\nimport { namespacePart } from '../_shared/keys';\n\n/**\n * Validate the caller-supplied key prefix: no control characters and none of the reserved key/tag delimiters\n * (`{`, `}`, `|`, `:`), so it can neither break out of the hash tag nor alias another prefix's keyspace.\n * Empty/undefined ⇒ no prefix.\n */\nexport function normalizeRedisPrefix(prefix: string | undefined): string {\n if (prefix === undefined || prefix === '') return '';\n for (const ch of prefix) {\n const code = ch.charCodeAt(0);\n if (code < 0x20 || ch === '{' || ch === '}' || ch === '|' || ch === ':') {\n throw new ValidationError(\n `keyPrefix must not contain \"{\", \"}\", \"|\", \":\", or control characters`,\n );\n }\n }\n return prefix;\n}\n\n/** The shared hash-tag body for a segment: `<prefix>|<ns>|<seg>` (all delimiter-free by grammar). */\nfunction tag(prefix: string, ref: SegmentRef): string {\n return `${prefix}|${namespacePart(ref.namespace)}|${ref.segment}`;\n}\n\n/** The sorted-set index key holding a segment's live chunk keys (scored by chunkKey): `{tag}idx`. */\nexport function segmentIndexKey(prefix: string, ref: SegmentRef): string {\n validateSegmentRef(ref);\n return `{${tag(prefix, ref)}}idx`;\n}\n\n/** The hash key for one chunk's warm row (`t`/`b` fields): `{tag}c:<chunkKey>`. Validates the full ref. */\nexport function chunkHashKey(prefix: string, ref: ChunkRef): string {\n validateChunkRef(ref);\n return `{${tag(prefix, ref)}}c:${ref.chunkKey}`;\n}\n\n/** Parse a chunkKey out of a sorted-set index member (the canonical decimal the writer stored), or null. */\nexport function parseIndexMember(member: string): number | null {\n if (!/^(0|[1-9]\\d*)$/.test(member)) return null;\n const chunkKey = Number(member);\n return Number.isInteger(chunkKey) && chunkKey >= 0 && chunkKey <= 0xffff ? chunkKey : null;\n}\n","/**\n * Pure helpers for classifying Redis (`ioredis`) errors (Phase 7; transient class mirrors the other drivers).\n *\n * SDK-free + side-effect-free — they read only structural shapes off the thrown value (`err.code`, a Node\n * socket code; or the error message, which for a Redis server reply begins with the error-code word). The\n * driver signals an OCC conflict itself (its Lua CAS returns 0), so there's no \"conflict\" classifier — only\n * transient-vs-not. Everything not transient propagates unchanged so a real bug is never blind-retried.\n */\n\n/** A network-level error `code` string (starts with `E`, uppercase letters + underscore, no digits). */\nfunction networkCode(err: unknown): string | undefined {\n const code = (err as { code?: unknown } | null)?.code;\n return typeof code === 'string' && /^E[A-Z_]+$/.test(code) ? code : undefined;\n}\n\n/**\n * Transient Redis error-reply prefixes + ioredis connection-state messages. A Redis server error reply's\n * message begins with the code word (`LOADING`, `CLUSTERDOWN`, …); ioredis raises connection faults as an\n * `Error` whose message is one of the phrases below. All are safe to retry (the resilience layer rides them\n * out); a deterministic reply (`WRONGTYPE`, a Lua `ERR`, etc.) is NOT here, so it surfaces.\n */\nconst TRANSIENT_MESSAGE =\n /^(LOADING|CLUSTERDOWN|TRYAGAIN|MASTERDOWN|READONLY)\\b|Connection is closed|Command timed out|Stream isn't writeable|Reached the max retries per request limit|max number of clients reached|failed to refresh slots cache/i;\n\nexport function isTransient(err: unknown): boolean {\n const net = networkCode(err);\n if (\n net === 'ECONNRESET' ||\n net === 'ECONNABORTED' ||\n net === 'ETIMEDOUT' ||\n net === 'ESOCKETTIMEDOUT' ||\n net === 'ECONNREFUSED' ||\n net === 'EHOSTUNREACH' ||\n net === 'ENETUNREACH' ||\n net === 'EPIPE' ||\n net === 'EAI_AGAIN' ||\n net === 'ENOTFOUND'\n ) {\n return true;\n }\n return err instanceof Error && TRANSIENT_MESSAGE.test(err.message);\n}\n","/**\n * `RedisWarmDriver` — an {@link IWarmDriver} over Redis (Phase 7).\n *\n * The \"sub-millisecond writes, accept always-on\" warm tier. Uses the official `ioredis`, an **optional peer\n * dependency** — only consumers of `cloud-roaring/redis` install it. An `ioredis` client is **injected**\n * (dependency injection): the driver owns no connection/credential logic, so it's thin and reuses the\n * caller's client.\n *\n * Each chunk is one Redis **hash** (`t` = opaque OCC token, a random UUID per write; `b` = delta payload).\n * Redis has no range scan, so each segment keeps a **sorted-set index** of its live chunk keys (scored by\n * chunkKey) that `listChunks` reads. Optimistic concurrency is a **server-side atomic compare-and-set**: a\n * Lua script (Redis runs it atomically, single-threaded — no `WATCH`/`MULTI` needed) checks the stored token\n * and only then writes, keeping the hash and the index consistent in one step:\n * - **create-if-absent** — fails if the hash exists ⇒ {@link WriteConflictError}.\n * - **token-fenced update / delete** — fails unless the stored `t` equals the expected token ⇒\n * {@link WriteConflictError}.\n *\n * The hash and index for a segment share a Redis-Cluster **hash tag** so the multi-key script is slot-safe.\n * Tokens are not reused across delete→recreate (ABA-safe, D3): a delete removes the hash and a recreate mints\n * a fresh random UUID (probabilistic, like the Postgres driver; conformance D3 pins it). Drivers may use\n * `node:crypto`; only `core/` is bound by the determinism lint.\n */\nimport { randomUUID } from 'node:crypto';\nimport type { Redis } from 'ioredis';\nimport { IntegrityError, TransientError, ValidationError, WriteConflictError } from '@/core/errors';\nimport { NO_ROW } from '@/core/ports';\nimport type { ChunkRef, IWarmDriver, NoRow, SegmentRef, Token, WarmRow } from '@/core/ports';\nimport { chunkHashKey, normalizeRedisPrefix, parseIndexMember, segmentIndexKey } from './keys';\nimport { isTransient } from './redis-errors';\n\n/** listChunks value-fetch batch — bounds resident payloads to ~this many rows on a very wide segment. */\nconst DEFAULT_LIST_BATCH = 500;\n\n// Atomic OCC compare-and-set scripts. KEYS[1] = chunk hash, KEYS[2] = segment index (sorted set). Redis runs\n// each atomically, so the check-then-write can't race. Return 1 = applied, 0 = OCC conflict. Redis scripts\n// have no rollback, so the hash write is ordered before the index write (and DEL before ZREM): the only way\n// the second command could fail is a WRONGTYPE from external tampering on the shared-tag keyspace — outside\n// the driver's closed world, where hash and index are only ever these keys.\nconst PUT_LUA = `\nif ARGV[1] == 'create' then\n if redis.call('EXISTS', KEYS[1]) == 1 then return 0 end\n redis.call('HSET', KEYS[1], 't', ARGV[3], 'b', ARGV[5])\n redis.call('ZADD', KEYS[2], tonumber(ARGV[4]), ARGV[4])\n return 1\nelse\n local cur = redis.call('HGET', KEYS[1], 't')\n if not cur or cur ~= ARGV[2] then return 0 end\n redis.call('HSET', KEYS[1], 't', ARGV[3], 'b', ARGV[5])\n return 1\nend`;\nconst DEL_LUA = `\nlocal cur = redis.call('HGET', KEYS[1], 't')\nif not cur or cur ~= ARGV[1] then return 0 end\nredis.call('DEL', KEYS[1])\nredis.call('ZREM', KEYS[2], ARGV[2])\nreturn 1`;\n\n/** ioredis client augmented with the two custom OCC commands defined in the constructor. */\ntype WarmRedis = Redis & {\n crWarmPut(\n hashKey: string,\n indexKey: string,\n mode: string,\n expected: string,\n token: string,\n member: string,\n payload: Buffer,\n ): Promise<number>;\n crWarmDel(hashKey: string, indexKey: string, expected: string, member: string): Promise<number>;\n};\n\nexport interface RedisWarmDriverOptions {\n /**\n * A constructed `ioredis` client. The driver never connects/quits it — the caller owns its lifecycle.\n *\n * **The warm tier holds the only live copy of un-compacted deltas, so the instance must be configured for\n * durability + no silent loss:**\n * - `maxmemory-policy noeviction` (or no `maxmemory`). Under a key-evicting policy the chunk hash and its\n * segment-index entry are independent keys and can evict independently → `listChunks` silently drops or\n * ghosts a chunk (the hash-tag guarantees same-*slot*, not co-eviction). The driver sets no TTLs.\n * - Durable persistence (AOF, e.g. `appendfsync everysec`) if you rely on warm survival across a restart.\n * - **Reads must hit the master** — the OCC read-modify-write needs read-your-writes, so do NOT enable\n * replica reads (`scaleReads: 'slave'` / a replica `readFrom`), which could serve stale rows and break OCC.\n */\n readonly client: Redis;\n /** Optional key prefix so several logical stores can share one Redis (no `{`,`}`,`|`,`:`, or control chars). */\n readonly keyPrefix?: string;\n /** Advanced: `listChunks` value-fetch batch size (default 500). Tunes peak memory on wide segments. */\n readonly listPageSize?: number;\n}\n\nexport class RedisWarmDriver implements IWarmDriver {\n private readonly client: WarmRedis;\n private readonly keyPrefix: string;\n private readonly listBatch: number;\n\n constructor(options: RedisWarmDriverOptions) {\n this.keyPrefix = normalizeRedisPrefix(options.keyPrefix);\n const batch = options.listPageSize ?? DEFAULT_LIST_BATCH;\n if (!Number.isSafeInteger(batch) || batch < 1) {\n throw new ValidationError(`listPageSize must be a positive safe integer; got ${batch}`);\n }\n this.listBatch = batch;\n const client = options.client as WarmRedis;\n // Register the OCC scripts once (idempotent across driver instances sharing a client). ioredis caches\n // them and dispatches via EVALSHA with an automatic EVAL fallback.\n if (typeof client.crWarmPut !== 'function') {\n client.defineCommand('crWarmPut', { numberOfKeys: 2, lua: PUT_LUA });\n }\n if (typeof client.crWarmDel !== 'function') {\n client.defineCommand('crWarmDel', { numberOfKeys: 2, lua: DEL_LUA });\n }\n this.client = client;\n }\n\n async get(ref: ChunkRef): Promise<WarmRow | null> {\n const hashKey = chunkHashKey(this.keyPrefix, ref); // validates ref\n let raw: Record<string, Buffer>;\n try {\n raw = await this.client.hgetallBuffer(hashKey);\n } catch (err) {\n throw this.mapError(err);\n }\n if (Object.keys(raw).length === 0) return null; // absent hash → ioredis returns {}\n return this.rowFrom(raw, ref.chunkKey); // present-but-token-less ⇒ IntegrityError (untrusted-data posture)\n }\n\n async putConditional(\n ref: ChunkRef,\n bytes: Uint8Array,\n expected: Token | NoRow,\n ): Promise<{ token: Token }> {\n const hashKey = chunkHashKey(this.keyPrefix, ref); // validates ref\n const indexKey = segmentIndexKey(this.keyPrefix, ref);\n const token = randomUUID();\n const payload = Buffer.from(bytes); // copy: severs any caller-owned/reused input buffer\n const member = String(ref.chunkKey);\n let applied: number;\n try {\n applied =\n expected === NO_ROW\n ? await this.client.crWarmPut(hashKey, indexKey, 'create', '', token, member, payload)\n : await this.client.crWarmPut(\n hashKey,\n indexKey,\n 'update',\n expected,\n token,\n member,\n payload,\n );\n } catch (err) {\n throw this.mapError(err);\n }\n if (applied !== 1) {\n throw new WriteConflictError(\n expected === NO_ROW\n ? `chunk ${ref.chunkKey} already exists (create-if-absent)`\n : `OCC conflict on chunk ${ref.chunkKey}`,\n );\n }\n return { token };\n }\n\n async deleteConditional(ref: ChunkRef, expected: Token): Promise<void> {\n const hashKey = chunkHashKey(this.keyPrefix, ref); // validates ref\n const indexKey = segmentIndexKey(this.keyPrefix, ref);\n let applied: number;\n try {\n applied = await this.client.crWarmDel(hashKey, indexKey, expected, String(ref.chunkKey));\n } catch (err) {\n throw this.mapError(err);\n }\n if (applied !== 1) {\n throw new WriteConflictError(`fenced-delete conflict on chunk ${ref.chunkKey}`);\n }\n }\n\n async *listChunks(ref: SegmentRef): AsyncIterable<{ chunkKey: number } & WarmRow> {\n const indexKey = segmentIndexKey(this.keyPrefix, ref); // validates ref\n let members: string[];\n try {\n // The index is a sorted set scored by chunkKey, so ZRANGE returns members ascending. It holds only small\n // integers, so reading it whole is cheap; the (larger) payloads are fetched in bounded batches below.\n members = await this.client.zrange(indexKey, 0, -1);\n } catch (err) {\n throw this.mapError(err);\n }\n for (let i = 0; i < members.length; i += this.listBatch) {\n const batch = members.slice(i, i + this.listBatch);\n // Resolve members → chunk keys first, dropping any foreign/corrupt member, then fetch that batch's\n // hashes in one pipeline (bounded resident payloads on a very wide segment).\n const keys = batch\n .map((m) => ({ chunkKey: parseIndexMember(m) }))\n .filter((x): x is { chunkKey: number } => x.chunkKey !== null);\n let results: [Error | null, unknown][] | null;\n try {\n const pipeline = this.client.pipeline();\n for (const { chunkKey } of keys) {\n pipeline.hgetallBuffer(chunkHashKey(this.keyPrefix, { ...ref, chunkKey }));\n }\n results = await pipeline.exec();\n } catch (err) {\n throw this.mapError(err);\n }\n results ??= [];\n for (let j = 0; j < keys.length; j++) {\n const [err, raw] = results[j] ?? [null, undefined];\n if (err) throw this.mapError(err);\n const row = raw as Record<string, Buffer> | undefined;\n // A member with no hash means a delete raced our snapshot — skip it (matches the moving-target\n // semantics of the DynamoDB/Postgres paginated lists).\n if (row === undefined || row.t === undefined) continue;\n yield { chunkKey: keys[j]!.chunkKey, ...this.rowFrom(row, keys[j]!.chunkKey) };\n }\n }\n }\n\n /** Build a {@link WarmRow} from a raw hash; reject a hash missing its fields (corrupt/foreign). */\n private rowFrom(raw: Record<string, Buffer>, chunkKey: number): WarmRow {\n const token = raw.t;\n if (token === undefined) {\n throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its token`);\n }\n const payload = raw.b;\n if (payload === undefined) {\n throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its payload`);\n }\n return { token: token.toString('utf8'), bytes: new Uint8Array(payload) };\n }\n\n /** Reclassify a transient Redis fault as a retryable {@link TransientError}; else propagate unchanged. */\n private mapError(err: unknown): unknown {\n if (isTransient(err)) {\n return new TransientError(\n `transient Redis fault: ${(err as { message?: unknown } | null)?.message ?? 'unknown'}`,\n { cause: err },\n );\n }\n return err;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/core/errors.ts","../../src/core/ports.ts","../../src/core/validate.ts","../../src/drivers/_shared/keys.ts","../../src/drivers/redis/keys.ts","../../src/drivers/redis/redis-errors.ts","../../src/drivers/redis/warm.ts"],"names":["randomUUID"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Typed errors — 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. The package ships as multiple bundles — the core entry and the `./s3` / `./dynamodb`\n * subpaths — and the builder inlines `core/errors` into each. A driver in a subpath bundle therefore throws a\n * *different* class object than the one the core engine/retry code would `instanceof`-check, so `instanceof`\n * silently returns false across that boundary in the published CJS package (defeating OCC/transient retry and\n * compaction race-handling). These `Symbol.for` brands are identity-stable across bundles/realms; classify\n * errors with the exported predicates below (never `instanceof`) anywhere an error may cross the 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 — see the predicates ({@link isCloudRoaringError}, …). Non-enumerable-ish (symbol key ⇒ 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 — 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 — 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 * persistent drivers from Phase 2 — the Phase-1 engine + in-memory drivers return `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** — the bytes are well-formed but unreadable here (an unknown `.crbm` major version, an\n * encrypted file before the crypto path exists) — distinct from `IntegrityError` (corruption); and (2)\n * **store configuration** — an operation this store's wiring doesn't support (e.g. a lifecycle helper like\n * `compact`/`eraseSubject` called on a store built without a raw cold driver + registry). 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 Cold 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** — too many backend requests for a single\n * `count`/`iterate`/`intersect`/`subjectReport`/`eraseSubject` call — so it is refused **before** fanning out\n * (Decision #3 / invariant T3). 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 — 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 — 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 — 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 (threat-model S12): `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 — a stalled request often succeeds on a fresh connection. Raised by a\n * driver whose injected client reports a request timeout — 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 — use these, not `instanceof`, wherever an error may cross the core↔driver\n * (`./s3` / `./dynamodb`) 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 — 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 * Storage-driver contracts the engine depends on.\n *\n * Phase 1 uses the subset needed by the in-memory engine: the per-chunk Warm store (under OCC) and a\n * per-chunk read view of Cold. Drivers move opaque bytes + an OCC token; they never understand roaring,\n * `adds`/`removes`, or the `.crbm` layout. The full `IColdDriver` (blob/range) + `IRegistryDriver` arrive\n * with generations in Phase 2/3 — the `.crbm` reader will implement `ColdChunkSource`.\n */\n\nimport type { BlobSink } from './blob';\nimport type { WrappedDek } from './crypto';\n\n/** Opaque optimistic-concurrency token — unique per write, compared by equality only. */\nexport type Token = string;\n\n/** Sentinel for `putConditional` meaning \"the row must not exist yet\" (create). */\n// `Symbol.for` (the global registry), NOT `Symbol()`: the package ships multiple bundles (the core entry and\n// the `./dynamodb` / `./s3` subpaths), and tsup inlines `core/ports` into each. A plain `Symbol('no-row')`\n// would be a DISTINCT instance per bundle, so the engine's `NO_ROW` wouldn't `===` the one the DynamoDb warm\n// driver compares against — every create would misroute to the token-fenced path and fail. A registry symbol\n// is identity-stable across bundles/realms. (Invisible in tests, which share one source module graph.)\nexport const NO_ROW: unique symbol = Symbol.for('cloud-roaring.no-row');\nexport type NoRow = typeof NO_ROW;\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 — a single generation of a segment. */\nexport interface GenKey extends SegmentRef {\n readonly generation: number;\n}\n\nexport interface WarmRow {\n readonly token: Token;\n /** Read-only: owned by the driver. Callers must NOT mutate the buffer (re-encode to write). */\n readonly bytes: Uint8Array;\n}\n\n/**\n * Read consistency for a Warm fetch (gap #9). `consistent: true` (the default — omitted ⇒ true) is a\n * strong, read-your-writes read, as the OCC read-modify-write path requires. `consistent: false` requests an\n * **eventually-consistent** read — on DynamoDB that is ~½ the RCU cost; a driver that is always strongly\n * consistent (in-memory, LocalFs) ignores it. The engine uses it only on read paths (`has`/`count`/`iterate`/\n * `intersect`) when the store opts into `warmReadConsistency: 'eventual'`, trading read-after-write for cost.\n */\nexport interface WarmReadOptions {\n readonly consistent?: boolean;\n}\n\n/** Per-chunk Warm store under optimistic concurrency. Returned byte buffers are read-only. */\nexport interface IWarmDriver {\n get(ref: ChunkRef, opts?: WarmReadOptions): Promise<WarmRow | null>;\n /** OCC write. `expected` = `NO_ROW` to create, else the token previously read. Throws `WriteConflictError` on mismatch. */\n putConditional(\n ref: ChunkRef,\n bytes: Uint8Array,\n expected: Token | NoRow,\n ): Promise<{ token: Token }>;\n /**\n * Fenced delete — only if the stored token still equals `expected`; throws `WriteConflictError`\n * otherwise. **Used by compaction (Phase 4); the Phase-1 engine never calls it** — drivers still\n * implement it to satisfy the conformance suite.\n */\n deleteConditional(ref: ChunkRef, expected: Token): Promise<void>;\n /** All dirty chunks of a segment, ascending `chunkKey`. Yielded `bytes` are read-only. */\n listChunks(\n ref: SegmentRef,\n opts?: WarmReadOptions,\n ): AsyncIterable<{ chunkKey: number } & WarmRow>;\n}\n\n/**\n * A segment's grounded on-disk footprint (Phase 5b) — the current generation's Cold object bytes, read\n * cheaply from the `.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 Cold tier (implemented by the `.crbm` reader from Phase 2). */\nexport interface ColdChunkSource {\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 (Phase 5b): the current generation's grounded size, cheaply (from the already-parsed\n * `.crbm` index — no payload reads), or `null` if the segment has no Cold generation. Powers the\n * grounded `costReport()`.\n */\n sizeOf?(ref: SegmentRef): Promise<SegmentSize | null>;\n /**\n * Optional (Phase 5c): the current generation's **per-chunk cardinality** (`chunkKey → count`), read\n * from the already-parsed `.crbm` index with **no payload reads**, or `null` if the segment has no Cold\n * generation. Powers the cheap `count()` — a warm-delta-free chunk is counted straight from the index\n * instead of fetching + deserializing it. A source with no index (e.g. the in-memory source) omits this,\n * and `count()` falls back to fetching + merging every chunk.\n */\n cardinalities?(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;\n /**\n * Optional (Phase B, gap #4): the segment's **current generation number** as this source resolves it right now\n * (registry `currentGen`, or the highest cold generation), or `null` if the segment has no Cold generation. The\n * engine keys its HOT chunk cache by this so a generation bump (a compaction commit) is observed — a new\n * generation misses the cache instead of serving a stale decoded chunk, and an erased id can't resurrect from a\n * cached pre-compaction chunk. Cheap: served from the source's own (short-TTL-refreshed) snapshot, **not** a\n * fresh backend read per call. A source that pins one immutable generation for its whole lifetime and never\n * refreshes may omit this — the engine then keys the cache without a generation, exactly as before.\n */\n currentGeneration?(ref: SegmentRef): Promise<number | null>;\n}\n\n/** Capabilities a Cold driver advertises; validated at wiring time, fail-fast. */\nexport interface ColdCaps {\n /** REQUIRED — the format relies on byte-range reads. */\n readonly rangeRead: true;\n /** Largest single object the backend accepts (informs single-object-vs-shard, B7 — 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 IColdDriver {\n capabilities(): ColdCaps;\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;\n * `compacting`/`erasing` are transient flags a daemon sets while it works (Phase 4d/4e); `destroyed` is the\n * post-crypto-shred tombstone (the row is kept for audit but the segment is logically gone). The 4c registry\n * stores and round-trips the field; the *transitions* are driven by their owning features.\n */\nexport type RegistryStatus = 'active' | 'compacting' | 'erasing' | 'destroyed';\n\n/**\n * Free-form, JSON-serializable governance metadata — 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` now has one **reserved key with defined semantics**: `expiresAt`, an absolute epoch-ms after which a\n * retention sweep may retire the segment — 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 — the authoritative per-segment record. Exactly one per segment.\n */\nexport interface RegistryRecord extends SegmentRef {\n /**\n * **The** authoritative LATEST pointer: which immutable Cold generation is current — or **`null` for a segment\n * that has no Cold generation yet.**\n *\n * `null` is not \"unknown\", it is a positive statement: *this segment exists and has no Cold data.* It is what\n * lets a **warm-only accumulator** (created by writing to it, never bulk-loaded, never compacted) have a\n * registry row at all — which it needs to be reachable by `registry.list()`, and therefore by retention\n * sweeps, `checkConsistency`, `eraseNamespace` and every other fleet-wide operation. Without it those\n * segments are invisible to every admin tool in the library.\n *\n * The alternative — a row with `currentGen: 0` and no object behind it — is the forbidden\n * `missing-cold-generation` state, and it fails *per operation* rather than cleanly: `has()` short-circuits on\n * the Warm delta and keeps answering, while `count()` resolves the generation and throws `NotFoundError`.\n *\n * Generation resolution maps `null` onto the same path a segment with **no row** takes, so Cold contributes\n * the empty set and the Warm delta alone produces the answer. Read behaviour is unchanged by construction.\n */\n readonly currentGen: number | null;\n /**\n * Per-segment data-key (DEK) wrappings for encryption-at-rest (Phase 4e): the DEK envelope-wrapped under one\n * or more KEKs (active + optional recovery). Reading unwraps with any held KEK; **crypto-shred deletes this\n * whole list**, making the segment's at-rest bytes permanently unrecoverable. Absent ⇒ the segment is\n * cleartext.\n */\n readonly wrappedDeks?: readonly WrappedDek[];\n /**\n * Optional **external** key reference (reserved) — 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 /**\n * Dirty-row hint written by `findCompactable`. Discovery still recomputes the live dirty count by scanning\n * Warm each cycle (the count that decides candidacy), but it now **reads** this hint to gate a change-guarded\n * CAS — the hint is only rewritten when the count actually moved, so an all-idle fleet issues no per-segment\n * registry writes. It does not yet drive candidacy — turning the Warm scan itself into O(dirty) is a\n * deferred cheap-enumeration fix.\n */\n readonly dirtyChunkCount: number;\n readonly status: RegistryStatus;\n /**\n * Daemon health (Phase D, gap #2): epoch-ms of the last **successfully committed** compaction, and the\n * count of **consecutive** compaction failures (reset to 0 by a successful commit). `lastCompactedAt`\n * powers a dead-man's-switch / staleness alarm; `consecutiveFailures` drives **poison-segment quarantine** —\n * discovery skips a segment past a failure threshold so one corrupt Warm row can't freeze the compaction of a\n * segment **that has a committed generation** forever (with no alarm) while its Warm backlog grows unbounded.\n * (A segment whose *very first* compaction keeps failing has no registry row to count against yet —\n * bootstrap faults are surfaced as `error` results but not yet quarantined; gap #2.) Both\n * **optional** (absent on rows written before Phase D, and on a segment never compacted) — read them as\n * `lastCompactedAt ?? undefined`, `consecutiveFailures ?? 0`.\n */\n readonly lastCompactedAt?: number;\n readonly consecutiveFailures?: number;\n /**\n * Compaction lease (Phase 4d): the opaque id of the worker currently compacting this segment, and the\n * epoch-ms at which its lease expires. `status === 'compacting'` with `leaseExpiresAt` in the future means\n * a live daemon owns it; once `leaseExpiresAt` is past, another worker may **steal** the lease (the prior\n * holder crashed). Both absent in the steady (`active`) state. The lease is an efficiency guard only — the\n * 2-phase commit is correct without it (OCC-fenced swap + write-once generations + version-fenced purge).\n */\n readonly leaseOwner?: string;\n readonly leaseExpiresAt?: number;\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 — 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 — compare-by-equality, never reused (ABA-safe), exactly like {@link WarmRow.token}. */\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` ⇒ the segment has no Cold generation yet — see {@link RegistryRecord.currentGen}. */\n readonly currentGen: number | null;\n readonly wrappedDeks?: readonly WrappedDek[];\n readonly keyId?: string;\n /** Defaults to 0. */\n readonly dirtyChunkCount?: number;\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'\n | 'wrappedDeks'\n | 'keyId'\n | 'dirtyChunkCount'\n | 'status'\n | 'leaseOwner'\n | 'leaseExpiresAt'\n | 'lastCompactedAt'\n | 'consecutiveFailures'\n | 'retention'\n | 'residency'\n >\n>;\n\n/** Capabilities a registry driver advertises; validated fail-fast at wiring time. */\nexport interface RegCaps {\n /** REQUIRED — `currentGen` feeds read correctness + compaction CAS, so reads must be strongly consistent. */\n readonly strongRead: true;\n}\n\n/**\n * Per-segment registry — the authoritative source of `currentGen`, the discovery index, and (Phase 4e) the\n * wrapped-DEK holder. One row per segment under OCC: a\n * never-reused, equality-compared {@link Token} (ABA-safe across delete→recreate, like the Warm tier).\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** — 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 — 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 — a later `create` still gets a fresh, greater token). */\n delete(ref: SegmentRef): Promise<void>;\n}\n","/**\n * Boundary validation for segment / namespace names (finding S2).\n * Names become object keys, partition keys, and file paths — so they're validated before they\n * ever reach storage: strict charset, bounded length, no path traversal.\n */\nimport { ValidationError } from './errors';\nimport type { ChunkRef, SegmentRef } from './ports';\n\nconst CHUNK_KEY_MAX = 0xffff;\n\n// The locked name grammar: 1 leading alphanumeric + up to 255 more (max 256 chars).\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;\n\nfunction validatePart(value: string, field: string): void {\n if (typeof value !== 'string' || !NAME.test(value) || value.includes('..')) {\n throw new ValidationError(\n `${field} must match ${String(NAME)} and contain no \"..\": got ${JSON.stringify(value)}`,\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 grammar plus `chunkKey` ∈ `[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 * Shared key-grammar fragments used by every driver's logical-ref → physical-key mapping (Phase 4c).\n *\n * Extracted once the registry drivers became the 4th/5th consumer of the same `_default` namespace sentinel\n * (it lived copy-pasted in `s3/keys`, `dynamodb/keys`, `localfs/paths`). Pure string logic, no SDK, no I/O —\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. The name grammar forbids a leading underscore\n * so `_default` can never collide with a real\n * namespace — `segment(\"s\")` and `segment(\"s\", { namespace: \"_default\" })` would be a grammar error, not an\n * aliasing hazard.\n */\nexport const DEFAULT_NAMESPACE = '_default';\n\n/** Map an optional namespace to its physical part: the namespace itself, or {@link DEFAULT_NAMESPACE}. */\nexport function namespacePart(namespace: string | undefined): string {\n return namespace ?? DEFAULT_NAMESPACE;\n}\n","/**\n * Logical-ref → Redis key mapping for {@link RedisWarmDriver} (Phase 7).\n *\n * Pure, SDK-free string logic — unit-testable without a live Redis. Each chunk is one hash (`t` = token,\n * `b` = payload); each segment has one sorted-set index of its live chunk keys (Redis has no range scan, so\n * `listChunks` reads the index). Both keys for a segment share a **hash tag** `{<prefix>|<ns>|<seg>}` so they\n * always land in the same Redis Cluster slot — the OCC Lua script touches both atomically, which Cluster only\n * allows for same-slot keys. The name grammar (validateSegmentRef) forbids `|`/`{`/`}`/`:`/control chars in\n * ns/seg, and {@link normalizeRedisPrefix} forbids them in the prefix, so the tag can't be broken or aliased.\n */\nimport { ValidationError } from '@/core/errors';\nimport { validateChunkRef, validateSegmentRef } from '@/core/validate';\nimport type { ChunkRef, SegmentRef } from '@/core/ports';\nimport { namespacePart } from '../_shared/keys';\n\n/**\n * Validate the caller-supplied key prefix: no control characters and none of the reserved key/tag delimiters\n * (`{`, `}`, `|`, `:`), so it can neither break out of the hash tag nor alias another prefix's keyspace.\n * Empty/undefined ⇒ no prefix.\n */\nexport function normalizeRedisPrefix(prefix: string | undefined): string {\n if (prefix === undefined || prefix === '') return '';\n for (const ch of prefix) {\n const code = ch.charCodeAt(0);\n if (code < 0x20 || ch === '{' || ch === '}' || ch === '|' || ch === ':') {\n throw new ValidationError(\n `keyPrefix must not contain \"{\", \"}\", \"|\", \":\", or control characters`,\n );\n }\n }\n return prefix;\n}\n\n/** The shared hash-tag body for a segment: `<prefix>|<ns>|<seg>` (all delimiter-free by grammar). */\nfunction tag(prefix: string, ref: SegmentRef): string {\n return `${prefix}|${namespacePart(ref.namespace)}|${ref.segment}`;\n}\n\n/** The sorted-set index key holding a segment's live chunk keys (scored by chunkKey): `{tag}idx`. */\nexport function segmentIndexKey(prefix: string, ref: SegmentRef): string {\n validateSegmentRef(ref);\n return `{${tag(prefix, ref)}}idx`;\n}\n\n/** The hash key for one chunk's warm row (`t`/`b` fields): `{tag}c:<chunkKey>`. Validates the full ref. */\nexport function chunkHashKey(prefix: string, ref: ChunkRef): string {\n validateChunkRef(ref);\n return `{${tag(prefix, ref)}}c:${ref.chunkKey}`;\n}\n\n/** Parse a chunkKey out of a sorted-set index member (the canonical decimal the writer stored), or null. */\nexport function parseIndexMember(member: string): number | null {\n if (!/^(0|[1-9]\\d*)$/.test(member)) return null;\n const chunkKey = Number(member);\n return Number.isInteger(chunkKey) && chunkKey >= 0 && chunkKey <= 0xffff ? chunkKey : null;\n}\n","/**\n * Pure helpers for classifying Redis (`ioredis`) errors (Phase 7; transient class mirrors the other drivers).\n *\n * SDK-free + side-effect-free — they read only structural shapes off the thrown value (`err.code`, a Node\n * socket code; or the error message, which for a Redis server reply begins with the error-code word). The\n * driver signals an OCC conflict itself (its Lua CAS returns 0), so there's no \"conflict\" classifier — only\n * transient-vs-not. Everything not transient propagates unchanged so a real bug is never blind-retried.\n */\n\n/** A network-level error `code` string (starts with `E`, uppercase letters + underscore, no digits). */\nfunction networkCode(err: unknown): string | undefined {\n const code = (err as { code?: unknown } | null)?.code;\n return typeof code === 'string' && /^E[A-Z_]+$/.test(code) ? code : undefined;\n}\n\n/**\n * Transient Redis error-reply prefixes + ioredis connection-state messages. A Redis server error reply's\n * message begins with the code word (`LOADING`, `CLUSTERDOWN`, …); ioredis raises connection faults as an\n * `Error` whose message is one of the phrases below. All are safe to retry (the resilience layer rides them\n * out); a deterministic reply (`WRONGTYPE`, a Lua `ERR`, etc.) is NOT here, so it surfaces.\n */\nconst TRANSIENT_MESSAGE =\n /^(LOADING|CLUSTERDOWN|TRYAGAIN|MASTERDOWN|READONLY)\\b|Connection is closed|Command timed out|Stream isn't writeable|Reached the max retries per request limit|max number of clients reached|failed to refresh slots cache/i;\n\nexport function isTransient(err: unknown): boolean {\n const net = networkCode(err);\n if (\n net === 'ECONNRESET' ||\n net === 'ECONNABORTED' ||\n net === 'ETIMEDOUT' ||\n net === 'ESOCKETTIMEDOUT' ||\n net === 'ECONNREFUSED' ||\n net === 'EHOSTUNREACH' ||\n net === 'ENETUNREACH' ||\n net === 'EPIPE' ||\n net === 'EAI_AGAIN' ||\n net === 'ENOTFOUND'\n ) {\n return true;\n }\n return err instanceof Error && TRANSIENT_MESSAGE.test(err.message);\n}\n","/**\n * `RedisWarmDriver` — an {@link IWarmDriver} over Redis (Phase 7).\n *\n * The \"sub-millisecond writes, accept always-on\" warm tier. Uses the official `ioredis`, an **optional peer\n * dependency** — only consumers of `cloud-roaring/redis` install it. An `ioredis` client is **injected**\n * (dependency injection): the driver owns no connection/credential logic, so it's thin and reuses the\n * caller's client.\n *\n * Each chunk is one Redis **hash** (`t` = opaque OCC token, a random UUID per write; `b` = delta payload).\n * Redis has no range scan, so each segment keeps a **sorted-set index** of its live chunk keys (scored by\n * chunkKey) that `listChunks` reads. Optimistic concurrency is a **server-side atomic compare-and-set**: a\n * Lua script (Redis runs it atomically, single-threaded — no `WATCH`/`MULTI` needed) checks the stored token\n * and only then writes, keeping the hash and the index consistent in one step:\n * - **create-if-absent** — fails if the hash exists ⇒ {@link WriteConflictError}.\n * - **token-fenced update / delete** — fails unless the stored `t` equals the expected token ⇒\n * {@link WriteConflictError}.\n *\n * The hash and index for a segment share a Redis-Cluster **hash tag** so the multi-key script is slot-safe.\n * Tokens are not reused across delete→recreate (ABA-safe, D3): a delete removes the hash and a recreate mints\n * a fresh random UUID (probabilistic, like the Postgres driver; conformance D3 pins it). Drivers may use\n * `node:crypto`; only `core/` is bound by the determinism lint.\n */\nimport { randomUUID } from 'node:crypto';\nimport type { Redis } from 'ioredis';\nimport { IntegrityError, TransientError, ValidationError, WriteConflictError } from '@/core/errors';\nimport { NO_ROW } from '@/core/ports';\nimport type { ChunkRef, IWarmDriver, NoRow, SegmentRef, Token, WarmRow } from '@/core/ports';\nimport { chunkHashKey, normalizeRedisPrefix, parseIndexMember, segmentIndexKey } from './keys';\nimport { isTransient } from './redis-errors';\n\n/** listChunks value-fetch batch — bounds resident payloads to ~this many rows on a very wide segment. */\nconst DEFAULT_LIST_BATCH = 500;\n\n// Atomic OCC compare-and-set scripts. KEYS[1] = chunk hash, KEYS[2] = segment index (sorted set). Redis runs\n// each atomically, so the check-then-write can't race. Return 1 = applied, 0 = OCC conflict. Redis scripts\n// have no rollback, so the hash write is ordered before the index write (and DEL before ZREM): the only way\n// the second command could fail is a WRONGTYPE from external tampering on the shared-tag keyspace — outside\n// the driver's closed world, where hash and index are only ever these keys.\nconst PUT_LUA = `\nif ARGV[1] == 'create' then\n if redis.call('EXISTS', KEYS[1]) == 1 then return 0 end\n redis.call('HSET', KEYS[1], 't', ARGV[3], 'b', ARGV[5])\n redis.call('ZADD', KEYS[2], tonumber(ARGV[4]), ARGV[4])\n return 1\nelse\n local cur = redis.call('HGET', KEYS[1], 't')\n if not cur or cur ~= ARGV[2] then return 0 end\n redis.call('HSET', KEYS[1], 't', ARGV[3], 'b', ARGV[5])\n return 1\nend`;\nconst DEL_LUA = `\nlocal cur = redis.call('HGET', KEYS[1], 't')\nif not cur or cur ~= ARGV[1] then return 0 end\nredis.call('DEL', KEYS[1])\nredis.call('ZREM', KEYS[2], ARGV[2])\nreturn 1`;\n\n/** ioredis client augmented with the two custom OCC commands defined in the constructor. */\ntype WarmRedis = Redis & {\n crWarmPut(\n hashKey: string,\n indexKey: string,\n mode: string,\n expected: string,\n token: string,\n member: string,\n payload: Buffer,\n ): Promise<number>;\n crWarmDel(hashKey: string, indexKey: string, expected: string, member: string): Promise<number>;\n};\n\nexport interface RedisWarmDriverOptions {\n /**\n * A constructed `ioredis` client. The driver never connects/quits it — the caller owns its lifecycle.\n *\n * **The warm tier holds the only live copy of un-compacted deltas, so the instance must be configured for\n * durability + no silent loss:**\n * - `maxmemory-policy noeviction` (or no `maxmemory`). Under a key-evicting policy the chunk hash and its\n * segment-index entry are independent keys and can evict independently → `listChunks` silently drops or\n * ghosts a chunk (the hash-tag guarantees same-*slot*, not co-eviction). The driver sets no TTLs.\n * - Durable persistence (AOF, e.g. `appendfsync everysec`) if you rely on warm survival across a restart.\n * - **Reads must hit the master** — the OCC read-modify-write needs read-your-writes, so do NOT enable\n * replica reads (`scaleReads: 'slave'` / a replica `readFrom`), which could serve stale rows and break OCC.\n */\n readonly client: Redis;\n /** Optional key prefix so several logical stores can share one Redis (no `{`,`}`,`|`,`:`, or control chars). */\n readonly keyPrefix?: string;\n /** Advanced: `listChunks` value-fetch batch size (default 500). Tunes peak memory on wide segments. */\n readonly listPageSize?: number;\n}\n\nexport class RedisWarmDriver implements IWarmDriver {\n private readonly client: WarmRedis;\n private readonly keyPrefix: string;\n private readonly listBatch: number;\n\n constructor(options: RedisWarmDriverOptions) {\n this.keyPrefix = normalizeRedisPrefix(options.keyPrefix);\n const batch = options.listPageSize ?? DEFAULT_LIST_BATCH;\n if (!Number.isSafeInteger(batch) || batch < 1) {\n throw new ValidationError(`listPageSize must be a positive safe integer; got ${batch}`);\n }\n this.listBatch = batch;\n const client = options.client as WarmRedis;\n // Register the OCC scripts once (idempotent across driver instances sharing a client). ioredis caches\n // them and dispatches via EVALSHA with an automatic EVAL fallback.\n if (typeof client.crWarmPut !== 'function') {\n client.defineCommand('crWarmPut', { numberOfKeys: 2, lua: PUT_LUA });\n }\n if (typeof client.crWarmDel !== 'function') {\n client.defineCommand('crWarmDel', { numberOfKeys: 2, lua: DEL_LUA });\n }\n this.client = client;\n }\n\n async get(ref: ChunkRef): Promise<WarmRow | null> {\n const hashKey = chunkHashKey(this.keyPrefix, ref); // validates ref\n let raw: Record<string, Buffer>;\n try {\n raw = await this.client.hgetallBuffer(hashKey);\n } catch (err) {\n throw this.mapError(err);\n }\n if (Object.keys(raw).length === 0) return null; // absent hash → ioredis returns {}\n return this.rowFrom(raw, ref.chunkKey); // present-but-token-less ⇒ IntegrityError (untrusted-data posture)\n }\n\n async putConditional(\n ref: ChunkRef,\n bytes: Uint8Array,\n expected: Token | NoRow,\n ): Promise<{ token: Token }> {\n const hashKey = chunkHashKey(this.keyPrefix, ref); // validates ref\n const indexKey = segmentIndexKey(this.keyPrefix, ref);\n const token = randomUUID();\n const payload = Buffer.from(bytes); // copy: severs any caller-owned/reused input buffer\n const member = String(ref.chunkKey);\n let applied: number;\n try {\n applied =\n expected === NO_ROW\n ? await this.client.crWarmPut(hashKey, indexKey, 'create', '', token, member, payload)\n : await this.client.crWarmPut(\n hashKey,\n indexKey,\n 'update',\n expected,\n token,\n member,\n payload,\n );\n } catch (err) {\n throw this.mapError(err);\n }\n if (applied !== 1) {\n throw new WriteConflictError(\n expected === NO_ROW\n ? `chunk ${ref.chunkKey} already exists (create-if-absent)`\n : `OCC conflict on chunk ${ref.chunkKey}`,\n );\n }\n return { token };\n }\n\n async deleteConditional(ref: ChunkRef, expected: Token): Promise<void> {\n const hashKey = chunkHashKey(this.keyPrefix, ref); // validates ref\n const indexKey = segmentIndexKey(this.keyPrefix, ref);\n let applied: number;\n try {\n applied = await this.client.crWarmDel(hashKey, indexKey, expected, String(ref.chunkKey));\n } catch (err) {\n throw this.mapError(err);\n }\n if (applied !== 1) {\n throw new WriteConflictError(`fenced-delete conflict on chunk ${ref.chunkKey}`);\n }\n }\n\n async *listChunks(ref: SegmentRef): AsyncIterable<{ chunkKey: number } & WarmRow> {\n const indexKey = segmentIndexKey(this.keyPrefix, ref); // validates ref\n let members: string[];\n try {\n // The index is a sorted set scored by chunkKey, so ZRANGE returns members ascending. It holds only small\n // integers, so reading it whole is cheap; the (larger) payloads are fetched in bounded batches below.\n members = await this.client.zrange(indexKey, 0, -1);\n } catch (err) {\n throw this.mapError(err);\n }\n for (let i = 0; i < members.length; i += this.listBatch) {\n const batch = members.slice(i, i + this.listBatch);\n // Resolve members → chunk keys first, dropping any foreign/corrupt member, then fetch that batch's\n // hashes in one pipeline (bounded resident payloads on a very wide segment).\n const keys = batch\n .map((m) => ({ chunkKey: parseIndexMember(m) }))\n .filter((x): x is { chunkKey: number } => x.chunkKey !== null);\n let results: [Error | null, unknown][] | null;\n try {\n const pipeline = this.client.pipeline();\n for (const { chunkKey } of keys) {\n pipeline.hgetallBuffer(chunkHashKey(this.keyPrefix, { ...ref, chunkKey }));\n }\n results = await pipeline.exec();\n } catch (err) {\n throw this.mapError(err);\n }\n results ??= [];\n for (let j = 0; j < keys.length; j++) {\n const [err, raw] = results[j] ?? [null, undefined];\n if (err) throw this.mapError(err);\n const row = raw as Record<string, Buffer> | undefined;\n // A member with no hash means a delete raced our snapshot — skip it (matches the moving-target\n // semantics of the DynamoDB/Postgres paginated lists).\n if (row === undefined || row.t === undefined) continue;\n yield { chunkKey: keys[j]!.chunkKey, ...this.rowFrom(row, keys[j]!.chunkKey) };\n }\n }\n }\n\n /** Build a {@link WarmRow} from a raw hash; reject a hash missing its fields (corrupt/foreign). */\n private rowFrom(raw: Record<string, Buffer>, chunkKey: number): WarmRow {\n const token = raw.t;\n if (token === undefined) {\n throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its token`);\n }\n const payload = raw.b;\n if (payload === undefined) {\n throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its payload`);\n }\n return { token: token.toString('utf8'), bytes: new Uint8Array(payload) };\n }\n\n /** Reclassify a transient Redis fault as a retryable {@link TransientError}; else propagate unchanged. */\n private mapError(err: unknown): unknown {\n if (isTransient(err)) {\n return new TransientError(\n `transient Redis fault: ${(err as { message?: unknown } | null)?.message ?? 'unknown'}`,\n { cause: err },\n );\n }\n return err;\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { Redis } from 'ioredis';
2
- import { a as IWarmDriver, b as ChunkRef, c as WarmRow, T as Token, N as NoRow, S as SegmentRef } from '../ports-5BQTGKze.cjs';
2
+ import { a as IWarmDriver, b as ChunkRef, c as WarmRow, T as Token, N as NoRow, S as SegmentRef } from '../ports-CvcvJWf6.cjs';
3
3
 
4
4
  interface RedisWarmDriverOptions {
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { Redis } from 'ioredis';
2
- import { a as IWarmDriver, b as ChunkRef, c as WarmRow, T as Token, N as NoRow, S as SegmentRef } from '../ports-5BQTGKze.js';
2
+ import { a as IWarmDriver, b as ChunkRef, c as WarmRow, T as Token, N as NoRow, S as SegmentRef } from '../ports-CvcvJWf6.js';
3
3
 
4
4
  interface RedisWarmDriverOptions {
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { NO_ROW } from '../chunk-YEHMG6W2.js';
1
+ import { NO_ROW } from '../chunk-OZ6LAFV4.js';
2
2
  import { ValidationError, WriteConflictError, IntegrityError, TransientError, validateChunkRef, validateSegmentRef, namespacePart } from '../chunk-NUIDEEFZ.js';
3
3
  import { randomUUID } from 'crypto';
4
4
 
package/dist/s3/index.cjs CHANGED
@@ -496,9 +496,21 @@ var MAX_GOVERNANCE_BYTES = 64 * 1024;
496
496
  var MAX_WRAPPED_DEKS = 8;
497
497
  var MAX_WRAPPED_DEK_BYTES = 4 * 1024;
498
498
  function validateGeneration(gen) {
499
+ if (gen === null) return;
499
500
  if (!Number.isInteger(gen) || gen < 0) {
500
- throw new ValidationError(`currentGen must be a non-negative integer; got ${gen}`);
501
+ throw new ValidationError(
502
+ `currentGen must be a non-negative integer or null (no Cold generation yet); got ${gen}`
503
+ );
504
+ }
505
+ }
506
+ function validatePatchGeneration(patch) {
507
+ if (!("currentGen" in patch)) return;
508
+ if (patch.currentGen === void 0) {
509
+ throw new ValidationError(
510
+ `currentGen was present in the patch but undefined. Pass \`null\` to clear the pointer (the segment has no Cold generation), or omit the key to leave it unchanged \u2014 an undefined value is refused because it used to be a no-op and would now un-publish the segment's Cold data.`
511
+ );
501
512
  }
513
+ validateGeneration(patch.currentGen);
502
514
  }
503
515
  function validateCount(count, field = "dirtyChunkCount") {
504
516
  if (!Number.isInteger(count) || count < 0) {
@@ -517,6 +529,11 @@ function validateStatus(status) {
517
529
  }
518
530
  function validateGovernance(meta, field) {
519
531
  if (meta === void 0) return;
532
+ if (meta === null || typeof meta !== "object" || Array.isArray(meta)) {
533
+ throw new ValidationError(
534
+ `${field} must be a plain object (got ${meta === null ? "null" : typeof meta})`
535
+ );
536
+ }
520
537
  let json;
521
538
  try {
522
539
  json = JSON.stringify(meta);
@@ -557,7 +574,7 @@ function validateNewRegistryRecord(rec) {
557
574
  validateGovernance(rec.residency, "residency");
558
575
  }
559
576
  function validateRegistryPatch(patch) {
560
- if (patch.currentGen !== void 0) validateGeneration(patch.currentGen);
577
+ validatePatchGeneration(patch);
561
578
  if (patch.dirtyChunkCount !== void 0) validateCount(patch.dirtyChunkCount);
562
579
  if (patch.consecutiveFailures !== void 0)
563
580
  validateCount(patch.consecutiveFailures, "consecutiveFailures");
@@ -638,10 +655,10 @@ function recordFromNew(ref, rec, now, token) {
638
655
  };
639
656
  }
640
657
  function assertStoredRecordShape(r, ctx) {
641
- if (typeof r.segment !== "string" || r.namespace !== void 0 && typeof r.namespace !== "string" || typeof r.currentGen !== "number" || typeof r.dirtyChunkCount !== "number" || typeof r.status !== "string" || typeof r.createdAt !== "number" || typeof r.updatedAt !== "number") {
658
+ if (typeof r.segment !== "string" || r.namespace !== void 0 && typeof r.namespace !== "string" || r.currentGen !== null && typeof r.currentGen !== "number" || typeof r.dirtyChunkCount !== "number" || typeof r.status !== "string" || typeof r.createdAt !== "number" || typeof r.updatedAt !== "number") {
642
659
  throw new IntegrityError(`registry record is missing required fields: ${ctx}`);
643
660
  }
644
- if (!Number.isInteger(r.currentGen) || r.currentGen < 0) {
661
+ if (r.currentGen !== null && (!Number.isInteger(r.currentGen) || r.currentGen < 0)) {
645
662
  throw new IntegrityError(`registry record has an invalid currentGen (${r.currentGen}): ${ctx}`);
646
663
  }
647
664
  if (!Number.isInteger(r.dirtyChunkCount) || r.dirtyChunkCount < 0) {
@@ -666,12 +683,22 @@ function assertStoredRecordShape(r, ctx) {
666
683
  throw new IntegrityError(`registry record has an invalid keyId: ${ctx}`);
667
684
  }
668
685
  validateWrappedDeks(r.wrappedDeks, true);
686
+ for (const field of ["retention", "residency"]) {
687
+ const meta = r[field];
688
+ if (meta !== void 0 && (meta === null || typeof meta !== "object" || Array.isArray(meta))) {
689
+ throw new IntegrityError(`registry record has a non-object ${field}: ${ctx}`);
690
+ }
691
+ }
669
692
  }
670
693
  function applyRegistryPatch(prev, patch, now, token) {
671
694
  return {
672
695
  namespace: prev.namespace,
673
696
  segment: prev.segment,
674
- currentGen: patch.currentGen ?? prev.currentGen,
697
+ // `'currentGen' in patch`, NOT `patch.currentGen ?? prev.currentGen`. `null` is a legal, meaningful value
698
+ // here (no Cold generation yet), and `??` treats it as absent — so the nullish form would silently ignore a
699
+ // patch that clears the pointer and leave the old generation in place. The same reason `wrappedDeks` and
700
+ // `keyId` below use presence: any field whose null is a *value* cannot be merged with `??`.
701
+ currentGen: "currentGen" in patch ? patch.currentGen ?? null : prev.currentGen,
675
702
  wrappedDeks: "wrappedDeks" in patch ? patch.wrappedDeks : prev.wrappedDeks,
676
703
  keyId: "keyId" in patch ? patch.keyId : prev.keyId,
677
704
  dirtyChunkCount: patch.dirtyChunkCount ?? prev.dirtyChunkCount,