@sqlite-sync/core 0.3.0 → 0.4.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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/{chunk-RKBTBPNC.js → chunk-DYAQIVJO.js} +103 -35
- package/dist/chunk-DYAQIVJO.js.map +1 -0
- package/dist/{crdt-sync-remote-source-Da77s4k0.d.ts → crdt-sync-remote-source-DyEELVsx.d.ts} +9 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/{reset-state-0LGwO78x.d.ts → reset-state-BeduI9vB.d.ts} +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/worker.d.ts +6 -4
- package/dist/worker.js +2 -4
- package/dist/worker.js.map +1 -1
- package/package.json +9 -7
- package/dist/chunk-RKBTBPNC.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sqlite-sync contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @sqlite-sync/core
|
|
2
|
+
|
|
3
|
+
Core sync engine for [sqlite-sync](https://github.com/krolebord-dev/sqlite-sync) — a local-first SQLite sync engine for web apps, with reactive queries, offline persistence, and CRDT-based replication.
|
|
4
|
+
|
|
5
|
+
This package provides:
|
|
6
|
+
|
|
7
|
+
- `createSyncedDb()` for client orchestration (worker attach, snapshot hydration, sync state).
|
|
8
|
+
- The schema builder (`createSyncDbSchema`) and migrations (`createMigrations`).
|
|
9
|
+
- Live query primitives (`db.createLiveQuery(...)`), typed through [Kysely](https://kysely.dev/).
|
|
10
|
+
- CRDT primitives with Last-Write-Wins per-field replication and HLC timestamps.
|
|
11
|
+
- The worker runtime (`@sqlite-sync/core/worker`) and server protocol types (`@sqlite-sync/core/server`).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @sqlite-sync/core kysely
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For React bindings, add [`@sqlite-sync/react`](https://www.npmjs.com/package/@sqlite-sync/react).
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createMigrations, createSyncDbSchema, createSyncedDb } from "@sqlite-sync/core";
|
|
25
|
+
|
|
26
|
+
type Todo = {
|
|
27
|
+
id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
completed: boolean;
|
|
30
|
+
tombstone?: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const migrations = createMigrations((b) => ({
|
|
34
|
+
0: [
|
|
35
|
+
b.createTable("_todo", (t) =>
|
|
36
|
+
t
|
|
37
|
+
.addColumn("id", "text", (col) => col.primaryKey().notNull())
|
|
38
|
+
.addColumn("title", "text", (col) => col.notNull())
|
|
39
|
+
.addColumn("completed", "boolean", (col) => col.notNull().defaultTo(false))
|
|
40
|
+
.addColumn("tombstone", "boolean", (col) => col.notNull().defaultTo(false)),
|
|
41
|
+
),
|
|
42
|
+
],
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
export const syncDbSchema = createSyncDbSchema({ migrations })
|
|
46
|
+
.addTable<Todo>()
|
|
47
|
+
.withConfig({ baseTableName: "_todo", crdtTableName: "todo" })
|
|
48
|
+
.build();
|
|
49
|
+
|
|
50
|
+
const worker = new Worker(new URL("./db-worker.ts", import.meta.url), { type: "module" });
|
|
51
|
+
|
|
52
|
+
export const db = await createSyncedDb({
|
|
53
|
+
dbId: "app-db",
|
|
54
|
+
worker,
|
|
55
|
+
workerProps: undefined,
|
|
56
|
+
syncDbSchema,
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Start the worker (remote sync optional):
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// db-worker.ts
|
|
64
|
+
import { startDbWorker } from "@sqlite-sync/core/worker";
|
|
65
|
+
import { syncDbSchema } from "./db-schema";
|
|
66
|
+
|
|
67
|
+
await startDbWorker({ syncDbSchema });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Requirements
|
|
71
|
+
|
|
72
|
+
- Browser: Web Workers + Web Locks + an OPFS-capable SQLite WASM environment.
|
|
73
|
+
- Peer dependency: `kysely` (`^0.28.0 || ^0.29.0`).
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
See the [full documentation](https://github.com/krolebord-dev/sqlite-sync/blob/main/docs.md) and the [project README](https://github.com/krolebord-dev/sqlite-sync) for guides, recovery/storage versioning, and the Cloudflare sync backend.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -1392,8 +1392,67 @@ var createCrdtSyncProducer = ({ storage, broadcastEvents }) => {
|
|
|
1392
1392
|
});
|
|
1393
1393
|
};
|
|
1394
1394
|
|
|
1395
|
+
// src/sqlite-crdt/retry-remote-operation.ts
|
|
1396
|
+
var REMOTE_RETRY_OPTIONS = {
|
|
1397
|
+
maxAttempts: 3,
|
|
1398
|
+
backoffBaseMs: 100,
|
|
1399
|
+
backoffExponent: 1.5,
|
|
1400
|
+
backoffJitterMs: 150,
|
|
1401
|
+
timeoutMs: 1e4
|
|
1402
|
+
};
|
|
1403
|
+
var RetryTimeoutError = class extends Error {
|
|
1404
|
+
constructor(message, previous) {
|
|
1405
|
+
super(message);
|
|
1406
|
+
this.previous = previous;
|
|
1407
|
+
this.name = "TimeoutError";
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
var applyJitter = (delayMs, maxJitterMs) => {
|
|
1411
|
+
const jitter = Math.random() * maxJitterMs * (Math.random() > 0.5 ? 1 : -1);
|
|
1412
|
+
return Math.max(0, delayMs + jitter);
|
|
1413
|
+
};
|
|
1414
|
+
var delay = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1415
|
+
var withTimeout = async (operation, timeoutMs, previousError) => {
|
|
1416
|
+
let timeoutId;
|
|
1417
|
+
try {
|
|
1418
|
+
return await Promise.race([
|
|
1419
|
+
operation(),
|
|
1420
|
+
new Promise((_, reject) => {
|
|
1421
|
+
timeoutId = setTimeout(
|
|
1422
|
+
() => reject(new RetryTimeoutError("Remote operation timed out", previousError)),
|
|
1423
|
+
timeoutMs
|
|
1424
|
+
);
|
|
1425
|
+
})
|
|
1426
|
+
]);
|
|
1427
|
+
} finally {
|
|
1428
|
+
if (timeoutId) {
|
|
1429
|
+
clearTimeout(timeoutId);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
var retryRemoteOperation = async (operation, options) => {
|
|
1434
|
+
let lastError;
|
|
1435
|
+
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
|
|
1436
|
+
try {
|
|
1437
|
+
return await withTimeout(operation, options.timeoutMs, lastError);
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
lastError = error;
|
|
1440
|
+
if (attempt >= options.maxAttempts) {
|
|
1441
|
+
throw error;
|
|
1442
|
+
}
|
|
1443
|
+
const backoffDelay = applyJitter(
|
|
1444
|
+
options.backoffBaseMs * options.backoffExponent ** (attempt - 1),
|
|
1445
|
+
options.backoffJitterMs
|
|
1446
|
+
);
|
|
1447
|
+
if (backoffDelay > 0) {
|
|
1448
|
+
await delay(backoffDelay);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
throw lastError;
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1395
1455
|
// src/sqlite-crdt/crdt-sync-remote-source.ts
|
|
1396
|
-
import retryAsPromised from "retry-as-promised";
|
|
1397
1456
|
var SchemaVersionMismatchError = class extends Error {
|
|
1398
1457
|
constructor(remoteSchemaVersion, localSchemaVersion) {
|
|
1399
1458
|
super(`Schema version mismatch: remote ${remoteSchemaVersion} != local ${localSchemaVersion}`);
|
|
@@ -1412,10 +1471,15 @@ var createCrdtSyncRemoteSource = ({
|
|
|
1412
1471
|
remoteFactory
|
|
1413
1472
|
}) => {
|
|
1414
1473
|
const eventTarget = createTypedEventTarget();
|
|
1415
|
-
let remoteState = {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1474
|
+
let remoteState = {
|
|
1475
|
+
type: "offline",
|
|
1476
|
+
reason: "NOT_INITIALIZED",
|
|
1477
|
+
deSynced: false,
|
|
1478
|
+
schemaVersionMismatched: false
|
|
1479
|
+
};
|
|
1480
|
+
const patchRemoteState = (state) => {
|
|
1481
|
+
remoteState = { ...remoteState, ...state };
|
|
1482
|
+
eventTarget.dispatchEvent("state-changed", remoteState.type);
|
|
1419
1483
|
};
|
|
1420
1484
|
const initRemote = ensureSingletonExecution(
|
|
1421
1485
|
async () => {
|
|
@@ -1424,10 +1488,10 @@ var createCrdtSyncRemoteSource = ({
|
|
|
1424
1488
|
}
|
|
1425
1489
|
if (!remoteFactory) {
|
|
1426
1490
|
console.warn("Remote source factory not provided. Going offline.");
|
|
1427
|
-
|
|
1491
|
+
patchRemoteState({ type: "offline", reason: "NOT_INITIALIZED" });
|
|
1428
1492
|
return;
|
|
1429
1493
|
}
|
|
1430
|
-
|
|
1494
|
+
patchRemoteState({ type: "pending" });
|
|
1431
1495
|
const factoryResult = await tryCatchAsync(async () => {
|
|
1432
1496
|
return await remoteFactory?.({
|
|
1433
1497
|
onEventsAvailable: ({ newSyncId, remoteEventHlcSum }) => {
|
|
@@ -1436,13 +1500,15 @@ var createCrdtSyncRemoteSource = ({
|
|
|
1436
1500
|
});
|
|
1437
1501
|
});
|
|
1438
1502
|
if (!factoryResult.success) {
|
|
1439
|
-
|
|
1503
|
+
patchRemoteState({ type: "offline", reason: "INITIALIZATION_FAILED" });
|
|
1440
1504
|
console.warn("Failed to create remote source", factoryResult.error);
|
|
1441
1505
|
return;
|
|
1442
1506
|
}
|
|
1443
|
-
|
|
1507
|
+
patchRemoteState({
|
|
1444
1508
|
type: "online",
|
|
1445
|
-
source: factoryResult.data
|
|
1509
|
+
source: factoryResult.data,
|
|
1510
|
+
deSynced: false,
|
|
1511
|
+
schemaVersionMismatched: false
|
|
1446
1512
|
});
|
|
1447
1513
|
},
|
|
1448
1514
|
{ queueReExecution: false }
|
|
@@ -1463,14 +1529,14 @@ var createCrdtSyncRemoteSource = ({
|
|
|
1463
1529
|
return;
|
|
1464
1530
|
}
|
|
1465
1531
|
const source = remoteState.source;
|
|
1466
|
-
|
|
1532
|
+
patchRemoteState({ type: "pending" });
|
|
1467
1533
|
const disconnectResult = await tryCatchAsync(async () => {
|
|
1468
1534
|
return await source.disconnect?.();
|
|
1469
1535
|
});
|
|
1470
1536
|
if (!disconnectResult.success) {
|
|
1471
1537
|
console.warn("Error while disconnecting from remote source", disconnectResult.error);
|
|
1472
1538
|
}
|
|
1473
|
-
|
|
1539
|
+
patchRemoteState({ type: "offline", reason });
|
|
1474
1540
|
},
|
|
1475
1541
|
{ queueReExecution: false }
|
|
1476
1542
|
);
|
|
@@ -1523,18 +1589,12 @@ var createCrdtSyncRemoteSource = ({
|
|
|
1523
1589
|
return;
|
|
1524
1590
|
}
|
|
1525
1591
|
const source = remoteState.source;
|
|
1526
|
-
const response = await
|
|
1592
|
+
const response = await retryRemoteOperation(
|
|
1527
1593
|
() => source.pullEvents({
|
|
1528
1594
|
...opts,
|
|
1529
1595
|
afterSyncId
|
|
1530
1596
|
}),
|
|
1531
|
-
|
|
1532
|
-
max: 3,
|
|
1533
|
-
backoffBase: 100,
|
|
1534
|
-
backoffExponent: 1.5,
|
|
1535
|
-
backoffJitter: 150,
|
|
1536
|
-
timeout: 1e4
|
|
1537
|
-
}
|
|
1597
|
+
REMOTE_RETRY_OPTIONS
|
|
1538
1598
|
);
|
|
1539
1599
|
hasMore = response.hasMore;
|
|
1540
1600
|
afterSyncId = response.nextSyncId;
|
|
@@ -1546,6 +1606,9 @@ var createCrdtSyncRemoteSource = ({
|
|
|
1546
1606
|
remoteSchemaVersion: x.schema_version,
|
|
1547
1607
|
localSchemaVersion: migrator.currentSchemaVersion
|
|
1548
1608
|
});
|
|
1609
|
+
if (remoteState.type === "online" && !remoteState.schemaVersionMismatched) {
|
|
1610
|
+
patchRemoteState({ schemaVersionMismatched: true });
|
|
1611
|
+
}
|
|
1549
1612
|
throw new SchemaVersionMismatchError(x.schema_version, migrator.currentSchemaVersion);
|
|
1550
1613
|
}
|
|
1551
1614
|
return x;
|
|
@@ -1574,11 +1637,15 @@ var createCrdtSyncRemoteSource = ({
|
|
|
1574
1637
|
if (localEventHlcSum === null) {
|
|
1575
1638
|
return;
|
|
1576
1639
|
}
|
|
1577
|
-
if (localEventHlcSum
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1640
|
+
if (localEventHlcSum === remoteEventHlcSum) {
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
eventTarget.dispatchEvent("de-sync-detected", { reason: "CHECKSUM_MISMATCH" });
|
|
1644
|
+
console.warn(
|
|
1645
|
+
`[sqlite-sync] De-sync detected at syncId ${remoteSyncId}: local HLC checksum ${localEventHlcSum} != remote ${remoteEventHlcSum}. Local and remote have diverged despite being caught up.`
|
|
1646
|
+
);
|
|
1647
|
+
if (remoteState.type === "online" && !remoteState.deSynced) {
|
|
1648
|
+
patchRemoteState({ deSynced: true });
|
|
1582
1649
|
}
|
|
1583
1650
|
};
|
|
1584
1651
|
const startPushingEvents = ensureSingletonExecution(async () => {
|
|
@@ -1598,7 +1665,7 @@ var createCrdtSyncRemoteSource = ({
|
|
|
1598
1665
|
const source = remoteState.source;
|
|
1599
1666
|
let response;
|
|
1600
1667
|
try {
|
|
1601
|
-
response = await
|
|
1668
|
+
response = await retryRemoteOperation(
|
|
1602
1669
|
() => source.pushEvents({
|
|
1603
1670
|
nodeId,
|
|
1604
1671
|
events: eventsBatch.events.map((event) => ({
|
|
@@ -1610,13 +1677,7 @@ var createCrdtSyncRemoteSource = ({
|
|
|
1610
1677
|
payload: event.payload
|
|
1611
1678
|
}))
|
|
1612
1679
|
}),
|
|
1613
|
-
|
|
1614
|
-
max: 3,
|
|
1615
|
-
backoffBase: 100,
|
|
1616
|
-
backoffExponent: 1.5,
|
|
1617
|
-
backoffJitter: 150,
|
|
1618
|
-
timeout: 1e4
|
|
1619
|
-
}
|
|
1680
|
+
REMOTE_RETRY_OPTIONS
|
|
1620
1681
|
);
|
|
1621
1682
|
} catch (error) {
|
|
1622
1683
|
console.error("Error pushing events. Going offline.", error);
|
|
@@ -1637,8 +1698,15 @@ var createCrdtSyncRemoteSource = ({
|
|
|
1637
1698
|
});
|
|
1638
1699
|
const remoteEventApplyFailedSubscription = storage.addEventListener("remote-event-apply-failed", () => {
|
|
1639
1700
|
eventTarget.dispatchEvent("de-sync-detected", { reason: "ERROR_APPLYING_REMOTE_EVENT" });
|
|
1701
|
+
if (remoteState.type === "online" && !remoteState.deSynced) {
|
|
1702
|
+
patchRemoteState({ deSynced: true });
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
const getState = () => ({
|
|
1706
|
+
remoteState: remoteState.type,
|
|
1707
|
+
deSynced: remoteState.deSynced,
|
|
1708
|
+
schemaVersionMismatched: remoteState.schemaVersionMismatched
|
|
1640
1709
|
});
|
|
1641
|
-
const getState = () => remoteState.type;
|
|
1642
1710
|
const dispose = async () => {
|
|
1643
1711
|
await goOffline("DISCONNECTED");
|
|
1644
1712
|
eventsAppliedSubscription.unsubscribe();
|
|
@@ -1796,4 +1864,4 @@ export {
|
|
|
1796
1864
|
createResetStateStore,
|
|
1797
1865
|
createReloadRequestHandler
|
|
1798
1866
|
};
|
|
1799
|
-
//# sourceMappingURL=chunk-
|
|
1867
|
+
//# sourceMappingURL=chunk-DYAQIVJO.js.map
|