@cloudbitmaps/core 0.20.0 → 0.22.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 (77) hide show
  1. package/README.md +101 -5
  2. package/dist/{chunk-YSC7NJWV.js → chunk-OSU7W2Y3.js} +811 -164
  3. package/dist/chunk-OSU7W2Y3.js.map +7 -0
  4. package/dist/core/audit.d.ts +12 -2
  5. package/dist/core/bit-route.d.ts +9 -1
  6. package/dist/core/blob.d.ts +12 -1
  7. package/dist/core/codec.d.ts +1 -1
  8. package/dist/core/combine-expr.d.ts +10 -8
  9. package/dist/core/combine-feed.d.ts +12 -18
  10. package/dist/core/combine-many.d.ts +29 -22
  11. package/dist/core/concurrency.d.ts +8 -0
  12. package/dist/core/consistency.d.ts +69 -17
  13. package/dist/core/crbm/reader.d.ts +52 -6
  14. package/dist/core/crbm-storage-source.d.ts +65 -452
  15. package/dist/core/crypto.d.ts +6 -1
  16. package/dist/core/determinism.d.ts +2 -1
  17. package/dist/core/engine.d.ts +222 -33
  18. package/dist/core/erase-id.d.ts +60 -15
  19. package/dist/core/erasure.d.ts +5 -4
  20. package/dist/core/errors.d.ts +61 -11
  21. package/dist/core/leases.d.ts +4 -3
  22. package/dist/core/load.d.ts +58 -45
  23. package/dist/core/lru.d.ts +2 -15
  24. package/dist/core/metrics.d.ts +5 -3
  25. package/dist/core/pinned-storage-source.d.ts +4 -4
  26. package/dist/core/ports.d.ts +96 -22
  27. package/dist/core/registry-scan.d.ts +0 -7
  28. package/dist/core/retention-sweep.d.ts +70 -66
  29. package/dist/core/retention.d.ts +2 -2
  30. package/dist/core/retry.d.ts +15 -18
  31. package/dist/core/rollback.d.ts +61 -31
  32. package/dist/core/validate.d.ts +2 -0
  33. package/dist/driver-kit.d.ts +11 -2
  34. package/dist/driver-kit.js +84 -16
  35. package/dist/driver-kit.js.map +3 -3
  36. package/dist/drivers/_shared/bytes.d.ts +6 -0
  37. package/dist/drivers/_shared/faults.d.ts +15 -0
  38. package/dist/drivers/_shared/object-keys.d.ts +18 -0
  39. package/dist/drivers/_shared/object-registry-keys.d.ts +0 -2
  40. package/dist/drivers/_shared/object-registry.d.ts +2 -0
  41. package/dist/drivers/_shared/options.d.ts +19 -0
  42. package/dist/drivers/backends.d.ts +2 -0
  43. package/dist/drivers/crypto.d.ts +1 -1
  44. package/dist/export/index.d.ts +9 -8
  45. package/dist/index.d.ts +11 -10
  46. package/dist/index.js +4202 -2833
  47. package/dist/index.js.map +4 -4
  48. package/package.json +7 -2
  49. package/dist/chunk-YSC7NJWV.js.map +0 -7
  50. package/dist/core/chunk-checks.d.ts +0 -34
  51. package/dist/core/chunk-stream.d.ts +0 -22
  52. package/dist/core/chunk-window.d.ts +0 -30
  53. package/dist/core/combine-ledger.d.ts +0 -43
  54. package/dist/core/crbm/crc32c.d.ts +0 -10
  55. package/dist/core/crbm/fingerprint.d.ts +0 -18
  56. package/dist/core/crbm/format.d.ts +0 -151
  57. package/dist/core/crbm/frame.d.ts +0 -10
  58. package/dist/core/crbm/plan-reads.d.ts +0 -44
  59. package/dist/core/crbm/varint.d.ts +0 -13
  60. package/dist/core/crbm/writer.d.ts +0 -68
  61. package/dist/core/due-index.d.ts +0 -53
  62. package/dist/core/generation-gc.d.ts +0 -179
  63. package/dist/core/item-pull.d.ts +0 -29
  64. package/dist/core/kept-generations.d.ts +0 -40
  65. package/dist/core/metadata.d.ts +0 -42
  66. package/dist/core/reader-defaults.d.ts +0 -29
  67. package/dist/core/summary.d.ts +0 -78
  68. package/dist/core/token.d.ts +0 -41
  69. package/dist/drivers/_shared/entropy.d.ts +0 -5
  70. package/dist/drivers/_shared/registry.d.ts +0 -104
  71. package/dist/drivers/localfs/exact-case.d.ts +0 -37
  72. package/dist/drivers/localfs/fs-util.d.ts +0 -33
  73. package/dist/drivers/localfs/paths.d.ts +0 -40
  74. package/dist/drivers/localfs/registry.d.ts +0 -44
  75. package/dist/drivers/localfs/storage.d.ts +0 -21
  76. package/dist/drivers/memory.d.ts +0 -61
  77. package/dist/testing/fuzz-core.d.ts +0 -15
@@ -38,7 +38,7 @@ export type AuditEvent = {
38
38
  } | {
39
39
  /**
40
40
  * A segment's pointer was moved by a rollback to a generation still in the bucket that the caller named:
41
- * **backwards**, or forward with `allowForward`, which undoes an earlier rollback.
41
+ * **backwards**, or forward onto a generation a rollback left above the pointer, which undoes an earlier rollback.
42
42
  *
43
43
  * The only non-forward-only pointer move in the library, and the only one no automatic path can perform —
44
44
  * a human decided the current generation was wrong and named the one they wanted. Every other pointer move
@@ -54,6 +54,14 @@ export type AuditEvent = {
54
54
  readonly incarnation?: string;
55
55
  readonly fromGeneration: number | null;
56
56
  readonly generation: number;
57
+ /**
58
+ * Present, and `true`, when the row did not vouch for the target: the call passed `allowUnknown` for a generation the
59
+ * row does not know or whose object is not the one it recorded, or the row has no usable record of its generations
60
+ * (a registry that drops the field, or a pointer moved by hand) or the target is below a full record's bound, where the
61
+ * pointer alone decides for a target below it. Absent when the move was
62
+ * checked against the row's record.
63
+ */
64
+ readonly unknown?: true;
57
65
  } | {
58
66
  /**
59
67
  * A load was **refused**: it did not publish, because its result failed a guard or another writer got there
@@ -109,7 +117,9 @@ export type AuditEvent = {
109
117
  * exists only for a finished erasure. A call that ends otherwise throws or reports `erased: false` with another
110
118
  * reason and emits nothing, even when it had deleted a holder; its re-run emits when it finishes, and reports
111
119
  * `'not-member'` with no event when another collection deleted the last holder meanwhile, as its ledger entry
112
- * does.
120
+ * does. One finished erasure emits it and then throws `NotFoundError`: one that found, after its deletes, the
121
+ * segment's pointer on a generation it had deleted (a rollback moved it there meanwhile), so the deletion is on
122
+ * the record although the call cannot answer.
113
123
  *
114
124
  * `fromGeneration` is the newest generation the call found holding the id, and is absent when it found the id in
115
125
  * none and deleted only objects it could not search: then the ledger lists no entry for the segment, and this
@@ -1,11 +1,18 @@
1
+ /**
2
+ * BitRouteSplitter — maps a 32-bit id to its chunk (top 16 bits) and remainder
3
+ * (bottom 16 bits), per the data model.
4
+ */
5
+ import { ValidationError } from './errors.js';
1
6
  export declare const U32_MAX = 4294967295;
2
7
  export declare const CHUNK_COUNT = 65536;
3
- export interface BitRoute {
8
+ interface BitRoute {
4
9
  /** Top 16 bits — the chunk key (0..65535). */
5
10
  readonly chunkKey: number;
6
11
  /** Bottom 16 bits — the value within the chunk (0..65535). */
7
12
  readonly remainder: number;
8
13
  }
14
+ /** Was `err` thrown by {@link splitId} for an id outside the u32 range? */
15
+ export declare function isBadIdError(err: unknown): err is ValidationError;
9
16
  /** Split a u32 id into `{ chunkKey, remainder }`. Throws `ValidationError` for non-u32 input. */
10
17
  export declare function splitId(id: number): BitRoute;
11
18
  /**
@@ -19,3 +26,4 @@ export declare const MAX_REMAINDER: number;
19
26
  * invariant 5); `remainder` is masked to 16 bits.
20
27
  */
21
28
  export declare function joinId(chunkKey: number, remainder: number): number;
29
+ export {};
@@ -12,12 +12,23 @@ export interface BlobSink {
12
12
  export interface BlobReader {
13
13
  /** Exactly `length` bytes starting at `offset`. Out-of-range is a `ValidationError`, never a short read. */
14
14
  getRange(offset: number, length: number): Promise<Uint8Array>;
15
- /** The last `min(maxBytes, size)` bytes plus the total size — the one-GET footer+index path. */
15
+ /**
16
+ * The last `min(maxBytes, size)` bytes plus the total size — the one-GET footer+index path. `maxBytes` is a safe
17
+ * integer, zero or less for an empty tail; any other length is refused with `ValidationError`.
18
+ */
16
19
  getTail(maxBytes: number): Promise<{
17
20
  bytes: Uint8Array;
18
21
  size: number;
19
22
  }>;
20
23
  }
24
+ /**
25
+ * Check the length a tail read asks for, the one rule every tail read holds to (each storage driver's `getTail` and
26
+ * {@link BufferReader.getTail}): a safe integer. Zero or less answers an empty tail with the size, and a length past
27
+ * the object's size answers the whole object. Anything else is refused with `ValidationError` before anything is read:
28
+ * `NaN`, a fraction (a negative one too), an infinity of either sign, and a whole number past the safe range (2^53 or
29
+ * more, or -2^53 or less), so no reader takes one for some other length.
30
+ */
31
+ export declare function checkTailLength(maxBytes: number): void;
21
32
  /** In-memory `BlobSink` that accumulates written bytes; `bytes()` returns the concatenation. */
22
33
  export declare class BufferSink implements BlobSink {
23
34
  private readonly chunks;
@@ -103,7 +103,7 @@ export interface CodecBitmap {
103
103
  /**
104
104
  * This set as storage chunks: ascending by chunk key, empty chunks left out, each payload exactly the bytes that
105
105
  * {@link CodecInterface.fromValues} of that chunk's low 16 bits, then {@link optimize}, then {@link serialize}
106
- * would give. So a generation written from these is byte for byte the one the same ids write. It may re-encode
106
+ * would give. So a generation written from these holds chunk for chunk what the same ids write. It may re-encode
107
107
  * this bitmap in place first, as `optimize()` does; membership is untouched.
108
108
  *
109
109
  * **Flavor-author surface, and optional.** A load from a bitmap or from serialized bytes writes these, never
@@ -1,10 +1,14 @@
1
- /** A string names an operand; a node holds exactly one operator over a non-empty list of expressions. */
2
- export type CombineExpr = string | {
3
- and: CombineExpr[];
1
+ /**
2
+ * An expression over the operands of a batch combine (a store's `materializeMany`): a string names an operand; a node
3
+ * holds exactly one of `and`, `or` or `andNot` over a non-empty list of expressions. `andNot` subtracts every entry after
4
+ * the first from the first, so it takes at least two. Plain JSON, so it can be logged and sent between tasks.
5
+ */
6
+ export type Expr = string | {
7
+ and: Expr[];
4
8
  } | {
5
- or: CombineExpr[];
9
+ or: Expr[];
6
10
  } | {
7
- andNot: CombineExpr[];
11
+ andNot: Expr[];
8
12
  };
9
13
  /** The most operator nodes one expression may nest. A literal that contains itself is refused by it. */
10
14
  export declare const MAX_EXPR_DEPTH = 64;
@@ -21,7 +25,7 @@ export type ExprNode = {
21
25
  readonly kids: readonly ExprNode[];
22
26
  };
23
27
  /** How many nodes one {@link compileExpr} call may still allocate; shared across the expressions of one output. */
24
- export interface NodeBudget {
28
+ interface NodeBudget {
25
29
  remaining: number;
26
30
  }
27
31
  /**
@@ -38,8 +42,6 @@ export interface KeyBounds {
38
42
  readonly keys: Uint16Array;
39
43
  readonly card: Uint32Array;
40
44
  }
41
- /** The keys of an operand the plan cannot know (a fed one): every key. Compared by identity, never by content. */
42
- export declare const ANY_KEYS: Uint16Array;
43
45
  /** The bounds of an operand the plan cannot know. */
44
46
  export declare const ANY_BOUNDS: KeyBounds;
45
47
  /** What {@link planKeys} keeps of each operator node: the keys it can be non-empty at. */
@@ -1,21 +1,14 @@
1
+ import type { CodecBitmap, CodecInterface } from './codec.js';
1
2
  /**
2
- * The chunk-ordered feed of a batch combine: operands that arrive as records, one chunk key at a time, and are checked
3
- * and converted as they arrive. Pure: it reads through the iterable it is given and knows no store, clock or storage.
4
- *
5
- * ```
6
- * record { key, operands: { name: Uint32Array } }
7
- * check key in range and not below the last; every name declared; every value a real Uint32Array; ids strictly
8
- * ascending and inside the key; each operand at most once per key (a refusal, never a repair)
9
- * convert each operand's ids into one compressed bitmap, charged to the ledger as it is built
10
- * group the records of one key, held until a record with a higher key arrives or the feed ends
11
- * end of feed counts equal what was seen; a declared operand that never appeared is refused unless allowed
12
- * ```
13
- *
14
- * Nothing here ever reads a bad feed as fewer members: every check is a refusal, and a refusal ends the feed.
3
+ * One record of the feed of a batch combine (a store's `materializeMany`): the ids some of the fed operands hold at one
4
+ * chunk key. `key` is `id >>> 16`, an integer from 0 to 65,535, never below the previous record's; a key may arrive as
5
+ * several consecutive records, each carrying some of the operands, and one record per key is the usual shape. `operands`
6
+ * maps a declared fed name to its ids at the key: a real `Uint32Array` (not an `Int32Array`, `Buffer`, array or
7
+ * look-alike), strictly ascending, every id inside the key. A name is given at most once per key, and an empty array is
8
+ * the same as leaving the name out. The record is copied as it arrives, so the caller may drop or reuse its arrays once
9
+ * it has yielded it.
15
10
  */
16
- import type { CodecBitmap, CodecInterface } from './codec.js';
17
- /** One record of a feed: the ids each named operand holds at one chunk key. */
18
- export interface CombineManyFeedRecord {
11
+ export interface MaterializeManyFeedRecord {
19
12
  /** The chunk key, `id >>> 16`, from 0 to 65,535. Non-decreasing over the feed; a key may arrive as several records. */
20
13
  readonly key: number;
21
14
  /** Ids by operand name: ascending, unique, every one inside `key`. An operand is named at most once per key. */
@@ -26,7 +19,7 @@ export interface CombineManyFeed {
26
19
  /** The fed operand names, declared up front. */
27
20
  readonly names: readonly string[];
28
21
  /** The records, read once, in key order. */
29
- readonly records: AsyncIterable<CombineManyFeedRecord>;
22
+ readonly records: AsyncIterable<MaterializeManyFeedRecord>;
30
23
  /**
31
24
  * The ids each fed operand holds over the whole feed, keyed by exactly `names`: an object, or a function called once
32
25
  * after the last record. A count that differs from what the feed held refuses every fed output.
@@ -56,7 +49,7 @@ export declare class FeedClosed extends Error {
56
49
  export declare class FeedStale extends Error {
57
50
  }
58
51
  /** What a {@link FeedCursor} needs from the pass that runs it. */
59
- export interface FeedCursorDeps {
52
+ interface FeedCursorDeps {
60
53
  readonly feed: CheckedFeed;
61
54
  readonly codec: CodecInterface;
62
55
  /** The index the first fed operand has among the call's operands. */
@@ -136,3 +129,4 @@ export declare class FeedCursor {
136
129
  /** The counts and the empty rule, run once, after the iterator has ended and before anything is published. */
137
130
  private finish;
138
131
  }
132
+ export {};
@@ -1,18 +1,16 @@
1
1
  import type { CodecBitmap, CodecInterface } from './codec.js';
2
2
  import type { IMetricsSink } from './metrics.js';
3
3
  import type { CombineManyFeed } from './combine-feed.js';
4
- import type { CombineExpr } from './combine-expr.js';
4
+ import type { Expr } from './combine-expr.js';
5
5
  import type { Clock } from './determinism.js';
6
6
  import type { Budget } from './budget.js';
7
7
  import type { LoadGuard } from './load.js';
8
8
  import type { GenerationMetadata, SegmentRef, StorageChunkSource } from './ports.js';
9
- export type { CombineExpr } from './combine-expr.js';
9
+ export type { Expr } from './combine-expr.js';
10
10
  export { MAX_EXPR_DEPTH, MAX_EXPR_NODES } from './combine-expr.js';
11
- export type { CombineManyFeed, CombineManyFeedRecord } from './combine-feed.js';
11
+ export type { CombineManyFeed, MaterializeManyFeedRecord } from './combine-feed.js';
12
12
  /** The range requests a pass keeps in flight at once across all its operands, inside a client's socket pool. */
13
13
  export declare const MAX_RANGES_IN_FLIGHT = 64;
14
- /** The largest `publishConcurrency` and `concurrency` a call may ask for. */
15
- export declare const MAX_BATCH_PARALLELISM = 1024;
16
14
  /** One named stored operand, as the pass reads it. */
17
15
  export interface CombineManyOperand {
18
16
  readonly name: string;
@@ -50,9 +48,9 @@ export interface CombineManyOperand {
50
48
  }
51
49
  /** What one output asks for. */
52
50
  export interface CombineManyOutput<R> {
53
- readonly expr: CombineExpr;
51
+ readonly expr: Expr;
54
52
  /** Subtracted from the result: a root `andNot`. */
55
- readonly exclude?: readonly CombineExpr[];
53
+ readonly exclude?: readonly Expr[];
56
54
  readonly allowEmpty?: boolean;
57
55
  readonly guard?: LoadGuard;
58
56
  readonly metadata?: GenerationMetadata;
@@ -128,8 +126,8 @@ export type CombineManyOutcome<R> = {
128
126
  readonly ok: false;
129
127
  readonly error: unknown;
130
128
  };
131
- /** What the call did to one operand. */
132
- export interface CombineManyOperandStats {
129
+ /** What a batch combine (a store's `materializeMany`) did to one operand: see {@link MaterializeManyStats.operands}. */
130
+ export interface MaterializeManyOperandStats {
133
131
  /** Whether an output of the call read it. An operand no output names is not read. */
134
132
  readonly read: boolean;
135
133
  /** Whether the operand was read at a pinned generation. */
@@ -157,8 +155,8 @@ export interface CombineManyOperandStats {
157
155
  /** Chunk reads made of it, over all groups. */
158
156
  readonly chunkReads: number;
159
157
  }
160
- /** What the call did to one output. */
161
- export interface CombineManyOutputStats {
158
+ /** What a batch combine (a store's `materializeMany`) did to one output: see {@link MaterializeManyStats.outputs}. */
159
+ export interface MaterializeManyOutputStats {
162
160
  /** The names of the operands its expression reads: the stored ones in call order, then the fed ones in `feed.names` order. */
163
161
  readonly operands: readonly string[];
164
162
  /** The group that computed it (from 0), or `null` when it never ran. */
@@ -173,7 +171,11 @@ export interface CombineManyOutputStats {
173
171
  /** The most its buffer held resident, in the ledger's own measure; compare with `boundBytes` to see how tight a plan was. */
174
172
  readonly peakBytes: number;
175
173
  }
176
- export interface CombineManyStats {
174
+ /**
175
+ * What a batch combine (a store's `materializeMany`) did: the groups it ran, its requests by class, the chunk-read budget
176
+ * it used, the most it held resident, and, for each operand, the generation it was pinned at, began at and ended at.
177
+ */
178
+ export interface MaterializeManyStats {
177
179
  /** Groups run, the re-runs of outputs a lying index made too big included. */
178
180
  readonly groups: number;
179
181
  readonly requests: {
@@ -235,19 +237,17 @@ export interface CombineManyStats {
235
237
  readonly ids: number;
236
238
  };
237
239
  /** By operand name, an object with no prototype, so any name is a key. */
238
- readonly operands: Readonly<Record<string, CombineManyOperandStats>>;
239
- readonly outputs: readonly CombineManyOutputStats[];
240
+ readonly operands: Readonly<Record<string, MaterializeManyOperandStats>>;
241
+ readonly outputs: readonly MaterializeManyOutputStats[];
240
242
  }
241
243
  export interface CombineManyRun<R> {
242
244
  readonly outputs: readonly CombineManyOutcome<R>[];
243
- readonly stats: CombineManyStats;
245
+ readonly stats: MaterializeManyStats;
244
246
  }
245
247
  /** A request that passed every check that needs no I/O: what {@link runCombineMany} runs. */
246
248
  export interface CompiledCombineMany<R> {
247
249
  readonly request: CombineManyRequest<R>;
248
250
  }
249
- /** The most nodes the expressions of one call may hold in all, so that a thousand large outputs cannot be planned at once. */
250
- export declare const MAX_CALL_NODES = 200000;
251
251
  /**
252
252
  * Check a request: every expression, option and number, before any request is made. Throws {@link ValidationError}
253
253
  * naming the output index and the path. Pure.
@@ -259,7 +259,7 @@ export declare function compileCombineMany<R>(request: CombineManyRequest<R>): C
259
259
  */
260
260
  export declare function rebindCombineMany<R>(compiled: CompiledCombineMany<R>, operands: readonly CombineManyOperand[]): CompiledCombineMany<R>;
261
261
  /** What {@link GroupCost} reads of an output. */
262
- export interface GroupCostItem {
262
+ interface GroupCostItem {
263
263
  readonly bound: number;
264
264
  readonly serializedBound: number;
265
265
  readonly rootKeys: Uint16Array;
@@ -270,14 +270,15 @@ export interface GroupCostItem {
270
270
  }
271
271
  /**
272
272
  * Work counters of the planner, for the tests that hold it to its cost: how many times a group was priced, how many
273
- * operands those prices walked, and how many group plans were built. Plain numbers, never read by the planner.
273
+ * operands those prices walked, and how many group plans were built. A call counts into the one it is given
274
+ * ({@link runCombineManyCounted}), and into none otherwise; the planner never reads them.
274
275
  */
275
- export declare const combineWork: {
276
+ export interface PlanWork {
276
277
  adds: number;
277
278
  withs: number;
278
279
  operandSteps: number;
279
280
  planBuilds: number;
280
- };
281
+ }
281
282
  /**
282
283
  * The resident bytes a group needs, kept as outputs are added so that one is priced in work proportional to its own
283
284
  * operands, not to the group's size: its buffered outputs, its operand streams, the chunks being evaluated, the objects
@@ -286,6 +287,8 @@ export declare const combineWork: {
286
287
  export declare class GroupCost {
287
288
  private readonly streamBytes;
288
289
  private readonly publishConcurrency;
290
+ /** Where this pricing is counted, when it is. */
291
+ private readonly work?;
289
292
  private outputs;
290
293
  private depth;
291
294
  /** The serialized bounds of the largest outputs, descending: at most `publishConcurrency` of them are written at once. */
@@ -294,7 +297,9 @@ export declare class GroupCost {
294
297
  private streams;
295
298
  private planWork;
296
299
  private readonly operands;
297
- constructor(streamBytes: (operand: number) => number, publishConcurrency: number);
300
+ constructor(streamBytes: (operand: number) => number, publishConcurrency: number,
301
+ /** Where this pricing is counted, when it is. */
302
+ work?: PlanWork | undefined);
298
303
  /** The cost of the group with `o` added. */
299
304
  with(o: GroupCostItem): number;
300
305
  add(o: GroupCostItem): void;
@@ -313,6 +318,8 @@ export declare class GroupCost {
313
318
  * budget, both before any chunk is read.
314
319
  */
315
320
  export declare function runCombineMany<R>(compiled: CompiledCombineMany<R>, deps: CombineManyDeps): Promise<CombineManyRun<R>>;
321
+ /** {@link runCombineMany}, counting the planner's work into `work`. Not exported from the package. */
322
+ export declare function runCombineManyCounted<R>(compiled: CompiledCombineMany<R>, deps: CombineManyDeps, work: PlanWork): Promise<CombineManyRun<R>>;
316
323
  /**
317
324
  * The `pointerId` of a row's version (`<generation>:<pointerId>`, as a pin holds it), or `undefined` where the version
318
325
  * names no row. A live version from the `.crbm` source ends with the opened object's fingerprint: read it through
@@ -13,3 +13,11 @@
13
13
  * S3 page, chunk map), so an array keeps the pool trivial and race-free (no shared-iterator hazard).
14
14
  */
15
15
  export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
16
+ /**
17
+ * `Promise.all` over `items` with at most `limit` calls of `fn` in flight: the results in input order, or the first
18
+ * failure, at once. Unlike {@link mapWithConcurrency} it does not wait for the calls still in flight before it rejects,
19
+ * as `Promise.all` does not; it starts no call after a failure, and what the calls in flight answer is dropped. For reads
20
+ * a caller abandons on the first fault, never for work that must finish or be accounted for once begun. When every item
21
+ * fits under `limit` it is `Promise.all` itself, so a small fan-out costs and interleaves exactly as `Promise.all` does.
22
+ */
23
+ export declare function boundedAll<T, R>(items: readonly T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]>;
@@ -3,18 +3,23 @@
3
3
  * generations can be restored **independently**, so a failover can recover the registry *ahead of* the storage
4
4
  * objects — leaving `currentGen` pointing at a generation whose `.crbm` isn't present yet. That is likelier
5
5
  * than it sounds even with both in one bucket: a restore scoped to a prefix, or replayed
6
- * per-object from a version history, recovers the two prefixes at different points. That's a torn restore: reads of the affected segment then throw. This scan
6
+ * per-object from a version history, recovers the two prefixes at different points. That's a torn restore: reads of
7
+ * the affected segment then throw. This scan
7
8
  * detects it up front (run it at startup after a restore) instead of discovering it on the first read.
8
9
  *
9
10
  * Read-only; bounded fan-out. `destroyed` (crypto-shredded) segments are skipped — their Storage is intentionally
10
11
  * gone/unreadable, not a torn restore. A segment whose Storage/registry can't be read this pass is recorded in
11
- * `errored` (never aborts the scan). Each segment is checked against its **authoritative live pointer** — one
12
+ * `errored` (never aborts the scan). One segment can add several entries to `inconsistent` (its current object, and with
13
+ * `generations` one per known generation whose object is not the one recorded) and can also be in `errored`, when a
14
+ * read failed after those were found; with `generations` a segment costs up to one read per entry of its ledger.
15
+ * Each segment is checked against its **authoritative live pointer** — one
12
16
  * strong `registry.get` per segment — never the enumeration snapshot from `registry.list`, which a driver may
13
17
  * serve eventually-consistently and so lag a recent in-place pointer advance: trusting it would both
14
18
  * miss a torn *live* generation and cry torn on a generation the pointer has already advanced past (GC'd during
15
- * the scan). Residual: a load's publish plus a GC landing in the tiny per-segment get→list gap can still yield a
16
- * transient false positive — run the scan against a quiesced fleet (the documented restore procedure), or re-run
17
- * to confirm a reported tear.
19
+ * the scan). Residual: a load's publish plus a GC landing in the per-segment gap between the pointer read and the
20
+ * object reads (one read, or with `generations` as many as the ledger names) can still yield a transient false
21
+ * positive — run the scan against a quiesced fleet (the documented restore procedure), or re-run to confirm a
22
+ * reported tear.
18
23
  */
19
24
  import type { IStorageDriver, IRegistryDriver } from './ports.js';
20
25
  import type { IKeystore } from './crypto.js';
@@ -33,7 +38,11 @@ export { DEFAULT_MAX_SCAN_SEGMENTS } from './registry-scan.js';
33
38
  export interface ConsistencyIssue {
34
39
  readonly segment: string;
35
40
  readonly namespace?: string;
36
- /** The registry's `currentGen` for the segment — the generation whose `.crbm` is missing from Storage. */
41
+ /**
42
+ * The registry's `currentGen` for the segment. For `missing-storage-generation` it is the generation whose `.crbm` is
43
+ * missing from Storage; for the other issues it is the segment's current generation, whichever generation the issue
44
+ * is about (see `generation`).
45
+ */
37
46
  readonly currentGen: number;
38
47
  /**
39
48
  * `missing-storage-generation`: `currentGen` references a Storage generation that is not present (torn restore).
@@ -44,8 +53,20 @@ export interface ConsistencyIssue {
44
53
  * that opens the object refuses it; one whose count or metadata disagrees is answered from until a read opens the
45
54
  * object, which stops that store using it; one whose fingerprint names no object is not used, so every read opens the
46
55
  * object unchecked.
56
+ * `known-generation-mismatch` (only when asked for): the object under the number `generation` is not the one the row's
57
+ * ledger of known generations recorded for it: its footer checksum differs, or its footer fails its own checks (it is
58
+ * not a `.crbm` object, or is cut short). It says nothing of what a rollback does with that generation.
59
+ * What it covers: each object's footer is held to the recorded checksum, so another object, or bytes that are not a
60
+ * `.crbm`, under a known number is found. It does not find damage inside an object whose footer is intact (a cut or a
61
+ * flipped byte in the body: a read of that generation reports it as `IntegrityError`), nor a metadata change that keeps
62
+ * the footer's bytes.
47
63
  */
48
- readonly issue: 'missing-storage-generation' | 'summary-mismatch';
64
+ readonly issue: 'missing-storage-generation' | 'summary-mismatch' | 'known-generation-mismatch';
65
+ /**
66
+ * With `known-generation-mismatch` only: the generation whose object is not the one the row recorded. It is
67
+ * `currentGen` when the current object is the one that differs. Absent on every other issue.
68
+ */
69
+ readonly generation?: number;
49
70
  }
50
71
  /** A segment that could not be checked this pass (Storage/registry read fault) — not proof of a torn restore. */
51
72
  export interface ConsistencyErrorEntry {
@@ -57,12 +78,17 @@ export interface ConsistencyErrorEntry {
57
78
  export interface ConsistencyReport {
58
79
  /** Registered segments scanned (a `destroyed` segment counts as scanned but is never an issue). */
59
80
  readonly checked: number;
60
- /** Segments whose `currentGen` `.crbm` is absent — recover the object store (or restore to a coherent point). */
81
+ /**
82
+ * The issues found, one entry each: a segment whose `currentGen` `.crbm` is absent (recover the object store, or
83
+ * restore to a coherent point), and with the options asked for the other kinds. One segment can have several entries:
84
+ * at most one for its current object plus one per entry of its ledger, so at most 1 plus the ledger's length (384 at
85
+ * most). The ledger's length is bounded by the registry, and the report is not capped.
86
+ */
61
87
  readonly inconsistent: ConsistencyIssue[];
62
88
  /**
63
89
  * Segments that couldn't be read this pass (a transient/partial object store during a restore is exactly when
64
- * this runs) — **triage these too**: an unread segment is neither proven-coherent nor proven-torn. Empty on a
65
- * clean pass.
90
+ * this runs) — **triage these too**: an unread segment is neither proven-coherent nor proven-torn. A segment can be in
91
+ * this list and in `inconsistent` too: what was found before the read that failed is kept. Empty on a clean pass.
66
92
  */
67
93
  readonly errored: ConsistencyErrorEntry[];
68
94
  /**
@@ -71,19 +97,44 @@ export interface ConsistencyReport {
71
97
  * only when summaries were asked for.
72
98
  */
73
99
  readonly summariesUnchecked?: number;
100
+ /**
101
+ * With `generations: true`, the rows with a pointer whose ledger of known generations could not be used (absent, or
102
+ * failing the validation every reader of it applies, or not shaped as a ledger a registry of this library stores):
103
+ * none of their known generations was held against its object. A count, not an issue. Present only when generations
104
+ * were asked for.
105
+ */
106
+ readonly generationsUnchecked?: number;
74
107
  }
75
108
  /**
76
109
  * Verify every registered segment's `currentGen` `.crbm` actually exists in Storage. Enumerates the registry
77
- * (optionally one namespace) and, for each non-`destroyed` segment, checks the object store lists that
78
- * generation. Returns the torn segments in `inconsistent` (empty ⇒ coherent) and any unreadable segments in
110
+ * (optionally one namespace) and, for each non-`destroyed` segment, asks the object store for that generation with a
111
+ * zero-length tail read. Returns the torn segments in `inconsistent` (empty ⇒ coherent) and any unreadable segments in
79
112
  * `errored`.
80
113
  *
114
+ * When that read fails, in any way, the segment's listing decides and its own error is the segment's, so a missing
115
+ * object, a missing bucket and a failing store are reported as when every segment was listed. One difference: a store
116
+ * that answers reads but refuses listings (an identity without list permission, or a listing-only outage) reports each
117
+ * object that is there as present, where every segment was reported errored.
118
+ *
81
119
  * With `summaries: true` it also opens each segment's current object (one tail read, and a second for an index longer
82
- * than it) and holds the row's summary against it: the object it names by its fingerprint, and the count and metadata it
83
- * says, reporting `summary-mismatch` where they disagree, and for a clear summary whose fingerprint names no object,
84
- * which no read uses. A sealed summary
85
- * needs `deps.keystore`; without it the segment is counted in `summariesUnchecked`. Off by default, since the
86
- * default check only lists.
120
+ * than it) and holds the row's summary against it: the object it names by its fingerprint, and the count and metadata
121
+ * it says, reporting `summary-mismatch` where they disagree, and for a clear summary whose fingerprint names no
122
+ * object, which no read uses. For a row with a clear summary that open is the only request of the object; if it fails
123
+ * before its first read answered, the listing decides, and once that read has answered a failure is an error of the
124
+ * segment. A sealed summary needs `deps.keystore`; without it the segment is counted in `summariesUnchecked`. Off by
125
+ * default, since the default check only looks for the object.
126
+ *
127
+ * With `generations: true` it also holds every generation a row's ledger of known generations names, the current one
128
+ * included, against its object: one footer read each (104 bytes, no key, not retried), reporting
129
+ * `known-generation-mismatch` with the `generation` where the object's footer checksum is not the one the ledger records
130
+ * or its footer fails its own checks. That finds another object, or bytes that are not a `.crbm`, under a known number;
131
+ * it does not find damage inside an object whose footer is intact (a read of it reports that as `IntegrityError`), nor a
132
+ * metadata change that keeps the footer's bytes. A segment adds at most 1 plus its ledger's length issues. A named
133
+ * generation that is not in the bucket is not reported. A row with a pointer whose ledger cannot be used (absent, failing
134
+ * the validation every reader of it applies, or not shaped as a ledger a registry of this library stores) is counted in
135
+ * `generationsUnchecked` and none of its objects is read. The listing is not used: a read of a missing object
136
+ * answers `NotFoundError`, which is how an absent one is told. A segment whose current object is missing is reported as
137
+ * ever and its older generations are still checked. Off by default.
87
138
  */
88
139
  export declare function runConsistencyCheck(deps: {
89
140
  readonly storage: IStorageDriver;
@@ -94,4 +145,5 @@ export declare function runConsistencyCheck(deps: {
94
145
  concurrency?: number;
95
146
  maxScanSegments?: number;
96
147
  summaries?: boolean;
148
+ generations?: boolean;
97
149
  }): Promise<ConsistencyReport>;
@@ -7,7 +7,7 @@ import type { ChunkRead, GenerationMetadata, ReadChunksOptions } from '../ports.
7
7
  * `cardinalityMinusOne` stores `cardinality - 1` because a chunk's cardinality runs `[1, 65536]` and 65,536 does
8
8
  * not fit a `u16`; an offset is a `f64` because an object past 4 GiB puts a payload beyond a `u32`.
9
9
  */
10
- export interface ParsedIndex {
10
+ interface ParsedIndex {
11
11
  readonly keys: Uint16Array;
12
12
  readonly cardinalityMinusOne: Uint16Array;
13
13
  readonly lengths: Uint32Array;
@@ -29,7 +29,12 @@ export declare const RETAINED_BYTES_PER_INDEX_ENTRY = 20;
29
29
  */
30
30
  export declare function indexCapacity(indexLength: number): number;
31
31
  export interface CrbmReaderOptions {
32
- /** Speculative tail size in bytes (default 256 KB; clamped up to at least the footer size). */
32
+ /**
33
+ * How many bytes the first tail read asks for, a whole number (default 256 KiB; clamped up to at least the footer
34
+ * size). Any value of at least an object's `tailBytes` opens it in that one read. Where an open keeps a small object's
35
+ * chunks (a `CrbmStorageChunkSource`'s does), it keeps them only when this read covers the whole object, so a value
36
+ * below the default can leave them to reads of their own.
37
+ */
33
38
  readonly tailBytes?: number;
34
39
  /** Hard cap on a single chunk payload length (default: the 1 MiB decode cap, plus the 28-byte nonce and tag on an encrypted object). The engine decodes under `maxBitmapBytes`; raise this with it. */
35
40
  readonly maxPayloadBytes?: number;
@@ -49,6 +54,20 @@ export interface CrbmReaderOptions {
49
54
  */
50
55
  readonly lineage?: unknown;
51
56
  }
57
+ /**
58
+ * Refuses a `size` that is not a whole byte count its own tail fits in. The size is the tier's word too: one that
59
+ * is not would turn off every bounds check an open makes against it.
60
+ */
61
+ export declare function checkSize(tail: Uint8Array, size: number): void;
62
+ /**
63
+ * What the footer of the object behind `blob` says with no key and no index, from one tail read of a footer's worth:
64
+ * whether it is encrypted, and its CRC (the one in {@link CrbmReader.fingerprint}). A footer that fails its own checks
65
+ * throws, as an open would.
66
+ */
67
+ export declare function readFooter(blob: BlobReader): Promise<{
68
+ readonly encrypted: boolean;
69
+ readonly footerCrc: number;
70
+ }>;
52
71
  /**
53
72
  * Whether the object behind `blob` says it is encrypted, from one tail read of a footer's worth, with no key and no
54
73
  * index; a footer that fails its own checks throws, as an open would. For a caller that must not open an object with
@@ -61,6 +80,14 @@ export declare function footerSaysEncrypted(blob: BlobReader): Promise<boolean>;
61
80
  * object and that region is at most `keepChunkBytesUpTo` bytes. Not exported from a package entry.
62
81
  */
63
82
  export declare function openCrbmReaderKeeping(blob: BlobReader, options: CrbmReaderOptions, keepChunkBytesUpTo: number | undefined): Promise<CrbmReader>;
83
+ /**
84
+ * How many bytes the first tail read of an open asks for: the caller's `tailBytes` or the default, and a footer's worth
85
+ * at least. Not exported from a package entry: {@link CrbmReader.open} and a caller that sends that read ahead of it
86
+ * ask for the same size.
87
+ */
88
+ export declare function openTailBytes(options: {
89
+ readonly tailBytes?: number;
90
+ }): number;
64
91
  export declare class CrbmReader {
65
92
  private readonly blob;
66
93
  private readonly objectSize;
@@ -102,6 +129,11 @@ export declare class CrbmReader {
102
129
  get fingerprint(): string;
103
130
  /** Total object bytes (from the one-GET tail read): the `sizeBytes` a segment's `stat()` reports. */
104
131
  get sizeBytes(): number;
132
+ /**
133
+ * The bytes of the object after its chunk payloads: any extension block, the index and the footer. A tail read of
134
+ * this many bytes covers all of them.
135
+ */
136
+ get tailBytes(): number;
105
137
  /**
106
138
  * Retained JS heap of this reader's parsed index — the weight the storage reader cache bounds on (a wide
107
139
  * segment's parsed index, not its payloads, dominates the reader's footprint): the exact byte length of the
@@ -134,6 +166,16 @@ export declare class CrbmReader {
134
166
  count(): number;
135
167
  /** The slot holding `chunkKey` (binary search over the ascending keys), or -1 when this generation has none. */
136
168
  private slotOf;
169
+ /**
170
+ * {@link slotOf} for a key at or after the one that held slot `at`, which a walk over ascending keys has: the slot
171
+ * itself or the next are tried first, so a read of nearly every chunk costs no search.
172
+ */
173
+ private slotFrom;
174
+ /**
175
+ * The extents of the chunks `chunkKeys` name that this generation holds, in key order, each once (a key given twice
176
+ * is read once). A walk, so nothing is looked up until the plan pulls it.
177
+ */
178
+ private neededExtents;
137
179
  has(chunkKey: number): boolean;
138
180
  /**
139
181
  * Range-GET one chunk's payload, verifying its CRC32C before returning. Returns `null` if the chunk is
@@ -168,10 +210,13 @@ export declare class CrbmReader {
168
210
  * as {@link getChunk} answers for an absent key. A key given twice is read once and answered at both positions with
169
211
  * the same view.
170
212
  *
171
- * The whole key list is planned once, up front, against this one generation's index. The ranges then run through a
172
- * window of `options.concurrency` reads ({@link MAX_RANGES_IN_FLIGHT} by default) that counts a range until the
173
- * consumer has taken it, so a stream never holds more than that many ranges, in flight or landed, however many keys
174
- * it was given or how slowly it is read; with `options.ramp` the window opens 1, 2, 4 … wide. A range is at most 1 MiB
213
+ * The key list is planned against this one generation's index ({@link planChunkReads}), as a walk the window pulls
214
+ * from only as it opens a range, so a stream that stops early has planned little: the same ranges, in the same
215
+ * order, as a plan of the whole list would make, and each chunk's extent is checked before the range that carries
216
+ * it is requested. The ranges run through a window of `options.concurrency` reads ({@link MAX_RANGES_IN_FLIGHT} by
217
+ * default) that counts a range until the consumer has taken it, so a stream never holds more than that many ranges,
218
+ * in flight or landed, however many keys it was given or how slowly it is read; with `options.ramp` the window opens
219
+ * 1, 2, 4 … wide. A range is at most 1 MiB
175
220
  * unless it is one chunk, which is at most the payload cap, so the bound is that many times the per-chunk cap
176
221
  * (1 MiB, and 28 bytes more for an encrypted chunk). Nothing is read until the first item is asked for, and a
177
222
  * consumer that stops launches no more: ranges already in flight finish and are dropped, and a failure of one of
@@ -223,3 +268,4 @@ export type ChunkStream = AsyncGenerator<Omit<ChunkRead, 'version'>> & {
223
268
  */
224
269
  export declare function parseExtension(sections: Uint8Array, crypto: CrbmCrypto | undefined): GenerationMetadata | undefined;
225
270
  export declare function parseIndex(indexBytes: Uint8Array, payloadEnd: number, maxPayloadBytes: number, minPayloadBytes?: number): ParsedIndex;
271
+ export {};