@delali/sirannon-db 0.1.6 → 0.1.8
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/README.md +154 -19
- package/dist/backup-scheduler/index.d.ts +18 -3
- package/dist/backup-scheduler/index.mjs +2 -2
- package/dist/{change-tracker-CFTQ9TSn.d.ts → change-tracker-CbmaMO-N.d.ts} +12 -2
- package/dist/chunk-4ISB7XMA.mjs +21 -0
- package/dist/chunk-BNUTBHHH.mjs +22 -0
- package/dist/chunk-CJLYFDP5.mjs +26 -0
- package/dist/chunk-CW6S3WL5.mjs +222 -0
- package/dist/chunk-DVWQD3GF.mjs +49 -0
- package/dist/chunk-GEZUUIKV.mjs +268 -0
- package/dist/{chunk-UVMVN3OT.mjs → chunk-H5AB6NIR.mjs} +1 -1
- package/dist/{chunk-UTO3ZAFS.mjs → chunk-HHRMRFFR.mjs} +148 -28
- package/dist/chunk-TKGHYWQ6.mjs +35 -0
- package/dist/chunk-UXLAO6ZH.mjs +352 -0
- package/dist/chunk-VLTICJOD.mjs +470 -0
- package/dist/{chunk-O7BHI3CF.mjs → chunk-YPYVQJ4C.mjs} +15 -1
- package/dist/client/index.d.ts +107 -15
- package/dist/client/index.mjs +348 -37
- package/dist/core/index.d.ts +70 -13
- package/dist/core/index.mjs +635 -192
- package/dist/core/writer-worker.d.ts +2 -0
- package/dist/core/writer-worker.mjs +129 -0
- package/dist/{database-BVY1GqE7.d.ts → database-DuGp0Rtr.d.ts} +51 -19
- package/dist/driver/better-sqlite3.d.ts +1 -1
- package/dist/driver/better-sqlite3.mjs +44 -6
- package/dist/driver/bun.mjs +35 -6
- package/dist/driver/expo.mjs +2 -2
- package/dist/driver/node.d.ts +1 -1
- package/dist/driver/node.mjs +48 -5
- package/dist/driver/wa-sqlite.d.ts +109 -0
- package/dist/driver/wa-sqlite.mjs +30 -2
- package/dist/{errors-C00ed08Q.d.ts → errors-5Nf5ZAEC.d.ts} +19 -1
- package/dist/file-migrations/index.d.ts +2 -3
- package/dist/file-migrations/index.mjs +1 -1
- package/dist/replication/coordinator/etcd.mjs +2 -2
- package/dist/replication/index.d.ts +7 -8
- package/dist/replication/index.mjs +79 -78
- package/dist/server/index.d.ts +111 -28
- package/dist/server/index.mjs +1016 -384
- package/dist/{sirannon-Cd-lK6T0.d.ts → sirannon-4SspRvP5.d.ts} +3 -3
- package/dist/transport/grpc.d.ts +3 -4
- package/dist/transport/grpc.mjs +2 -2
- package/dist/{types-Lc7ywFx7.d.ts → types-BsjobKbl.d.ts} +2 -2
- package/dist/{types-BeozgNPr.d.ts → types-D4p4UyDK.d.ts} +1 -1
- package/dist/types-D_hQW1hr.d.ts +494 -0
- package/package.json +3 -23
- package/dist/chunk-3MCMONVP.mjs +0 -115
- package/dist/chunk-74UN4DIE.mjs +0 -14
- package/dist/chunk-FB2U2Q3Y.mjs +0 -21
- package/dist/chunk-GS7T5YMI.mjs +0 -51
- package/dist/chunk-PXKAKK2V.mjs +0 -124
- package/dist/index-CLdNrcPz.d.ts +0 -16
- package/dist/types-BFSsG77t.d.ts +0 -29
- package/dist/types-D-74JiXb.d.ts +0 -265
|
@@ -12,12 +12,40 @@ function defineDriver(config) {
|
|
|
12
12
|
if (!config.capabilities || typeof config.open !== "function") {
|
|
13
13
|
throw new SirannonError("Driver must define capabilities and open()", "INVALID_DRIVER");
|
|
14
14
|
}
|
|
15
|
+
if (config.worker !== void 0 && typeof config.worker.specifier !== "string") {
|
|
16
|
+
throw new SirannonError("Driver worker entry must define a string specifier", "INVALID_DRIVER");
|
|
17
|
+
}
|
|
15
18
|
return Object.freeze({
|
|
16
19
|
capabilities: Object.freeze({ ...config.capabilities }),
|
|
17
|
-
open: config.open
|
|
20
|
+
open: config.open,
|
|
21
|
+
...config.worker ? { worker: Object.freeze({ ...config.worker }) } : {},
|
|
22
|
+
...config.startWriterHost ? { startWriterHost: config.startWriterHost } : {},
|
|
23
|
+
...config.createWriterContext ? { createWriterContext: config.createWriterContext } : {},
|
|
24
|
+
...config.createBackupEngine ? { createBackupEngine: config.createBackupEngine } : {},
|
|
25
|
+
...config.resolveExtensionPath ? { resolveExtensionPath: config.resolveExtensionPath } : {}
|
|
18
26
|
});
|
|
19
27
|
}
|
|
20
28
|
|
|
29
|
+
// src/core/driver/synchronous.ts
|
|
30
|
+
var DEFAULT_SYNCHRONOUS = "normal";
|
|
31
|
+
var SYNCHRONOUS_PRAGMA_VALUES = {
|
|
32
|
+
off: "OFF",
|
|
33
|
+
normal: "NORMAL",
|
|
34
|
+
full: "FULL",
|
|
35
|
+
extra: "EXTRA"
|
|
36
|
+
};
|
|
37
|
+
function synchronousPragmaValue(level) {
|
|
38
|
+
const resolved = level ?? DEFAULT_SYNCHRONOUS;
|
|
39
|
+
const value = SYNCHRONOUS_PRAGMA_VALUES[resolved];
|
|
40
|
+
if (value === void 0) {
|
|
41
|
+
throw new SirannonError(
|
|
42
|
+
`Invalid synchronous level '${String(resolved)}': expected 'off', 'normal', 'full', or 'extra'`,
|
|
43
|
+
"INVALID_SYNCHRONOUS"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
21
49
|
// src/drivers/wa-sqlite/index.ts
|
|
22
50
|
function waSqlite(driverOptions) {
|
|
23
51
|
return defineDriver({
|
|
@@ -46,7 +74,7 @@ function waSqlite(driverOptions) {
|
|
|
46
74
|
} catch {
|
|
47
75
|
}
|
|
48
76
|
}
|
|
49
|
-
await sqlite3.exec(db,
|
|
77
|
+
await sqlite3.exec(db, `PRAGMA synchronous = ${synchronousPragmaValue(options?.synchronous)}`);
|
|
50
78
|
await sqlite3.exec(db, "PRAGMA foreign_keys = ON");
|
|
51
79
|
async function execStatements(sql, params) {
|
|
52
80
|
const columns = [];
|
|
@@ -67,6 +67,15 @@ declare class HookDeniedError extends SirannonError {
|
|
|
67
67
|
declare class CDCError extends SirannonError {
|
|
68
68
|
constructor(message: string);
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Thrown when a caller reaches for a table Sirannon reserves for itself. The
|
|
72
|
+
* internal bookkeeping tables and SQLite's own catalogue are off limits to the
|
|
73
|
+
* query API so a caller cannot read or corrupt the change log, replication
|
|
74
|
+
* ledger, or schema catalogue.
|
|
75
|
+
*/
|
|
76
|
+
declare class ForbiddenSqlError extends SirannonError {
|
|
77
|
+
constructor(message: string);
|
|
78
|
+
}
|
|
70
79
|
/**
|
|
71
80
|
* Thrown when a backup operation fails, whether that is an online backup via
|
|
72
81
|
* the SQLite backup API or a file-level copy.
|
|
@@ -89,6 +98,15 @@ declare class ConnectionPoolError extends SirannonError {
|
|
|
89
98
|
declare class MaxDatabasesError extends SirannonError {
|
|
90
99
|
constructor(max: number);
|
|
91
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Thrown when more writes are pending than the writer-worker limit allows. It
|
|
103
|
+
* signals load shedding, so the server maps it to a 503 with a Retry-After hint.
|
|
104
|
+
*/
|
|
105
|
+
declare class WriteOverloadError extends SirannonError {
|
|
106
|
+
readonly limit: number;
|
|
107
|
+
readonly retryAfterMs: number;
|
|
108
|
+
constructor(limit: number, retryAfterMs: number);
|
|
109
|
+
}
|
|
92
110
|
/**
|
|
93
111
|
* Thrown when a native SQLite extension cannot be loaded. The `path` argument
|
|
94
112
|
* is the filesystem path passed to `load_extension`, and the optional `cause`
|
|
@@ -98,4 +116,4 @@ declare class ExtensionError extends SirannonError {
|
|
|
98
116
|
constructor(path: string, cause?: string);
|
|
99
117
|
}
|
|
100
118
|
|
|
101
|
-
export { BackupError as B, CDCError as C, DatabaseAlreadyExistsError as D, ExtensionError as E, HookDeniedError as H, MaxDatabasesError as M, QueryError as Q, ReadOnlyError as R, SirannonError as S, TransactionError as T, ConnectionPoolError as a, DatabaseNotFoundError as b, MigrationError as c };
|
|
119
|
+
export { BackupError as B, CDCError as C, DatabaseAlreadyExistsError as D, ExtensionError as E, ForbiddenSqlError as F, HookDeniedError as H, MaxDatabasesError as M, QueryError as Q, ReadOnlyError as R, SirannonError as S, TransactionError as T, WriteOverloadError as W, ConnectionPoolError as a, DatabaseNotFoundError as b, MigrationError as c };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { compatibilityAllowsPromotion } from '../../chunk-ER7ODTDA.mjs';
|
|
2
|
-
import { CoordinatorError, NoSafePrimaryError } from '../../chunk-
|
|
3
|
-
import '../../chunk-
|
|
2
|
+
import { CoordinatorError, NoSafePrimaryError } from '../../chunk-H5AB6NIR.mjs';
|
|
3
|
+
import '../../chunk-YPYVQJ4C.mjs';
|
|
4
4
|
import { Etcd3 } from 'etcd3';
|
|
5
5
|
|
|
6
6
|
var MIN_AUTOMATIC_FAILOVER_VOTERS = 3;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { C as ConflictResolver, h as ConflictContext, i as ConflictResolution, H as HLCTimestamp, R as ReplicationBatch, A as ApplyResult, j as SyncPhase, k as SyncTableManifest, I as InFlightBatch, P as PeerState, b as ForwardedTransactionResult, c as SyncBatch, d as SyncComplete, e as SyncAck, S as SyncRequest, l as ReplicationConfig, m as SyncState, n as ReplicationStatus, o as ReplicationErrorEvent, a as ReplicationAck, p as Topology, T as TopologyRole } from '../types-
|
|
2
|
-
export { F as ForwardedTransaction, N as NodeInfo, q as ReplicationChange, f as ReplicationTransport, g as TransportConfig } from '../types-
|
|
1
|
+
import { C as ConflictResolver, h as ConflictContext, i as ConflictResolution, H as HLCTimestamp, R as ReplicationBatch, A as ApplyResult, j as SyncPhase, k as SyncTableManifest, I as InFlightBatch, P as PeerState, b as ForwardedTransactionResult, c as SyncBatch, d as SyncComplete, e as SyncAck, S as SyncRequest, l as ReplicationConfig, m as SyncState, n as ReplicationStatus, o as ReplicationErrorEvent, a as ReplicationAck, p as Topology, T as TopologyRole } from '../types-BsjobKbl.js';
|
|
2
|
+
export { F as ForwardedTransaction, N as NodeInfo, q as ReplicationChange, f as ReplicationTransport, g as TransportConfig } from '../types-BsjobKbl.js';
|
|
3
3
|
import { c as ReplicationGroupState } from '../types-BEu1I_9_.js';
|
|
4
4
|
export { A as AcquireControllerLeaseInput, a as AcquireControllerLeaseResult, h as AdmitNodeToInSyncSetInput, C as ClusterCoordinator, f as CompareAndAdvancePrimaryTermInput, g as CompareAndAdvancePrimaryTermResult, j as CoordinatorCompatibilityMetadata, k as CoordinatorLease, b as CoordinatorNodeSession, l as CoordinatorPrimary, e as CoordinatorWatchDisposer, P as PromoteEligibleReplicaInput, R as RegisterNodeSessionInput, d as ReplicationGroupWatcher, S as SetReplicationGroupStateInput, U as UpdateInSyncSetInput, i as UpdateNodeMaintenanceInput } from '../types-BEu1I_9_.js';
|
|
5
5
|
import { EventEmitter } from 'node:events';
|
|
6
|
-
import { C as ChangeTracker } from '../change-tracker-
|
|
7
|
-
import { D as Database } from '../database-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import '../types-BeozgNPr.js';
|
|
6
|
+
import { C as ChangeTracker } from '../change-tracker-CbmaMO-N.js';
|
|
7
|
+
import { D as Database } from '../database-DuGp0Rtr.js';
|
|
8
|
+
import { e as SQLiteConnection, P as Params, p as QueryOptions, T as Transaction, E as ExecuteResult } from '../types-D_hQW1hr.js';
|
|
9
|
+
import { S as SirannonError } from '../errors-5Nf5ZAEC.js';
|
|
10
|
+
import '../types-D4p4UyDK.js';
|
|
12
11
|
|
|
13
12
|
type ColumnVersionGetter = (table: string, rowId: string) => Promise<Map<string, {
|
|
14
13
|
hlc: string;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Transaction } from '../chunk-3MCMONVP.mjs';
|
|
2
|
-
import { decodeTaggedValues } from '../chunk-GS7T5YMI.mjs';
|
|
3
1
|
import { compatibilityAllowsPromotion } from '../chunk-ER7ODTDA.mjs';
|
|
4
|
-
import { ReplicationError, WriteConcernError, AuthorityError, SyncError, TopologyError, CoordinatorError, StalePrimaryError, NodeDrainingError, NodeNotInSyncError, ReadConcernError, ProtocolVersionMismatchError, BatchValidationError } from '../chunk-
|
|
5
|
-
export { AuthorityError, BatchValidationError, ConflictError, CoordinatorError, FailoverError, NoSafePrimaryError, NodeDrainingError, NodeNotInSyncError, ProtocolVersionMismatchError, ReadConcernError, ReplicationError, StalePrimaryError, SyncError, TopologyError, TransportError, UnsafeRecoveryRequiredError, WriteConcernError } from '../chunk-
|
|
6
|
-
import '../chunk-
|
|
2
|
+
import { ReplicationError, WriteConcernError, AuthorityError, SyncError, TopologyError, CoordinatorError, StalePrimaryError, NodeDrainingError, NodeNotInSyncError, ReadConcernError, ProtocolVersionMismatchError, BatchValidationError } from '../chunk-H5AB6NIR.mjs';
|
|
3
|
+
export { AuthorityError, BatchValidationError, ConflictError, CoordinatorError, FailoverError, NoSafePrimaryError, NodeDrainingError, NodeNotInSyncError, ProtocolVersionMismatchError, ReadConcernError, ReplicationError, StalePrimaryError, SyncError, TopologyError, TransportError, UnsafeRecoveryRequiredError, WriteConcernError } from '../chunk-H5AB6NIR.mjs';
|
|
4
|
+
import { Transaction } from '../chunk-TKGHYWQ6.mjs';
|
|
5
|
+
import '../chunk-CW6S3WL5.mjs';
|
|
6
|
+
import { CHANGES_TABLE, APPLIED_CHANGES_TABLE, COLUMN_VERSIONS_TABLE, PEER_STATE_TABLE, SYNC_STATE_TABLE, INTERNAL_TABLE_PREFIX, decodeTaggedValues } from '../chunk-GEZUUIKV.mjs';
|
|
7
|
+
import '../chunk-YPYVQJ4C.mjs';
|
|
7
8
|
import { randomBytes, randomUUID, createHash } from 'crypto';
|
|
8
9
|
import { EventEmitter } from 'events';
|
|
9
10
|
|
|
@@ -77,6 +78,43 @@ var HLC = class _HLC {
|
|
|
77
78
|
}
|
|
78
79
|
};
|
|
79
80
|
|
|
81
|
+
// src/replication/log/canonicalise.ts
|
|
82
|
+
function canonicaliseForChecksum(value) {
|
|
83
|
+
return canonicaliseValue(value);
|
|
84
|
+
}
|
|
85
|
+
function canonicaliseValue(v) {
|
|
86
|
+
if (v === null || v === void 0) return "null";
|
|
87
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
88
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
89
|
+
if (typeof v === "bigint") return `{"__sint":"${v.toString()}"}`;
|
|
90
|
+
if (typeof v === "number") {
|
|
91
|
+
if (Number.isInteger(v)) return `{"__sint":"${v.toString()}"}`;
|
|
92
|
+
if (Number.isFinite(v)) return JSON.stringify(v);
|
|
93
|
+
return "null";
|
|
94
|
+
}
|
|
95
|
+
if (typeof v === "object") {
|
|
96
|
+
if (v instanceof Uint8Array) {
|
|
97
|
+
const buf = Buffer.isBuffer(v) ? v : Buffer.from(v);
|
|
98
|
+
return `{"__blob":"${buf.toString("base64")}"}`;
|
|
99
|
+
}
|
|
100
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(v)) {
|
|
101
|
+
return `{"__blob":"${v.toString("base64")}"}`;
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(v)) {
|
|
104
|
+
return `[${v.map(canonicaliseValue).join(",")}]`;
|
|
105
|
+
}
|
|
106
|
+
const obj = v;
|
|
107
|
+
const parts = [];
|
|
108
|
+
for (const key of Object.keys(obj).sort()) {
|
|
109
|
+
const val = obj[key];
|
|
110
|
+
if (val === void 0) continue;
|
|
111
|
+
parts.push(`${JSON.stringify(key)}:${canonicaliseValue(val)}`);
|
|
112
|
+
}
|
|
113
|
+
return `{${parts.join(",")}}`;
|
|
114
|
+
}
|
|
115
|
+
return "null";
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
// src/replication/conflict/lww.ts
|
|
81
119
|
var LWWResolver = class {
|
|
82
120
|
resolve(ctx) {
|
|
@@ -115,12 +153,12 @@ var FieldMergeResolver = class {
|
|
|
115
153
|
const localChanged = /* @__PURE__ */ new Set();
|
|
116
154
|
const remoteChanged = /* @__PURE__ */ new Set();
|
|
117
155
|
for (const key of Object.keys(localData)) {
|
|
118
|
-
if (
|
|
156
|
+
if (canonicaliseForChecksum(localData[key]) !== canonicaliseForChecksum(oldData[key])) {
|
|
119
157
|
localChanged.add(key);
|
|
120
158
|
}
|
|
121
159
|
}
|
|
122
160
|
for (const key of Object.keys(remoteData)) {
|
|
123
|
-
if (
|
|
161
|
+
if (canonicaliseForChecksum(remoteData[key]) !== canonicaliseForChecksum(oldData[key])) {
|
|
124
162
|
remoteChanged.add(key);
|
|
125
163
|
}
|
|
126
164
|
}
|
|
@@ -189,43 +227,6 @@ var PrimaryWinsResolver = class {
|
|
|
189
227
|
}
|
|
190
228
|
};
|
|
191
229
|
|
|
192
|
-
// src/replication/log/canonicalise.ts
|
|
193
|
-
function canonicaliseForChecksum(value) {
|
|
194
|
-
return canonicaliseValue(value);
|
|
195
|
-
}
|
|
196
|
-
function canonicaliseValue(v) {
|
|
197
|
-
if (v === null || v === void 0) return "null";
|
|
198
|
-
if (typeof v === "boolean") return v ? "true" : "false";
|
|
199
|
-
if (typeof v === "string") return JSON.stringify(v);
|
|
200
|
-
if (typeof v === "bigint") return `{"__sint":"${v.toString()}"}`;
|
|
201
|
-
if (typeof v === "number") {
|
|
202
|
-
if (Number.isInteger(v)) return `{"__sint":"${v.toString()}"}`;
|
|
203
|
-
if (Number.isFinite(v)) return JSON.stringify(v);
|
|
204
|
-
return "null";
|
|
205
|
-
}
|
|
206
|
-
if (typeof v === "object") {
|
|
207
|
-
if (v instanceof Uint8Array) {
|
|
208
|
-
const buf = Buffer.isBuffer(v) ? v : Buffer.from(v);
|
|
209
|
-
return `{"__blob":"${buf.toString("base64")}"}`;
|
|
210
|
-
}
|
|
211
|
-
if (typeof Buffer !== "undefined" && Buffer.isBuffer(v)) {
|
|
212
|
-
return `{"__blob":"${v.toString("base64")}"}`;
|
|
213
|
-
}
|
|
214
|
-
if (Array.isArray(v)) {
|
|
215
|
-
return `[${v.map(canonicaliseValue).join(",")}]`;
|
|
216
|
-
}
|
|
217
|
-
const obj = v;
|
|
218
|
-
const parts = [];
|
|
219
|
-
for (const key of Object.keys(obj).sort()) {
|
|
220
|
-
const val = obj[key];
|
|
221
|
-
if (val === void 0) continue;
|
|
222
|
-
parts.push(`${JSON.stringify(key)}:${canonicaliseValue(val)}`);
|
|
223
|
-
}
|
|
224
|
-
return `{${parts.join(",")}}`;
|
|
225
|
-
}
|
|
226
|
-
return "null";
|
|
227
|
-
}
|
|
228
|
-
|
|
229
230
|
// src/replication/engine/constants.ts
|
|
230
231
|
var DEFAULT_BATCH_SIZE = 100;
|
|
231
232
|
var DEFAULT_BATCH_INTERVAL_MS = 100;
|
|
@@ -359,7 +360,7 @@ var BatchReader = class {
|
|
|
359
360
|
const rows = await selectStmt.all(afterSeq.toString(), this.localNodeId);
|
|
360
361
|
for (const row of rows) {
|
|
361
362
|
if (row.operation === "DELETE") {
|
|
362
|
-
const delStmt = await tx.prepare(
|
|
363
|
+
const delStmt = await tx.prepare(`DELETE FROM ${COLUMN_VERSIONS_TABLE} WHERE table_name = ? AND row_id = ?`);
|
|
363
364
|
await delStmt.run(row.table_name, String(row.row_id));
|
|
364
365
|
continue;
|
|
365
366
|
}
|
|
@@ -378,7 +379,7 @@ var BatchReader = class {
|
|
|
378
379
|
}
|
|
379
380
|
}
|
|
380
381
|
const upsertStmt = await tx.prepare(
|
|
381
|
-
`INSERT INTO
|
|
382
|
+
`INSERT INTO ${COLUMN_VERSIONS_TABLE} (table_name, row_id, column_name, hlc, node_id)
|
|
382
383
|
VALUES (?, ?, ?, ?, ?)
|
|
383
384
|
ON CONFLICT(table_name, row_id, column_name)
|
|
384
385
|
DO UPDATE SET hlc = excluded.hlc, node_id = excluded.node_id`
|
|
@@ -443,7 +444,7 @@ var BatchApplier = class {
|
|
|
443
444
|
if (needsPartialDedup) {
|
|
444
445
|
appliedSeqSet = /* @__PURE__ */ new Set();
|
|
445
446
|
const checkStmt = await this.conn.prepare(
|
|
446
|
-
|
|
447
|
+
`SELECT source_seq FROM ${APPLIED_CHANGES_TABLE} WHERE source_node_id = ? AND source_seq >= ? AND source_seq <= ?`
|
|
447
448
|
);
|
|
448
449
|
const applied2 = await checkStmt.all(
|
|
449
450
|
batch.sourceNodeId,
|
|
@@ -548,7 +549,7 @@ var BatchApplier = class {
|
|
|
548
549
|
}
|
|
549
550
|
}
|
|
550
551
|
const recordStmt = await this.conn.prepare(
|
|
551
|
-
|
|
552
|
+
`INSERT OR IGNORE INTO ${APPLIED_CHANGES_TABLE} (source_node_id, source_seq, applied_at) VALUES (?, ?, ?)`
|
|
552
553
|
);
|
|
553
554
|
const nowSec = Date.now() / 1e3;
|
|
554
555
|
for (let seq = batch.fromSeq; seq <= batch.toSeq; seq += 1n) {
|
|
@@ -573,7 +574,7 @@ var BatchApplier = class {
|
|
|
573
574
|
}
|
|
574
575
|
async getLocalHlcForRow(tx, table, rowId) {
|
|
575
576
|
const stmt = await tx.prepare(
|
|
576
|
-
|
|
577
|
+
`SELECT MAX(hlc) as max_hlc FROM ${COLUMN_VERSIONS_TABLE} WHERE table_name = ? AND row_id = ?`
|
|
577
578
|
);
|
|
578
579
|
const row = await stmt.get(table, rowId);
|
|
579
580
|
return row?.max_hlc ?? null;
|
|
@@ -660,13 +661,13 @@ var BatchApplier = class {
|
|
|
660
661
|
}
|
|
661
662
|
async recordColumnVersions(tx, change, data) {
|
|
662
663
|
if (change.operation === "delete") {
|
|
663
|
-
const delStmt = await tx.prepare(
|
|
664
|
+
const delStmt = await tx.prepare(`DELETE FROM ${COLUMN_VERSIONS_TABLE} WHERE table_name = ? AND row_id = ?`);
|
|
664
665
|
await delStmt.run(change.table, change.rowId);
|
|
665
666
|
return;
|
|
666
667
|
}
|
|
667
668
|
if (!data) return;
|
|
668
669
|
const upsertStmt = await tx.prepare(
|
|
669
|
-
`INSERT INTO
|
|
670
|
+
`INSERT INTO ${COLUMN_VERSIONS_TABLE} (table_name, row_id, column_name, hlc, node_id)
|
|
670
671
|
VALUES (?, ?, ?, ?, ?)
|
|
671
672
|
ON CONFLICT(table_name, row_id, column_name)
|
|
672
673
|
DO UPDATE SET hlc = excluded.hlc, node_id = excluded.node_id`
|
|
@@ -931,21 +932,21 @@ CREATE TABLE IF NOT EXISTS "${this.changesTable}" (
|
|
|
931
932
|
hlc TEXT NOT NULL DEFAULT ''
|
|
932
933
|
)`);
|
|
933
934
|
await this.conn.exec(`
|
|
934
|
-
CREATE TABLE IF NOT EXISTS
|
|
935
|
+
CREATE TABLE IF NOT EXISTS ${PEER_STATE_TABLE} (
|
|
935
936
|
peer_node_id TEXT PRIMARY KEY,
|
|
936
937
|
last_acked_seq INTEGER NOT NULL DEFAULT 0,
|
|
937
938
|
last_received_hlc TEXT NOT NULL DEFAULT '',
|
|
938
939
|
updated_at REAL NOT NULL
|
|
939
940
|
)`);
|
|
940
941
|
await this.conn.exec(`
|
|
941
|
-
CREATE TABLE IF NOT EXISTS
|
|
942
|
+
CREATE TABLE IF NOT EXISTS ${APPLIED_CHANGES_TABLE} (
|
|
942
943
|
source_node_id TEXT NOT NULL,
|
|
943
944
|
source_seq INTEGER NOT NULL,
|
|
944
945
|
applied_at REAL NOT NULL,
|
|
945
946
|
PRIMARY KEY (source_node_id, source_seq)
|
|
946
947
|
)`);
|
|
947
948
|
await this.conn.exec(`
|
|
948
|
-
CREATE TABLE IF NOT EXISTS
|
|
949
|
+
CREATE TABLE IF NOT EXISTS ${COLUMN_VERSIONS_TABLE} (
|
|
949
950
|
table_name TEXT NOT NULL,
|
|
950
951
|
row_id TEXT NOT NULL,
|
|
951
952
|
column_name TEXT NOT NULL,
|
|
@@ -954,7 +955,7 @@ CREATE TABLE IF NOT EXISTS _sirannon_column_versions (
|
|
|
954
955
|
PRIMARY KEY (table_name, row_id, column_name)
|
|
955
956
|
)`);
|
|
956
957
|
await this.conn.exec(`
|
|
957
|
-
CREATE TABLE IF NOT EXISTS
|
|
958
|
+
CREATE TABLE IF NOT EXISTS ${SYNC_STATE_TABLE} (
|
|
958
959
|
table_name TEXT PRIMARY KEY,
|
|
959
960
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
960
961
|
row_count INTEGER NOT NULL DEFAULT 0,
|
|
@@ -966,7 +967,7 @@ CREATE TABLE IF NOT EXISTS _sirannon_sync_state (
|
|
|
966
967
|
request_id TEXT
|
|
967
968
|
)`);
|
|
968
969
|
}
|
|
969
|
-
async dumpSchema(conn, excludePrefix =
|
|
970
|
+
async dumpSchema(conn, excludePrefix = INTERNAL_TABLE_PREFIX) {
|
|
970
971
|
const stmt = await conn.prepare(
|
|
971
972
|
`SELECT type, name, sql FROM sqlite_master
|
|
972
973
|
WHERE type IN ('table', 'index') AND name NOT LIKE ? AND name NOT LIKE 'sqlite_%' AND sql IS NOT NULL`
|
|
@@ -1003,7 +1004,7 @@ CREATE TABLE IF NOT EXISTS _sirannon_sync_state (
|
|
|
1003
1004
|
}
|
|
1004
1005
|
async getTablesInFkOrder(conn) {
|
|
1005
1006
|
const stmt = await conn.prepare(
|
|
1006
|
-
`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE '
|
|
1007
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE '${INTERNAL_TABLE_PREFIX}%' AND name NOT LIKE 'sqlite_%'`
|
|
1007
1008
|
);
|
|
1008
1009
|
const tableRows = await stmt.all();
|
|
1009
1010
|
const tableNames = tableRows.map((r) => r.name);
|
|
@@ -1070,7 +1071,7 @@ CREATE TABLE IF NOT EXISTS _sirannon_sync_state (
|
|
|
1070
1071
|
for (const table of reversed) {
|
|
1071
1072
|
await tx.exec(`DELETE FROM "${table}"`);
|
|
1072
1073
|
}
|
|
1073
|
-
await tx.exec(`DELETE FROM
|
|
1074
|
+
await tx.exec(`DELETE FROM ${SYNC_STATE_TABLE} WHERE table_name != '__sync_meta__'`);
|
|
1074
1075
|
});
|
|
1075
1076
|
} finally {
|
|
1076
1077
|
await conn.exec("PRAGMA foreign_keys = ON");
|
|
@@ -1118,7 +1119,7 @@ var StateOps = class {
|
|
|
1118
1119
|
const changesRow = await changesStmt.get();
|
|
1119
1120
|
best = mergeCandidate(best, changesRow?.max_hlc ?? null);
|
|
1120
1121
|
const versionsStmt = await this.conn.prepare(
|
|
1121
|
-
`SELECT MAX(hlc) AS max_hlc FROM
|
|
1122
|
+
`SELECT MAX(hlc) AS max_hlc FROM ${COLUMN_VERSIONS_TABLE} WHERE hlc != ''`
|
|
1122
1123
|
);
|
|
1123
1124
|
const versionsRow = await versionsStmt.get();
|
|
1124
1125
|
best = mergeCandidate(best, versionsRow?.max_hlc ?? null);
|
|
@@ -1126,7 +1127,7 @@ var StateOps = class {
|
|
|
1126
1127
|
}
|
|
1127
1128
|
async getLastAppliedSeq(fromNodeId) {
|
|
1128
1129
|
const stmt = await this.conn.prepare(
|
|
1129
|
-
|
|
1130
|
+
`SELECT MAX(source_seq) as max_seq FROM ${APPLIED_CHANGES_TABLE} WHERE source_node_id = ?`
|
|
1130
1131
|
);
|
|
1131
1132
|
const row = await stmt.get(fromNodeId);
|
|
1132
1133
|
if (!row || row.max_seq === null) {
|
|
@@ -1136,20 +1137,20 @@ var StateOps = class {
|
|
|
1136
1137
|
}
|
|
1137
1138
|
async setLastAppliedSeq(fromNodeId, seq) {
|
|
1138
1139
|
const stmt = await this.conn.prepare(
|
|
1139
|
-
`INSERT INTO
|
|
1140
|
+
`INSERT INTO ${PEER_STATE_TABLE} (peer_node_id, last_acked_seq, updated_at)
|
|
1140
1141
|
VALUES (?, ?, ?)
|
|
1141
1142
|
ON CONFLICT(peer_node_id)
|
|
1142
1143
|
DO UPDATE SET
|
|
1143
|
-
last_acked_seq = max(
|
|
1144
|
+
last_acked_seq = max(${PEER_STATE_TABLE}.last_acked_seq, excluded.last_acked_seq),
|
|
1144
1145
|
updated_at = CASE
|
|
1145
|
-
WHEN excluded.last_acked_seq >=
|
|
1146
|
-
ELSE
|
|
1146
|
+
WHEN excluded.last_acked_seq >= ${PEER_STATE_TABLE}.last_acked_seq THEN excluded.updated_at
|
|
1147
|
+
ELSE ${PEER_STATE_TABLE}.updated_at
|
|
1147
1148
|
END`
|
|
1148
1149
|
);
|
|
1149
1150
|
await stmt.run(fromNodeId, seq.toString(), Date.now() / 1e3);
|
|
1150
1151
|
}
|
|
1151
1152
|
async getPeerAckedSeq(peerNodeId) {
|
|
1152
|
-
const stmt = await this.conn.prepare(
|
|
1153
|
+
const stmt = await this.conn.prepare(`SELECT last_acked_seq FROM ${PEER_STATE_TABLE} WHERE peer_node_id = ?`);
|
|
1153
1154
|
const row = await stmt.get(peerNodeId);
|
|
1154
1155
|
if (!row || row.last_acked_seq === null) {
|
|
1155
1156
|
return 0n;
|
|
@@ -1166,7 +1167,7 @@ var StateOps = class {
|
|
|
1166
1167
|
}
|
|
1167
1168
|
async getMinAckedSeq() {
|
|
1168
1169
|
const stmt = await this.conn.prepare(
|
|
1169
|
-
|
|
1170
|
+
`SELECT MIN(last_acked_seq) as min_seq, COUNT(*) as cnt FROM ${PEER_STATE_TABLE}`
|
|
1170
1171
|
);
|
|
1171
1172
|
const row = await stmt.get();
|
|
1172
1173
|
const hasPeers = row !== void 0 && row.cnt > 0;
|
|
@@ -1191,7 +1192,7 @@ var StateOps = class {
|
|
|
1191
1192
|
}
|
|
1192
1193
|
async setSyncTableStatus(table, status, rowCount, pkHash) {
|
|
1193
1194
|
const stmt = await this.conn.prepare(
|
|
1194
|
-
`INSERT INTO
|
|
1195
|
+
`INSERT INTO ${SYNC_STATE_TABLE} (table_name, status, row_count, pk_hash, completed_at)
|
|
1195
1196
|
VALUES (?, ?, ?, ?, ?)
|
|
1196
1197
|
ON CONFLICT(table_name) DO UPDATE SET
|
|
1197
1198
|
status = excluded.status,
|
|
@@ -1203,7 +1204,7 @@ var StateOps = class {
|
|
|
1203
1204
|
}
|
|
1204
1205
|
async setSyncMeta(phase, snapshotSeq, sourcePeerId, requestId) {
|
|
1205
1206
|
const stmt = await this.conn.prepare(
|
|
1206
|
-
`INSERT INTO
|
|
1207
|
+
`INSERT INTO ${SYNC_STATE_TABLE} (table_name, status, snapshot_seq, source_peer_id, started_at, request_id)
|
|
1207
1208
|
VALUES ('__sync_meta__', ?, ?, ?, ?, ?)
|
|
1208
1209
|
ON CONFLICT(table_name) DO UPDATE SET
|
|
1209
1210
|
status = excluded.status,
|
|
@@ -1222,14 +1223,14 @@ var StateOps = class {
|
|
|
1222
1223
|
}
|
|
1223
1224
|
async getSyncState() {
|
|
1224
1225
|
const metaStmt = await this.conn.prepare(
|
|
1225
|
-
`SELECT status, snapshot_seq, source_peer_id FROM
|
|
1226
|
+
`SELECT status, snapshot_seq, source_peer_id FROM ${SYNC_STATE_TABLE} WHERE table_name = '__sync_meta__'`
|
|
1226
1227
|
);
|
|
1227
1228
|
const meta = await metaStmt.get();
|
|
1228
1229
|
if (!meta) {
|
|
1229
1230
|
return { phase: "ready", completedTables: [], snapshotSeq: null, sourcePeerId: null };
|
|
1230
1231
|
}
|
|
1231
1232
|
const completedStmt = await this.conn.prepare(
|
|
1232
|
-
`SELECT table_name FROM
|
|
1233
|
+
`SELECT table_name FROM ${SYNC_STATE_TABLE} WHERE table_name != '__sync_meta__' AND status = 'completed'`
|
|
1233
1234
|
);
|
|
1234
1235
|
const completedRows = await completedStmt.all();
|
|
1235
1236
|
return {
|
|
@@ -1240,7 +1241,7 @@ var StateOps = class {
|
|
|
1240
1241
|
};
|
|
1241
1242
|
}
|
|
1242
1243
|
async isSyncCompleted() {
|
|
1243
|
-
const stmt = await this.conn.prepare(`SELECT status FROM
|
|
1244
|
+
const stmt = await this.conn.prepare(`SELECT status FROM ${SYNC_STATE_TABLE} WHERE table_name = '__sync_meta__'`);
|
|
1244
1245
|
const row = await stmt.get();
|
|
1245
1246
|
return row?.status === "ready";
|
|
1246
1247
|
}
|
|
@@ -1267,7 +1268,7 @@ function mergeCandidate(current, candidate) {
|
|
|
1267
1268
|
|
|
1268
1269
|
// src/replication/log/log.ts
|
|
1269
1270
|
var ReplicationLog = class {
|
|
1270
|
-
constructor(conn, localNodeId, hlc, changesTable =
|
|
1271
|
+
constructor(conn, localNodeId, hlc, changesTable = CHANGES_TABLE, tracker) {
|
|
1271
1272
|
this.changesTable = changesTable;
|
|
1272
1273
|
this.pkResolver = new PkResolver(conn);
|
|
1273
1274
|
this.state = new StateOps(conn);
|
|
@@ -1672,7 +1673,7 @@ var LocalExecutor = class {
|
|
|
1672
1673
|
const r = await stmt.run(...bindValues);
|
|
1673
1674
|
if (isDdl) {
|
|
1674
1675
|
const ddlStmt = await tx.prepare(
|
|
1675
|
-
`INSERT INTO "
|
|
1676
|
+
`INSERT INTO "${CHANGES_TABLE}" (table_name, operation, row_id, new_data, node_id, tx_id, hlc)
|
|
1676
1677
|
VALUES ('__ddl__', 'DDL', '', ?, ?, ?, ?)`
|
|
1677
1678
|
);
|
|
1678
1679
|
const hlcVal = engine.hlc.now();
|
|
@@ -1738,7 +1739,7 @@ var LocalExecutor = class {
|
|
|
1738
1739
|
droppedTables.push(droppedTable);
|
|
1739
1740
|
}
|
|
1740
1741
|
const ddlStmt = await tx.prepare(
|
|
1741
|
-
`INSERT INTO "
|
|
1742
|
+
`INSERT INTO "${CHANGES_TABLE}" (table_name, operation, row_id, new_data, node_id, tx_id, hlc)
|
|
1742
1743
|
VALUES ('__ddl__', 'DDL', '', ?, ?, ?, ?)`
|
|
1743
1744
|
);
|
|
1744
1745
|
const hlcVal = engine.hlc.now();
|
|
@@ -1789,7 +1790,7 @@ var LocalExecutor = class {
|
|
|
1789
1790
|
hooks.droppedTables.push(droppedTable);
|
|
1790
1791
|
}
|
|
1791
1792
|
const ddlStmt = await tx.prepare(
|
|
1792
|
-
`INSERT INTO "
|
|
1793
|
+
`INSERT INTO "${CHANGES_TABLE}" (table_name, operation, row_id, new_data, node_id, tx_id, hlc)
|
|
1793
1794
|
VALUES ('__ddl__', 'DDL', '', ?, ?, ?, ?)`
|
|
1794
1795
|
);
|
|
1795
1796
|
const hlcVal = engine.hlc.now();
|
|
@@ -2433,7 +2434,7 @@ var SyncJoiner = class {
|
|
|
2433
2434
|
}
|
|
2434
2435
|
await engine.log.setLastAppliedSeq(fromPeerId, complete.snapshotSeq);
|
|
2435
2436
|
const recordStmt = await engine.writerConn.prepare(
|
|
2436
|
-
|
|
2437
|
+
`INSERT OR IGNORE INTO ${APPLIED_CHANGES_TABLE} (source_node_id, source_seq, applied_at) VALUES (?, ?, ?)`
|
|
2437
2438
|
);
|
|
2438
2439
|
await recordStmt.run(fromPeerId, complete.snapshotSeq.toString(), Date.now() / 1e3);
|
|
2439
2440
|
const previousApplied = engine.appliedSeqByPeer.get(fromPeerId) ?? 0n;
|
|
@@ -2929,7 +2930,7 @@ var ReplicationEngine = class extends EventEmitter {
|
|
|
2929
2930
|
this.nodeId = config.nodeId ?? generateNodeId();
|
|
2930
2931
|
this.hlc = new HLC(this.nodeId);
|
|
2931
2932
|
this.tracker = config.changeTracker;
|
|
2932
|
-
this.log = new ReplicationLog(writerConn, this.nodeId, this.hlc,
|
|
2933
|
+
this.log = new ReplicationLog(writerConn, this.nodeId, this.hlc, CHANGES_TABLE, this.tracker);
|
|
2933
2934
|
this.defaultResolver = config.defaultConflictResolver ?? new LWWResolver();
|
|
2934
2935
|
this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
2935
2936
|
this.batchIntervalMs = config.batchIntervalMs ?? DEFAULT_BATCH_INTERVAL_MS;
|
|
@@ -3122,7 +3123,7 @@ var ReplicationEngine = class extends EventEmitter {
|
|
|
3122
3123
|
}
|
|
3123
3124
|
async loadAppliedSeqs() {
|
|
3124
3125
|
const stmt = await this.writerConn.prepare(
|
|
3125
|
-
|
|
3126
|
+
`SELECT source_node_id, MAX(source_seq) AS max_seq FROM ${APPLIED_CHANGES_TABLE} GROUP BY source_node_id`
|
|
3126
3127
|
);
|
|
3127
3128
|
const rows = await stmt.all();
|
|
3128
3129
|
for (const row of rows) {
|