@sqlite-sync/core 0.8.2 → 0.9.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/dist/index-Ct_YbBmV.d.ts +1159 -0
- package/dist/index.d.ts +2 -351
- package/dist/index.js +904 -1285
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +3 -0
- package/dist/make-crdt-table-BvsEnvu-.js +107 -0
- package/dist/make-crdt-table-BvsEnvu-.js.map +1 -0
- package/dist/reset-state-D3nsfx_t.js +2018 -0
- package/dist/reset-state-D3nsfx_t.js.map +1 -0
- package/dist/server.d.ts +65 -67
- package/dist/server.js +30 -32
- package/dist/server.js.map +1 -1
- package/dist/system-schema-CkglcfTS.js +185 -0
- package/dist/system-schema-CkglcfTS.js.map +1 -0
- package/dist/utils-Dr5JwcTP.js +143 -0
- package/dist/utils-Dr5JwcTP.js.map +1 -0
- package/dist/worker.d.ts +35 -37
- package/dist/worker.js +399 -479
- package/dist/worker.js.map +1 -1
- package/package.json +10 -6
- package/dist/chunk-GMF45NFH.js +0 -2298
- package/dist/chunk-GMF45NFH.js.map +0 -1
- package/dist/chunk-NHT3ELMN.js +0 -160
- package/dist/chunk-NHT3ELMN.js.map +0 -1
- package/dist/crdt-sync-remote-source-CnB4p4Ry.d.ts +0 -713
- package/dist/reset-state-D6UslXaj.d.ts +0 -19
package/dist/worker.js
CHANGED
|
@@ -1,139 +1,99 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
applyWorkerDbSchema,
|
|
5
|
-
createBroadcastChannels,
|
|
6
|
-
createCrdtStorage,
|
|
7
|
-
createCrdtSyncProducer,
|
|
8
|
-
createCrdtSyncRemoteSource,
|
|
9
|
-
createIdbResetStore,
|
|
10
|
-
createImportData,
|
|
11
|
-
createMigrator,
|
|
12
|
-
createReloadRequestHandler,
|
|
13
|
-
createResetStateStore,
|
|
14
|
-
formatSchemaVerificationIssues,
|
|
15
|
-
isWorkerInitMessage,
|
|
16
|
-
isWorkerRequestMessage,
|
|
17
|
-
syncDbClientLockName,
|
|
18
|
-
syncDbWorkerLockName,
|
|
19
|
-
verifySyncSchema,
|
|
20
|
-
workerDbConfig,
|
|
21
|
-
xxhash
|
|
22
|
-
} from "./chunk-GMF45NFH.js";
|
|
23
|
-
import {
|
|
24
|
-
createDeferredPromise,
|
|
25
|
-
jsonSafeParse
|
|
26
|
-
} from "./chunk-NHT3ELMN.js";
|
|
27
|
-
|
|
28
|
-
// src/web-socket/ws-remote-source.ts
|
|
29
|
-
var createWsRemoteSource = ({ createWebSocket }) => {
|
|
30
|
-
return async ({ onEventsAvailable }) => {
|
|
31
|
-
const socket = createWebSocket();
|
|
32
|
-
const openPromise = createDeferredPromise({
|
|
33
|
-
timeout: 5e3,
|
|
34
|
-
onTimeout: () => {
|
|
35
|
-
socket.close();
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
socket.addEventListener("open", () => {
|
|
39
|
-
openPromise.resolve(void 0);
|
|
40
|
-
});
|
|
41
|
-
await openPromise.promise;
|
|
42
|
-
const requestsMap = /* @__PURE__ */ new Map();
|
|
43
|
-
const pushEvents = async (request) => {
|
|
44
|
-
const requestId = crypto.randomUUID();
|
|
45
|
-
const promise = createDeferredPromise({ timeout: 5e3 });
|
|
46
|
-
requestsMap.set(requestId, promise);
|
|
47
|
-
const wsRequest = {
|
|
48
|
-
type: "push-events",
|
|
49
|
-
requestId,
|
|
50
|
-
nodeId: request.nodeId,
|
|
51
|
-
events: request.events
|
|
52
|
-
};
|
|
53
|
-
socket.send(JSON.stringify(wsRequest));
|
|
54
|
-
return promise.promise;
|
|
55
|
-
};
|
|
56
|
-
const pullEvents = async (request) => {
|
|
57
|
-
const requestId = crypto.randomUUID();
|
|
58
|
-
const promise = createDeferredPromise({ timeout: 2e3 });
|
|
59
|
-
requestsMap.set(requestId, promise);
|
|
60
|
-
const wsRequest = {
|
|
61
|
-
type: "pull-events",
|
|
62
|
-
requestId,
|
|
63
|
-
afterSyncId: request.afterSyncId,
|
|
64
|
-
excludeNodeId: request.excludeNodeId
|
|
65
|
-
};
|
|
66
|
-
socket.send(JSON.stringify(wsRequest));
|
|
67
|
-
return promise.promise;
|
|
68
|
-
};
|
|
69
|
-
socket.onmessage = (event) => {
|
|
70
|
-
const result = jsonSafeParse(event.data);
|
|
71
|
-
if (!result.success || !("type" in result.data) || !result.data.type) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const message = result.data;
|
|
75
|
-
switch (message.type) {
|
|
76
|
-
case "events-pull-response": {
|
|
77
|
-
const promise = requestsMap.get(message.requestId);
|
|
78
|
-
if (!promise) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
promise.resolve(message.data);
|
|
82
|
-
requestsMap.delete(message.requestId);
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
case "events-push-response": {
|
|
86
|
-
const promise = requestsMap.get(message.requestId);
|
|
87
|
-
if (!promise) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
promise.resolve(message.data);
|
|
91
|
-
requestsMap.delete(message.requestId);
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
case "events-applied":
|
|
95
|
-
onEventsAvailable({ newSyncId: message.newSyncId, remoteEventHlcSum: message.eventHlcSum });
|
|
96
|
-
break;
|
|
97
|
-
default:
|
|
98
|
-
message;
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
return {
|
|
103
|
-
pushEvents,
|
|
104
|
-
pullEvents,
|
|
105
|
-
disconnect: () => {
|
|
106
|
-
socket.close();
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
};
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// src/worker-db/db-worker.ts
|
|
1
|
+
import { C as createMigrator, D as HLCCounter, N as createImportData, a as createBroadcastChannels, d as syncDbClientLockName, f as syncDbWorkerLockName, h as createCrdtStorage, i as createResetStateStore, j as xxhash, l as isWorkerRequestMessage, m as createCrdtSyncProducer, n as createIdbResetStore, p as createCrdtSyncRemoteSource, r as createReloadRequestHandler, s as isWorkerInitMessage, v as formatSchemaVerificationIssues, w as SQLiteDbWrapper, y as verifySyncSchema } from "./reset-state-D3nsfx_t.js";
|
|
2
|
+
import { r as createDeferredPromise, s as jsonSafeParse } from "./utils-Dr5JwcTP.js";
|
|
3
|
+
import { n as applyWorkerDbSchema, s as workerDbConfig } from "./system-schema-CkglcfTS.js";
|
|
113
4
|
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
5
|
+
//#region src/web-socket/ws-remote-source.ts
|
|
6
|
+
const createWsRemoteSource = ({ createWebSocket }) => {
|
|
7
|
+
return async ({ onEventsAvailable }) => {
|
|
8
|
+
const socket = createWebSocket();
|
|
9
|
+
const openPromise = createDeferredPromise({
|
|
10
|
+
timeout: 5e3,
|
|
11
|
+
onTimeout: () => {
|
|
12
|
+
socket.close();
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
socket.addEventListener("open", () => {
|
|
16
|
+
openPromise.resolve(void 0);
|
|
17
|
+
});
|
|
18
|
+
await openPromise.promise;
|
|
19
|
+
const requestsMap = /* @__PURE__ */ new Map();
|
|
20
|
+
const pushEvents = async (request) => {
|
|
21
|
+
const requestId = crypto.randomUUID();
|
|
22
|
+
const promise = createDeferredPromise({ timeout: 5e3 });
|
|
23
|
+
requestsMap.set(requestId, promise);
|
|
24
|
+
const wsRequest = {
|
|
25
|
+
type: "push-events",
|
|
26
|
+
requestId,
|
|
27
|
+
nodeId: request.nodeId,
|
|
28
|
+
events: request.events
|
|
29
|
+
};
|
|
30
|
+
socket.send(JSON.stringify(wsRequest));
|
|
31
|
+
return promise.promise;
|
|
32
|
+
};
|
|
33
|
+
const pullEvents = async (request) => {
|
|
34
|
+
const requestId = crypto.randomUUID();
|
|
35
|
+
const promise = createDeferredPromise({ timeout: 2e3 });
|
|
36
|
+
requestsMap.set(requestId, promise);
|
|
37
|
+
const wsRequest = {
|
|
38
|
+
type: "pull-events",
|
|
39
|
+
requestId,
|
|
40
|
+
afterSyncId: request.afterSyncId,
|
|
41
|
+
excludeNodeId: request.excludeNodeId
|
|
42
|
+
};
|
|
43
|
+
socket.send(JSON.stringify(wsRequest));
|
|
44
|
+
return promise.promise;
|
|
45
|
+
};
|
|
46
|
+
socket.onmessage = (event) => {
|
|
47
|
+
const result = jsonSafeParse(event.data);
|
|
48
|
+
if (!result.success || !("type" in result.data) || !result.data.type) return;
|
|
49
|
+
const message = result.data;
|
|
50
|
+
switch (message.type) {
|
|
51
|
+
case "events-pull-response": {
|
|
52
|
+
const promise = requestsMap.get(message.requestId);
|
|
53
|
+
if (!promise) return;
|
|
54
|
+
promise.resolve(message.data);
|
|
55
|
+
requestsMap.delete(message.requestId);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "events-push-response": {
|
|
59
|
+
const promise = requestsMap.get(message.requestId);
|
|
60
|
+
if (!promise) return;
|
|
61
|
+
promise.resolve(message.data);
|
|
62
|
+
requestsMap.delete(message.requestId);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case "events-applied":
|
|
66
|
+
onEventsAvailable({
|
|
67
|
+
newSyncId: message.newSyncId,
|
|
68
|
+
remoteEventHlcSum: message.eventHlcSum
|
|
69
|
+
});
|
|
70
|
+
break;
|
|
71
|
+
default: return;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
pushEvents,
|
|
76
|
+
pullEvents,
|
|
77
|
+
disconnect: () => {
|
|
78
|
+
socket.close();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
};
|
|
117
83
|
function runWorkerEventLogGc({ db, dbConfig, pushSyncId, eventHlcAccumulator, logger }) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
return { skipped: true };
|
|
133
|
-
}
|
|
134
|
-
const [{ deleteBeforeSyncId }] = db.executePreparedRaw({
|
|
135
|
-
key: "event-log-gc-delete-before-sync-id",
|
|
136
|
-
sql: `
|
|
84
|
+
const eventsTable = dbConfig.eventsTable.fullIdentifier;
|
|
85
|
+
if (eventHlcAccumulator.current === "") {
|
|
86
|
+
const [{ count }] = db.executePreparedRaw({
|
|
87
|
+
key: "event-log-gc-count-applied-events",
|
|
88
|
+
sql: `SELECT count(*) AS count FROM ${eventsTable} WHERE "status" = 'applied'`,
|
|
89
|
+
meta: { loggerLevel: "system" }
|
|
90
|
+
});
|
|
91
|
+
if (count > 0) logger?.("worker", "Skipping event log GC until the event HLC checksum has been computed from full history", "info");
|
|
92
|
+
return { skipped: true };
|
|
93
|
+
}
|
|
94
|
+
const [{ deleteBeforeSyncId }] = db.executePreparedRaw({
|
|
95
|
+
key: "event-log-gc-delete-before-sync-id",
|
|
96
|
+
sql: `
|
|
137
97
|
SELECT min("sync_id") AS deleteBeforeSyncId
|
|
138
98
|
FROM (
|
|
139
99
|
SELECT "sync_id"
|
|
@@ -143,369 +103,329 @@ function runWorkerEventLogGc({ db, dbConfig, pushSyncId, eventHlcAccumulator, lo
|
|
|
143
103
|
LIMIT ?
|
|
144
104
|
)
|
|
145
105
|
`,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
key: "event-log-gc-delete-events",
|
|
154
|
-
sql: `
|
|
106
|
+
params: [100],
|
|
107
|
+
meta: { loggerLevel: "system" }
|
|
108
|
+
});
|
|
109
|
+
if (deleteBeforeSyncId === null) return { skipped: false };
|
|
110
|
+
db.executePreparedRaw({
|
|
111
|
+
key: "event-log-gc-delete-events",
|
|
112
|
+
sql: `
|
|
155
113
|
DELETE FROM ${eventsTable}
|
|
156
114
|
WHERE "sync_id" < ?
|
|
157
115
|
AND "status" IN ('applied', 'deduped')
|
|
158
116
|
AND ("origin" = 'remote' OR "sync_id" <= ?)
|
|
159
117
|
`,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
118
|
+
params: [deleteBeforeSyncId, pushSyncId.current],
|
|
119
|
+
meta: { loggerLevel: "system" }
|
|
120
|
+
});
|
|
121
|
+
logger?.("worker", "Event log GC completed", "info");
|
|
122
|
+
return { skipped: false };
|
|
165
123
|
}
|
|
166
|
-
|
|
167
|
-
// src/worker-db/storage-version.ts
|
|
168
|
-
var LIB_STORAGE_VERSION = 1;
|
|
169
|
-
var storageVersionKey = (dbId) => `sqlite-sync-storage-version-${dbId}`;
|
|
124
|
+
const storageVersionKey = (dbId) => `sqlite-sync-storage-version-${dbId}`;
|
|
170
125
|
function formatStorageVersion(appStorageVersion) {
|
|
171
|
-
|
|
126
|
+
return appStorageVersion === void 0 ? `lib-v2` : `lib-v2:app-${appStorageVersion}`;
|
|
172
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Worker-owned durable storage version. The current version combines the
|
|
130
|
+
* internal lib version with the dev-provided app version; when the stored
|
|
131
|
+
* version does not match (including a missing record), the elected worker
|
|
132
|
+
* initializes with `clearOnInit: true`. Wiping on a missing record is
|
|
133
|
+
* harmless for fresh installs and correctly resets databases persisted
|
|
134
|
+
* before versioning existed.
|
|
135
|
+
*/
|
|
173
136
|
function createStorageVersionStore({ store, dbId, appStorageVersion }) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
137
|
+
const key = storageVersionKey(dbId);
|
|
138
|
+
const currentVersion = formatStorageVersion(appStorageVersion);
|
|
139
|
+
return {
|
|
140
|
+
currentVersion,
|
|
141
|
+
async isVersionMismatch() {
|
|
142
|
+
return await store.get(key) !== currentVersion;
|
|
143
|
+
},
|
|
144
|
+
/**
|
|
145
|
+
* Record the current version. Must be called only after the worker has
|
|
146
|
+
* successfully initialized with the wiped DB, so a failed init can be
|
|
147
|
+
* retried by a later elected worker.
|
|
148
|
+
*/
|
|
149
|
+
async markCurrentVersionApplied() {
|
|
150
|
+
await store.set(key, currentVersion);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
191
153
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/worker-db/db-worker.ts
|
|
156
|
+
const defaultLogger = (type, message, level = "info") => {
|
|
157
|
+
const logMessage = `[${type}] ${message}`;
|
|
158
|
+
switch (level) {
|
|
159
|
+
case "info":
|
|
160
|
+
console.log(logMessage);
|
|
161
|
+
break;
|
|
162
|
+
case "warning":
|
|
163
|
+
console.warn(logMessage);
|
|
164
|
+
break;
|
|
165
|
+
case "error":
|
|
166
|
+
console.error(logMessage);
|
|
167
|
+
break;
|
|
168
|
+
case "trace": console.trace(logMessage);
|
|
169
|
+
}
|
|
210
170
|
};
|
|
211
171
|
async function createDbWorker(config, opts) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const response = {
|
|
419
|
-
type: "response",
|
|
420
|
-
requestId: message.requestId,
|
|
421
|
-
data
|
|
422
|
-
};
|
|
423
|
-
broadcastChannels.responses.postMessage(response);
|
|
424
|
-
}
|
|
425
|
-
} catch (error) {
|
|
426
|
-
sendError(error);
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
|
-
rpcTarget.postState();
|
|
430
|
-
return async () => {
|
|
431
|
-
stateChangedSubscription.unsubscribe();
|
|
432
|
-
deSyncDetectedSubscription.unsubscribe();
|
|
433
|
-
remoteSchemaVersionMismatchSubscription.unsubscribe();
|
|
434
|
-
await remoteSource.dispose();
|
|
435
|
-
broadcastChannels.requests.close();
|
|
436
|
-
broadcastChannels.responses.close();
|
|
437
|
-
db.close();
|
|
438
|
-
};
|
|
172
|
+
const broadcastChannels = createBroadcastChannels(config.dbId);
|
|
173
|
+
const logger = opts.logger ?? defaultLogger;
|
|
174
|
+
if (opts.verifySchema) {
|
|
175
|
+
const issues = await verifySyncSchema(opts.syncDbSchema);
|
|
176
|
+
if (issues.length > 0) throw new Error(formatSchemaVerificationIssues(issues));
|
|
177
|
+
logger("worker", "Schema verification passed: migrations match the declared schema", "info");
|
|
178
|
+
}
|
|
179
|
+
const [sqlite3] = await Promise.all([sqlite3InitModule(), xxhash.ensureLoaded()]);
|
|
180
|
+
const resetStore = opts.resetStore ?? createIdbResetStore();
|
|
181
|
+
const resetState = createResetStateStore({
|
|
182
|
+
store: resetStore,
|
|
183
|
+
dbId: config.dbId
|
|
184
|
+
});
|
|
185
|
+
const storageVersion = createStorageVersionStore({
|
|
186
|
+
store: resetStore,
|
|
187
|
+
dbId: config.dbId,
|
|
188
|
+
appStorageVersion: opts.storageVersion
|
|
189
|
+
});
|
|
190
|
+
const [pendingReset, isVersionMismatch] = await Promise.all([resetState.resolvePendingReset(), storageVersion.isVersionMismatch()]);
|
|
191
|
+
if (isVersionMismatch) logger("worker", `Storage version mismatch — resetting local DB to ${storageVersion.currentVersion}`, "warning");
|
|
192
|
+
const pool = await sqlite3.installOpfsSAHPoolVfs({
|
|
193
|
+
name: config.dbId,
|
|
194
|
+
directory: `.${config.dbId}`,
|
|
195
|
+
clearOnInit: !!pendingReset || isVersionMismatch,
|
|
196
|
+
initialCapacity: 8
|
|
197
|
+
});
|
|
198
|
+
const db = new SQLiteDbWrapper({
|
|
199
|
+
db: () => new pool.OpfsSAHPoolDb(`/${config.dbId}-main.db`),
|
|
200
|
+
logger,
|
|
201
|
+
loggerPrefix: "worker",
|
|
202
|
+
sqlite3
|
|
203
|
+
});
|
|
204
|
+
db.execute("PRAGMA locking_mode=exclusive", { loggerLevel: "system" });
|
|
205
|
+
db.execute("PRAGMA journal_mode=WAL", { loggerLevel: "system" });
|
|
206
|
+
db.execute("PRAGMA synchronous=NORMAL", { loggerLevel: "system" });
|
|
207
|
+
db.execute(`ATTACH DATABASE '/${config.dbId}-worker.db' as worker`, { loggerLevel: "system" });
|
|
208
|
+
db.execute("PRAGMA worker.locking_mode=exclusive", { loggerLevel: "system" });
|
|
209
|
+
db.execute("PRAGMA worker.journal_mode=WAL", { loggerLevel: "system" });
|
|
210
|
+
db.execute("PRAGMA worker.synchronous=NORMAL", { loggerLevel: "system" });
|
|
211
|
+
const { kvStore } = applyWorkerDbSchema(db);
|
|
212
|
+
const migrator = createMigrator({
|
|
213
|
+
migrations: opts.syncDbSchema.migrations,
|
|
214
|
+
schemaVersion: kvStore.createNumberStoredValue("schema-version", -1),
|
|
215
|
+
updateLogTableName: workerDbConfig.updateLogTable.fullIdentifier
|
|
216
|
+
});
|
|
217
|
+
migrator.migrateDbToLatest({ startTransaction: (callback) => {
|
|
218
|
+
db.executeTransaction((tx) => callback({ execute: (sql, parameters, meta) => tx.execute({
|
|
219
|
+
sql,
|
|
220
|
+
parameters
|
|
221
|
+
}, meta) }));
|
|
222
|
+
} });
|
|
223
|
+
db.invalidateDbSchema();
|
|
224
|
+
const pullSyncId = kvStore.createNumberStoredValue("pull-sync-id", -1);
|
|
225
|
+
const pushSyncId = kvStore.createNumberStoredValue("push-sync-id", -1);
|
|
226
|
+
const eventHlcAccumulator = kvStore.createStringStoredValue("crdt.consistency.event_hlc_sum.v2", "");
|
|
227
|
+
if (pendingReset) await resetState.markResetApplied(pendingReset.epoch);
|
|
228
|
+
if (isVersionMismatch) await storageVersion.markCurrentVersionApplied();
|
|
229
|
+
if (opts.eventLogGc === true) runWorkerEventLogGc({
|
|
230
|
+
db,
|
|
231
|
+
dbConfig: workerDbConfig,
|
|
232
|
+
pushSyncId,
|
|
233
|
+
eventHlcAccumulator,
|
|
234
|
+
logger
|
|
235
|
+
});
|
|
236
|
+
const crdtStorage = createCrdtStorage({
|
|
237
|
+
nodeId: config.clientId,
|
|
238
|
+
initialLocalSyncId: getMaxSyncId(db, "none"),
|
|
239
|
+
migrator,
|
|
240
|
+
hlc: new HLCCounter(config.clientId, () => Date.now()),
|
|
241
|
+
db,
|
|
242
|
+
dbConfig: workerDbConfig,
|
|
243
|
+
eventHlcAccumulator,
|
|
244
|
+
schema: opts.syncDbSchema
|
|
245
|
+
});
|
|
246
|
+
createCrdtSyncProducer({
|
|
247
|
+
storage: crdtStorage,
|
|
248
|
+
broadcastEvents: (chunk) => {
|
|
249
|
+
broadcastChannels.responses.postMessage({
|
|
250
|
+
notificationType: "new-event-chunk-applied",
|
|
251
|
+
newSyncId: chunk.newSyncId,
|
|
252
|
+
eventHlcSum: chunk.eventHlcSum
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
const remoteSource = createRemoteSource({
|
|
257
|
+
crdtStorage,
|
|
258
|
+
migrator,
|
|
259
|
+
clientId: config.clientId,
|
|
260
|
+
pullSyncId,
|
|
261
|
+
pushSyncId,
|
|
262
|
+
remoteFactory: opts.createRemoteSource
|
|
263
|
+
});
|
|
264
|
+
remoteSource.goOnline();
|
|
265
|
+
const broadcastNotification = (notification) => {
|
|
266
|
+
broadcastChannels.responses.postMessage(notification);
|
|
267
|
+
};
|
|
268
|
+
const postState = () => {
|
|
269
|
+
broadcastNotification({
|
|
270
|
+
notificationType: "state-changed",
|
|
271
|
+
state: remoteSource.getState()
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
const stateChangedSubscription = remoteSource.addEventListener("state-changed", () => {
|
|
275
|
+
postState();
|
|
276
|
+
});
|
|
277
|
+
const deSyncDetectedSubscription = remoteSource.addEventListener("de-sync-detected", (event) => {
|
|
278
|
+
broadcastNotification({
|
|
279
|
+
notificationType: "de-sync-detected",
|
|
280
|
+
reason: event.payload.reason
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
const remoteSchemaVersionMismatchSubscription = remoteSource.addEventListener("remote-schema-version-mismatch", (event) => {
|
|
284
|
+
broadcastNotification({
|
|
285
|
+
notificationType: "remote-schema-version-mismatch",
|
|
286
|
+
remoteSchemaVersion: event.payload.remoteSchemaVersion,
|
|
287
|
+
localSchemaVersion: event.payload.localSchemaVersion
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
const rpcTarget = {
|
|
291
|
+
execute: (query) => db.execute(query),
|
|
292
|
+
getSnapshot: () => {
|
|
293
|
+
const appliedSyncId = getMaxSyncId(db, "pending");
|
|
294
|
+
db.execute("PRAGMA journal_mode=off", { loggerLevel: "system" });
|
|
295
|
+
const file = db.createSnapshot();
|
|
296
|
+
db.execute("PRAGMA journal_mode=WAL", { loggerLevel: "system" });
|
|
297
|
+
return {
|
|
298
|
+
file,
|
|
299
|
+
syncId: appliedSyncId,
|
|
300
|
+
schemaVersion: migrator.currentSchemaVersion
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
importData: createImportData({
|
|
304
|
+
migrator,
|
|
305
|
+
tablesConfig: opts.syncDbSchema.tablesConfig,
|
|
306
|
+
applyEvents: async (events) => {
|
|
307
|
+
crdtStorage.applyOwnEvents(events);
|
|
308
|
+
await crdtStorage.internal.processEnqueuedEvents();
|
|
309
|
+
}
|
|
310
|
+
}),
|
|
311
|
+
postState,
|
|
312
|
+
pushTabEvents: (request) => {
|
|
313
|
+
const { beforeSyncId, afterSyncId } = crdtStorage.enqueueLocalEvents(request.events, request.nodeId);
|
|
314
|
+
return {
|
|
315
|
+
ok: true,
|
|
316
|
+
beforeSyncId,
|
|
317
|
+
afterSyncId
|
|
318
|
+
};
|
|
319
|
+
},
|
|
320
|
+
pullEvents: (request) => {
|
|
321
|
+
return crdtStorage.getEventsBatch({
|
|
322
|
+
afterSyncId: request.afterSyncId,
|
|
323
|
+
status: "applied",
|
|
324
|
+
excludeNodeId: request.excludeNodeId ?? "",
|
|
325
|
+
limit: 100
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
goOnline: () => remoteSource.goOnline(),
|
|
329
|
+
goOffline: () => remoteSource.goOffline("DISCONNECTED"),
|
|
330
|
+
requestReload: createReloadRequestHandler({
|
|
331
|
+
resetState,
|
|
332
|
+
broadcast: broadcastNotification
|
|
333
|
+
})
|
|
334
|
+
};
|
|
335
|
+
broadcastChannels.requests.onmessage = (event) => {
|
|
336
|
+
const message = event.data;
|
|
337
|
+
if (!isWorkerRequestMessage(message)) return;
|
|
338
|
+
const sendError = (error) => {
|
|
339
|
+
const response = {
|
|
340
|
+
type: "error-response",
|
|
341
|
+
requestId: message.requestId,
|
|
342
|
+
error: error instanceof Error ? error.message : String(error)
|
|
343
|
+
};
|
|
344
|
+
broadcastChannels.responses.postMessage(response);
|
|
345
|
+
};
|
|
346
|
+
try {
|
|
347
|
+
const data = rpcTarget[message.method].apply(null, message.args);
|
|
348
|
+
if (data instanceof Promise) data.then((result) => {
|
|
349
|
+
const response = {
|
|
350
|
+
type: "response",
|
|
351
|
+
requestId: message.requestId,
|
|
352
|
+
data: result
|
|
353
|
+
};
|
|
354
|
+
broadcastChannels.responses.postMessage(response);
|
|
355
|
+
}).catch(sendError);
|
|
356
|
+
else {
|
|
357
|
+
const response = {
|
|
358
|
+
type: "response",
|
|
359
|
+
requestId: message.requestId,
|
|
360
|
+
data
|
|
361
|
+
};
|
|
362
|
+
broadcastChannels.responses.postMessage(response);
|
|
363
|
+
}
|
|
364
|
+
} catch (error) {
|
|
365
|
+
sendError(error);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
rpcTarget.postState();
|
|
369
|
+
return async () => {
|
|
370
|
+
stateChangedSubscription.unsubscribe();
|
|
371
|
+
deSyncDetectedSubscription.unsubscribe();
|
|
372
|
+
remoteSchemaVersionMismatchSubscription.unsubscribe();
|
|
373
|
+
await remoteSource.dispose();
|
|
374
|
+
broadcastChannels.requests.close();
|
|
375
|
+
broadcastChannels.responses.close();
|
|
376
|
+
db.close();
|
|
377
|
+
};
|
|
439
378
|
}
|
|
440
|
-
function createRemoteSource({
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
pullSyncId,
|
|
451
|
-
pushSyncId,
|
|
452
|
-
nodeId: clientId,
|
|
453
|
-
storage: crdtStorage,
|
|
454
|
-
migrator,
|
|
455
|
-
remoteFactory
|
|
456
|
-
});
|
|
379
|
+
function createRemoteSource({ clientId, crdtStorage, migrator, pullSyncId, pushSyncId, remoteFactory }) {
|
|
380
|
+
return createCrdtSyncRemoteSource({
|
|
381
|
+
bufferSize: 50,
|
|
382
|
+
pullSyncId,
|
|
383
|
+
pushSyncId,
|
|
384
|
+
nodeId: clientId,
|
|
385
|
+
storage: crdtStorage,
|
|
386
|
+
migrator,
|
|
387
|
+
remoteFactory
|
|
388
|
+
});
|
|
457
389
|
}
|
|
458
390
|
async function getWorkerConfig() {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
};
|
|
473
|
-
return responsePromise.promise;
|
|
391
|
+
let configSet = false;
|
|
392
|
+
const responsePromise = createDeferredPromise();
|
|
393
|
+
self.onmessage = (event) => {
|
|
394
|
+
if (configSet) {
|
|
395
|
+
console.error("Worker config already set");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const message = event.data;
|
|
399
|
+
if (!isWorkerInitMessage(message)) return;
|
|
400
|
+
responsePromise.resolve(message.config);
|
|
401
|
+
configSet = true;
|
|
402
|
+
};
|
|
403
|
+
return responsePromise.promise;
|
|
474
404
|
}
|
|
475
405
|
async function startDbWorker(opts) {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
await cleanup();
|
|
494
|
-
});
|
|
495
|
-
self.close();
|
|
406
|
+
const config = opts.workerConfig ?? await getWorkerConfig();
|
|
407
|
+
await navigator.locks.request(`${syncDbWorkerLockName}-${config.dbId}`, { mode: "exclusive" }, async (lock) => {
|
|
408
|
+
if (!lock) return;
|
|
409
|
+
const cleanup = await createDbWorker(config, opts);
|
|
410
|
+
const clientLockName = `${syncDbClientLockName}-${config.dbId}`;
|
|
411
|
+
await new Promise((resolve) => {
|
|
412
|
+
const interval = setInterval(async () => {
|
|
413
|
+
const { held } = await navigator.locks.query();
|
|
414
|
+
if (!held?.some((l) => l.name === clientLockName && l.mode === "shared")) {
|
|
415
|
+
clearInterval(interval);
|
|
416
|
+
resolve();
|
|
417
|
+
}
|
|
418
|
+
}, 5e3);
|
|
419
|
+
});
|
|
420
|
+
await cleanup();
|
|
421
|
+
});
|
|
422
|
+
self.close();
|
|
496
423
|
}
|
|
497
424
|
function getMaxSyncId(db, excludingStatus) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
{ excludingStatus },
|
|
501
|
-
(db2, params) => db2.selectFrom("worker.crdt_events").where("status", "!=", params("excludingStatus")).select((eb) => eb.fn.max("sync_id").as("sync_id")),
|
|
502
|
-
{ loggerLevel: "system" }
|
|
503
|
-
);
|
|
504
|
-
return result?.sync_id ?? 0;
|
|
425
|
+
const [result] = db.executePrepared("get-max-sync-id", { excludingStatus }, (db, params) => db.selectFrom("worker.crdt_events").where("status", "!=", params("excludingStatus")).select((eb) => eb.fn.max("sync_id").as("sync_id")), { loggerLevel: "system" });
|
|
426
|
+
return result?.sync_id ?? 0;
|
|
505
427
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
startDbWorker
|
|
510
|
-
};
|
|
428
|
+
//#endregion
|
|
429
|
+
export { createWsRemoteSource, getWorkerConfig, startDbWorker };
|
|
430
|
+
|
|
511
431
|
//# sourceMappingURL=worker.js.map
|