@jarenjs/db 0.49.2 → 0.66.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +420 -71
- package/README.md +711 -79
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +309 -45
- package/docs/LIVE-FORMAT.md +156 -19
- package/docs/MIGRATION-FORMAT.md +247 -40
- package/docs/MODEL-FORMAT.md +968 -86
- package/package.json +21 -8
- package/schemas/jaren-migration.draft-07.schema.json +73 -0
- package/schemas/jaren-migration.schema.json +73 -0
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +255 -44
- package/src/cli.js +337 -50
- package/src/cursor.js +411 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +125 -11
- package/src/dialect.js +267 -112
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +245 -12
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +503 -69
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +18 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit-model.js +14 -0
- package/src/emit.js +349 -51
- package/src/entity.js +102 -59
- package/src/errors.js +430 -2
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -19
- package/src/introspect.js +583 -0
- package/src/jobs.js +870 -99
- package/src/json-bytes.js +58 -0
- package/src/live-time.js +12 -3
- package/src/live.js +11 -1
- package/src/maintenance.js +175 -0
- package/src/migrate.js +606 -333
- package/src/model.js +241 -8
- package/src/plan.js +1238 -160
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1748 -312
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1672 -276
- package/src/tracker.js +367 -68
- package/src/udf.js +88 -7
- package/types/index.d.ts +1246 -32
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +72 -3
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +81 -3
- package/types/wasm.d.ts +21 -0
- package/dist/types/algebra.d.ts +0 -230
- package/dist/types/app.d.ts +0 -49
- package/dist/types/capture.d.ts +0 -85
- package/dist/types/cli.d.ts +0 -2
- package/dist/types/dag-job.d.ts +0 -40
- package/dist/types/ddl.d.ts +0 -229
- package/dist/types/derive.d.ts +0 -250
- package/dist/types/dialect.d.ts +0 -154
- package/dist/types/dialects/sqlite.d.ts +0 -9
- package/dist/types/driver.d.ts +0 -110
- package/dist/types/drivers/bun.d.ts +0 -47
- package/dist/types/drivers/node.d.ts +0 -37
- package/dist/types/drivers/wasm.d.ts +0 -65
- package/dist/types/emit-model.d.ts +0 -44
- package/dist/types/emit.d.ts +0 -75
- package/dist/types/entity.d.ts +0 -23
- package/dist/types/errors.d.ts +0 -170
- package/dist/types/graph.d.ts +0 -28
- package/dist/types/index.d.ts +0 -37
- package/dist/types/jobs.d.ts +0 -140
- package/dist/types/knn.d.ts +0 -69
- package/dist/types/live-time.d.ts +0 -141
- package/dist/types/live.d.ts +0 -64
- package/dist/types/migrate.d.ts +0 -170
- package/dist/types/model.d.ts +0 -36
- package/dist/types/patch-sql.d.ts +0 -37
- package/dist/types/plan.d.ts +0 -142
- package/dist/types/profile.d.ts +0 -80
- package/dist/types/query.d.ts +0 -112
- package/dist/types/residual.d.ts +0 -64
- package/dist/types/series.d.ts +0 -227
- package/dist/types/store.d.ts +0 -60
- package/dist/types/tracker.d.ts +0 -43
- package/dist/types/typed.d.ts +0 -15
- package/dist/types/types.d.ts +0 -26
- package/dist/types/udf.d.ts +0 -75
- package/dist/types/window.d.ts +0 -52
package/src/documents.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Migrations without a database: the same `jslt` transforms and
|
|
4
|
+
* `query` assertions a Store applies to its tables, applied to
|
|
5
|
+
* documents a caller already holds or can stream past once.
|
|
6
|
+
*
|
|
7
|
+
* Two surfaces, because they cost different things and a caller should
|
|
8
|
+
* have to say which it is buying. {@link migrateDocuments} takes arrays
|
|
9
|
+
* and answers arrays: the source is REWINDABLE, so it runs the steps
|
|
10
|
+
* exactly as the Store does — every step over every document, in step
|
|
11
|
+
* order — and therefore refuses on exactly the step the Store would
|
|
12
|
+
* refuse on. {@link streamDocuments} takes anything iterable once and
|
|
13
|
+
* writes each document out as it finishes: bounded, and honest that a
|
|
14
|
+
* single pass cannot look ahead (see its own note).
|
|
15
|
+
*
|
|
16
|
+
* A physical step — rendered DDL, a data statement spelled as SQL, the
|
|
17
|
+
* table-rebuild procedure, the stored-derived-column backfill — has no
|
|
18
|
+
* meaning without tables. Neither surface skips one: both refuse, by
|
|
19
|
+
* name, before they ask the source for its first document.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { resolveRuntime } from '@jarenjs/core/runtime';
|
|
23
|
+
import { setObjectMember } from '@jarenjs/core/object';
|
|
24
|
+
import { compileJsonQuery } from '@jarenjs/json/query';
|
|
25
|
+
import { compileJsltStylesheet } from '@jarenjs/json/jslt';
|
|
26
|
+
|
|
27
|
+
import { DbCompileError } from './errors.js';
|
|
28
|
+
import { refuseCancelled } from './cancellation.js';
|
|
29
|
+
import {
|
|
30
|
+
compileDocumentStep, checkMigrationDocument, PHYSICAL_STEP_KINDS, DOCUMENT_STEP_KINDS,
|
|
31
|
+
normalizeAssertionBounds, createAssertionBoundGuard,
|
|
32
|
+
} from './document-steps.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Compile every migration's document steps and refuse, before any
|
|
36
|
+
* document is read, anything this host cannot honour.
|
|
37
|
+
* @param {any[]} migrations
|
|
38
|
+
* @param {Set<string>} present - the collections the caller supplied
|
|
39
|
+
* @param {{ compileJslt: Function, compileQuery: Function, keys: Record<string, string[]> }} context
|
|
40
|
+
* @returns {{ id: string, to: string, from: string, operations: any[] }[]}
|
|
41
|
+
*/
|
|
42
|
+
function planStorelessRun(migrations, present, context) {
|
|
43
|
+
if (!Array.isArray(migrations))
|
|
44
|
+
throw new TypeError('a document migration needs the ordered migration list');
|
|
45
|
+
const plans = [];
|
|
46
|
+
for (const migration of migrations) {
|
|
47
|
+
checkMigrationDocument(migration);
|
|
48
|
+
// the whole refusal happens here, before the first document: a
|
|
49
|
+
// half-applied migration is the one outcome a runner without a
|
|
50
|
+
// transaction can never take back
|
|
51
|
+
for (let i = 0; i < migration.steps.length; i++) {
|
|
52
|
+
const step = migration.steps[i];
|
|
53
|
+
if (PHYSICAL_STEP_KINDS.has(step.kind)) {
|
|
54
|
+
throw new DbCompileError('JD0023',
|
|
55
|
+
`migration '${migration.id}' step ${i} (${step.kind}) needs a database: a `
|
|
56
|
+
+ 'document runner has no tables to change. Run this migration against a '
|
|
57
|
+
+ 'store, or split the physical steps out of it');
|
|
58
|
+
}
|
|
59
|
+
if (!DOCUMENT_STEP_KINDS.has(step.kind)) {
|
|
60
|
+
throw new DbCompileError('JD0023',
|
|
61
|
+
`migration '${migration.id}' step ${i} has no recognised kind`);
|
|
62
|
+
}
|
|
63
|
+
if (!present.has(step.collection)) {
|
|
64
|
+
throw new DbCompileError('JD0023',
|
|
65
|
+
`migration '${migration.id}' step ${i} (${step.kind}) names collection `
|
|
66
|
+
+ `'${step.collection}', which was not supplied`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
plans.push({
|
|
70
|
+
id: migration.id,
|
|
71
|
+
from: migration.from,
|
|
72
|
+
to: migration.to,
|
|
73
|
+
operations: migration.steps.map((step, i) => compileDocumentStep(step, i, {
|
|
74
|
+
migrationId: migration.id,
|
|
75
|
+
compileJslt: context.compileJslt,
|
|
76
|
+
compileQuery: context.compileQuery,
|
|
77
|
+
keys: Object.hasOwn(context.keys, step.collection) ? (context.keys[step.collection] ?? []) : [],
|
|
78
|
+
})),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return plans;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The shared option surface both surfaces read. */
|
|
85
|
+
function runContext(options) {
|
|
86
|
+
const runtime = resolveRuntime(options.runtime);
|
|
87
|
+
return {
|
|
88
|
+
batchSize: options.batchSize ?? 500,
|
|
89
|
+
onProgress: options.onProgress,
|
|
90
|
+
keys: options.keys ?? {},
|
|
91
|
+
compileJslt: options.compileJslt ?? compileJsltStylesheet,
|
|
92
|
+
compileQuery: options.compileQuery ?? compileJsonQuery,
|
|
93
|
+
assertionBounds: normalizeAssertionBounds(options.assertionBounds),
|
|
94
|
+
check: () => refuseCancelled({ signal: options.signal, deadline: options.deadline },
|
|
95
|
+
runtime.now, {
|
|
96
|
+
abortCode: 'JD2080', aborted: 'its next step', passed: 'its next step',
|
|
97
|
+
ran: 'no further step ran; a document migration leaves the documents it has '
|
|
98
|
+
+ 'already written where they are',
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The report both surfaces answer with, before its counts are filled. */
|
|
104
|
+
const emptyReport = (plans) => ({
|
|
105
|
+
applied: plans.map((plan) => plan.id),
|
|
106
|
+
skipped: [],
|
|
107
|
+
shape: plans.length > 0 ? plans[plans.length - 1].to : null,
|
|
108
|
+
counts: /** @type {Record<string, any>} */ ({}),
|
|
109
|
+
strategy: /** @type {Record<string, string>} */ ({}),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/** The per-collection counters a report carries. */
|
|
113
|
+
const countersFor = (report, collection) => {
|
|
114
|
+
if (!Object.hasOwn(report.counts, collection))
|
|
115
|
+
setObjectMember(report.counts, collection, { read: 0, transformed: 0, asserted: 0 });
|
|
116
|
+
return report.counts[collection];
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Migrate documents held in memory: `{ users: [...] }` in, the migrated
|
|
121
|
+
* `{ users: [...] }` out, alongside the report.
|
|
122
|
+
*
|
|
123
|
+
* The source is rewindable, so the steps run exactly as a Store runs
|
|
124
|
+
* them — every step over the whole collection, in step order — which is
|
|
125
|
+
* what makes this mode's answer, and its refusal, identical to the
|
|
126
|
+
* Store's for the same migration and the same documents.
|
|
127
|
+
*
|
|
128
|
+
* @param {Record<string, any[]>} collections - the documents, per collection
|
|
129
|
+
* @param {any[]} migrations - the ordered migration list
|
|
130
|
+
* @param {{
|
|
131
|
+
* batchSize?: number,
|
|
132
|
+
* onProgress?: Function,
|
|
133
|
+
* keys?: Record<string, string[]>,
|
|
134
|
+
* compileJslt?: Function,
|
|
135
|
+
* compileQuery?: Function,
|
|
136
|
+
* runtime?: any,
|
|
137
|
+
* signal?: AbortSignal,
|
|
138
|
+
* deadline?: number,
|
|
139
|
+
* }} [options]
|
|
140
|
+
* @returns {Promise<{ documents: Record<string, any[]>, report: any }>}
|
|
141
|
+
*/
|
|
142
|
+
export async function migrateDocuments(collections, migrations, options = {}) {
|
|
143
|
+
if (collections === null || typeof collections !== 'object' || Array.isArray(collections))
|
|
144
|
+
throw new TypeError('migrateDocuments needs { [collection]: document[] }');
|
|
145
|
+
for (const [name, documents] of Object.entries(collections)) {
|
|
146
|
+
if (!Array.isArray(documents))
|
|
147
|
+
throw new TypeError(`migrateDocuments needs an array for collection '${name}'`);
|
|
148
|
+
}
|
|
149
|
+
const context = runContext(options);
|
|
150
|
+
const plans = planStorelessRun(migrations, new Set(Object.keys(collections)), context);
|
|
151
|
+
context.check();
|
|
152
|
+
|
|
153
|
+
/** @type {Record<string, any[]>} */
|
|
154
|
+
const state = {};
|
|
155
|
+
for (const [name, documents] of Object.entries(collections)) setObjectMember(state, name, [...documents]);
|
|
156
|
+
const report = emptyReport(plans);
|
|
157
|
+
for (const name of Object.keys(state)) {
|
|
158
|
+
countersFor(report, name).read = state[name].length;
|
|
159
|
+
setObjectMember(report.strategy, name, 'materialized');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const plan of plans) {
|
|
163
|
+
for (const operation of plan.operations) {
|
|
164
|
+
context.check();
|
|
165
|
+
const documents = state[operation.collection];
|
|
166
|
+
const counters = countersFor(report, operation.collection);
|
|
167
|
+
if (operation.kind === 'jslt') {
|
|
168
|
+
for (let i = 0; i < documents.length; i++) {
|
|
169
|
+
documents[i] = operation.apply(documents[i], i);
|
|
170
|
+
counters.transformed++;
|
|
171
|
+
if ((i + 1) % context.batchSize === 0) {
|
|
172
|
+
context.onProgress?.({ migration: plan.id, collection: operation.collection,
|
|
173
|
+
transformed: counters.transformed });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (documents.length % context.batchSize !== 0) {
|
|
177
|
+
context.onProgress?.({ migration: plan.id, collection: operation.collection,
|
|
178
|
+
transformed: counters.transformed });
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (operation.fold !== null) {
|
|
183
|
+
// an associative aggregate: the same batches, combined
|
|
184
|
+
let accumulated = operation.fold.start();
|
|
185
|
+
for (let at = 0; at < documents.length; at += context.batchSize) {
|
|
186
|
+
context.check();
|
|
187
|
+
accumulated = operation.fold.combine(accumulated,
|
|
188
|
+
documents.slice(at, at + context.batchSize));
|
|
189
|
+
counters.asserted += Math.min(context.batchSize, documents.length - at);
|
|
190
|
+
context.onProgress?.({ migration: plan.id, collection: operation.collection,
|
|
191
|
+
asserted: counters.asserted });
|
|
192
|
+
}
|
|
193
|
+
operation.fold.finish(accumulated);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!operation.perDocument) {
|
|
197
|
+
// materializing: this mode already holds the collection, but the
|
|
198
|
+
// bound is what the caller was promised, so it is checked
|
|
199
|
+
const guard = createAssertionBoundGuard(context.assertionBounds, operation.collection,
|
|
200
|
+
`the assertion of migration '${plan.id}'`);
|
|
201
|
+
for (const document of documents) guard.admit(document);
|
|
202
|
+
operation.assert(documents);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// per-document: the same keyset-sized batches the Store asserts
|
|
206
|
+
// over, so a refusal counts the same violations it counts there
|
|
207
|
+
for (let at = 0; at < documents.length; at += context.batchSize) {
|
|
208
|
+
context.check();
|
|
209
|
+
const batch = documents.slice(at, at + context.batchSize);
|
|
210
|
+
operation.assert(batch);
|
|
211
|
+
counters.asserted += batch.length;
|
|
212
|
+
context.onProgress?.({ migration: plan.id, collection: operation.collection,
|
|
213
|
+
asserted: counters.asserted });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { documents: state, report };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Migrate documents the caller can only walk once, writing each out as
|
|
222
|
+
* it finishes. The source is consumed exactly once and nothing beyond
|
|
223
|
+
* one batch is held, so a collection larger than memory still migrates.
|
|
224
|
+
*
|
|
225
|
+
* **What a single pass cannot do.** The Store, and {@link migrateDocuments},
|
|
226
|
+
* run each step over the whole collection before the next step begins,
|
|
227
|
+
* so when two different steps would each refuse, the EARLIER step
|
|
228
|
+
* refuses first. One pass carries each batch through every step, so the
|
|
229
|
+
* later step can refuse first. Both refuse, with the same code and the
|
|
230
|
+
* same words; only which step is named can differ. A caller who needs
|
|
231
|
+
* that identity has a rewindable source and should use
|
|
232
|
+
* {@link migrateDocuments}.
|
|
233
|
+
*
|
|
234
|
+
* A cross-document assertion (`$count`, `$let`, `$distinct`, a nested
|
|
235
|
+
* `$for`) needs every document at once and so cannot run in one pass;
|
|
236
|
+
* it is refused here by name rather than silently buffering the
|
|
237
|
+
* collection.
|
|
238
|
+
*
|
|
239
|
+
* @param {Record<string, Iterable<any> | AsyncIterable<any>>} sources
|
|
240
|
+
* @param {any[]} migrations - the ordered migration list
|
|
241
|
+
* @param {{
|
|
242
|
+
* write: (collection: string, document: any) => any,
|
|
243
|
+
* batchSize?: number,
|
|
244
|
+
* onProgress?: Function,
|
|
245
|
+
* keys?: Record<string, string[]>,
|
|
246
|
+
* compileJslt?: Function,
|
|
247
|
+
* compileQuery?: Function,
|
|
248
|
+
* runtime?: any,
|
|
249
|
+
* signal?: AbortSignal,
|
|
250
|
+
* deadline?: number,
|
|
251
|
+
* }} options
|
|
252
|
+
* @returns {Promise<any>} the report
|
|
253
|
+
*/
|
|
254
|
+
export async function streamDocuments(sources, migrations, options) {
|
|
255
|
+
if (sources === null || typeof sources !== 'object' || Array.isArray(sources))
|
|
256
|
+
throw new TypeError('streamDocuments needs { [collection]: iterable }');
|
|
257
|
+
if (options === null || typeof options !== 'object' || typeof options.write !== 'function')
|
|
258
|
+
throw new TypeError('streamDocuments needs { write(collection, document) }');
|
|
259
|
+
const context = runContext(options);
|
|
260
|
+
const plans = planStorelessRun(migrations, new Set(Object.keys(sources)), context);
|
|
261
|
+
|
|
262
|
+
// the second refusal a single pass owes before it reads anything: a
|
|
263
|
+
// MATERIALIZING assertion. An associative aggregate is not one — its
|
|
264
|
+
// batches combine, so a single pass answers it exactly.
|
|
265
|
+
for (const plan of plans) {
|
|
266
|
+
for (const operation of plan.operations) {
|
|
267
|
+
if (operation.kind === 'query' && operation.strategy === 'materialize') {
|
|
268
|
+
operation.fail('a cross-document assertion needs every document of '
|
|
269
|
+
+ `'${operation.collection}' at once, which a single pass does not hold — `
|
|
270
|
+
+ 'migrate this collection from a rewindable source');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
context.check();
|
|
275
|
+
|
|
276
|
+
const report = emptyReport(plans);
|
|
277
|
+
// one collection's whole ordered pipeline: every migration's steps
|
|
278
|
+
// over it, in order, applied to each document as it passes
|
|
279
|
+
/** @type {Record<string, any[]>} */
|
|
280
|
+
const pipeline = Object.create(null);
|
|
281
|
+
for (const plan of plans) {
|
|
282
|
+
for (const operation of plan.operations) {
|
|
283
|
+
(pipeline[operation.collection] ??= []).push({
|
|
284
|
+
migration: plan.id, operation, accumulated: undefined, folded: false,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const name of Object.keys(sources)) {
|
|
290
|
+
const counters = countersFor(report, name);
|
|
291
|
+
setObjectMember(report.strategy, name, 'streamed');
|
|
292
|
+
const stages = pipeline[name] ?? [];
|
|
293
|
+
/** @type {any[]} */
|
|
294
|
+
let batch = [];
|
|
295
|
+
const flush = async () => {
|
|
296
|
+
if (batch.length === 0) return;
|
|
297
|
+
context.check();
|
|
298
|
+
for (const stage of stages) {
|
|
299
|
+
if (stage.operation.kind === 'jslt') {
|
|
300
|
+
for (let i = 0; i < batch.length; i++) {
|
|
301
|
+
batch[i] = stage.operation.apply(batch[i], counters.read - batch.length + i);
|
|
302
|
+
counters.transformed++;
|
|
303
|
+
}
|
|
304
|
+
context.onProgress?.({ migration: stage.migration, collection: name,
|
|
305
|
+
transformed: counters.transformed });
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (stage.operation.fold !== null) {
|
|
309
|
+
stage.accumulated = stage.operation.fold.combine(
|
|
310
|
+
stage.accumulated ?? stage.operation.fold.start(), batch);
|
|
311
|
+
stage.folded = true;
|
|
312
|
+
}
|
|
313
|
+
else stage.operation.assert(batch);
|
|
314
|
+
counters.asserted += batch.length;
|
|
315
|
+
context.onProgress?.({ migration: stage.migration, collection: name,
|
|
316
|
+
asserted: counters.asserted });
|
|
317
|
+
}
|
|
318
|
+
for (const document of batch) await options.write(name, document);
|
|
319
|
+
batch = [];
|
|
320
|
+
};
|
|
321
|
+
for await (const document of sources[name]) {
|
|
322
|
+
counters.read++;
|
|
323
|
+
batch.push(document);
|
|
324
|
+
if (batch.length >= context.batchSize) await flush();
|
|
325
|
+
}
|
|
326
|
+
await flush();
|
|
327
|
+
// a fold's verdict is on the TOTAL, so it is reached once the source
|
|
328
|
+
// is spent — an empty source still folds, to its aggregate's identity
|
|
329
|
+
for (const stage of stages) {
|
|
330
|
+
if (stage.operation.fold === null) continue;
|
|
331
|
+
stage.operation.fold.finish(stage.folded ? stage.accumulated : stage.operation.fold.start());
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return report;
|
|
335
|
+
}
|