@jarenjs/db 0.66.1 → 0.72.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/ARCHITECTURE.md +19 -0
- package/README.md +44 -4
- package/docs/JOBS-FORMAT.md +14 -1
- package/docs/LIVE-FORMAT.md +47 -6
- package/docs/MIGRATION-FORMAT.md +45 -26
- package/docs/MODEL-FORMAT.md +8 -0
- package/docs/REPLICATION-FORMAT.md +208 -0
- package/package.json +4 -4
- package/schemas/jaren-replication-snapshot.draft-07.schema.json +83 -0
- package/schemas/jaren-replication-snapshot.schema.json +83 -0
- package/schemas/jaren-replication.draft-07.schema.json +82 -0
- package/schemas/jaren-replication.schema.json +82 -0
- package/src/capture.js +13 -3
- package/src/cli.js +89 -68
- package/src/cursor.js +11 -5
- package/src/dag-job.js +17 -12
- package/src/dialect.js +1 -1
- package/src/dialects/sqlite.js +1 -0
- package/src/document-files.js +76 -20
- package/src/document-steps.js +94 -120
- package/src/documents.js +24 -6
- package/src/drivers/node.js +1 -1
- package/src/errors.js +8 -0
- package/src/index.js +2 -0
- package/src/jobs.js +42 -0
- package/src/live-join.js +250 -0
- package/src/live-nested.js +120 -0
- package/src/live.js +18 -4
- package/src/logical-rows.js +90 -0
- package/src/migrate.js +44 -5
- package/src/replication-format.js +115 -0
- package/src/replication.js +332 -0
- package/src/store.js +73 -14
- package/types/index.d.ts +94 -10
- package/types/node.d.ts +9 -3
- package/types/typed.d.ts +3 -2
package/src/store.js
CHANGED
|
@@ -29,16 +29,21 @@ import { chain, toPromise, isThenable, attempt } from './driver.js';
|
|
|
29
29
|
import { planCollection, planEntity, planJoinTable, verifyShape } from './ddl.js';
|
|
30
30
|
import { translatePatch } from './patch-sql.js';
|
|
31
31
|
import { createQueryEngine, createQueryState, createEntityQueryEngine, createLoadEngine } from './query.js';
|
|
32
|
-
import { admitCursor, admitSyncCursor } from './cursor.js';
|
|
32
|
+
import { admitCursor, admitSyncCursor, createCursor, drainPage, utf8Length } from './cursor.js';
|
|
33
33
|
import { refuseUnsupportedPragmaKeys, resolvePragmaRequests, configurePragmas } from './pragmas.js';
|
|
34
34
|
import { createMaintenance } from './maintenance.js';
|
|
35
35
|
import { createBackup } from './backup.js';
|
|
36
36
|
import { normalizeProfile, assertProfileRoots } from './profile.js';
|
|
37
|
-
import { normalizeEntities, explainMapping } from './model.js';
|
|
37
|
+
import { normalizeEntities, explainMapping, joinTableRoots } from './model.js';
|
|
38
38
|
import { entityCore } from './entity.js';
|
|
39
39
|
import { createTracker, membershipKeys } from './tracker.js';
|
|
40
40
|
import { createCaptureEngine, DEFAULT_RETENTION } from './capture.js';
|
|
41
|
+
import { createReplicationEngine } from './replication.js';
|
|
42
|
+
import { REPLICATION_DEFAULTS } from './replication-format.js';
|
|
43
|
+
import { createLogicalRows } from './logical-rows.js';
|
|
44
|
+
import { shapeHash } from './migrate.js';
|
|
41
45
|
import { createLiveRegistry, classifyLiveQuery, LIVE_DEFAULTS } from './live.js';
|
|
46
|
+
import { classifyEntityLive } from './live-join.js';
|
|
42
47
|
import { normalizeEventTime } from './live-time.js';
|
|
43
48
|
import { createJobEngine } from './jobs.js';
|
|
44
49
|
import { introspectModel } from './introspect.js';
|
|
@@ -961,6 +966,9 @@ export function openStore(model, options) {
|
|
|
961
966
|
collections = normalizeModel(model, options.expressions);
|
|
962
967
|
entities = normalizeEntities(model);
|
|
963
968
|
mapping = entities.size > 0 ? explainMapping(model) : null;
|
|
969
|
+
if (options.replication !== undefined && [...collections.keys(), ...entities.keys()]
|
|
970
|
+
.some((name) => name.toLowerCase().startsWith('_jaren_replica')))
|
|
971
|
+
throw new DbCompileError('JD0060', 'replication reserves table names beginning with _jaren_replica');
|
|
964
972
|
if (collections.size === 0 && entities.size === 0) {
|
|
965
973
|
throw modelError('JD0005',
|
|
966
974
|
'the model must declare at least one collection or entity', '');
|
|
@@ -1439,10 +1447,13 @@ export function openStore(model, options) {
|
|
|
1439
1447
|
};
|
|
1440
1448
|
|
|
1441
1449
|
// ————— change capture (LIVE-FORMAT §§1–6) —————
|
|
1442
|
-
const
|
|
1450
|
+
const captureOption = options.capture ?? (options.replication === undefined ? undefined : true);
|
|
1451
|
+
const captureRequested = captureOption === true
|
|
1443
1452
|
? {}
|
|
1444
|
-
: (
|
|
1445
|
-
? null :
|
|
1453
|
+
: (captureOption === undefined || captureOption === false
|
|
1454
|
+
? null : captureOption);
|
|
1455
|
+
if (options.replication !== undefined && (captureRequested === null || readOnly))
|
|
1456
|
+
throw new TypeError('replication requires a writable store with capture enabled');
|
|
1446
1457
|
let captureMode = 'none';
|
|
1447
1458
|
if (captureRequested !== null) {
|
|
1448
1459
|
const wanted = captureRequested.mode ?? 'auto';
|
|
@@ -1468,6 +1479,9 @@ export function openStore(model, options) {
|
|
|
1468
1479
|
? (hasSessions ? 'session' : 'journal')
|
|
1469
1480
|
: wanted;
|
|
1470
1481
|
}
|
|
1482
|
+
if (options.replication !== undefined && captureMode === 'journal'
|
|
1483
|
+
&& Object.values(mapping?.entities ?? {}).some((entity) => entity.foreignKeys.some((fk) => fk.onDelete !== 'restrict')))
|
|
1484
|
+
throw new DbCompileError('JD0051', 'journal replication cannot capture cascading or set-null child relations; use session capture');
|
|
1471
1485
|
const captureShapes = new Map();
|
|
1472
1486
|
if (captureMode !== 'none') {
|
|
1473
1487
|
for (const [collectionName, plan] of plans) {
|
|
@@ -1525,6 +1539,7 @@ export function openStore(model, options) {
|
|
|
1525
1539
|
// a column upgrade; a read-only store creates nothing and takes
|
|
1526
1540
|
// no lock
|
|
1527
1541
|
const firstOpen = readOnly ? (fn) => fn() : (fn) => immediately(connection, fn);
|
|
1542
|
+
let replicationEngine = null;
|
|
1528
1543
|
const capture = captureMode === 'none' ? null : createCaptureEngine({
|
|
1529
1544
|
connection,
|
|
1530
1545
|
bracket: firstOpen,
|
|
@@ -1534,6 +1549,7 @@ export function openStore(model, options) {
|
|
|
1534
1549
|
|| (captureRequested.log !== undefined && captureRequested.log !== false),
|
|
1535
1550
|
retention: captureRequested.log?.retention ?? DEFAULT_RETENTION,
|
|
1536
1551
|
now: runtime.now,
|
|
1552
|
+
beforeCommit: (patch, context) => replicationEngine?.commit(patch, context),
|
|
1537
1553
|
});
|
|
1538
1554
|
// the capture scope around a write runs statements of its own
|
|
1539
1555
|
// (a session's changeset read, the journal's old-row read, the
|
|
@@ -1562,6 +1578,7 @@ export function openStore(model, options) {
|
|
|
1562
1578
|
const liveRegistry = capture === null ? null : createLiveRegistry({
|
|
1563
1579
|
maxQueries: options.live?.maxQueries ?? LIVE_DEFAULTS.maxQueries,
|
|
1564
1580
|
maxMaintained: options.live?.maxMaintained ?? LIVE_DEFAULTS.maxMaintained,
|
|
1581
|
+
maxBytes: options.live?.maxBytes ?? LIVE_DEFAULTS.maxBytes,
|
|
1565
1582
|
});
|
|
1566
1583
|
if (capture !== null) {
|
|
1567
1584
|
capture.observe((record) => /** @type {any} */ (liveRegistry).deliver(record));
|
|
@@ -1613,6 +1630,8 @@ export function openStore(model, options) {
|
|
|
1613
1630
|
classification,
|
|
1614
1631
|
execute: (doc, executeOptions) => core.execute(doc, executeOptions),
|
|
1615
1632
|
readRow: (token) => core.get(token),
|
|
1633
|
+
rowPosition: (token) => createLogicalRows({ connection, shapes: captureShapes, capture,
|
|
1634
|
+
collectionCore: coreFor, entityCore: entityCoreFor }).position(core.model.name, token),
|
|
1616
1635
|
keyOf: (doc) => String(extractKey(doc, core.model.keySegments,
|
|
1617
1636
|
core.model.key, core.model.name, core.model.docPath)),
|
|
1618
1637
|
}));
|
|
@@ -1655,13 +1674,24 @@ export function openStore(model, options) {
|
|
|
1655
1674
|
const sql = `SELECT ${columns.map(dialect.quoteIdentifier).join(', ')} `
|
|
1656
1675
|
+ `FROM ${dialect.quoteIdentifier(joinName)} `
|
|
1657
1676
|
+ `WHERE ${dialect.quoteIdentifier(own.column)} = ${dialect.parameterRef(1, 'v')}`;
|
|
1658
|
-
|
|
1659
|
-
|
|
1677
|
+
const boundedRows = () => {
|
|
1678
|
+
const { maxOperations = REPLICATION_DEFAULTS.maxOperations, maxBytes = REPLICATION_DEFAULTS.maxBytes } = options.replication;
|
|
1679
|
+
const cursor = createCursor({ streaming: 'row', barrier: null,
|
|
1680
|
+
open: () => chain(connection.prepare(`${sql} LIMIT ?`), (statement) => statement.iterate([keyParts[0], maxOperations + 1])),
|
|
1681
|
+
items: (row) => [row] });
|
|
1682
|
+
return chain(drainPage(cursor, { limit: maxOperations, maxBytes,
|
|
1683
|
+
sizeOf: (row) => utf8Length(JSON.stringify(row)), continuationOf: () => null }), (page) => {
|
|
1684
|
+
if (page.hasMore) throw new DbRuntimeError('JD2106', 'membership cascade exceeds replication capacity');
|
|
1685
|
+
return page.items;
|
|
1686
|
+
});
|
|
1687
|
+
};
|
|
1688
|
+
return chain(options.replication === undefined
|
|
1689
|
+
? chain(connection.prepare(sql), (statement) => statement.all([keyParts[0]])) : boundedRows(), (rows) => {
|
|
1660
1690
|
for (const row of rows) {
|
|
1661
1691
|
capture.record(joinName, columns.map((column) => row[column]), undefined, null);
|
|
1662
1692
|
}
|
|
1663
1693
|
return nextJoin(i + 1);
|
|
1664
|
-
})
|
|
1694
|
+
});
|
|
1665
1695
|
};
|
|
1666
1696
|
return nextJoin(0);
|
|
1667
1697
|
};
|
|
@@ -2170,23 +2200,27 @@ export function openStore(model, options) {
|
|
|
2170
2200
|
'live eventTime maintains a collection view — an entity document re-runs, '
|
|
2171
2201
|
+ 'so a watermark would describe nothing (LIVE-FORMAT §13)');
|
|
2172
2202
|
}
|
|
2173
|
-
const roots = collectEntityRoots(document, entities);
|
|
2203
|
+
const roots = collectEntityRoots(document, new Map([...entities, ...joinTableRoots(entities, mapping).entities]));
|
|
2174
2204
|
if (roots.size === 0) {
|
|
2175
2205
|
throw new TypeError(
|
|
2176
2206
|
'store.live takes an entity-root document — for a collection, '
|
|
2177
2207
|
+ 'use store.collection(name).live');
|
|
2178
2208
|
}
|
|
2209
|
+
const logicalRows = createLogicalRows({ connection, shapes: captureShapes, capture,
|
|
2210
|
+
collectionCore: coreFor, entityCore: entityCoreFor });
|
|
2179
2211
|
return closeOnRollback(liveRegistry.register({
|
|
2180
2212
|
name: [...roots].join('+'),
|
|
2181
2213
|
tables: roots,
|
|
2182
2214
|
document,
|
|
2183
2215
|
externals: liveOptions?.externals ?? {},
|
|
2184
2216
|
demanded: liveOptions?.mode,
|
|
2185
|
-
classification: {
|
|
2217
|
+
classification: liveOptions?.mode === 'rerun' ? {
|
|
2186
2218
|
strategy: 'rerun',
|
|
2187
|
-
reason: '
|
|
2188
|
-
},
|
|
2219
|
+
reason: 're-run mode was explicitly requested',
|
|
2220
|
+
} : classifyEntityLive(document, entities, mapping, operators),
|
|
2189
2221
|
execute: (doc, executeOptions) => entityEngine.execute(doc, executeOptions),
|
|
2222
|
+
readDependency: logicalRows.read,
|
|
2223
|
+
dependencyPosition: logicalRows.position,
|
|
2190
2224
|
readRow: null,
|
|
2191
2225
|
keyOf: null,
|
|
2192
2226
|
}));
|
|
@@ -2405,6 +2439,7 @@ export function openStore(model, options) {
|
|
|
2405
2439
|
checkpointsFor: (job) => {
|
|
2406
2440
|
const inner = jobsEngine.checkpointsFor(job);
|
|
2407
2441
|
return Object.freeze({
|
|
2442
|
+
inspect: (runId, nodeId) => gated(() => inner.inspect(runId, nodeId), 'a root checkpoint identity read'),
|
|
2408
2443
|
load: (runId) => gated(() => inner.load(runId), 'a root checkpoint read'),
|
|
2409
2444
|
save: (runId, nodeId, value) =>
|
|
2410
2445
|
gated(() => inner.save(runId, nodeId, value), 'a root checkpoint save'),
|
|
@@ -2425,6 +2460,7 @@ export function openStore(model, options) {
|
|
|
2425
2460
|
gated(() => jobsEngine.cancel(id, cancelOptions), 'a root job cancellation'),
|
|
2426
2461
|
(outcome) => chain(jobsEngine.settledLocally(id), () => outcome))),
|
|
2427
2462
|
requeue: lift((...args) => gated(() => jobsEngine.requeue(...args), 'a root job requeue')),
|
|
2463
|
+
reset: lift((...args) => gated(() => jobsEngine.reset(...args), 'a root job reset')),
|
|
2428
2464
|
sweep: lift((...args) => gated(() => jobsEngine.sweep(...args), 'a root job sweep')),
|
|
2429
2465
|
}),
|
|
2430
2466
|
/**
|
|
@@ -2754,6 +2790,7 @@ export function openStore(model, options) {
|
|
|
2754
2790
|
// member is ABSENT rather than a second way to close the
|
|
2755
2791
|
// raw connection under its own savepoint
|
|
2756
2792
|
close: override(undefined),
|
|
2793
|
+
replication: override(undefined),
|
|
2757
2794
|
// nor does it run maintenance: a checkpoint inside an open
|
|
2758
2795
|
// transaction is a no-op the engine answers quietly, and
|
|
2759
2796
|
// the other three are store-level operations — ABSENT here
|
|
@@ -2840,6 +2877,10 @@ export function openStore(model, options) {
|
|
|
2840
2877
|
checkpointsFor: (/** @type {any} */ job) => {
|
|
2841
2878
|
const inner = jobsEngine.checkpointsFor(job);
|
|
2842
2879
|
return Object.freeze({
|
|
2880
|
+
inspect: lift((/** @type {any} */ runId, /** @type {any} */ nodeId) => {
|
|
2881
|
+
requireScope(identity);
|
|
2882
|
+
return inner.inspect(runId, nodeId);
|
|
2883
|
+
}),
|
|
2843
2884
|
load: lift((/** @type {any} */ runId) => {
|
|
2844
2885
|
requireScope(identity);
|
|
2845
2886
|
return inner.load(runId);
|
|
@@ -3010,8 +3051,26 @@ export function openStore(model, options) {
|
|
|
3010
3051
|
});
|
|
3011
3052
|
}
|
|
3012
3053
|
return chain(capture === null ? null : capture.ready,
|
|
3013
|
-
() => chain(jobsEngine === null ? null : jobsEngine.ready,
|
|
3014
|
-
|
|
3054
|
+
() => chain(jobsEngine === null ? null : jobsEngine.ready, () => {
|
|
3055
|
+
if (options.replication !== undefined) {
|
|
3056
|
+
replicationEngine = createReplicationEngine({ connection, capture,
|
|
3057
|
+
config: options.replication, model: shapeHash(model), now: runtime.now, bracket: firstOpen,
|
|
3058
|
+
rows: createLogicalRows({ connection, shapes: captureShapes, capture,
|
|
3059
|
+
collectionCore: coreFor, entityCore: entityCoreFor, captureJoinDelete }),
|
|
3060
|
+
});
|
|
3061
|
+
store.replication = Object.freeze({
|
|
3062
|
+
frontier: lift(() => gated(() => replicationEngine.frontier())),
|
|
3063
|
+
page: lift((request) => topLevelTransaction(() => replicationEngine.page(request), request?.signal, undefined, 'immediate')),
|
|
3064
|
+
conflicts: lift((request) => gated(() => replicationEngine.conflicts(request), 'replication conflict read', request?.signal)),
|
|
3065
|
+
snapshot: lift((request) => topLevelTransaction(() => replicationEngine.snapshot(request), request?.signal, undefined, 'immediate')),
|
|
3066
|
+
reset: lift((snapshot, request) => replicationEngine.reset(snapshot, request,
|
|
3067
|
+
(fn) => topLevelTransaction(fn, request?.signal, createUnitOfWork(), 'immediate'))),
|
|
3068
|
+
apply: lift((envelope, request) => replicationEngine.apply(envelope, request,
|
|
3069
|
+
(fn) => topLevelTransaction(fn, request?.signal, createUnitOfWork(), 'immediate'))),
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
return chain(replicationEngine === null ? null : replicationEngine.ready, () => Object.freeze(store));
|
|
3073
|
+
}));
|
|
3015
3074
|
})))));
|
|
3016
3075
|
|
|
3017
3076
|
/**
|
package/types/index.d.ts
CHANGED
|
@@ -709,6 +709,7 @@ export interface SyncStore {
|
|
|
709
709
|
}
|
|
710
710
|
|
|
711
711
|
export interface Store {
|
|
712
|
+
readonly replication?: Replication;
|
|
712
713
|
readonly capabilities: StoreCapabilities;
|
|
713
714
|
readonly dialect: Dialect;
|
|
714
715
|
stats(): StoreStats;
|
|
@@ -867,7 +868,7 @@ export interface TransactionSyncStore extends SyncStore {
|
|
|
867
868
|
*/
|
|
868
869
|
export interface TransactionStore extends Omit<Store,
|
|
869
870
|
'close' | 'transaction' | 'sync' | 'checkpoint' | 'integrityCheck' | 'foreignKeyCheck' | 'optimize' | 'backupTo'
|
|
870
|
-
| 'jobs'> {
|
|
871
|
+
| 'jobs' | 'replication'> {
|
|
871
872
|
/** The transactional outbox (JOBS-FORMAT §3): no administration here —
|
|
872
873
|
* an admin operation is a root call. */
|
|
873
874
|
readonly jobs?: JobsApi;
|
|
@@ -978,7 +979,7 @@ export interface LiveEventTime {
|
|
|
978
979
|
|
|
979
980
|
export interface LiveMode {
|
|
980
981
|
readonly strategy: 'rows' | 'window' | 'accumulator' | 'group'
|
|
981
|
-
| 'bucket' | 'rolling' | 'rerun';
|
|
982
|
+
| 'bucket' | 'rolling' | 'join' | 'graph' | 'nested-group' | 'rerun';
|
|
982
983
|
readonly mode: 'incremental' | 'rerun';
|
|
983
984
|
/** Present exactly when the strategy is 'rerun': the named reason. */
|
|
984
985
|
readonly reason?: string;
|
|
@@ -1004,6 +1005,9 @@ export interface LiveEvent {
|
|
|
1004
1005
|
}
|
|
1005
1006
|
|
|
1006
1007
|
export interface LiveStats {
|
|
1008
|
+
dependencyReads?: number;
|
|
1009
|
+
refreshedRoots?: number;
|
|
1010
|
+
refreshedGroups?: number;
|
|
1007
1011
|
records: number;
|
|
1008
1012
|
matched: number;
|
|
1009
1013
|
emissions: number;
|
|
@@ -1037,6 +1041,8 @@ export interface LiveQuery {
|
|
|
1037
1041
|
}
|
|
1038
1042
|
|
|
1039
1043
|
export interface LiveBounds {
|
|
1044
|
+
/** Serialized input/output cache credit for join, graph and nested-group strategies. */
|
|
1045
|
+
maxBytes?: number;
|
|
1040
1046
|
/** Registrations beyond it are JD0052 (default 64). */
|
|
1041
1047
|
maxQueries?: number;
|
|
1042
1048
|
/** Per-query ceiling on maintained entries — rows, window entries
|
|
@@ -1045,6 +1051,7 @@ export interface LiveBounds {
|
|
|
1045
1051
|
}
|
|
1046
1052
|
|
|
1047
1053
|
export interface OpenStoreOptions {
|
|
1054
|
+
replication?: ReplicationOptions;
|
|
1048
1055
|
driver: Driver;
|
|
1049
1056
|
path?: string;
|
|
1050
1057
|
/**
|
|
@@ -1267,6 +1274,23 @@ export declare function readSchema(connection: unknown, options?: {
|
|
|
1267
1274
|
tables?: readonly string[];
|
|
1268
1275
|
}): unknown;
|
|
1269
1276
|
|
|
1277
|
+
export interface AssertionBounds {
|
|
1278
|
+
maxRows?: number | null;
|
|
1279
|
+
maxBytes?: number | null;
|
|
1280
|
+
/** Opt into a distinct fold with at most this many unique items. */
|
|
1281
|
+
maxDistinct?: number;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
export interface AssertionPlan {
|
|
1285
|
+
migration: string;
|
|
1286
|
+
step: number;
|
|
1287
|
+
collection: string;
|
|
1288
|
+
strategy: 'provider' | 'perDocument' | 'fold' | 'materialize';
|
|
1289
|
+
shape: string | null;
|
|
1290
|
+
reason: string;
|
|
1291
|
+
bounds: AssertionBounds | null;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1270
1294
|
export interface MigrateOptions {
|
|
1271
1295
|
baseline: unknown;
|
|
1272
1296
|
model?: unknown;
|
|
@@ -1300,13 +1324,14 @@ export interface MigrateOptions {
|
|
|
1300
1324
|
/** An epoch-millisecond deadline on `runtime`'s clock (`JD2075`). */
|
|
1301
1325
|
deadline?: number;
|
|
1302
1326
|
/** What a MATERIALIZING assertion may hold. A per-document predicate
|
|
1303
|
-
* and a
|
|
1327
|
+
* and a supported ordered aggregate over independent rows are answered in
|
|
1304
1328
|
* batches and are never bounded by this; anything else must hold the
|
|
1305
1329
|
* collection at once and crosses these bounds before the excess is
|
|
1306
1330
|
* held (`JD2007` rows, `JD2076` bytes). Defaults to
|
|
1307
1331
|
* {@link ASSERTION_BOUNDS_DEFAULT}; `null` on either member removes
|
|
1308
1332
|
* that bound, deliberately. */
|
|
1309
|
-
assertionBounds?:
|
|
1333
|
+
assertionBounds?: AssertionBounds;
|
|
1334
|
+
onAssertionPlan?: (plan: AssertionPlan) => void;
|
|
1310
1335
|
}
|
|
1311
1336
|
|
|
1312
1337
|
/** The finite defaults a materializing assertion runs under when the
|
|
@@ -1317,9 +1342,8 @@ export declare const ASSERTION_BOUNDS_DEFAULT: {
|
|
|
1317
1342
|
};
|
|
1318
1343
|
|
|
1319
1344
|
/** How a host must run an assertion, and why. `perDocument` walks in
|
|
1320
|
-
* batches; `fold`
|
|
1321
|
-
|
|
1322
|
-
export declare function classifyAssertion(query: unknown): {
|
|
1345
|
+
* batches; `fold` accumulates query items in order; `materialize` needs every document at once and is bounded. */
|
|
1346
|
+
export declare function classifyAssertion(query: unknown, options?: { expect?: string; maxDistinct?: number }): {
|
|
1323
1347
|
strategy: 'perDocument' | 'fold' | 'materialize';
|
|
1324
1348
|
shape: string | null;
|
|
1325
1349
|
reason: string;
|
|
@@ -1395,7 +1419,8 @@ export interface DocumentMigrationOptions {
|
|
|
1395
1419
|
/** What a MATERIALIZING assertion may hold; the same bounds, and the
|
|
1396
1420
|
* same refusals, a Store applies. Defaults to
|
|
1397
1421
|
* {@link ASSERTION_BOUNDS_DEFAULT}. */
|
|
1398
|
-
assertionBounds?:
|
|
1422
|
+
assertionBounds?: AssertionBounds;
|
|
1423
|
+
onAssertionPlan?: (plan: AssertionPlan) => void;
|
|
1399
1424
|
/** Documents per assertion batch and per progress event (default 500). */
|
|
1400
1425
|
batchSize?: number;
|
|
1401
1426
|
onProgress?: (progress: MigrationProgress) => void;
|
|
@@ -1450,6 +1475,7 @@ export declare function compileDocumentStep(
|
|
|
1450
1475
|
compileJslt: (stylesheet: unknown) => (document: unknown) => unknown;
|
|
1451
1476
|
compileQuery: (query: unknown) => unknown;
|
|
1452
1477
|
keys?: readonly string[];
|
|
1478
|
+
assertionBounds?: { maxRows: number | null; maxBytes: number | null; maxDistinct?: number };
|
|
1453
1479
|
},
|
|
1454
1480
|
): unknown;
|
|
1455
1481
|
/** Structural validation of one migration document (`JD0023`/`JD0021`). */
|
|
@@ -1605,7 +1631,7 @@ export declare function createLiveRegistry(
|
|
|
1605
1631
|
bounds: { maxQueries: number; maxMaintained: number }): unknown;
|
|
1606
1632
|
export declare function diffRows(oldRows: readonly unknown[], newRows: readonly unknown[]):
|
|
1607
1633
|
Array<{ op: string; path: string; value?: unknown }>;
|
|
1608
|
-
export declare const LIVE_DEFAULTS: { maxQueries: number; maxMaintained: number };
|
|
1634
|
+
export declare const LIVE_DEFAULTS: { maxQueries: number; maxMaintained: number; maxBytes: number };
|
|
1609
1635
|
export declare function createSortedWindow(
|
|
1610
1636
|
terms: unknown[], limit: number | null): unknown;
|
|
1611
1637
|
export declare function compareCodepoint(a: string, b: string): number;
|
|
@@ -1808,6 +1834,7 @@ export interface JobsApi {
|
|
|
1808
1834
|
* written up to it, and a settlement prunes no further, so a stale
|
|
1809
1835
|
* attempt cannot erase a live one's work. */
|
|
1810
1836
|
checkpointsFor(job: ClaimedJob): {
|
|
1837
|
+
inspect(runId: string, nodeId: string): unknown;
|
|
1811
1838
|
load(runId: string): unknown;
|
|
1812
1839
|
save(runId: string, nodeId: string, value: unknown): unknown;
|
|
1813
1840
|
complete(runId: string, result: unknown): unknown;
|
|
@@ -1833,6 +1860,11 @@ export interface JobPageOptions {
|
|
|
1833
1860
|
* schedule: WHEN to sweep or cancel is the host's call.
|
|
1834
1861
|
*/
|
|
1835
1862
|
export interface JobsAdminApi {
|
|
1863
|
+
/** Discard an inactive run's checkpoints and restart its attempts, atomically.
|
|
1864
|
+
* Requires its observed generation; refuses done jobs and live leases.
|
|
1865
|
+
* External effects are not undone. */
|
|
1866
|
+
reset(id: string, options: { expectedGeneration: number; signal?: AbortSignal; deadline?: number }):
|
|
1867
|
+
Promise<{ reset: true; discarded: number; generation: number }>;
|
|
1836
1868
|
/** A keyset cursor over the queue by id, admitted per pull under the
|
|
1837
1869
|
* store gate; each item is the record `get` answers. */
|
|
1838
1870
|
page(options?: JobPageOptions): QueryCursor<JobRecord>;
|
|
@@ -1869,7 +1901,7 @@ export interface JobsOptions {
|
|
|
1869
1901
|
export declare function createDagJobRunner(store: Store, options: {
|
|
1870
1902
|
compileDag: Function;
|
|
1871
1903
|
documents: Record<string, unknown>;
|
|
1872
|
-
tasks?: Record<string, Function>;
|
|
1904
|
+
tasks?: Record<string, Function | { run: Function; version?: string; taskVersions?: Record<string, string> }>;
|
|
1873
1905
|
concurrency?: number;
|
|
1874
1906
|
pollInterval?: number;
|
|
1875
1907
|
leaseMs?: number;
|
|
@@ -1898,3 +1930,55 @@ export declare const JOB_DEFAULTS: Readonly<{
|
|
|
1898
1930
|
export declare function describeValue(value: unknown): string;
|
|
1899
1931
|
/** A job result as the queue stores it: JSON text, or the reason it could not be. */
|
|
1900
1932
|
export declare function serializeResult(value: unknown): unknown;
|
|
1933
|
+
|
|
1934
|
+
/** Transport-neutral, net logical operations in a single transaction. */
|
|
1935
|
+
export interface ReplicationOperation {
|
|
1936
|
+
table: string;
|
|
1937
|
+
key: string;
|
|
1938
|
+
before: Record<string, unknown> | null;
|
|
1939
|
+
after: Record<string, unknown> | null;
|
|
1940
|
+
}
|
|
1941
|
+
export type ReplicationFrontier = Record<string, number>;
|
|
1942
|
+
export interface ReplicationEnvelope {
|
|
1943
|
+
$replication: '0.1'; replica: string; seq: number; model: string;
|
|
1944
|
+
frontier: ReplicationFrontier; operations: ReplicationOperation[];
|
|
1945
|
+
}
|
|
1946
|
+
export interface ReplicationConflict {
|
|
1947
|
+
envelope: string; table: string; key: string;
|
|
1948
|
+
base: Record<string, unknown> | null;
|
|
1949
|
+
local: { value: Record<string, unknown> | null; frontier: ReplicationFrontier };
|
|
1950
|
+
remote: { value: Record<string, unknown> | null; replica: string; seq: number; frontier: ReplicationFrontier };
|
|
1951
|
+
resolver: string | null;
|
|
1952
|
+
resolution: { action: 'local' | 'remote' | 'merged'; value: Record<string, unknown> | null } | null;
|
|
1953
|
+
}
|
|
1954
|
+
export interface ReplicationOptions {
|
|
1955
|
+
replica: string; retention?: number; maxOperations?: number; maxBytes?: number;
|
|
1956
|
+
resolver?: { id: string; resolve(conflict: Readonly<ReplicationConflict>):
|
|
1957
|
+
{ action: 'local' | 'remote' } | { action: 'merged'; value: Record<string, unknown> | null } };
|
|
1958
|
+
}
|
|
1959
|
+
export interface ReplicationRequest { signal?: AbortSignal; deadline?: number }
|
|
1960
|
+
export interface Replication {
|
|
1961
|
+
snapshot(request?: ReplicationRequest): Promise<ReplicationSnapshot>;
|
|
1962
|
+
reset(snapshot: ReplicationSnapshot, request?: ReplicationRequest): Promise<{ status: 'reset'; frontier: ReplicationFrontier }>;
|
|
1963
|
+
frontier(): Promise<ReplicationFrontier>;
|
|
1964
|
+
apply(envelope: ReplicationEnvelope, request?: ReplicationRequest): Promise<{
|
|
1965
|
+
status: 'applied' | 'duplicate' | 'conflict'; frontier: ReplicationFrontier; conflicts: ReplicationConflict[];
|
|
1966
|
+
}>;
|
|
1967
|
+
page(request?: ReplicationRequest & { after?: number; limit?: number; maxBytes?: number }): Promise<{
|
|
1968
|
+
items: ReplicationEnvelope[]; earliestAvailable: number | null; highWatermark: number;
|
|
1969
|
+
next?: number; bytes?: number; hasMore: boolean; resetRequired: boolean;
|
|
1970
|
+
}>;
|
|
1971
|
+
conflicts(request?: ReplicationRequest & { limit?: number; maxBytes?: number }): Promise<ReplicationConflict[]>;
|
|
1972
|
+
}
|
|
1973
|
+
export declare const REPLICATION_VERSION: '0.1';
|
|
1974
|
+
export declare const REPLICATION_DEFAULTS: Readonly<{ retention: number; maxOperations: number; maxBytes: number }>;
|
|
1975
|
+
export declare function normalizeFrontier(value: unknown): ReplicationFrontier;
|
|
1976
|
+
export declare function replicationIdentity(replica: string, seq: number): string;
|
|
1977
|
+
export declare function normalizeReplication(document: unknown): ReplicationEnvelope;
|
|
1978
|
+
export declare function encodeReplication(document: unknown): string;
|
|
1979
|
+
export interface ReplicationSnapshot {
|
|
1980
|
+
$replicationSnapshot: '0.1'; model: string; frontier: ReplicationFrontier;
|
|
1981
|
+
rows: { table: string; key: string; value: Record<string, unknown> | null; frontier: ReplicationFrontier }[];
|
|
1982
|
+
receipts: ReplicationEnvelope[];
|
|
1983
|
+
}
|
|
1984
|
+
export declare function normalizeReplicationSnapshot(document: unknown): ReplicationSnapshot;
|
package/types/node.d.ts
CHANGED
|
@@ -61,7 +61,7 @@ export declare function readDocuments(
|
|
|
61
61
|
export interface DocumentTarget {
|
|
62
62
|
/** The sibling file being filled, or null for a sink with no file. */
|
|
63
63
|
readonly temporary: string | null;
|
|
64
|
-
write(document: unknown): Promise<void>;
|
|
64
|
+
write(document: unknown, collection?: string): Promise<void>;
|
|
65
65
|
/** Flush, rename over the target, and answer what was written. */
|
|
66
66
|
commit(): Promise<{ bytes: number; documents: number }>;
|
|
67
67
|
/** Remove the temporary; the target keeps the bytes it had. */
|
|
@@ -72,13 +72,19 @@ export interface DocumentTarget {
|
|
|
72
72
|
* on `commit`, and removed on `abort`, so a failed run leaves the
|
|
73
73
|
* original byte for byte. */
|
|
74
74
|
export declare function openAtomicTarget(
|
|
75
|
-
target: string, format: 'json' | 'jsonl',
|
|
75
|
+
target: string, format: 'json' | 'jsonl' | 'collections',
|
|
76
|
+
options?: { collections?: string[] },
|
|
76
77
|
): Promise<DocumentTarget>;
|
|
77
78
|
|
|
78
79
|
/** Write to an open stream; `abort` cannot take back what has left. */
|
|
79
80
|
export declare function openStreamTarget(
|
|
80
|
-
stream: DocumentByteSink, format: 'json' | 'jsonl',
|
|
81
|
+
stream: DocumentByteSink, format: 'json' | 'jsonl' | 'collections',
|
|
82
|
+
options?: { collections?: string[] },
|
|
81
83
|
): DocumentTarget;
|
|
82
84
|
|
|
83
85
|
/** Validate everything and write nothing. */
|
|
84
86
|
export declare function openNullTarget(): DocumentTarget;
|
|
87
|
+
|
|
88
|
+
/** Read an explicit collection bundle, materialized under the declared bounds. */
|
|
89
|
+
export declare function readCollectionBundle(source: DocumentByteSource,
|
|
90
|
+
bounds: { maxBytes: number | null; maxRows: number | null }): Promise<Record<string, unknown[]>>;
|
package/types/typed.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
StoreStats, Collection, ExecuteOptions, SequenceResult, ValueOrPromise,
|
|
18
18
|
Dialect, ChangeRecord, LiveOptions, LiveQuery, JobsApi, SyncStore,
|
|
19
19
|
EntityScope, RelationEntry, RelationTable, EntityCursorOptions, QueryCursor,
|
|
20
|
-
LoadContinuation, PageOptions, Page, ChangesReader,
|
|
20
|
+
LoadContinuation, PageOptions, Page, ChangesReader, Replication, TransactionStore,
|
|
21
21
|
} from '@jarenjs/db';
|
|
22
22
|
|
|
23
23
|
/** The self-referential constraint an interface can satisfy: generated
|
|
@@ -164,7 +164,7 @@ export interface TypedStore<E extends MetaMap<E>> {
|
|
|
164
164
|
/** The relation tables of every entity, keyed by entity name. */
|
|
165
165
|
readonly relations?: Readonly<Record<keyof E & string, RelationTable>>;
|
|
166
166
|
saveChanges?(): Promise<SaveReport>;
|
|
167
|
-
transaction<R>(fn: (store:
|
|
167
|
+
transaction<R>(fn: (store: TransactionStore) => R | Promise<R>): Promise<Awaited<R>>;
|
|
168
168
|
observe(fn: (record: ChangeRecord) => void): () => void;
|
|
169
169
|
/** Unbounded, and unsafe for a reconnecting consumer: `changes.page()`
|
|
170
170
|
* is the supported path (LIVE-FORMAT §5). */
|
|
@@ -174,6 +174,7 @@ export interface TypedStore<E extends MetaMap<E>> {
|
|
|
174
174
|
live?(document: unknown, options?: LiveOptions): Promise<LiveQuery>;
|
|
175
175
|
close(options?: { graceMs?: number }): Promise<void>;
|
|
176
176
|
readonly jobs?: JobsApi;
|
|
177
|
+
readonly replication?: Replication;
|
|
177
178
|
readonly sync?: SyncStore;
|
|
178
179
|
}
|
|
179
180
|
|