korajs 0.4.0 → 0.5.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/chunk-WALHLVVF.js +683 -0
- package/dist/chunk-WALHLVVF.js.map +1 -0
- package/dist/index.cjs +1090 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +105 -7
- package/dist/index.d.ts +105 -7
- package/dist/index.js +450 -149
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +700 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +111 -0
- package/dist/testing.d.ts +111 -0
- package/dist/testing.js +13 -0
- package/dist/testing.js.map +1 -0
- package/package.json +19 -7
package/dist/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApplyPipeline,
|
|
3
|
+
MergeAwareSyncStore,
|
|
4
|
+
StoreQueueStorage,
|
|
5
|
+
StoreSyncStatePersistence
|
|
6
|
+
} from "./chunk-WALHLVVF.js";
|
|
7
|
+
|
|
1
8
|
// src/create-app.ts
|
|
2
9
|
import { buildScopeMap } from "@korajs/core";
|
|
3
10
|
import { SimpleEventEmitter } from "@korajs/core/internal";
|
|
@@ -6,12 +13,18 @@ import { MergeEngine } from "@korajs/merge";
|
|
|
6
13
|
import { Store } from "@korajs/store";
|
|
7
14
|
import {
|
|
8
15
|
ConnectionMonitor,
|
|
16
|
+
HttpLongPollingTransport,
|
|
9
17
|
ReconnectionManager,
|
|
18
|
+
SyncEncryptor,
|
|
10
19
|
SyncEngine,
|
|
11
20
|
WebSocketTransport
|
|
12
21
|
} from "@korajs/sync";
|
|
13
22
|
|
|
14
23
|
// src/adapter-resolver.ts
|
|
24
|
+
function importOptionalPeer(specifier) {
|
|
25
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
26
|
+
return dynamicImport(specifier);
|
|
27
|
+
}
|
|
15
28
|
function detectAdapterType() {
|
|
16
29
|
if (typeof globalThis !== "undefined" && typeof globalThis.__TAURI_INTERNALS__ !== "undefined") {
|
|
17
30
|
return "tauri-sqlite";
|
|
@@ -27,13 +40,10 @@ function detectAdapterType() {
|
|
|
27
40
|
}
|
|
28
41
|
return "better-sqlite3";
|
|
29
42
|
}
|
|
30
|
-
async function createAdapter(type, dbName, workerUrl) {
|
|
43
|
+
async function createAdapter(type, dbName, workerUrl, emitter, workerResponseTimeoutMs, sharedWorkerUrl) {
|
|
31
44
|
switch (type) {
|
|
32
45
|
case "tauri-sqlite": {
|
|
33
|
-
const { TauriSqliteAdapter } = await
|
|
34
|
-
/* @vite-ignore */
|
|
35
|
-
"@korajs/tauri"
|
|
36
|
-
);
|
|
46
|
+
const { TauriSqliteAdapter } = await importOptionalPeer("@korajs/tauri");
|
|
37
47
|
return new TauriSqliteAdapter({ path: `${dbName}.db` });
|
|
38
48
|
}
|
|
39
49
|
case "better-sqlite3": {
|
|
@@ -45,11 +55,11 @@ async function createAdapter(type, dbName, workerUrl) {
|
|
|
45
55
|
}
|
|
46
56
|
case "sqlite-wasm": {
|
|
47
57
|
const { SqliteWasmAdapter } = await import("@korajs/store/sqlite-wasm");
|
|
48
|
-
return new SqliteWasmAdapter({ dbName, workerUrl });
|
|
58
|
+
return new SqliteWasmAdapter({ dbName, workerUrl, sharedWorkerUrl, workerResponseTimeoutMs });
|
|
49
59
|
}
|
|
50
60
|
case "indexeddb": {
|
|
51
61
|
const { IndexedDbAdapter } = await import("@korajs/store/indexeddb");
|
|
52
|
-
return new IndexedDbAdapter({ dbName, workerUrl });
|
|
62
|
+
return new IndexedDbAdapter({ dbName, workerUrl, emitter, workerResponseTimeoutMs });
|
|
53
63
|
}
|
|
54
64
|
default: {
|
|
55
65
|
const _exhaustive = type;
|
|
@@ -58,133 +68,289 @@ async function createAdapter(type, dbName, workerUrl) {
|
|
|
58
68
|
}
|
|
59
69
|
}
|
|
60
70
|
|
|
61
|
-
// src/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
mergeEngine;
|
|
70
|
-
emitter;
|
|
71
|
-
getVersionVector() {
|
|
72
|
-
return this.store.getVersionVector();
|
|
71
|
+
// src/app-not-ready-error.ts
|
|
72
|
+
import { KoraError } from "@korajs/core";
|
|
73
|
+
var AppNotReadyError = class extends KoraError {
|
|
74
|
+
constructor(detail) {
|
|
75
|
+
super(detail, "APP_NOT_READY", {
|
|
76
|
+
fix: "Await app.ready, or wrap your UI in <KoraProvider app={app}> before calling useQuery()."
|
|
77
|
+
});
|
|
78
|
+
this.name = "AppNotReadyError";
|
|
73
79
|
}
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/audit-bridge.ts
|
|
83
|
+
import { persistedAuditTraceFromEvent } from "@korajs/store";
|
|
84
|
+
var AUDIT_EVENT_TYPES = ["merge:completed", "merge:conflict", "constraint:violated"];
|
|
85
|
+
function isAuditEvent(event) {
|
|
86
|
+
return AUDIT_EVENT_TYPES.includes(event.type);
|
|
87
|
+
}
|
|
88
|
+
function wireAuditPersistence(store, emitter) {
|
|
89
|
+
const unsubscribers = AUDIT_EVENT_TYPES.map(
|
|
90
|
+
(eventType) => emitter.on(eventType, (event) => {
|
|
91
|
+
if (!isAuditEvent(event)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const trace = persistedAuditTraceFromEvent(event);
|
|
95
|
+
void store.appendAuditTrace(trace).catch(() => {
|
|
96
|
+
});
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
return () => {
|
|
100
|
+
for (const unsub of unsubscribers) {
|
|
101
|
+
unsub();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/sync-query-bridge.ts
|
|
107
|
+
function queryDescriptorToSyncSubset(descriptor) {
|
|
108
|
+
const where = {};
|
|
109
|
+
for (const [field, value] of Object.entries(descriptor.where)) {
|
|
110
|
+
if (value === null || value === void 0) {
|
|
111
|
+
where[field] = value;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
115
|
+
where[field] = value;
|
|
116
|
+
}
|
|
76
117
|
}
|
|
77
|
-
|
|
78
|
-
return
|
|
118
|
+
if (Object.keys(where).length === 0) {
|
|
119
|
+
return null;
|
|
79
120
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
121
|
+
return {
|
|
122
|
+
collection: descriptor.collection,
|
|
123
|
+
where
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function createSyncQuerySubscriptionHook(getSyncEngine) {
|
|
127
|
+
return (descriptor) => {
|
|
128
|
+
const subset = queryDescriptorToSyncSubset(descriptor);
|
|
129
|
+
if (!subset) {
|
|
130
|
+
return () => {
|
|
131
|
+
};
|
|
83
132
|
}
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
133
|
+
const engine = getSyncEngine();
|
|
134
|
+
if (!engine) {
|
|
135
|
+
return () => {
|
|
136
|
+
};
|
|
88
137
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
138
|
+
return engine.registerQuerySubset(subset);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/sync-status-bridge.ts
|
|
143
|
+
var OFFLINE_STATUS = Object.freeze({
|
|
144
|
+
status: "offline",
|
|
145
|
+
pendingOperations: 0,
|
|
146
|
+
lastSyncedAt: null,
|
|
147
|
+
lastSuccessfulPush: null,
|
|
148
|
+
lastSuccessfulPull: null,
|
|
149
|
+
conflicts: 0
|
|
150
|
+
});
|
|
151
|
+
var SYNC_STATUS_EVENT_TYPES = [
|
|
152
|
+
"sync:connected",
|
|
153
|
+
"sync:disconnected",
|
|
154
|
+
"sync:schema-mismatch",
|
|
155
|
+
"sync:auth-failed",
|
|
156
|
+
"sync:sent",
|
|
157
|
+
"sync:received",
|
|
158
|
+
"sync:acknowledged",
|
|
159
|
+
"sync:apply-failed",
|
|
160
|
+
"sync:diagnostics",
|
|
161
|
+
"sync:initial-sync-progress"
|
|
162
|
+
];
|
|
163
|
+
function createSyncStatusBridge(emitter, getSyncEngine) {
|
|
164
|
+
let currentStatus = OFFLINE_STATUS;
|
|
165
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
166
|
+
const refresh = () => {
|
|
167
|
+
const engine = getSyncEngine();
|
|
168
|
+
const next = engine ? engine.getStatus() : OFFLINE_STATUS;
|
|
169
|
+
const prevSerialized = JSON.stringify(currentStatus);
|
|
170
|
+
const nextSerialized = JSON.stringify(next);
|
|
171
|
+
if (prevSerialized === nextSerialized) {
|
|
172
|
+
return;
|
|
93
173
|
}
|
|
94
|
-
|
|
95
|
-
for (const
|
|
96
|
-
|
|
97
|
-
const currentLocal = currentRecord[field];
|
|
98
|
-
if (!deepEqual(expectedBase, currentLocal)) {
|
|
99
|
-
hasConflict = true;
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
174
|
+
currentStatus = next;
|
|
175
|
+
for (const listener of listeners) {
|
|
176
|
+
listener(currentStatus);
|
|
102
177
|
}
|
|
103
|
-
|
|
104
|
-
|
|
178
|
+
};
|
|
179
|
+
const unsubs = [];
|
|
180
|
+
for (const type of SYNC_STATUS_EVENT_TYPES) {
|
|
181
|
+
unsubs.push(emitter.on(type, refresh));
|
|
182
|
+
}
|
|
183
|
+
const bridge = {
|
|
184
|
+
get status() {
|
|
185
|
+
return currentStatus;
|
|
186
|
+
},
|
|
187
|
+
subscribe(listener) {
|
|
188
|
+
listeners.add(listener);
|
|
189
|
+
listener(currentStatus);
|
|
190
|
+
return () => {
|
|
191
|
+
listeners.delete(listener);
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
refresh,
|
|
195
|
+
destroy() {
|
|
196
|
+
for (const unsub of unsubs) {
|
|
197
|
+
unsub();
|
|
198
|
+
}
|
|
199
|
+
unsubs.length = 0;
|
|
200
|
+
listeners.clear();
|
|
105
201
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
202
|
+
};
|
|
203
|
+
refresh();
|
|
204
|
+
return bridge;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/validate-config.ts
|
|
208
|
+
import { KoraError as KoraError2, SchemaValidationError } from "@korajs/core";
|
|
209
|
+
function validateCreateAppConfig(config) {
|
|
210
|
+
if (!config.schema) {
|
|
211
|
+
throw new SchemaValidationError("createApp requires a schema.", {
|
|
212
|
+
fix: "Pass schema: defineSchema({ version: 1, collections: { ... } })"
|
|
110
213
|
});
|
|
111
|
-
const baseState = { ...op2.previousData };
|
|
112
|
-
const localOp = {
|
|
113
|
-
...op2,
|
|
114
|
-
// The "local" operation's data is the diff between base and current local state
|
|
115
|
-
data: buildLocalDiff(op2.previousData, currentRecord, Object.keys(op2.data)),
|
|
116
|
-
previousData: op2.previousData,
|
|
117
|
-
nodeId: this.store.getNodeId()
|
|
118
|
-
};
|
|
119
|
-
const input = {
|
|
120
|
-
local: localOp,
|
|
121
|
-
remote: op2,
|
|
122
|
-
baseState,
|
|
123
|
-
collectionDef
|
|
124
|
-
};
|
|
125
|
-
const result = this.mergeEngine.mergeFields(input);
|
|
126
|
-
for (const trace of result.traces) {
|
|
127
|
-
this.emitter?.emit({ type: "merge:conflict", trace });
|
|
128
|
-
}
|
|
129
|
-
const firstTrace = result.traces[0];
|
|
130
|
-
if (firstTrace) {
|
|
131
|
-
this.emitter?.emit({ type: "merge:completed", trace: firstTrace });
|
|
132
|
-
}
|
|
133
|
-
const mergedOp = {
|
|
134
|
-
...op2,
|
|
135
|
-
data: result.mergedData
|
|
136
|
-
};
|
|
137
|
-
return this.store.applyRemoteOperation(mergedOp);
|
|
138
214
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
215
|
+
if (config.schema.version < 1) {
|
|
216
|
+
throw new SchemaValidationError("Schema version must be at least 1.", {
|
|
217
|
+
version: config.schema.version
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const collectionNames = Object.keys(config.schema.collections);
|
|
221
|
+
if (collectionNames.length === 0) {
|
|
222
|
+
throw new SchemaValidationError("Schema must define at least one collection.", {
|
|
223
|
+
fix: "Add entries under collections in defineSchema()."
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (config.sync) {
|
|
227
|
+
validateSyncUrl(config.sync.url, config.sync.transport ?? "websocket");
|
|
228
|
+
}
|
|
229
|
+
const adapter = config.store?.adapter ?? detectAdapterType();
|
|
230
|
+
const isBrowser = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
|
|
231
|
+
if (isBrowser && (adapter === "sqlite-wasm" || adapter === "indexeddb") && !config.store?.workerUrl) {
|
|
232
|
+
throw new KoraError2(
|
|
233
|
+
"Browser storage requires store.workerUrl pointing to the SQLite WASM worker script.",
|
|
234
|
+
"MISSING_WORKER_URL",
|
|
235
|
+
{
|
|
236
|
+
adapter,
|
|
237
|
+
fix: 'Add store: { workerUrl: "/sqlite-wasm-worker.js" } (see create-kora-app templates).'
|
|
238
|
+
}
|
|
239
|
+
);
|
|
144
240
|
}
|
|
145
|
-
return diff;
|
|
146
241
|
}
|
|
147
|
-
function
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
153
|
-
if (a.length !== b.length) return false;
|
|
154
|
-
return a.every((val, i) => deepEqual(val, b[i]));
|
|
242
|
+
function validateSyncUrl(url, transport) {
|
|
243
|
+
if (!url || url.trim().length === 0) {
|
|
244
|
+
throw new KoraError2("sync.url is required when sync is configured.", "INVALID_SYNC_URL", {
|
|
245
|
+
fix: 'Pass sync: { url: "wss://your-server/kora" }.'
|
|
246
|
+
});
|
|
155
247
|
}
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
248
|
+
try {
|
|
249
|
+
const parsed = new URL(url);
|
|
250
|
+
if (transport === "http") {
|
|
251
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
252
|
+
throw new Error("bad protocol");
|
|
253
|
+
}
|
|
254
|
+
} else if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
|
255
|
+
throw new Error("bad protocol");
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
throw new KoraError2(
|
|
259
|
+
`Invalid sync URL "${url}" for transport "${transport}".`,
|
|
260
|
+
"INVALID_SYNC_URL",
|
|
261
|
+
{
|
|
262
|
+
url,
|
|
263
|
+
transport,
|
|
264
|
+
fix: transport === "http" ? "Use an absolute http:// or https:// URL." : "Use an absolute ws:// or wss:// URL."
|
|
265
|
+
}
|
|
162
266
|
);
|
|
163
267
|
}
|
|
164
|
-
return false;
|
|
165
268
|
}
|
|
166
269
|
|
|
167
270
|
// src/create-app.ts
|
|
168
271
|
function createApp(config) {
|
|
272
|
+
validateCreateAppConfig(config);
|
|
169
273
|
const emitter = new SimpleEventEmitter();
|
|
170
274
|
const mergeEngine = new MergeEngine();
|
|
275
|
+
if (config.onSyncEvent) {
|
|
276
|
+
const handler = config.onSyncEvent;
|
|
277
|
+
const syncTypes = [
|
|
278
|
+
"sync:connected",
|
|
279
|
+
"sync:disconnected",
|
|
280
|
+
"sync:schema-mismatch",
|
|
281
|
+
"sync:auth-failed",
|
|
282
|
+
"sync:sent",
|
|
283
|
+
"sync:received",
|
|
284
|
+
"sync:acknowledged",
|
|
285
|
+
"sync:apply-failed",
|
|
286
|
+
"sync:diagnostics",
|
|
287
|
+
"sync:bandwidth",
|
|
288
|
+
"sync:initial-sync-progress"
|
|
289
|
+
];
|
|
290
|
+
for (const type of syncTypes) {
|
|
291
|
+
emitter.on(type, (event) => {
|
|
292
|
+
handler(event);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
171
296
|
let store = null;
|
|
172
297
|
let syncEngine = null;
|
|
173
298
|
let unsubscribeSync = null;
|
|
299
|
+
let unsubscribeAudit = null;
|
|
174
300
|
let reconnectionManager = null;
|
|
175
301
|
let connectionMonitor = null;
|
|
176
302
|
let instrumenter = null;
|
|
177
303
|
let intentionalDisconnect = false;
|
|
178
304
|
let qualityInterval = null;
|
|
305
|
+
let syncStatusBridge = null;
|
|
306
|
+
let destroyDevtoolsOverlay = null;
|
|
179
307
|
if (config.devtools) {
|
|
180
308
|
instrumenter = new Instrumenter(emitter, {
|
|
181
309
|
bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
|
|
182
310
|
});
|
|
311
|
+
if (typeof globalThis !== "undefined" && "window" in globalThis) {
|
|
312
|
+
void import("@korajs/devtools/overlay").then(({ mountKoraDevtoolsOverlay }) => {
|
|
313
|
+
if (instrumenter) {
|
|
314
|
+
destroyDevtoolsOverlay = mountKoraDevtoolsOverlay(instrumenter);
|
|
315
|
+
}
|
|
316
|
+
}).catch(() => {
|
|
317
|
+
});
|
|
318
|
+
}
|
|
183
319
|
}
|
|
184
320
|
const ready = initializeAsync(config, emitter, mergeEngine).then((result) => {
|
|
185
321
|
store = result.store;
|
|
186
322
|
syncEngine = result.syncEngine;
|
|
187
323
|
unsubscribeSync = result.unsubscribeSync;
|
|
324
|
+
unsubscribeAudit = result.unsubscribeAudit;
|
|
325
|
+
const authBinding = result.authBinding;
|
|
326
|
+
if (config.sync) {
|
|
327
|
+
syncStatusBridge = createSyncStatusBridge(emitter, () => syncEngine);
|
|
328
|
+
syncStatusBridge.refresh();
|
|
329
|
+
}
|
|
330
|
+
if (config.sync && syncEngine && authBinding?.subscribe) {
|
|
331
|
+
authBinding.subscribe(() => {
|
|
332
|
+
const engine = syncEngine;
|
|
333
|
+
if (!engine) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
void (async () => {
|
|
337
|
+
const headers = await authBinding.auth();
|
|
338
|
+
if (!headers.token) {
|
|
339
|
+
await engine.stop();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (authBinding.resolveScopeMap) {
|
|
343
|
+
const nextScope = await authBinding.resolveScopeMap();
|
|
344
|
+
engine.updateScope(nextScope);
|
|
345
|
+
}
|
|
346
|
+
const status = engine.getStatus().status;
|
|
347
|
+
if (status !== "offline") {
|
|
348
|
+
await engine.stop();
|
|
349
|
+
}
|
|
350
|
+
await engine.start();
|
|
351
|
+
})();
|
|
352
|
+
});
|
|
353
|
+
}
|
|
188
354
|
if (config.sync && syncEngine) {
|
|
189
355
|
connectionMonitor = new ConnectionMonitor();
|
|
190
356
|
reconnectionManager = new ReconnectionManager({
|
|
@@ -209,10 +375,24 @@ function createApp(config) {
|
|
|
209
375
|
qualityInterval = null;
|
|
210
376
|
}
|
|
211
377
|
});
|
|
378
|
+
const browserGlobal = globalThis;
|
|
379
|
+
if (typeof browserGlobal.addEventListener === "function") {
|
|
380
|
+
const engine = syncEngine;
|
|
381
|
+
browserGlobal.addEventListener("online", () => {
|
|
382
|
+
if (intentionalDisconnect || config.sync?.autoReconnect === false) return;
|
|
383
|
+
reconnectionManager?.wake();
|
|
384
|
+
reconnectionManager?.reset();
|
|
385
|
+
void engine.retryNow();
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
emitter.on("sync:schema-mismatch", () => {
|
|
389
|
+
reconnectionManager?.stop();
|
|
390
|
+
intentionalDisconnect = true;
|
|
391
|
+
});
|
|
212
392
|
if (config.sync.autoReconnect !== false) {
|
|
213
393
|
const engine = syncEngine;
|
|
214
394
|
emitter.on("sync:disconnected", () => {
|
|
215
|
-
if (intentionalDisconnect) return;
|
|
395
|
+
if (intentionalDisconnect || engine.isSchemaBlocked()) return;
|
|
216
396
|
if (reconnectionManager?.isRunning()) return;
|
|
217
397
|
engine.setReconnecting(true);
|
|
218
398
|
reconnectionManager?.stop();
|
|
@@ -229,9 +409,32 @@ function createApp(config) {
|
|
|
229
409
|
});
|
|
230
410
|
});
|
|
231
411
|
}
|
|
412
|
+
if (config.sync.autoConnect === true && syncEngine) {
|
|
413
|
+
void syncEngine.start().catch(() => {
|
|
414
|
+
});
|
|
415
|
+
}
|
|
232
416
|
}
|
|
233
417
|
});
|
|
418
|
+
const offlineSyncStatus = () => ({
|
|
419
|
+
status: "offline",
|
|
420
|
+
pendingOperations: 0,
|
|
421
|
+
lastSyncedAt: null,
|
|
422
|
+
lastSuccessfulPush: null,
|
|
423
|
+
lastSuccessfulPull: null,
|
|
424
|
+
conflicts: 0
|
|
425
|
+
});
|
|
234
426
|
const syncControl = config.sync ? {
|
|
427
|
+
get status() {
|
|
428
|
+
return syncStatusBridge?.status ?? offlineSyncStatus();
|
|
429
|
+
},
|
|
430
|
+
subscribeStatus(listener) {
|
|
431
|
+
if (syncStatusBridge) {
|
|
432
|
+
return syncStatusBridge.subscribe(listener);
|
|
433
|
+
}
|
|
434
|
+
listener(offlineSyncStatus());
|
|
435
|
+
return () => {
|
|
436
|
+
};
|
|
437
|
+
},
|
|
235
438
|
async connect() {
|
|
236
439
|
await ready;
|
|
237
440
|
if (syncEngine) {
|
|
@@ -239,6 +442,7 @@ function createApp(config) {
|
|
|
239
442
|
reconnectionManager?.stop();
|
|
240
443
|
reconnectionManager?.reset();
|
|
241
444
|
await syncEngine.start();
|
|
445
|
+
syncStatusBridge?.refresh();
|
|
242
446
|
}
|
|
243
447
|
},
|
|
244
448
|
async disconnect() {
|
|
@@ -247,20 +451,14 @@ function createApp(config) {
|
|
|
247
451
|
intentionalDisconnect = true;
|
|
248
452
|
reconnectionManager?.stop();
|
|
249
453
|
await syncEngine.stop();
|
|
454
|
+
syncStatusBridge?.refresh();
|
|
250
455
|
}
|
|
251
456
|
},
|
|
252
457
|
getStatus() {
|
|
253
458
|
if (syncEngine) {
|
|
254
459
|
return syncEngine.getStatus();
|
|
255
460
|
}
|
|
256
|
-
return
|
|
257
|
-
status: "offline",
|
|
258
|
-
pendingOperations: 0,
|
|
259
|
-
lastSyncedAt: null,
|
|
260
|
-
lastSuccessfulPush: null,
|
|
261
|
-
lastSuccessfulPull: null,
|
|
262
|
-
conflicts: 0
|
|
263
|
-
};
|
|
461
|
+
return offlineSyncStatus();
|
|
264
462
|
},
|
|
265
463
|
async retryNow() {
|
|
266
464
|
await ready;
|
|
@@ -268,6 +466,9 @@ function createApp(config) {
|
|
|
268
466
|
await syncEngine.retryNow();
|
|
269
467
|
}
|
|
270
468
|
},
|
|
469
|
+
clearSchemaBlock() {
|
|
470
|
+
syncEngine?.clearSchemaBlock();
|
|
471
|
+
},
|
|
271
472
|
exportDiagnostics() {
|
|
272
473
|
if (syncEngine) {
|
|
273
474
|
return syncEngine.exportDiagnostics();
|
|
@@ -301,33 +502,23 @@ function createApp(config) {
|
|
|
301
502
|
if (!store) {
|
|
302
503
|
throw new Error("Store not initialized. Await app.ready before using transactions.");
|
|
303
504
|
}
|
|
304
|
-
const txContext = store.createTransaction();
|
|
305
|
-
if (mutationName !== void 0) {
|
|
306
|
-
txContext.setMutationName(mutationName);
|
|
307
|
-
}
|
|
308
505
|
const collectionNames = Object.keys(config.schema.collections);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
506
|
+
return store.transaction(async (tx) => {
|
|
507
|
+
if (mutationName !== void 0) {
|
|
508
|
+
tx.setMutationName(mutationName);
|
|
509
|
+
}
|
|
510
|
+
const proxy = {};
|
|
511
|
+
for (const name of collectionNames) {
|
|
512
|
+
Object.defineProperty(proxy, name, {
|
|
513
|
+
get() {
|
|
514
|
+
return tx.collection(name);
|
|
515
|
+
},
|
|
516
|
+
enumerable: true,
|
|
517
|
+
configurable: false
|
|
518
|
+
});
|
|
519
|
+
}
|
|
320
520
|
await fn(proxy);
|
|
321
|
-
|
|
322
|
-
for (const op2 of operations) {
|
|
323
|
-
store.getSubscriptionManager().notify(op2.collection, op2);
|
|
324
|
-
emitter.emit({ type: "operation:created", operation: op2 });
|
|
325
|
-
}
|
|
326
|
-
return operations;
|
|
327
|
-
} catch (error) {
|
|
328
|
-
txContext.rollback();
|
|
329
|
-
throw error;
|
|
330
|
-
}
|
|
521
|
+
});
|
|
331
522
|
}
|
|
332
523
|
const sequences = {
|
|
333
524
|
async next(name, config2) {
|
|
@@ -374,6 +565,10 @@ function createApp(config) {
|
|
|
374
565
|
qualityInterval = null;
|
|
375
566
|
}
|
|
376
567
|
reconnectionManager?.stop();
|
|
568
|
+
if (destroyDevtoolsOverlay) {
|
|
569
|
+
destroyDevtoolsOverlay();
|
|
570
|
+
destroyDevtoolsOverlay = null;
|
|
571
|
+
}
|
|
377
572
|
if (instrumenter) {
|
|
378
573
|
instrumenter.destroy();
|
|
379
574
|
instrumenter = null;
|
|
@@ -382,15 +577,51 @@ function createApp(config) {
|
|
|
382
577
|
unsubscribeSync();
|
|
383
578
|
unsubscribeSync = null;
|
|
384
579
|
}
|
|
580
|
+
if (unsubscribeAudit) {
|
|
581
|
+
unsubscribeAudit();
|
|
582
|
+
unsubscribeAudit = null;
|
|
583
|
+
}
|
|
385
584
|
if (syncEngine) {
|
|
386
585
|
await syncEngine.stop();
|
|
387
586
|
syncEngine = null;
|
|
388
587
|
}
|
|
588
|
+
if (syncStatusBridge) {
|
|
589
|
+
syncStatusBridge.destroy();
|
|
590
|
+
syncStatusBridge = null;
|
|
591
|
+
}
|
|
389
592
|
if (store) {
|
|
390
593
|
await store.close();
|
|
391
594
|
store = null;
|
|
392
595
|
}
|
|
393
596
|
emitter.clear();
|
|
597
|
+
},
|
|
598
|
+
async exportBackup(options) {
|
|
599
|
+
await ready;
|
|
600
|
+
if (!store) {
|
|
601
|
+
throw new Error("Store not initialized. Await app.ready before exporting backup.");
|
|
602
|
+
}
|
|
603
|
+
return store.exportBackup(options);
|
|
604
|
+
},
|
|
605
|
+
async importBackup(data, options) {
|
|
606
|
+
await ready;
|
|
607
|
+
if (!store) {
|
|
608
|
+
throw new Error("Store not initialized. Await app.ready before importing backup.");
|
|
609
|
+
}
|
|
610
|
+
return store.importBackup(data, options);
|
|
611
|
+
},
|
|
612
|
+
async replayTo(operationId) {
|
|
613
|
+
await ready;
|
|
614
|
+
if (!store) {
|
|
615
|
+
throw new Error("Store not initialized. Await app.ready before replaying operations.");
|
|
616
|
+
}
|
|
617
|
+
return store.replayTo(operationId);
|
|
618
|
+
},
|
|
619
|
+
async exportAudit(options) {
|
|
620
|
+
await ready;
|
|
621
|
+
if (!store) {
|
|
622
|
+
throw new Error("Store not initialized. Await app.ready before exporting audit data.");
|
|
623
|
+
}
|
|
624
|
+
return store.exportAudit(options);
|
|
394
625
|
}
|
|
395
626
|
};
|
|
396
627
|
for (const collectionName of Object.keys(config.schema.collections)) {
|
|
@@ -407,39 +638,87 @@ function createApp(config) {
|
|
|
407
638
|
async function initializeAsync(config, emitter, mergeEngine) {
|
|
408
639
|
const adapterType = config.store?.adapter ?? detectAdapterType();
|
|
409
640
|
const dbName = config.store?.name ?? "kora-db";
|
|
410
|
-
const adapter = await createAdapter(
|
|
641
|
+
const adapter = await createAdapter(
|
|
642
|
+
adapterType,
|
|
643
|
+
dbName,
|
|
644
|
+
config.store?.workerUrl,
|
|
645
|
+
emitter,
|
|
646
|
+
config.store?.workerResponseTimeoutMs,
|
|
647
|
+
config.store?.sharedWorkerUrl
|
|
648
|
+
);
|
|
649
|
+
const authBinding = config.sync?.authClient;
|
|
650
|
+
const authNodeId = authBinding?.resolveNodeId ? await authBinding.resolveNodeId() : void 0;
|
|
651
|
+
let syncEngine = null;
|
|
411
652
|
const store = new Store({
|
|
412
653
|
schema: config.schema,
|
|
413
654
|
adapter,
|
|
414
|
-
emitter
|
|
655
|
+
emitter,
|
|
656
|
+
dbName,
|
|
657
|
+
nodeId: authNodeId,
|
|
658
|
+
isolation: authNodeId ? "shared" : config.store?.isolation,
|
|
659
|
+
...config.sync ? { onQuerySubscribed: createSyncQuerySubscriptionHook(() => syncEngine) } : {}
|
|
415
660
|
});
|
|
416
661
|
await store.open();
|
|
417
|
-
let
|
|
662
|
+
let recordConflict;
|
|
663
|
+
const applyPipeline = new ApplyPipeline({
|
|
664
|
+
store,
|
|
665
|
+
mergeEngine,
|
|
666
|
+
emitter,
|
|
667
|
+
onMergeConflict: () => recordConflict?.()
|
|
668
|
+
});
|
|
669
|
+
store.setLocalMutationHandler(applyPipeline);
|
|
670
|
+
const unsubscribeAudit = wireAuditPersistence(store, emitter);
|
|
418
671
|
let unsubscribeSync = null;
|
|
419
672
|
if (config.sync) {
|
|
420
|
-
const transport =
|
|
421
|
-
const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter
|
|
422
|
-
|
|
673
|
+
const transport = createSyncTransport(config.sync);
|
|
674
|
+
const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter, {
|
|
675
|
+
onMergeConflict: () => recordConflict?.()
|
|
676
|
+
});
|
|
677
|
+
let scopeMap = config.sync.scope ? buildScopeMap(config.schema, config.sync.scope) : void 0;
|
|
678
|
+
if (authBinding?.resolveScopeMap) {
|
|
679
|
+
scopeMap = await authBinding.resolveScopeMap() ?? scopeMap;
|
|
680
|
+
}
|
|
681
|
+
const syncAuth = authBinding?.auth ?? config.sync.auth;
|
|
682
|
+
const encryptor = config.sync.encryption?.enabled === true ? await SyncEncryptor.create(config.sync.encryption) : void 0;
|
|
423
683
|
syncEngine = new SyncEngine({
|
|
424
684
|
transport,
|
|
425
685
|
store: mergeAwareStore,
|
|
426
686
|
config: {
|
|
427
687
|
url: config.sync.url,
|
|
428
688
|
transport: config.sync.transport,
|
|
429
|
-
auth:
|
|
689
|
+
auth: syncAuth,
|
|
430
690
|
batchSize: config.sync.batchSize,
|
|
431
691
|
schemaVersion: config.sync.schemaVersion ?? config.schema.version,
|
|
432
|
-
scopeMap
|
|
692
|
+
scopeMap,
|
|
693
|
+
encryption: config.sync.encryption,
|
|
694
|
+
strictHandshake: config.sync.strictHandshake,
|
|
695
|
+
operationTransforms: config.sync.operationTransforms
|
|
433
696
|
},
|
|
434
|
-
emitter
|
|
697
|
+
emitter,
|
|
698
|
+
queueStorage: new StoreQueueStorage(adapter),
|
|
699
|
+
syncState: new StoreSyncStatePersistence(store, scopeMap),
|
|
700
|
+
encryptor
|
|
435
701
|
});
|
|
702
|
+
recordConflict = () => syncEngine?.recordConflict();
|
|
436
703
|
unsubscribeSync = emitter.on("operation:created", (event) => {
|
|
437
704
|
if (syncEngine) {
|
|
438
705
|
syncEngine.pushOperation(event.operation);
|
|
439
706
|
}
|
|
440
707
|
});
|
|
441
708
|
}
|
|
442
|
-
return {
|
|
709
|
+
return {
|
|
710
|
+
store,
|
|
711
|
+
syncEngine,
|
|
712
|
+
unsubscribeSync,
|
|
713
|
+
unsubscribeAudit,
|
|
714
|
+
authBinding: config.sync?.authClient ?? null
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function createSyncTransport(sync) {
|
|
718
|
+
if (sync.transport === "http") {
|
|
719
|
+
return new HttpLongPollingTransport();
|
|
720
|
+
}
|
|
721
|
+
return new WebSocketTransport();
|
|
443
722
|
}
|
|
444
723
|
function createCollectionAccessor(collectionName, getStore) {
|
|
445
724
|
return {
|
|
@@ -504,15 +783,19 @@ function createPendingQueryBuilder(initialWhere) {
|
|
|
504
783
|
return this;
|
|
505
784
|
},
|
|
506
785
|
async exec() {
|
|
507
|
-
|
|
786
|
+
throw new AppNotReadyError(
|
|
787
|
+
"Cannot execute a query before app.ready. Await app.ready or use <KoraProvider app={app}>."
|
|
788
|
+
);
|
|
508
789
|
},
|
|
509
790
|
async count() {
|
|
510
|
-
|
|
791
|
+
throw new AppNotReadyError(
|
|
792
|
+
"Cannot count query results before app.ready. Await app.ready or use <KoraProvider app={app}>."
|
|
793
|
+
);
|
|
511
794
|
},
|
|
512
|
-
subscribe(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
795
|
+
subscribe(_callback) {
|
|
796
|
+
throw new AppNotReadyError(
|
|
797
|
+
"Cannot subscribe to a query before app.ready. Await app.ready or use <KoraProvider app={app}>."
|
|
798
|
+
);
|
|
516
799
|
},
|
|
517
800
|
getDescriptor() {
|
|
518
801
|
return { ...descriptor, where: { ...descriptor.where }, orderBy: [...descriptor.orderBy] };
|
|
@@ -522,19 +805,30 @@ function createPendingQueryBuilder(initialWhere) {
|
|
|
522
805
|
}
|
|
523
806
|
|
|
524
807
|
// src/index.ts
|
|
808
|
+
import {
|
|
809
|
+
decodeAuditExport,
|
|
810
|
+
readAuditExportManifest,
|
|
811
|
+
verifyAuditExportChecksum
|
|
812
|
+
} from "@korajs/store";
|
|
525
813
|
import { defineSchema, migrate, t } from "@korajs/core";
|
|
526
814
|
import { HybridLogicalClock } from "@korajs/core";
|
|
527
815
|
import { generateUUIDv7 } from "@korajs/core";
|
|
528
816
|
import { createOperation } from "@korajs/core";
|
|
529
|
-
import { KoraError } from "@korajs/core";
|
|
817
|
+
import { KoraError as KoraError3 } from "@korajs/core";
|
|
530
818
|
import { op } from "@korajs/core";
|
|
531
819
|
import { SequenceManager, Store as Store2 } from "@korajs/store";
|
|
532
820
|
import { TransactionContext } from "@korajs/store";
|
|
821
|
+
import {
|
|
822
|
+
exportBackup,
|
|
823
|
+
readBackupManifest,
|
|
824
|
+
restoreBackup,
|
|
825
|
+
verifyBackupChecksum
|
|
826
|
+
} from "@korajs/store";
|
|
533
827
|
import { MergeEngine as MergeEngine2 } from "@korajs/merge";
|
|
534
828
|
import { SyncEngine as SyncEngine2, WebSocketTransport as WebSocketTransport2 } from "@korajs/sync";
|
|
535
829
|
export {
|
|
536
830
|
HybridLogicalClock,
|
|
537
|
-
KoraError,
|
|
831
|
+
KoraError3 as KoraError,
|
|
538
832
|
MergeEngine2 as MergeEngine,
|
|
539
833
|
SequenceManager,
|
|
540
834
|
Store2 as Store,
|
|
@@ -543,10 +837,17 @@ export {
|
|
|
543
837
|
WebSocketTransport2 as WebSocketTransport,
|
|
544
838
|
createApp,
|
|
545
839
|
createOperation,
|
|
840
|
+
decodeAuditExport,
|
|
546
841
|
defineSchema,
|
|
842
|
+
exportBackup,
|
|
547
843
|
generateUUIDv7,
|
|
548
844
|
migrate,
|
|
549
845
|
op,
|
|
550
|
-
|
|
846
|
+
readAuditExportManifest,
|
|
847
|
+
readBackupManifest,
|
|
848
|
+
restoreBackup,
|
|
849
|
+
t,
|
|
850
|
+
verifyAuditExportChecksum,
|
|
851
|
+
verifyBackupChecksum
|
|
551
852
|
};
|
|
552
853
|
//# sourceMappingURL=index.js.map
|