@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,423 @@
1
+ /**
2
+ * Storage-driver contracts the engine depends on.
3
+ *
4
+ * Two tiers and one pointer. **Storage** is immutable object storage holding `.crbm` generations (`IStorageDriver`,
5
+ * read by the engine through the per-chunk `StorageChunkSource` view the `.crbm` reader implements); the
6
+ * **registry** is the authoritative per-segment record — which generation is current, the wrapped DEKs, the
7
+ * governance metadata — under optimistic concurrency (`IRegistryDriver`). Drivers move opaque bytes and OCC
8
+ * tokens; they never understand roaring or the `.crbm` layout.
9
+ */
10
+ import type { BlobSink } from './blob.js';
11
+ import type { WrappedDek } from './crypto.js';
12
+ /** Opaque optimistic-concurrency token — unique per write, compared by equality only. */
13
+ export type Token = string;
14
+ /**
15
+ * The address of one segment: a `namespace` and a `segment` name, both any non-empty string of at most 256
16
+ * characters once encoded for storage. Omitting the namespace addresses a different segment from one that names it.
17
+ *
18
+ * **A namespace starting with `cbm.due.` is reserved**, and every call of the store, and of the free functions
19
+ * behind it, that takes a ref or a `namespace` option throws `ValidationError` for one. The library keeps the due
20
+ * index there (one pointer row per expiring segment), and every fleet-wide scan skips those rows as bookkeeping, so a
21
+ * segment in such a namespace would be invisible to an erasure, a consistency check, an export and a retention sweep.
22
+ * A driver takes the prefix, since the due index's own rows go through it. Only that exact prefix is reserved:
23
+ * `cbm.dueX` and `cbmdue.eu` are ordinary namespaces, and a segment name is never restricted.
24
+ */
25
+ export interface SegmentRef {
26
+ readonly namespace?: string;
27
+ readonly segment: string;
28
+ }
29
+ export interface ChunkRef extends SegmentRef {
30
+ readonly chunkKey: number;
31
+ }
32
+ /** Identifies one immutable `.crbm` object — a single generation of a segment. */
33
+ export interface GenKey extends SegmentRef {
34
+ readonly generation: number;
35
+ }
36
+ /**
37
+ * A segment's grounded on-disk footprint — the current generation's Storage object bytes, read cheaply from the
38
+ * `.crbm` footer/index (no payload reads). Powers the grounded `costReport()`.
39
+ */
40
+ export interface SegmentSize {
41
+ readonly sizeBytes: number;
42
+ }
43
+ /** Per-chunk read view of the immutable Storage tier (implemented by the `.crbm` reader). */
44
+ export interface StorageChunkSource {
45
+ /** Read-only bytes for the chunk, or `null` if absent. Callers must not mutate the buffer. */
46
+ getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
47
+ listChunkKeys(ref: SegmentRef): Promise<number[]>;
48
+ /**
49
+ * Optional: the current generation's grounded size, cheaply (from the already-parsed `.crbm` index — no
50
+ * payload reads), or `null` if the segment has no Storage generation. Powers the grounded `costReport()`.
51
+ */
52
+ sizeOf?(ref: SegmentRef): Promise<SegmentSize | null>;
53
+ /**
54
+ * Optional: how often this source re-reads a segment's pointer while the segment is being read, in ms, or 0 if
55
+ * it never does. The grounded `costReport()` prices the pointer refresh at this interval; a source that omits it
56
+ * is priced at the store's default.
57
+ */
58
+ readonly pointerRefreshMs?: number;
59
+ /**
60
+ * Optional: the current generation's **per-chunk cardinality** (`chunkKey → count`), read from the
61
+ * already-parsed `.crbm` index with **no payload reads**, or `null` if the segment has no Storage generation.
62
+ * Powers the free `count()` — the engine sums the index instead of fetching a single chunk. A source with no
63
+ * index (e.g. the in-memory source) omits this, and `count()` falls back to fetching every chunk.
64
+ */
65
+ cardinalities?(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
66
+ /**
67
+ * Optional: the segment's **current generation number** as this source resolves it right now (registry
68
+ * `currentGen`, or the highest storage generation), or `null` if the segment has no Storage generation. The engine
69
+ * keys its chunk cache by this so a generation bump (a load's publish) is observed — a new generation
70
+ * misses the cache instead of serving a stale decoded chunk, and an erased id can't resurrect from a cached
71
+ * superseded chunk. Cheap: served from the source's own (short-TTL-refreshed) snapshot, **not** a fresh
72
+ * backend read per call. A source that only ever serves one immutable generation may omit this — the engine
73
+ * then keys the cache without a generation.
74
+ */
75
+ currentGeneration?(ref: SegmentRef): Promise<number | null>;
76
+ /**
77
+ * Optional: forget everything this source has derived from `ref` — its resolved snapshot, its open reader,
78
+ * and any key material that reader captured — so the next read resolves from the registry again.
79
+ *
80
+ * The TTL refresh and the generation-keyed chunk cache are both built for **a publish that advances
81
+ * `currentGen`**. Neither covers an event that *destroys* what the cache was derived from: an erasure that
82
+ * deletes the generation holding the bit, a `dropSegment`, a crypto-shred, a retirement. After one of those
83
+ * a source that had already resolved the segment keeps answering from memory — with no backend read at all,
84
+ * so no storage-side control can close the window — until its TTL lapses, and with no bound at all if it has
85
+ * no registry, no clock or `cache.genTtlMs: 0`.
86
+ *
87
+ * Callers that destroy or retire a segment must call this. It is synchronous and best-effort: dropping
88
+ * memoized state cannot fail, and a source that memoizes nothing may omit the method entirely.
89
+ */
90
+ invalidate?(ref: SegmentRef): void;
91
+ /**
92
+ * Optional: does this segment **exist as a registered name**, as distinct from resolving to no generation?
93
+ *
94
+ * `currentGeneration` collapses two very different states into `null`: a name nobody ever created, and a
95
+ * segment that exists and is legitimately empty. For a *read* that distinction does not matter — both answer
96
+ * empty. For an **operand of a combine** it is the difference between a suppression list with nobody on it
97
+ * yet and a suppression list you misspelled, and those must not look alike: the first correctly suppresses
98
+ * nothing, the second silently suppresses nothing while you believe it is working.
99
+ *
100
+ * Consulted **only** when an operand resolved to no chunks at all, so a normal combine never calls it.
101
+ */
102
+ exists?(ref: SegmentRef): Promise<boolean>;
103
+ /**
104
+ * Optional: an opaque token identifying **which bytes** a read of this segment will see right now —
105
+ * the generation *and* the incarnation of the name it belongs to.
106
+ *
107
+ * {@link StorageChunkSource.currentGeneration} is not enough to key a decoded-chunk cache, and the gap is not
108
+ * theoretical: a load numbers its generation `max(currentGen, highest object) + 1`, so the number **restarts at 0** once
109
+ * a registry row is purged and the bucket emptied. A retired, re-created name therefore serves different
110
+ * data at the same `currentGen`, and a cache keyed on `(segment, chunk, generation)` hands back the previous
111
+ * incarnation's ids — an erased id reappearing, with no read to intercept.
112
+ *
113
+ * Note that putting the incarnation in the *object key* would not fix this: the new incarnation still starts
114
+ * at generation 0, so the cache key collides either way. The identity has to reach the cache.
115
+ *
116
+ * Returns `null` when the segment resolves to no generation. A source that only ever serves one immutable
117
+ * generation may omit this, and the engine falls back to the generation alone.
118
+ */
119
+ currentVersion?(ref: SegmentRef): Promise<string | null>;
120
+ }
121
+ /** Capabilities a Storage driver advertises; validated at wiring time, fail-fast. */
122
+ export interface StorageCaps {
123
+ /** REQUIRED — the format relies on byte-range reads. */
124
+ readonly rangeRead: true;
125
+ /** Largest single object the backend accepts (informs single-object-vs-shard — future). */
126
+ readonly maxObjectBytes: number;
127
+ /** Optional: enables the pure-object `LATEST`-pointer registry variant (S3 conditional put). */
128
+ readonly conditionalPut?: boolean;
129
+ }
130
+ /**
131
+ * Immutable object storage for `.crbm` generations. A "dumb byte mover": it
132
+ * understands neither roaring nor the `.crbm` layout, only opaque bytes addressed by a {@link GenKey}.
133
+ * The core never reuses a key, so puts are write-once.
134
+ *
135
+ * **What a driver must do** — callers rely on each of these, and the storage conformance suite holds every
136
+ * shipped driver to them:
137
+ *
138
+ * - **`putImmutable` is write-once and reports a collision as {@link WriteConflictError}.** It never overwrites a
139
+ * stored object. `load` reads that error as a lost race for the generation number (`superseded`), so a driver
140
+ * that threw anything else for an existing key would surface as a failed load.
141
+ * - **A missing object makes `getRange` and `getTail` throw {@link NotFoundError}** — never an empty or short
142
+ * result. Heal-forward (a read whose generation a sweep collected re-resolves), the erasure's holder probe and
143
+ * its verify of the rewrite, and a pin's replaced-object check all branch on that one error. (A zero-length
144
+ * `getRange` may answer empty without reaching the backend, so it proves neither that the object exists nor
145
+ * that the offset lies inside it: the cloud drivers answer it empty, the memory and local-filesystem drivers
146
+ * throw for a missing object or an offset past the end.)
147
+ * - **An out-of-range read throws {@link ValidationError}**: a range that runs past the end of the object, or a
148
+ * negative or non-integer offset or length. Never a clamped, short or adjacent read. A pin whose object
149
+ * was purged and loaded again as a smaller one asks for a range past its end, and this error is what sends it
150
+ * to check that the object is still the one it pinned.
151
+ * - **`getTail` reports the object's true total size**, not the number of bytes it returned.
152
+ * - **`delete` is idempotent.** Deleting an absent key is a no-op, not an error: collection passes race each
153
+ * other and retry.
154
+ * - **`list` is strongly consistent, read-after-delete.** Once `delete` has resolved, `list` no longer yields
155
+ * that generation, and once `putImmutable` has resolved it does. The erasure's re-check for a generation still
156
+ * holding the id, `dropSegment`'s `generationsRemaining`, the retention sweep's check that a tombstone's storage
157
+ * is gone, and rollback's post-move check prove a deletion or a presence by listing, so a listing that lags
158
+ * reports an erasure as incomplete or a stale object as gone.
159
+ * - **Never replay a conditional write without telling the replay apart.** A `putImmutable` that lands and then
160
+ * loses its response, sent again, finds its own object and would report a collision. Either send each write
161
+ * once, with the backend client's retry off for that request, or recognise your own write when the
162
+ * precondition fails by reading an id you stored with it back, and treat a match as success. The first is
163
+ * what the S3 driver and the single-request GCS upload do; the second is what the Azure Blob driver and the
164
+ * resumable GCS upload do.
165
+ * - **Raise a transient fault as {@link TransientError}** (throttling, a 5xx, a dropped connection), so the read
166
+ * retry can ride it out and a write's caller can tell it from a deterministic failure.
167
+ */
168
+ export interface IStorageDriver {
169
+ capabilities(): StorageCaps;
170
+ /**
171
+ * Stream a new immutable generation. The driver opens a destination, hands `write` a {@link BlobSink},
172
+ * then atomically commits (and computes the content hash). Throws {@link WriteConflictError} if the key
173
+ * already exists (write-once).
174
+ */
175
+ putImmutable(key: GenKey, write: (sink: BlobSink) => Promise<void>): Promise<{
176
+ size: number;
177
+ sha256: string;
178
+ }>;
179
+ /**
180
+ * Range read; the caller bounds-checks. Out-of-range throws {@link ValidationError}, never a short/adjacent
181
+ * read, and a missing object throws {@link NotFoundError}.
182
+ */
183
+ getRange(key: GenKey, offset: number, length: number): Promise<Uint8Array>;
184
+ /**
185
+ * Speculative tail read: the last `min(maxBytes, size)` bytes + the object's **true total size**. A missing
186
+ * object throws {@link NotFoundError}.
187
+ */
188
+ getTail(key: GenKey, maxBytes: number): Promise<{
189
+ bytes: Uint8Array;
190
+ size: number;
191
+ }>;
192
+ /** Idempotent: deleting an absent key is a no-op, not an error. */
193
+ delete(key: GenKey): Promise<void>;
194
+ /**
195
+ * Enumerate the generations present for a segment (orphan sweep / latest-gen resolution). Strongly
196
+ * consistent, read-after-delete and read-after-put.
197
+ */
198
+ list(ref: SegmentRef): AsyncIterable<GenKey>;
199
+ }
200
+ /**
201
+ * Lifecycle status of a segment. `active` is the steady state; `destroyed` is the post-crypto-shred tombstone
202
+ * (the row is kept for audit but the segment is logically gone).
203
+ */
204
+ export type RegistryStatus = 'active' | 'destroyed';
205
+ /**
206
+ * Free-form, JSON-serializable governance metadata — a **plain object** (both registry boundaries reject `null`,
207
+ * an array or a primitive, because a reader that tests `'key' in meta` would throw an untyped `TypeError`).
208
+ *
209
+ * `retention` has one **reserved key with defined semantics**: `expiresAt`, an absolute epoch-ms after which a
210
+ * retention sweep may retire the segment — written by `setSegmentRetention`, parsed by `readRetentionPolicy`. Any
211
+ * other key is yours (a legal hold, a note, an owner) and is preserved across policy writes. `residency` is still
212
+ * shape-later: stored and round-tripped, no semantics attached.
213
+ */
214
+ export type GovernanceMeta = Record<string, unknown>;
215
+ /**
216
+ * One registry row — the authoritative per-segment record. Exactly one per segment.
217
+ */
218
+ export interface RegistryRecord extends SegmentRef {
219
+ /**
220
+ * **The** authoritative LATEST pointer: which immutable Storage generation is current — or **`null` for a segment
221
+ * that has no Storage generation yet.**
222
+ *
223
+ * `null` is not "unknown", it is a positive statement: *this segment exists and has no Storage data.* It is what
224
+ * lets a segment have a registry row **before its first load** — `setSegmentRetention` mints one so a policy
225
+ * can be recorded ahead of the data, and so the segment is reachable by `registry.list()` and therefore by
226
+ * retention sweeps, `checkConsistency` and every other fleet-wide operation. The alternative — a row with
227
+ * `currentGen: 0` and no object behind it — is the forbidden `missing-storage-generation` state, which fails per
228
+ * read with `NotFoundError`. Generation resolution maps `null` onto the same path a segment with **no row**
229
+ * takes: every read answers empty, and the first publish advances the pointer.
230
+ */
231
+ readonly currentGen: number | null;
232
+ /**
233
+ * Per-segment data-key (DEK) wrappings for encryption-at-rest: the DEK envelope-wrapped under one or more KEKs
234
+ * (active + optional recovery). Reading unwraps with any held KEK; **crypto-shred deletes this whole list**,
235
+ * making the segment's at-rest bytes permanently unrecoverable. Absent ⇒ the segment is cleartext.
236
+ */
237
+ readonly wrappedDeks?: readonly WrappedDek[];
238
+ /**
239
+ * Optional **external** key reference (reserved) — e.g. a KMS key ARN for a future KMS keystore adapter that
240
+ * keeps wrapped material in the KMS rather than in-band {@link wrappedDeks}. Unused by the in-process BYOK
241
+ * keystore. Clearable on crypto-shred.
242
+ */
243
+ readonly keyId?: string;
244
+ readonly status: RegistryStatus;
245
+ /**
246
+ * Governance policy. `retention.expiresAt` drives the retention sweep (see {@link GovernanceMeta}); `residency`
247
+ * is stored and round-tripped with no semantics yet. Both must be plain objects, and both must survive a
248
+ * `list()` projection — a fleet sweep reads the policy from the enumeration rather than per-segment.
249
+ */
250
+ readonly retention?: GovernanceMeta;
251
+ readonly residency?: GovernanceMeta;
252
+ /** Epoch-ms of creation / last mutation (from the driver's injected clock). */
253
+ readonly createdAt: number;
254
+ readonly updatedAt: number;
255
+ /** Opaque OCC token — compare-by-equality, never reused (ABA-safe). */
256
+ readonly token: Token;
257
+ }
258
+ /** The caller-settable fields at {@link IRegistryDriver.create} (audit + token are driver-managed). */
259
+ export interface NewRegistryRecord {
260
+ /** `null` ⇒ the segment has no Storage generation yet — see {@link RegistryRecord.currentGen}. */
261
+ readonly currentGen: number | null;
262
+ readonly wrappedDeks?: readonly WrappedDek[];
263
+ readonly keyId?: string;
264
+ /** Defaults to `'active'`. */
265
+ readonly status?: RegistryStatus;
266
+ readonly retention?: GovernanceMeta;
267
+ readonly residency?: GovernanceMeta;
268
+ }
269
+ /** Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity + audit + token are off-limits). */
270
+ export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency'>>;
271
+ /** Capabilities a registry driver advertises; validated fail-fast at wiring time. */
272
+ export interface RegCaps {
273
+ /** REQUIRED — `currentGen` feeds read correctness + the publish CAS, so reads must be strongly consistent. */
274
+ readonly strongRead: true;
275
+ }
276
+ /**
277
+ * Brand for {@link StorageBackend}.
278
+ *
279
+ * `Symbol.for` rather than a class check, for the same reason the error brands are: a class object is only
280
+ * shared while one copy of core is loaded, so a backend built from one copy and checked by another — after a
281
+ * version skew, or behind a duplicating bundler — would fail an `instanceof`. A registered symbol is
282
+ * identity-stable across all of them, and across realms.
283
+ *
284
+ * A brand string is part of that identity contract, so once a release carries one it never changes: a changed
285
+ * string would stop matching the copies already installed.
286
+ */
287
+ declare const STORAGE_BACKEND_BRAND: unique symbol;
288
+ /**
289
+ * Where a store keeps everything: the generations, and the pointer saying which one is current.
290
+ *
291
+ * **Branded on purpose.** The two fields alone are not enough to qualify, and that is the whole point. This
292
+ * shape is also {@link LoadDeps}, so before the brand any `{ storage, registry }`
293
+ * object literal satisfied it — including one assembling halves from two *unrelated* stores, which the store
294
+ * accepted and then answered **empty** for a segment that holds data, because it read a pointer from a place
295
+ * nothing had ever been written. That is the exact silent-empty failure one-class-per-backend exists to
296
+ * remove, and it was reachable in five lines.
297
+ *
298
+ * So a backend comes from `MemoryStorage`, `LocalFsStorage`, `S3Storage`, `GcsStorage` or
299
+ * `AzureBlobStorage` — each deriving both halves from one bucket and one prefix. A driver author composing halves
300
+ * of their own says so on purpose, with {@link brandAsBackend}. The halves stay readable as `.storage` /
301
+ * `.registry`, because the free functions genuinely need them.
302
+ *
303
+ * **What this stops is the accident, not a determined caller.** The brand is a registered symbol, so it can
304
+ * be written by hand; nothing here is a security boundary. It stops the two spellings people reach for
305
+ * without deciding to — an object literal, and a spread of a real backend.
306
+ */
307
+ export interface StorageBackend {
308
+ /** Cross-bundle brand. Set by the backend classes; not part of the public surface. */
309
+ readonly [STORAGE_BACKEND_BRAND]: true;
310
+ /** Where the immutable `.crbm` generations live. */
311
+ readonly storage: IStorageDriver;
312
+ /** Where the `currentGen` pointer, the discovery index and the wrapped DEKs live. */
313
+ readonly registry: IRegistryDriver;
314
+ }
315
+ /**
316
+ * The brand key, for the type-only `declare` field a backend class carries.
317
+ *
318
+ * Exported from `@cloudbitmaps/core/driver-kit` and part of the driver contract: a third-party storage
319
+ * package needs it to declare the field that {@link brandAsBackend} then stamps at runtime.
320
+ */
321
+ export declare const STORAGE_BACKEND: typeof STORAGE_BACKEND_BRAND;
322
+ /**
323
+ * Stamp the brand on a backend, **non-enumerably**, after checking both halves are drivers. Returns `target`.
324
+ *
325
+ * This is the driver author's door, and it works on a backend class (`brandAsBackend(this)` as the last line of
326
+ * its constructor) and on a plain `{ storage, registry }` object alike — the latter is how a backend of your
327
+ * own halves is built: a driver wrapped for auditing, metrics, tenant scoping or client-side encryption, or
328
+ * paired with a registry in a database you already run. An application does not need it: it gets both halves
329
+ * from one backend class, which derives them from one bucket and one prefix so they cannot disagree.
330
+ *
331
+ * It throws {@link ValidationError} when `storage` has no `putImmutable` or `registry` has no `compareAndSwap`,
332
+ * so a half that is not a driver is refused here rather than at its first write, and when `target` is frozen or
333
+ * non-extensible, since the brand is stamped on it in place.
334
+ *
335
+ * It cannot check that the halves agree: `IStorageDriver` and `IRegistryDriver` do not expose a location, so
336
+ * nothing here can compare one. Branding two unrelated halves is you taking that on — the plain
337
+ * `{ storage, registry }` literal is refused by the store precisely because it let the two halves come from
338
+ * unrelated places by accident, and answer **empty** rather than fail.
339
+ *
340
+ * Non-enumerable is load-bearing, not tidiness. A class field (`readonly [BRAND] = true`) emits an
341
+ * *enumerable* own property, and spread and `Object.assign` copy exactly those — so
342
+ * `{ ...backend, registry: other.registry }` carried the brand and was accepted, reproducing the silent-empty
343
+ * bug the brand exists to close. This does not make the brand unforgeable — the symbol is registered, so anyone
344
+ * who wants it can write `Symbol.for('cloudbitmaps.storage-backend')`. That is deliberate effort equivalent to
345
+ * calling this function, and it is not what the check is for. The check is for the accident.
346
+ */
347
+ export declare function brandAsBackend<T extends {
348
+ storage: IStorageDriver;
349
+ registry: IRegistryDriver;
350
+ }>(target: T): T & StorageBackend;
351
+ /**
352
+ * Is this a branded backend — from a backend class or from {@link brandAsBackend}?
353
+ *
354
+ * Checks the brand, not the shape — see {@link StorageBackend} for why the shape alone is not enough.
355
+ */
356
+ export declare function isStorageBackend(value: unknown): value is StorageBackend;
357
+ /**
358
+ * Per-segment registry — the authoritative source of `currentGen`, the discovery index, and the wrapped-DEK
359
+ * holder. One row per segment under OCC: a never-reused, equality-compared {@link Token} (ABA-safe across
360
+ * delete→recreate).
361
+ *
362
+ * **What a driver must do** — callers rely on each of these, and the registry conformance suite holds every
363
+ * shipped driver to them:
364
+ *
365
+ * - **`create` and `compareAndSwap` are atomic conditional writes.** A `create` over a live row, and a
366
+ * `compareAndSwap` whose token is not the stored one, throw {@link WriteConflictError} and change nothing.
367
+ * Two racing writers have exactly one winner.
368
+ * - **Tokens are never reused**, not even across `delete` then `create`: a delete leaves a tombstone (or a
369
+ * global counter) so a recreated row always carries a fresh token and a stale holder cannot swap into it.
370
+ * - **`delete` is idempotent** without an `expected` token: deleting an absent row is a no-op, not an error.
371
+ * **With `expected` it is fenced**: it lands only while the row still carries that token, and otherwise throws
372
+ * {@link WriteConflictError} and leaves the row.
373
+ * - **Reads are strongly consistent** (`RegCaps.strongRead`), and `list` yields every existing row,
374
+ * `destroyed` tombstones included, with every field (see {@link IRegistryDriver.list}).
375
+ * - **Never replay a conditional write without telling the replay apart.** A `create` or `compareAndSwap`
376
+ * that lands and then loses its response, sent again, finds its own write and would report a conflict.
377
+ * Either send each write once, with the backend client's retry off for that request, or recognise your own
378
+ * write when the precondition fails by reading an id you stored with it back. The shipped registries do
379
+ * one or the other.
380
+ * - **Raise a transient fault as {@link TransientError}.**
381
+ */
382
+ export interface IRegistryDriver {
383
+ capabilities(): RegCaps;
384
+ /**
385
+ * The segment's record, or `null` if it doesn't exist (or was deleted). Throw {@link TransientError} for a fault
386
+ * worth riding out (throttling, a 5xx, a dropped connection): a reader whose pointer refresh meets one keeps
387
+ * serving what it holds and asks again shortly, while any other error reaches the read that met it.
388
+ */
389
+ get(ref: SegmentRef): Promise<RegistryRecord | null>;
390
+ /** Create the row; throws {@link WriteConflictError} if it already exists (use CAS to mutate). */
391
+ create(ref: SegmentRef, record: NewRegistryRecord): Promise<{
392
+ token: Token;
393
+ }>;
394
+ /** Server-side compare-and-set: apply `patch` iff the stored token equals `expected`, else `WriteConflictError`. */
395
+ compareAndSwap(ref: SegmentRef, expected: Token, patch: RegistryPatch): Promise<{
396
+ token: Token;
397
+ }>;
398
+ /**
399
+ * Discovery: every **existing** record, optionally scoped to one namespace. Order is unspecified.
400
+ *
401
+ * "Existing" means not `delete`d. A **`destroyed` tombstone is still a record and must be yielded** — a driver
402
+ * that filters by `status` breaks callers silently, and two already depend on seeing them: `runConsistencyCheck`
403
+ * skips them itself, and the retention sweep can only clean up a tombstone row it can see (filtering it makes
404
+ * the cleanup a permanent no-op, indistinguishable from having nothing to do, while dead rows accumulate). Same
405
+ * rule for every other field: a row with **`currentGen: null`** must be yielded like any other, and `retention`
406
+ * must survive the projection — a fleet sweep reads the policy straight out of this enumeration rather than
407
+ * paying a `get()` per segment, so a `list()` that drops the field means nothing ever expires, silently.
408
+ */
409
+ list(namespace?: string): AsyncIterable<RegistryRecord>;
410
+ /**
411
+ * Remove the row (tombstoned for ABA-safety — a later `create` still gets a fresh, greater token).
412
+ *
413
+ * Without `expected`, idempotent: deleting an absent row is a no-op.
414
+ *
415
+ * With `expected`, the delete is **fenced like a {@link IRegistryDriver.compareAndSwap}**: it lands only while
416
+ * the row still carries exactly that token, and otherwise — the token differs, or the row is absent or already
417
+ * deleted — throws {@link WriteConflictError} and leaves the row. Pass the token of the row you read when you
418
+ * decided to delete it, so a delete that is delayed, replayed, or racing a re-create cannot tombstone a row
419
+ * created after the decision. A third-party driver that ignores `expected` keeps the unfenced behaviour.
420
+ */
421
+ delete(ref: SegmentRef, expected?: Token): Promise<void>;
422
+ }
423
+ export {};
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The storage reader's defaults, stated once for the two modules that need them: the reader, which applies them,
3
+ * and the cost model, which prices what they cause. The cost model once imported them from the reader itself,
4
+ * which pulled the whole read path into a module that is otherwise arithmetic.
5
+ */
6
+ /** Default TTL (ms) for re-resolving a segment's `currentGen` — the bound on post-publish read staleness. */
7
+ export declare const DEFAULT_CURRENT_GEN_TTL_MS = 2000;
8
+ /**
9
+ * How long (ms) a reader keeps serving after a pointer refresh failed transiently, before the next read asks the
10
+ * registry again. Short, so an outage ends the stale serving soon after the registry answers; fixed, so a registry
11
+ * that is down is asked at most once per this interval per segment being read (the refresh is coalesced per
12
+ * segment). Never longer than the TTL itself.
13
+ */
14
+ export declare const REFRESH_RETRY_MS = 500;
15
+ /** Default ceiling on cached segment readers (each holds a parsed `.crbm` index) — the steady-state count bound. */
16
+ export declare const DEFAULT_MAX_OPEN_SEGMENTS = 1024;
17
+ /** Default aggregate ceiling (bytes) on resident parsed indices in the reader cache — the steady-state byte bound. */
18
+ export declare const DEFAULT_MAX_OPEN_INDEX_BYTES: number;
@@ -0,0 +1,47 @@
1
+ import type { IRegistryDriver, RegistryRecord } from './ports.js';
2
+ /**
3
+ * Ceiling on registry records one fleet-wide scan holds resident. Raise it via the caller's `maxScanSegments` when
4
+ * the fleet really is that large *and* the memory is there; narrow the scan with a `namespace` otherwise.
5
+ */
6
+ export declare const DEFAULT_MAX_SCAN_SEGMENTS = 250000;
7
+ /** Fail fast on a bad ceiling BEFORE the (possibly huge) scan, not after. */
8
+ export declare function validateMaxScanSegments(value: number, op: string): void;
9
+ /**
10
+ * Drain every registry record (optionally one namespace) into an array, refusing past `maxScanSegments`.
11
+ *
12
+ * The bound is checked **before** each push, so the array never exceeds the ceiling the caller agreed to. Pushing
13
+ * first and then throwing would leave the very row that broke the budget resident, and a caller sizing a container
14
+ * against `maxScanSegments` off by one row at the worst possible moment.
15
+ */
16
+ export declare function drainRegistry(registry: IRegistryDriver, options: {
17
+ namespace?: string;
18
+ maxScanSegments: number;
19
+ op: string;
20
+ /**
21
+ * How a caller of `op` raises the ceiling, completing "Narrow it with `namespace`, or …" in the refusal.
22
+ * Defaults to raising `maxScanSegments`, which is right wherever `op` takes that option itself.
23
+ */
24
+ raise?: string;
25
+ /**
26
+ * Whether the caller could scope the scan down to a `namespace`. Defaults to true; `eraseNamespace` is already
27
+ * one namespace, so it passes false and the refusal does not tell it to narrow what it cannot.
28
+ */
29
+ narrowable?: boolean;
30
+ }): Promise<RegistryRecord[]>;
31
+ /**
32
+ * Is this record **bookkeeping** rather than a segment? Covers every reserved family — today the due-index
33
+ * pointers. **Every unscoped fleet-wide enumeration must skip these**: the bounded drain,
34
+ * the export/eject scan, and the all-namespaces GDPR paths.
35
+ *
36
+ * This is the ONE place a reserved family is declared, and a new one belongs here rather than at the call
37
+ * sites. A comparison inlined per site is easy to miss at one of them — at `subjectReport`, the rows would consume
38
+ * an Art. 15 request's per-op budget. A filter you have to remember at each site is a check that cannot fire.
39
+ */
40
+ export declare function isReservedRow(record: Pick<RegistryRecord, 'namespace'>): boolean;
41
+ /**
42
+ * Filter bookkeeping rows out of a registry listing **before** anything downstream pays for them. Needed where
43
+ * the enumeration is consumed inside a budgeted collector rather than a plain loop: charging a GDPR Art. 15
44
+ * request's per-op budget for pointer rows can refuse a subject report for a reason that has nothing to do with
45
+ * the subject.
46
+ */
47
+ export declare function excludingReservedRows(source: AsyncIterable<RegistryRecord>): AsyncIterable<RegistryRecord>;