@cloudbitmaps/core 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-TNWTIAON.js → chunk-YSC7NJWV.js} +314 -216
- package/dist/chunk-YSC7NJWV.js.map +7 -0
- package/dist/core/audit.d.ts +56 -3
- package/dist/core/combine-many.d.ts +13 -4
- package/dist/core/consistency.d.ts +10 -4
- package/dist/core/crbm/fingerprint.d.ts +18 -0
- package/dist/core/crbm/format.d.ts +2 -4
- package/dist/core/crbm/frame.d.ts +10 -0
- package/dist/core/crbm/reader.d.ts +1 -3
- package/dist/core/crbm-storage-source.d.ts +249 -96
- package/dist/core/engine.d.ts +22 -14
- package/dist/core/erase-id.d.ts +62 -26
- package/dist/core/generation-gc.d.ts +3 -2
- package/dist/core/keys.d.ts +28 -0
- package/dist/core/leases.d.ts +7 -0
- package/dist/core/load.d.ts +11 -7
- package/dist/core/metrics.d.ts +5 -5
- package/dist/core/pinned-storage-source.d.ts +3 -3
- package/dist/core/pointer-id.d.ts +19 -0
- package/dist/core/ports.d.ts +131 -35
- package/dist/core/reader-defaults.d.ts +14 -3
- package/dist/core/rollback.d.ts +2 -1
- package/dist/core/summary.d.ts +38 -21
- package/dist/core/token.d.ts +13 -8
- package/dist/driver-kit.d.ts +2 -1
- package/dist/driver-kit.js +11 -4
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/object-registry.d.ts +1 -1
- package/dist/drivers/_shared/registry.d.ts +26 -17
- package/dist/drivers/localfs/storage.d.ts +7 -6
- package/dist/drivers/memory.d.ts +8 -6
- package/dist/drivers/retry/retrying-chunk-source.d.ts +1 -5
- package/dist/export/index.d.ts +16 -0
- package/dist/index.d.ts +2 -5
- package/dist/index.js +1288 -1080
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-TNWTIAON.js.map +0 -7
- package/dist/core/cost.d.ts +0 -351
package/dist/core/ports.d.ts
CHANGED
|
@@ -38,8 +38,9 @@ export interface GenKey extends SegmentRef {
|
|
|
38
38
|
readonly generation: number;
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
* A segment's
|
|
42
|
-
*
|
|
41
|
+
* A segment's on-disk footprint — the current generation's Storage object bytes, read cheaply from the `.crbm`
|
|
42
|
+
* footer/index (no payload reads). It is the `sizeBytes` a segment's `stat()` reports, and what bounds the bytes a combine
|
|
43
|
+
* holds for an operand's stream.
|
|
43
44
|
*/
|
|
44
45
|
export interface SegmentSize {
|
|
45
46
|
readonly sizeBytes: number;
|
|
@@ -119,16 +120,10 @@ export interface StorageChunkSource {
|
|
|
119
120
|
*/
|
|
120
121
|
getChunks?(ref: SegmentRef, keys: readonly number[], options?: ReadChunksOptions): AsyncIterable<ChunkRead>;
|
|
121
122
|
/**
|
|
122
|
-
* Optional: the current generation's
|
|
123
|
-
*
|
|
123
|
+
* Optional: the current generation's size, cheaply (from the already-parsed `.crbm` index — no payload reads), or
|
|
124
|
+
* `null` if the segment has no Storage generation.
|
|
124
125
|
*/
|
|
125
126
|
sizeOf?(ref: SegmentRef): Promise<SegmentSize | null>;
|
|
126
|
-
/**
|
|
127
|
-
* Optional: how often this source re-reads a segment's pointer while the segment is being read, in ms, or 0 if
|
|
128
|
-
* it never does. The grounded `costReport()` prices the pointer refresh at this interval; a source that omits it
|
|
129
|
-
* is priced at the store's default.
|
|
130
|
-
*/
|
|
131
|
-
readonly pointerRefreshMs?: number;
|
|
132
127
|
/**
|
|
133
128
|
* Optional: the current generation's **per-chunk cardinality** (`chunkKey → count`), read from the
|
|
134
129
|
* already-parsed `.crbm` index with **no payload reads**, or `null` if the segment has no Storage generation.
|
|
@@ -144,6 +139,17 @@ export interface StorageChunkSource {
|
|
|
144
139
|
* summary omits this, and `count()` keeps its other paths.
|
|
145
140
|
*/
|
|
146
141
|
summary?(ref: SegmentRef): Promise<GenerationSummary | null>;
|
|
142
|
+
/**
|
|
143
|
+
* Optional: what {@link summary} answers, with the size of the generation's object, both from **one** resolution of
|
|
144
|
+
* the segment, so the size is the size of the generation described; `null` if the segment has no Storage
|
|
145
|
+
* generation. The `.crbm` source answers from the registry row's summary when it can use it, whose fingerprint
|
|
146
|
+
* carries the object's size: one registry read and no read of the object. When it cannot, it opens the generation
|
|
147
|
+
* (one tail read of the object when it is not open already, and a range read for an index longer than the tail) and
|
|
148
|
+
* answers from its footer and index, with no payload read. For a source that omits it, `stat()` asks
|
|
149
|
+
* {@link summary} and {@link sizeOf}, two resolutions that can straddle a publish, and reports no size when
|
|
150
|
+
* there is no `sizeOf` either.
|
|
151
|
+
*/
|
|
152
|
+
stat?(ref: SegmentRef): Promise<(GenerationSummary & SegmentSize) | null>;
|
|
147
153
|
/**
|
|
148
154
|
* Optional: the segment's **current generation number** as this source resolves it right now (registry
|
|
149
155
|
* `currentGen`, or the highest storage generation), or `null` if the segment has no Storage generation. The engine
|
|
@@ -194,6 +200,10 @@ export interface StorageChunkSource {
|
|
|
194
200
|
* Note that putting the incarnation in the *object key* would not fix this: the new incarnation still starts
|
|
195
201
|
* at generation 0, so the cache key collides either way. The identity has to reach the cache.
|
|
196
202
|
*
|
|
203
|
+
* Within one incarnation a number can be taken again once its object is deleted, so two objects must never share
|
|
204
|
+
* a version either, however long after its pointer read a source opens the object: the `.crbm` source's version
|
|
205
|
+
* ends with the fingerprint of the object it opened.
|
|
206
|
+
*
|
|
197
207
|
* Returns `null` when the segment resolves to no generation. A source that only ever serves one immutable
|
|
198
208
|
* generation may omit this, and the engine falls back to the generation alone.
|
|
199
209
|
*/
|
|
@@ -207,6 +217,46 @@ export interface StorageCaps {
|
|
|
207
217
|
readonly maxObjectBytes: number;
|
|
208
218
|
/** Optional: enables the pure-object `LATEST`-pointer registry variant (S3 conditional put). */
|
|
209
219
|
readonly conditionalPut?: boolean;
|
|
220
|
+
/**
|
|
221
|
+
* Optional: `true` when `delete` applies {@link StorageDeleteOptions.ifVersion}. The driver then removes an object
|
|
222
|
+
* only while it is still the one that version names, by a check the backend applies in the same step as the removal
|
|
223
|
+
* (a precondition on the request, or a check and a removal with nothing between them), refuses another object under
|
|
224
|
+
* the key with {@link WriteConflictError}, and reports a `version` on every {@link IStorageDriver.getTail} of an
|
|
225
|
+
* object. `false` or absent: `delete` ignores `ifVersion` and removes whatever is under the key, and a caller that
|
|
226
|
+
* decided to delete one object can remove another stored under the key since.
|
|
227
|
+
*
|
|
228
|
+
* A driver that learns this from its client may report `false` until its first delete given `ifVersion`; what the
|
|
229
|
+
* delete does is what it states. The S3 driver does: it reads where the client sends its requests, and whether the
|
|
230
|
+
* SDK sends the header, on its first such delete, and reports `false` for good when that cannot be found out.
|
|
231
|
+
*/
|
|
232
|
+
readonly conditionalDelete?: boolean;
|
|
233
|
+
}
|
|
234
|
+
/** What {@link IStorageDriver.getTail} read. */
|
|
235
|
+
export interface TailRead {
|
|
236
|
+
/** The last `min(maxBytes, size)` bytes of the object. */
|
|
237
|
+
readonly bytes: Uint8Array;
|
|
238
|
+
/** The object's true total size, not the number of bytes returned. */
|
|
239
|
+
readonly size: number;
|
|
240
|
+
/**
|
|
241
|
+
* Optional: which object the read saw, as the backend names it (an S3 or Azure Blob ETag, a GCS object generation),
|
|
242
|
+
* taken from the response that carried `bytes`, or from the metadata read when no bytes were asked for. Opaque, and
|
|
243
|
+
* compared by equality only, by the driver that reported it. It names the object, not the key: two objects stored
|
|
244
|
+
* one after the other under one key carry different versions, except where the backend's version is computed from
|
|
245
|
+
* the bytes (an S3 ETag of an object stored without SSE-KMS or SSE-C is), where two objects with the same bytes, each
|
|
246
|
+
* stored whole in one request, or each in parts of the same sizes, share one. A driver that reports
|
|
247
|
+
* `conditionalDelete: true` reports it on every read; another may omit it.
|
|
248
|
+
*/
|
|
249
|
+
readonly version?: string;
|
|
250
|
+
}
|
|
251
|
+
/** Options of {@link IStorageDriver.delete}. */
|
|
252
|
+
export interface StorageDeleteOptions {
|
|
253
|
+
/**
|
|
254
|
+
* Delete the object only while it is the one this version names: the `version` a {@link IStorageDriver.getTail} of
|
|
255
|
+
* it reported. Pass the one read when the decision to delete was made, never one read after it, or the condition
|
|
256
|
+
* names whatever is under the key by then. Applied only by a driver that reports
|
|
257
|
+
* {@link StorageCaps.conditionalDelete}; ignored by any other, and absent, the delete is unconditional.
|
|
258
|
+
*/
|
|
259
|
+
readonly ifVersion?: string;
|
|
210
260
|
}
|
|
211
261
|
/**
|
|
212
262
|
* Immutable object storage for `.crbm` generations. A "dumb byte mover": it
|
|
@@ -235,9 +285,27 @@ export interface StorageCaps {
|
|
|
235
285
|
* negative or non-integer offset or length. Never a clamped, short or adjacent read. A pin whose object
|
|
236
286
|
* was purged and loaded again as a smaller one asks for a range past its end, and this error is what sends it
|
|
237
287
|
* to check that the object is still the one it pinned.
|
|
238
|
-
* - **`getTail` reports the object's true total size**, not the number of bytes it returned
|
|
288
|
+
* - **`getTail` reports the object's true total size**, not the number of bytes it returned, and, where the driver has
|
|
289
|
+
* one, the object's `version` ({@link TailRead.version}) from the same response as the bytes.
|
|
239
290
|
* - **`delete` is idempotent.** Deleting an absent key is a no-op, not an error: collection passes race each
|
|
240
|
-
* other and retry.
|
|
291
|
+
* other and retry. That holds with `ifVersion` too: an absent object is a no-op whatever version is given.
|
|
292
|
+
* - **`delete` with `ifVersion` does what {@link StorageCaps.conditionalDelete} says.** A driver that reports `true`
|
|
293
|
+
* removes the object only while it is the one the version names, by a check the backend applies in the same step as
|
|
294
|
+
* the removal (S3 `If-Match`, GCS `ifGenerationMatch`, Azure Blob `ifMatch`; in memory, the check and the removal with
|
|
295
|
+
* no await between them), and for another object under the key throws {@link WriteConflictError} and leaves it. A
|
|
296
|
+
* check made by reading the object first and deleting after is not one step, and does not qualify. Where the service
|
|
297
|
+
* answers a failed precondition for a key with no object, the driver looks again before it reports a conflict, so an
|
|
298
|
+
* absent object stays a no-op. A driver that reports `false`, or omits it, ignores `ifVersion` and deletes whatever is
|
|
299
|
+
* under the key. A number can be taken again once its object is deleted, so this is what keeps a delete decided from
|
|
300
|
+
* one read, and delayed, from removing an object stored under the number since: the erasure's delete of a holder
|
|
301
|
+
* above the pointer passes the version it read when it searched that object, and a refused load's delete of an
|
|
302
|
+
* object it proved its own by a footer read passes the version of that read.
|
|
303
|
+
* - **A driver that wraps another forwards `ifVersion` and the `version` of a tail read, or reports
|
|
304
|
+
* `conditionalDelete: false`.** A wrapper that hands `delete` the key alone, and reports the inner driver's
|
|
305
|
+
* capability, makes every conditional delete through it unconditional without a word.
|
|
306
|
+
* - **A conditional delete may be sent again**, by the backend client's retry, after a lost response: a copy that meets
|
|
307
|
+
* its own landed delete finds the object absent, which is its success, and one that meets an object stored since is
|
|
308
|
+
* refused. It can remove nothing the first copy could not.
|
|
241
309
|
* - **`list` is strongly consistent, read-after-delete.** Once `delete` has resolved, `list` no longer yields
|
|
242
310
|
* that generation, and once `putImmutable` has resolved it does. The erasure's re-check for a generation still
|
|
243
311
|
* holding the id, `dropSegment`'s `generationsRemaining`, the retention sweep's check that a tombstone's storage
|
|
@@ -272,15 +340,16 @@ export interface IStorageDriver {
|
|
|
272
340
|
*/
|
|
273
341
|
getRange(key: GenKey, offset: number, length: number): Promise<Uint8Array>;
|
|
274
342
|
/**
|
|
275
|
-
* Speculative tail read: the last `min(maxBytes, size)` bytes + the object's **true total size
|
|
276
|
-
* object throws {@link NotFoundError}.
|
|
343
|
+
* Speculative tail read: the last `min(maxBytes, size)` bytes + the object's **true total size**, and its `version`
|
|
344
|
+
* where the driver has one ({@link TailRead}). A missing object throws {@link NotFoundError}.
|
|
277
345
|
*/
|
|
278
|
-
getTail(key: GenKey, maxBytes: number): Promise<
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
346
|
+
getTail(key: GenKey, maxBytes: number): Promise<TailRead>;
|
|
347
|
+
/**
|
|
348
|
+
* Idempotent: deleting an absent key is a no-op, not an error. With `options.ifVersion`, on a driver that reports
|
|
349
|
+
* {@link StorageCaps.conditionalDelete}, the object is removed only while it is the one that version names, and
|
|
350
|
+
* another object under the key is left and the delete throws {@link WriteConflictError}; any other driver ignores it.
|
|
351
|
+
*/
|
|
352
|
+
delete(key: GenKey, options?: StorageDeleteOptions): Promise<void>;
|
|
284
353
|
/**
|
|
285
354
|
* Enumerate the generations present for a segment (orphan sweep / latest-gen resolution). Strongly
|
|
286
355
|
* consistent, read-after-delete and read-after-put.
|
|
@@ -303,10 +372,12 @@ export type RegistryStatus = 'active' | 'destroyed';
|
|
|
303
372
|
*/
|
|
304
373
|
export type GovernanceMeta = Record<string, unknown>;
|
|
305
374
|
/**
|
|
306
|
-
* A cached description of one generation, carried on the registry row: how many ids it holds,
|
|
307
|
-
*
|
|
308
|
-
* moves the pointer, and it names the generation it describes
|
|
309
|
-
* carry it can never be taken for another generation's.
|
|
375
|
+
* A cached description of one generation, carried on the registry row: how many ids it holds, the metadata it was
|
|
376
|
+
* loaded with, and the fingerprint of its object (its size and footer checksum). The generation's `.crbm` object stays
|
|
377
|
+
* the truth; this is a copy, written by the same write that moves the pointer, and it names the generation it describes
|
|
378
|
+
* so a copy left behind by a writer that did not carry it can never be taken for another generation's. The fingerprint
|
|
379
|
+
* names the object too: a reader checks every object it opens for the row's generation against it, and refuses one
|
|
380
|
+
* that is another object under that number.
|
|
310
381
|
*
|
|
311
382
|
* Two shapes. On a cleartext segment the values are in the clear. On an encrypted segment (a row with
|
|
312
383
|
* `wrappedDeks`) they are sealed under the segment's data key, as the generation's index is, so the row reveals
|
|
@@ -333,6 +404,12 @@ export interface ClearRegistrySummary {
|
|
|
333
404
|
readonly generation: number;
|
|
334
405
|
/** How many ids the generation holds: an integer from 0 to 2^32. */
|
|
335
406
|
readonly cardinality: number;
|
|
407
|
+
/**
|
|
408
|
+
* The fingerprint of the generation's object, `<size>:<footer checksum>` in decimal, as `CrbmReader.fingerprint` and
|
|
409
|
+
* a pin's `pinnedAt.fingerprint` spell it: the object's size in bytes, which `stat()` reports, and the CRC32C its
|
|
410
|
+
* footer stores.
|
|
411
|
+
*/
|
|
412
|
+
readonly fingerprint: string;
|
|
336
413
|
/** The generation's metadata, when it has any. Never the empty object. */
|
|
337
414
|
readonly metadata?: GenerationMetadata;
|
|
338
415
|
}
|
|
@@ -341,8 +418,9 @@ export interface SealedRegistrySummary {
|
|
|
341
418
|
/** The generation this describes. A non-negative safe integer. */
|
|
342
419
|
readonly generation: number;
|
|
343
420
|
/**
|
|
344
|
-
* Base64 of `nonce(12) ‖ ciphertext ‖ tag(16)`, sealing the cardinality as a little-endian u64
|
|
345
|
-
*
|
|
421
|
+
* Base64 of `nonce(12) ‖ ciphertext ‖ tag(16)`, sealing the cardinality as a little-endian u64, the object's size as a
|
|
422
|
+
* little-endian u64 and its footer checksum as a little-endian u32 (its fingerprint), then the metadata's canonical
|
|
423
|
+
* JSON, if any. The numbers are fixed-width, so the length reveals only the metadata's size.
|
|
346
424
|
*/
|
|
347
425
|
readonly sealed: string;
|
|
348
426
|
}
|
|
@@ -426,12 +504,23 @@ export interface RegistryRecord extends SegmentRef {
|
|
|
426
504
|
* restored from a backup. It holds with overwhelming probability rather than by construction, since the token carries random parts: a
|
|
427
505
|
* 128-bit incarnation id drawn when the row is created, and a 64-bit part drawn for each write. Two incarnations of one
|
|
428
506
|
* name meet with probability 2^-128 for any pair (about n^2 / 2^129 among n of them), and two writes at one counter,
|
|
429
|
-
* after a restore, with probability 2^-64.
|
|
430
|
-
* carries neither part.
|
|
507
|
+
* after a restore, with probability 2^-64.
|
|
431
508
|
*/
|
|
432
509
|
readonly token: Token;
|
|
510
|
+
/**
|
|
511
|
+
* The token of the most recent write that named a field a read resolves through: the row's create, or a compare-and-swap
|
|
512
|
+
* whose patch named `currentGen`, `status`, `wrappedDeks`, `keyId` or `summary` (the resolved fields), at any value,
|
|
513
|
+
* the one the field already had included. A write that names only `leases`, `retention`, `residency` or `keptGens`
|
|
514
|
+
* leaves it. The registry sets it; a caller never does. Compared by equality only, and never reused under one name,
|
|
515
|
+
* since a token is not: a reader keys what it caches on a generation with it, so a lease or a policy write leaves a
|
|
516
|
+
* warm reader's cache standing, and a write that changes the resolution never does.
|
|
517
|
+
*/
|
|
518
|
+
readonly pointerId: Token;
|
|
433
519
|
}
|
|
434
|
-
/**
|
|
520
|
+
/**
|
|
521
|
+
* The caller-settable fields at {@link IRegistryDriver.create} (audit, `token` and `pointerId` are driver-managed: the new
|
|
522
|
+
* row's `pointerId` is the token the create returns).
|
|
523
|
+
*/
|
|
435
524
|
export interface NewRegistryRecord {
|
|
436
525
|
/** `null` ⇒ the segment has no Storage generation yet — see {@link RegistryRecord.currentGen}. */
|
|
437
526
|
readonly currentGen: number | null;
|
|
@@ -447,13 +536,18 @@ export interface NewRegistryRecord {
|
|
|
447
536
|
readonly keptGens?: readonly number[];
|
|
448
537
|
}
|
|
449
538
|
/**
|
|
450
|
-
* Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity
|
|
539
|
+
* Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity, audit, `token` and `pointerId` are
|
|
540
|
+
* off-limits).
|
|
451
541
|
*
|
|
452
542
|
* Presence-based: a field the patch does not mention is left as it was. `summary` is the one exception, because it
|
|
453
543
|
* describes the current generation: a patch that moves `currentGen` and does not mention `summary` drops the old
|
|
454
544
|
* one rather than keep a description of another generation. A `summary` the patch gives must name the
|
|
455
545
|
* `currentGen` the row will have. `keptGens` follows the pointer the same way: a patch that moves `currentGen` and does
|
|
456
546
|
* not mention it drops it, and one that gives it must name only generations below the `currentGen` the row will have.
|
|
547
|
+
* "Moves" means changes value: a patch that names `currentGen` at the value it has keeps both.
|
|
548
|
+
*
|
|
549
|
+
* A patch that names a resolved field (`currentGen`, `status`, `wrappedDeks`, `keyId`, `summary`), by presence and at any
|
|
550
|
+
* value, sets the row's {@link RegistryRecord.pointerId} to the token the write returns; any other patch leaves it.
|
|
457
551
|
*/
|
|
458
552
|
export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary' | 'keptGens' | 'leases'>>;
|
|
459
553
|
/**
|
|
@@ -488,9 +582,7 @@ export interface RegCaps {
|
|
|
488
582
|
* `true` when this registry's `delete` removes a row from its backend for good, so that no later `list` reads it, and
|
|
489
583
|
* removes it only while it is still the exact version the delete read: by a delete the backend applies under a
|
|
490
584
|
* precondition (an S3 or Azure Blob ETag, a GCS object generation), or under a lock no other writer of the backend
|
|
491
|
-
* can take.
|
|
492
|
-
* 0.12 has a bare decimal token, and a delete still tombstones it: a process on that release, re-creating the name
|
|
493
|
-
* over nothing, would issue those counters again from 0, so its row could not be told apart from the deleted one.
|
|
585
|
+
* can take.
|
|
494
586
|
*
|
|
495
587
|
* `false` or absent: every `delete` leaves a tombstone, which every later full `list` still reads. A shipped registry
|
|
496
588
|
* says which it is.
|
|
@@ -609,6 +701,10 @@ export declare function isStorageBackend(value: unknown): value is StorageBacken
|
|
|
609
701
|
* keeps going; where nothing of the earlier row is left, or the row is restored from a backup to an older counter, the
|
|
610
702
|
* random parts alone keep the tokens apart: with probability 1 - 2^-128 per pair of incarnations, and 1 - 2^-64 per
|
|
611
703
|
* pair of writes at one counter.
|
|
704
|
+
* - **Every row carries its `pointerId`** ({@link RegistryRecord.pointerId}): `create` sets it to the token it returns,
|
|
705
|
+
* a `compareAndSwap` whose patch names a resolved field (`renewsPointer` in `@cloudbitmaps/core/driver-kit`) sets it
|
|
706
|
+
* to the token that write returns, and every other write leaves it. `get` and `list` return it. A caller's write
|
|
707
|
+
* never sets it. A row returned without one fails every read of the segment with {@link UnsupportedError}.
|
|
612
708
|
* - **`delete` is idempotent** without an `expected` token: deleting an absent row is a no-op, not an error.
|
|
613
709
|
* **With `expected` it is fenced**: it lands only while the row still carries that token, and otherwise throws
|
|
614
710
|
* {@link WriteConflictError} and leaves the row.
|
|
@@ -662,8 +758,8 @@ export interface IRegistryDriver {
|
|
|
662
758
|
*/
|
|
663
759
|
list(namespace?: string): AsyncIterable<RegistryRecord>;
|
|
664
760
|
/**
|
|
665
|
-
* Remove the row. A registry whose {@link RegCaps.conditionalDelete} is `true` removes
|
|
666
|
-
*
|
|
761
|
+
* Remove the row. A registry whose {@link RegCaps.conditionalDelete} is `true` removes it from its backend; any other
|
|
762
|
+
* tombstones it. Either way a later `create` gets a token no
|
|
667
763
|
* earlier incarnation of the name held, but for a collision of probability 2^-128 per pair of incarnations.
|
|
668
764
|
*
|
|
669
765
|
* Without `expected`, idempotent: deleting an absent row is a no-op.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The storage reader's defaults
|
|
3
|
-
*
|
|
4
|
-
* which pulled the whole read path into a module that is otherwise arithmetic.
|
|
2
|
+
* The storage reader's defaults. `@cloudbitmaps/tools` prices what two of them cause and keeps its own copies, which
|
|
3
|
+
* `tests/tools/store-defaults.test.ts` holds equal to these.
|
|
5
4
|
*/
|
|
6
5
|
/** Default TTL (ms) for re-resolving a segment's `currentGen` — the bound on post-publish read staleness. */
|
|
7
6
|
export declare const DEFAULT_CURRENT_GEN_TTL_MS = 2000;
|
|
@@ -16,3 +15,15 @@ export declare const REFRESH_RETRY_MS = 500;
|
|
|
16
15
|
export declare const DEFAULT_MAX_OPEN_SEGMENTS = 1024;
|
|
17
16
|
/** Default aggregate ceiling (bytes) on resident parsed indices in the reader cache — the steady-state byte bound. */
|
|
18
17
|
export declare const DEFAULT_MAX_OPEN_INDEX_BYTES: number;
|
|
18
|
+
/**
|
|
19
|
+
* How many segments' resolutions a store keeps per reader it keeps open: the resolution cache's count bound is this
|
|
20
|
+
* times `cache.readerMax` (8,192 by default). A resolution is the few hundred bytes of a row a read resolves through,
|
|
21
|
+
* kept for `cache.genTtlMs` whether or not the segment's reader is still open, so a store reading more segments at once
|
|
22
|
+
* than its readers hold keeps more of them than it keeps readers.
|
|
23
|
+
*/
|
|
24
|
+
export declare const RESOLUTIONS_PER_OPEN_SEGMENT = 8;
|
|
25
|
+
/**
|
|
26
|
+
* The resolution cache's byte bound is `cache.readerMaxBytes` divided by this (4 MiB by default): what it holds is a
|
|
27
|
+
* sixteenth of what the readers may.
|
|
28
|
+
*/
|
|
29
|
+
export declare const RESOLUTION_BYTES_DIVISOR = 16;
|
package/dist/core/rollback.d.ts
CHANGED
|
@@ -95,7 +95,8 @@ export interface RollbackResult {
|
|
|
95
95
|
* `currentGen`, where generation collection never looks. They stay until loads pass them (the first load whose
|
|
96
96
|
* number one of them holds numbers above them all, and collection then keeps the newest `keep` of what is below its
|
|
97
97
|
* pointer), `dropSegment` deletes them, or a subject erasure
|
|
98
|
-
* deletes them: all of those present when it rewrites, and
|
|
98
|
+
* deletes them: all of those present when it rewrites, and, when the current generation does not hold the id, only those
|
|
99
|
+
* holding it and any no read of the segment can open, whatever the id.
|
|
99
100
|
* Not collecting them is a deliberate trade: an operator who has just undone a bad load should not have the
|
|
100
101
|
* evidence collected out from under them, while a rollback target that still holds erased data would make the
|
|
101
102
|
* erasure undoable by an ordinary operator action.
|
package/dist/core/summary.d.ts
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
* A generation's summary on its registry row: how a writer builds it, how a reader opens it, and how it is held
|
|
3
3
|
* against the object it describes.
|
|
4
4
|
*
|
|
5
|
-
* The row carries a cached description of the current generation, `{ generation, cardinality, metadata? }`,
|
|
6
|
-
* the same compare-and-swap that moves the pointer.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* The row carries a cached description of the current generation, `{ generation, cardinality, fingerprint, metadata? }`,
|
|
6
|
+
* written by the same compare-and-swap that moves the pointer. The fingerprint names the generation's object, by its
|
|
7
|
+
* size and footer checksum, as `CrbmReader.fingerprint` does. On a cleartext segment the values are in the clear. On an
|
|
8
|
+
* encrypted one the row holds `{ generation, sealed }` instead: the cardinality as a fixed-width little-endian u64, the
|
|
9
|
+
* object's size as a u64 and its footer checksum as a u32, then the metadata's canonical JSON if there is any, sealed
|
|
10
|
+
* under the segment's data key with the summary scope of the associated data, which binds it to its namespace, segment
|
|
11
|
+
* and generation. The numbers are fixed-width so the ciphertext's length reveals only the metadata's size, never how
|
|
12
|
+
* many digits the count or the size has. Pure: no I/O.
|
|
11
13
|
*
|
|
12
14
|
* The generation's `.crbm` object stays the truth, and the summary is a copy of it. {@link summaryAgrees} is the
|
|
13
|
-
* comparison of one with the other.
|
|
15
|
+
* comparison of one with the other, and the fingerprint is what a reader holds every object it opens against.
|
|
14
16
|
*/
|
|
15
17
|
import type { Aead } from './crypto.js';
|
|
16
18
|
import type { ClearRegistrySummary, GenerationMetadata, RegistryRecord, RegistrySummary, SealedRegistrySummary, SegmentRef } from './ports.js';
|
|
@@ -19,28 +21,39 @@ export interface GenerationDescription {
|
|
|
19
21
|
readonly cardinality: number;
|
|
20
22
|
readonly metadata?: GenerationMetadata | undefined;
|
|
21
23
|
}
|
|
22
|
-
/**
|
|
23
|
-
export
|
|
24
|
+
/** What a row's summary says: the generation's description, and the fingerprint of the object it describes. */
|
|
25
|
+
export interface SummaryDescription extends GenerationDescription {
|
|
26
|
+
readonly fingerprint: string;
|
|
27
|
+
}
|
|
28
|
+
/** The size of the object a fingerprint names, in bytes: what `stat()` reports from a row's summary. */
|
|
29
|
+
export declare function sizeOfFingerprint(fingerprint: string): number;
|
|
30
|
+
/**
|
|
31
|
+
* The clear summary of a generation on a cleartext segment, naming its object by `fingerprint`. `metadata` is left off
|
|
32
|
+
* when there is none.
|
|
33
|
+
*/
|
|
34
|
+
export declare function clearSummary(generation: number, cardinality: number, fingerprint: string, metadata?: GenerationMetadata): ClearRegistrySummary;
|
|
24
35
|
/**
|
|
25
|
-
* The sealed summary of a generation on an encrypted segment: the cardinality as a u64,
|
|
26
|
-
* JSON, sealed under `aead` and bound to `ref` and
|
|
36
|
+
* The sealed summary of a generation on an encrypted segment: the cardinality as a u64, the object's size as a u64 and
|
|
37
|
+
* its footer checksum as a u32, then the metadata's canonical JSON, sealed under `aead` and bound to `ref` and
|
|
38
|
+
* `generation`.
|
|
27
39
|
*/
|
|
28
|
-
export declare function sealSummary(aead: Aead, ref: SegmentRef, generation: number, cardinality: number, metadata?: GenerationMetadata): SealedRegistrySummary;
|
|
40
|
+
export declare function sealSummary(aead: Aead, ref: SegmentRef, generation: number, cardinality: number, fingerprint: string, metadata?: GenerationMetadata): SealedRegistrySummary;
|
|
29
41
|
/**
|
|
30
42
|
* What a sealed summary says, opened with `aead` under the summary scope of `ref` and the summary's own generation.
|
|
31
43
|
* The bytes are untrusted: a blob that is not base64, is the wrong size, fails its authentication (tampered, or moved
|
|
32
|
-
* from another generation's row), or holds a count outside 0 to 2^32
|
|
33
|
-
* {@link IntegrityError}.
|
|
44
|
+
* from another generation's row), or holds a count outside 0 to 2^32, a size no object can have, or metadata that
|
|
45
|
+
* breaks the rules, is an {@link IntegrityError}.
|
|
34
46
|
*/
|
|
35
|
-
export declare function openSummary(aead: Aead, ref: SegmentRef, summary: SealedRegistrySummary):
|
|
47
|
+
export declare function openSummary(aead: Aead, ref: SegmentRef, summary: SealedRegistrySummary): SummaryDescription;
|
|
36
48
|
/**
|
|
37
49
|
* What a row's summary says of its current generation, or `undefined` when it says nothing a reader may use. It is
|
|
38
50
|
* used only for the generation it names, on an active row, and in the shape the row's keys call for: clear on a row
|
|
39
51
|
* with no wrapped keys, sealed on a row with them. A sealed one is opened with `aead`, and is unusable without it,
|
|
40
|
-
* or when it does not open, so a summary a tamperer moved or rewrote is never believed
|
|
41
|
-
* summary: the row's copy is a cache, and a copy that cannot be
|
|
52
|
+
* or when it does not open, so a summary a tamperer moved or rewrote is never believed; a clear one whose fingerprint
|
|
53
|
+
* names no object is unusable too. Never throws for a bad summary: the row's copy is a cache, and a copy that cannot be
|
|
54
|
+
* used is a copy that is not there.
|
|
42
55
|
*/
|
|
43
|
-
export declare function usableSummary(ref: SegmentRef, row: RegistryRecord, aead: Aead | undefined):
|
|
56
|
+
export declare function usableSummary(ref: SegmentRef, row: Pick<RegistryRecord, 'summary' | 'status' | 'currentGen' | 'wrappedDeks'>, aead: Aead | undefined): SummaryDescription | undefined;
|
|
44
57
|
/**
|
|
45
58
|
* Whether a summary's description of a generation is what its object holds: the same id count and the same metadata,
|
|
46
59
|
* compared in canonical form, so key order never matters. A summary with metadata over an object with none, and the
|
|
@@ -54,8 +67,12 @@ export declare function summaryAgrees(summary: GenerationDescription, object: Ge
|
|
|
54
67
|
* metadata block is not authenticated, so a block stripped from one reads as none, and the sealed summary on the row is
|
|
55
68
|
* the one copy that says there was a block; carrying it keeps the rewrite from erasing the record of what it dropped.
|
|
56
69
|
* Pass `authenticated` only for a summary that was opened (a clear one has no more authority than the block it sits
|
|
57
|
-
* beside). Never refuses
|
|
70
|
+
* beside). Never refuses. The erasure reaches this with a stripped block only past a fingerprint forged to match the
|
|
71
|
+
* row's, since stripping the block changes the object's size and the open is held to the row's fingerprint.
|
|
58
72
|
*/
|
|
59
73
|
export declare function metadataToCarry(object: GenerationMetadata | undefined, authenticated: GenerationDescription | undefined): GenerationMetadata | undefined;
|
|
60
|
-
/**
|
|
61
|
-
|
|
74
|
+
/**
|
|
75
|
+
* The summary of a generation of `ref`, describing the object `description.fingerprint` names, clear or sealed under
|
|
76
|
+
* `aead` as the segment's keys call for.
|
|
77
|
+
*/
|
|
78
|
+
export declare function summaryOf(ref: SegmentRef, generation: number, description: SummaryDescription, aead: Aead | undefined): RegistrySummary;
|
package/dist/core/token.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* A shipped registry gives a row it creates a token `<incarnation>.<counter>.<write>`: 32 lowercase hex digits drawn
|
|
5
5
|
* from injected entropy when the row is created, a decimal counter, and 16 hex digits drawn for each write. Every write
|
|
6
6
|
* of the row keeps the incarnation, and a name created again draws a new one, so two incarnations are told apart by it
|
|
7
|
-
* even when their rows were created in the same millisecond or one is gone. A
|
|
8
|
-
*
|
|
7
|
+
* even when their rows were created in the same millisecond or one is gone. A registry of someone else's issues tokens
|
|
8
|
+
* in any form, and one whose tokens carry no incarnation id is told apart only by the row's creation stamp.
|
|
9
9
|
*
|
|
10
10
|
* This lives in `core/` so the publish can read it, and the registry drivers, which build the tokens, import it from
|
|
11
11
|
* here: the form has one definition.
|
|
@@ -14,18 +14,23 @@ import type { Token } from './ports.js';
|
|
|
14
14
|
/** An incarnation-form token: the incarnation id, the counter and a write part, `.`-separated, in lowercase hex. */
|
|
15
15
|
export declare const INCARNATION_TOKEN: RegExp;
|
|
16
16
|
/**
|
|
17
|
-
* The incarnation id in a token, or `undefined` for one that has none: a
|
|
18
|
-
*
|
|
17
|
+
* The incarnation id in a token, or `undefined` for one that has none: a token a registry of someone else's issues in
|
|
18
|
+
* another form. Two tokens with the same incarnation are writes of one incarnation of a row.
|
|
19
19
|
*/
|
|
20
20
|
export declare function incarnationOf(token: Token): string | undefined;
|
|
21
|
+
/** An audit event's `incarnation`, from the token of the row it is about: absent for a token that carries none. */
|
|
22
|
+
export declare function incarnationField(token: Token | undefined): {
|
|
23
|
+
readonly incarnation?: string;
|
|
24
|
+
};
|
|
21
25
|
/**
|
|
22
26
|
* Whether two reads of one name's row are one incarnation of it.
|
|
23
27
|
*
|
|
24
28
|
* - Both tokens carry an incarnation id: the ids decide, and the clock stamps do not enter.
|
|
25
|
-
* - Only one does: different incarnations. A
|
|
26
|
-
*
|
|
27
|
-
* - Neither does (a
|
|
28
|
-
* is the only signal left. It is a clock reading, so a caller that acts on this answer proves its own object
|
|
29
|
+
* - Only one does: different incarnations. A registry gives its rows one form of token, so a row without an id never
|
|
30
|
+
* becomes a row with one, nor the reverse.
|
|
31
|
+
* - Neither does (a registry of someone else's whose tokens carry none): there is no id to compare, and the creation
|
|
32
|
+
* stamp is the only signal left. It is a clock reading, so a caller that acts on this answer proves its own object
|
|
33
|
+
* too.
|
|
29
34
|
*/
|
|
30
35
|
export declare function sameIncarnation(a: {
|
|
31
36
|
readonly token: Token;
|
package/dist/driver-kit.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export type { GenKey, IRegistryDriver, IStorageDriver, SegmentRef, StorageBackend, StorageCaps, } from './core/ports.js';
|
|
1
|
+
export type { GenKey, IRegistryDriver, IStorageDriver, SegmentRef, StorageBackend, StorageCaps, StorageDeleteOptions, TailRead, } from './core/ports.js';
|
|
2
2
|
export { brandAsBackend, STORAGE_BACKEND } from './core/ports.js';
|
|
3
3
|
export type { IMetricsSink, MetricEvent } from './core/metrics.js';
|
|
4
4
|
export { IntegrityError, isIntegrityError, isNotFoundError, isTransientError, isValidationError, isWriteConflictError, NotFoundError, TransientError, ValidationError, WriteConflictError, } from './core/errors.js';
|
|
5
|
+
export { RESOLVED_FIELDS, pointerIdOf, renewPointer, renewsPointer } from './core/pointer-id.js';
|
|
5
6
|
export { validateSegmentRef } from './core/validate.js';
|
|
6
7
|
export type { BlobSink } from './core/blob.js';
|
|
7
8
|
export { MAX_ROW_BYTES, ObjectStoreRegistry, ObjectVersionRaced, } from './drivers/_shared/object-registry.js';
|
package/dist/driver-kit.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
DEFAULT_NAMESPACE,
|
|
3
3
|
IntegrityError,
|
|
4
4
|
NotFoundError,
|
|
5
|
+
RESOLVED_FIELDS,
|
|
5
6
|
STORAGE_BACKEND,
|
|
6
7
|
TransientError,
|
|
7
8
|
ValidationError,
|
|
@@ -12,7 +13,6 @@ import {
|
|
|
12
13
|
encodeNameForKey,
|
|
13
14
|
encodeNameForPath,
|
|
14
15
|
entropyIsAvailable,
|
|
15
|
-
incarnationOf,
|
|
16
16
|
isIntegrityError,
|
|
17
17
|
isNotFoundError,
|
|
18
18
|
isTransientError,
|
|
@@ -24,13 +24,16 @@ import {
|
|
|
24
24
|
newIncarnationToken,
|
|
25
25
|
nextRegistryToken,
|
|
26
26
|
parseRegistryEnvelope,
|
|
27
|
+
pointerIdOf,
|
|
27
28
|
recordFromNew,
|
|
29
|
+
renewPointer,
|
|
30
|
+
renewsPointer,
|
|
28
31
|
serializeRegistryEnvelope,
|
|
29
32
|
validateNewRegistryRecord,
|
|
30
33
|
validateRegistryPatch,
|
|
31
34
|
validateSegmentRef,
|
|
32
35
|
webCryptoEntropy
|
|
33
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-YSC7NJWV.js";
|
|
34
37
|
|
|
35
38
|
// src/drivers/_shared/object-registry-keys.ts
|
|
36
39
|
var REGISTRY_SUFFIX = ".reg";
|
|
@@ -137,7 +140,7 @@ var ObjectStoreRegistry = class {
|
|
|
137
140
|
const conditionalDelete = this.removesRows();
|
|
138
141
|
return entropyIsAvailable(this.entropy) ? { strongRead: true, conditionalDelete } : { strongRead: true, canWrite: false, conditionalDelete };
|
|
139
142
|
}
|
|
140
|
-
/** Whether a delete removes a row
|
|
143
|
+
/** Whether a delete removes a row: the store has a delete, and vouches for it. */
|
|
141
144
|
removesRows() {
|
|
142
145
|
return this.store.conditionalDelete === true && typeof this.store.delete === "function";
|
|
143
146
|
}
|
|
@@ -242,7 +245,7 @@ var ObjectStoreRegistry = class {
|
|
|
242
245
|
}
|
|
243
246
|
try {
|
|
244
247
|
const remove = this.removesRows() ? this.store.delete : void 0;
|
|
245
|
-
if (remove !== void 0
|
|
248
|
+
if (remove !== void 0) {
|
|
246
249
|
await remove.call(this.store, key, { version: current.version });
|
|
247
250
|
return;
|
|
248
251
|
}
|
|
@@ -316,6 +319,7 @@ export {
|
|
|
316
319
|
NotFoundError,
|
|
317
320
|
ObjectStoreRegistry,
|
|
318
321
|
ObjectVersionRaced,
|
|
322
|
+
RESOLVED_FIELDS,
|
|
319
323
|
STORAGE_BACKEND,
|
|
320
324
|
TransientError,
|
|
321
325
|
ValidationError,
|
|
@@ -331,7 +335,10 @@ export {
|
|
|
331
335
|
namespaceKeyPart,
|
|
332
336
|
namespacePathPart,
|
|
333
337
|
normalizeObjectPrefix,
|
|
338
|
+
pointerIdOf,
|
|
334
339
|
prefixPart,
|
|
340
|
+
renewPointer,
|
|
341
|
+
renewsPointer,
|
|
335
342
|
validateSegmentRef
|
|
336
343
|
};
|
|
337
344
|
//# sourceMappingURL=driver-kit.js.map
|