@cloudbitmaps/core 0.24.0 → 0.26.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.
@@ -16,9 +16,9 @@
16
16
  * different codecs, or a cached bitmap from codec A could reach codec B's in-place op. The `CloudRoaring` facade
17
17
  * mints the cache per store, so this cannot arise in normal use.)
18
18
  *
19
- * **Interface surface = exactly what the engine needs**, kept general enough for the known fast-follow codecs
20
- * (positional/rank-select access + raw-bitset interop that `@cloudbitmaps/bitset` will add live on that
21
- * package's own extended value type, not here — the engine never calls them). The one codec-linked concern
19
+ * **Interface surface = exactly what the engine needs**, and nothing a codec might one day offer: an operation the
20
+ * engine never calls (positional or rank/select access, say) is not on {@link CodecBitmap}, and a second codec that
21
+ * has such operations would carry them on its own value type. The one codec-linked concern
22
22
  * this seam intentionally leaves in the format layer is the `.crbm` **payload codec id**
23
23
  * (`PAYLOAD_CODEC_ROARING_PORTABLE` / `KNOWN_PAYLOAD_CODEC_IDS` in `crbm/format.ts`): it is stamped in the footer
24
24
  * and validated on read, as membership of a registry rather than equality with one constant, so a codec-specific
@@ -22,7 +22,7 @@
22
22
  * the microtask queue drains to empty before the event loop advances a single phase. A loop awaiting them
23
23
  * yields to other *microtasks* and to nothing else; no pending I/O callback runs. Measured: 555 ms of
24
24
  * starvation against a 568 ms unyielded baseline **on the synthetic yield-primitive benchmark below** —
25
- * a different, smaller workload than the 442 ms end-to-end load above, which is why the two baselines
25
+ * a different, smaller workload than the 518 ms end-to-end load above, which is why the two baselines
26
26
  * differ. The yield must be a **macrotask** — hence
27
27
  * `Clock.yieldNow`, backed by `setImmediate` in production wiring.
28
28
  * 2. **Yielding per unit of work costs more than the work.** The yield has to be periodic, and `everyN` has to be
@@ -405,9 +405,15 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
405
405
  * ({@link replacedPins}), and a check under way is shared by the reads that ask at once; neither "gone" nor "the
406
406
  * same" is kept, since either can change. A transient fault is rethrown, for the store's retries to see for what it
407
407
  * is. A footer that cannot be read otherwise says nothing about which object is there, so this returns, and the
408
- * caller's own error stands.
408
+ * caller's own error stands; a caller with no error of its own passes `strict`, and that fault is raised.
409
409
  */
410
410
  private throwIfReplaced;
411
+ /**
412
+ * Throws the pin's `NotFoundError` when the object under `generation`'s key is no longer the one `held` names: one
413
+ * footer read, shared with a check already under way for the same pin. It fails closed: a footer that cannot be read,
414
+ * or an object that is not a generation object of this store, raises its own fault rather than passing.
415
+ */
416
+ verifyHeld(ref: SegmentRef, generation: number, held: PinnedObject): Promise<void>;
411
417
  /** Chunk keys of a specific generation — the pinned shape read. `held` as for {@link getChunkAt}. */
412
418
  listChunkKeysAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<number[]>;
413
419
  /**
@@ -101,6 +101,14 @@ export interface CombinedChunk {
101
101
  readonly chunkKey: number;
102
102
  readonly bitmap: CodecBitmap;
103
103
  }
104
+ /** Options of {@link SegmentEngine.combineCount}: a combine's, and a check the count runs at its start and at each look. */
105
+ export interface CountOptions extends CombineOptions {
106
+ /**
107
+ * Runs before the first key and at each look, and may throw to stop the count (a lease that has ended). A count
108
+ * answered from the index fetches no chunk, so this is where such a handle is held to its terms.
109
+ */
110
+ readonly guard?: () => void;
111
+ }
104
112
  export declare class SegmentEngine {
105
113
  private readonly storage;
106
114
  private readonly cache;
@@ -397,6 +405,51 @@ export declare class SegmentEngine {
397
405
  private combineBatches;
398
406
  /** The chunk read: the plan every combine shares, then each key's result, cut to the range at its edges. */
399
407
  private combineChunks;
408
+ /**
409
+ * How many ids `segs[0] ∩ …` (`mode` 'all') or `segs[0] ∪ …` ('any') holds, minus `excludeSegs`, without streaming
410
+ * them: the size of what {@link combine} would stream. Each chunk key is taken one of two ways. A key exactly one
411
+ * include holds, that no exclude holds and the range does not cut, is answered from the index's count for it, with
412
+ * no chunk read. Every other key is decoded and counted as the combine reads it, with the same plan, the same ranges
413
+ * and the same window, over those keys alone. So an intersection reads what its stream reads, and a union or a
414
+ * difference reads no more than its stream.
415
+ *
416
+ * The keys answered from the index are trusted as {@link count} trusts the index, and checked no further than the
417
+ * reader checks it when it opens the object; a decoded key is exact. A source that reports no per-chunk counts has
418
+ * every key decoded. A union or a difference of one operand with no exclude and no range is {@link count}.
419
+ *
420
+ * A running union or difference, or an intersection with an `exclude`, looks at its operands' versions every
421
+ * `concurrency` keys, a key answered from the index being one taken, and any count looks before the first key after
422
+ * an invalidation, and runs `options.guard` at each look. On a move it plans again above the last key taken, as a
423
+ * combine does, so the bound on what it serves of an earlier generation is the combine's. A pinned operand's object
424
+ * is looked up before the first key and at each look there is (one footer read each, a fault other than a transient
425
+ * one raised as it is), so an erasure of what it pinned stops the count there with the pin's `NotFoundError`. An
426
+ * intersection of two or more operands with no exclude decodes every key and so shows a move without a version
427
+ * look; its guard still runs every `concurrency` keys.
428
+ */
429
+ combineCount(segs: readonly SegmentRef[], excludeSegs: readonly SegmentRef[], mode: 'all' | 'any', op: 'intersect' | 'union' | 'andNot', options?: Omit<CountOptions, 'exclude'>): Promise<number>;
430
+ /** Take every step of `run`: the index's figure for one, the size of the next decoded chunk for another. */
431
+ private runCount;
432
+ /** Run the guard every `limit` keys when the version looks are off (an AND of two or more includes). */
433
+ private guardAtCadence;
434
+ /**
435
+ * Look up each pinned operand's generation object, as a pinned read finds it: one footer read per pinned operand, and
436
+ * the pin's `NotFoundError` when the object is gone or is another. An operand that is not pinned costs nothing.
437
+ */
438
+ private verifyPinned;
439
+ /** Look at every operand's version, as {@link nextSlotAfterLook} does, and plan the keys above `last` again on a move. */
440
+ private countLook;
441
+ /**
442
+ * {@link combineOperands} for a count: each include also carries the per-chunk counts of the generation its keys were
443
+ * listed from. The counts are asked for after the keys, and the two must name the same keys, or the operand is listed
444
+ * again (a move between the two reads), and refused if a source keeps disagreeing with itself. A source with no
445
+ * per-chunk counts gives none, and every key of that operand is decoded.
446
+ */
447
+ private countOperands;
448
+ /**
449
+ * The per-chunk counts of `seg`, checked as untrusted tier data; `undefined` when the source reports none, and `'moved'`
450
+ * when they name other keys than `listed`, which the segment's generation changing between the two reads explains.
451
+ */
452
+ private chunkCounts;
400
453
  /**
401
454
  * The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
402
455
  * Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
@@ -115,10 +115,16 @@ export declare function prepareLoadInput(input: LoadInput, codec: CodecInterface
115
115
  * input of a load goes through, and what a flavor offers its users to decode bytes they hold themselves.
116
116
  *
117
117
  * The bytes must be a `Uint8Array` (a Node `Buffer` is one), at most {@link MAX_SERIALIZED_LOAD_BYTES}, one whole
118
- * bitmap and structurally sound. A view is read over the bytes it really holds, and bytes in a `SharedArrayBuffer`
119
- * are copied first. An empty or detached buffer is the empty bitmap.
118
+ * bitmap and structurally sound. A view is read over the bytes it really holds, once, and the size checks, the
119
+ * copy and the decode all use that one view. Bytes in a `SharedArrayBuffer` are copied first. An empty or detached
120
+ * buffer is the empty bitmap, unless `refuseEmpty` is set.
120
121
  *
121
122
  * @param what names the input in a refusal's message.
123
+ * @param options `maxBytes` lowers the size cap for this call (it cannot raise it); `refuseEmpty` refuses an empty or
124
+ * detached buffer, which is not a bitmap.
122
125
  * @throws {ValidationError} for anything that is not such bytes; nothing reaches the codec's decoder then.
123
126
  */
124
- export declare function decodeSerialized(bytes: unknown, codec: CodecInterface, what?: string): CodecBitmap;
127
+ export declare function decodeSerialized(bytes: unknown, codec: CodecInterface, what?: string, options?: {
128
+ readonly maxBytes?: number;
129
+ readonly refuseEmpty?: boolean;
130
+ }): CodecBitmap;
@@ -112,5 +112,7 @@ export declare class PinnedStorageChunkSource implements StorageChunkSource {
112
112
  */
113
113
  currentVersion(ref: SegmentRef): Promise<string | null>;
114
114
  exists(ref: SegmentRef): Promise<boolean>;
115
+ /** Whether the object this segment's pin holds is still there: see {@link HeldObjectLookup.verifyPinned}. */
116
+ verifyPinned(ref: SegmentRef): Promise<void>;
115
117
  invalidate(ref: SegmentRef): void;
116
118
  }
@@ -147,7 +147,9 @@ export interface StorageChunkSource {
147
147
  * already-parsed `.crbm` index with **no payload reads**, or `null` if the segment has no Storage generation.
148
148
  * Powers the free `count()` of a source with no {@link StorageChunkSource.summary} — the engine sums the index
149
149
  * instead of fetching a single chunk. A source with no
150
- * index (e.g. the in-memory source) omits this, and `count()` falls back to fetching every chunk.
150
+ * index (e.g. the in-memory source) omits this, and `count()` falls back to fetching every chunk. A count of a
151
+ * combine also asks for it, and takes its keys to be the ones `listChunkKeys` names for the same generation (an emptied
152
+ * chunk is named by both or by neither), listing again when they differ and refusing a source that keeps disagreeing.
151
153
  */
152
154
  cardinalities?(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
153
155
  /**
@@ -227,6 +229,19 @@ export interface StorageChunkSource {
227
229
  */
228
230
  currentVersion?(ref: SegmentRef): Promise<string | null>;
229
231
  }
232
+ /**
233
+ * What a source that reads pinned generations also offers the engine, apart from {@link StorageChunkSource}: a lookup of
234
+ * the object a pin holds. Not part of the port a source implements; the engine asks for it when it is there.
235
+ */
236
+ export interface HeldObjectLookup {
237
+ /**
238
+ * Resolves when the segment is not pinned here, or when the object its pin holds is still under its key; rejects with
239
+ * the pin's `NotFoundError` when it is gone or is another object, and with the fault itself when the footer cannot be
240
+ * read or is not a generation object's (a transient fault is for the caller's retries). One footer read for a pinned
241
+ * segment, none for any other.
242
+ */
243
+ verifyPinned(ref: SegmentRef): Promise<void>;
244
+ }
230
245
  /** Capabilities a Storage driver advertises; validated at wiring time, fail-fast. */
231
246
  export interface StorageCaps {
232
247
  /** REQUIRED — the format relies on byte-range reads. */
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  mapWithConcurrency
3
- } from "./chunk-BUDAVGBI.js";
3
+ } from "./chunk-7VNEBKC4.js";
4
4
  import {
5
5
  DEFAULT_NAMESPACE,
6
6
  applyRegistryPatch,
@@ -18,7 +18,7 @@ import {
18
18
  validateNewRegistryRecord,
19
19
  validateRegistryPatch,
20
20
  webCryptoEntropy
21
- } from "./chunk-45ML6YU4.js";
21
+ } from "./chunk-LJLZRVYN.js";
22
22
  import {
23
23
  CloudRoaringError,
24
24
  IntegrityError,
@@ -45,7 +45,7 @@ import {
45
45
  requireNonEmptyString,
46
46
  unsettledConflict,
47
47
  validateSegmentRef
48
- } from "./chunk-2B2QF4FU.js";
48
+ } from "./chunk-UFOMYONZ.js";
49
49
 
50
50
  // src/drivers/_shared/object-registry-keys.ts
51
51
  var REGISTRY_SUFFIX = ".reg";
@@ -51,6 +51,7 @@ export declare class RetryingStorageChunkSource implements StorageChunkSource {
51
51
  readonly invalidate?: (ref: SegmentRef) => void;
52
52
  readonly exists?: (ref: SegmentRef) => Promise<boolean>;
53
53
  readonly currentVersion?: (ref: SegmentRef) => Promise<string | null>;
54
+ readonly verifyPinned?: (ref: SegmentRef) => Promise<void>;
54
55
  constructor(inner: StorageChunkSource, opts: RetryingOptions);
55
56
  getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
56
57
  listChunkKeys(ref: SegmentRef): Promise<number[]>;
package/dist/index.d.ts CHANGED
@@ -19,12 +19,12 @@
19
19
  * publishable on its own and every codec reuses one driver set with zero duplication.
20
20
  */
21
21
  export { SegmentEngine } from './core/engine.js';
22
- export type { EngineDeps, CombineOptions as EngineCombineOptions, IdRange } from './core/engine.js';
22
+ export type { EngineDeps, CombineOptions as EngineCombineOptions, CountOptions as EngineCountOptions, IdRange, } from './core/engine.js';
23
23
  export { BoundedLru } from './core/lru.js';
24
24
  export { safeMetrics } from './core/metrics.js';
25
25
  export { runExport } from './export/index.js';
26
26
  export type { SegmentReader } from './export/index.js';
27
- export { splitId } from './core/bit-route.js';
27
+ export { splitId, refusalOfId } from './core/bit-route.js';
28
28
  export { mapWithConcurrency } from './core/concurrency.js';
29
29
  export { resolveBudget, resolvePerOpBudget, checkBudget, collectWithinBudget } from './core/budget.js';
30
30
  export { segmentExists, listSegments } from './core/discover.js';
@@ -43,7 +43,7 @@ export { compileCombineMany, rebindCombineMany, runCombineMany } from './core/co
43
43
  export type { CombineManyDeps, CombineManyFeed, CombineManyOperand, CombineManyOutcome, CombineManyOutput, CombineManyRequest, CombineManyRun, CombineManyWrite, CompiledCombineMany, Expr, MaterializeManyFeedRecord, MaterializeManyOperandStats, MaterializeManyOutputStats, MaterializeManyStats, } from './core/combine-many.js';
44
44
  export type { LoadDeps, LoadOptions, LoadGuard, GuardRefusal, LoadJudgement, LoadResult, LoadRefusal, } from './core/load.js';
45
45
  export type { LoadInput, PortableBitmap } from './core/load-input.js';
46
- export { decodeSerialized } from './core/load-input.js';
46
+ export { decodeSerialized, MAX_SERIALIZED_LOAD_BYTES } from './core/load-input.js';
47
47
  export { listGenerations, rollbackSegment } from './core/rollback.js';
48
48
  export type { GenerationListDeps, GenerationEntry, RollbackResult } from './core/rollback.js';
49
49
  export { eraseIdFromSegment, findIdUnderTombstone } from './core/erase-id.js';