@sqlite-sync/core 0.6.0 → 0.7.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/dist/{chunk-3L4QG3BZ.js → chunk-LBYL5L4O.js} +371 -34
- package/dist/chunk-LBYL5L4O.js.map +1 -0
- package/dist/{crdt-sync-remote-source-CJrVzW-B.d.ts → crdt-sync-remote-source-yXp-iMEd.d.ts} +304 -185
- package/dist/index.d.ts +126 -18
- package/dist/index.js +296 -74
- package/dist/index.js.map +1 -1
- package/dist/reset-state-D6UslXaj.d.ts +19 -0
- package/dist/server.d.ts +2 -2
- package/dist/worker.d.ts +10 -3
- package/dist/worker.js +12 -2
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-3L4QG3BZ.js.map +0 -1
- package/dist/reset-state-Comk7IC0.d.ts +0 -81
|
@@ -171,6 +171,8 @@ function introspectDb(_db) {
|
|
|
171
171
|
isNullable: !col.notnull,
|
|
172
172
|
isAutoIncrementing: col.name === autoIncrementCol,
|
|
173
173
|
hasDefaultValue: col.dflt_value != null,
|
|
174
|
+
defaultValueSql: col.dflt_value == null ? null : String(col.dflt_value),
|
|
175
|
+
isPrimaryKey: col.pk > 0,
|
|
174
176
|
comment: void 0
|
|
175
177
|
}))
|
|
176
178
|
}
|
|
@@ -892,6 +894,276 @@ function applyMemoryDbSchema(db) {
|
|
|
892
894
|
);
|
|
893
895
|
}
|
|
894
896
|
|
|
897
|
+
// src/schema/validate-crdt-event.ts
|
|
898
|
+
var CrdtEventValidationError = class extends Error {
|
|
899
|
+
constructor(errors) {
|
|
900
|
+
super(`Invalid CRDT events: ${errors.join("; ")}`);
|
|
901
|
+
this.errors = errors;
|
|
902
|
+
this.name = "CrdtEventValidationError";
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
var crdtEventTypes = ["item-created", "item-updated", "item-deleted"];
|
|
906
|
+
function validateNewCrdtEvent(schema, input) {
|
|
907
|
+
const errors = [];
|
|
908
|
+
if (!isRecord(input)) {
|
|
909
|
+
return { success: false, errors: ["Event must be an object"] };
|
|
910
|
+
}
|
|
911
|
+
const type = input.type;
|
|
912
|
+
const dataset = input.dataset;
|
|
913
|
+
const itemId = input.item_id;
|
|
914
|
+
let payload = input.payload;
|
|
915
|
+
let payloadParseFailed = false;
|
|
916
|
+
if (typeof payload === "string") {
|
|
917
|
+
try {
|
|
918
|
+
payload = JSON.parse(payload);
|
|
919
|
+
} catch {
|
|
920
|
+
payloadParseFailed = true;
|
|
921
|
+
errors.push(`Event payload is not valid JSON`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (!isCrdtEventType(type)) {
|
|
925
|
+
errors.push(`Invalid event type "${String(type)}"`);
|
|
926
|
+
}
|
|
927
|
+
if (typeof dataset !== "string") {
|
|
928
|
+
errors.push(`Event dataset must be a string`);
|
|
929
|
+
}
|
|
930
|
+
if (typeof itemId !== "string") {
|
|
931
|
+
errors.push(`Event item_id must be a string`);
|
|
932
|
+
}
|
|
933
|
+
if (!payloadParseFailed && !isRecord(payload)) {
|
|
934
|
+
errors.push(`Event payload must be an object`);
|
|
935
|
+
}
|
|
936
|
+
const datasetMatch = typeof dataset === "string" ? findTableForDataset(schema, dataset) : null;
|
|
937
|
+
if (typeof dataset === "string" && !datasetMatch) {
|
|
938
|
+
errors.push(`Unknown dataset "${dataset}"`);
|
|
939
|
+
}
|
|
940
|
+
if (isCrdtEventType(type) && datasetMatch && isRecord(payload)) {
|
|
941
|
+
switch (type) {
|
|
942
|
+
case "item-created": {
|
|
943
|
+
const result = datasetMatch.table.validatePayload(payload, { event: "item-created" });
|
|
944
|
+
if (!result.success) {
|
|
945
|
+
errors.push(...result.errors.map((error) => `payload: ${error}`));
|
|
946
|
+
}
|
|
947
|
+
if (typeof itemId === "string" && "id" in payload && payload.id !== itemId) {
|
|
948
|
+
errors.push(`payload: Column "id" must match item_id "${itemId}"`);
|
|
949
|
+
}
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
case "item-updated": {
|
|
953
|
+
const result = datasetMatch.table.validatePayload(payload, { event: "item-updated" });
|
|
954
|
+
if (!result.success) {
|
|
955
|
+
errors.push(...result.errors.map((error) => `payload: ${error}`));
|
|
956
|
+
}
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
case "item-deleted":
|
|
960
|
+
if (Object.keys(payload).length > 0) {
|
|
961
|
+
errors.push(`payload: item-deleted events must have an empty payload`);
|
|
962
|
+
}
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (errors.length > 0) {
|
|
967
|
+
return { success: false, errors };
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
success: true,
|
|
971
|
+
event: {
|
|
972
|
+
type,
|
|
973
|
+
dataset: datasetMatch?.baseTableName ?? dataset,
|
|
974
|
+
item_id: itemId,
|
|
975
|
+
payload
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
function findTableForDataset(schema, dataset) {
|
|
980
|
+
const config = schema.tablesConfig.find(
|
|
981
|
+
(tableConfig) => tableConfig.baseTableName === dataset || tableConfig.crdtTableName === dataset
|
|
982
|
+
);
|
|
983
|
+
if (config) {
|
|
984
|
+
const table2 = schema.tables[config.crdtTableName];
|
|
985
|
+
return table2 ? { table: table2, baseTableName: config.baseTableName } : null;
|
|
986
|
+
}
|
|
987
|
+
const table = schema.tables[dataset];
|
|
988
|
+
return table ? { table, baseTableName: dataset } : null;
|
|
989
|
+
}
|
|
990
|
+
function isRecord(value) {
|
|
991
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
992
|
+
}
|
|
993
|
+
function isCrdtEventType(value) {
|
|
994
|
+
return typeof value === "string" && crdtEventTypes.includes(value);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// src/schema/verify-sync-schema.ts
|
|
998
|
+
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
|
999
|
+
var acceptedDataTypes = {
|
|
1000
|
+
text: ["TEXT"],
|
|
1001
|
+
integer: ["INTEGER", "INT"],
|
|
1002
|
+
real: ["REAL", "DOUBLE", "FLOAT"],
|
|
1003
|
+
boolean: ["BOOLEAN", "INTEGER", "INT"],
|
|
1004
|
+
enum: ["TEXT"]
|
|
1005
|
+
};
|
|
1006
|
+
var sqliteModule = null;
|
|
1007
|
+
async function verifySyncSchema(schema) {
|
|
1008
|
+
if (!sqliteModule) {
|
|
1009
|
+
sqliteModule = await sqlite3InitModule();
|
|
1010
|
+
}
|
|
1011
|
+
const sqlite3 = sqliteModule;
|
|
1012
|
+
const db = new SQLiteDbWrapper({
|
|
1013
|
+
db: () => new sqlite3.oo1.DB({ filename: ":memory:" }),
|
|
1014
|
+
logger: () => {
|
|
1015
|
+
},
|
|
1016
|
+
loggerPrefix: "schema-verify",
|
|
1017
|
+
sqlite3
|
|
1018
|
+
});
|
|
1019
|
+
try {
|
|
1020
|
+
const migrator = createMigrator({
|
|
1021
|
+
migrations: schema.migrations,
|
|
1022
|
+
schemaVersion: createStoredValue({ initialValue: -1 })
|
|
1023
|
+
});
|
|
1024
|
+
try {
|
|
1025
|
+
migrator.migrateDbToLatest({
|
|
1026
|
+
startTransaction: (callback) => {
|
|
1027
|
+
db.executeTransaction(
|
|
1028
|
+
(tx) => callback({ execute: (sql4, parameters, meta) => tx.execute({ sql: sql4, parameters }, meta) })
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
return [
|
|
1034
|
+
{
|
|
1035
|
+
table: "(migrations)",
|
|
1036
|
+
message: `migration replay failed on an empty database: ${error instanceof Error ? error.message : String(error)}`
|
|
1037
|
+
}
|
|
1038
|
+
];
|
|
1039
|
+
}
|
|
1040
|
+
const introspection = introspectDb(db);
|
|
1041
|
+
const issues = [];
|
|
1042
|
+
for (const { crdtTableName, baseTableName } of schema.tablesConfig) {
|
|
1043
|
+
const declared = schema.tables[crdtTableName];
|
|
1044
|
+
if (!declared) continue;
|
|
1045
|
+
verifyTable(db, baseTableName, declared.columns, introspection[baseTableName], issues);
|
|
1046
|
+
}
|
|
1047
|
+
return issues;
|
|
1048
|
+
} finally {
|
|
1049
|
+
db.close();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
function formatSchemaVerificationIssues(issues) {
|
|
1053
|
+
const lines = issues.map(
|
|
1054
|
+
(issue) => ` - table "${issue.table}"${issue.column ? ` column "${issue.column}"` : ""}: ${issue.message}`
|
|
1055
|
+
);
|
|
1056
|
+
return `sync schema verification failed (${issues.length} issue${issues.length === 1 ? "" : "s"}):
|
|
1057
|
+
${lines.join("\n")}`;
|
|
1058
|
+
}
|
|
1059
|
+
function verifyTable(db, table, declaredColumns, actual, issues) {
|
|
1060
|
+
if (!actual) {
|
|
1061
|
+
issues.push({
|
|
1062
|
+
table,
|
|
1063
|
+
message: "declared in the schema but missing after migrations \u2014 add a createTable step in a new version"
|
|
1064
|
+
});
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (actual.isView) {
|
|
1068
|
+
issues.push({ table, message: "expected a base table, but migrations created a view with this name" });
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const actualColumns = new Map(actual.columns.map((column) => [column.name, column]));
|
|
1072
|
+
for (const [name, meta] of Object.entries(declaredColumns)) {
|
|
1073
|
+
const column = actualColumns.get(name);
|
|
1074
|
+
if (!column) {
|
|
1075
|
+
issues.push({
|
|
1076
|
+
table,
|
|
1077
|
+
column: name,
|
|
1078
|
+
message: "declared in the schema but missing after migrations \u2014 add an addColumn step in a new version"
|
|
1079
|
+
});
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
const accepted = acceptedDataTypes[meta.kind];
|
|
1083
|
+
if (!accepted.includes(column.dataType.toUpperCase())) {
|
|
1084
|
+
issues.push({
|
|
1085
|
+
table,
|
|
1086
|
+
column: name,
|
|
1087
|
+
message: `type mismatch \u2014 schema declares ${meta.kind} (${accepted.join("/")}), migrated column is ${column.dataType.toUpperCase()}`
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
if (column.isNullable !== meta.nullable) {
|
|
1091
|
+
issues.push({
|
|
1092
|
+
table,
|
|
1093
|
+
column: name,
|
|
1094
|
+
message: meta.nullable ? "schema declares the column nullable, but the migrated column is NOT NULL" : "schema declares the column NOT NULL, but the migrated column is nullable"
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
if (name === "id" && !column.isPrimaryKey) {
|
|
1098
|
+
issues.push({ table, column: name, message: "the id column must be the PRIMARY KEY" });
|
|
1099
|
+
}
|
|
1100
|
+
verifyDefault(db, table, name, meta, column.defaultValueSql, issues);
|
|
1101
|
+
}
|
|
1102
|
+
for (const column of actual.columns) {
|
|
1103
|
+
if (!(column.name in declaredColumns)) {
|
|
1104
|
+
issues.push({
|
|
1105
|
+
table,
|
|
1106
|
+
column: column.name,
|
|
1107
|
+
message: "created by migrations but not declared in the schema \u2014 sync event payloads include every column, so declare it (or drop it in a new version)"
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function verifyDefault(db, table, column, meta, defaultValueSql, issues) {
|
|
1113
|
+
if (meta.hasDefault && meta.defaultValue !== null && defaultValueSql === null) {
|
|
1114
|
+
issues.push({
|
|
1115
|
+
table,
|
|
1116
|
+
column,
|
|
1117
|
+
message: `schema declares default ${formatExpected(meta)}, but the migrated column has no default`
|
|
1118
|
+
});
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (!meta.hasDefault && defaultValueSql !== null) {
|
|
1122
|
+
issues.push({
|
|
1123
|
+
table,
|
|
1124
|
+
column,
|
|
1125
|
+
message: `migrated column has default ${defaultValueSql}, but the schema declares none`
|
|
1126
|
+
});
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (!meta.hasDefault || defaultValueSql === null) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
let actual;
|
|
1133
|
+
try {
|
|
1134
|
+
actual = db.execute({
|
|
1135
|
+
sql: `SELECT (${defaultValueSql}) AS value`,
|
|
1136
|
+
parameters: []
|
|
1137
|
+
}).rows[0]?.value;
|
|
1138
|
+
} catch {
|
|
1139
|
+
actual = void 0;
|
|
1140
|
+
}
|
|
1141
|
+
if (actual !== expectedDefaultValue(meta)) {
|
|
1142
|
+
issues.push({
|
|
1143
|
+
table,
|
|
1144
|
+
column,
|
|
1145
|
+
message: `default mismatch \u2014 schema declares ${formatExpected(meta)}, migrated column default is ${defaultValueSql}`
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function expectedDefaultValue(meta) {
|
|
1150
|
+
if (meta.defaultValue === null) return null;
|
|
1151
|
+
switch (meta.kind) {
|
|
1152
|
+
case "boolean":
|
|
1153
|
+
return meta.defaultValue ? 1 : 0;
|
|
1154
|
+
case "integer":
|
|
1155
|
+
case "real":
|
|
1156
|
+
return Number(meta.defaultValue);
|
|
1157
|
+
case "text":
|
|
1158
|
+
case "enum":
|
|
1159
|
+
return String(meta.defaultValue);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
function formatExpected(meta) {
|
|
1163
|
+
const expected = expectedDefaultValue(meta);
|
|
1164
|
+
return typeof expected === "string" ? `'${expected}'` : String(expected);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
895
1167
|
// src/sqlite-crdt/apply-crdt-event.ts
|
|
896
1168
|
var createSQLiteCrdtApplyFunction = ({
|
|
897
1169
|
db,
|
|
@@ -1084,6 +1356,16 @@ function createCrdtStorage(storage) {
|
|
|
1084
1356
|
let localSyncId = storage.initialLocalSyncId;
|
|
1085
1357
|
const db = storage.db;
|
|
1086
1358
|
const crdtEventsTable = storage.dbConfig.eventsTable.fullIdentifier;
|
|
1359
|
+
const getInitialSequentialSyncId = () => {
|
|
1360
|
+
const [firstPendingEvent] = db.executePrepared(
|
|
1361
|
+
"get-first-pending-event",
|
|
1362
|
+
{ status: "pending" },
|
|
1363
|
+
(db2, params) => db2.selectFrom(crdtEventsTable).select("sync_id").where("status", "=", params("status")).orderBy("sync_id", "asc").limit(sql3.lit(1)),
|
|
1364
|
+
{ loggerLevel: "system" }
|
|
1365
|
+
);
|
|
1366
|
+
return firstPendingEvent ? firstPendingEvent.sync_id - 1 : localSyncId;
|
|
1367
|
+
};
|
|
1368
|
+
let sequentialSyncId = getInitialSequentialSyncId();
|
|
1087
1369
|
const eventTarget = createTypedEventTarget();
|
|
1088
1370
|
const persistEvent = (tx, event) => {
|
|
1089
1371
|
tx.executePrepared(
|
|
@@ -1138,38 +1420,77 @@ function createCrdtStorage(storage) {
|
|
|
1138
1420
|
};
|
|
1139
1421
|
const notifyEventApplied = (event) => {
|
|
1140
1422
|
if (event.status === "applied") {
|
|
1141
|
-
|
|
1423
|
+
queueMicrotask(() => {
|
|
1424
|
+
storage.onEventApplied?.(event);
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
const applyOwnEvents = (events) => {
|
|
1429
|
+
const schema = storage.schema;
|
|
1430
|
+
if (!schema) {
|
|
1431
|
+
throw new Error("applyOwnEvents requires a sync schema; none was provided to createCrdtStorage");
|
|
1432
|
+
}
|
|
1433
|
+
const validatedEvents = [];
|
|
1434
|
+
const errors = [];
|
|
1435
|
+
for (const [index, event] of events.entries()) {
|
|
1436
|
+
const result = validateNewCrdtEvent(schema, {
|
|
1437
|
+
type: event.type,
|
|
1438
|
+
dataset: event.dataset,
|
|
1439
|
+
item_id: event.item_id,
|
|
1440
|
+
payload: event.payload
|
|
1441
|
+
});
|
|
1442
|
+
if (!result.success) {
|
|
1443
|
+
errors.push(...result.errors.map((error) => `[${index}] ${error}`));
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
validatedEvents.push(result.event);
|
|
1142
1447
|
}
|
|
1448
|
+
if (errors.length > 0) {
|
|
1449
|
+
throw new CrdtEventValidationError(errors);
|
|
1450
|
+
}
|
|
1451
|
+
db.executeTransaction((tx) => {
|
|
1452
|
+
for (const event of validatedEvents) {
|
|
1453
|
+
const persistedEvent = {
|
|
1454
|
+
schema_version: storage.migrator.currentSchemaVersion,
|
|
1455
|
+
timestamp: serializeHLC(storage.hlc.getNextHLC()),
|
|
1456
|
+
type: event.type,
|
|
1457
|
+
dataset: event.dataset,
|
|
1458
|
+
item_id: event.item_id,
|
|
1459
|
+
origin: "own-applied",
|
|
1460
|
+
source_node_id: storage.nodeId,
|
|
1461
|
+
payload: JSON.stringify(event.payload),
|
|
1462
|
+
sync_id: ++localSyncId,
|
|
1463
|
+
status: "pending"
|
|
1464
|
+
};
|
|
1465
|
+
persistEvent(tx, persistedEvent);
|
|
1466
|
+
applyCrdtEvent(persistedEvent);
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
void processEnqueuedEvents();
|
|
1143
1470
|
};
|
|
1144
|
-
const
|
|
1471
|
+
const applyOwnEventFromTransaction = (tx, event) => {
|
|
1145
1472
|
const persistedEvent = {
|
|
1146
1473
|
schema_version: storage.migrator.currentSchemaVersion,
|
|
1147
1474
|
timestamp: serializeHLC(storage.hlc.getNextHLC()),
|
|
1148
1475
|
type: event.type,
|
|
1149
1476
|
dataset: event.dataset,
|
|
1150
1477
|
item_id: event.item_id,
|
|
1151
|
-
origin: "own",
|
|
1478
|
+
origin: "own-applied",
|
|
1152
1479
|
source_node_id: storage.nodeId,
|
|
1153
1480
|
payload: event.payload,
|
|
1154
1481
|
sync_id: ++localSyncId,
|
|
1155
1482
|
status: "pending"
|
|
1156
1483
|
};
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
persistEvent(tx, persistedEvent);
|
|
1160
|
-
processPersistedEvent(tx, persistedEvent);
|
|
1161
|
-
persistEventHlcAccumulator();
|
|
1162
|
-
});
|
|
1163
|
-
} else {
|
|
1164
|
-
persistEvent(db, persistedEvent);
|
|
1165
|
-
processPersistedEvent(db, persistedEvent);
|
|
1166
|
-
persistEventHlcAccumulator();
|
|
1167
|
-
}
|
|
1484
|
+
persistEvent(tx, persistedEvent);
|
|
1485
|
+
applyCrdtEvent(persistedEvent);
|
|
1168
1486
|
};
|
|
1169
|
-
const dispatchEventsApplied = (
|
|
1487
|
+
const dispatchEventsApplied = (previousSequentialSyncId) => {
|
|
1488
|
+
if (sequentialSyncId <= previousSequentialSyncId) {
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1170
1491
|
eventTarget.dispatchEvent("events-applied", {
|
|
1171
|
-
syncId,
|
|
1172
|
-
eventHlcSum: eventHlcAccumulator?.current ?? null
|
|
1492
|
+
syncId: sequentialSyncId,
|
|
1493
|
+
eventHlcSum: sequentialSyncId === localSyncId ? eventHlcAccumulator?.current ?? null : null
|
|
1173
1494
|
});
|
|
1174
1495
|
};
|
|
1175
1496
|
const hasPendingEvents = () => {
|
|
@@ -1188,7 +1509,8 @@ function createCrdtStorage(storage) {
|
|
|
1188
1509
|
status: options.status ?? null,
|
|
1189
1510
|
afterSyncId: options.afterSyncId ?? null,
|
|
1190
1511
|
excludeOrigin: options.excludeOrigin ?? null,
|
|
1191
|
-
excludeNodeId: options.excludeNodeId ?? null
|
|
1512
|
+
excludeNodeId: options.excludeNodeId ?? null,
|
|
1513
|
+
maxSyncId: options.status === "applied" ? sequentialSyncId : localSyncId
|
|
1192
1514
|
};
|
|
1193
1515
|
const filterKeys = [
|
|
1194
1516
|
queryParams.excludeNodeId ? "nodeid" : "no-nodeid",
|
|
@@ -1197,17 +1519,22 @@ function createCrdtStorage(storage) {
|
|
|
1197
1519
|
const events = db.executePrepared(
|
|
1198
1520
|
`get-events-batch-${filterKeys.join("-")}`,
|
|
1199
1521
|
queryParams,
|
|
1200
|
-
(db2, params) => db2.selectFrom(crdtEventsTable).where("sync_id", ">", params("afterSyncId")).where("status", "=", params("status")).$if(!!queryParams.excludeNodeId, (qb) => qb.where("source_node_id", "!=", params("excludeNodeId"))).$if(!!queryParams.excludeOrigin, (qb) => qb.where("origin", "!=", params("excludeOrigin"))).selectAll().limit(params("limit")).orderBy("sync_id", "asc"),
|
|
1522
|
+
(db2, params) => db2.selectFrom(crdtEventsTable).where("sync_id", ">", params("afterSyncId")).where("sync_id", "<=", params("maxSyncId")).where("status", "=", params("status")).$if(!!queryParams.excludeNodeId, (qb) => qb.where("source_node_id", "!=", params("excludeNodeId"))).$if(!!queryParams.excludeOrigin, (qb) => qb.where("origin", "!=", params("excludeOrigin"))).selectAll().limit(params("limit")).orderBy("sync_id", "asc"),
|
|
1201
1523
|
{ loggerLevel: "system" }
|
|
1202
1524
|
);
|
|
1203
1525
|
const hasMore = events.length > limit;
|
|
1204
1526
|
if (hasMore) {
|
|
1205
1527
|
events.pop();
|
|
1206
1528
|
}
|
|
1529
|
+
const lastReturnedSyncId = events[events.length - 1]?.sync_id ?? options.afterSyncId ?? 0;
|
|
1530
|
+
let nextSyncId = lastReturnedSyncId;
|
|
1531
|
+
if (!hasMore && options.status === "applied") {
|
|
1532
|
+
nextSyncId = Math.max(lastReturnedSyncId, queryParams.maxSyncId);
|
|
1533
|
+
}
|
|
1207
1534
|
return {
|
|
1208
1535
|
events,
|
|
1209
1536
|
hasMore,
|
|
1210
|
-
nextSyncId
|
|
1537
|
+
nextSyncId
|
|
1211
1538
|
};
|
|
1212
1539
|
};
|
|
1213
1540
|
const checkIsQuiescent = (pushedSyncId) => {
|
|
@@ -1278,6 +1605,10 @@ function createCrdtStorage(storage) {
|
|
|
1278
1605
|
throw new Error(`Event ${event.sync_id} is not pending`);
|
|
1279
1606
|
}
|
|
1280
1607
|
try {
|
|
1608
|
+
if (event.origin === "own-applied") {
|
|
1609
|
+
event.status = "applied";
|
|
1610
|
+
return event;
|
|
1611
|
+
}
|
|
1281
1612
|
if (hasAcceptedEventWithTimestamp(tx, event)) {
|
|
1282
1613
|
event.status = "deduped";
|
|
1283
1614
|
return event;
|
|
@@ -1288,7 +1619,6 @@ function createCrdtStorage(storage) {
|
|
|
1288
1619
|
if (isNoOpCrdtEventPayload(event.payload)) {
|
|
1289
1620
|
applyCrdtEvent(event);
|
|
1290
1621
|
event.status = "applied";
|
|
1291
|
-
eventHlcAccumulator?.add(event.timestamp);
|
|
1292
1622
|
return event;
|
|
1293
1623
|
}
|
|
1294
1624
|
const migratedEvent = storage.migrator.migrateEvent(event, storage.migrator.latestSchemaVersion);
|
|
@@ -1297,7 +1627,6 @@ function createCrdtStorage(storage) {
|
|
|
1297
1627
|
event.payload = CRDT_EVENT_NO_OP_PAYLOAD;
|
|
1298
1628
|
applyCrdtEvent(event);
|
|
1299
1629
|
event.status = "applied";
|
|
1300
|
-
eventHlcAccumulator?.add(event.timestamp);
|
|
1301
1630
|
return event;
|
|
1302
1631
|
}
|
|
1303
1632
|
event.schema_version = migratedEvent.schema_version;
|
|
@@ -1307,7 +1636,6 @@ function createCrdtStorage(storage) {
|
|
|
1307
1636
|
event.payload = migratedEvent.payload;
|
|
1308
1637
|
applyCrdtEvent(event);
|
|
1309
1638
|
event.status = "applied";
|
|
1310
|
-
eventHlcAccumulator?.add(event.timestamp);
|
|
1311
1639
|
} catch (error) {
|
|
1312
1640
|
console.error("Error applying enqueued CRDT event", error);
|
|
1313
1641
|
event.status = "failed";
|
|
@@ -1345,26 +1673,28 @@ function createCrdtStorage(storage) {
|
|
|
1345
1673
|
if (hasMore) {
|
|
1346
1674
|
events.pop();
|
|
1347
1675
|
}
|
|
1676
|
+
const previousSequentialSyncId = sequentialSyncId;
|
|
1348
1677
|
if (events.length === 0) {
|
|
1678
|
+
sequentialSyncId = localSyncId;
|
|
1679
|
+
dispatchEventsApplied(previousSequentialSyncId);
|
|
1349
1680
|
break;
|
|
1350
1681
|
}
|
|
1351
|
-
let appliedSyncId = null;
|
|
1352
1682
|
const failedRemoteSyncIds = [];
|
|
1353
1683
|
db.executeTransaction((tx) => {
|
|
1354
1684
|
for (const event of events) {
|
|
1355
1685
|
processPersistedEvent(tx, event);
|
|
1356
|
-
notifyEventApplied(event);
|
|
1357
1686
|
if (event.status === "applied") {
|
|
1358
|
-
|
|
1359
|
-
}
|
|
1687
|
+
eventHlcAccumulator?.add(event.timestamp);
|
|
1688
|
+
}
|
|
1689
|
+
notifyEventApplied(event);
|
|
1690
|
+
if (event.status === "failed" && event.origin === "remote") {
|
|
1360
1691
|
failedRemoteSyncIds.push(event.sync_id);
|
|
1361
1692
|
}
|
|
1362
1693
|
}
|
|
1363
1694
|
persistEventHlcAccumulator();
|
|
1364
1695
|
});
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
}
|
|
1696
|
+
sequentialSyncId = hasMore ? events[events.length - 1]?.sync_id ?? sequentialSyncId : localSyncId;
|
|
1697
|
+
dispatchEventsApplied(previousSequentialSyncId);
|
|
1368
1698
|
for (const syncId of failedRemoteSyncIds) {
|
|
1369
1699
|
eventTarget.dispatchEvent("remote-event-apply-failed", { syncId });
|
|
1370
1700
|
}
|
|
@@ -1377,12 +1707,15 @@ function createCrdtStorage(storage) {
|
|
|
1377
1707
|
enqueueLocalEvents,
|
|
1378
1708
|
enqueueOwnEvents,
|
|
1379
1709
|
enqueueRemoteEvents,
|
|
1380
|
-
|
|
1381
|
-
dispatchEventsApplied,
|
|
1710
|
+
applyOwnEvents,
|
|
1382
1711
|
checkIsQuiescent,
|
|
1383
1712
|
getEventHlcAccumulator: () => eventHlcAccumulator?.current ?? null,
|
|
1384
1713
|
addEventListener: eventTarget.addEventListener,
|
|
1385
|
-
removeEventListener: eventTarget.removeEventListener
|
|
1714
|
+
removeEventListener: eventTarget.removeEventListener,
|
|
1715
|
+
internal: {
|
|
1716
|
+
applyOwnEventFromTransaction,
|
|
1717
|
+
processEnqueuedEvents
|
|
1718
|
+
}
|
|
1386
1719
|
};
|
|
1387
1720
|
}
|
|
1388
1721
|
|
|
@@ -1850,6 +2183,10 @@ export {
|
|
|
1850
2183
|
runSystemMigrations,
|
|
1851
2184
|
applyWorkerDbSchema,
|
|
1852
2185
|
applyMemoryDbSchema,
|
|
2186
|
+
CrdtEventValidationError,
|
|
2187
|
+
validateNewCrdtEvent,
|
|
2188
|
+
verifySyncSchema,
|
|
2189
|
+
formatSchemaVerificationIssues,
|
|
1853
2190
|
createSQLiteCrdtApplyFunction,
|
|
1854
2191
|
createCrdtApplyFunction,
|
|
1855
2192
|
createCrdtStorage,
|
|
@@ -1868,4 +2205,4 @@ export {
|
|
|
1868
2205
|
createResetStateStore,
|
|
1869
2206
|
createReloadRequestHandler
|
|
1870
2207
|
};
|
|
1871
|
-
//# sourceMappingURL=chunk-
|
|
2208
|
+
//# sourceMappingURL=chunk-LBYL5L4O.js.map
|