@cloudbitmaps/core 0.9.0 → 0.11.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 (132) hide show
  1. package/NOTICE +4 -4
  2. package/README.md +33 -16
  3. package/dist/chunk-5OMC5U7U.js +508 -0
  4. package/dist/chunk-5OMC5U7U.js.map +7 -0
  5. package/dist/core/audit.d.ts +128 -0
  6. package/dist/core/bit-route.d.ts +21 -0
  7. package/dist/core/blob.d.ts +34 -0
  8. package/dist/core/budget.d.ts +68 -0
  9. package/dist/core/codec.d.ts +122 -0
  10. package/dist/core/concurrency.d.ts +15 -0
  11. package/dist/core/consistency.d.ts +73 -0
  12. package/dist/core/cooperative.d.ts +78 -0
  13. package/dist/core/cost.d.ts +254 -0
  14. package/dist/core/crbm/crc32c.d.ts +10 -0
  15. package/dist/core/crbm/format.d.ts +114 -0
  16. package/dist/core/crbm/reader.d.ts +86 -0
  17. package/dist/core/crbm/varint.d.ts +13 -0
  18. package/dist/core/crbm/writer.d.ts +39 -0
  19. package/dist/core/crbm-storage-source.d.ts +522 -0
  20. package/dist/core/crypto.d.ts +80 -0
  21. package/dist/core/determinism.d.ts +42 -0
  22. package/dist/core/discover.d.ts +77 -0
  23. package/dist/core/due-index.d.ts +53 -0
  24. package/dist/core/engine.d.ts +246 -0
  25. package/dist/core/erase-id.d.ts +176 -0
  26. package/dist/core/erasure.d.ts +185 -0
  27. package/dist/core/errors.d.ts +113 -0
  28. package/dist/core/generation-gc.d.ts +56 -0
  29. package/dist/core/keys.d.ts +23 -0
  30. package/dist/core/load.d.ts +121 -0
  31. package/dist/core/lru.d.ts +69 -0
  32. package/dist/core/metrics.d.ts +136 -0
  33. package/dist/core/name-codec.d.ts +69 -0
  34. package/dist/core/pinned-storage-source.d.ts +90 -0
  35. package/dist/core/ports.d.ts +423 -0
  36. package/dist/core/reader-defaults.d.ts +18 -0
  37. package/dist/core/registry-scan.d.ts +47 -0
  38. package/dist/core/retention-sweep.d.ts +198 -0
  39. package/dist/core/retention.d.ts +67 -0
  40. package/dist/core/retry.d.ts +69 -0
  41. package/dist/core/rollback.d.ts +75 -0
  42. package/dist/core/validate.d.ts +44 -0
  43. package/dist/driver-kit.d.ts +9 -0
  44. package/dist/driver-kit.js +275 -0
  45. package/dist/driver-kit.js.map +7 -0
  46. package/dist/drivers/_shared/keys.d.ts +26 -0
  47. package/dist/drivers/_shared/object-registry-keys.d.ts +28 -0
  48. package/dist/drivers/_shared/object-registry.d.ts +89 -0
  49. package/dist/drivers/_shared/registry.d.ts +66 -0
  50. package/dist/drivers/backends.d.ts +43 -0
  51. package/dist/drivers/crypto.d.ts +38 -0
  52. package/dist/drivers/localfs/fs-util.d.ts +21 -0
  53. package/dist/drivers/localfs/paths.d.ts +33 -0
  54. package/dist/drivers/localfs/registry.d.ts +28 -0
  55. package/dist/drivers/localfs/storage.d.ts +19 -0
  56. package/dist/drivers/memory.d.ts +49 -0
  57. package/dist/drivers/retry/retrying-chunk-source.d.ts +59 -0
  58. package/dist/export/index.d.ts +93 -0
  59. package/dist/index.d.ts +76 -2760
  60. package/dist/index.js +3527 -3133
  61. package/dist/index.js.map +7 -1
  62. package/dist/testing/fuzz-core.d.ts +14 -0
  63. package/package.json +19 -115
  64. package/dist/azure/index.cjs +0 -368
  65. package/dist/azure/index.cjs.map +0 -1
  66. package/dist/azure/index.d.cts +0 -48
  67. package/dist/azure/index.d.ts +0 -48
  68. package/dist/azure/index.js +0 -304
  69. package/dist/azure/index.js.map +0 -1
  70. package/dist/cassandra/index.cjs +0 -265
  71. package/dist/cassandra/index.cjs.map +0 -1
  72. package/dist/cassandra/index.d.cts +0 -52
  73. package/dist/cassandra/index.d.ts +0 -52
  74. package/dist/cassandra/index.js +0 -204
  75. package/dist/cassandra/index.js.map +0 -1
  76. package/dist/chunk-7LMLYSVJ.js +0 -43
  77. package/dist/chunk-7LMLYSVJ.js.map +0 -1
  78. package/dist/chunk-BD22WDAN.js +0 -230
  79. package/dist/chunk-BD22WDAN.js.map +0 -1
  80. package/dist/chunk-NUIDEEFZ.js +0 -91
  81. package/dist/chunk-NUIDEEFZ.js.map +0 -1
  82. package/dist/chunk-OZ6LAFV4.js +0 -6
  83. package/dist/chunk-OZ6LAFV4.js.map +0 -1
  84. package/dist/chunk-SNJVZ227.js +0 -35
  85. package/dist/chunk-SNJVZ227.js.map +0 -1
  86. package/dist/dynamodb/index.cjs +0 -767
  87. package/dist/dynamodb/index.cjs.map +0 -1
  88. package/dist/dynamodb/index.d.cts +0 -106
  89. package/dist/dynamodb/index.d.ts +0 -106
  90. package/dist/dynamodb/index.js +0 -469
  91. package/dist/dynamodb/index.js.map +0 -1
  92. package/dist/gcs/index.cjs +0 -343
  93. package/dist/gcs/index.cjs.map +0 -1
  94. package/dist/gcs/index.d.cts +0 -46
  95. package/dist/gcs/index.d.ts +0 -46
  96. package/dist/gcs/index.js +0 -279
  97. package/dist/gcs/index.js.map +0 -1
  98. package/dist/index.cjs +0 -4990
  99. package/dist/index.cjs.map +0 -1
  100. package/dist/index.d.cts +0 -2761
  101. package/dist/mongodb/index.cjs +0 -260
  102. package/dist/mongodb/index.cjs.map +0 -1
  103. package/dist/mongodb/index.d.cts +0 -45
  104. package/dist/mongodb/index.d.ts +0 -45
  105. package/dist/mongodb/index.js +0 -199
  106. package/dist/mongodb/index.js.map +0 -1
  107. package/dist/mysql/index.cjs +0 -281
  108. package/dist/mysql/index.cjs.map +0 -1
  109. package/dist/mysql/index.d.cts +0 -56
  110. package/dist/mysql/index.d.ts +0 -56
  111. package/dist/mysql/index.js +0 -211
  112. package/dist/mysql/index.js.map +0 -1
  113. package/dist/ports-CvcvJWf6.d.cts +0 -395
  114. package/dist/ports-CvcvJWf6.d.ts +0 -395
  115. package/dist/postgres/index.cjs +0 -273
  116. package/dist/postgres/index.cjs.map +0 -1
  117. package/dist/postgres/index.d.cts +0 -54
  118. package/dist/postgres/index.d.ts +0 -54
  119. package/dist/postgres/index.js +0 -203
  120. package/dist/postgres/index.js.map +0 -1
  121. package/dist/redis/index.cjs +0 -257
  122. package/dist/redis/index.cjs.map +0 -1
  123. package/dist/redis/index.d.cts +0 -42
  124. package/dist/redis/index.d.ts +0 -42
  125. package/dist/redis/index.js +0 -197
  126. package/dist/redis/index.js.map +0 -1
  127. package/dist/s3/index.cjs +0 -918
  128. package/dist/s3/index.cjs.map +0 -1
  129. package/dist/s3/index.d.cts +0 -112
  130. package/dist/s3/index.d.ts +0 -112
  131. package/dist/s3/index.js +0 -555
  132. package/dist/s3/index.js.map +0 -1
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Whole-segment erasure: crypto-shred (`destroySegment`, `eraseNamespace`) and disposal (`dropSegment`).
3
+ *
4
+ * GDPR "right to erasure" on immutable, backed-up storage: you can't delete a `.crbm` object from every
5
+ * backup, but you can **delete its key**. `destroySegment` drops the segment's wrapped DEK(s) from the registry
6
+ * (a CAS to a `destroyed` tombstone) — the encrypted Storage bytes are then permanently unreadable, everywhere,
7
+ * forever. **Immediate + irreversible**, so it's gated behind an explicit confirmation (name the exact
8
+ * segment/namespace). A `destroyed` segment reads as empty (its DEK is gone). Only works on an **encrypted**
9
+ * segment; a cleartext segment has no key to shred.
10
+ *
11
+ * The tombstone is also the fence every writer respects: a load refuses
12
+ * a `destroyed` row, so a load racing an erasure cannot resurrect the segment. A single id's erasure is a different
13
+ * operation — `eraseIdFromSegment` rewrites the generation without it.
14
+ */
15
+ import { type IAuditSink } from './audit.js';
16
+ import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
17
+ export interface EraseDeps {
18
+ readonly registry: IRegistryDriver;
19
+ }
20
+ export interface DestroyResult {
21
+ readonly segment: string;
22
+ readonly namespace?: string;
23
+ /** True iff the segment is now a `destroyed` tombstone (incl. the idempotent already-destroyed case). */
24
+ readonly destroyed: boolean;
25
+ /**
26
+ * True iff **this call** performed a genuine crypto-shred — an encrypted segment whose wrapped DEK(s) were
27
+ * dropped, so its Storage bytes are now permanently unreadable. False for a cleartext tombstone (bytes remain
28
+ * readable), for the idempotent already-destroyed case, and for an absent segment. This is what the
29
+ * `segment.erase` audit event keys off — a cleartext "erase" is not an irreversible destruction.
30
+ */
31
+ readonly cryptoShredded: boolean;
32
+ /**
33
+ * Why this segment was not a fresh crypto-shred. `'absent'` (no registry row), `'already'` (already
34
+ * destroyed), `'cleartext'` (no DEK to shred) — and from `eraseNamespace` only, where per-segment faults are
35
+ * isolated so one failure cannot discard the ledger: `'contended'` (the row was rewritten during every CAS
36
+ * attempt) or `` `failed: <message>` `` (any other fault).
37
+ *
38
+ * The last two come with `destroyed: false` and mean **the segment still holds data**. A namespace erase
39
+ * returns them rather than throwing, so its entries have to be inspected — see {@link eraseNamespace}.
40
+ */
41
+ readonly reason?: string;
42
+ }
43
+ /**
44
+ * Crypto-shred one segment. **Irreversible.** `confirmSegment` must equal `ref.segment` (a guard against an
45
+ * accidental destroy — you must name the exact segment). A non-encrypted segment is rejected unless you opt in
46
+ * via `allowCleartext` (there's no key to shred; its Storage bytes stay readable — the tombstone only stops the
47
+ * segment resolving).
48
+ */
49
+ export declare function destroySegment(ref: SegmentRef, deps: EraseDeps, options: {
50
+ confirmSegment: string;
51
+ allowCleartext?: boolean;
52
+ audit?: IAuditSink;
53
+ }): Promise<DestroyResult>;
54
+ /**
55
+ * Crypto-shred every segment in a namespace. **Irreversible.** `confirmNamespace` must equal `namespace`.
56
+ * Returns a per-segment result (skips cleartext segments unless `allowCleartext`).
57
+ *
58
+ * **Bounded.** It lists the whole namespace before it destroys anything, and holds the listing resident, so the
59
+ * listing is capped at `maxScanSegments` (default {@link DEFAULT_MAX_SCAN_SEGMENTS}, 250,000), the ceiling every
60
+ * other fleet scan keeps. A namespace over it throws `BudgetExceededError` **before any segment is destroyed**,
61
+ * never part-way through; raise `maxScanSegments` when the namespace really is that large and the memory is there.
62
+ * A `maxScanSegments` that is not a finite number >= 1 throws `ValidationError`, again before anything is destroyed.
63
+ */
64
+ export declare function eraseNamespace(namespace: string, deps: EraseDeps, options: {
65
+ confirmNamespace: string;
66
+ allowCleartext?: boolean;
67
+ audit?: IAuditSink;
68
+ maxScanSegments?: number;
69
+ }): Promise<{
70
+ destroyed: DestroyResult[];
71
+ }>;
72
+ /**
73
+ * Deps for {@link dropSegment}. Adds `storage`, because unlike a crypto-shred this one deletes the objects.
74
+ */
75
+ export interface DropDeps extends EraseDeps {
76
+ readonly storage: IStorageDriver;
77
+ }
78
+ export interface DropResult {
79
+ readonly segment: string;
80
+ readonly namespace?: string;
81
+ /**
82
+ * **True iff this segment is now empty as a result of this call — or already was.** This is the field a
83
+ * retention job should branch on: `false` means the call found nothing to do and your ref is probably wrong.
84
+ */
85
+ readonly dropped: boolean;
86
+ /** Storage generations physically deleted, ascending. Empty on a dry run — see {@link DropResult.wouldDelete}. */
87
+ readonly generationsDeleted: readonly number[];
88
+ /**
89
+ * Generations still present in Storage when the sweep gave up, ascending. **Empty is the normal outcome** — a
90
+ * non-empty value means the storage was NOT fully reclaimed and the drop should be re-run.
91
+ *
92
+ * Without it, a drop would return `dropped: true` with a populated `generationsDeleted` and no `reason` even
93
+ * when an object holding the **complete set** had just been left in the bucket by a writer that was already
94
+ * mid-write when the tombstone landed (its publish is refused, but the object survives). For a cleartext
95
+ * segment those bytes are readable — and the collection only takes a destroyed segment's generations
96
+ * when something runs it. That result would look like a clean drop, and an operator would get no signal to
97
+ * re-run.
98
+ */
99
+ readonly generationsRemaining: readonly number[];
100
+ /**
101
+ * On a dry run, the generations that **would** be deleted. `undefined` on a real run.
102
+ *
103
+ * This exists because the confirmation guard the erasure calls use — naming the segment twice — protects a
104
+ * hand-typed literal and nothing else. In the loop this operation is actually *for*
105
+ * (`for (const day of expired) drop({segment: day}, {confirmSegment: day})`) the same variable appears twice
106
+ * and the guard is pure ceremony. A dry run is the guard that still works when a machine is calling.
107
+ *
108
+ * It is a **snapshot**: a later real run can see a different set.
109
+ */
110
+ readonly wouldDelete?: readonly number[];
111
+ /**
112
+ * On a dry run, whether the real drop would **also** crypto-shred (i.e. the segment is encrypted).
113
+ * `undefined` on a real run. This is the one fact in the preview that is irreversible *everywhere*, backups
114
+ * included — a dry run that did not surface it was previewing the recoverable half only.
115
+ */
116
+ readonly wouldCryptoShred?: boolean;
117
+ /**
118
+ * True iff the segment was encrypted and its wrapped DEK(s) were dropped as part of this call — so its bytes
119
+ * are unreadable *everywhere*, backups included, not merely deleted from the bucket. Deleting an object does
120
+ * not reach a noncurrent version, a replica, or a PITR snapshot; discarding the key does.
121
+ */
122
+ readonly cryptoShredded: boolean;
123
+ /**
124
+ * How the segment got to its current state. Absent on a fresh, ordinary drop (tombstone written, Storage swept).
125
+ *
126
+ * - `'already'` — it was already a tombstone. Idempotent; pairs with `dropped: true`. A re-drop is **not** a
127
+ * no-op — it re-sweeps Storage, so it is how a residual in `generationsRemaining` is collected.
128
+ * - `'absent'` — **nothing existed.** No registry row and no Storage objects. Pairs with `dropped: false`, and it
129
+ * is the one value worth alerting on: the usual cause is a mistyped name or an omitted `namespace`, both of
130
+ * which address a *different* segment than you meant.
131
+ */
132
+ readonly reason?: string;
133
+ }
134
+ /**
135
+ * **Dispose of a segment: tombstone it, then delete its Storage objects.** Irreversible.
136
+ *
137
+ * WHY THIS EXISTS, given {@link destroySegment} already erases. Because `destroySegment` answers a *compliance*
138
+ * question and this one answers an *operational* question, and they are not the same:
139
+ *
140
+ * - `destroySegment` **crypto-shreds** — it discards the key so the bytes are unreadable everywhere including
141
+ * immutable backups, which is the only erasure that survives WORM. But it **leaves the objects in the
142
+ * bucket**, still billed, and it *requires* encryption because a cleartext segment has no key to discard.
143
+ * - `dropSegment` **removes the storage**. It works on a cleartext segment, and on an encrypted one it *also*
144
+ * drops the DEKs, so it is a strict superset there.
145
+ *
146
+ * It is the supported way to delete a segment and stop paying for it. The obvious workaround — an object-store
147
+ * lifecycle rule on the key prefix — deletes the bytes while the registry still points at them. That is exactly the `missing-storage-generation` state the DR runbook says not to serve traffic
148
+ * on, and it surfaces *intermittently*, because a read consults the cache before Storage: cached chunks answer
149
+ * correctly and evicted ones throw. The whole value of this function is that the ordering below cannot be got
150
+ * wrong by a caller.
151
+ *
152
+ * **THE ORDER IS THE CONTRACT — registry first, then Storage.**
153
+ *
154
+ * 1. **Registry first.** After the tombstone nothing resolves a generation for this segment, so no reader can
155
+ * reach for bytes that are about to disappear, and no writer can publish onto it (a
156
+ * load refuses a `destroyed` row).
157
+ * 2. **Storage second, best-effort, and re-swept.** Once the pointer is a tombstone the segment resolves as empty,
158
+ * so a failure part-way through leaves **orphaned bytes, not a wrong answer.** Orphans cost money and are
159
+ * cleaned up by re-running; a torn pointer costs correctness and is not self-healing. Given the choice, leak
160
+ * bytes — but say so: whatever survives the sweep is reported in {@link DropResult.generationsRemaining}.
161
+ *
162
+ * **Why step 2 sweeps more than once.** A load that was already writing its object when the tombstone landed
163
+ * still finishes the write — its publish is then refused, but the object survives, and it holds the complete
164
+ * set. A single list-then-delete misses it entirely. The re-sweep converges because the tombstone *is* a hard
165
+ * fence on **publishing**, so only already-in-flight writes can appear and they are finite.
166
+ *
167
+ * **When "reads as empty" starts being true.** Not instantly, for a store that has already read this segment: a
168
+ * resolved generation is cached and decoded chunks sit in the cache, so an in-flight reader can answer from
169
+ * cache for a window. A fresh store, or any reader that had not touched the segment, sees empty at once.
170
+ *
171
+ * That window is bounded by `cache.genTtlMs` (default 2 s) **only for a reader whose storage source has both a clock
172
+ * and a registry and a positive TTL** — expiry needs all three. A source built without a clock or a registry, or
173
+ * with `cache.genTtlMs: 0`, has no timed refresh: it notices the drop only when a read has to fetch from a deleted
174
+ * generation or its reader cache evicts the segment. Because a cache-LRU hit never reaches Storage, such a reader
175
+ * can answer `true` for a dropped segment **indefinitely**, and must be invalidated or restarted. This is the *same* caching that makes the delete-bytes-first ordering fail intermittently rather
176
+ * than loudly — it cuts both ways.
177
+ *
178
+ * `confirmSegment` must equal `ref.segment`, matching `destroySegment`/`eraseNamespace`. For an automated
179
+ * caller that guard is ceremony — use `dryRun` first, which reports what would go without touching anything.
180
+ */
181
+ export declare function dropSegment(ref: SegmentRef, deps: DropDeps, options: {
182
+ confirmSegment: string;
183
+ dryRun?: boolean;
184
+ audit?: IAuditSink;
185
+ }): Promise<DropResult>;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Typed errors — callers learn *why* something failed, never by parsing strings. Retry is not the engine's job: a
3
+ * driver classifies its backend's failures into this vocabulary, and the store's read wrapper retries the
4
+ * transient ones through `withRetry`.
5
+ */
6
+ /**
7
+ * Registry-symbol brands, so an error stays classifiable when the class object is not shared.
8
+ *
9
+ * In an ordinary install there is ONE copy of these classes and `instanceof` holds everywhere — across the
10
+ * flavor, the driver packages and core itself — because every package leaves `@cloudbitmaps/*` external and
11
+ * resolves core at runtime rather than inlining it.
12
+ *
13
+ * What still breaks it is a SECOND copy: a version skew between our packages that the installer could not
14
+ * dedupe, or a bundler that emits core twice. Then a driver throws a class the catching side's `instanceof`
15
+ * does not match — and silently, because a `catch` that stops matching just falls through, defeating
16
+ * transient retry and the publish path's conflict handling with no error of its own.
17
+ *
18
+ * `Symbol.for` is identity-stable across copies, bundles and realms, so classify errors with the exported
19
+ * predicates below (never `instanceof`) anywhere an error may cross that boundary.
20
+ */
21
+ declare const ERROR_BRAND: unique symbol;
22
+ declare const TRANSIENT_BRAND: unique symbol;
23
+ /** Base class for every error CloudRoaring throws. */
24
+ export declare class CloudRoaringError extends Error {
25
+ /** Cross-bundle brand — see the predicates ({@link isCloudRoaringError}, …). Non-enumerable-ish (symbol key ⇒ not in JSON). */
26
+ readonly [ERROR_BRAND]: true;
27
+ constructor(message: string);
28
+ }
29
+ /** Invalid caller input (bad id, segment name, options). Raised before any storage call. */
30
+ export declare class ValidationError extends CloudRoaringError {
31
+ }
32
+ /** An OCC conditional write/delete lost the race — the row changed since it was read. */
33
+ export declare class WriteConflictError extends CloudRoaringError {
34
+ }
35
+ /** Bytes from a tier are corrupt, oversized, or fail a checksum/format check. */
36
+ export declare class IntegrityError extends CloudRoaringError {
37
+ }
38
+ /**
39
+ * A requested object/row does not exist. Part of the driver error vocabulary: every storage driver this
40
+ * library ships, the in-memory one included, throws it for a read of a generation object that is not there.
41
+ * A registry `get` of a row that is not there returns `null` instead.
42
+ */
43
+ export declare class NotFoundError extends CloudRoaringError {
44
+ }
45
+ /**
46
+ * This build/configuration cannot perform the requested operation, though nothing is malformed. Two uses:
47
+ * (1) **format** — the bytes are well-formed but unreadable here (an unknown `.crbm` major version) —
48
+ * distinct from `IntegrityError` (corruption); and (2)
49
+ * **store configuration** — an operation this store's wiring doesn't support (e.g. a lifecycle helper like
50
+ * `eraseSubject`/`retireExpired` called on a store built without a storage backend). Raised at
51
+ * operation time, before any mutation.
52
+ */
53
+ export declare class UnsupportedError extends CloudRoaringError {
54
+ }
55
+ /**
56
+ * A driver cannot meet a capability the chosen topology requires (e.g. a Storage driver without range
57
+ * reads). Raised fail-fast at wiring time, never mid-operation.
58
+ */
59
+ export declare class CapabilityError extends CloudRoaringError {
60
+ }
61
+ /**
62
+ * An operation would exceed its per-op **denial-of-wallet budget** — too many backend requests for a single
63
+ * `count`/`iterate`/`intersect`/`subjectReport`/`eraseSubject` call — so it is refused **before** fanning out
64
+ * (hard invariant 6). Default-on but generous (normal ops never hit it); tune it
65
+ * per store (`budget`) or per op, or disable with `budget: false`. Deterministic (never retried): the op is too
66
+ * big by policy, not by luck. Each request's bytes are separately capped (the safe-deserialize ceiling), so
67
+ * bounding the request count transitively bounds bytes. Carries the projected count + the limit, never data.
68
+ */
69
+ export declare class BudgetExceededError extends CloudRoaringError {
70
+ }
71
+ /**
72
+ * An encrypted segment's data key (DEK) cannot be unwrapped because the keystore holds none of the
73
+ * key-encryption-keys (KEKs) its wrappings reference — the KEK was never configured, rotated away without
74
+ * keeping the old key, or lost. Deterministic (never retried): without a KEK the ciphertext is unreadable by
75
+ * design. The flip side of crypto-shred — when this is *intended* (a destroyed segment) the registry row is
76
+ * already a `destroyed` tombstone; when it's *not*, restore the missing KEK (or its recovery KEK). Carries no
77
+ * key material.
78
+ */
79
+ export declare class KeyUnavailableError extends CloudRoaringError {
80
+ }
81
+ /**
82
+ * A **transient** infrastructure fault — throttling, a 5xx, a dropped connection, a client-side request
83
+ * timeout. Retrying a read that failed this way is safe; a write that failed this way may still have landed, so
84
+ * its caller re-runs the call rather than replaying the request. Drivers classify their backend's retryable faults and raise this (the
85
+ * SDK-specific knowledge stays in the SDK-specific driver); the retry layer (`core/retry`) retries **only**
86
+ * this class, never a deterministic error like {@link ValidationError}, {@link IntegrityError},
87
+ * {@link NotFoundError}, or {@link WriteConflictError} (retrying those is pointless or wrong). The original
88
+ * error is preserved in `cause` so callers can still inspect it.
89
+ *
90
+ * Note for logging hygiene: `cause` is the **raw SDK error**, which may carry operational
91
+ * metadata (endpoint host, request IDs, `$metadata`). The library's own `message` is identifier-only and safe
92
+ * to log; if you serialize the whole error *chain*, be aware you're including that metadata.
93
+ */
94
+ export declare class TransientError extends CloudRoaringError {
95
+ /** A second brand so a transient fault is classifiable cross-bundle. */
96
+ readonly [TRANSIENT_BRAND]: true;
97
+ constructor(message: string, options?: {
98
+ cause?: unknown;
99
+ });
100
+ }
101
+ /** Any error thrown by CloudRoaring (any tier, any bundle). */
102
+ export declare function isCloudRoaringError(err: unknown): err is CloudRoaringError;
103
+ /** An OCC conditional write/delete lost the race — retry the read-modify-write, don't fail. */
104
+ export declare function isWriteConflictError(err: unknown): err is WriteConflictError;
105
+ /** A retryable transient infrastructure fault. The retry layer keys on this. */
106
+ export declare function isTransientError(err: unknown): err is TransientError;
107
+ /** A requested object/row does not exist. */
108
+ export declare function isNotFoundError(err: unknown): err is NotFoundError;
109
+ /** Corrupt/oversized/failed-checksum bytes from a tier. */
110
+ export declare function isIntegrityError(err: unknown): err is IntegrityError;
111
+ /** Invalid caller input. */
112
+ export declare function isValidationError(err: unknown): err is ValidationError;
113
+ export {};
@@ -0,0 +1,56 @@
1
+ import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
2
+ /** The two ports generation bookkeeping needs: the objects, and the pointer that says which one is current. */
3
+ export interface GenerationDeps {
4
+ readonly storage: IStorageDriver;
5
+ readonly registry: IRegistryDriver;
6
+ }
7
+ /**
8
+ * The generation number a writer should use for the segment's **next** object: one above the highest generation
9
+ * the registry points at *or* that is present in Storage — whichever is higher.
10
+ *
11
+ * Both are consulted because they can disagree. A load that wrote its object and crashed before publishing leaves
12
+ * an object *above* `currentGen`; a writer that consulted only the pointer would pick that same number and hit the
13
+ * write-once conflict on every retry. Skipping past it costs one `list` and keeps the retry trivial — the crashed
14
+ * object is an orphan {@link gcOrphanGenerations} collects once the pointer moves on. Reusing such an object
15
+ * instead of skipping it (verify-and-publish) is `load()`'s job, not a numbering helper's.
16
+ *
17
+ * A segment with no row and no objects starts at 0. An admin/write-path helper, never on the read path.
18
+ */
19
+ export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): Promise<number>;
20
+ /**
21
+ * Garbage-collect superseded Storage generations for a segment: everything strictly below `currentGen`, keeping
22
+ * the most recent `keep` of them as a grace window, so a read still fetching from a just-superseded
23
+ * generation need not re-resolve mid-call (**invariant 4**). It is a window, not a lock: a read whose
24
+ * generation is swept anyway re-resolves and retries once rather than failing (see `withFreshSnapshot`), so
25
+ * `keep` trades storage for round trips. Generations ≥ `currentGen` are never touched. Returns the generations
26
+ * deleted.
27
+ *
28
+ * **Except on a `destroyed` segment, where EVERY generation is garbage** and the grace window is meaningless.
29
+ * A tombstoned segment resolves no generation for any reader, pinned or not — a pin cannot be taken on one, and
30
+ * a pin taken while it was still active re-checks the status on every open — and nothing else in the library
31
+ * would ever collect them, so without this those objects are billed forever.
32
+ *
33
+ * **Throws {@link WriteConflictError} when it cannot act on the row it read**: the row is gone when it is re-read,
34
+ * after the listing or before a delete; on an active segment, the pointer has fallen below the cutoff before a
35
+ * delete; on a `destroyed` segment, the row's token has changed. Not to be confused with the empty array it still
36
+ * returns when there was genuinely nothing to collect (no row at all, or no pointer yet), which is why an empty
37
+ * array is not a receipt. The row is read before the listing and acted on after it, so a name that was purged and
38
+ * re-created in that window is a *different* segment wearing the same name, and its live object must not be
39
+ * collected on the strength of the old row: on an active segment the lower of the two pointers is what keeps it
40
+ * safe, and on a `destroyed` one the token. Refusing is safe — the objects are not going anywhere and the next
41
+ * pass reads a consistent row — but it has to be **distinguishable** from "there was nothing to collect", which
42
+ * an empty array is not: `eraseIdFromSegment` reads the returned list as the physical half of its erasure
43
+ * receipt, and would otherwise report `erased: true` over bytes still in the bucket. Re-run it.
44
+ *
45
+ * That state is reachable in practice: a `dropSegment` whose Storage sweep threw part-way, or a load that was
46
+ * already writing its object when the tombstone landed and finished the write afterwards. `dropSegment` re-sweeps
47
+ * and reports whatever it could not reclaim in `generationsRemaining`, but a drop that was never re-run leaves a
48
+ * residual, and this is what eventually collects it from the retention sweep.
49
+ *
50
+ * `keep: 0` is how a subject-erasure rewrite makes a bit **physically** gone on return: the generation that held
51
+ * it is collected the moment the rewrite is current. A live read still fetching from it heals forward to the
52
+ * rewrite, and a pin of it (`seg.pin()`) fails with `NotFoundError` for any chunk it has yet to read.
53
+ */
54
+ export declare function gcOrphanGenerations(ref: SegmentRef, deps: GenerationDeps, options?: {
55
+ keep?: number;
56
+ }): Promise<number[]>;
@@ -0,0 +1,23 @@
1
+ import type { ChunkRef, SegmentRef } from './ports.js';
2
+ export declare function segmentKey(ref: SegmentRef): string;
3
+ export declare function segmentPrefix(ref: SegmentRef): string;
4
+ export declare function chunkRefKey(ref: ChunkRef): string;
5
+ /**
6
+ * Cache key for a chunk scoped to a specific **version** of the segment — {@link chunkRefKey} plus that
7
+ * version (space-delimited, injection-proof exactly as above — the name parts are already encoded). A publish bumps the version, so it naturally
8
+ * misses the cache instead of serving a stale superseded chunk, and the superseded entries age out under the
9
+ * LRU ceiling (no active purge needed).
10
+ *
11
+ * `version` is whatever the Storage source reports as identifying the bytes a read will see — its
12
+ * `currentVersion` where it has one, otherwise the bare generation number. **A generation number alone is not
13
+ * an identity**: a load's generation number restarts at 0 once a registry row is purged and the bucket emptied, so a
14
+ * retired-and-re-created name serves different data at the same `currentGen`, and a key built on the number
15
+ * hands the new incarnation the old one's decoded chunks.
16
+ */
17
+ export declare function chunkGenKey(ref: ChunkRef, version: string | number): string;
18
+ /**
19
+ * Deterministic FNV-1a shard assignment for a segment key — dependency-free, stable across workers and
20
+ * restarts. One definition, so every sharded fleet operation (today the retention sweep) agrees on it exactly:
21
+ * two definitions that disagreed would make the union across workers neither disjoint nor complete.
22
+ */
23
+ export declare function shardOf(key: string, totalShards: number): number;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * `loadSegment` — the loaded store's primary write path, as one call.
3
+ *
4
+ * A load is always the same four steps: take the next generation number, write one immutable object, move the
5
+ * pointer, collect what the move superseded. Inside the library each is its own internal function, and the
6
+ * step a hand-composed load leaves out is the last one, so a store that composed it would accumulate superseded
7
+ * generations it keeps paying for.
8
+ * Composing it here, once, is the point.
9
+ *
10
+ * The other reason it is one call is the **guard**. A load REPLACES a segment: whatever the stream contains is
11
+ * what the segment contains afterwards. That makes an upstream query returning fewer rows than usual — or none —
12
+ * a silent, successful-looking wipe, and nothing downstream can tell it from a legitimate shrink. The guard is
13
+ * the place to say what a plausible result looks like, and it has to sit **between the write and the publish**,
14
+ * because that is the only moment where the new content is known and the old one is still authoritative.
15
+ *
16
+ * Refusing therefore has to clean up after itself. The object is already durable at that point, and it sits
17
+ * ABOVE `currentGen`, where generation collection deliberately never looks (it deletes strictly below the
18
+ * pointer), so a refused load deletes its own object before returning, while the segment's row is unchanged or
19
+ * gone. Once another write has changed the row, the object's number may name a re-created segment's live object,
20
+ * so it stays, and collection takes it like any other generation once one above it is current.
21
+ */
22
+ import { type IAuditSink } from './audit.js';
23
+ import { type CodecInterface } from './codec.js';
24
+ import type { Clock } from './determinism.js';
25
+ import type { IKeystore } from './crypto.js';
26
+ import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
27
+ /** What {@link loadSegment} needs: the objects, the pointer, the codec, and key material if encrypted. */
28
+ export interface LoadDeps {
29
+ readonly storage: IStorageDriver;
30
+ readonly registry: IRegistryDriver;
31
+ readonly codec?: CodecInterface;
32
+ readonly keystore?: IKeystore;
33
+ readonly requireEncryption?: boolean;
34
+ readonly clock?: Clock;
35
+ }
36
+ /**
37
+ * What a plausible result looks like, so an implausible one is refused instead of published.
38
+ *
39
+ * Both bounds are compared against the **current** generation's cardinality, read from the `.crbm` index rather
40
+ * than its payload — one object-header read, and only when a bound is actually set.
41
+ */
42
+ export interface LoadGuard {
43
+ /** Refuse a generation with fewer than this many ids. Use for "this segment is never legitimately tiny". */
44
+ readonly minCardinality?: number;
45
+ /**
46
+ * Refuse a generation that retains less than this fraction of the current one — `0.5` means "at least half the
47
+ * ids must survive". A *growing* load is never refused by this bound, and neither is the first load of a
48
+ * segment, which has nothing to shrink from.
49
+ *
50
+ * Phrased as a floor rather than a ceiling on the loss so that `0` means the same thing here as it does on
51
+ * `minCardinality` above: **no bound**. The inverse spelling (`maxShrink`) would make `0` mean "refuse any
52
+ * shrink at all" — maximally strict — sitting in the same object literal as a field where `0` is maximally
53
+ * permissive, which is where an inversion would eventually be written and not noticed.
54
+ */
55
+ readonly minRetained?: number;
56
+ }
57
+ export interface LoadOptions {
58
+ /**
59
+ * Publish an empty generation over a non-empty one. Off by default: an empty result is far more often an
60
+ * upstream failure than an intent, and the failure is indistinguishable from success once it lands.
61
+ */
62
+ readonly allowEmpty?: boolean;
63
+ readonly guard?: LoadGuard;
64
+ /**
65
+ * Generations to keep below the new pointer as a grace window (default 1). A non-negative integer: anything
66
+ * else (negative, fractional, `NaN`, infinite) throws `ValidationError` before anything is written. Keep at
67
+ * least one: a read that is
68
+ * still fetching chunks from the just-superseded generation would otherwise have it deleted out from under it
69
+ * and pay a re-resolve. A wider window costs storage, and it is what a pinned handle needs: a pin is never
70
+ * re-resolved, so a chunk a pinned read has not fetched fails with `NotFoundError` once its generation is
71
+ * collected. Keep at least one generation for every one that can be written above the pinned one while your
72
+ * longest pinned job runs, on every writer that loads the segment — see "Generations and `keep`" in the loading
73
+ * guide.
74
+ */
75
+ readonly keep?: number;
76
+ readonly audit?: IAuditSink;
77
+ }
78
+ /** Why a load did not become current. */
79
+ export type LoadRefusal = 'empty' | 'min-cardinality' | 'min-retained'
80
+ /**
81
+ * Another writer got there first. Either another load wrote the same generation number first, so the write-once
82
+ * put refused this one and it wrote nothing (`size: 0`); or the segment's registry row changed while the load was
83
+ * writing: another load published, a retention change, a rollback or an erasure wrote the row, or the row was
84
+ * deleted. Once another write has changed the row, the object stays in the bucket, because its generation number
85
+ * may by then name another incarnation's live object; once the row is gone, the object is deleted.
86
+ */
87
+ | 'superseded';
88
+ export interface LoadResult {
89
+ /**
90
+ * The generation written. Present even when refused: a refusal deletes that object while the segment's row is
91
+ * unchanged or gone, and leaves it in the bucket once another write has changed the row. A load that lost its
92
+ * generation number to another wrote nothing, and reports `size: 0`.
93
+ */
94
+ readonly generation: number;
95
+ /** Whether this generation is now the segment's current one. */
96
+ readonly published: boolean;
97
+ /** Set only when `published` is false. */
98
+ readonly reason?: LoadRefusal;
99
+ readonly size: number;
100
+ readonly sha256: string;
101
+ readonly chunkCount: number;
102
+ /** Distinct ids in the generation that was written, post-dedup. */
103
+ readonly cardinality: number;
104
+ /**
105
+ * What the segment held when the guard judged it — `null` when there was no current generation, or when no
106
+ * bound needed it. Returned because an alert on `reason: 'min-retained'` is useless without it: "refused" is a
107
+ * page, "refused: 4 → 1" is a diagnosis, and re-reading the segment to find out costs a round trip and races
108
+ * whatever happens next.
109
+ */
110
+ readonly cardinalityBefore: number | null;
111
+ /** The superseded generations collected after publishing. Empty when nothing was published. */
112
+ readonly collected: readonly number[];
113
+ }
114
+ /**
115
+ * Replace a segment's contents with `ids`, as one immutable generation, and make it current.
116
+ *
117
+ * Returns what was written and whether it became current. A refusal is **not** an exception: `published: false`
118
+ * with a `reason` is a normal outcome a caller branches on, in the same shape as a successful load, because the
119
+ * interesting cases (a guard tripped, a racing writer won) are operational facts rather than faults.
120
+ */
121
+ export declare function loadSegment(ref: SegmentRef, ids: Iterable<number> | AsyncIterable<number>, deps: LoadDeps, options?: LoadOptions): Promise<LoadResult>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Bounded LRU cache with optional TTL — the cache tier's memory ceiling.
3
+ *
4
+ * Time comes from an injected `Clock` (never `Date.now()`) so the cache is deterministic
5
+ * under simulation.
6
+ *
7
+ * The cache can be bounded by entry **count** (`maxEntries`, always) and, optionally, by an aggregate
8
+ * **byte weight** (`maxBytes`). The byte bound exists for values whose footprint is large and uneven — the
9
+ * storage reader cache, where one entry pins a fully-parsed `.crbm` index that can be ~6 MB for a wide segment
10
+ * yet a few hundred bytes for a small one (a count-only bound lets 1024 wide indices blow a Lambda's
11
+ * memory even though the count is "in bounds"). Weights arrive **after** insertion (a cached reader is a
12
+ * pending promise; its size is known only once it resolves), so weight is reported via {@link setWeight} and
13
+ * eviction re-runs then.
14
+ */
15
+ import type { Clock } from './determinism.js';
16
+ export interface LruOptions {
17
+ /** Hard ceiling on entries; the least-recently-used is evicted past it. */
18
+ readonly maxEntries: number;
19
+ /**
20
+ * Optional aggregate byte ceiling across all entries' reported weights (see {@link BoundedLru.setWeight}).
21
+ * When set, the least-recently-used entries are evicted until the total is within budget — in addition to
22
+ * the `maxEntries` bound. Omit for a pure count-bounded cache (weights stay 0, no byte eviction).
23
+ */
24
+ readonly maxBytes?: number;
25
+ /** Optional time-to-live in ms; an entry older than this is treated as absent. */
26
+ readonly ttlMs?: number;
27
+ /** Only `now()` is used (never `sleep`), so a count-only cache can pass a minimal time source. */
28
+ readonly clock: Pick<Clock, 'now'>;
29
+ }
30
+ export declare class BoundedLru<K, V> {
31
+ private readonly map;
32
+ private readonly maxEntries;
33
+ private readonly maxBytes;
34
+ private readonly ttlMs;
35
+ private readonly clock;
36
+ private totalBytes;
37
+ constructor(options: LruOptions);
38
+ get(key: K): V | undefined;
39
+ /** Read a value **without** counting as an access (recency unchanged, no purge on expiry). */
40
+ peek(key: K): V | undefined;
41
+ set(key: K, value: V): void;
42
+ /**
43
+ * Report an entry's byte weight (typically once a cached promise resolves and its size is known). Updates
44
+ * the running total and evicts the least-recently-used entries if the aggregate now exceeds `maxBytes`.
45
+ * Recency is **not** changed. The `key` being weighted is protected from this eviction pass (we just
46
+ * populated it) — so a single entry larger than the whole budget is kept rather than immediately dropped.
47
+ */
48
+ setWeight(key: K, bytes: number): void;
49
+ private evictToBudget;
50
+ /** The least-recently-used key that is not `protectKey`, or undefined if none other exists. */
51
+ private oldestExcept;
52
+ private dropKey;
53
+ private drop;
54
+ /** Membership test that does NOT count as an access (recency is unchanged). Purges if expired. */
55
+ has(key: K): boolean;
56
+ delete(key: K): boolean;
57
+ /**
58
+ * Delete every entry whose key matches `pred`, returning how many went. Used to drop one segment's decoded
59
+ * chunks when something destroys the generation they came from — a deletion the generation-keyed cache
60
+ * cannot express on its own, because a *destroyed* segment has no newer generation whose key would miss.
61
+ * Linear in the cache size; called on admin paths (erasure, drop, shred, retirement), never on a read.
62
+ */
63
+ deleteWhere(pred: (key: K) => boolean): number;
64
+ clear(): void;
65
+ /** Current entry count (may include not-yet-evicted expired entries). */
66
+ get size(): number;
67
+ /** Current aggregate reported weight (bytes) across all entries. */
68
+ get weightBytes(): number;
69
+ }