@cloudbitmaps/roaring 0.10.0 → 0.11.1

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.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * A storage driver that charges every **generation open** to a per-op budget, for `eraseSubject`.
3
+ *
4
+ * The budget counts fan-out in units of one backend read. `eraseSubject`'s scan charges one unit per registered
5
+ * segment, but a segment whose current generation lacks the id is searched anyway: every other generation in its
6
+ * bucket is opened, one tail read each, and with the keep-everything default of the `*Into` verbs that is one per
7
+ * generation the segment ever had. Those opens are the work the scan's own count leaves out. They are counted here,
8
+ * at the storage port where they happen, so an open added to the erasure later is charged without anyone
9
+ * remembering to charge it, and no option of the public erasure call had to grow.
10
+ *
11
+ * Two opens are never charged, because the segment's unit already pays for them: the generation the row named when
12
+ * the scan listed it, which every erasure must read to decide membership, and an object this call wrote itself,
13
+ * which it reads back to verify. A refusal here comes from a read, and in an erasure the reads that can be refused
14
+ * come before the deletes.
15
+ */
16
+ import type { GenKey, IStorageDriver, SegmentRef } from '@cloudbitmaps/core';
17
+ export declare class OpenChargingStorage implements IStorageDriver {
18
+ private readonly inner;
19
+ private readonly charge;
20
+ private readonly free;
21
+ /**
22
+ * @param inner the driver the erasure would otherwise use
23
+ * @param current the generation the segment's row named when it was listed, or `null` when it named none
24
+ * @param charge called once per charged open; it throws to refuse
25
+ */
26
+ constructor(inner: IStorageDriver, current: number | null, charge: () => void);
27
+ capabilities(): ReturnType<IStorageDriver['capabilities']>;
28
+ putImmutable(key: GenKey, write: Parameters<IStorageDriver['putImmutable']>[1]): ReturnType<IStorageDriver['putImmutable']>;
29
+ getRange(key: GenKey, offset: number, length: number): Promise<Uint8Array>;
30
+ /** One `getTail` is one open: a reader takes the footer and the index from it, and reads chunks with `getRange`. */
31
+ getTail(key: GenKey, maxBytes: number): ReturnType<IStorageDriver['getTail']>;
32
+ delete(key: GenKey): Promise<void>;
33
+ list(ref: SegmentRef): AsyncIterable<GenKey>;
34
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The keys `new CloudRoaring(options)` takes: at the top level, and inside each group whose value is an object. The
3
+ * constructor refuses any other key by name, because an option it ignored would do nothing and look as if it had,
4
+ * and the gates that read the docs' samples and the harnesses' option bags hold them to the same table. A test
5
+ * holds the table to the option interfaces, in both directions.
6
+ *
7
+ * NOT part of the public API. It is its own module, rather than an `export` from the barrel, so the constructor
8
+ * and those gates read one copy.
9
+ */
10
+ export declare const OPTION_KEYS: {
11
+ readonly top: readonly ["storage", "cache", "encryption", "retry", "metrics", "budget", "seams"];
12
+ readonly cache: readonly ["maxChunks", "ttlMs", "genTtlMs", "readerMax", "readerMaxBytes"];
13
+ readonly encryption: readonly ["keystore", "required"];
14
+ readonly retry: readonly ["maxAttempts", "baseDelayMs", "maxDelayMs", "backoffFactor", "jitter", "onRetry"];
15
+ readonly budget: readonly ["maxRequests"];
16
+ readonly seams: readonly ["clock", "rng"];
17
+ };
18
+ /** The groups: the top-level keys whose value, when it is an object, has keys of its own. */
19
+ export type OptionGroup = Exclude<keyof typeof OPTION_KEYS, 'top'>;
@@ -1,25 +1,24 @@
1
1
  /**
2
2
  * A decoded portable-roaring bitmap that can be queried but not modified.
3
3
  *
4
- * Construction parses only the headers — container payloads are read on demand, so a membership test against a
5
- * large bitmap touches one container rather than materializing the set.
4
+ * Construction checks every container once, in {@link parsePortableLayout}, which is the same check the native
5
+ * path runs; after that a membership test reads one container rather than materializing the set.
6
6
  */
7
7
  export declare class PortableRoaringReader {
8
- private readonly bytes;
9
8
  private readonly view;
10
9
  private readonly containers;
11
10
  private constructor();
12
11
  /**
13
- * Parse the headers of a portable-format bitmap.
12
+ * Parse and check a portable-format bitmap.
14
13
  *
15
- * @throws {IntegrityError} if the cookie is unrecognized, or any header field runs past the buffer.
14
+ * @throws {IntegrityError} if the bytes are not a well-formed portable bitmap: see {@link parsePortableLayout}.
16
15
  */
17
16
  static decode(bytes: Uint8Array): PortableRoaringReader;
18
17
  /**
19
18
  * Total cardinality.
20
19
  *
21
- * Served entirely from the descriptive header — no container payload is read, so this is O(containers) and
22
- * independent of how many values the bitmap holds.
20
+ * Served entirely from the descriptive header, which {@link parsePortableLayout} has checked against every
21
+ * container's payload, so this is O(containers) and independent of how many values the bitmap holds.
23
22
  */
24
23
  count(): number;
25
24
  /** Is `value` (a `u32`) in the set? */
@@ -0,0 +1,32 @@
1
+ /** A container of sorted `u16` values. */
2
+ export declare const ARRAY = 0;
3
+ /** A container of 65,536 bits. */
4
+ export declare const BITMAP = 1;
5
+ /** A container of `(start, length)` runs. */
6
+ export declare const RUN = 2;
7
+ export type ContainerKind = typeof ARRAY | typeof BITMAP | typeof RUN;
8
+ export interface PortableContainer {
9
+ /** High 16 bits of every value in this container. */
10
+ readonly key: number;
11
+ /** Number of values. Stored as `cardinality - 1` on the wire, normalized here. */
12
+ readonly cardinality: number;
13
+ readonly kind: ContainerKind;
14
+ /** Absolute byte offset of this container's payload within the source buffer; a run container's run count. */
15
+ readonly offset: number;
16
+ }
17
+ /**
18
+ * Parse the portable-format bitmap in `bytes`, and check its structure.
19
+ *
20
+ * An empty buffer is the empty bitmap, as it is to the native addon, which returns one without reading a byte.
21
+ *
22
+ * @throws {IntegrityError} if the cookie is unrecognized, any field runs past the buffer, or the structure is not
23
+ * one a writer produces (see the module header for the list).
24
+ */
25
+ export declare function parsePortableLayout(bytes: Uint8Array): PortableContainer[];
26
+ /**
27
+ * {@link parsePortableLayout}'s check alone, for a caller that hands the bytes to another decoder afterwards and
28
+ * so has no use for the layout: it allocates nothing per container.
29
+ *
30
+ * @throws {IntegrityError} exactly where {@link parsePortableLayout} does.
31
+ */
32
+ export declare function checkPortableLayout(bytes: Uint8Array): void;
@@ -0,0 +1,3 @@
1
+ export declare const RESERVED_NAMESPACE_PREFIX = "cbm.due.";
2
+ /** Throw `ValidationError` for a namespace in the reserved prefix; `undefined` and every look-alike pass. */
3
+ export declare function refuseReservedNamespace(namespace: string | undefined): void;
@@ -5,9 +5,14 @@ export declare class SafeBitmap implements CodecBitmap {
5
5
  static empty(): SafeBitmap;
6
6
  static fromValues(values: Iterable<number>): SafeBitmap;
7
7
  /**
8
- * Validate size, then deserialize with the **portable** (validated) format.
9
- * Throws `IntegrityError` if the input exceeds `maxBytes` or fails to decode — the
10
- * native addon is never handed unbounded or unsafe-format input.
8
+ * Cap the size, check the structure, then deserialize with the **portable** format.
9
+ * Throws `IntegrityError` if the input exceeds `maxBytes`, is not a well-formed portable bitmap, or fails to
10
+ * decode — the native addon is never handed unbounded, malformed or unsafe-format input.
11
+ *
12
+ * The structural check is what makes the decoded bitmap's answers mean anything. The native deserializer
13
+ * bounds its reads and checks nothing else, so bytes of the right length and the wrong shape (containers or
14
+ * values out of order, overlapping runs, a cardinality that disagrees with the bits) decode into a bitmap
15
+ * whose `maximum()`, `has()` and `size` are wrong, and some of those shapes crash the process when used.
11
16
  */
12
17
  static safeDeserialize(bytes: Uint8Array, maxBytes: number): SafeBitmap;
13
18
  serialize(): Uint8Array;
@@ -31,8 +36,10 @@ export declare class SafeBitmap implements CodecBitmap {
31
36
  */
32
37
  optimize(): void;
33
38
  /**
34
- * Largest value, or `undefined` when empty. O(1) — roaring reads it off the container index, so the engine's
35
- * per-chunk range assertion costs one call per chunk rather than a walk per id.
39
+ * Largest value, or `undefined` when empty. O(1) — roaring reads it off the last container, so the engine's
40
+ * per-chunk range assertion costs one call per chunk rather than a walk per id. The last container holds the
41
+ * largest value only because {@link SafeBitmap.safeDeserialize} refuses bytes whose containers or values are
42
+ * out of order.
36
43
  */
37
44
  maximum(): number | undefined;
38
45
  add(value: number): void;
@@ -56,6 +63,6 @@ export declare class SafeBitmap implements CodecBitmap {
56
63
  /**
57
64
  * The roaring {@link CodecInterface} — the flagship codec, delegating to {@link SafeBitmap}'s statics. This is
58
65
  * the default the `CloudRoaring` facade injects into the engine; `core/` itself never hard-references it once a
59
- * caller supplies a codec. Moves to `@cloudbitmaps/roaring` when the package split lands.
66
+ * caller supplies a codec.
60
67
  */
61
68
  export declare const roaringCodec: CodecInterface;
@@ -4,9 +4,8 @@
4
4
  * It lives outside `core/`, so `Date.now()`, `setTimeout` and `setImmediate` are allowed here; that separation is
5
5
  * the whole point of the seam, and it is lint-enforced on the other side.
6
6
  *
7
- * It sits in its own module rather than in `index.ts` because two places need it — the facade, and
8
- * `codec-bound.ts`, which pre-binds it into `bulkLoadCrbmGeneration`. Importing it from `index.ts` would put a
9
- * cycle between the barrel and a module the barrel re-exports.
7
+ * It sits in its own module rather than in `index.ts` so the facade, the tests and anything else that needs a real
8
+ * clock can import it without importing the whole barrel.
10
9
  */
11
10
  import type { Clock } from '@cloudbitmaps/core';
12
11
  /** Default real-time clock — lives outside `core/`, so `Date.now()` + timers are allowed here. */
@@ -19,7 +18,7 @@ export declare class SystemClock implements Clock {
19
18
  * A long CPU-bound loop in `core/` (bulk-load is the one that matters) periodically hands the loop back so a
20
19
  * co-resident HTTP server keeps answering. Measured on the **synthetic yield-primitive benchmark** — a
21
20
  * 61,035-iteration loop yielding every 1,024, isolating the primitive rather than timing a real load (the
22
- * end-to-end figures are 450 ms → 19 ms; see `cooperative.ts`, which lists all three experiments):
21
+ * end-to-end figures are 519 ms → 24 ms; see `cooperative.ts`, which lists all three experiments):
23
22
  *
24
23
  * ```text
25
24
  * no yield 568 ms wall 568.0 ms worst event-loop gap
@@ -1,5 +1,5 @@
1
1
  import { SafeBitmap } from '../roaring-codec.js';
2
- import type { StorageChunkSource, IRegistryDriver } from '@cloudbitmaps/core';
2
+ import type { StorageChunkSource, IRegistryDriver, IStorageDriver } from '@cloudbitmaps/core';
3
3
  /**
4
4
  * The segment every conformance seeder must write. Exported so a driver's own test file derives the name
5
5
  * instead of restating it — a second copy that drifts makes the suite query a segment nobody seeded, which
@@ -14,6 +14,26 @@ export declare function storageChunkSourceConformance(label: string, makeSource:
14
14
  chunkKey: number;
15
15
  bitmap: SafeBitmap;
16
16
  }>) => Promise<StorageChunkSource>): void;
17
+ /** The storage-driver cases {@link storageDriverConformance} runs; an emulator that cannot model one skips it by name. */
18
+ export type StorageDriverCase = 'write-once round trip' | 'collision' | 'collision on a large upload' | 'missing object' | 'out-of-range read' | 'tail size' | 'idempotent delete' | 'list read-after-delete';
19
+ /**
20
+ * Contract tests for an {@link IStorageDriver} — the contract its doc comment lists: write-once with
21
+ * `WriteConflictError` on a collision, `NotFoundError` for a missing object, `ValidationError` for an out-of-range
22
+ * read, `getTail`'s true total size, idempotent `delete`, and a `list` that is read-after-delete. `makeDriver` MUST
23
+ * return a driver over an empty, isolated keyspace on each call.
24
+ *
25
+ * `skip` names the cases a backend cannot exercise (a local emulator that does not model a contract), so each
26
+ * skip is visible at the call site, with the reason beside it, rather than hidden in a weakened assertion.
27
+ */
28
+ export declare function storageDriverConformance(label: string, makeDriver: () => IStorageDriver, options?: {
29
+ readonly skip?: readonly StorageDriverCase[];
30
+ /**
31
+ * Size of the object the `collision on a large upload` case writes. Pass one above the driver's single-request
32
+ * upload threshold to exercise its multipart or resumable path, where the write-once precondition is
33
+ * sent on a different request than a small object's. Omitted, the case does not run.
34
+ */
35
+ readonly largeBytes?: number;
36
+ }): void;
17
37
  /**
18
38
  * Contract tests for an {@link IRegistryDriver}. `makeDriver` MUST return a fresh, empty, isolated driver on
19
39
  * each call. The OCC contract (create / token-fenced CAS / ABA) plus the registry's record semantics (forward
@@ -1,9 +1,23 @@
1
1
  /**
2
2
  * Internal surface for the coverage-guided fuzz harness, CODEC half.
3
3
  *
4
- * Only the concrete bitmap codec lives here — the core half (`CrbmReader`, the non-public `parseIndex`,
5
- * `BufferReader`, `CloudRoaringError`, `DEFAULT_MAX_PAYLOAD_BYTES`) is built from `@cloudbitmaps/core`'s own
6
- * `src/testing/fuzz-core.ts` to `fuzz/build/fuzz-core.js`. Splitting it this way keeps the published entries
7
- * of both packages free of test-only exports: neither widened its public API to feed the fuzzer.
4
+ * The concrete bitmap codec, and the one property the harness holds every decode it accepts to. The core half
5
+ * (`CrbmReader`, the non-public `parseIndex`, `BufferReader`, `CloudRoaringError`, `DEFAULT_MAX_PAYLOAD_BYTES`) is
6
+ * built from `@cloudbitmaps/core`'s own `src/testing/fuzz-core.ts` to `fuzz/build/fuzz-core.js`. Splitting it this
7
+ * way keeps the published entries of both packages free of test-only exports: neither widened its public API to
8
+ * feed the fuzzer.
8
9
  */
9
- export { SafeBitmap } from '../roaring-codec.js';
10
+ import { SafeBitmap } from '../roaring-codec.js';
11
+ export { SafeBitmap };
12
+ /**
13
+ * What an accepted decode must be: it iterates strictly ascending, `size` is the number of values it iterates, and
14
+ * `has` confirms what it iterates. Throws a plain `Error` otherwise, which the fuzz targets and the corpus replay
15
+ * report as a finding.
16
+ *
17
+ * Memory safety alone does not see a structurally broken bitmap. Bytes with containers or values out of order, or
18
+ * a cardinality that disagrees with the bits, decode into memory that behaves until an operation trusts one of the
19
+ * broken invariants, so a contract of "no crash" passes them. This fails them. It is bounded like the
20
+ * materialization it replaces, because a valid run-heavy bitmap is tiny on disk and can hold billions of ids, and it
21
+ * asks `has` of the first 4,096 values only, which is enough to reach every container a small input can hold.
22
+ */
23
+ export declare function assertConsistentDecode(bm: SafeBitmap): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbitmaps/roaring",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Distributed, cloud-native Roaring Bitmaps — query and intersect billion-scale integer sets straight out of object storage, from a bounded RAM cache over immutable objects in your own bucket, at a fraction of an always-on cache.",
5
5
  "keywords": [
6
6
  "roaring",
@@ -24,7 +24,6 @@
24
24
  "segmentation",
25
25
  "audience",
26
26
  "cloud",
27
- "cloud-roaring",
28
27
  "cloudbitmaps"
29
28
  ],
30
29
  "license": "Apache-2.0",
@@ -66,7 +65,7 @@
66
65
  },
67
66
  "dependencies": {
68
67
  "roaring": "2.7.0",
69
- "@cloudbitmaps/core": "^0.10.0"
68
+ "@cloudbitmaps/core": "^0.11.1"
70
69
  },
71
70
  "devDependencies": {
72
71
  "@types/node": "^22.0.0",
@@ -1,38 +0,0 @@
1
- /**
2
- * Codec-bound wrappers over the public `@cloudbitmaps/core` entry points that need a bitmap codec.
3
- *
4
- * `core` is codec-agnostic and therefore cannot default the codec (the concrete codec lives *here*, in a package
5
- * that depends on core — a default over there would invert the dependency arrow). But an application using
6
- * `@cloudbitmaps/roaring` should never have to pass one. So this module re-binds each such entry point with
7
- * {@link roaringCodec} and the facade re-exports these **explicitly**, which shadows the same names coming from
8
- * `export * from '@cloudbitmaps/core'` (an explicit export always wins over a star export). Net effect: every
9
- * call signature is unchanged from before the family split — you can still call
10
- * `bulkLoadCrbmGeneration(driver, key, ids)` with no options at all.
11
- *
12
- * A caller who *wants* a different codec passes it explicitly; the binding only fills an absent one — via
13
- * `?? roaringCodec` rather than spread order, so an explicit `codec: undefined` still gets the binding
14
- * (`exactOptionalPropertyTypes` is off, so that call typechecks and must not fall through to the throw).
15
- */
16
- import { bulkLoadCrbmGeneration as coreBulkLoad, eraseIdFromSegment as coreEraseIdFromSegment, loadSegment as coreLoadSegment, runExport as coreRunExport } from '@cloudbitmaps/core';
17
- type BulkLoad = typeof coreBulkLoad;
18
- type LoadSegment = typeof coreLoadSegment;
19
- type EraseIdFromSegment = typeof coreEraseIdFromSegment;
20
- type RunExport = typeof coreRunExport;
21
- /**
22
- * {@link coreBulkLoad} with the roaring codec **and a real clock** pre-bound.
23
- *
24
- * The clock is what makes a large load **cooperative**: bulk-load yields the event loop periodically instead of
25
- * stalling the process for its whole duration (measured end-to-end at 450 ms of unbroken blocking for a 1M-id
26
- * load, now ~19 ms worst case). `core/` cannot
27
- * default it — it is timer-free by lint, which is precisely why waiting goes through the `Clock` seam — so the
28
- * flavor package supplies it, exactly as it supplies the codec. A caller who passes their own clock (a virtual
29
- * one in a simulation, say) keeps it.
30
- */
31
- export declare const bulkLoadCrbmGeneration: BulkLoad;
32
- /** {@link coreEraseIdFromSegment} with the roaring codec and a real (cooperative) clock pre-bound. */
33
- export declare const eraseIdFromSegment: EraseIdFromSegment;
34
- /** {@link coreRunExport} with the roaring codec pre-bound (only the `'roaring'` format needs it). */
35
- export declare const runExport: RunExport;
36
- /** {@link coreLoadSegment} with the roaring codec and a real clock pre-bound, for the same reasons as above. */
37
- export declare const loadSegment: LoadSegment;
38
- export {};
@@ -1,14 +0,0 @@
1
- /**
2
- * The 0.9.x option spellings this release no longer accepts, and what to tell someone who passes one.
3
- *
4
- * NOT part of the public API. It lives in its own module — rather than as an `export` from the barrel — so
5
- * that the gates which must agree with it (the doc-fence scan, the harness scan, and the test that diffs this
6
- * table against `CloudRoaringOptions` at the previous tag) can all read ONE copy, without the migration
7
- * table becoming a permanent public export in the same release whose point was to shrink that surface.
8
- *
9
- * Retyping it was tried and is what this replaces: the hand-maintained copies had drifted to naming three
10
- * spellings no release ever shipped while omitting the five that actually went away with the live tier.
11
- */
12
- export type MovedOptionKind = 'group' | 'renamed' | 'gone';
13
- /** `[the 0.9.x key, what to do instead, which kind of answer that is]`. */
14
- export declare const MOVED_OPTIONS: ReadonlyArray<readonly [string, string, MovedOptionKind]>;