@cloudbitmaps/core 0.9.0 → 0.11.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 (132) hide show
  1. package/NOTICE +4 -4
  2. package/README.md +33 -16
  3. package/dist/chunk-5OMC5U7U.js +508 -0
  4. package/dist/chunk-5OMC5U7U.js.map +7 -0
  5. package/dist/core/audit.d.ts +128 -0
  6. package/dist/core/bit-route.d.ts +21 -0
  7. package/dist/core/blob.d.ts +34 -0
  8. package/dist/core/budget.d.ts +68 -0
  9. package/dist/core/codec.d.ts +122 -0
  10. package/dist/core/concurrency.d.ts +15 -0
  11. package/dist/core/consistency.d.ts +73 -0
  12. package/dist/core/cooperative.d.ts +78 -0
  13. package/dist/core/cost.d.ts +254 -0
  14. package/dist/core/crbm/crc32c.d.ts +10 -0
  15. package/dist/core/crbm/format.d.ts +114 -0
  16. package/dist/core/crbm/reader.d.ts +86 -0
  17. package/dist/core/crbm/varint.d.ts +13 -0
  18. package/dist/core/crbm/writer.d.ts +39 -0
  19. package/dist/core/crbm-storage-source.d.ts +522 -0
  20. package/dist/core/crypto.d.ts +80 -0
  21. package/dist/core/determinism.d.ts +42 -0
  22. package/dist/core/discover.d.ts +77 -0
  23. package/dist/core/due-index.d.ts +53 -0
  24. package/dist/core/engine.d.ts +246 -0
  25. package/dist/core/erase-id.d.ts +176 -0
  26. package/dist/core/erasure.d.ts +185 -0
  27. package/dist/core/errors.d.ts +113 -0
  28. package/dist/core/generation-gc.d.ts +56 -0
  29. package/dist/core/keys.d.ts +23 -0
  30. package/dist/core/load.d.ts +121 -0
  31. package/dist/core/lru.d.ts +69 -0
  32. package/dist/core/metrics.d.ts +136 -0
  33. package/dist/core/name-codec.d.ts +69 -0
  34. package/dist/core/pinned-storage-source.d.ts +90 -0
  35. package/dist/core/ports.d.ts +423 -0
  36. package/dist/core/reader-defaults.d.ts +18 -0
  37. package/dist/core/registry-scan.d.ts +47 -0
  38. package/dist/core/retention-sweep.d.ts +198 -0
  39. package/dist/core/retention.d.ts +67 -0
  40. package/dist/core/retry.d.ts +69 -0
  41. package/dist/core/rollback.d.ts +75 -0
  42. package/dist/core/validate.d.ts +44 -0
  43. package/dist/driver-kit.d.ts +9 -0
  44. package/dist/driver-kit.js +275 -0
  45. package/dist/driver-kit.js.map +7 -0
  46. package/dist/drivers/_shared/keys.d.ts +26 -0
  47. package/dist/drivers/_shared/object-registry-keys.d.ts +28 -0
  48. package/dist/drivers/_shared/object-registry.d.ts +89 -0
  49. package/dist/drivers/_shared/registry.d.ts +66 -0
  50. package/dist/drivers/backends.d.ts +43 -0
  51. package/dist/drivers/crypto.d.ts +38 -0
  52. package/dist/drivers/localfs/fs-util.d.ts +21 -0
  53. package/dist/drivers/localfs/paths.d.ts +33 -0
  54. package/dist/drivers/localfs/registry.d.ts +28 -0
  55. package/dist/drivers/localfs/storage.d.ts +19 -0
  56. package/dist/drivers/memory.d.ts +49 -0
  57. package/dist/drivers/retry/retrying-chunk-source.d.ts +59 -0
  58. package/dist/export/index.d.ts +93 -0
  59. package/dist/index.d.ts +76 -2760
  60. package/dist/index.js +3527 -3133
  61. package/dist/index.js.map +7 -1
  62. package/dist/testing/fuzz-core.d.ts +14 -0
  63. package/package.json +19 -115
  64. package/dist/azure/index.cjs +0 -368
  65. package/dist/azure/index.cjs.map +0 -1
  66. package/dist/azure/index.d.cts +0 -48
  67. package/dist/azure/index.d.ts +0 -48
  68. package/dist/azure/index.js +0 -304
  69. package/dist/azure/index.js.map +0 -1
  70. package/dist/cassandra/index.cjs +0 -265
  71. package/dist/cassandra/index.cjs.map +0 -1
  72. package/dist/cassandra/index.d.cts +0 -52
  73. package/dist/cassandra/index.d.ts +0 -52
  74. package/dist/cassandra/index.js +0 -204
  75. package/dist/cassandra/index.js.map +0 -1
  76. package/dist/chunk-7LMLYSVJ.js +0 -43
  77. package/dist/chunk-7LMLYSVJ.js.map +0 -1
  78. package/dist/chunk-BD22WDAN.js +0 -230
  79. package/dist/chunk-BD22WDAN.js.map +0 -1
  80. package/dist/chunk-NUIDEEFZ.js +0 -91
  81. package/dist/chunk-NUIDEEFZ.js.map +0 -1
  82. package/dist/chunk-OZ6LAFV4.js +0 -6
  83. package/dist/chunk-OZ6LAFV4.js.map +0 -1
  84. package/dist/chunk-SNJVZ227.js +0 -35
  85. package/dist/chunk-SNJVZ227.js.map +0 -1
  86. package/dist/dynamodb/index.cjs +0 -767
  87. package/dist/dynamodb/index.cjs.map +0 -1
  88. package/dist/dynamodb/index.d.cts +0 -106
  89. package/dist/dynamodb/index.d.ts +0 -106
  90. package/dist/dynamodb/index.js +0 -469
  91. package/dist/dynamodb/index.js.map +0 -1
  92. package/dist/gcs/index.cjs +0 -343
  93. package/dist/gcs/index.cjs.map +0 -1
  94. package/dist/gcs/index.d.cts +0 -46
  95. package/dist/gcs/index.d.ts +0 -46
  96. package/dist/gcs/index.js +0 -279
  97. package/dist/gcs/index.js.map +0 -1
  98. package/dist/index.cjs +0 -4990
  99. package/dist/index.cjs.map +0 -1
  100. package/dist/index.d.cts +0 -2761
  101. package/dist/mongodb/index.cjs +0 -260
  102. package/dist/mongodb/index.cjs.map +0 -1
  103. package/dist/mongodb/index.d.cts +0 -45
  104. package/dist/mongodb/index.d.ts +0 -45
  105. package/dist/mongodb/index.js +0 -199
  106. package/dist/mongodb/index.js.map +0 -1
  107. package/dist/mysql/index.cjs +0 -281
  108. package/dist/mysql/index.cjs.map +0 -1
  109. package/dist/mysql/index.d.cts +0 -56
  110. package/dist/mysql/index.d.ts +0 -56
  111. package/dist/mysql/index.js +0 -211
  112. package/dist/mysql/index.js.map +0 -1
  113. package/dist/ports-CvcvJWf6.d.cts +0 -395
  114. package/dist/ports-CvcvJWf6.d.ts +0 -395
  115. package/dist/postgres/index.cjs +0 -273
  116. package/dist/postgres/index.cjs.map +0 -1
  117. package/dist/postgres/index.d.cts +0 -54
  118. package/dist/postgres/index.d.ts +0 -54
  119. package/dist/postgres/index.js +0 -203
  120. package/dist/postgres/index.js.map +0 -1
  121. package/dist/redis/index.cjs +0 -257
  122. package/dist/redis/index.cjs.map +0 -1
  123. package/dist/redis/index.d.cts +0 -42
  124. package/dist/redis/index.d.ts +0 -42
  125. package/dist/redis/index.js +0 -197
  126. package/dist/redis/index.js.map +0 -1
  127. package/dist/s3/index.cjs +0 -918
  128. package/dist/s3/index.cjs.map +0 -1
  129. package/dist/s3/index.d.cts +0 -112
  130. package/dist/s3/index.d.ts +0 -112
  131. package/dist/s3/index.js +0 -555
  132. package/dist/s3/index.js.map +0 -1
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Metrics seam — an injected sink the engine pushes typed events to, exactly like the `Clock`/driver seams
3
+ * keep `core/` pure and storage-agnostic.
4
+ *
5
+ * Design: the library emits **neutral domain events** and stays vendor-agnostic — you (or a ~12-line
6
+ * adapter) map them to OpenTelemetry / Datadog / a log line. The default is a **no-op**, and emission is
7
+ * skipped entirely when no sink is wired (near-zero overhead, no telemetry dependency). Events carry **raw
8
+ * observations** (bytes, counts, ms); turning those into billable units / dollars is the cost model's job,
9
+ * which keeps rates pluggable.
10
+ *
11
+ * Two caveats for sink authors: `onEvent` runs **synchronously on the I/O path**, so keep it cheap and
12
+ * non-blocking (offload batching/network to your own async queue); and `segment`/`namespace` are
13
+ * caller-controlled strings that may be PII and are **unbounded-cardinality** — do not map them to
14
+ * per-series metric labels unless they're known low-cardinality and PII-free. See the observability
15
+ * guide.
16
+ */
17
+ /** The segment operations that emit an `op` latency event (timed with the injected clock, at the facade). */
18
+ export type MetricOpName = 'has' | 'count' | 'intersectInto' | 'unionInto' | 'andNotInto';
19
+ /**
20
+ * One observability event. A discriminated union on `kind` — new variants can be added over time without
21
+ * breaking consumers (they switch on the kinds they care about). `namespace` is the segment's namespace
22
+ * (absent ⇒ the default namespace). Segment names are identifiers, never bitmap contents (threat model:
23
+ * never log PII / bitmap bits).
24
+ */
25
+ export type MetricEvent = {
26
+ readonly kind: 'storage.get';
27
+ readonly namespace?: string;
28
+ readonly segment: string;
29
+ /** Bytes returned (0 if the chunk was absent — a GET still happened). */
30
+ readonly bytes: number;
31
+ /**
32
+ * Elapsed wall time of the read — includes any transient-retry backoff on the storage call. From the
33
+ * injected clock, clamped to ≥ 0 (0 under the no-wait test clock).
34
+ */
35
+ readonly ms: number;
36
+ } | {
37
+ readonly kind: 'cache';
38
+ readonly hit: boolean;
39
+ } | {
40
+ readonly kind: 'retry';
41
+ /** Infrastructure-fault backoff (throttling, 5xx, a dropped connection) — the one kind of retry the store does. */
42
+ readonly reason: 'transient';
43
+ /** 1-based number of the attempt about to be retried. */
44
+ readonly attempt: number;
45
+ readonly delayMs: number;
46
+ } | {
47
+ readonly kind: 'intersect';
48
+ /**
49
+ * Which chunk-aligned combine this was. **Optional** — absent means `'intersect'`.
50
+ *
51
+ * For `'union'` over its *include* operands alone, `skippedChunks` is 0 by construction — union reads
52
+ * every chunk of every operand, so there is nothing to prune. It can still be non-zero when a union
53
+ * carries `exclude` operands, because a suppression list may hold keys no include does and those are
54
+ * legitimately never fetched. So: 0 is the expected shape for a plain union, not a guarantee.
55
+ */
56
+ readonly op?: 'intersect' | 'union' | 'andNot';
57
+ /** Operand count, including any `exclude` (suppression) operands. */
58
+ readonly operands: number;
59
+ /**
60
+ * Distinct chunk **keys** selected to fetch — for `'intersect'`, those present in every operand.
61
+ *
62
+ * **Keys, not requests.** The actual storage GETs are roughly `fetchedChunks × operands`, plus one per
63
+ * `exclude` that holds a given key, which is why this number is smaller than what the per-op budget
64
+ * charges for the same call. The two are different units by design; if you are reconciling a bill, the
65
+ * budget's accounting is the one that models requests.
66
+ */
67
+ readonly fetchedChunks: number;
68
+ /**
69
+ * **Distinct** chunk-keys pruned — never fetched (the chunk-skipping saving). Counts distinct keys,
70
+ * not per-operand GETs, so it under-states the true GET saving when 3+ operands partially overlap.
71
+ *
72
+ * On a read bounded by `after` / `through`, this and `fetchedChunks` count only the keys inside the range:
73
+ * the keys outside it are never considered, so they are not counted as pruned either. Both count the keys the
74
+ * call would fetch if read to the end, so a page that stops early reports every key up to `through`, or to the
75
+ * end of the segment when `through` is left out.
76
+ */
77
+ readonly skippedChunks: number;
78
+ } | {
79
+ readonly kind: 'op';
80
+ readonly name: MetricOpName;
81
+ readonly ms: number;
82
+ };
83
+ /**
84
+ * The sink you plug in. One method, synchronous, fire-and-forget. It must never throw back into the
85
+ * caller — `CloudRoaring` wraps a user sink with {@link safeMetrics} so a buggy sink can't break I/O.
86
+ */
87
+ export interface IMetricsSink {
88
+ onEvent(event: MetricEvent): void;
89
+ }
90
+ /** The default sink: discards everything, zero overhead. Used whenever no sink is wired. */
91
+ export declare const NOOP_METRICS: IMetricsSink;
92
+ /**
93
+ * Wrap a sink so a throwing/buggy `onEvent` can never break the data path — observability is strictly
94
+ * best-effort. `CloudRoaring` applies this to the user-supplied sink at construction.
95
+ */
96
+ export declare function safeMetrics(sink: IMetricsSink): IMetricsSink;
97
+ /** Accumulated totals — the shape returned by {@link CountingMetricsSink.snapshot}. */
98
+ export interface MetricsSnapshot {
99
+ readonly storage: {
100
+ readonly gets: number;
101
+ readonly bytes: number;
102
+ readonly totalMs: number;
103
+ };
104
+ readonly cache: {
105
+ readonly hits: number;
106
+ readonly misses: number;
107
+ };
108
+ readonly retries: {
109
+ readonly transient: number;
110
+ };
111
+ readonly intersect: {
112
+ readonly calls: number;
113
+ readonly fetchedChunks: number;
114
+ readonly skippedChunks: number;
115
+ };
116
+ readonly ops: Readonly<Record<MetricOpName, {
117
+ readonly count: number;
118
+ readonly totalMs: number;
119
+ }>>;
120
+ }
121
+ /**
122
+ * A ready-made sink that tallies events into a {@link MetricsSnapshot} — handy for tests, quick scripts,
123
+ * and as the grounded-request-cost source behind `costReport()`. `snapshot()` returns an independent copy;
124
+ * `reset()` zeroes the counters.
125
+ */
126
+ export declare class CountingMetricsSink implements IMetricsSink {
127
+ private storage;
128
+ private cache;
129
+ private retries;
130
+ private intersect;
131
+ private ops;
132
+ private static zeroOps;
133
+ onEvent(event: MetricEvent): void;
134
+ snapshot(): MetricsSnapshot;
135
+ reset(): void;
136
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Name → physical-key encoding. The library validates *length*, and encodes everything else.
3
+ *
4
+ * Lives in `core/` rather than beside the drivers because it is pure string logic — no I/O, no SDK, no
5
+ * `node:` builtin — and because `validate.ts` needs it to measure a name against the key budget. Core may
6
+ * not import a driver (invariant 7), and a driver may import core, so this is the only layer both can see.
7
+ *
8
+ * A name is **any non-empty string**, and each physical boundary escapes what *it* cannot take literally. An
9
+ * allowlist of characters would reject names for the storage layer's convenience, and the storage layer's
10
+ * convenience is the library's job, not the caller's. It would also let through the names that are actually
11
+ * dangerous: `con`, `nul` and `com1` are Windows device names made only of letters and digits, which pass any
12
+ * allowlist and fail only on a user's machine. A grammar can be both too narrow and too permissive at once,
13
+ * which is the argument against having one.
14
+ *
15
+ * Percent-encoding, because it is the one escape everybody already reads. `%` escapes itself as `%25` and is
16
+ * encoded **first**, which is what makes the transform injective: every `%` in an encoded string starts an
17
+ * escape, so decoding is unambiguous and two distinct names can never collide on one key.
18
+ *
19
+ * There are two alphabets, because the boundaries differ:
20
+ *
21
+ * - **Object keys** (S3, GCS, Azure Blob) take almost anything. They need `/` escaped so a name cannot
22
+ * invent hierarchy or break a key parser that splits on it, and control characters because they are not
23
+ * legal in the XML an S3 LIST returns. Every other character outside `[A-Za-z0-9._:-]` is escaped as well.
24
+ * - **Filesystem paths** need all of that plus `:` (an NTFS alternate-data-stream separator, where a write can
25
+ * *succeed* while `readdir` never lists the result), plus three hazards that are about the component as a
26
+ * whole rather than its characters — see {@link encodeNameForPath}.
27
+ *
28
+ * **The key alphabet is fixed for good.** Adding a character to it would change what `encodeNameForKey` emits
29
+ * for every name containing that character, silently moving those segments to keys nothing looks up. A test pins
30
+ * it, one printable ASCII character at a time.
31
+ *
32
+ * **A plain name encodes to itself on the OBJECT-KEY alphabet** — one made only of `[A-Za-z0-9._:-]` that
33
+ * starts with a letter or digit — so S3, GCS and Azure keys read as the names that made them. That is the
34
+ * property to preserve if this file is ever edited, and a property test asserts it over every plain name.
35
+ *
36
+ * The **path** alphabet escapes more. Besides `:`, two classes of plain name are escaped on a path: a Windows
37
+ * device-name stem (`con`, `nul`, `com1`, `con.backup`) and a trailing dot (`a.`), since both silently alias
38
+ * or fail on Windows.
39
+ */
40
+ /**
41
+ * Encode a name for use inside an **object key** (S3, GCS, Azure Blob).
42
+ *
43
+ * Leaves `[A-Za-z0-9._:-]` literal, except a leading `_` and a name that is only `.` or `..`, so a plain name
44
+ * reads as itself, and escapes everything else — including `/` (which would invent hierarchy and break a parser
45
+ * that splits on it) and control characters (not legal in S3's XML responses).
46
+ */
47
+ export declare function encodeNameForKey(name: string): string;
48
+ /** Inverse of {@link encodeNameForKey}. */
49
+ export declare function decodeNameFromKey(encoded: string): string;
50
+ /**
51
+ * Encode a name for use as a **filesystem path component**.
52
+ *
53
+ * Everything {@link encodeNameForKey} escapes, plus `:`, plus three hazards that are properties of the whole
54
+ * component rather than of any character in it. All three are silent corruption rather than errors, which is
55
+ * why they are handled here instead of being left to the OS:
56
+ *
57
+ * 1. **`.` and `..`** are traversal. `join(root, '..')` leaves the storage root.
58
+ * 2. **Windows reserved device names** — `CON`, `PRN`, `AUX`, `NUL`, `COM1`–`COM9`, `LPT1`–`LPT9`, case
59
+ * insensitive and *with or without an extension*, so `con.0.crbm` is reserved too. Opening one addresses a
60
+ * device rather than a file, so `store.segment('con')` would break only on a user's Windows box.
61
+ * 3. **A trailing `.` or space**, which Windows silently strips — so `a.` and `a` would become the same
62
+ * directory, quietly merging two segments.
63
+ *
64
+ * Each is fixed by escaping the one character that defuses it, which keeps the encoding injective: a name that
65
+ * needed defusing produces a `%XX` no other name produces.
66
+ */
67
+ export declare function encodeNameForPath(name: string): string;
68
+ /** Inverse of {@link encodeNameForPath}. */
69
+ export declare function decodeNameFromPath(encoded: string): string;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * A {@link StorageChunkSource} view that holds **one** segment at **one** generation, and passes everything else
3
+ * through to the live source.
4
+ *
5
+ * A pin exists so a long job describes a single instant. An ordinary handle re-resolves on `cache.genTtlMs`, so a
6
+ * publish part-way through an export, a send or a reconciliation means the second half of the job describes a
7
+ * different instant than the first — and nothing in the result says so.
8
+ *
9
+ * **A pin is a generation number and the object it named, not a captured reader.** The reader behind it can live
10
+ * in the shared bounded LRU and be evicted freely: re-opening at the same number reopens the same object, or, if
11
+ * the name was purged and loaded again since, finds another one, which the pin's fingerprint refuses. Holding the
12
+ * reader instead put every live pin outside the library's memory ceiling.
13
+ *
14
+ * **Other segments are not pinned, and that is the contract, not an omission.** `snap.intersect([other])` reads
15
+ * `snap` at its pinned generation and `other` at whatever is current. Pinning the whole query means pinning
16
+ * each segment. The alternative shapes are both worse: refusing the combine outright makes a pin useless to the
17
+ * export and reconciliation jobs it exists for, and silently pinning the operands would quietly change what
18
+ * `other` means to a caller who did not ask.
19
+ *
20
+ * What this must never do is the inverse — read a pinned handle's segment **live** because the call was made
21
+ * on some other handle. That is why the routing is by segment identity here rather than by which object the
22
+ * caller happened to start from.
23
+ */
24
+ import type { ChunkRef, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
25
+ import type { CrbmStorageChunkSource } from './crbm-storage-source.js';
26
+ /**
27
+ * What a pin holds for one segment: the generation, the version identifying those exact bytes, and the pinned
28
+ * object's fingerprint, so a read can tell that the generation it opens is still that object.
29
+ *
30
+ * `fingerprint` is optional so that a pin built by hand, before there was one, still compiles. Nothing checks the
31
+ * object such a pin reads, so it cannot tell a name purged and loaded again from the segment it pinned.
32
+ * `seg.pin()` always records it.
33
+ */
34
+ export interface PinnedAt {
35
+ readonly generation: number | null;
36
+ readonly version: string | null;
37
+ readonly fingerprint?: string | null;
38
+ }
39
+ export declare class PinnedStorageChunkSource implements StorageChunkSource {
40
+ private readonly inner;
41
+ /**
42
+ * The pinned segments, keyed by {@link segmentKey}. A **set** rather than one entry because a combine may
43
+ * involve several pinned handles, and each must be read at its own pin — including when the call was made
44
+ * on a different handle. Reading a pinned handle live because the caller started elsewhere is the silent
45
+ * wrong answer this exists to prevent.
46
+ */
47
+ private readonly pins;
48
+ constructor(inner: CrbmStorageChunkSource,
49
+ /**
50
+ * The pinned segments, keyed by {@link segmentKey}. A **set** rather than one entry because a combine may
51
+ * involve several pinned handles, and each must be read at its own pin — including when the call was made
52
+ * on a different handle. Reading a pinned handle live because the caller started elsewhere is the silent
53
+ * wrong answer this exists to prevent.
54
+ */
55
+ pins: ReadonlyMap<string, PinnedAt>);
56
+ /** The unpinned segments refresh as the store's own source does; a pinned one never does. */
57
+ get pointerRefreshMs(): number;
58
+ /**
59
+ * The pin for this segment, or `undefined` if it is not pinned here.
60
+ *
61
+ * A pin whose `generation` is `null` is a segment that had none when it was pinned: that handle reads empty
62
+ * for its lifetime, which is exactly what an unpinned read of it does. That is **not** the same as "this
63
+ * source cannot pin" — a source that genuinely cannot is a capability gap and fails fast at `pin()`.
64
+ */
65
+ private pinFor;
66
+ getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
67
+ listChunkKeys(ref: SegmentRef): Promise<number[]>;
68
+ sizeOf(ref: SegmentRef): Promise<SegmentSize | null>;
69
+ cardinalities(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
70
+ currentGeneration(ref: SegmentRef): Promise<number | null>;
71
+ /**
72
+ * A pinned segment reports the version captured when it was pinned, marked as a pin's, so its decoded chunks
73
+ * are cached under keys that no live read writes. A live read keys its fetches by the version it resolved when
74
+ * it began, but is served whatever generation the live source holds when each fetch lands — after a publish
75
+ * and a lapsed `cache.genTtlMs`, a reader-cache eviction, a sweep that heals the read forward, or an
76
+ * invalidation, a different one — so an entry under a live version can hold another generation's chunk.
77
+ * Invariant 3 lets that live call see it, and later live reads resolve the current version and never look the
78
+ * entry up; a pin sharing the key would be handed it, and return a read that mixed two generations. A pinned
79
+ * read fetches exactly its own generation, so the entries it fills are always that generation's.
80
+ *
81
+ * The pin still shares the store's chunk cache, and its memory ceiling; what it gives up is a hit on a chunk a
82
+ * live read of the same version cached, which costs a pin one GET per such chunk. Live reads make no call for it,
83
+ * though a pin's entries share the cache's bound with theirs, so under a small `cache.maxChunks` each can evict
84
+ * the other. The version, not the bare generation, is still what the key carries: sharing on a generation-only
85
+ * key was how a pinned read could resurrect an id that `eraseIdFromSegment` had reported physically gone.
86
+ */
87
+ currentVersion(ref: SegmentRef): Promise<string | null>;
88
+ exists(ref: SegmentRef): Promise<boolean>;
89
+ invalidate(ref: SegmentRef): void;
90
+ }