@cloudbitmaps/roaring 0.11.2 → 0.12.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.
- package/PRIVACY.md +48 -13
- package/README.md +6 -4
- package/dist/bin/export-segments.js +237 -60
- package/dist/bin/export-segments.js.map +3 -3
- package/dist/index.d.ts +143 -33
- package/dist/index.js +237 -60
- package/dist/index.js.map +3 -3
- package/dist/portable/layout.d.ts +20 -1
- package/dist/roaring-codec.d.ts +28 -3
- package/dist/testing/conformance.d.ts +35 -3
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -112,11 +112,47 @@ function overrun(bytes, offset, length, what) {
|
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
function checkPortableLayout(bytes) {
|
|
115
|
-
walk(bytes, null);
|
|
115
|
+
return walk(bytes, null);
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
var SINGLE_HEADER_BYTES = 16;
|
|
118
|
+
var SINGLE_RUN_HEADER_BYTES = 9;
|
|
119
|
+
function* containerPayloads(bytes) {
|
|
120
|
+
if (bytes.byteLength === 0) return;
|
|
121
|
+
const { view, count, runFlagsAt, descriptiveAt, bodiesAt } = header(bytes);
|
|
122
|
+
let pos = bodiesAt;
|
|
123
|
+
let previousKey = -1;
|
|
124
|
+
for (let i = 0; i < count; i++) {
|
|
125
|
+
const key = view.getUint16(descriptiveAt + i * 4, true);
|
|
126
|
+
if (key <= previousKey) {
|
|
127
|
+
throw new IntegrityError(
|
|
128
|
+
`portable roaring: container keys are not strictly ascending (${previousKey} then ${key} at index ${i})`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
previousKey = key;
|
|
132
|
+
const cardinality = view.getUint16(descriptiveAt + i * 4 + 2, true) + 1;
|
|
133
|
+
const isRun = runFlagsAt >= 0 && (bytes[runFlagsAt + (i >>> 3)] & 1 << (i & 7)) !== 0;
|
|
134
|
+
const kind = isRun ? RUN : cardinality > ARRAY_MAX_CARDINALITY ? BITMAP : ARRAY;
|
|
135
|
+
const size = checkContainer(bytes, view, pos, kind, cardinality, i);
|
|
136
|
+
const headerBytes = isRun ? SINGLE_RUN_HEADER_BYTES : SINGLE_HEADER_BYTES;
|
|
137
|
+
const payload = new Uint8Array(headerBytes + size);
|
|
138
|
+
const out = new DataView(payload.buffer);
|
|
139
|
+
if (isRun) {
|
|
140
|
+
out.setUint32(0, SERIAL_COOKIE, true);
|
|
141
|
+
payload[4] = 1;
|
|
142
|
+
out.setUint16(7, cardinality - 1, true);
|
|
143
|
+
} else {
|
|
144
|
+
out.setUint32(0, SERIAL_COOKIE_NO_RUNCONTAINER, true);
|
|
145
|
+
out.setUint32(4, 1, true);
|
|
146
|
+
out.setUint16(10, cardinality - 1, true);
|
|
147
|
+
out.setUint32(12, SINGLE_HEADER_BYTES, true);
|
|
148
|
+
}
|
|
149
|
+
payload.set(bytes.subarray(pos, pos + size), headerBytes);
|
|
150
|
+
pos += size;
|
|
151
|
+
yield { chunkKey: key, payload, cardinality };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function header(bytes) {
|
|
118
155
|
const length = bytes.byteLength;
|
|
119
|
-
if (length === 0) return;
|
|
120
156
|
const view = new DataView(bytes.buffer, bytes.byteOffset, length);
|
|
121
157
|
if (length < 4) throw overrun(bytes, 0, 4, "the cookie");
|
|
122
158
|
const cookie = view.getUint32(0, true);
|
|
@@ -149,6 +185,12 @@ function walk(bytes, out) {
|
|
|
149
185
|
offsetsAt = pos;
|
|
150
186
|
pos += count * 4;
|
|
151
187
|
}
|
|
188
|
+
return { view, count, runFlagsAt, descriptiveAt, offsetsAt, bodiesAt: pos };
|
|
189
|
+
}
|
|
190
|
+
function walk(bytes, out) {
|
|
191
|
+
if (bytes.byteLength === 0) return 0;
|
|
192
|
+
const { view, count, runFlagsAt, descriptiveAt, offsetsAt, bodiesAt } = header(bytes);
|
|
193
|
+
let pos = bodiesAt;
|
|
152
194
|
let previousKey = -1;
|
|
153
195
|
for (let i = 0; i < count; i++) {
|
|
154
196
|
const key = view.getUint16(descriptiveAt + i * 4, true);
|
|
@@ -169,35 +211,43 @@ function walk(bytes, out) {
|
|
|
169
211
|
);
|
|
170
212
|
}
|
|
171
213
|
}
|
|
172
|
-
|
|
173
|
-
if (kind === ARRAY) {
|
|
174
|
-
size = cardinality * 2;
|
|
175
|
-
if (pos + size > length) throw overrun(bytes, pos, size, `container ${i} (array)`);
|
|
176
|
-
checkArray(view, pos, cardinality, i);
|
|
177
|
-
} else if (kind === BITMAP) {
|
|
178
|
-
size = BITMAP_CONTAINER_BYTES;
|
|
179
|
-
if (pos + size > length) throw overrun(bytes, pos, size, `container ${i} (bitmap)`);
|
|
180
|
-
const bits = popcount(view, pos);
|
|
181
|
-
if (bits !== cardinality) {
|
|
182
|
-
throw new IntegrityError(
|
|
183
|
-
`portable roaring: container ${i} (bitmap) holds ${bits} values, but its header says ${cardinality}`
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
} else {
|
|
187
|
-
if (pos + 2 > length) throw overrun(bytes, pos, 2, `container ${i} (run count)`);
|
|
188
|
-
const runs = view.getUint16(pos, true);
|
|
189
|
-
size = 2 + runs * 4;
|
|
190
|
-
if (pos + size > length) throw overrun(bytes, pos, size, `container ${i} (run)`);
|
|
191
|
-
const covered = checkRuns(view, pos + 2, runs, i);
|
|
192
|
-
if (covered !== cardinality) {
|
|
193
|
-
throw new IntegrityError(
|
|
194
|
-
`portable roaring: container ${i} (run) covers ${covered} values, but its header says ${cardinality}`
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
214
|
+
const size = checkContainer(bytes, view, pos, kind, cardinality, i);
|
|
198
215
|
out?.push({ key, cardinality, kind, offset: pos });
|
|
199
216
|
pos += size;
|
|
200
217
|
}
|
|
218
|
+
return pos;
|
|
219
|
+
}
|
|
220
|
+
function checkContainer(bytes, view, pos, kind, cardinality, i) {
|
|
221
|
+
const length = bytes.byteLength;
|
|
222
|
+
if (kind === ARRAY) {
|
|
223
|
+
const size2 = cardinality * 2;
|
|
224
|
+
if (pos + size2 > length) throw overrun(bytes, pos, size2, `container ${i} (array)`);
|
|
225
|
+
checkArray(view, pos, cardinality, i);
|
|
226
|
+
return size2;
|
|
227
|
+
}
|
|
228
|
+
if (kind === BITMAP) {
|
|
229
|
+
if (pos + BITMAP_CONTAINER_BYTES > length) {
|
|
230
|
+
throw overrun(bytes, pos, BITMAP_CONTAINER_BYTES, `container ${i} (bitmap)`);
|
|
231
|
+
}
|
|
232
|
+
const bits = popcount(view, pos);
|
|
233
|
+
if (bits !== cardinality) {
|
|
234
|
+
throw new IntegrityError(
|
|
235
|
+
`portable roaring: container ${i} (bitmap) holds ${bits} values, but its header says ${cardinality}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return BITMAP_CONTAINER_BYTES;
|
|
239
|
+
}
|
|
240
|
+
if (pos + 2 > length) throw overrun(bytes, pos, 2, `container ${i} (run count)`);
|
|
241
|
+
const runs = view.getUint16(pos, true);
|
|
242
|
+
const size = 2 + runs * 4;
|
|
243
|
+
if (pos + size > length) throw overrun(bytes, pos, size, `container ${i} (run)`);
|
|
244
|
+
const covered = checkRuns(view, pos + 2, runs, i);
|
|
245
|
+
if (covered !== cardinality) {
|
|
246
|
+
throw new IntegrityError(
|
|
247
|
+
`portable roaring: container ${i} (run) covers ${covered} values, but its header says ${cardinality}`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return size;
|
|
201
251
|
}
|
|
202
252
|
function checkArray(view, at, cardinality, index) {
|
|
203
253
|
let previous = -1;
|
|
@@ -266,18 +316,25 @@ var SafeBitmap = class _SafeBitmap {
|
|
|
266
316
|
/**
|
|
267
317
|
* Cap the size, check the structure, then deserialize with the **portable** format.
|
|
268
318
|
* Throws `IntegrityError` if the input exceeds `maxBytes`, is not a well-formed portable bitmap, or fails to
|
|
269
|
-
* decode — the native addon is never handed unbounded, malformed or unsafe-format input.
|
|
319
|
+
* decode — the native addon is never handed unbounded, malformed or unsafe-format input. With `whole`, bytes
|
|
320
|
+
* after the bitmap's last container are refused too; without it they are ignored, as the native decoder ignores
|
|
321
|
+
* them.
|
|
270
322
|
*
|
|
271
323
|
* The structural check is what makes the decoded bitmap's answers mean anything. The native deserializer
|
|
272
324
|
* bounds its reads and checks nothing else, so bytes of the right length and the wrong shape (containers or
|
|
273
325
|
* values out of order, overlapping runs, a cardinality that disagrees with the bits) decode into a bitmap
|
|
274
326
|
* whose `maximum()`, `has()` and `size` are wrong, and some of those shapes crash the process when used.
|
|
275
327
|
*/
|
|
276
|
-
static safeDeserialize(bytes, maxBytes) {
|
|
328
|
+
static safeDeserialize(bytes, maxBytes, options = {}) {
|
|
277
329
|
if (bytes.length > maxBytes) {
|
|
278
330
|
throw new IntegrityError2(`serialized bitmap is ${bytes.length}B, exceeds cap ${maxBytes}B`);
|
|
279
331
|
}
|
|
280
|
-
checkPortableLayout(bytes);
|
|
332
|
+
const end = checkPortableLayout(bytes);
|
|
333
|
+
if (options.whole === true && end !== bytes.byteLength) {
|
|
334
|
+
throw new IntegrityError2(
|
|
335
|
+
`portable roaring: ${bytes.byteLength - end} bytes follow the bitmap's last container, which ends at byte ${end}. One buffer holds one bitmap: to load several as one segment, combine them first (RoaringBitmap32.orMany) and serialize the result.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
281
338
|
try {
|
|
282
339
|
return new _SafeBitmap(RoaringBitmap32.deserialize(bytes, DeserializationFormat.portable));
|
|
283
340
|
} catch (err) {
|
|
@@ -304,10 +361,29 @@ var SafeBitmap = class _SafeBitmap {
|
|
|
304
361
|
*
|
|
305
362
|
* Called only when writing an immutable objects generation — see {@link CodecBitmap.optimize} for why the read
|
|
306
363
|
* path deliberately does not.
|
|
364
|
+
*
|
|
365
|
+
* CANONICAL, which `runOptimize()` alone is not. Where a container's run and array encodings are the same size
|
|
366
|
+
* (cardinality = 2 × runs + 1: three values in one run, five in two), CRoaring keeps whichever kind the
|
|
367
|
+
* container already has. A chunk built from ids starts as an array and stays one; the same set reached through
|
|
368
|
+
* `addRange`, an earlier `runOptimize()` or an operation over run containers is a run, and serializes to other
|
|
369
|
+
* bytes. Undoing run compression first starts every container where one built from ids starts, so the bytes
|
|
370
|
+
* depend on membership alone. On a bitmap built from ids there is nothing to undo.
|
|
307
371
|
*/
|
|
308
372
|
optimize() {
|
|
373
|
+
this.bitmap.removeRunCompression();
|
|
309
374
|
this.bitmap.runOptimize();
|
|
310
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* This set as storage chunks, cut from its own containers: {@link optimize} and one `serialize` when called, then
|
|
378
|
+
* a lazy header walk that copies one container out per step (`containerPayloads`). Per container and per byte,
|
|
379
|
+
* never per id, and the payloads are the ones {@link SafeBitmap.fromValues} of each chunk's low 16 bits would
|
|
380
|
+
* serialize to. The walk trusts the bytes it cuts because the native serializer wrote them, from a bitmap that
|
|
381
|
+
* passed the structural check when it was decoded.
|
|
382
|
+
*/
|
|
383
|
+
encodeChunks() {
|
|
384
|
+
this.optimize();
|
|
385
|
+
return containerPayloads(this.serialize());
|
|
386
|
+
}
|
|
311
387
|
/**
|
|
312
388
|
* Largest value, or `undefined` when empty. O(1) — roaring reads it off the last container, so the engine's
|
|
313
389
|
* per-chunk range assertion costs one call per chunk rather than a walk per id. The last container holds the
|
|
@@ -364,10 +440,13 @@ var SafeBitmap = class _SafeBitmap {
|
|
|
364
440
|
return this.bitmap.toArray();
|
|
365
441
|
}
|
|
366
442
|
};
|
|
443
|
+
function bitmapAsLoadInput(input) {
|
|
444
|
+
return input instanceof RoaringBitmap32 ? { bitmap: input } : input;
|
|
445
|
+
}
|
|
367
446
|
var roaringCodec = {
|
|
368
447
|
empty: () => SafeBitmap.empty(),
|
|
369
448
|
fromValues: (values) => SafeBitmap.fromValues(values),
|
|
370
|
-
safeDeserialize: (bytes, maxBytes) => SafeBitmap.safeDeserialize(bytes, maxBytes)
|
|
449
|
+
safeDeserialize: (bytes, maxBytes, options) => SafeBitmap.safeDeserialize(bytes, maxBytes, options)
|
|
371
450
|
};
|
|
372
451
|
|
|
373
452
|
// src/system-clock.ts
|
|
@@ -581,6 +660,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
581
660
|
retryOptions;
|
|
582
661
|
crbmSource;
|
|
583
662
|
clock;
|
|
663
|
+
rng;
|
|
584
664
|
metrics;
|
|
585
665
|
// The store's own drivers, kept so the lifecycle helpers and the `*Into` verbs reuse them instead of making
|
|
586
666
|
// you re-pass deps. `storageDriver` is set only when `storage` was a raw IStorageDriver (a pre-built StorageChunkSource has
|
|
@@ -686,6 +766,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
686
766
|
this.cache = cache;
|
|
687
767
|
this.crbmSource = resolved.source instanceof CrbmStorageChunkSource ? resolved.source : void 0;
|
|
688
768
|
this.clock = clock;
|
|
769
|
+
this.rng = rng;
|
|
689
770
|
this.metrics = metrics;
|
|
690
771
|
this.storageDriver = resolved.driver;
|
|
691
772
|
this.registry = resolved.registry;
|
|
@@ -713,10 +794,12 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
713
794
|
storage: this.storageDriver,
|
|
714
795
|
registry: this.registry,
|
|
715
796
|
clock: this.clock,
|
|
797
|
+
rng: this.rng,
|
|
716
798
|
codec: roaringCodec,
|
|
717
799
|
// facade injects the flagship codec
|
|
718
800
|
keystore: this.keystore,
|
|
719
|
-
requireEncryption: this.requireEncryption
|
|
801
|
+
requireEncryption: this.requireEncryption,
|
|
802
|
+
readRetry: this.retryOptions
|
|
720
803
|
};
|
|
721
804
|
}
|
|
722
805
|
/** Get a handle to a segment. Validates the name (non-empty, well-formed, within the encoded-length cap). */
|
|
@@ -746,7 +829,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
746
829
|
/**
|
|
747
830
|
* Write `ids` as a **new generation of `dest`** and publish it — the shared body of the `*Into` verbs. An `*Into`
|
|
748
831
|
* materialisation is a load, and publishes the same way (hard invariant 1): every load that finds a row fences its
|
|
749
|
-
* publish on the row's token; a guarded load (the default, since the empty refusal
|
|
832
|
+
* publish on the row's token; a guarded load (the default, since the empty refusal needs the size of the current generation)
|
|
750
833
|
* also fences on the pointer it judged (`expectFrom`), and one that found no row fences on that absence instead.
|
|
751
834
|
* Only an unguarded load (`allowEmpty: true` and no `guard.minRetained`) onto a segment with no row publishes bare
|
|
752
835
|
* forward-only. The destination's previous generation stays readable until the publish lands (readers re-resolve
|
|
@@ -784,12 +867,14 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
784
867
|
* **And by default it collects nothing**, unlike `load()`. See the `keep` default below.
|
|
785
868
|
*/
|
|
786
869
|
async materialize(dest, ids, op, options) {
|
|
787
|
-
const deps = this.lifecycleDeps(op);
|
|
870
|
+
const deps = { ...this.lifecycleDeps(op), collectByListing: true };
|
|
871
|
+
const metadata = options?.metadata;
|
|
788
872
|
let result;
|
|
789
873
|
try {
|
|
790
874
|
result = await loadSegment(dest, ids, deps, {
|
|
791
875
|
...options?.allowEmpty === void 0 ? {} : { allowEmpty: options.allowEmpty },
|
|
792
876
|
...options?.guard === void 0 ? {} : { guard: options.guard },
|
|
877
|
+
...metadata === void 0 ? {} : { metadata },
|
|
793
878
|
// COLLECT NOTHING by default, which `loadSegment` does not — it keeps a grace window of 1 and deletes
|
|
794
879
|
// the rest. By default a materialisation collects nothing: the guide states "**It deletes nothing**,
|
|
795
880
|
// unlike `load()`. The destination's previous generations stay in the bucket until you collect them", and
|
|
@@ -805,9 +890,9 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
805
890
|
}
|
|
806
891
|
const reason = result.reason;
|
|
807
892
|
if (reason === "superseded") {
|
|
808
|
-
const wrote = result.size > 0 ? `generation ${result.generation} was written and
|
|
893
|
+
const wrote = result.size > 0 ? `generation ${result.generation} was written and is not current` : `nothing was written \u2014 another writer took generation ${result.generation} first`;
|
|
809
894
|
throw new WriteConflictError(
|
|
810
|
-
`${op}: the destination "${dest.segment}" changed while this materialisation was in flight, so
|
|
895
|
+
`${op}: the destination "${dest.segment}" changed while this materialisation was in flight, so its result is not the destination's current generation: ${wrote}. The pointer may have moved, the row may have been rewritten (a retention policy does this), dropped or purged. Re-read the destination and re-run.`
|
|
811
896
|
);
|
|
812
897
|
}
|
|
813
898
|
return {
|
|
@@ -1013,7 +1098,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1013
1098
|
* for a `destroyed` tombstone, because a read answers empty in both cases.
|
|
1014
1099
|
*
|
|
1015
1100
|
* Two states answer `true` where a read still gives you nothing: a torn restore (a live pointer whose object
|
|
1016
|
-
* was deleted) makes reads *throw* rather than answer empty
|
|
1101
|
+
* was deleted) makes reads of the object *throw* rather than answer empty, while a cold `count()` answers the number
|
|
1102
|
+
* its row records — `checkConsistency` is the call for that — and
|
|
1017
1103
|
* a handle carrying an expired `expiresAt` reads empty by a rule that lives on the handle, not the row.
|
|
1018
1104
|
*
|
|
1019
1105
|
* Not a lock: the answer can change the moment it returns. If it has to hold, use the fence built for that —
|
|
@@ -1075,13 +1161,20 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1075
1161
|
* one cannot.
|
|
1076
1162
|
*
|
|
1077
1163
|
* It refuses rather than guesses: a generation not in the bucket (collected, or never written) throws
|
|
1078
|
-
* `NotFoundError` naming what *is* available,
|
|
1079
|
-
* {@link ValidationError} because every generation of it is unreadable
|
|
1080
|
-
*
|
|
1164
|
+
* `NotFoundError` naming what *is* available, a crypto-shredded segment throws
|
|
1165
|
+
* {@link ValidationError} because every generation of it is unreadable, and a target that is cleartext under an
|
|
1166
|
+
* encrypted segment, or encrypted under a cleartext one, throws {@link IntegrityError} from one tail read of it,
|
|
1167
|
+
* because every read would refuse it. Rolling to the generation already current is a no-op that reports itself.
|
|
1168
|
+
*
|
|
1169
|
+
* The same tail read (and a range read when the target's index is longer than it) gives the target's id count and
|
|
1170
|
+
* metadata, which the rollback writes into the row with the pointer, so a reader that sees the target as current sees
|
|
1171
|
+
* what describes it. The store's keystore opens an encrypted target for this; without it, or when it cannot open the
|
|
1172
|
+
* key for any reason, the segment still rolls back and the row carries no summary of the target.
|
|
1081
1173
|
*
|
|
1082
1174
|
* It deletes nothing. The generations above the new pointer stay put — which is what makes this reversible —
|
|
1083
|
-
* and are then *above* `currentGen`, where collection never looks. They remain until
|
|
1084
|
-
*
|
|
1175
|
+
* and are then *above* `currentGen`, where collection never looks. They remain until loads pass them (the first
|
|
1176
|
+
* load whose number one of them holds numbers above them all, and collection then keeps the newest `keep` of what is
|
|
1177
|
+
* below its pointer), {@link CloudRoaring.dropSegment}
|
|
1085
1178
|
* deletes them, or {@link CloudRoaring.eraseSubject} does: all of those present when it rewrites, and only those holding
|
|
1086
1179
|
* the id when the current generation does not. An operator who has just undone a bad load should not have the
|
|
1087
1180
|
* evidence collected out from under them, while a rollback target that still holds erased data would make the
|
|
@@ -1103,7 +1196,14 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1103
1196
|
}
|
|
1104
1197
|
}
|
|
1105
1198
|
/**
|
|
1106
|
-
* **Replace this segment's contents** with `
|
|
1199
|
+
* **Replace this segment's contents** with `input`, as one new immutable generation, and make it current.
|
|
1200
|
+
*
|
|
1201
|
+
* `input` is ids (any sync or async iterable of integers in `[0, 2^32)`), or a whole bitmap: `{ bitmap }`, anything
|
|
1202
|
+
* with `serialize('portable')` such as `roaring`'s `RoaringBitmap32`, or `{ serialized }`, portable Roaring bytes.
|
|
1203
|
+
* A bitmap is checked (size cap, structure, safe deserializer) before the first request, written from its own
|
|
1204
|
+
* containers with no per-id work, and gives the generation byte for byte the one its ids would. A bare
|
|
1205
|
+
* `RoaringBitmap32` passed as ids loads as `{ bitmap }`. A `Uint8Array` or `Buffer` passed as ids is refused with
|
|
1206
|
+
* `ValidationError`, since each byte would load as an id: pass bytes as `{ serialized }`.
|
|
1107
1207
|
*
|
|
1108
1208
|
* The whole write path in one call: take the next generation number, write the object, check the result is
|
|
1109
1209
|
* plausible, move the pointer, collect what the move superseded. Composed by hand those are four functions and
|
|
@@ -1117,6 +1217,14 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1117
1217
|
* if (!r.published) console.warn(`load refused: ${r.reason}`);
|
|
1118
1218
|
* ```
|
|
1119
1219
|
*
|
|
1220
|
+
* `metadata` attaches a small record of your own to the generation: a flat object of string keys and string or finite
|
|
1221
|
+
* number values, at most 1,024 bytes as canonical JSON and no key over 128 bytes. A record that breaks a rule throws
|
|
1222
|
+
* `ValidationError` before the load makes a request. It is copied when you call, stored in the generation's object,
|
|
1223
|
+
* and written to the segment's row with the generation's id count by the write that moves the pointer, so a reader
|
|
1224
|
+
* that sees the generation as current sees its metadata. It never changes after that, a rollback puts the target's own
|
|
1225
|
+
* back, and an erasure carries it over without scanning it, so keep a subject's id out of it. It is sealed under the
|
|
1226
|
+
* segment's key on an encrypted segment.
|
|
1227
|
+
*
|
|
1120
1228
|
* **A load REPLACES.** Whatever the stream contains is what the segment contains afterwards, so an upstream
|
|
1121
1229
|
* query that returns fewer rows than usual is a shrink nobody asked for and an empty one is a wipe — both
|
|
1122
1230
|
* reported as a successful write, because at the storage layer they are one. That is what `guard` and the
|
|
@@ -1140,20 +1248,39 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1140
1248
|
* so it leaves the orphan rather than risk deleting live data. The orphan is an ordinary generation once a later
|
|
1141
1249
|
* one is current above it, and collection counts it within `keep`.
|
|
1142
1250
|
*
|
|
1251
|
+
* **Collection is by name for the default `keep`.** With `keep` of 0 or 1, a load that found nothing above the
|
|
1252
|
+
* pointer deletes the one generation its publish pushed out of the window and lists nothing; it lists the segment's
|
|
1253
|
+
* objects on every sixteenth generation, and on any load that met an object above the pointer or whose check found
|
|
1254
|
+
* the current generation's object gone, to take what the name-only passes leave, such as the generations an
|
|
1255
|
+
* earlier, wider `keep` held. `keep` of 2 or more lists on every load. {@link LoadResult.collected} then names what
|
|
1256
|
+
* the pass deleted by name, and that generation may have been gone already.
|
|
1257
|
+
*
|
|
1143
1258
|
* What it **throws** for is a fault rather than an outcome: invalid options or ids, and a crypto-shredded
|
|
1144
1259
|
* segment (`ValidationError`); a key the keystore cannot provide (`KeyUnavailableError`); a current generation
|
|
1145
|
-
* that will not open when a guard
|
|
1146
|
-
*
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1260
|
+
* that will not open when a guard has to read its size, which it does only when the row has no usable summary of it
|
|
1261
|
+
* (`IntegrityError`); a driver failure; and a collection pass
|
|
1262
|
+
* by listing that could not prove the segment was still the same one (`WriteConflictError`). That one, and a
|
|
1263
|
+
* failure in the collection's own reads or deletes, can be raised **after** the publish already landed, so a throw
|
|
1264
|
+
* does not by itself mean the load did not take effect — re-read the pointer rather than assuming. A collection by
|
|
1265
|
+
* name that finds the segment changed returns an empty `collected` instead.
|
|
1266
|
+
*
|
|
1267
|
+
* **A `TransientError` from the registry write can leave the publish unsettled, and deletes nothing.** The
|
|
1268
|
+
* generation's object is sent again after a throttle where the backend allows it (a write id tells a first send
|
|
1269
|
+
* that landed from another writer's object), and a driver sends each registry write once. When that write ends
|
|
1270
|
+
* without an answer, the load reads the row: its own landed write is `published: true`, a row that has moved on is
|
|
1271
|
+
* `superseded`, and a row still as the write found it gets a fresh compare-and-swap from the version just read, at
|
|
1272
|
+
* most three times, after a wait on the store's clock. Still unanswered, the load throws the registry's `TransientError` and keeps its object, which
|
|
1273
|
+
* a write may still point the row at. Re-run the load: it numbers past that object, and collection removes it once a
|
|
1274
|
+
* generation above it is current. A `'superseded'` refusal that follows an unanswered write sets `unanswered: true`
|
|
1275
|
+
* on its audit event: that write may have landed, and the generation been current for a while, first.
|
|
1149
1276
|
*
|
|
1150
1277
|
* Needs a backend (throws {@link UnsupportedError} otherwise).
|
|
1151
1278
|
*/
|
|
1152
|
-
async load(ref,
|
|
1279
|
+
async load(ref, input, options = {}) {
|
|
1153
1280
|
validateSegmentRef(ref);
|
|
1154
1281
|
const deps = this.lifecycleDeps("load");
|
|
1155
1282
|
try {
|
|
1156
|
-
return await loadSegment(ref,
|
|
1283
|
+
return await loadSegment(ref, bitmapAsLoadInput(input), deps, options);
|
|
1157
1284
|
} finally {
|
|
1158
1285
|
this.engine.invalidate(ref);
|
|
1159
1286
|
}
|
|
@@ -1183,9 +1310,12 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1183
1310
|
* forever and quietly. Branch on `dropped`, and treat `reason: 'absent'` as the alert.
|
|
1184
1311
|
*
|
|
1185
1312
|
* **Inspect `generationsRemaining`.** Empty is the normal outcome; non-empty means the storage was NOT fully
|
|
1186
|
-
* reclaimed and the drop should be re-run. A load that
|
|
1187
|
-
*
|
|
1188
|
-
* could not remove rather than returning a result that looks like a
|
|
1313
|
+
* reclaimed and the drop should be re-run. A load that had read the segment before the tombstone landed, whether it
|
|
1314
|
+
* was writing or still consuming its ids, can still write its object, so a single sweep can miss it — this call
|
|
1315
|
+
* re-sweeps and then reports whatever it still could not remove rather than returning a result that looks like a
|
|
1316
|
+
* clean drop. A load that writes after the last sweep deletes its own object once its publish is refused; only one
|
|
1317
|
+
* whose process stops in between, or whose publish fails without a definite answer (a lost response, a timeout),
|
|
1318
|
+
* leaves it, for a re-run of the drop.
|
|
1189
1319
|
*
|
|
1190
1320
|
* Reads become empty within `cache.genTtlMs` (default 2 s), not instantly: a store that had already read this
|
|
1191
1321
|
* segment may answer from its cached generation + cached chunks until that window lapses, or, while the registry
|
|
@@ -1293,6 +1423,12 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1293
1423
|
* wall-clock knob too, and `retired` counts deletions only — a dry run reports `wouldRetire` instead, so a
|
|
1294
1424
|
* dashboard summing `retired` can never show a phantom deletion.
|
|
1295
1425
|
*
|
|
1426
|
+
* **Check `purgeFaults`.** A delete the registry refuses for a reason other than a lost race (a policy that denies
|
|
1427
|
+
* delete, an Azure blob with a snapshot, a raw provider error), whether of a tombstone or of a due-index pointer, is
|
|
1428
|
+
* counted there, with the first one's reason in `firstPurgeFault`. A refused purge is `skipped` in the ledger, is not
|
|
1429
|
+
* charged to `limit`, so it never holds the retirements behind it, and purging stops for the rest of the call after
|
|
1430
|
+
* three refused purges in a row (a purge that succeeds starts the count again).
|
|
1431
|
+
*
|
|
1296
1432
|
* It also **deletes the tombstone rows its own past retirements left**, after `tombstoneGraceMs` (default 24 h)
|
|
1297
1433
|
* and only once that segment's Storage generations are provably gone — collecting a straggler generation itself
|
|
1298
1434
|
* first, since nothing else ever would for a tombstoned segment. Attribution is a **positive marker the sweep
|
|
@@ -1300,7 +1436,12 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1300
1436
|
* `retention` untouched, so setting a policy and then honouring a right-to-erasure request mid-window produces
|
|
1301
1437
|
* exactly that row, and deleting it would destroy the Art. 17 attestation and un-fence the name. By default a
|
|
1302
1438
|
* segment that held nothing has its row deleted in the pass that retires it, since that row would only fence the name.
|
|
1303
|
-
* Pass `purgeTombstones: false` to keep every tombstone, that row included.
|
|
1439
|
+
* Pass `purgeTombstones: false` to keep every tombstone, that row included. On a backend whose registry reports
|
|
1440
|
+
* `conditionalDelete` (AWS S3, Azure Blob, the local filesystem and memory, by default; GCS when you set `conditionalDelete: true`)
|
|
1441
|
+
* the purge removes the row from the bucket for good, by a delete the store applies only to the version it judged,
|
|
1442
|
+
* so a full sweep reads what is live or inside its grace rather than every name a namespace ever held; elsewhere,
|
|
1443
|
+
* and for a row a release before 0.12 wrote, it leaves a tombstone. Each retirement files a pointer in the due index
|
|
1444
|
+
* under the day its tombstone's grace ends, so `scan: 'index'` purges as well as retires.
|
|
1304
1445
|
*
|
|
1305
1446
|
* Needs the store built with a **backend** (throws {@link UnsupportedError} otherwise),
|
|
1306
1447
|
* because retiring a segment deletes its storage objects. `now` defaults to the store's clock.
|
|
@@ -1471,6 +1612,13 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1471
1612
|
* (throws {@link UnsupportedError} otherwise). `destroyed` (crypto-shredded) segments are skipped. Pair it with
|
|
1472
1613
|
* the DR runbook (docs/guide/disaster-recovery.md).
|
|
1473
1614
|
*
|
|
1615
|
+
* A segment whose pointer names a missing object is the torn restore this reports; a cold `count()` of it still
|
|
1616
|
+
* answers the number its row records, while a read of the object throws, so the count alone never shows it. With
|
|
1617
|
+
* `summaries: true` it also opens each segment's current object (one tail read each) and reports a segment whose
|
|
1618
|
+
* row's summary says another id count or metadata than the object holds as `summary-mismatch`, which is what a
|
|
1619
|
+
* restore of the registry from another point than the bucket leaves a count to answer. A sealed summary is held
|
|
1620
|
+
* against its object when this store has the key, and counted in `summariesUnchecked` when it does not.
|
|
1621
|
+
*
|
|
1474
1622
|
* It holds the registry rows it enumerates resident, at most 250,000 of them, and throws
|
|
1475
1623
|
* `BudgetExceededError` past that rather than report a partial scan as a whole one. This method takes no
|
|
1476
1624
|
* ceiling of its own: narrow the scan with `namespace`, or call `runConsistencyCheck` over the backend's
|
|
@@ -1478,7 +1626,10 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1478
1626
|
*/
|
|
1479
1627
|
async checkConsistency(options = {}) {
|
|
1480
1628
|
const deps = this.lifecycleDeps("checkConsistency");
|
|
1481
|
-
return runConsistencyCheck(
|
|
1629
|
+
return runConsistencyCheck(
|
|
1630
|
+
{ storage: deps.storage, registry: deps.registry, keystore: deps.keystore },
|
|
1631
|
+
options
|
|
1632
|
+
);
|
|
1482
1633
|
}
|
|
1483
1634
|
/**
|
|
1484
1635
|
* **Export ("eject") every registered segment's current generation** through the injected `sink`, using only
|
|
@@ -1725,10 +1876,18 @@ var Segment = class _Segment {
|
|
|
1725
1876
|
return this.timed("has", () => this.engine.has(this.ref, id));
|
|
1726
1877
|
}
|
|
1727
1878
|
/**
|
|
1728
|
-
* Cardinality
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
*
|
|
1879
|
+
* Cardinality of the generation this handle reads: **one registry read when cold, none when warm, and no read of the
|
|
1880
|
+
* object**, with **zero payload reads**. The registry row records the id count of the generation it names, written
|
|
1881
|
+
* by the write that made it current, and a count answers from that. A row with no summary it can use (one written
|
|
1882
|
+
* before rows carried it, one that names another generation, a sealed one that does not open) sends the count to the
|
|
1883
|
+
* `.crbm` index, summed with a tail read of the object. A segment whose pointer names a missing object (a torn
|
|
1884
|
+
* restore) still counts the row's number, while a read of the object throws; `checkConsistency` is what finds it.
|
|
1885
|
+
*
|
|
1886
|
+
* What this trusts: the row's summary, or the index's sum, and no payload is decoded to confirm it. The summary is
|
|
1887
|
+
* used only for the generation it names, on an active row, and a sealed one only if it opens under the segment's
|
|
1888
|
+
* key; it is held against the object whenever the object is opened anyway (a `has`, an `iterate`, a combine, a
|
|
1889
|
+
* `pin()`), at no extra request, and a disagreement stops this store using it, and fails nothing. It is not
|
|
1890
|
+
* confirmed on the cold path, so a party who can write the registry row can make a count wrong. The index is checked for internal consistency when the object is opened (each key in
|
|
1732
1891
|
* range and ascending, each cardinality in `1..65536`, each payload inside the payload region, and, on an
|
|
1733
1892
|
* unencrypted object, the footer's chunk count and total agreeing with the index), and a corrupt index that is
|
|
1734
1893
|
* still internally consistent yields a wrong count. `iterate()` and the combines decode the payloads, whose
|
|
@@ -1738,6 +1897,24 @@ var Segment = class _Segment {
|
|
|
1738
1897
|
if (this.expired()) return Promise.resolve(0);
|
|
1739
1898
|
return this.timed("count", () => this.engine.count(this.ref));
|
|
1740
1899
|
}
|
|
1900
|
+
/**
|
|
1901
|
+
* What the generation this handle reads is, from one resolution: its number, its id count and the metadata it
|
|
1902
|
+
* was loaded with (absent when it has none). It is what answers {@link Segment.count}, so the three describe one
|
|
1903
|
+
* generation and cannot straddle a publish. One registry read when cold, none while warm, and none on a pinned
|
|
1904
|
+
* handle, which answers for the generation it pinned. A segment with no generation, and an expired handle,
|
|
1905
|
+
* answer `{ generation: null, cardinality: 0 }`.
|
|
1906
|
+
*
|
|
1907
|
+
* Trust is as for `count()`: the registry row's word, not confirmed against the object until the object is
|
|
1908
|
+
* opened, when a disagreement makes this process stop using that row's summary.
|
|
1909
|
+
*
|
|
1910
|
+
* ```ts
|
|
1911
|
+
* const { generation, cardinality, metadata } = await store.segment('active-30d').stat();
|
|
1912
|
+
* ```
|
|
1913
|
+
*/
|
|
1914
|
+
async stat() {
|
|
1915
|
+
if (this.expired()) return { generation: null, cardinality: 0 };
|
|
1916
|
+
return this.engine.stat(this.ref);
|
|
1917
|
+
}
|
|
1741
1918
|
/**
|
|
1742
1919
|
* Every id, ascending, streamed one chunk at a time. Pass a range to read part of the segment: the ids in
|
|
1743
1920
|
* `(after, through]`, fetching only the chunks the range overlaps. The per-op budget is charged for every chunk in
|