korajs 0.5.0 → 0.6.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/README.md +20 -17
- package/dist/index.cjs +554 -539
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -52
- package/dist/index.d.ts +9 -52
- package/dist/index.js +538 -528
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +47 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +3 -0
- package/dist/react.d.ts +3 -0
- package/dist/react.js +24 -0
- package/dist/react.js.map +1 -0
- package/dist/svelte.cjs +71 -0
- package/dist/svelte.cjs.map +1 -0
- package/dist/svelte.d.cts +1 -0
- package/dist/svelte.d.ts +1 -0
- package/dist/svelte.js +48 -0
- package/dist/svelte.js.map +1 -0
- package/dist/vue.cjs +57 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.d.cts +1 -0
- package/dist/vue.d.ts +1 -0
- package/dist/vue.js +34 -0
- package/dist/vue.js.map +1 -0
- package/package.json +71 -7
package/dist/index.js
CHANGED
|
@@ -6,19 +6,57 @@ import {
|
|
|
6
6
|
} from "./chunk-WALHLVVF.js";
|
|
7
7
|
|
|
8
8
|
// src/create-app.ts
|
|
9
|
-
import { buildScopeMap } from "@korajs/core";
|
|
10
9
|
import { SimpleEventEmitter } from "@korajs/core/internal";
|
|
11
|
-
import { Instrumenter } from "@korajs/devtools";
|
|
12
10
|
import { MergeEngine } from "@korajs/merge";
|
|
11
|
+
import { QueryStoreCache } from "@korajs/store";
|
|
12
|
+
|
|
13
|
+
// src/collection-accessor.ts
|
|
14
|
+
import { AppNotReadyError } from "@korajs/core";
|
|
15
|
+
function createCollectionAccessor(collectionName, getStore) {
|
|
16
|
+
const notReady = (action) => new AppNotReadyError(
|
|
17
|
+
`Cannot ${action} on collection "${collectionName}" before app.ready. Await app.ready or use <KoraProvider app={app}>.`
|
|
18
|
+
);
|
|
19
|
+
return {
|
|
20
|
+
async insert(data) {
|
|
21
|
+
const currentStore = getStore();
|
|
22
|
+
if (!currentStore) {
|
|
23
|
+
throw notReady("insert into");
|
|
24
|
+
}
|
|
25
|
+
return currentStore.collection(collectionName).insert(data);
|
|
26
|
+
},
|
|
27
|
+
async findById(id) {
|
|
28
|
+
const currentStore = getStore();
|
|
29
|
+
if (!currentStore) return null;
|
|
30
|
+
return currentStore.collection(collectionName).findById(id);
|
|
31
|
+
},
|
|
32
|
+
async update(id, data) {
|
|
33
|
+
const currentStore = getStore();
|
|
34
|
+
if (!currentStore) {
|
|
35
|
+
throw notReady("update");
|
|
36
|
+
}
|
|
37
|
+
return currentStore.collection(collectionName).update(id, data);
|
|
38
|
+
},
|
|
39
|
+
async delete(id) {
|
|
40
|
+
const currentStore = getStore();
|
|
41
|
+
if (!currentStore) {
|
|
42
|
+
throw notReady("delete from");
|
|
43
|
+
}
|
|
44
|
+
return currentStore.collection(collectionName).delete(id);
|
|
45
|
+
},
|
|
46
|
+
where(conditions) {
|
|
47
|
+
const currentStore = getStore();
|
|
48
|
+
if (!currentStore) {
|
|
49
|
+
throw notReady("query");
|
|
50
|
+
}
|
|
51
|
+
return currentStore.collection(collectionName).where(conditions);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/initialize-app.ts
|
|
57
|
+
import { buildScopeMap } from "@korajs/core";
|
|
13
58
|
import { Store } from "@korajs/store";
|
|
14
|
-
import {
|
|
15
|
-
ConnectionMonitor,
|
|
16
|
-
HttpLongPollingTransport,
|
|
17
|
-
ReconnectionManager,
|
|
18
|
-
SyncEncryptor,
|
|
19
|
-
SyncEngine,
|
|
20
|
-
WebSocketTransport
|
|
21
|
-
} from "@korajs/sync";
|
|
59
|
+
import { SyncEncryptor, SyncEngine } from "@korajs/sync";
|
|
22
60
|
|
|
23
61
|
// src/adapter-resolver.ts
|
|
24
62
|
function importOptionalPeer(specifier) {
|
|
@@ -68,17 +106,6 @@ async function createAdapter(type, dbName, workerUrl, emitter, workerResponseTim
|
|
|
68
106
|
}
|
|
69
107
|
}
|
|
70
108
|
|
|
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";
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
109
|
// src/audit-bridge.ts
|
|
83
110
|
import { persistedAuditTraceFromEvent } from "@korajs/store";
|
|
84
111
|
var AUDIT_EVENT_TYPES = ["merge:completed", "merge:conflict", "constraint:violated"];
|
|
@@ -103,9 +130,19 @@ function wireAuditPersistence(store, emitter) {
|
|
|
103
130
|
};
|
|
104
131
|
}
|
|
105
132
|
|
|
133
|
+
// src/create-sync-transport.ts
|
|
134
|
+
import { HttpLongPollingTransport, WebSocketTransport } from "@korajs/sync";
|
|
135
|
+
function createSyncTransport(sync) {
|
|
136
|
+
if (sync.transport === "http") {
|
|
137
|
+
return new HttpLongPollingTransport();
|
|
138
|
+
}
|
|
139
|
+
return new WebSocketTransport();
|
|
140
|
+
}
|
|
141
|
+
|
|
106
142
|
// src/sync-query-bridge.ts
|
|
107
143
|
function queryDescriptorToSyncSubset(descriptor) {
|
|
108
144
|
const where = {};
|
|
145
|
+
const skippedFields = [];
|
|
109
146
|
for (const [field, value] of Object.entries(descriptor.where)) {
|
|
110
147
|
if (value === null || value === void 0) {
|
|
111
148
|
where[field] = value;
|
|
@@ -113,7 +150,14 @@ function queryDescriptorToSyncSubset(descriptor) {
|
|
|
113
150
|
}
|
|
114
151
|
if (typeof value !== "object" || Array.isArray(value)) {
|
|
115
152
|
where[field] = value;
|
|
153
|
+
continue;
|
|
116
154
|
}
|
|
155
|
+
skippedFields.push(field);
|
|
156
|
+
}
|
|
157
|
+
if (skippedFields.length > 0 && typeof console !== "undefined") {
|
|
158
|
+
console.warn(
|
|
159
|
+
`[Kora] Sync query subset omitted non-equality filters on ${descriptor.collection}: ${skippedFields.join(", ")}. Only plain equality WHERE clauses are registered for incremental sync.`
|
|
160
|
+
);
|
|
117
161
|
}
|
|
118
162
|
if (Object.keys(where).length === 0) {
|
|
119
163
|
return null;
|
|
@@ -139,297 +183,148 @@ function createSyncQuerySubscriptionHook(getSyncEngine) {
|
|
|
139
183
|
};
|
|
140
184
|
}
|
|
141
185
|
|
|
142
|
-
// src/
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
186
|
+
// src/initialize-app.ts
|
|
187
|
+
async function initializeApp(config, emitter, mergeEngine) {
|
|
188
|
+
const adapterType = config.store?.adapter ?? detectAdapterType();
|
|
189
|
+
const dbName = config.store?.name ?? "kora-db";
|
|
190
|
+
const adapter = await createAdapter(
|
|
191
|
+
adapterType,
|
|
192
|
+
dbName,
|
|
193
|
+
config.store?.workerUrl,
|
|
194
|
+
emitter,
|
|
195
|
+
config.store?.workerResponseTimeoutMs,
|
|
196
|
+
config.store?.sharedWorkerUrl
|
|
197
|
+
);
|
|
198
|
+
const authBinding = config.sync?.authClient ?? null;
|
|
199
|
+
const authNodeId = authBinding?.resolveNodeId ? await authBinding.resolveNodeId() : void 0;
|
|
200
|
+
let syncEngine = null;
|
|
201
|
+
const store = new Store({
|
|
202
|
+
schema: config.schema,
|
|
203
|
+
adapter,
|
|
204
|
+
emitter,
|
|
205
|
+
dbName,
|
|
206
|
+
nodeId: authNodeId,
|
|
207
|
+
isolation: authNodeId ? "shared" : config.store?.isolation,
|
|
208
|
+
...config.sync ? { onQuerySubscribed: createSyncQuerySubscriptionHook(() => syncEngine) } : {}
|
|
209
|
+
});
|
|
210
|
+
await store.open();
|
|
211
|
+
let recordConflict;
|
|
212
|
+
const applyPipeline = new ApplyPipeline({
|
|
213
|
+
store,
|
|
214
|
+
mergeEngine,
|
|
215
|
+
emitter,
|
|
216
|
+
onMergeConflict: () => recordConflict?.()
|
|
217
|
+
});
|
|
218
|
+
store.setLocalMutationHandler(applyPipeline);
|
|
219
|
+
const unsubscribeAudit = wireAuditPersistence(store, emitter);
|
|
220
|
+
let unsubscribeSync = null;
|
|
221
|
+
if (config.sync) {
|
|
222
|
+
const transport = createSyncTransport(config.sync);
|
|
223
|
+
const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter, {
|
|
224
|
+
onMergeConflict: () => recordConflict?.()
|
|
225
|
+
});
|
|
226
|
+
let scopeMap = config.sync.scope ? buildScopeMap(config.schema, config.sync.scope) : void 0;
|
|
227
|
+
if (authBinding?.resolveScopeMap) {
|
|
228
|
+
scopeMap = await authBinding.resolveScopeMap() ?? scopeMap;
|
|
173
229
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
230
|
+
const syncAuth = authBinding?.auth ?? config.sync.auth;
|
|
231
|
+
const encryptor = config.sync.encryption?.enabled === true ? await SyncEncryptor.create(config.sync.encryption) : void 0;
|
|
232
|
+
syncEngine = new SyncEngine({
|
|
233
|
+
transport,
|
|
234
|
+
store: mergeAwareStore,
|
|
235
|
+
config: {
|
|
236
|
+
url: config.sync.url,
|
|
237
|
+
transport: config.sync.transport,
|
|
238
|
+
auth: syncAuth,
|
|
239
|
+
batchSize: config.sync.batchSize,
|
|
240
|
+
schemaVersion: config.sync.schemaVersion ?? config.schema.version,
|
|
241
|
+
scopeMap,
|
|
242
|
+
encryption: config.sync.encryption,
|
|
243
|
+
strictHandshake: config.sync.strictHandshake,
|
|
244
|
+
operationTransforms: config.sync.operationTransforms
|
|
245
|
+
},
|
|
246
|
+
emitter,
|
|
247
|
+
queueStorage: new StoreQueueStorage(adapter),
|
|
248
|
+
syncState: new StoreSyncStatePersistence(store, scopeMap),
|
|
249
|
+
encryptor
|
|
250
|
+
});
|
|
251
|
+
recordConflict = () => syncEngine?.recordConflict();
|
|
252
|
+
unsubscribeSync = emitter.on("operation:created", (event) => {
|
|
253
|
+
if (syncEngine) {
|
|
254
|
+
syncEngine.pushOperation(event.operation);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
store,
|
|
260
|
+
syncEngine,
|
|
261
|
+
unsubscribeSync,
|
|
262
|
+
unsubscribeAudit,
|
|
263
|
+
authBinding
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/sequences-accessor.ts
|
|
268
|
+
function createSequencesAccessor(ready, getStore) {
|
|
269
|
+
const requireStore = async () => {
|
|
270
|
+
await ready;
|
|
271
|
+
const store = getStore();
|
|
272
|
+
if (!store) {
|
|
273
|
+
throw new Error("Store not initialized. Await app.ready before using sequences.");
|
|
177
274
|
}
|
|
275
|
+
return store;
|
|
178
276
|
};
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const bridge = {
|
|
184
|
-
get status() {
|
|
185
|
-
return currentStatus;
|
|
277
|
+
return {
|
|
278
|
+
async next(name, config) {
|
|
279
|
+
const store = await requireStore();
|
|
280
|
+
return store.getSequenceManager().next(name, config);
|
|
186
281
|
},
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return () => {
|
|
191
|
-
listeners.delete(listener);
|
|
192
|
-
};
|
|
282
|
+
async current(name, config) {
|
|
283
|
+
const store = await requireStore();
|
|
284
|
+
return store.getSequenceManager().current(name, config);
|
|
193
285
|
},
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
unsub();
|
|
198
|
-
}
|
|
199
|
-
unsubs.length = 0;
|
|
200
|
-
listeners.clear();
|
|
286
|
+
async reset(name, config) {
|
|
287
|
+
const store = await requireStore();
|
|
288
|
+
return store.getSequenceManager().reset(name, config);
|
|
201
289
|
}
|
|
202
290
|
};
|
|
203
|
-
refresh();
|
|
204
|
-
return bridge;
|
|
205
291
|
}
|
|
206
292
|
|
|
207
|
-
// src/
|
|
208
|
-
import {
|
|
209
|
-
function
|
|
210
|
-
if (!config.
|
|
211
|
-
|
|
212
|
-
fix: "Pass schema: defineSchema({ version: 1, collections: { ... } })"
|
|
213
|
-
});
|
|
214
|
-
}
|
|
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
|
-
);
|
|
293
|
+
// src/setup-devtools.ts
|
|
294
|
+
import { Instrumenter } from "@korajs/devtools";
|
|
295
|
+
function setupDevtools(config, emitter) {
|
|
296
|
+
if (!config.devtools) {
|
|
297
|
+
return { instrumenter: null, destroyOverlay: null };
|
|
240
298
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
299
|
+
const instrumenter = new Instrumenter(emitter, {
|
|
300
|
+
bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
|
|
301
|
+
});
|
|
302
|
+
let destroyOverlay = null;
|
|
303
|
+
if (typeof globalThis !== "undefined" && "window" in globalThis) {
|
|
304
|
+
void import("@korajs/devtools/overlay").then(({ mountKoraDevtoolsOverlay }) => {
|
|
305
|
+
destroyOverlay = mountKoraDevtoolsOverlay(instrumenter);
|
|
306
|
+
}).catch(() => {
|
|
246
307
|
});
|
|
247
308
|
}
|
|
248
|
-
|
|
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
|
-
}
|
|
266
|
-
);
|
|
267
|
-
}
|
|
309
|
+
return { instrumenter, destroyOverlay: () => destroyOverlay?.() };
|
|
268
310
|
}
|
|
269
311
|
|
|
270
|
-
// src/
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
}
|
|
296
|
-
let store = null;
|
|
297
|
-
let syncEngine = null;
|
|
298
|
-
let unsubscribeSync = null;
|
|
299
|
-
let unsubscribeAudit = null;
|
|
300
|
-
let reconnectionManager = null;
|
|
301
|
-
let connectionMonitor = null;
|
|
302
|
-
let instrumenter = null;
|
|
303
|
-
let intentionalDisconnect = false;
|
|
304
|
-
let qualityInterval = null;
|
|
305
|
-
let syncStatusBridge = null;
|
|
306
|
-
let destroyDevtoolsOverlay = null;
|
|
307
|
-
if (config.devtools) {
|
|
308
|
-
instrumenter = new Instrumenter(emitter, {
|
|
309
|
-
bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
|
|
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
|
-
}
|
|
312
|
+
// src/sync-control.ts
|
|
313
|
+
import { OFFLINE_SYNC_STATUS } from "@korajs/sync";
|
|
314
|
+
function createSyncControl(options) {
|
|
315
|
+
const { config, ready, state } = options;
|
|
316
|
+
if (!config.sync) {
|
|
317
|
+
return null;
|
|
319
318
|
}
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
}
|
|
354
|
-
if (config.sync && syncEngine) {
|
|
355
|
-
connectionMonitor = new ConnectionMonitor();
|
|
356
|
-
reconnectionManager = new ReconnectionManager({
|
|
357
|
-
initialDelay: config.sync.reconnectInterval,
|
|
358
|
-
maxDelay: config.sync.maxReconnectInterval
|
|
359
|
-
});
|
|
360
|
-
emitter.on("sync:sent", () => connectionMonitor?.recordActivity());
|
|
361
|
-
emitter.on("sync:received", () => connectionMonitor?.recordActivity());
|
|
362
|
-
emitter.on("sync:acknowledged", () => connectionMonitor?.recordActivity());
|
|
363
|
-
emitter.on("sync:connected", () => {
|
|
364
|
-
if (qualityInterval !== null) clearInterval(qualityInterval);
|
|
365
|
-
qualityInterval = setInterval(() => {
|
|
366
|
-
if (connectionMonitor) {
|
|
367
|
-
emitter.emit({ type: "connection:quality", quality: connectionMonitor.getQuality() });
|
|
368
|
-
}
|
|
369
|
-
}, 5e3);
|
|
370
|
-
});
|
|
371
|
-
emitter.on("sync:disconnected", () => {
|
|
372
|
-
connectionMonitor?.reset();
|
|
373
|
-
if (qualityInterval !== null) {
|
|
374
|
-
clearInterval(qualityInterval);
|
|
375
|
-
qualityInterval = null;
|
|
376
|
-
}
|
|
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
|
-
});
|
|
392
|
-
if (config.sync.autoReconnect !== false) {
|
|
393
|
-
const engine = syncEngine;
|
|
394
|
-
emitter.on("sync:disconnected", () => {
|
|
395
|
-
if (intentionalDisconnect || engine.isSchemaBlocked()) return;
|
|
396
|
-
if (reconnectionManager?.isRunning()) return;
|
|
397
|
-
engine.setReconnecting(true);
|
|
398
|
-
reconnectionManager?.stop();
|
|
399
|
-
reconnectionManager?.start(async () => {
|
|
400
|
-
try {
|
|
401
|
-
await engine.start();
|
|
402
|
-
engine.setReconnecting(false);
|
|
403
|
-
return true;
|
|
404
|
-
} catch {
|
|
405
|
-
return false;
|
|
406
|
-
}
|
|
407
|
-
}).then(() => {
|
|
408
|
-
engine.setReconnecting(false);
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
if (config.sync.autoConnect === true && syncEngine) {
|
|
413
|
-
void syncEngine.start().catch(() => {
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
});
|
|
418
|
-
const offlineSyncStatus = () => ({
|
|
419
|
-
status: "offline",
|
|
420
|
-
pendingOperations: 0,
|
|
421
|
-
lastSyncedAt: null,
|
|
422
|
-
lastSuccessfulPush: null,
|
|
423
|
-
lastSuccessfulPull: null,
|
|
424
|
-
conflicts: 0
|
|
425
|
-
});
|
|
426
|
-
const syncControl = config.sync ? {
|
|
319
|
+
const offlineSyncStatus = () => OFFLINE_SYNC_STATUS;
|
|
320
|
+
const bridgeStatus = () => state.syncStatusBridge?.status ?? offlineSyncStatus();
|
|
321
|
+
return {
|
|
427
322
|
get status() {
|
|
428
|
-
return
|
|
323
|
+
return bridgeStatus();
|
|
429
324
|
},
|
|
430
325
|
subscribeStatus(listener) {
|
|
431
|
-
if (syncStatusBridge) {
|
|
432
|
-
return syncStatusBridge.subscribe(listener);
|
|
326
|
+
if (state.syncStatusBridge) {
|
|
327
|
+
return state.syncStatusBridge.subscribe(listener);
|
|
433
328
|
}
|
|
434
329
|
listener(offlineSyncStatus());
|
|
435
330
|
return () => {
|
|
@@ -437,41 +332,41 @@ function createApp(config) {
|
|
|
437
332
|
},
|
|
438
333
|
async connect() {
|
|
439
334
|
await ready;
|
|
440
|
-
if (syncEngine) {
|
|
441
|
-
intentionalDisconnect = false;
|
|
442
|
-
reconnectionManager?.stop();
|
|
443
|
-
reconnectionManager?.reset();
|
|
444
|
-
await syncEngine.start();
|
|
445
|
-
syncStatusBridge?.refresh();
|
|
335
|
+
if (state.syncEngine) {
|
|
336
|
+
state.intentionalDisconnect = false;
|
|
337
|
+
state.reconnectionManager?.stop();
|
|
338
|
+
state.reconnectionManager?.reset();
|
|
339
|
+
await state.syncEngine.start();
|
|
340
|
+
state.syncStatusBridge?.refresh();
|
|
446
341
|
}
|
|
447
342
|
},
|
|
448
343
|
async disconnect() {
|
|
449
344
|
await ready;
|
|
450
|
-
if (syncEngine) {
|
|
451
|
-
intentionalDisconnect = true;
|
|
452
|
-
reconnectionManager?.stop();
|
|
453
|
-
await syncEngine.stop();
|
|
454
|
-
syncStatusBridge?.refresh();
|
|
345
|
+
if (state.syncEngine) {
|
|
346
|
+
state.intentionalDisconnect = true;
|
|
347
|
+
state.reconnectionManager?.stop();
|
|
348
|
+
await state.syncEngine.stop();
|
|
349
|
+
state.syncStatusBridge?.refresh();
|
|
455
350
|
}
|
|
456
351
|
},
|
|
457
352
|
getStatus() {
|
|
458
|
-
if (syncEngine) {
|
|
459
|
-
return syncEngine.getStatus();
|
|
353
|
+
if (state.syncEngine) {
|
|
354
|
+
return state.syncEngine.getStatus();
|
|
460
355
|
}
|
|
461
356
|
return offlineSyncStatus();
|
|
462
357
|
},
|
|
463
358
|
async retryNow() {
|
|
464
359
|
await ready;
|
|
465
|
-
if (syncEngine) {
|
|
466
|
-
await syncEngine.retryNow();
|
|
360
|
+
if (state.syncEngine) {
|
|
361
|
+
await state.syncEngine.retryNow();
|
|
467
362
|
}
|
|
468
363
|
},
|
|
469
364
|
clearSchemaBlock() {
|
|
470
|
-
syncEngine?.clearSchemaBlock();
|
|
365
|
+
state.syncEngine?.clearSchemaBlock();
|
|
471
366
|
},
|
|
472
367
|
exportDiagnostics() {
|
|
473
|
-
if (syncEngine) {
|
|
474
|
-
return syncEngine.exportDiagnostics();
|
|
368
|
+
if (state.syncEngine) {
|
|
369
|
+
return state.syncEngine.exportDiagnostics();
|
|
475
370
|
}
|
|
476
371
|
return {
|
|
477
372
|
state: "disconnected",
|
|
@@ -496,10 +391,201 @@ function createApp(config) {
|
|
|
496
391
|
timestamp: Date.now()
|
|
497
392
|
};
|
|
498
393
|
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/auth-sync-coordinator.ts
|
|
398
|
+
var AuthSyncCoordinator = class {
|
|
399
|
+
constructor(getEngine, authBinding) {
|
|
400
|
+
this.getEngine = getEngine;
|
|
401
|
+
this.authBinding = authBinding;
|
|
402
|
+
}
|
|
403
|
+
getEngine;
|
|
404
|
+
authBinding;
|
|
405
|
+
inFlight = null;
|
|
406
|
+
pending = false;
|
|
407
|
+
disposed = false;
|
|
408
|
+
scheduleReconnect() {
|
|
409
|
+
if (this.disposed) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (this.inFlight) {
|
|
413
|
+
this.pending = true;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
this.inFlight = this.run().finally(() => {
|
|
417
|
+
this.inFlight = null;
|
|
418
|
+
if (this.pending && !this.disposed) {
|
|
419
|
+
this.pending = false;
|
|
420
|
+
this.scheduleReconnect();
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
destroy() {
|
|
425
|
+
this.disposed = true;
|
|
426
|
+
this.pending = false;
|
|
427
|
+
}
|
|
428
|
+
async run() {
|
|
429
|
+
const engine = this.getEngine();
|
|
430
|
+
if (!engine) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const headers = await this.authBinding.auth();
|
|
434
|
+
if (!headers.token) {
|
|
435
|
+
await engine.stop();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (this.authBinding.resolveScopeMap) {
|
|
439
|
+
const nextScope = await this.authBinding.resolveScopeMap();
|
|
440
|
+
engine.updateScope(nextScope);
|
|
441
|
+
}
|
|
442
|
+
const status = engine.getStatus().status;
|
|
443
|
+
if (status !== "offline") {
|
|
444
|
+
await engine.stop();
|
|
445
|
+
}
|
|
446
|
+
await engine.start();
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// src/sync-status-bridge.ts
|
|
451
|
+
import { createSyncStatusController } from "@korajs/sync";
|
|
452
|
+
function createSyncStatusBridge(emitter, getSyncEngine) {
|
|
453
|
+
const controller = createSyncStatusController({
|
|
454
|
+
getSyncEngine,
|
|
455
|
+
subscribeSyncStatus: null,
|
|
456
|
+
events: emitter
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
get status() {
|
|
460
|
+
return controller.getSnapshot();
|
|
461
|
+
},
|
|
462
|
+
subscribe(listener) {
|
|
463
|
+
return controller.subscribe(() => {
|
|
464
|
+
listener(controller.getSnapshot());
|
|
465
|
+
});
|
|
466
|
+
},
|
|
467
|
+
refresh: () => controller.refresh(),
|
|
468
|
+
destroy: () => controller.destroy()
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/sync-lifecycle.ts
|
|
473
|
+
import {
|
|
474
|
+
ConnectionMonitor,
|
|
475
|
+
ReconnectionManager
|
|
476
|
+
} from "@korajs/sync";
|
|
477
|
+
function wireSyncLifecycleAfterReady(config, emitter, state, init) {
|
|
478
|
+
state.syncEngine = init.syncEngine;
|
|
479
|
+
if (!config.sync) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
state.syncStatusBridge = createSyncStatusBridge(emitter, () => state.syncEngine);
|
|
483
|
+
state.syncStatusBridge.refresh();
|
|
484
|
+
if (state.syncEngine && init.authBinding?.subscribe) {
|
|
485
|
+
state.authSyncCoordinator = new AuthSyncCoordinator(() => state.syncEngine, init.authBinding);
|
|
486
|
+
init.authBinding.subscribe(() => {
|
|
487
|
+
state.authSyncCoordinator?.scheduleReconnect();
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
if (!state.syncEngine) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const syncEngine = state.syncEngine;
|
|
494
|
+
state.connectionMonitor = new ConnectionMonitor();
|
|
495
|
+
state.reconnectionManager = new ReconnectionManager({
|
|
496
|
+
initialDelay: config.sync.reconnectInterval,
|
|
497
|
+
maxDelay: config.sync.maxReconnectInterval
|
|
498
|
+
});
|
|
499
|
+
emitter.on("sync:sent", () => state.connectionMonitor?.recordActivity());
|
|
500
|
+
emitter.on("sync:received", () => state.connectionMonitor?.recordActivity());
|
|
501
|
+
emitter.on("sync:acknowledged", () => state.connectionMonitor?.recordActivity());
|
|
502
|
+
emitter.on("sync:connected", () => {
|
|
503
|
+
if (state.qualityInterval !== null) {
|
|
504
|
+
clearInterval(state.qualityInterval);
|
|
505
|
+
}
|
|
506
|
+
state.qualityInterval = setInterval(() => {
|
|
507
|
+
if (state.connectionMonitor) {
|
|
508
|
+
emitter.emit({
|
|
509
|
+
type: "connection:quality",
|
|
510
|
+
quality: state.connectionMonitor.getQuality()
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}, 5e3);
|
|
514
|
+
});
|
|
515
|
+
emitter.on("sync:disconnected", () => {
|
|
516
|
+
state.connectionMonitor?.reset();
|
|
517
|
+
if (state.qualityInterval !== null) {
|
|
518
|
+
clearInterval(state.qualityInterval);
|
|
519
|
+
state.qualityInterval = null;
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
const browserGlobal = globalThis;
|
|
523
|
+
if (typeof browserGlobal.addEventListener === "function") {
|
|
524
|
+
const onOnline = () => {
|
|
525
|
+
if (state.intentionalDisconnect || config.sync?.autoReconnect === false) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
state.reconnectionManager?.wake();
|
|
529
|
+
state.reconnectionManager?.reset();
|
|
530
|
+
void syncEngine.retryNow();
|
|
531
|
+
};
|
|
532
|
+
browserGlobal.addEventListener("online", onOnline);
|
|
533
|
+
state.removeOnlineListener = () => {
|
|
534
|
+
browserGlobal.removeEventListener?.("online", onOnline);
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
emitter.on("sync:schema-mismatch", () => {
|
|
538
|
+
state.reconnectionManager?.stop();
|
|
539
|
+
state.intentionalDisconnect = true;
|
|
540
|
+
});
|
|
541
|
+
if (config.sync.autoReconnect !== false) {
|
|
542
|
+
emitter.on("sync:disconnected", () => {
|
|
543
|
+
if (state.intentionalDisconnect || syncEngine.isSchemaBlocked()) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (state.reconnectionManager?.isRunning()) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
syncEngine.setReconnecting(true);
|
|
550
|
+
state.reconnectionManager?.stop();
|
|
551
|
+
state.reconnectionManager?.start(async () => {
|
|
552
|
+
try {
|
|
553
|
+
await syncEngine.start();
|
|
554
|
+
syncEngine.setReconnecting(false);
|
|
555
|
+
return true;
|
|
556
|
+
} catch {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
}).then(() => {
|
|
560
|
+
syncEngine.setReconnecting(false);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
if (config.sync.autoConnect === true) {
|
|
565
|
+
void syncEngine.start().catch(() => {
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function teardownSyncLifecycle(state) {
|
|
570
|
+
if (state.qualityInterval !== null) {
|
|
571
|
+
clearInterval(state.qualityInterval);
|
|
572
|
+
state.qualityInterval = null;
|
|
573
|
+
}
|
|
574
|
+
state.reconnectionManager?.stop();
|
|
575
|
+
state.syncStatusBridge?.destroy();
|
|
576
|
+
state.syncStatusBridge = null;
|
|
577
|
+
state.authSyncCoordinator?.destroy();
|
|
578
|
+
state.authSyncCoordinator = null;
|
|
579
|
+
state.removeOnlineListener?.();
|
|
580
|
+
state.removeOnlineListener = null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/transaction-executor.ts
|
|
584
|
+
function createTransactionExecutor(config, ready, getStore) {
|
|
585
|
+
return async (fn, mutationName) => {
|
|
586
|
+
await ready;
|
|
587
|
+
const store = getStore();
|
|
588
|
+
if (!store) {
|
|
503
589
|
throw new Error("Store not initialized. Await app.ready before using transactions.");
|
|
504
590
|
}
|
|
505
591
|
const collectionNames = Object.keys(config.schema.collections);
|
|
@@ -519,29 +605,130 @@ function createApp(config) {
|
|
|
519
605
|
}
|
|
520
606
|
await fn(proxy);
|
|
521
607
|
});
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/validate-config.ts
|
|
612
|
+
import { KoraError, SchemaValidationError } from "@korajs/core";
|
|
613
|
+
function validateCreateAppConfig(config) {
|
|
614
|
+
if (!config.schema) {
|
|
615
|
+
throw new SchemaValidationError("createApp requires a schema.", {
|
|
616
|
+
fix: "Pass schema: defineSchema({ version: 1, collections: { ... } })"
|
|
617
|
+
});
|
|
522
618
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
619
|
+
if (config.schema.version < 1) {
|
|
620
|
+
throw new SchemaValidationError("Schema version must be at least 1.", {
|
|
621
|
+
version: config.schema.version
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
const collectionNames = Object.keys(config.schema.collections);
|
|
625
|
+
if (collectionNames.length === 0) {
|
|
626
|
+
throw new SchemaValidationError("Schema must define at least one collection.", {
|
|
627
|
+
fix: "Add entries under collections in defineSchema()."
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
if (config.sync) {
|
|
631
|
+
validateSyncUrl(config.sync.url, config.sync.transport ?? "websocket");
|
|
632
|
+
}
|
|
633
|
+
const adapter = config.store?.adapter ?? detectAdapterType();
|
|
634
|
+
const isBrowser = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
|
|
635
|
+
if (isBrowser && (adapter === "sqlite-wasm" || adapter === "indexeddb") && !config.store?.workerUrl) {
|
|
636
|
+
throw new KoraError(
|
|
637
|
+
"Browser storage requires store.workerUrl pointing to the SQLite WASM worker script.",
|
|
638
|
+
"MISSING_WORKER_URL",
|
|
639
|
+
{
|
|
640
|
+
adapter,
|
|
641
|
+
fix: 'Add store: { workerUrl: "/sqlite-wasm-worker.js" } (see create-kora-app templates).'
|
|
642
|
+
}
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function validateSyncUrl(url, transport) {
|
|
647
|
+
if (!url || url.trim().length === 0) {
|
|
648
|
+
throw new KoraError("sync.url is required when sync is configured.", "INVALID_SYNC_URL", {
|
|
649
|
+
fix: 'Pass sync: { url: "wss://your-server/kora" }.'
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
const parsed = new URL(url);
|
|
654
|
+
if (transport === "http") {
|
|
655
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
656
|
+
throw new Error("bad protocol");
|
|
657
|
+
}
|
|
658
|
+
} else if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
|
659
|
+
throw new Error("bad protocol");
|
|
538
660
|
}
|
|
661
|
+
} catch {
|
|
662
|
+
throw new KoraError(
|
|
663
|
+
`Invalid sync URL "${url}" for transport "${transport}".`,
|
|
664
|
+
"INVALID_SYNC_URL",
|
|
665
|
+
{
|
|
666
|
+
url,
|
|
667
|
+
transport,
|
|
668
|
+
fix: transport === "http" ? "Use an absolute http:// or https:// URL." : "Use an absolute ws:// or wss:// URL."
|
|
669
|
+
}
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/wire-sync-event-forwarding.ts
|
|
675
|
+
var SYNC_EVENT_TYPES = [
|
|
676
|
+
"sync:connected",
|
|
677
|
+
"sync:disconnected",
|
|
678
|
+
"sync:schema-mismatch",
|
|
679
|
+
"sync:auth-failed",
|
|
680
|
+
"sync:sent",
|
|
681
|
+
"sync:received",
|
|
682
|
+
"sync:acknowledged",
|
|
683
|
+
"sync:apply-failed",
|
|
684
|
+
"sync:diagnostics",
|
|
685
|
+
"sync:bandwidth",
|
|
686
|
+
"sync:initial-sync-progress"
|
|
687
|
+
];
|
|
688
|
+
function wireSyncEventForwarding(emitter, handler) {
|
|
689
|
+
for (const type of SYNC_EVENT_TYPES) {
|
|
690
|
+
emitter.on(type, (event) => {
|
|
691
|
+
handler(event);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// src/create-app.ts
|
|
697
|
+
function createApp(config) {
|
|
698
|
+
validateCreateAppConfig(config);
|
|
699
|
+
const emitter = new SimpleEventEmitter();
|
|
700
|
+
const mergeEngine = new MergeEngine();
|
|
701
|
+
if (config.onSyncEvent) {
|
|
702
|
+
wireSyncEventForwarding(emitter, config.onSyncEvent);
|
|
703
|
+
}
|
|
704
|
+
let store = null;
|
|
705
|
+
let unsubscribeSync = null;
|
|
706
|
+
let unsubscribeAudit = null;
|
|
707
|
+
const syncState = {
|
|
708
|
+
syncEngine: null,
|
|
709
|
+
syncStatusBridge: null,
|
|
710
|
+
authSyncCoordinator: null,
|
|
711
|
+
reconnectionManager: null,
|
|
712
|
+
connectionMonitor: null,
|
|
713
|
+
qualityInterval: null,
|
|
714
|
+
intentionalDisconnect: false,
|
|
715
|
+
removeOnlineListener: null
|
|
539
716
|
};
|
|
717
|
+
const devtools = setupDevtools(config, emitter);
|
|
718
|
+
const queryStoreCache = new QueryStoreCache(config.store?.name ?? "kora-db");
|
|
719
|
+
const ready = initializeApp(config, emitter, mergeEngine).then((init) => {
|
|
720
|
+
store = init.store;
|
|
721
|
+
unsubscribeSync = init.unsubscribeSync;
|
|
722
|
+
unsubscribeAudit = init.unsubscribeAudit;
|
|
723
|
+
wireSyncLifecycleAfterReady(config, emitter, syncState, init);
|
|
724
|
+
});
|
|
725
|
+
const getStore = () => store;
|
|
726
|
+
const executeTransaction = createTransactionExecutor(config, ready, getStore);
|
|
540
727
|
const app = {
|
|
541
728
|
ready,
|
|
542
729
|
events: emitter,
|
|
543
|
-
sync:
|
|
544
|
-
sequences,
|
|
730
|
+
sync: createSyncControl({ config, ready, state: syncState }),
|
|
731
|
+
sequences: createSequencesAccessor(ready, getStore),
|
|
545
732
|
getStore() {
|
|
546
733
|
if (!store) {
|
|
547
734
|
throw new Error("Store not initialized. Await app.ready before accessing the store.");
|
|
@@ -549,30 +736,23 @@ function createApp(config) {
|
|
|
549
736
|
return store;
|
|
550
737
|
},
|
|
551
738
|
getSyncEngine() {
|
|
552
|
-
return syncEngine;
|
|
739
|
+
return syncState.syncEngine;
|
|
740
|
+
},
|
|
741
|
+
getQueryStoreCache() {
|
|
742
|
+
return queryStoreCache;
|
|
553
743
|
},
|
|
554
|
-
|
|
744
|
+
transaction(fn) {
|
|
555
745
|
return executeTransaction(fn);
|
|
556
746
|
},
|
|
557
|
-
|
|
747
|
+
mutation(name, fn) {
|
|
558
748
|
return executeTransaction(fn, name);
|
|
559
749
|
},
|
|
560
750
|
async close() {
|
|
561
751
|
await ready;
|
|
562
|
-
intentionalDisconnect = true;
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
reconnectionManager?.stop();
|
|
568
|
-
if (destroyDevtoolsOverlay) {
|
|
569
|
-
destroyDevtoolsOverlay();
|
|
570
|
-
destroyDevtoolsOverlay = null;
|
|
571
|
-
}
|
|
572
|
-
if (instrumenter) {
|
|
573
|
-
instrumenter.destroy();
|
|
574
|
-
instrumenter = null;
|
|
575
|
-
}
|
|
752
|
+
syncState.intentionalDisconnect = true;
|
|
753
|
+
teardownSyncLifecycle(syncState);
|
|
754
|
+
devtools.destroyOverlay?.();
|
|
755
|
+
devtools.instrumenter?.destroy();
|
|
576
756
|
if (unsubscribeSync) {
|
|
577
757
|
unsubscribeSync();
|
|
578
758
|
unsubscribeSync = null;
|
|
@@ -581,14 +761,11 @@ function createApp(config) {
|
|
|
581
761
|
unsubscribeAudit();
|
|
582
762
|
unsubscribeAudit = null;
|
|
583
763
|
}
|
|
584
|
-
if (syncEngine) {
|
|
585
|
-
await syncEngine.stop();
|
|
586
|
-
syncEngine = null;
|
|
587
|
-
}
|
|
588
|
-
if (syncStatusBridge) {
|
|
589
|
-
syncStatusBridge.destroy();
|
|
590
|
-
syncStatusBridge = null;
|
|
764
|
+
if (syncState.syncEngine) {
|
|
765
|
+
await syncState.syncEngine.stop();
|
|
766
|
+
syncState.syncEngine = null;
|
|
591
767
|
}
|
|
768
|
+
queryStoreCache.clear();
|
|
592
769
|
if (store) {
|
|
593
770
|
await store.close();
|
|
594
771
|
store = null;
|
|
@@ -627,7 +804,7 @@ function createApp(config) {
|
|
|
627
804
|
for (const collectionName of Object.keys(config.schema.collections)) {
|
|
628
805
|
Object.defineProperty(app, collectionName, {
|
|
629
806
|
get() {
|
|
630
|
-
return createCollectionAccessor(collectionName,
|
|
807
|
+
return createCollectionAccessor(collectionName, getStore);
|
|
631
808
|
},
|
|
632
809
|
enumerable: true,
|
|
633
810
|
configurable: false
|
|
@@ -635,174 +812,6 @@ function createApp(config) {
|
|
|
635
812
|
}
|
|
636
813
|
return app;
|
|
637
814
|
}
|
|
638
|
-
async function initializeAsync(config, emitter, mergeEngine) {
|
|
639
|
-
const adapterType = config.store?.adapter ?? detectAdapterType();
|
|
640
|
-
const dbName = config.store?.name ?? "kora-db";
|
|
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;
|
|
652
|
-
const store = new Store({
|
|
653
|
-
schema: config.schema,
|
|
654
|
-
adapter,
|
|
655
|
-
emitter,
|
|
656
|
-
dbName,
|
|
657
|
-
nodeId: authNodeId,
|
|
658
|
-
isolation: authNodeId ? "shared" : config.store?.isolation,
|
|
659
|
-
...config.sync ? { onQuerySubscribed: createSyncQuerySubscriptionHook(() => syncEngine) } : {}
|
|
660
|
-
});
|
|
661
|
-
await store.open();
|
|
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);
|
|
671
|
-
let unsubscribeSync = null;
|
|
672
|
-
if (config.sync) {
|
|
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;
|
|
683
|
-
syncEngine = new SyncEngine({
|
|
684
|
-
transport,
|
|
685
|
-
store: mergeAwareStore,
|
|
686
|
-
config: {
|
|
687
|
-
url: config.sync.url,
|
|
688
|
-
transport: config.sync.transport,
|
|
689
|
-
auth: syncAuth,
|
|
690
|
-
batchSize: config.sync.batchSize,
|
|
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
|
|
696
|
-
},
|
|
697
|
-
emitter,
|
|
698
|
-
queueStorage: new StoreQueueStorage(adapter),
|
|
699
|
-
syncState: new StoreSyncStatePersistence(store, scopeMap),
|
|
700
|
-
encryptor
|
|
701
|
-
});
|
|
702
|
-
recordConflict = () => syncEngine?.recordConflict();
|
|
703
|
-
unsubscribeSync = emitter.on("operation:created", (event) => {
|
|
704
|
-
if (syncEngine) {
|
|
705
|
-
syncEngine.pushOperation(event.operation);
|
|
706
|
-
}
|
|
707
|
-
});
|
|
708
|
-
}
|
|
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();
|
|
722
|
-
}
|
|
723
|
-
function createCollectionAccessor(collectionName, getStore) {
|
|
724
|
-
return {
|
|
725
|
-
async insert(data) {
|
|
726
|
-
const currentStore = getStore();
|
|
727
|
-
if (!currentStore) {
|
|
728
|
-
throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
|
|
729
|
-
}
|
|
730
|
-
return currentStore.collection(collectionName).insert(data);
|
|
731
|
-
},
|
|
732
|
-
async findById(id) {
|
|
733
|
-
const currentStore = getStore();
|
|
734
|
-
if (!currentStore) return null;
|
|
735
|
-
return currentStore.collection(collectionName).findById(id);
|
|
736
|
-
},
|
|
737
|
-
async update(id, data) {
|
|
738
|
-
const currentStore = getStore();
|
|
739
|
-
if (!currentStore) {
|
|
740
|
-
throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
|
|
741
|
-
}
|
|
742
|
-
return currentStore.collection(collectionName).update(id, data);
|
|
743
|
-
},
|
|
744
|
-
async delete(id) {
|
|
745
|
-
const currentStore = getStore();
|
|
746
|
-
if (!currentStore) {
|
|
747
|
-
throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
|
|
748
|
-
}
|
|
749
|
-
return currentStore.collection(collectionName).delete(id);
|
|
750
|
-
},
|
|
751
|
-
where(conditions) {
|
|
752
|
-
const currentStore = getStore();
|
|
753
|
-
if (!currentStore) {
|
|
754
|
-
return createPendingQueryBuilder(conditions);
|
|
755
|
-
}
|
|
756
|
-
return currentStore.collection(collectionName).where(conditions);
|
|
757
|
-
}
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
|
-
function createPendingQueryBuilder(initialWhere) {
|
|
761
|
-
const descriptor = {
|
|
762
|
-
collection: "__pending__",
|
|
763
|
-
where: { ...initialWhere },
|
|
764
|
-
orderBy: [],
|
|
765
|
-
limit: void 0,
|
|
766
|
-
offset: void 0
|
|
767
|
-
};
|
|
768
|
-
const builder = {
|
|
769
|
-
where(conditions) {
|
|
770
|
-
descriptor.where = { ...descriptor.where, ...conditions };
|
|
771
|
-
return this;
|
|
772
|
-
},
|
|
773
|
-
orderBy(field, direction = "asc") {
|
|
774
|
-
descriptor.orderBy.push({ field, direction });
|
|
775
|
-
return this;
|
|
776
|
-
},
|
|
777
|
-
limit(n) {
|
|
778
|
-
descriptor.limit = n;
|
|
779
|
-
return this;
|
|
780
|
-
},
|
|
781
|
-
offset(n) {
|
|
782
|
-
descriptor.offset = n;
|
|
783
|
-
return this;
|
|
784
|
-
},
|
|
785
|
-
async exec() {
|
|
786
|
-
throw new AppNotReadyError(
|
|
787
|
-
"Cannot execute a query before app.ready. Await app.ready or use <KoraProvider app={app}>."
|
|
788
|
-
);
|
|
789
|
-
},
|
|
790
|
-
async count() {
|
|
791
|
-
throw new AppNotReadyError(
|
|
792
|
-
"Cannot count query results before app.ready. Await app.ready or use <KoraProvider app={app}>."
|
|
793
|
-
);
|
|
794
|
-
},
|
|
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
|
-
);
|
|
799
|
-
},
|
|
800
|
-
getDescriptor() {
|
|
801
|
-
return { ...descriptor, where: { ...descriptor.where }, orderBy: [...descriptor.orderBy] };
|
|
802
|
-
}
|
|
803
|
-
};
|
|
804
|
-
return builder;
|
|
805
|
-
}
|
|
806
815
|
|
|
807
816
|
// src/index.ts
|
|
808
817
|
import {
|
|
@@ -814,7 +823,7 @@ import { defineSchema, migrate, t } from "@korajs/core";
|
|
|
814
823
|
import { HybridLogicalClock } from "@korajs/core";
|
|
815
824
|
import { generateUUIDv7 } from "@korajs/core";
|
|
816
825
|
import { createOperation } from "@korajs/core";
|
|
817
|
-
import { KoraError as
|
|
826
|
+
import { KoraError as KoraError2, AppNotReadyError as AppNotReadyError2 } from "@korajs/core";
|
|
818
827
|
import { op } from "@korajs/core";
|
|
819
828
|
import { SequenceManager, Store as Store2 } from "@korajs/store";
|
|
820
829
|
import { TransactionContext } from "@korajs/store";
|
|
@@ -827,8 +836,9 @@ import {
|
|
|
827
836
|
import { MergeEngine as MergeEngine2 } from "@korajs/merge";
|
|
828
837
|
import { SyncEngine as SyncEngine2, WebSocketTransport as WebSocketTransport2 } from "@korajs/sync";
|
|
829
838
|
export {
|
|
839
|
+
AppNotReadyError2 as AppNotReadyError,
|
|
830
840
|
HybridLogicalClock,
|
|
831
|
-
|
|
841
|
+
KoraError2 as KoraError,
|
|
832
842
|
MergeEngine2 as MergeEngine,
|
|
833
843
|
SequenceManager,
|
|
834
844
|
Store2 as Store,
|