@jarenjs/db 0.56.0 → 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 +393 -56
- package/README.md +585 -53
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +293 -45
- package/docs/LIVE-FORMAT.md +122 -14
- package/docs/MIGRATION-FORMAT.md +142 -17
- package/docs/MODEL-FORMAT.md +744 -64
- package/package.json +21 -7
- 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 +218 -45
- package/src/cli.js +165 -59
- package/src/cursor.js +411 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +102 -8
- 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 +243 -11
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +448 -61
- 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 +17 -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.js +339 -48
- package/src/entity.js +20 -22
- package/src/errors.js +422 -19
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +46 -17
- package/src/introspect.js +583 -0
- package/src/jobs.js +843 -107
- package/src/json-bytes.js +58 -0
- package/src/maintenance.js +175 -0
- package/src/migrate.js +248 -181
- package/src/model.js +68 -0
- package/src/plan.js +1119 -138
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1634 -323
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1505 -264
- package/src/tracker.js +203 -29
- package/src/udf.js +88 -7
- package/types/index.d.ts +1097 -25
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +69 -2
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +25 -3
- package/types/wasm.d.ts +14 -0
package/src/capture.js
CHANGED
|
@@ -32,13 +32,23 @@
|
|
|
32
32
|
import { createJSONPatch } from '@jarenjs/json/patch';
|
|
33
33
|
import { encodeJSONPointerSegment, decodeJSONPointerSegment } from '@jarenjs/json/pointer';
|
|
34
34
|
|
|
35
|
-
import { DbCompileError, DbRuntimeError } from './errors.js';
|
|
35
|
+
import { DbCompileError, DbRuntimeError, wrapDriverError } from './errors.js';
|
|
36
36
|
import { chain, attempt } from './driver.js';
|
|
37
|
+
import { createCursor, drainPage, utf8Length, PAGE_LIMIT_DEFAULT } from './cursor.js';
|
|
37
38
|
|
|
38
39
|
/** The persisted change log (LIVE-FORMAT §5). */
|
|
39
40
|
export const CHANGES_TABLE = '_jaren_changes';
|
|
41
|
+
/**
|
|
42
|
+
* The log's durable state: one row holding the highest sequence ever
|
|
43
|
+
* allocated for this file, so the high watermark survives a log that
|
|
44
|
+
* retention or maintenance has emptied (LIVE-FORMAT §5).
|
|
45
|
+
*/
|
|
46
|
+
export const CHANGES_STATE_TABLE = '_jaren_changes_state';
|
|
40
47
|
export const DEFAULT_RETENTION = 1000;
|
|
41
48
|
|
|
49
|
+
/** A driver failure met by the change reader, classified — never raw. */
|
|
50
|
+
const captureFailure = (error) => wrapDriverError(error, { docPath: '/capture' });
|
|
51
|
+
|
|
42
52
|
//#region the binary changeset parser
|
|
43
53
|
|
|
44
54
|
const OP_INSERT = 18;
|
|
@@ -316,11 +326,18 @@ export function translateOperations(connection, shapes, operations) {
|
|
|
316
326
|
/**
|
|
317
327
|
* @param {{ connection: any, shapes: Map<string, TableShape>,
|
|
318
328
|
* mode: 'session' | 'journal',
|
|
319
|
-
* log: boolean, retention: number
|
|
329
|
+
* log: boolean, retention: number,
|
|
330
|
+
* now?: () => number,
|
|
331
|
+
* bracket?: (fn: () => any) => any }} options - `now` is the clock a
|
|
332
|
+
* delivery is stamped with (the store's runtime record); the platform's
|
|
333
|
+
* when absent. `bracket` runs the first-open DDL (the store's immediate
|
|
334
|
+
* transaction on a writable store); bare when absent
|
|
320
335
|
* @returns {any}
|
|
321
336
|
*/
|
|
322
337
|
export function createCaptureEngine(options) {
|
|
323
338
|
const { connection, shapes, mode } = options;
|
|
339
|
+
const clock = options.now ?? Date.now;
|
|
340
|
+
const bracket = options.bracket ?? ((/** @type {() => any} */ fn) => fn());
|
|
324
341
|
const dialect = connection.dialect;
|
|
325
342
|
const q = dialect.quoteIdentifier;
|
|
326
343
|
if (options.log && !(Number.isInteger(options.retention) && options.retention >= 1)) {
|
|
@@ -331,6 +348,9 @@ export function createCaptureEngine(options) {
|
|
|
331
348
|
|
|
332
349
|
/** @type {Set<Function>} */
|
|
333
350
|
const observers = new Set();
|
|
351
|
+
// the last sequence THIS process delivered: the per-process sequence
|
|
352
|
+
// when no log is kept, and otherwise the file's allocation read back
|
|
353
|
+
// from the durable row — never an answer to a watermark question
|
|
334
354
|
let seq = 0;
|
|
335
355
|
let depth = 0;
|
|
336
356
|
/** @type {any} */
|
|
@@ -350,33 +370,70 @@ export function createCaptureEngine(options) {
|
|
|
350
370
|
{ name: 'patch', type: dialect.typeFor('string', 'key') },
|
|
351
371
|
],
|
|
352
372
|
}),
|
|
353
|
-
// the
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
373
|
+
// the durable high watermark: a singleton row the file keeps, so an
|
|
374
|
+
// emptied log still knows the highest sequence it ever allocated
|
|
375
|
+
createState: dialect.ddl.createPlainTable({
|
|
376
|
+
table: CHANGES_STATE_TABLE,
|
|
377
|
+
columns: [
|
|
378
|
+
{ name: 'id', type: dialect.typeFor('integer', 'key'), primaryKey: true },
|
|
379
|
+
{ name: 'high', type: dialect.typeFor('integer', 'key') },
|
|
380
|
+
],
|
|
381
|
+
}),
|
|
382
|
+
// idempotent: an existing file is upgraded ONCE from its surviving
|
|
383
|
+
// maximum (a file that never held a row seeds 0); a second open, or
|
|
384
|
+
// a second store on the same file, changes nothing
|
|
385
|
+
hasState: `SELECT 1 AS ${q('present')} FROM ${q(CHANGES_STATE_TABLE)} WHERE ${q('id')} = 1`,
|
|
386
|
+
seedState: `INSERT INTO ${q(CHANGES_STATE_TABLE)} (${q('id')}, ${q('high')}) `
|
|
387
|
+
+ `SELECT 1, (SELECT COALESCE(MAX(${q('seq')}), 0) FROM ${q(CHANGES_TABLE)}) `
|
|
388
|
+
+ `WHERE NOT EXISTS (SELECT 1 FROM ${q(CHANGES_STATE_TABLE)} WHERE ${q('id')} = 1)`,
|
|
389
|
+
// the sequence is allocated by ONE statement against the durable
|
|
390
|
+
// row, inside the write's own transaction: the next value is one
|
|
391
|
+
// past the higher of the durable watermark and whatever survives in
|
|
392
|
+
// the log (so a state row that went missing can never lower it), and
|
|
393
|
+
// a rolled-back write takes its allocation back with the row. A
|
|
394
|
+
// process counter seeded once at open collided with another store's
|
|
395
|
+
// writes to the same file; the durable row is the file's fact, so two
|
|
396
|
+
// stores allocate distinct, increasing sequences.
|
|
397
|
+
allocate: `INSERT INTO ${q(CHANGES_STATE_TABLE)} (${q('id')}, ${q('high')}) `
|
|
398
|
+
+ `VALUES (1, (SELECT COALESCE(MAX(${q('seq')}), 0) + 1 FROM ${q(CHANGES_TABLE)})) `
|
|
399
|
+
+ `ON CONFLICT (${q('id')}) DO UPDATE SET ${q('high')} = CASE `
|
|
400
|
+
+ `WHEN ${q(CHANGES_STATE_TABLE)}.${q('high')} + 1 > ${dialect.excludedRef(q('high'))} `
|
|
401
|
+
+ `THEN ${q(CHANGES_STATE_TABLE)}.${q('high')} + 1 ELSE ${dialect.excludedRef(q('high'))} END `
|
|
402
|
+
+ `RETURNING ${q('high')} AS ${q('seq')}`,
|
|
357
403
|
insert: `INSERT INTO ${q(CHANGES_TABLE)} `
|
|
358
404
|
+ `(${['seq', 'at', 'source', 'patch'].map(q).join(', ')}) `
|
|
359
|
-
+ `VALUES (
|
|
360
|
-
+ `${[1, 2, 3].map((i) => dialect.parameterRef(i, 'v')).join(', ')}) `
|
|
361
|
-
+ `RETURNING ${q('seq')} AS ${q('seq')}`,
|
|
405
|
+
+ `VALUES (${[1, 2, 3, 4].map((i) => dialect.parameterRef(i, 'v')).join(', ')})`,
|
|
362
406
|
prune: `DELETE FROM ${q(CHANGES_TABLE)} WHERE ${q('seq')} <= ${dialect.parameterRef(1, 'v')}`,
|
|
363
|
-
highest: `SELECT MAX(${q('seq')}) AS ${q('n')} FROM ${q(CHANGES_TABLE)}`,
|
|
364
407
|
read: `SELECT ${['seq', 'at', 'source', 'patch'].map(q).join(', ')} `
|
|
365
408
|
+ `FROM ${q(CHANGES_TABLE)} WHERE ${q('seq')} > ${dialect.parameterRef(1, 'v')} `
|
|
366
409
|
+ `ORDER BY ${q('seq')}`,
|
|
410
|
+
// the bounded read: a page of records after a cursor, one more than
|
|
411
|
+
// the page so `hasMore` is a fact and not a guess
|
|
412
|
+
readPage: `SELECT ${['seq', 'at', 'source', 'patch'].map(q).join(', ')} `
|
|
413
|
+
+ `FROM ${q(CHANGES_TABLE)} WHERE ${q('seq')} > ${dialect.parameterRef(1, 'v')} `
|
|
414
|
+
+ `ORDER BY ${q('seq')} LIMIT ${dialect.parameterRef(2, 'v')}`,
|
|
415
|
+
// the two watermarks are the FILE's facts: the earliest surviving
|
|
416
|
+
// row, and the durable high (falling back to the surviving maximum
|
|
417
|
+
// only for a file whose state row is absent — never to this process)
|
|
418
|
+
bounds: `SELECT (SELECT MIN(${q('seq')}) FROM ${q(CHANGES_TABLE)}) AS ${q('lo')}, `
|
|
419
|
+
+ `COALESCE((SELECT ${q('high')} FROM ${q(CHANGES_STATE_TABLE)} WHERE ${q('id')} = 1), `
|
|
420
|
+
+ `(SELECT MAX(${q('seq')}) FROM ${q(CHANGES_TABLE)}), 0) AS ${q('hi')}`,
|
|
367
421
|
} : null;
|
|
368
422
|
|
|
369
423
|
const ready = logStatements === null
|
|
370
424
|
? null
|
|
371
|
-
|
|
425
|
+
// the seed is written only when the row is absent: a read-only open
|
|
426
|
+
// of an already-upgraded file reads the row and writes nothing
|
|
427
|
+
: chain(attempt(() => bracket(() => chain(connection.exec(logStatements.create), () =>
|
|
428
|
+
chain(connection.exec(logStatements.createState), () =>
|
|
429
|
+
chain(connection.prepare(logStatements.hasState), (probe) => chain(probe.get([]), (row) =>
|
|
430
|
+
(row === undefined || row === null
|
|
431
|
+
? chain(connection.prepare(logStatements.seedState), (statement) => statement.run([]))
|
|
432
|
+
: null)))))),
|
|
433
|
+
(error) => new DbCompileError('JD0002',
|
|
372
434
|
`the change log table could not be created (${error?.message ?? String(error)}) — `
|
|
373
435
|
+ 'a read-only store creates nothing; open it read-write once, or without capture.log',
|
|
374
|
-
'/capture', error)), () =>
|
|
375
|
-
chain(connection.prepare(logStatements.highest), (statement) =>
|
|
376
|
-
chain(statement.get([]), (row) => {
|
|
377
|
-
seq = Number(row?.n ?? 0) || 0;
|
|
378
|
-
return null;
|
|
379
|
-
})));
|
|
436
|
+
'/capture', error)), () => null);
|
|
380
437
|
|
|
381
438
|
/** A patch value must be JSON: a `{ ...doc, m: undefined }` write
|
|
382
439
|
* stores the member ABSENT, and the journal must record the JSON
|
|
@@ -457,11 +514,13 @@ export function createCaptureEngine(options) {
|
|
|
457
514
|
|
|
458
515
|
const persist = (patch, at) => {
|
|
459
516
|
if (logStatements === null || patch.length === 0) return null;
|
|
460
|
-
return chain(connection.prepare(logStatements.
|
|
461
|
-
chain(
|
|
517
|
+
return chain(connection.prepare(logStatements.allocate), (allocate) =>
|
|
518
|
+
chain(allocate.get([]), (row) => {
|
|
462
519
|
seq = Number(row.seq);
|
|
463
|
-
return chain(connection.prepare(logStatements.
|
|
464
|
-
chain(
|
|
520
|
+
return chain(connection.prepare(logStatements.insert), (insert) =>
|
|
521
|
+
chain(insert.run([seq, at, mode, JSON.stringify(patch)]), () =>
|
|
522
|
+
chain(connection.prepare(logStatements.prune), (prune) =>
|
|
523
|
+
chain(prune.run([seq - options.retention]), () => null))));
|
|
465
524
|
}));
|
|
466
525
|
};
|
|
467
526
|
|
|
@@ -502,12 +561,15 @@ export function createCaptureEngine(options) {
|
|
|
502
561
|
* Run `fn` inside the capture scope: the OUTERMOST scope opens a
|
|
503
562
|
* session (or journal buffer) plus a transaction, translates and
|
|
504
563
|
* persists inside it, and delivers to observers after commit.
|
|
564
|
+
*
|
|
565
|
+
* The transaction's scope arguments are FORWARDED to `fn`: the store
|
|
566
|
+
* pins a transaction view to the exact scope its callback runs in,
|
|
567
|
+
* and with capture that is the scope this wrap opens, not the one
|
|
568
|
+
* around it. Callers that need no scope simply ignore the arguments.
|
|
505
569
|
*/
|
|
506
570
|
const wrap = (fn) => {
|
|
507
571
|
if (depth > 0) return fn();
|
|
508
572
|
depth = 1;
|
|
509
|
-
if (mode === 'session') session = connection.session();
|
|
510
|
-
else journal = [];
|
|
511
573
|
const cleanupFailure = () => {
|
|
512
574
|
depth = 0;
|
|
513
575
|
if (session !== null) {
|
|
@@ -518,11 +580,13 @@ export function createCaptureEngine(options) {
|
|
|
518
580
|
};
|
|
519
581
|
let outcome;
|
|
520
582
|
try {
|
|
521
|
-
|
|
522
|
-
|
|
583
|
+
if (mode === 'session') session = connection.session();
|
|
584
|
+
else journal = [];
|
|
585
|
+
outcome = connection.transaction((...scopeArgs) =>
|
|
586
|
+
chain(fn(...scopeArgs), (result) =>
|
|
523
587
|
chain(collect(), (patch) => {
|
|
524
588
|
if (patch.length === 0) return { result, delivery: null };
|
|
525
|
-
const at =
|
|
589
|
+
const at = clock();
|
|
526
590
|
return chain(persist(patch, at), () => ({
|
|
527
591
|
result,
|
|
528
592
|
delivery: {
|
|
@@ -582,11 +646,28 @@ export function createCaptureEngine(options) {
|
|
|
582
646
|
return outcome;
|
|
583
647
|
};
|
|
584
648
|
|
|
649
|
+
/**
|
|
650
|
+
* The journal's checkpoint pair for NAMED partial rollback
|
|
651
|
+
* (MODEL-FORMAT §5.2). A named `SAVEPOINT` takes a mark; a
|
|
652
|
+
* `ROLLBACK TO` truncates the buffer to it, so records the engine
|
|
653
|
+
* undid vanish from the commit's patch exactly as a session drops
|
|
654
|
+
* rows undone by `ROLLBACK TO`. Session mode needs neither half —
|
|
655
|
+
* SQLite's own changeset already excludes the undone rows — and
|
|
656
|
+
* answers `null` so the caller stores nothing.
|
|
657
|
+
*/
|
|
658
|
+
const mark = () => (mode === 'journal' ? journal.length : null);
|
|
659
|
+
/** @param {number | null} at - a value {@link mark} answered */
|
|
660
|
+
const truncate = (at) => {
|
|
661
|
+
if (mode === 'journal' && at !== null && journal.length > at) journal.length = at;
|
|
662
|
+
};
|
|
663
|
+
|
|
585
664
|
return {
|
|
586
665
|
mode,
|
|
587
666
|
ready,
|
|
588
667
|
wrap,
|
|
589
668
|
nest,
|
|
669
|
+
mark,
|
|
670
|
+
truncate,
|
|
590
671
|
record,
|
|
591
672
|
observe(fn) {
|
|
592
673
|
if (typeof fn !== 'function')
|
|
@@ -594,29 +675,121 @@ export function createCaptureEngine(options) {
|
|
|
594
675
|
observers.add(fn);
|
|
595
676
|
return () => observers.delete(fn);
|
|
596
677
|
},
|
|
678
|
+
/**
|
|
679
|
+
* Read the persisted log forward from `after` — EVERY surviving
|
|
680
|
+
* record, in one array, with no bound and no watermark: a
|
|
681
|
+
* reconnecting consumer whose cursor fell below the retention floor
|
|
682
|
+
* receives the surviving suffix and cannot tell it from the whole.
|
|
683
|
+
* Kept for its callers; `changes.page()` is the bounded reader that
|
|
684
|
+
* reports the gap instead (LIVE-FORMAT §5).
|
|
685
|
+
* @param {number} after - the last seq seen
|
|
686
|
+
*/
|
|
597
687
|
changesSince(after) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
'the change log is not enabled — open the store with capture.log');
|
|
601
|
-
}
|
|
602
|
-
if (typeof after !== 'number' || !Number.isFinite(after)) {
|
|
603
|
-
throw new TypeError(`changesSince(after) takes the last seq seen as a number, got ${
|
|
604
|
-
after === undefined ? 'undefined' : JSON.stringify(after)}`);
|
|
605
|
-
}
|
|
688
|
+
requireLog('changesSince');
|
|
689
|
+
requireCursor(after, 'changesSince');
|
|
606
690
|
return chain(connection.prepare(logStatements.read), (statement) =>
|
|
607
|
-
chain(statement.all([after]), (rows) => rows.map(
|
|
608
|
-
const patch = JSON.parse(row.patch);
|
|
609
|
-
// the same record shape observers receive: `collections` too
|
|
610
|
-
return {
|
|
611
|
-
seq: Number(row.seq),
|
|
612
|
-
at: Number(row.at),
|
|
613
|
-
source: String(row.source),
|
|
614
|
-
collections: collectionsOf(patch),
|
|
615
|
-
patch,
|
|
616
|
-
};
|
|
617
|
-
})));
|
|
691
|
+
chain(attempt(() => statement.all([after]), captureFailure), (rows) => rows.map(recordOf)));
|
|
618
692
|
},
|
|
693
|
+
/** Whether the persisted log exists — what decides whether the
|
|
694
|
+
* bounded reader is offered at all. */
|
|
695
|
+
logged: logStatements !== null,
|
|
696
|
+
bounds: () => readBounds(),
|
|
697
|
+
page: (options) => readPage(options),
|
|
619
698
|
};
|
|
699
|
+
|
|
700
|
+
/** @param {string} member */
|
|
701
|
+
function requireLog(member) {
|
|
702
|
+
if (logStatements !== null) return;
|
|
703
|
+
throw new DbRuntimeError('JD2051',
|
|
704
|
+
`${member}: the change log is not enabled — open the store with capture.log`);
|
|
705
|
+
}
|
|
706
|
+
/** @param {any} after @param {string} member */
|
|
707
|
+
function requireCursor(after, member) {
|
|
708
|
+
if (typeof after === 'number' && Number.isFinite(after)) return;
|
|
709
|
+
throw new TypeError(`${member} takes the last seq seen as a number (after), got ${
|
|
710
|
+
after === undefined ? 'undefined' : JSON.stringify(after)}`);
|
|
711
|
+
}
|
|
712
|
+
/** The record shape observers receive, `collections` included. */
|
|
713
|
+
function recordOf(row) {
|
|
714
|
+
const patch = JSON.parse(row.patch);
|
|
715
|
+
return {
|
|
716
|
+
seq: Number(row.seq),
|
|
717
|
+
at: Number(row.at),
|
|
718
|
+
source: String(row.source),
|
|
719
|
+
collections: collectionsOf(patch),
|
|
720
|
+
patch,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* The log's two watermarks: the earliest surviving sequence (`null`
|
|
725
|
+
* when nothing survives) and the highest ever allocated, read from the
|
|
726
|
+
* durable state row. Both are the FILE's facts, never this process's:
|
|
727
|
+
* an emptied log, a reopened file and a second store over the same
|
|
728
|
+
* file all answer the same high watermark, so a stale consumer meets a
|
|
729
|
+
* reset rather than a plausible empty history.
|
|
730
|
+
*/
|
|
731
|
+
function readBounds() {
|
|
732
|
+
requireLog('changes.bounds');
|
|
733
|
+
return chain(connection.prepare(logStatements.bounds), (statement) =>
|
|
734
|
+
chain(attempt(() => statement.get([]), captureFailure), (row) => ({
|
|
735
|
+
earliestAvailable: row?.lo === null || row?.lo === undefined ? null : Number(row.lo),
|
|
736
|
+
highWatermark: Number(row?.hi ?? 0),
|
|
737
|
+
})));
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* One bounded page of the log after `after`: never more than `limit`
|
|
741
|
+
* records or `maxBytes` serialised patch bytes (the one page drain,
|
|
742
|
+
* cursor.js — a record larger than `maxBytes` is its `JD2074`, the
|
|
743
|
+
* cursor not advanced), `hasMore` by one peek, `signal` honoured at a
|
|
744
|
+
* record boundary. The watermarks are read AFTER the rows: a floor
|
|
745
|
+
* that rose during the read can only make the reset verdict stricter,
|
|
746
|
+
* never let a pruned gap pass as a continuation. A cursor below the
|
|
747
|
+
* floor — the record after `after` no longer survives — is a TOTAL
|
|
748
|
+
* refusal: `resetRequired: true`, no items, no `next`, because a
|
|
749
|
+
* partial suffix beside a reset flag invites a consumer to use both.
|
|
750
|
+
* @param {{ after: number, limit?: number, maxBytes?: number | null,
|
|
751
|
+
* signal?: AbortSignal }} options
|
|
752
|
+
*/
|
|
753
|
+
function readPage(options) {
|
|
754
|
+
requireLog('changes.page');
|
|
755
|
+
const after = options?.after;
|
|
756
|
+
requireCursor(after, 'changes.page');
|
|
757
|
+
const limit = options?.limit ?? PAGE_LIMIT_DEFAULT;
|
|
758
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
759
|
+
throw new TypeError('changes.page: limit must be a positive integer');
|
|
760
|
+
const declaredBytes = options?.maxBytes;
|
|
761
|
+
const maxBytes = declaredBytes === undefined || declaredBytes === null || declaredBytes === Infinity
|
|
762
|
+
? null : declaredBytes;
|
|
763
|
+
if (maxBytes !== null && !(Number.isSafeInteger(maxBytes) && maxBytes >= 1))
|
|
764
|
+
throw new TypeError('changes.page: maxBytes must be a positive integer, or Infinity for no byte bound');
|
|
765
|
+
const deadline = options?.deadline;
|
|
766
|
+
if (deadline !== undefined && (typeof deadline !== 'number' || !Number.isFinite(deadline)))
|
|
767
|
+
throw new TypeError('changes.page: deadline is an epoch-millisecond number');
|
|
768
|
+
// the deadline is read against the store's clock at every record
|
|
769
|
+
// boundary, as on every other page (JD2075)
|
|
770
|
+
const cursor = createCursor({ streaming: 'row', barrier: null, signal: options?.signal, deadline, now: clock,
|
|
771
|
+
open: () => chain(connection.prepare(logStatements.readPage),
|
|
772
|
+
(statement) => statement.iterate([after, limit + 1])),
|
|
773
|
+
wrap: captureFailure,
|
|
774
|
+
items: (row) => [{ record: recordOf(row), bytes: utf8Length(String(row.patch)) }] });
|
|
775
|
+
return drainPage(cursor, {
|
|
776
|
+
limit, maxBytes, after,
|
|
777
|
+
sizeOf: (item) => item.bytes,
|
|
778
|
+
continuationOf: (item) => item.record.seq,
|
|
779
|
+
}).then((page) => chain(readBounds(), (bounds) => {
|
|
780
|
+
const first = bounds.earliestAvailable ?? bounds.highWatermark + 1;
|
|
781
|
+
if (after + 1 < first) {
|
|
782
|
+
return { items: [], ...bounds, hasMore: false, resetRequired: true };
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
items: page.items.map((item) => item.record),
|
|
786
|
+
next: page.continuation ?? after,
|
|
787
|
+
...bounds,
|
|
788
|
+
hasMore: page.hasMore,
|
|
789
|
+
resetRequired: false,
|
|
790
|
+
};
|
|
791
|
+
}));
|
|
792
|
+
}
|
|
620
793
|
}
|
|
621
794
|
|
|
622
795
|
//#endregion
|