@cloudbitmaps/roaring 0.19.1 → 0.21.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.
@@ -2,17 +2,31 @@
2
2
  * The checks a stream makes of a lease: before its first pull, and each time it reads a chunk. Kept apart from the store
3
3
  * so each can be held on its own, and so a stream of another shape (a rank read, a plain iterable of ids) is wrapped by
4
4
  * the same function.
5
+ *
6
+ * ```
7
+ * caller ── next() ──> guarded stream ── next() ──> engine's stream
8
+ * │ check before the first pull
9
+ * │ check again at each chunk (or each item)
10
+ * └─ a throwing check ends the engine's stream, then rejects
11
+ * return() / throw() ─────────────────────────────> engine's stream (ended / thrown into)
12
+ * ```
13
+ *
14
+ * A guarded stream is its own iterator and single-use, as the engine's generator it wraps is: it answers `next`,
15
+ * `return` and `throw`, and iterating it gives it back.
5
16
  */
6
- import type { CodecBitmap } from '@cloudbitmaps/core';
17
+ /** An async iterator that is also its own iterable, with `return` and `throw` always present, as a generator's are. */
18
+ interface GuardedStream<T> extends AsyncIterableIterator<T> {
19
+ return(value?: unknown): Promise<IteratorResult<T, unknown>>;
20
+ throw(err: unknown): Promise<IteratorResult<T, unknown>>;
21
+ }
7
22
  /** What an id stream is, as far as a guard is concerned: ids, and the same ids a chunk at a time. */
8
23
  interface IdStream extends AsyncIterable<number> {
9
24
  batches(): AsyncIterable<Uint32Array>;
10
25
  }
11
- /** A combine's chunks, as an `*Into` writes them. */
12
- type ChunkStream = AsyncIterable<{
13
- chunkKey: number;
14
- bitmap: CodecBitmap;
15
- }>;
26
+ /** An id stream that is guarded: the per-id stream is an iterator of its own, and `batches()` is guarded too. */
27
+ type GuardedIds = GuardedStream<number> & {
28
+ batches(): GuardedStream<Uint32Array>;
29
+ };
16
30
  /**
17
31
  * A stream of ascending ids that checks a lease before it first pulls, and each time it reads a chunk after that: once
18
32
  * the engine has the next chunk and before an id of it is yielded, so nothing of a chunk fetched past the lease is
@@ -21,14 +35,12 @@ type ChunkStream = AsyncIterable<{
21
35
  * engine's reads. Used only when a handle in the call holds a lease: a call with none returns the engine's stream
22
36
  * untouched. It takes any async iterable of ids, so a stream with no `.batches()` (a rank read) is wrapped the same way.
23
37
  */
24
- export declare const guardIdIterable: (inner: AsyncIterable<number>, check: () => void) => AsyncIterable<number>;
38
+ export declare const guardIdIterable: (inner: AsyncIterable<number>, check: () => void) => GuardedStream<number>;
25
39
  /** {@link guardIdIterable} for an `IdStream`, whose `.batches()` is guarded too. */
26
- export declare const guardIds: (inner: IdStream, check: () => void) => IdStream;
40
+ export declare const guardIds: (inner: IdStream, check: () => void) => GuardedIds;
27
41
  /**
28
- * A stream of chunks (a batch of ids, or a chunk an `*Into` writes) that checks a lease before the first pull and before
29
- * each item is handed on: an item is one chunk.
42
+ * A stream of chunks that checks a lease before the first pull and before each item is handed on: an item is one
43
+ * chunk (a batch of ids, or a chunk an `*Into` writes).
30
44
  */
31
- export declare const guardItems: <T>(inner: AsyncIterable<T>, check: () => void) => AsyncIterable<T>;
32
- /** A chunk stream (what an `*Into` writes) that checks a lease before the first pull and before each chunk. */
33
- export declare const guardChunks: (inner: ChunkStream, check: () => void) => ChunkStream;
45
+ export declare const guardItems: <T>(inner: AsyncIterable<T>, check: () => void) => GuardedStream<T>;
34
46
  export {};
@@ -10,10 +10,12 @@
10
10
  *
11
11
  * Two opens are never charged, because the segment's unit already pays for them: the generation the row named when
12
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.
13
+ * which it reads back to verify. A refusal here comes from a read. Most of an erasure's reads that can be refused come
14
+ * before its deletes; two kinds come after one: the look for a holder left once the call has deleted, and the zero-byte
15
+ * read of a generation the call deleted that the row then names. A refusal of either is thrown in place of the call's
16
+ * answer, so the call reports the budget, not what it deleted.
15
17
  */
16
- import type { GenKey, IStorageDriver, SegmentRef } from '@cloudbitmaps/core';
18
+ import type { GenKey, IStorageDriver, SegmentRef, StorageDeleteOptions } from '@cloudbitmaps/core';
17
19
  export declare class OpenChargingStorage implements IStorageDriver {
18
20
  private readonly inner;
19
21
  private readonly charge;
@@ -29,6 +31,7 @@ export declare class OpenChargingStorage implements IStorageDriver {
29
31
  getRange(key: GenKey, offset: number, length: number): Promise<Uint8Array>;
30
32
  /** One `getTail` is one open: a reader takes the footer and the index from it, and reads chunks with `getRange`. */
31
33
  getTail(key: GenKey, maxBytes: number): ReturnType<IStorageDriver['getTail']>;
32
- delete(key: GenKey): Promise<void>;
34
+ /** Handed through whole: the erasure's delete of a holder is conditioned on the object it searched. */
35
+ delete(key: GenKey, options?: StorageDeleteOptions): Promise<void>;
33
36
  list(ref: SegmentRef): AsyncIterable<GenKey>;
34
37
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The shape checks every call that takes an options bag makes before it reads or writes anything: the bag is an
3
+ * object, it names only keys the call takes, and each switch is a boolean. The per-call key tables stay with the
4
+ * facade, because each is held to the options type it describes.
5
+ */
6
+ import { ValidationError } from '@cloudbitmaps/core';
7
+ /** A key of `T` that `K` does not list: a key table missing one fails to compile. */
8
+ type Unlisted<T, K extends readonly PropertyKey[]> = Exclude<keyof T, K[number]>;
9
+ /** The keys of `T`, every one of them, as a table the options check below reads. */
10
+ export declare const keysOf: <T>() => <const K extends readonly (keyof T & string)[]>(keys: K & ([Unlisted<T, K>] extends [never] ? unknown : never)) => readonly string[];
11
+ /** How a value of the wrong kind is named in a message: by its kind, never by its content. */
12
+ export declare const kindOf: (v: unknown) => string;
13
+ /**
14
+ * Why `options` cannot be a call's options, or `undefined` when it can. `undefined` and `null` read as none; anything
15
+ * but an object is refused, and so is a key `known` does not list, unless its value is `undefined`, so a spread of
16
+ * options keeps working; and each of `booleans` present must be a boolean, `null` included. A bag of the wrong shape, a misspelt key or
17
+ * a switch given as a string would otherwise read as no option at all, and silently widen what the call does: a scope
18
+ * passed as a string swept every namespace, and `dryRun: 'true'` deleted.
19
+ */
20
+ export declare function optionsError(options: unknown, op: string, known: readonly string[], booleans?: readonly string[]): ValidationError | undefined;
21
+ /**
22
+ * The keys of `options` a call takes, each read by name. A spread copies only own enumerable properties, so an option
23
+ * held in a getter or inherited from a prototype, which TypeScript accepts and {@link optionsError} reads, would pass
24
+ * the checks and then be dropped: a `dryRun` so held ran a real sweep, and a `namespace` exported every tenant. The
25
+ * call then reads the copy only, so it runs with exactly what was checked.
26
+ */
27
+ export declare function picked<T extends object>(options: T | null | undefined, keys: readonly string[]): Partial<T>;
28
+ /** {@link optionsError}, thrown: for a call that rejects, rather than one that returns a stream. */
29
+ export declare function checkOptions(options: unknown, op: string, known: readonly string[], booleans?: readonly string[]): void;
30
+ export {};
@@ -4,7 +4,7 @@
4
4
  * Construction checks every container once, in {@link parsePortableLayout}, which is the same check the native
5
5
  * path runs; after that a membership test reads one container rather than materializing the set.
6
6
  */
7
- export declare class PortableRoaringReader {
7
+ declare class PortableRoaringReader {
8
8
  private readonly view;
9
9
  private readonly containers;
10
10
  private constructor();
@@ -51,3 +51,4 @@ export declare class PortableRoaringReader {
51
51
  }
52
52
  /** Convenience wrapper over {@link PortableRoaringReader.decode}. */
53
53
  export declare function decodePortableRoaring(bytes: Uint8Array): PortableRoaringReader;
54
+ export {};
@@ -4,8 +4,8 @@ export declare const ARRAY = 0;
4
4
  /** A container of 65,536 bits. */
5
5
  export declare const BITMAP = 1;
6
6
  /** A container of `(start, length)` runs. */
7
- export declare const RUN = 2;
8
- export type ContainerKind = typeof ARRAY | typeof BITMAP | typeof RUN;
7
+ declare const RUN = 2;
8
+ type ContainerKind = typeof ARRAY | typeof BITMAP | typeof RUN;
9
9
  export interface PortableContainer {
10
10
  /** High 16 bits of every value in this container. */
11
11
  readonly key: number;
@@ -49,3 +49,4 @@ export declare function checkPortableLayout(bytes: Uint8Array): number;
49
49
  * @throws {IntegrityError} at the first container that runs past the buffer or does not hold what its header says.
50
50
  */
51
51
  export declare function containerPayloads(bytes: Uint8Array): Generator<EncodedChunk>;
52
+ export {};
@@ -15,13 +15,13 @@ export declare function storageChunkSourceConformance(label: string, makeSource:
15
15
  bitmap: SafeBitmap;
16
16
  }>) => Promise<StorageChunkSource>): void;
17
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' | 'delete of an absent key beside its neighbours' | 'list read-after-delete' | 'names and namespaces kept apart' | 'generations by number' | 'failed write' | 'missing location' | 'list across pages';
18
+ export type StorageDriverCase = 'write-once round trip' | 'collision' | 'collision on a large upload' | 'missing object' | 'out-of-range read' | 'tail size' | 'idempotent delete' | 'delete of an absent key beside its neighbours' | 'list read-after-delete' | 'names and namespaces kept apart' | 'generations by number' | 'failed write' | 'missing location' | 'list across pages' | 'conditional delete';
19
19
  /**
20
20
  * Contract tests for an {@link IStorageDriver} — the contract its doc comment lists: write-once with
21
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` (of an absent key too, with neighbours present), and a `list`
23
- * that is read-after-delete. `makeDriver` MUST
24
- * return a driver over an empty, isolated keyspace on each call.
22
+ * read, `getTail`'s true total size, idempotent `delete` (of an absent key too, with neighbours present) after which a
23
+ * read of the key is `NotFoundError` at once, a `list` that is read-after-delete, and a `delete` given `ifVersion` that does what `conditionalDelete` says. `makeDriver`
24
+ * MUST return a driver over an empty, isolated keyspace on each call.
25
25
  *
26
26
  * `skip` names the cases a backend cannot exercise (a local emulator that does not model a contract), so each
27
27
  * skip is visible at the call site, with the reason beside it, rather than hidden in a weakened assertion.
@@ -47,6 +47,13 @@ export declare function storageDriverConformance(label: string, makeDriver: () =
47
47
  * driver that drops its continuation token fails. Omitted, the case does not run.
48
48
  */
49
49
  readonly pagedListSize?: number;
50
+ /**
51
+ * What the driver under test reports as `capabilities().conditionalDelete` once it has made a delete given
52
+ * `ifVersion`, which is when a driver that resolves it lazily has resolved it. Stated at the call site, so a driver
53
+ * that stops applying the condition fails here rather than being exempted by its own report. Omitted, the case
54
+ * holds the driver to whatever it reports.
55
+ */
56
+ readonly conditionalDelete?: boolean;
50
57
  }): void;
51
58
  /**
52
59
  * Contract tests for an {@link IRegistryDriver}. `makeDriver` MUST return a fresh, empty, isolated driver on
@@ -92,8 +99,8 @@ export interface RegistryDeleteHarness {
92
99
  /** Whether the backend holds anything for `ref`: a live row, or the tombstone a delete left in its place. */
93
100
  stored(ref: SegmentRef): Promise<boolean>;
94
101
  /**
95
- * Put `text` at `ref`'s row, as another writer left it: here, a row as a release before 0.12 wrote it. Omitted by
96
- * a registry that persists nothing a test can plant (the in-memory one).
102
+ * Put `text` at `ref`'s row, as another writer left it: here, a row stamped with an earlier schema. Omitted by a
103
+ * registry that persists nothing a test can plant (the in-memory one).
97
104
  */
98
105
  plantRow?(ref: SegmentRef, text: string): Promise<void>;
99
106
  }
@@ -101,10 +108,8 @@ export interface RegistryDeleteHarness {
101
108
  * Contract tests for what `delete` leaves behind, which differs by driver and is declared by
102
109
  * `capabilities().conditionalDelete`:
103
110
  *
104
- * - **`true`**: a delete removes a row whose token carries an incarnation id from the backend for good, and does so
105
- * only while the row is still the version it read. A row a release before 0.12 wrote has a bare decimal token and
106
- * is tombstoned, never removed: a process still on that release, re-creating the name over nothing, would issue
107
- * those same counters again from 0.
111
+ * - **`true`**: a delete removes the row from the backend for good, and does so only while the row is still the
112
+ * version it read.
108
113
  * - **`false` or absent**: every delete leaves a tombstone, which a later full listing still reads.
109
114
  *
110
115
  * Either way `get` answers `null` afterwards, `list` skips the row, and a name created again is a new incarnation that
@@ -0,0 +1,38 @@
1
+ import type { Clock, IMetricsSink, IKeystore, IRegistryDriver, IStorageDriver, RetryPolicy, RetryingOptions, Rng, StorageChunkSource } from '@cloudbitmaps/core';
2
+ /** The slice of a store's options the wiring reads; `CloudRoaringOptions` satisfies it. */
3
+ interface StoreWiring {
4
+ readonly storage: unknown;
5
+ readonly encryption?: {
6
+ readonly keystore?: IKeystore;
7
+ readonly required?: boolean;
8
+ };
9
+ readonly cache?: {
10
+ readonly genTtlMs?: number;
11
+ readonly readerMax?: number;
12
+ readonly readerMaxBytes?: number;
13
+ };
14
+ readonly retry?: (Partial<RetryPolicy> & {
15
+ readonly onRetry?: (info: {
16
+ attempt: number;
17
+ delayMs: number;
18
+ err: unknown;
19
+ }) => void;
20
+ }) | false;
21
+ }
22
+ /** What {@link wireStore} builds for the constructor. */
23
+ interface WiredStore {
24
+ /** What the engine reads through: the source, wrapped in the read retry unless it is off. */
25
+ readonly storage: StorageChunkSource;
26
+ /** The resolved source before the retry wrapper. */
27
+ readonly source: StorageChunkSource;
28
+ readonly driver: IStorageDriver | undefined;
29
+ readonly registry: IRegistryDriver | undefined;
30
+ /** The read retry the store shares with its lifecycle reads; `undefined` when retry is off. */
31
+ readonly retryOptions: RetryingOptions | undefined;
32
+ }
33
+ /**
34
+ * Resolve the storage seam, hand a backend the metrics sink, and wrap the source in the read retry. Refusals come in
35
+ * that order: a bad `storage` first, then a bad `retry`.
36
+ */
37
+ export declare function wireStore(options: StoreWiring, clock: Clock, rng: Rng, metrics: IMetricsSink): WiredStore;
38
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbitmaps/roaring",
3
- "version": "0.19.1",
3
+ "version": "0.21.0",
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",
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "roaring": "2.7.0",
68
- "@cloudbitmaps/core": "^0.19.1"
68
+ "@cloudbitmaps/core": "^0.21.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^22.0.0",