korajs 0.3.5 → 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/config.cjs.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.cjs +1167 -103
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +222 -7
- package/dist/index.d.ts +222 -7
- package/dist/index.js +531 -119
- 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,11 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApplyPipeline,
|
|
3
|
+
MergeAwareSyncStore,
|
|
4
|
+
StoreQueueStorage,
|
|
5
|
+
StoreSyncStatePersistence
|
|
6
|
+
} from "./chunk-WALHLVVF.js";
|
|
7
|
+
|
|
1
8
|
// src/create-app.ts
|
|
9
|
+
import { buildScopeMap } from "@korajs/core";
|
|
2
10
|
import { SimpleEventEmitter } from "@korajs/core/internal";
|
|
3
11
|
import { Instrumenter } from "@korajs/devtools";
|
|
4
12
|
import { MergeEngine } from "@korajs/merge";
|
|
5
13
|
import { Store } from "@korajs/store";
|
|
6
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
ConnectionMonitor,
|
|
16
|
+
HttpLongPollingTransport,
|
|
17
|
+
ReconnectionManager,
|
|
18
|
+
SyncEncryptor,
|
|
19
|
+
SyncEngine,
|
|
20
|
+
WebSocketTransport
|
|
21
|
+
} from "@korajs/sync";
|
|
7
22
|
|
|
8
23
|
// src/adapter-resolver.ts
|
|
24
|
+
function importOptionalPeer(specifier) {
|
|
25
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
26
|
+
return dynamicImport(specifier);
|
|
27
|
+
}
|
|
9
28
|
function detectAdapterType() {
|
|
10
29
|
if (typeof globalThis !== "undefined" && typeof globalThis.__TAURI_INTERNALS__ !== "undefined") {
|
|
11
30
|
return "tauri-sqlite";
|
|
@@ -21,13 +40,10 @@ function detectAdapterType() {
|
|
|
21
40
|
}
|
|
22
41
|
return "better-sqlite3";
|
|
23
42
|
}
|
|
24
|
-
async function createAdapter(type, dbName, workerUrl) {
|
|
43
|
+
async function createAdapter(type, dbName, workerUrl, emitter, workerResponseTimeoutMs, sharedWorkerUrl) {
|
|
25
44
|
switch (type) {
|
|
26
45
|
case "tauri-sqlite": {
|
|
27
|
-
const { TauriSqliteAdapter } = await
|
|
28
|
-
/* @vite-ignore */
|
|
29
|
-
"@korajs/tauri"
|
|
30
|
-
);
|
|
46
|
+
const { TauriSqliteAdapter } = await importOptionalPeer("@korajs/tauri");
|
|
31
47
|
return new TauriSqliteAdapter({ path: `${dbName}.db` });
|
|
32
48
|
}
|
|
33
49
|
case "better-sqlite3": {
|
|
@@ -39,11 +55,11 @@ async function createAdapter(type, dbName, workerUrl) {
|
|
|
39
55
|
}
|
|
40
56
|
case "sqlite-wasm": {
|
|
41
57
|
const { SqliteWasmAdapter } = await import("@korajs/store/sqlite-wasm");
|
|
42
|
-
return new SqliteWasmAdapter({ dbName, workerUrl });
|
|
58
|
+
return new SqliteWasmAdapter({ dbName, workerUrl, sharedWorkerUrl, workerResponseTimeoutMs });
|
|
43
59
|
}
|
|
44
60
|
case "indexeddb": {
|
|
45
61
|
const { IndexedDbAdapter } = await import("@korajs/store/indexeddb");
|
|
46
|
-
return new IndexedDbAdapter({ dbName, workerUrl });
|
|
62
|
+
return new IndexedDbAdapter({ dbName, workerUrl, emitter, workerResponseTimeoutMs });
|
|
47
63
|
}
|
|
48
64
|
default: {
|
|
49
65
|
const _exhaustive = type;
|
|
@@ -52,133 +68,289 @@ async function createAdapter(type, dbName, workerUrl) {
|
|
|
52
68
|
}
|
|
53
69
|
}
|
|
54
70
|
|
|
55
|
-
// src/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
mergeEngine;
|
|
64
|
-
emitter;
|
|
65
|
-
getVersionVector() {
|
|
66
|
-
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";
|
|
67
79
|
}
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
}
|
|
70
117
|
}
|
|
71
|
-
|
|
72
|
-
return
|
|
118
|
+
if (Object.keys(where).length === 0) {
|
|
119
|
+
return null;
|
|
73
120
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
};
|
|
77
132
|
}
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
133
|
+
const engine = getSyncEngine();
|
|
134
|
+
if (!engine) {
|
|
135
|
+
return () => {
|
|
136
|
+
};
|
|
82
137
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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;
|
|
87
173
|
}
|
|
88
|
-
|
|
89
|
-
for (const
|
|
90
|
-
|
|
91
|
-
const currentLocal = currentRecord[field];
|
|
92
|
-
if (!deepEqual(expectedBase, currentLocal)) {
|
|
93
|
-
hasConflict = true;
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
174
|
+
currentStatus = next;
|
|
175
|
+
for (const listener of listeners) {
|
|
176
|
+
listener(currentStatus);
|
|
96
177
|
}
|
|
97
|
-
|
|
98
|
-
|
|
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();
|
|
99
201
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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: { ... } })"
|
|
104
213
|
});
|
|
105
|
-
const baseState = { ...op.previousData };
|
|
106
|
-
const localOp = {
|
|
107
|
-
...op,
|
|
108
|
-
// The "local" operation's data is the diff between base and current local state
|
|
109
|
-
data: buildLocalDiff(op.previousData, currentRecord, Object.keys(op.data)),
|
|
110
|
-
previousData: op.previousData,
|
|
111
|
-
nodeId: this.store.getNodeId()
|
|
112
|
-
};
|
|
113
|
-
const input = {
|
|
114
|
-
local: localOp,
|
|
115
|
-
remote: op,
|
|
116
|
-
baseState,
|
|
117
|
-
collectionDef
|
|
118
|
-
};
|
|
119
|
-
const result = this.mergeEngine.mergeFields(input);
|
|
120
|
-
for (const trace of result.traces) {
|
|
121
|
-
this.emitter?.emit({ type: "merge:conflict", trace });
|
|
122
|
-
}
|
|
123
|
-
const firstTrace = result.traces[0];
|
|
124
|
-
if (firstTrace) {
|
|
125
|
-
this.emitter?.emit({ type: "merge:completed", trace: firstTrace });
|
|
126
|
-
}
|
|
127
|
-
const mergedOp = {
|
|
128
|
-
...op,
|
|
129
|
-
data: result.mergedData
|
|
130
|
-
};
|
|
131
|
-
return this.store.applyRemoteOperation(mergedOp);
|
|
132
214
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
);
|
|
138
240
|
}
|
|
139
|
-
return diff;
|
|
140
241
|
}
|
|
141
|
-
function
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
147
|
-
if (a.length !== b.length) return false;
|
|
148
|
-
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
|
+
});
|
|
149
247
|
}
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
+
}
|
|
156
266
|
);
|
|
157
267
|
}
|
|
158
|
-
return false;
|
|
159
268
|
}
|
|
160
269
|
|
|
161
270
|
// src/create-app.ts
|
|
162
271
|
function createApp(config) {
|
|
272
|
+
validateCreateAppConfig(config);
|
|
163
273
|
const emitter = new SimpleEventEmitter();
|
|
164
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
|
+
}
|
|
165
296
|
let store = null;
|
|
166
297
|
let syncEngine = null;
|
|
167
298
|
let unsubscribeSync = null;
|
|
299
|
+
let unsubscribeAudit = null;
|
|
168
300
|
let reconnectionManager = null;
|
|
169
301
|
let connectionMonitor = null;
|
|
170
302
|
let instrumenter = null;
|
|
171
303
|
let intentionalDisconnect = false;
|
|
172
304
|
let qualityInterval = null;
|
|
305
|
+
let syncStatusBridge = null;
|
|
306
|
+
let destroyDevtoolsOverlay = null;
|
|
173
307
|
if (config.devtools) {
|
|
174
308
|
instrumenter = new Instrumenter(emitter, {
|
|
175
309
|
bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
|
|
176
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
|
+
}
|
|
177
319
|
}
|
|
178
320
|
const ready = initializeAsync(config, emitter, mergeEngine).then((result) => {
|
|
179
321
|
store = result.store;
|
|
180
322
|
syncEngine = result.syncEngine;
|
|
181
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
|
+
}
|
|
182
354
|
if (config.sync && syncEngine) {
|
|
183
355
|
connectionMonitor = new ConnectionMonitor();
|
|
184
356
|
reconnectionManager = new ReconnectionManager({
|
|
@@ -203,10 +375,24 @@ function createApp(config) {
|
|
|
203
375
|
qualityInterval = null;
|
|
204
376
|
}
|
|
205
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
|
+
});
|
|
206
392
|
if (config.sync.autoReconnect !== false) {
|
|
207
393
|
const engine = syncEngine;
|
|
208
394
|
emitter.on("sync:disconnected", () => {
|
|
209
|
-
if (intentionalDisconnect) return;
|
|
395
|
+
if (intentionalDisconnect || engine.isSchemaBlocked()) return;
|
|
210
396
|
if (reconnectionManager?.isRunning()) return;
|
|
211
397
|
engine.setReconnecting(true);
|
|
212
398
|
reconnectionManager?.stop();
|
|
@@ -223,9 +409,32 @@ function createApp(config) {
|
|
|
223
409
|
});
|
|
224
410
|
});
|
|
225
411
|
}
|
|
412
|
+
if (config.sync.autoConnect === true && syncEngine) {
|
|
413
|
+
void syncEngine.start().catch(() => {
|
|
414
|
+
});
|
|
415
|
+
}
|
|
226
416
|
}
|
|
227
417
|
});
|
|
418
|
+
const offlineSyncStatus = () => ({
|
|
419
|
+
status: "offline",
|
|
420
|
+
pendingOperations: 0,
|
|
421
|
+
lastSyncedAt: null,
|
|
422
|
+
lastSuccessfulPush: null,
|
|
423
|
+
lastSuccessfulPull: null,
|
|
424
|
+
conflicts: 0
|
|
425
|
+
});
|
|
228
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
|
+
},
|
|
229
438
|
async connect() {
|
|
230
439
|
await ready;
|
|
231
440
|
if (syncEngine) {
|
|
@@ -233,6 +442,7 @@ function createApp(config) {
|
|
|
233
442
|
reconnectionManager?.stop();
|
|
234
443
|
reconnectionManager?.reset();
|
|
235
444
|
await syncEngine.start();
|
|
445
|
+
syncStatusBridge?.refresh();
|
|
236
446
|
}
|
|
237
447
|
},
|
|
238
448
|
async disconnect() {
|
|
@@ -241,19 +451,97 @@ function createApp(config) {
|
|
|
241
451
|
intentionalDisconnect = true;
|
|
242
452
|
reconnectionManager?.stop();
|
|
243
453
|
await syncEngine.stop();
|
|
454
|
+
syncStatusBridge?.refresh();
|
|
244
455
|
}
|
|
245
456
|
},
|
|
246
457
|
getStatus() {
|
|
247
458
|
if (syncEngine) {
|
|
248
459
|
return syncEngine.getStatus();
|
|
249
460
|
}
|
|
250
|
-
return
|
|
461
|
+
return offlineSyncStatus();
|
|
462
|
+
},
|
|
463
|
+
async retryNow() {
|
|
464
|
+
await ready;
|
|
465
|
+
if (syncEngine) {
|
|
466
|
+
await syncEngine.retryNow();
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
clearSchemaBlock() {
|
|
470
|
+
syncEngine?.clearSchemaBlock();
|
|
471
|
+
},
|
|
472
|
+
exportDiagnostics() {
|
|
473
|
+
if (syncEngine) {
|
|
474
|
+
return syncEngine.exportDiagnostics();
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
state: "disconnected",
|
|
478
|
+
status: {
|
|
479
|
+
status: "offline",
|
|
480
|
+
pendingOperations: 0,
|
|
481
|
+
lastSyncedAt: null,
|
|
482
|
+
lastSuccessfulPush: null,
|
|
483
|
+
lastSuccessfulPull: null,
|
|
484
|
+
conflicts: 0
|
|
485
|
+
},
|
|
486
|
+
nodeId: "",
|
|
487
|
+
url: config.sync?.url ?? "",
|
|
488
|
+
schemaVersion: config.schema.version,
|
|
489
|
+
lastSyncedAt: null,
|
|
490
|
+
lastSuccessfulPush: null,
|
|
491
|
+
lastSuccessfulPull: null,
|
|
492
|
+
conflicts: 0,
|
|
493
|
+
pendingOperations: 0,
|
|
494
|
+
hasInFlightBatch: false,
|
|
495
|
+
reconnecting: false,
|
|
496
|
+
timestamp: Date.now()
|
|
497
|
+
};
|
|
251
498
|
}
|
|
252
499
|
} : null;
|
|
500
|
+
async function executeTransaction(fn, mutationName) {
|
|
501
|
+
await ready;
|
|
502
|
+
if (!store) {
|
|
503
|
+
throw new Error("Store not initialized. Await app.ready before using transactions.");
|
|
504
|
+
}
|
|
505
|
+
const collectionNames = Object.keys(config.schema.collections);
|
|
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
|
+
}
|
|
520
|
+
await fn(proxy);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
const sequences = {
|
|
524
|
+
async next(name, config2) {
|
|
525
|
+
await ready;
|
|
526
|
+
if (!store) throw new Error("Store not initialized. Await app.ready before using sequences.");
|
|
527
|
+
return store.getSequenceManager().next(name, config2);
|
|
528
|
+
},
|
|
529
|
+
async current(name, config2) {
|
|
530
|
+
await ready;
|
|
531
|
+
if (!store) throw new Error("Store not initialized. Await app.ready before using sequences.");
|
|
532
|
+
return store.getSequenceManager().current(name, config2);
|
|
533
|
+
},
|
|
534
|
+
async reset(name, config2) {
|
|
535
|
+
await ready;
|
|
536
|
+
if (!store) throw new Error("Store not initialized. Await app.ready before using sequences.");
|
|
537
|
+
return store.getSequenceManager().reset(name, config2);
|
|
538
|
+
}
|
|
539
|
+
};
|
|
253
540
|
const app = {
|
|
254
541
|
ready,
|
|
255
542
|
events: emitter,
|
|
256
543
|
sync: syncControl,
|
|
544
|
+
sequences,
|
|
257
545
|
getStore() {
|
|
258
546
|
if (!store) {
|
|
259
547
|
throw new Error("Store not initialized. Await app.ready before accessing the store.");
|
|
@@ -263,6 +551,12 @@ function createApp(config) {
|
|
|
263
551
|
getSyncEngine() {
|
|
264
552
|
return syncEngine;
|
|
265
553
|
},
|
|
554
|
+
async transaction(fn) {
|
|
555
|
+
return executeTransaction(fn);
|
|
556
|
+
},
|
|
557
|
+
async mutation(name, fn) {
|
|
558
|
+
return executeTransaction(fn, name);
|
|
559
|
+
},
|
|
266
560
|
async close() {
|
|
267
561
|
await ready;
|
|
268
562
|
intentionalDisconnect = true;
|
|
@@ -271,6 +565,10 @@ function createApp(config) {
|
|
|
271
565
|
qualityInterval = null;
|
|
272
566
|
}
|
|
273
567
|
reconnectionManager?.stop();
|
|
568
|
+
if (destroyDevtoolsOverlay) {
|
|
569
|
+
destroyDevtoolsOverlay();
|
|
570
|
+
destroyDevtoolsOverlay = null;
|
|
571
|
+
}
|
|
274
572
|
if (instrumenter) {
|
|
275
573
|
instrumenter.destroy();
|
|
276
574
|
instrumenter = null;
|
|
@@ -279,15 +577,51 @@ function createApp(config) {
|
|
|
279
577
|
unsubscribeSync();
|
|
280
578
|
unsubscribeSync = null;
|
|
281
579
|
}
|
|
580
|
+
if (unsubscribeAudit) {
|
|
581
|
+
unsubscribeAudit();
|
|
582
|
+
unsubscribeAudit = null;
|
|
583
|
+
}
|
|
282
584
|
if (syncEngine) {
|
|
283
585
|
await syncEngine.stop();
|
|
284
586
|
syncEngine = null;
|
|
285
587
|
}
|
|
588
|
+
if (syncStatusBridge) {
|
|
589
|
+
syncStatusBridge.destroy();
|
|
590
|
+
syncStatusBridge = null;
|
|
591
|
+
}
|
|
286
592
|
if (store) {
|
|
287
593
|
await store.close();
|
|
288
594
|
store = null;
|
|
289
595
|
}
|
|
290
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);
|
|
291
625
|
}
|
|
292
626
|
};
|
|
293
627
|
for (const collectionName of Object.keys(config.schema.collections)) {
|
|
@@ -304,37 +638,87 @@ function createApp(config) {
|
|
|
304
638
|
async function initializeAsync(config, emitter, mergeEngine) {
|
|
305
639
|
const adapterType = config.store?.adapter ?? detectAdapterType();
|
|
306
640
|
const dbName = config.store?.name ?? "kora-db";
|
|
307
|
-
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;
|
|
308
652
|
const store = new Store({
|
|
309
653
|
schema: config.schema,
|
|
310
654
|
adapter,
|
|
311
|
-
emitter
|
|
655
|
+
emitter,
|
|
656
|
+
dbName,
|
|
657
|
+
nodeId: authNodeId,
|
|
658
|
+
isolation: authNodeId ? "shared" : config.store?.isolation,
|
|
659
|
+
...config.sync ? { onQuerySubscribed: createSyncQuerySubscriptionHook(() => syncEngine) } : {}
|
|
312
660
|
});
|
|
313
661
|
await store.open();
|
|
314
|
-
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);
|
|
315
671
|
let unsubscribeSync = null;
|
|
316
672
|
if (config.sync) {
|
|
317
|
-
const transport =
|
|
318
|
-
const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter
|
|
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;
|
|
319
683
|
syncEngine = new SyncEngine({
|
|
320
684
|
transport,
|
|
321
685
|
store: mergeAwareStore,
|
|
322
686
|
config: {
|
|
323
687
|
url: config.sync.url,
|
|
324
688
|
transport: config.sync.transport,
|
|
325
|
-
auth:
|
|
689
|
+
auth: syncAuth,
|
|
326
690
|
batchSize: config.sync.batchSize,
|
|
327
|
-
schemaVersion: config.sync.schemaVersion ?? config.schema.version
|
|
691
|
+
schemaVersion: config.sync.schemaVersion ?? config.schema.version,
|
|
692
|
+
scopeMap,
|
|
693
|
+
encryption: config.sync.encryption,
|
|
694
|
+
strictHandshake: config.sync.strictHandshake,
|
|
695
|
+
operationTransforms: config.sync.operationTransforms
|
|
328
696
|
},
|
|
329
|
-
emitter
|
|
697
|
+
emitter,
|
|
698
|
+
queueStorage: new StoreQueueStorage(adapter),
|
|
699
|
+
syncState: new StoreSyncStatePersistence(store, scopeMap),
|
|
700
|
+
encryptor
|
|
330
701
|
});
|
|
702
|
+
recordConflict = () => syncEngine?.recordConflict();
|
|
331
703
|
unsubscribeSync = emitter.on("operation:created", (event) => {
|
|
332
704
|
if (syncEngine) {
|
|
333
705
|
syncEngine.pushOperation(event.operation);
|
|
334
706
|
}
|
|
335
707
|
});
|
|
336
708
|
}
|
|
337
|
-
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();
|
|
338
722
|
}
|
|
339
723
|
function createCollectionAccessor(collectionName, getStore) {
|
|
340
724
|
return {
|
|
@@ -399,15 +783,19 @@ function createPendingQueryBuilder(initialWhere) {
|
|
|
399
783
|
return this;
|
|
400
784
|
},
|
|
401
785
|
async exec() {
|
|
402
|
-
|
|
786
|
+
throw new AppNotReadyError(
|
|
787
|
+
"Cannot execute a query before app.ready. Await app.ready or use <KoraProvider app={app}>."
|
|
788
|
+
);
|
|
403
789
|
},
|
|
404
790
|
async count() {
|
|
405
|
-
|
|
791
|
+
throw new AppNotReadyError(
|
|
792
|
+
"Cannot count query results before app.ready. Await app.ready or use <KoraProvider app={app}>."
|
|
793
|
+
);
|
|
406
794
|
},
|
|
407
|
-
subscribe(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
+
);
|
|
411
799
|
},
|
|
412
800
|
getDescriptor() {
|
|
413
801
|
return { ...descriptor, where: { ...descriptor.where }, orderBy: [...descriptor.orderBy] };
|
|
@@ -417,25 +805,49 @@ function createPendingQueryBuilder(initialWhere) {
|
|
|
417
805
|
}
|
|
418
806
|
|
|
419
807
|
// src/index.ts
|
|
420
|
-
import {
|
|
808
|
+
import {
|
|
809
|
+
decodeAuditExport,
|
|
810
|
+
readAuditExportManifest,
|
|
811
|
+
verifyAuditExportChecksum
|
|
812
|
+
} from "@korajs/store";
|
|
813
|
+
import { defineSchema, migrate, t } from "@korajs/core";
|
|
421
814
|
import { HybridLogicalClock } from "@korajs/core";
|
|
422
815
|
import { generateUUIDv7 } from "@korajs/core";
|
|
423
816
|
import { createOperation } from "@korajs/core";
|
|
424
|
-
import { KoraError } from "@korajs/core";
|
|
425
|
-
import {
|
|
817
|
+
import { KoraError as KoraError3 } from "@korajs/core";
|
|
818
|
+
import { op } from "@korajs/core";
|
|
819
|
+
import { SequenceManager, Store as Store2 } from "@korajs/store";
|
|
820
|
+
import { TransactionContext } from "@korajs/store";
|
|
821
|
+
import {
|
|
822
|
+
exportBackup,
|
|
823
|
+
readBackupManifest,
|
|
824
|
+
restoreBackup,
|
|
825
|
+
verifyBackupChecksum
|
|
826
|
+
} from "@korajs/store";
|
|
426
827
|
import { MergeEngine as MergeEngine2 } from "@korajs/merge";
|
|
427
828
|
import { SyncEngine as SyncEngine2, WebSocketTransport as WebSocketTransport2 } from "@korajs/sync";
|
|
428
829
|
export {
|
|
429
830
|
HybridLogicalClock,
|
|
430
|
-
KoraError,
|
|
831
|
+
KoraError3 as KoraError,
|
|
431
832
|
MergeEngine2 as MergeEngine,
|
|
833
|
+
SequenceManager,
|
|
432
834
|
Store2 as Store,
|
|
433
835
|
SyncEngine2 as SyncEngine,
|
|
836
|
+
TransactionContext,
|
|
434
837
|
WebSocketTransport2 as WebSocketTransport,
|
|
435
838
|
createApp,
|
|
436
839
|
createOperation,
|
|
840
|
+
decodeAuditExport,
|
|
437
841
|
defineSchema,
|
|
842
|
+
exportBackup,
|
|
438
843
|
generateUUIDv7,
|
|
439
|
-
|
|
844
|
+
migrate,
|
|
845
|
+
op,
|
|
846
|
+
readAuditExportManifest,
|
|
847
|
+
readBackupManifest,
|
|
848
|
+
restoreBackup,
|
|
849
|
+
t,
|
|
850
|
+
verifyAuditExportChecksum,
|
|
851
|
+
verifyBackupChecksum
|
|
440
852
|
};
|
|
441
853
|
//# sourceMappingURL=index.js.map
|