@cloudbitmaps/roaring 0.17.0 → 0.18.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 +10 -0
- package/README.md +3 -3
- package/dist/bin/export-segments.js +527 -0
- package/dist/bin/export-segments.js.map +3 -3
- package/dist/index.d.ts +307 -1
- package/dist/index.js +530 -0
- package/dist/index.js.map +3 -3
- package/package.json +2 -2
package/PRIVACY.md
CHANGED
|
@@ -121,6 +121,14 @@ ledger is not by itself proof the id is gone.
|
|
|
121
121
|
generation the erasure did not rewrite, so the id can be current again. Roll back before the erasure starts or
|
|
122
122
|
after it returns, and re-run any erasure a rollback interrupted.
|
|
123
123
|
|
|
124
|
+
### A batch of materializations
|
|
125
|
+
|
|
126
|
+
A `store.materializeMany` call computes many `*Into` outputs in passes that read each operand once per group, and its window is the call's length, not one read's. An output of a `materializeMany` call can carry an id that was erased while the call ran, for as long as the call ran: the call reads each operand at the generation it pinned, and an output's publish is a load, so an erasure that lands after the chunks were read does not reach what the call holds, unless the output subtracts a pinned operand that the erasure rewrote, which the call re-reads just before the publishes and refuses. An erasure that rewrites a destination before that output's publish starts does not stop the publish, which, subject to the output's own `guard` and the refusal of an empty result over a non-empty destination, writes on top of the erasure's generation; an erasure that lands inside the publish's own write, between its pointer read and its pointer write, makes the publish lose with `WriteConflictError`; after any call that overlapped an erasure, re-run `eraseSubject` and keep both ledgers. `eraseSubject` deletes the generation it rewrites even when a call holds it pinned, so a pin does not outlive an erasure, and an output that still needs a deleted operand generation fails with `NotFoundError`.
|
|
127
|
+
|
|
128
|
+
A call with a feed reads operands the library never stores, which no erasure can reach, so the guarantee above does not describe it, and it is refused instead. A `materializeMany` call with a feed is refused at its next record, and immediately before each fed output's publish (after the output has waited for its room in the memory budget), once `eraseSubject` has started in this store, one already running when the call began included: nothing is published from operands the call read before the erasure returned, apart from the window below, and the one record in hand when the erasure lands is processed and discarded. The counter it reads is moved at the start and at the end of each `eraseSubject` this store runs, so an erasure that began and finished between two of the call's checks is seen too; `rollback`, `retireExpired` and `dropSegment` do not move it. An erasure that starts after a fed output's last check and finishes before that publish's pointer write is not caught by the counter, and its window is the publish's own object write and pointer write, which lasts as long as the object takes to upload; the fences above still apply to what it touched. Only this store's `eraseSubject` moves the counter such a call reads, so an erasure by any other path (another store, another process, or the free function `eraseIdFromSegment` in this process) is not seen by it, and is bounded by nothing. An id erased from the source of a feed has to be erased there: the library holds none of it.
|
|
129
|
+
|
|
130
|
+
A held operand, made by `store.memory`, is the caller's memory too, so no erasure can reach it either, and it is refused instead. A `materializeMany` call that reads an in-memory operand from `store.memory` fails the outputs that read it, before any request and immediately before each of their publishes, once `eraseSubject` has started in this store since the operand began to be made, one already running when it began included; an erasure by any path but this store's `eraseSubject` is not seen by such an operand, and is bounded by nothing. An erasure that starts after an output's last check and finishes before its publish writes is not caught by the counter, as for a feed, and the fences above still apply to what it touched. Build a held operand from your source of truth at the start of each refresh and release it at its end: the library keeps none of it once released.
|
|
131
|
+
|
|
124
132
|
### One process, and the rest of your fleet
|
|
125
133
|
|
|
126
134
|
Erasure and crypto-shred are **immediate in storage and immediate in the store whose verb performed them**, for
|
|
@@ -137,6 +145,8 @@ bucket.
|
|
|
137
145
|
| another store, with a registry and a `cache.genTtlMs` above 0 | within `cache.genTtlMs` (default 2 s), when its snapshot re-resolves, **while the registry can be read** (see below) |
|
|
138
146
|
| another store with **no registry** (a bare `IStorageDriver`), with `cache: { genTtlMs: 0 }`, or on a storage source built with **no clock** | **no bound** — only when its caches happen to let the segment go, or something tells it |
|
|
139
147
|
| `seg.pinAt` of a generation the erasure rewrote, in any store | on return it fails with `NotFoundError`, since the object is deleted (and, with a registry, the row has moved on); during the erasure, between the rewrite's publish and its delete of the old generation, it can still open that generation, and a handle opened then is a pin, covered by the row below |
|
|
148
|
+
| a `materializeMany` call with a feed, and an erasure by any path but this store's `eraseSubject` | **no bound**: the call is refused only by this store's `eraseSubject` |
|
|
149
|
+
| a held in-memory operand (`store.memory`), and an erasure by any path but this store's `eraseSubject` | **no bound**: the operand is refused only by this store's `eraseSubject`, started after the operand began to be made |
|
|
140
150
|
| a pinned handle (`seg.pin()`) in another store | **no bound** — until that store's reader cache evicts the pin's reader (in a store with a timed pointer refresh, a small generation's reader holds all of its chunks, decoded or not) and its chunk cache evicts the chunks the pin decoded, or something tells it |
|
|
141
151
|
| a leased pinned handle (`seg.pin({ leaseUntil })`) in another store | until its lease ends, at most 14 days after it was taken: its reads then throw `LeaseExpiredError`, whether or not its reader still holds the chunks. Until then, as for any pinned handle in the row above |
|
|
142
152
|
|
package/README.md
CHANGED
|
@@ -69,10 +69,10 @@ name.
|
|
|
69
69
|
|
|
70
70
|
## What it costs
|
|
71
71
|
|
|
72
|
-
A cold `intersect` of two 500,000-id segments sharing 100 of their 1,999 chunks made 6 GETs and took
|
|
73
|
-
million** at list prices: measured on S3 in-region, in the run of 2026-10-
|
|
72
|
+
A cold `intersect` of two 500,000-id segments sharing 100 of their 1,999 chunks made 6 GETs and took 99.80 ms at the median, **$2.40 per
|
|
73
|
+
million** at list prices: measured on S3 in-region, in the run of 2026-10-06, whose client had 128 sockets. On a spread layout it reads more
|
|
74
74
|
bytes than it needs, which cross-region can cost more than the requests it saves. A segment's first `store.load()` made
|
|
75
|
-
2 PUT + 3 GET, **$11.20 per million** at the default prices, pointer included: the requests are measured on S3 in-region, in the same run. A cold `count()` is one
|
|
75
|
+
2 PUT + 3 GET, **$11.20 per million** at the default prices, pointer included: the requests are measured on S3 in-region, in the same run. A steady `store.load()` at `keep: 12` that deletes a generation by name made 2 PUT + 4 GET and a free delete, measured: **$11.60 per million**. A cold `count()` is one
|
|
76
76
|
pointer read: it reads no payload and no object.
|
|
77
77
|
|
|
78
78
|
The trade is stated plainly. A membership check that misses the cache costs a ranged GET against object storage,
|
|
@@ -38,6 +38,10 @@ import {
|
|
|
38
38
|
groundedReport,
|
|
39
39
|
loadSegment,
|
|
40
40
|
loadSegmentChunks,
|
|
41
|
+
compileCombineMany,
|
|
42
|
+
prepareHeld,
|
|
43
|
+
rebindCombineMany,
|
|
44
|
+
runCombineMany,
|
|
41
45
|
mapWithConcurrency,
|
|
42
46
|
resolveBudget,
|
|
43
47
|
resolvePerOpBudget,
|
|
@@ -635,6 +639,7 @@ import {
|
|
|
635
639
|
BudgetExceededError,
|
|
636
640
|
LeaseExpiredError as LeaseExpiredError2,
|
|
637
641
|
LeaseLimitError,
|
|
642
|
+
StaleOperandError,
|
|
638
643
|
isCloudRoaringError,
|
|
639
644
|
isWriteConflictError,
|
|
640
645
|
isTransientError,
|
|
@@ -643,6 +648,7 @@ import {
|
|
|
643
648
|
isValidationError,
|
|
644
649
|
isLeaseExpiredError,
|
|
645
650
|
isLeaseLimitError,
|
|
651
|
+
isStaleOperandError,
|
|
646
652
|
CrbmReader,
|
|
647
653
|
BufferReader,
|
|
648
654
|
CountingMetricsSink,
|
|
@@ -673,6 +679,36 @@ function requireScope(options, op) {
|
|
|
673
679
|
);
|
|
674
680
|
}
|
|
675
681
|
}
|
|
682
|
+
var viewOf;
|
|
683
|
+
var MATERIALIZE_MANY_KEYS = [
|
|
684
|
+
"operands",
|
|
685
|
+
"outputs",
|
|
686
|
+
"keep",
|
|
687
|
+
"pin",
|
|
688
|
+
"maxBufferedBytes",
|
|
689
|
+
"budget",
|
|
690
|
+
"publishConcurrency",
|
|
691
|
+
"concurrency",
|
|
692
|
+
"allowAbsentOperands",
|
|
693
|
+
"after",
|
|
694
|
+
"through",
|
|
695
|
+
"feed",
|
|
696
|
+
"mayBeEmpty"
|
|
697
|
+
];
|
|
698
|
+
var MATERIALIZE_MANY_OUTPUT_KEYS = [
|
|
699
|
+
"dest",
|
|
700
|
+
"expr",
|
|
701
|
+
"exclude",
|
|
702
|
+
"guard",
|
|
703
|
+
"allowEmpty",
|
|
704
|
+
"metadata",
|
|
705
|
+
"audit",
|
|
706
|
+
"keep"
|
|
707
|
+
];
|
|
708
|
+
var DEFAULT_MAX_BUFFERED_BYTES = 256 * 1024 * 1024;
|
|
709
|
+
var DEFAULT_PUBLISH_CONCURRENCY = 8;
|
|
710
|
+
var DEFAULT_BATCH_CONCURRENCY = 1;
|
|
711
|
+
var PIN_PARALLELISM = 16;
|
|
676
712
|
function resolveStorageSource(options, clock) {
|
|
677
713
|
const storage = options.storage;
|
|
678
714
|
if (storage === null || typeof storage !== "object") {
|
|
@@ -785,6 +821,13 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
785
821
|
requireEncryption;
|
|
786
822
|
/** Resolved store-level per-op budget (null = disabled); the admin scans use it, with a per-op override. */
|
|
787
823
|
budget;
|
|
824
|
+
/**
|
|
825
|
+
* Moved at the start and at the end of each `eraseSubject` this store runs. A `materializeMany` call with a feed records
|
|
826
|
+
* it when it begins and is refused once it has moved: it reads caller memory, which no erasure can reach.
|
|
827
|
+
*/
|
|
828
|
+
erasureEpoch = 0;
|
|
829
|
+
/** How many `eraseSubject` calls of this store are running: while any is, every check of the counter counts as moved. */
|
|
830
|
+
erasuresRunning = 0;
|
|
788
831
|
/**
|
|
789
832
|
* Refuse any option key the store does not take, at the top level or inside a group, naming each one.
|
|
790
833
|
*
|
|
@@ -1024,6 +1067,440 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1024
1067
|
collected: result.collected
|
|
1025
1068
|
};
|
|
1026
1069
|
}
|
|
1070
|
+
/**
|
|
1071
|
+
* **Hold a set of ids in memory**, to be an operand of {@link CloudRoaring.materializeMany} and of no other verb.
|
|
1072
|
+
*
|
|
1073
|
+
* ```ts
|
|
1074
|
+
* const vip = await store.memory([7, 42, 99_001]);
|
|
1075
|
+
* await store.materializeMany({ operands: { vip, engaged: store.segment('cond-engaged') }, outputs: [...], keep: 12 });
|
|
1076
|
+
* vip.release();
|
|
1077
|
+
* ```
|
|
1078
|
+
*
|
|
1079
|
+
* It takes what {@link CloudRoaring.load} takes as input (ids as a sync or async iterable or a typed array, `{ bitmap }`
|
|
1080
|
+
* or `{ serialized }`) and checks it the same way, with the same errors: the size cap, the structure, the safe
|
|
1081
|
+
* deserializer and each id's range. **Only a real `Uint32Array` skips the per-id range check**; any other typed array goes
|
|
1082
|
+
* through it, so `Int32Array([-1])` and `Float64Array([1.5])` throw {@link ValidationError}. An object with an `ascending`
|
|
1083
|
+
* key is refused: ids that arrive in key order are a `feed`. An empty set is accepted here, and refused by
|
|
1084
|
+
* `materializeMany` unless its name is in `mayBeEmpty`.
|
|
1085
|
+
*
|
|
1086
|
+
* The ids are held as the chunks a stored generation would hold, until {@link MemoryOperand.release}; between calls that
|
|
1087
|
+
* memory is the caller's, and during a call it counts against `maxBufferedBytes`. There is no size option: the load's
|
|
1088
|
+
* own cap applies to a serialized input.
|
|
1089
|
+
*
|
|
1090
|
+
* **Erasure.** Only `eraseSubject` moves the store's counter (at its start and at its end). One that started in this store
|
|
1091
|
+
* since the handle began to be made, or is still running, makes a call that reads the handle fail the outputs that read it with
|
|
1092
|
+
* {@link StaleOperandError} (`reason: 'erased'`), before any request and again just before each publish. An erasure in
|
|
1093
|
+
* another store or another process is not seen, nor are `dropSegment`, `eraseNamespace`, `destroySegment` and the free
|
|
1094
|
+
* function `eraseIdFromSegment`, which do not move it: the handle is the caller's copy, so build it from the source of truth at
|
|
1095
|
+
* the start of each refresh and release it at the end.
|
|
1096
|
+
*
|
|
1097
|
+
* @throws {ValidationError} where a load of the same input would.
|
|
1098
|
+
*/
|
|
1099
|
+
async memory(input) {
|
|
1100
|
+
const epochAt = this.epochNow();
|
|
1101
|
+
const chunks = await prepareHeld(input, roaringCodec, this.clock);
|
|
1102
|
+
return makeMemoryOperand({ store: this, chunks, epochAt });
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* **Many outputs, each operand read once per group.** Each output is an expression over named stored operands (`and`, `or`, `andNot`, nested
|
|
1106
|
+
* to depth 64) published as a new generation of its own `dest`, exactly as an `*Into` would publish it, but computed
|
|
1107
|
+
* together: each operand's chunks are read once for all the outputs that use them, instead of once per output and
|
|
1108
|
+
* once more for every scratch segment a nested expression would otherwise need.
|
|
1109
|
+
*
|
|
1110
|
+
* ```ts
|
|
1111
|
+
* const run = await store.materializeMany({
|
|
1112
|
+
* operands: { us: store.segment('us'), engaged: store.segment('engaged'), optOut: store.segment('opt-out') },
|
|
1113
|
+
* outputs: [
|
|
1114
|
+
* { dest: store.segment('send-1'), expr: { and: ['us', 'engaged'] }, exclude: ['optOut'] },
|
|
1115
|
+
* { dest: store.segment('send-2'), expr: { andNot: ['us', 'engaged'] } },
|
|
1116
|
+
* ],
|
|
1117
|
+
* keep: 3,
|
|
1118
|
+
* });
|
|
1119
|
+
* run.outputs[0]; // a MaterializeResult, or { published: false, error }
|
|
1120
|
+
* ```
|
|
1121
|
+
*
|
|
1122
|
+
* **Results.** `run.outputs[i]` is index-aligned with `outputs` and is exactly what output `i`'s `*Into` would have
|
|
1123
|
+
* returned (a {@link MaterializeResult}: published, or refused with its `reason`), or `{ published: false, error }` for
|
|
1124
|
+
* what it would have thrown. One output's refusal or error never stops another. The call itself throws only for bad
|
|
1125
|
+
* input ({@link ValidationError} naming the output index and path, before any request), an operand that names no
|
|
1126
|
+
* segment (unless `allowAbsentOperands`), and a `budget` its own plan exceeds, the last two before any chunk is read.
|
|
1127
|
+
*
|
|
1128
|
+
* **One generation per operand.** By default every stored operand is pinned for the call: read at the generation it had
|
|
1129
|
+
* when the call started, however long the call runs and whatever loads land meanwhile. That is one generation per
|
|
1130
|
+
* operand, not one instant across operands (two pins are two registry reads). `pin: false` reads live, as a combine does.
|
|
1131
|
+
*
|
|
1132
|
+
* **Every pinned operand an output subtracts is re-checked.** An output subtracts an operand when it is in its `exclude`
|
|
1133
|
+
* or under any entry of an `andNot` after the first, at any depth. Immediately before the publishes the call re-reads the
|
|
1134
|
+
* registry row of each such operand and compares its generation and incarnation with what was pinned (a name deleted and
|
|
1135
|
+
* created again is a replacement though it is generation 0 again). If one was replaced, each output that subtracts it is
|
|
1136
|
+
* not published and carries {@link StaleOperandError}; the others publish. It costs one registry read per such operand per
|
|
1137
|
+
* group, and `stats.operands` carries every operand's pinned, start and end generation and whether it moved.
|
|
1138
|
+
*
|
|
1139
|
+
* **Memory.** Everything the pass holds resident is counted against `maxBufferedBytes` (default 256 MiB): each output's
|
|
1140
|
+
* buffered chunks, the operand streams' ranges, the chunks being evaluated, and the plan itself, and a call whose plan alone
|
|
1141
|
+
* passes it is refused before any chunk is read. When the outputs do not fit, they run in groups, each reading its operands
|
|
1142
|
+
* once (so the operand reads grow with the total output size over the budget), and a group's buffers are released only as
|
|
1143
|
+
* its publishes settle. A chunk count the index understated does not grow a group past the budget: the largest buffer is
|
|
1144
|
+
* dropped and that output re-runs alone. An output that cannot fit alone has `BudgetExceededError` as its result. The count
|
|
1145
|
+
* is the pass's own: process memory is more, measured at 1.6 times the ledger's high water on Linux and 2.5 to 5 times on
|
|
1146
|
+
* macOS on the in-memory backend (the guide has the shapes and what is not counted).
|
|
1147
|
+
*
|
|
1148
|
+
* **Untrusted bytes.** Every operand chunk is decoded through the checks on untrusted tier data. A key an operand's index
|
|
1149
|
+
* lists whose bytes are missing is an error for the outputs that read the operand, never an empty chunk.
|
|
1150
|
+
*
|
|
1151
|
+
* **Handles.** An expired or released handle anywhere in the call (an operand or a `dest`) is refused before any request,
|
|
1152
|
+
* as on the `*Into` verbs. A leased operand's lease, and an operand's deadline, are checked before each chunk key
|
|
1153
|
+
* is read: a lapse fails only the outputs that read that operand, never reads empty. Each `dest`'s lease and deadline
|
|
1154
|
+
* are checked again just before its publish.
|
|
1155
|
+
*
|
|
1156
|
+
* **The chunk cache is bypassed:** the pass reads through the storage source and never writes the decoded-chunk cache, so
|
|
1157
|
+
* a batch does not evict other readers' hot chunks. Each publish still invalidates its `dest` as an `*Into` does.
|
|
1158
|
+
*
|
|
1159
|
+
* **A limit of the form.** An output cannot name another output of the same call: a `dest` that is also an operand is
|
|
1160
|
+
* refused. Chain two calls.
|
|
1161
|
+
*
|
|
1162
|
+
* **Held operands.** An operand may be a {@link MemoryOperand} from {@link CloudRoaring.memory}: ids the caller holds,
|
|
1163
|
+
* read beside the stored ones and giving the same output byte for byte. It is read from memory (no request, and the
|
|
1164
|
+
* shared chunk cache never sees it), counts against `maxBufferedBytes` while the call runs, and is refused when empty
|
|
1165
|
+
* unless its name is in `mayBeEmpty`. A handle from another store, or a released one, is a {@link ValidationError}. An
|
|
1166
|
+
* erasure in this store since the handle was made fails the outputs that read it with {@link StaleOperandError}
|
|
1167
|
+
* (`reason: 'erased'`).
|
|
1168
|
+
*
|
|
1169
|
+
* **A feed.** `feed: { names, records, counts }` adds operands that arrive as records in chunk-key order, for conditions
|
|
1170
|
+
* too many to hold or store. Each record is checked before the pass sees it (see {@link MaterializeManyFeedRecord}): a bad
|
|
1171
|
+
* feed is refused, never read as fewer members, and **a fed output is published only after the whole feed was read and every
|
|
1172
|
+
* end-of-feed check passed**, so a bad feed refuses every fed output and publishes none of them; outputs that name only
|
|
1173
|
+
* stored operands are unaffected. At the end of the feed `counts` must equal the ids the feed held for each name, and a
|
|
1174
|
+
* name that appeared in no record is refused unless it is in `mayBeEmpty`. A feed is read once, so the call runs its outputs
|
|
1175
|
+
* as one group and **`maxBufferedBytes` is required**: each record is converted to compressed bitmaps as it arrives, the key
|
|
1176
|
+
* in hand and its fed outputs are counted against the budget, and the call fails as soon as the count passes it, with
|
|
1177
|
+
* {@link BudgetExceededError} for every fed output. **Erasure:** a fed call is refused at its next record, and before each
|
|
1178
|
+
* fed publish, once `eraseSubject` has started in this store ({@link StaleOperandError}, `reason: 'erased'`).
|
|
1179
|
+
*
|
|
1180
|
+
* ```ts
|
|
1181
|
+
* await store.materializeMany({
|
|
1182
|
+
* operands: { unsub: store.segment('global-unsub') },
|
|
1183
|
+
* feed: {
|
|
1184
|
+
* names: ['us', 'engaged'],
|
|
1185
|
+
* records: warehouseRecords(), // yields { key, operands: { us: Uint32Array, engaged: Uint32Array } } by key
|
|
1186
|
+
* counts: () => warehouseCounts(),
|
|
1187
|
+
* },
|
|
1188
|
+
* maxBufferedBytes: 512 * 1024 * 1024,
|
|
1189
|
+
* outputs: [{ dest: store.segment('send-1'), expr: { and: ['us', 'engaged'] }, exclude: ['unsub'] }],
|
|
1190
|
+
* keep: 12,
|
|
1191
|
+
* });
|
|
1192
|
+
* ```
|
|
1193
|
+
*/
|
|
1194
|
+
async materializeMany(options) {
|
|
1195
|
+
if (typeof options !== "object" || options === null) {
|
|
1196
|
+
throw new ValidationError2(
|
|
1197
|
+
"materializeMany needs an options object with `operands`, `outputs` and `keep`"
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
const epochAt = options.feed === void 0 ? 0 : this.epochNow();
|
|
1201
|
+
const unknown = Object.keys(options).filter(
|
|
1202
|
+
(k) => !MATERIALIZE_MANY_KEYS.includes(k)
|
|
1203
|
+
);
|
|
1204
|
+
if (unknown.length > 0) {
|
|
1205
|
+
throw new ValidationError2(
|
|
1206
|
+
`materializeMany: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")}`
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
const operandsIn = options.operands;
|
|
1210
|
+
const outputsIn = options.outputs;
|
|
1211
|
+
const keep = options.keep;
|
|
1212
|
+
const after = options.after;
|
|
1213
|
+
const through = options.through;
|
|
1214
|
+
const pinOption = options.pin ?? true;
|
|
1215
|
+
const feedIn = options.feed;
|
|
1216
|
+
const mayBeEmptyIn = options.mayBeEmpty;
|
|
1217
|
+
if (feedIn !== void 0 && options.maxBufferedBytes === void 0) {
|
|
1218
|
+
throw new ValidationError2(
|
|
1219
|
+
"materializeMany: maxBufferedBytes is required with a feed: the feed is read once, so the call runs as one group and the budget is what bounds it"
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
const maxBufferedBytes = options.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES;
|
|
1223
|
+
const publishConcurrency = options.publishConcurrency ?? DEFAULT_PUBLISH_CONCURRENCY;
|
|
1224
|
+
const concurrency = options.concurrency ?? DEFAULT_BATCH_CONCURRENCY;
|
|
1225
|
+
const budgetOption = options.budget;
|
|
1226
|
+
const allowAbsentOperands = options.allowAbsentOperands;
|
|
1227
|
+
if (typeof pinOption !== "boolean")
|
|
1228
|
+
throw new ValidationError2("materializeMany: pin must be a boolean");
|
|
1229
|
+
if (allowAbsentOperands !== void 0 && typeof allowAbsentOperands !== "boolean") {
|
|
1230
|
+
throw new ValidationError2("materializeMany: allowAbsentOperands must be a boolean");
|
|
1231
|
+
}
|
|
1232
|
+
if (typeof operandsIn !== "object" || operandsIn === null || Array.isArray(operandsIn)) {
|
|
1233
|
+
throw new ValidationError2("materializeMany: operands must be an object of name to segment");
|
|
1234
|
+
}
|
|
1235
|
+
if (!Array.isArray(outputsIn) || outputsIn.length === 0) {
|
|
1236
|
+
throw new ValidationError2("materializeMany: outputs must be a non-empty array");
|
|
1237
|
+
}
|
|
1238
|
+
const names = Object.keys(operandsIn);
|
|
1239
|
+
const handles = /* @__PURE__ */ new Map();
|
|
1240
|
+
const heldHandles = /* @__PURE__ */ new Map();
|
|
1241
|
+
for (const name of names) {
|
|
1242
|
+
const handle = operandsIn[name];
|
|
1243
|
+
if (handle instanceof MemoryOperand) {
|
|
1244
|
+
const view = heldOf(handle);
|
|
1245
|
+
if (view === void 0) {
|
|
1246
|
+
throw new ValidationError2(
|
|
1247
|
+
`materializeMany: operands["${name}"] is not a memory operand from store.memory()`
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
if (view.store !== this) {
|
|
1251
|
+
throw new ValidationError2(
|
|
1252
|
+
`materializeMany: operands["${name}"] was made by another store: a memory operand belongs to the store that made it`
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
if (view.chunks.released) {
|
|
1256
|
+
throw new ValidationError2(
|
|
1257
|
+
`materializeMany: operands["${name}"] was released: make another with store.memory()`
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
heldHandles.set(name, view);
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (!(handle instanceof Segment)) {
|
|
1264
|
+
throw new ValidationError2(
|
|
1265
|
+
`materializeMany: operands["${name}"] must be a segment from store.segment() or a memory operand from store.memory()`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
handles.set(name, handle);
|
|
1269
|
+
}
|
|
1270
|
+
const heldMayBeEmpty = /* @__PURE__ */ new Set();
|
|
1271
|
+
let fedMayBeEmpty = mayBeEmptyIn;
|
|
1272
|
+
if (mayBeEmptyIn !== void 0) {
|
|
1273
|
+
if (Array.isArray(mayBeEmptyIn)) {
|
|
1274
|
+
const rest = [];
|
|
1275
|
+
for (const name of mayBeEmptyIn) {
|
|
1276
|
+
if (typeof name === "string" && heldHandles.has(name)) heldMayBeEmpty.add(name);
|
|
1277
|
+
else rest.push(name);
|
|
1278
|
+
}
|
|
1279
|
+
fedMayBeEmpty = rest;
|
|
1280
|
+
}
|
|
1281
|
+
if (feedIn === void 0 && (heldHandles.size === 0 || !Array.isArray(fedMayBeEmpty) || fedMayBeEmpty.length > 0)) {
|
|
1282
|
+
throw new ValidationError2(
|
|
1283
|
+
"materializeMany: mayBeEmpty names fed operands, and the call has no feed"
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
const operandKeys = new Set([...handles.values()].map((h) => h.key()));
|
|
1288
|
+
const outputs = [];
|
|
1289
|
+
const destKeys = /* @__PURE__ */ new Set();
|
|
1290
|
+
outputsIn.forEach((raw, i) => {
|
|
1291
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1292
|
+
throw new ValidationError2(`outputs[${i}] must be an object`);
|
|
1293
|
+
}
|
|
1294
|
+
const bad = Object.keys(raw).filter(
|
|
1295
|
+
(k) => !MATERIALIZE_MANY_OUTPUT_KEYS.includes(k)
|
|
1296
|
+
);
|
|
1297
|
+
if (bad.length > 0) {
|
|
1298
|
+
throw new ValidationError2(
|
|
1299
|
+
`outputs[${i}]: unknown option${bad.length > 1 ? "s" : ""} ${bad.map((k) => `"${k}"`).join(", ")}`
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
const spec = raw;
|
|
1303
|
+
const dest = spec.dest;
|
|
1304
|
+
if (!(dest instanceof Segment)) {
|
|
1305
|
+
throw new ValidationError2(`outputs[${i}].dest must be a segment from store.segment()`);
|
|
1306
|
+
}
|
|
1307
|
+
if (operandKeys.has(dest.key())) {
|
|
1308
|
+
throw new ValidationError2(
|
|
1309
|
+
`outputs[${i}].dest is also an operand of the call: one call is one pass over operands that do not change under it, so chain two calls`
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
if (destKeys.has(dest.key())) {
|
|
1313
|
+
throw new ValidationError2(
|
|
1314
|
+
`outputs[${i}].dest names a segment another output of the call writes`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
destKeys.add(dest.key());
|
|
1318
|
+
outputs.push({ spec, dest });
|
|
1319
|
+
});
|
|
1320
|
+
const storeCeiling = this.budget === null || this.budget.maxRequests !== DEFAULT_BUDGET.maxRequests ? this.budget : void 0;
|
|
1321
|
+
const budget = budgetOption === void 0 ? storeCeiling : resolvePerOpBudget(budgetOption, this.budget);
|
|
1322
|
+
const operandSpecs = (pins2) => names.map((name) => {
|
|
1323
|
+
const view = heldHandles.get(name);
|
|
1324
|
+
if (view !== void 0) {
|
|
1325
|
+
return {
|
|
1326
|
+
name,
|
|
1327
|
+
ref: { segment: name },
|
|
1328
|
+
held: {
|
|
1329
|
+
chunks: view.chunks,
|
|
1330
|
+
moved: () => this.epochMoved(view.epochAt),
|
|
1331
|
+
mayBeEmpty: heldMayBeEmpty.has(name)
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
const handle = handles.get(name);
|
|
1336
|
+
const seg = viewOf(handle);
|
|
1337
|
+
const pinnedAt = pins2.get(handle.key());
|
|
1338
|
+
const guarded = seg.expiresAt !== void 0 || handle.lease !== void 0;
|
|
1339
|
+
return {
|
|
1340
|
+
name,
|
|
1341
|
+
ref: seg.ref,
|
|
1342
|
+
...guarded ? { check: () => this.assertReadable("materializeMany", handle) } : {},
|
|
1343
|
+
...pinnedAt === void 0 ? {} : {
|
|
1344
|
+
pinnedGeneration: pinnedAt.generation,
|
|
1345
|
+
pinnedVersion: pinnedAt.version,
|
|
1346
|
+
pinnedFingerprint: pinnedAt.fingerprint ?? null
|
|
1347
|
+
},
|
|
1348
|
+
current: (read) => this.currentRowOf(seg.ref, read?.fingerprint === true)
|
|
1349
|
+
};
|
|
1350
|
+
});
|
|
1351
|
+
const request = (pins2) => ({
|
|
1352
|
+
operands: operandSpecs(pins2),
|
|
1353
|
+
outputs: outputs.map(({ spec, dest }, i) => ({
|
|
1354
|
+
expr: spec.expr,
|
|
1355
|
+
...spec.exclude === void 0 ? {} : { exclude: spec.exclude },
|
|
1356
|
+
...spec.allowEmpty === void 0 ? {} : { allowEmpty: spec.allowEmpty },
|
|
1357
|
+
...spec.guard === void 0 ? {} : { guard: spec.guard },
|
|
1358
|
+
...spec.metadata === void 0 ? {} : { metadata: spec.metadata },
|
|
1359
|
+
...spec.keep === void 0 ? {} : { keep: spec.keep },
|
|
1360
|
+
beforePublish: () => this.assertWritable(`outputs[${i}]`, dest),
|
|
1361
|
+
// What a settled publish proves it sent: a published one wrote its object and moved the pointer, a refused one
|
|
1362
|
+
// wrote its object and deleted it.
|
|
1363
|
+
requestsOf: (result) => ({ get: 0, put: result.published ? 2 : 1 }),
|
|
1364
|
+
publish: (chunks, write) => this.materialize(viewOf(dest).ref, new CombineChunks(chunks), "materializeMany", {
|
|
1365
|
+
...write,
|
|
1366
|
+
...spec.audit === void 0 ? {} : { audit: spec.audit }
|
|
1367
|
+
})
|
|
1368
|
+
})),
|
|
1369
|
+
keep,
|
|
1370
|
+
...after === void 0 ? {} : { after },
|
|
1371
|
+
...through === void 0 ? {} : { through },
|
|
1372
|
+
maxBufferedBytes,
|
|
1373
|
+
publishConcurrency,
|
|
1374
|
+
concurrency,
|
|
1375
|
+
...budget === void 0 ? {} : { budget },
|
|
1376
|
+
...allowAbsentOperands === void 0 ? {} : { allowAbsentOperands },
|
|
1377
|
+
...feedIn === void 0 ? {} : { feed: this.feedOf(feedIn, fedMayBeEmpty, epochAt) }
|
|
1378
|
+
});
|
|
1379
|
+
const compiled = compileCombineMany(request(/* @__PURE__ */ new Map()));
|
|
1380
|
+
for (const { dest } of outputs) this.assertWritable("materializeMany", dest);
|
|
1381
|
+
for (const handle of handles.values()) this.assertReadable("materializeMany", handle);
|
|
1382
|
+
this.lifecycleDeps("materializeMany");
|
|
1383
|
+
const crbm = this.crbmSource;
|
|
1384
|
+
if (crbm === void 0) {
|
|
1385
|
+
throw new UnsupportedError(
|
|
1386
|
+
"materializeMany needs the `.crbm` storage source \u2014 pass a backend or a raw IStorageDriver as `storage`"
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
const held = /* @__PURE__ */ new Map();
|
|
1390
|
+
for (const [name, handle] of handles) {
|
|
1391
|
+
const key = handle.key();
|
|
1392
|
+
const at = handle.pinnedAt;
|
|
1393
|
+
const prior = held.get(key);
|
|
1394
|
+
if (prior === void 0) {
|
|
1395
|
+
held.set(key, { at, handle });
|
|
1396
|
+
} else if (prior.at !== void 0 && at !== void 0) {
|
|
1397
|
+
if (!samePin(prior.at, at)) {
|
|
1398
|
+
throw new ValidationError2(
|
|
1399
|
+
`materializeMany: operand "${name}" holds a segment another operand holds, ${twoPins(prior.at, at)}: one call reads a segment at one generation`
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
} else if (prior.at !== void 0 || at !== void 0) {
|
|
1403
|
+
if (!pinOption) {
|
|
1404
|
+
throw new ValidationError2(
|
|
1405
|
+
`materializeMany: operand "${name}" holds a segment another operand holds, ${twoPins(prior.at, at)}: one call reads a segment at one generation`
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
if (at !== void 0) held.set(key, { at, handle });
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const pins = /* @__PURE__ */ new Map();
|
|
1412
|
+
const toPin = [];
|
|
1413
|
+
for (const [key, { at, handle }] of held) {
|
|
1414
|
+
if (at !== void 0) pins.set(key, at);
|
|
1415
|
+
else if (pinOption) toPin.push([key, handle]);
|
|
1416
|
+
else pins.set(key, void 0);
|
|
1417
|
+
}
|
|
1418
|
+
const taken = await mapWithConcurrency(
|
|
1419
|
+
toPin,
|
|
1420
|
+
PIN_PARALLELISM,
|
|
1421
|
+
async ([key, handle]) => [key, (await handle.pin()).pinnedAt]
|
|
1422
|
+
);
|
|
1423
|
+
for (const [key, at] of taken) pins.set(key, at);
|
|
1424
|
+
const live = new Map([...pins].filter((e) => e[1] !== void 0));
|
|
1425
|
+
const pinnedSource = new PinnedStorageChunkSource(crbm, live);
|
|
1426
|
+
const source = this.retryOptions === void 0 ? pinnedSource : new RetryingStorageChunkSource(pinnedSource, this.retryOptions);
|
|
1427
|
+
const final = rebindCombineMany(compiled, operandSpecs(pins));
|
|
1428
|
+
const run = await runCombineMany(final, { source, codec: roaringCodec, clock: this.clock });
|
|
1429
|
+
return {
|
|
1430
|
+
outputs: run.outputs.map(
|
|
1431
|
+
(o) => o.ok ? o.value : {
|
|
1432
|
+
published: false,
|
|
1433
|
+
error: o.error instanceof Error ? o.error : new Error(String(o.error))
|
|
1434
|
+
}
|
|
1435
|
+
),
|
|
1436
|
+
stats: run.stats
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
epochNow() {
|
|
1440
|
+
return this.erasureEpoch;
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Whether an erasure has started in this store since `at` was read, or was running when it was: one that is still
|
|
1444
|
+
* running is seen by the count of those running, and one that has ended since by the counter, which its end moved.
|
|
1445
|
+
* The one check every call that reads caller memory uses.
|
|
1446
|
+
*/
|
|
1447
|
+
epochMoved(at) {
|
|
1448
|
+
return this.erasuresRunning > 0 || this.epochNow() !== at;
|
|
1449
|
+
}
|
|
1450
|
+
/** A call's feed with the store's erasure counter: read once here, checked by the combine before any record is read. */
|
|
1451
|
+
feedOf(feed, mayBeEmpty, epochAt) {
|
|
1452
|
+
if (typeof feed !== "object" || feed === null || Array.isArray(feed)) {
|
|
1453
|
+
throw new ValidationError2(
|
|
1454
|
+
"materializeMany: feed must be an object with names, records and counts"
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
const f = feed;
|
|
1458
|
+
return {
|
|
1459
|
+
names: f.names,
|
|
1460
|
+
records: f.records,
|
|
1461
|
+
counts: f.counts,
|
|
1462
|
+
...mayBeEmpty === void 0 ? {} : { mayBeEmpty },
|
|
1463
|
+
epoch: { moved: () => this.epochMoved(epochAt) }
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* The registry row of `ref`, read straight from the registry, not from a snapshot: a re-check must see a move. With
|
|
1468
|
+
* `fingerprint`, the current generation's object is opened too (one tail read, as a pin of it makes), and its
|
|
1469
|
+
* fingerprint is returned beside the row: a generation's number is identified by its row and its object, since a number
|
|
1470
|
+
* can be taken again once its object is gone.
|
|
1471
|
+
*/
|
|
1472
|
+
async currentRowOf(ref, fingerprint) {
|
|
1473
|
+
const registry = this.requireRegistry("materializeMany");
|
|
1474
|
+
if (fingerprint && this.crbmSource !== void 0) {
|
|
1475
|
+
const crbm = this.crbmSource;
|
|
1476
|
+
const at = await this.withRetries(() => crbm.pinGeneration(ref));
|
|
1477
|
+
const colon = at?.version.indexOf(":") ?? -1;
|
|
1478
|
+
if (at !== null && colon >= 0) {
|
|
1479
|
+
return {
|
|
1480
|
+
generation: at.generation,
|
|
1481
|
+
token: at.version.slice(colon + 1),
|
|
1482
|
+
fingerprint: at.fingerprint
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
const row = await this.withRetries(() => registry.get(ref));
|
|
1487
|
+
return row === null ? null : { generation: row.currentGen, token: row.token };
|
|
1488
|
+
}
|
|
1489
|
+
/** Throw what a read of `handle` must throw now: its lease error, or a refusal for a deadline that has passed. */
|
|
1490
|
+
assertReadable(op, handle) {
|
|
1491
|
+
const view = viewOf(handle);
|
|
1492
|
+
const lease = view.leaseError();
|
|
1493
|
+
if (lease !== void 0) throw lease;
|
|
1494
|
+
if (view.expired()) {
|
|
1495
|
+
throw new ValidationError2(
|
|
1496
|
+
`${op}: refusing to read "${view.ref.segment}", whose handle has expired \u2014 an expired handle reads as empty, so a generation built from it would be short. Open the handle without \`expiresAt\` if you meant to materialise.`
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
/** {@link assertReadable} for a destination, named by where it sits in the call. */
|
|
1501
|
+
assertWritable(where, dest) {
|
|
1502
|
+
this.assertReadable(where, dest);
|
|
1503
|
+
}
|
|
1027
1504
|
/**
|
|
1028
1505
|
* **Subject access (GDPR Art. 15 / CCPA right-to-know): which segments is this id a member of?**
|
|
1029
1506
|
*
|
|
@@ -1132,6 +1609,16 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
1132
1609
|
* is `dropSegment` / the `destroySegment`/`eraseNamespace` free functions.
|
|
1133
1610
|
*/
|
|
1134
1611
|
async eraseSubject(id, options = {}) {
|
|
1612
|
+
this.erasureEpoch++;
|
|
1613
|
+
this.erasuresRunning++;
|
|
1614
|
+
try {
|
|
1615
|
+
return await this.eraseSubjectRun(id, options);
|
|
1616
|
+
} finally {
|
|
1617
|
+
this.erasuresRunning--;
|
|
1618
|
+
this.erasureEpoch++;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
async eraseSubjectRun(id, options) {
|
|
1135
1622
|
const deps = this.lifecycleDeps("eraseSubject");
|
|
1136
1623
|
requireScope(options, "eraseSubject");
|
|
1137
1624
|
validateConcurrency(options.concurrency);
|
|
@@ -1948,6 +2435,39 @@ var AS_CHUNKS = {
|
|
|
1948
2435
|
andNot: (engine, base, refs, opts) => engine.andNotChunks(base, refs, opts)
|
|
1949
2436
|
};
|
|
1950
2437
|
var KEEP_EVERY_GENERATION = Number.MAX_SAFE_INTEGER;
|
|
2438
|
+
var heldOf;
|
|
2439
|
+
var makeMemoryOperand;
|
|
2440
|
+
var mintingMemory = false;
|
|
2441
|
+
var MemoryOperand = class _MemoryOperand {
|
|
2442
|
+
view;
|
|
2443
|
+
static {
|
|
2444
|
+
heldOf = (operand) => operand.view;
|
|
2445
|
+
makeMemoryOperand = (view) => {
|
|
2446
|
+
mintingMemory = true;
|
|
2447
|
+
try {
|
|
2448
|
+
return new _MemoryOperand(view);
|
|
2449
|
+
} finally {
|
|
2450
|
+
mintingMemory = false;
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
/** Not constructible: a handle comes from {@link CloudRoaring.memory}. */
|
|
2455
|
+
constructor(view) {
|
|
2456
|
+
if (!mintingMemory) {
|
|
2457
|
+
throw new ValidationError2(
|
|
2458
|
+
"a MemoryOperand is not constructed directly; call `await store.memory(ids)`"
|
|
2459
|
+
);
|
|
2460
|
+
}
|
|
2461
|
+
this.view = view;
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* Zero and drop the bytes it holds. Any later use throws {@link ValidationError}; releasing twice does nothing. A call
|
|
2465
|
+
* still reading it fails the outputs that read it.
|
|
2466
|
+
*/
|
|
2467
|
+
release() {
|
|
2468
|
+
this.view.chunks.release();
|
|
2469
|
+
}
|
|
2470
|
+
};
|
|
1951
2471
|
var makeSegment;
|
|
1952
2472
|
var minting = false;
|
|
1953
2473
|
var Segment = class _Segment {
|
|
@@ -1982,6 +2502,13 @@ var Segment = class _Segment {
|
|
|
1982
2502
|
minting = false;
|
|
1983
2503
|
}
|
|
1984
2504
|
};
|
|
2505
|
+
viewOf = (segment) => ({
|
|
2506
|
+
ref: segment.ref,
|
|
2507
|
+
pinnedAt: segment.pinnedAt,
|
|
2508
|
+
expiresAt: segment.expiresAt,
|
|
2509
|
+
leaseError: () => segment.leaseError(),
|
|
2510
|
+
expired: () => segment.expired()
|
|
2511
|
+
});
|
|
1985
2512
|
}
|
|
1986
2513
|
/**
|
|
1987
2514
|
* Not constructible: a handle comes from {@link CloudRoaring.segment}, which wires it to the store's engine,
|