korajs 0.4.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/chunk-WALHLVVF.js +683 -0
- package/dist/chunk-WALHLVVF.js.map +1 -0
- package/dist/index.cjs +1316 -347
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +92 -37
- package/dist/index.d.ts +92 -37
- package/dist/index.js +687 -376
- 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/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/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 +83 -7
package/dist/index.js
CHANGED
|
@@ -1,17 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApplyPipeline,
|
|
3
|
+
MergeAwareSyncStore,
|
|
4
|
+
StoreQueueStorage,
|
|
5
|
+
StoreSyncStatePersistence
|
|
6
|
+
} from "./chunk-WALHLVVF.js";
|
|
7
|
+
|
|
1
8
|
// src/create-app.ts
|
|
2
|
-
import { buildScopeMap } from "@korajs/core";
|
|
3
9
|
import { SimpleEventEmitter } from "@korajs/core/internal";
|
|
4
|
-
import { Instrumenter } from "@korajs/devtools";
|
|
5
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";
|
|
6
58
|
import { Store } from "@korajs/store";
|
|
7
|
-
import {
|
|
8
|
-
ConnectionMonitor,
|
|
9
|
-
ReconnectionManager,
|
|
10
|
-
SyncEngine,
|
|
11
|
-
WebSocketTransport
|
|
12
|
-
} from "@korajs/sync";
|
|
59
|
+
import { SyncEncryptor, SyncEngine } from "@korajs/sync";
|
|
13
60
|
|
|
14
61
|
// src/adapter-resolver.ts
|
|
62
|
+
function importOptionalPeer(specifier) {
|
|
63
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
64
|
+
return dynamicImport(specifier);
|
|
65
|
+
}
|
|
15
66
|
function detectAdapterType() {
|
|
16
67
|
if (typeof globalThis !== "undefined" && typeof globalThis.__TAURI_INTERNALS__ !== "undefined") {
|
|
17
68
|
return "tauri-sqlite";
|
|
@@ -27,13 +78,10 @@ function detectAdapterType() {
|
|
|
27
78
|
}
|
|
28
79
|
return "better-sqlite3";
|
|
29
80
|
}
|
|
30
|
-
async function createAdapter(type, dbName, workerUrl) {
|
|
81
|
+
async function createAdapter(type, dbName, workerUrl, emitter, workerResponseTimeoutMs, sharedWorkerUrl) {
|
|
31
82
|
switch (type) {
|
|
32
83
|
case "tauri-sqlite": {
|
|
33
|
-
const { TauriSqliteAdapter } = await
|
|
34
|
-
/* @vite-ignore */
|
|
35
|
-
"@korajs/tauri"
|
|
36
|
-
);
|
|
84
|
+
const { TauriSqliteAdapter } = await importOptionalPeer("@korajs/tauri");
|
|
37
85
|
return new TauriSqliteAdapter({ path: `${dbName}.db` });
|
|
38
86
|
}
|
|
39
87
|
case "better-sqlite3": {
|
|
@@ -45,11 +93,11 @@ async function createAdapter(type, dbName, workerUrl) {
|
|
|
45
93
|
}
|
|
46
94
|
case "sqlite-wasm": {
|
|
47
95
|
const { SqliteWasmAdapter } = await import("@korajs/store/sqlite-wasm");
|
|
48
|
-
return new SqliteWasmAdapter({ dbName, workerUrl });
|
|
96
|
+
return new SqliteWasmAdapter({ dbName, workerUrl, sharedWorkerUrl, workerResponseTimeoutMs });
|
|
49
97
|
}
|
|
50
98
|
case "indexeddb": {
|
|
51
99
|
const { IndexedDbAdapter } = await import("@korajs/store/indexeddb");
|
|
52
|
-
return new IndexedDbAdapter({ dbName, workerUrl });
|
|
100
|
+
return new IndexedDbAdapter({ dbName, workerUrl, emitter, workerResponseTimeoutMs });
|
|
53
101
|
}
|
|
54
102
|
default: {
|
|
55
103
|
const _exhaustive = type;
|
|
@@ -58,219 +106,267 @@ async function createAdapter(type, dbName, workerUrl) {
|
|
|
58
106
|
}
|
|
59
107
|
}
|
|
60
108
|
|
|
61
|
-
// src/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return this.store.getVersionVector();
|
|
73
|
-
}
|
|
74
|
-
getNodeId() {
|
|
75
|
-
return this.store.getNodeId();
|
|
76
|
-
}
|
|
77
|
-
async getOperationRange(nodeId, fromSeq, toSeq) {
|
|
78
|
-
return this.store.getOperationRange(nodeId, fromSeq, toSeq);
|
|
79
|
-
}
|
|
80
|
-
async applyRemoteOperation(op2) {
|
|
81
|
-
if (op2.type !== "update" || !op2.data || !op2.previousData) {
|
|
82
|
-
return this.store.applyRemoteOperation(op2);
|
|
83
|
-
}
|
|
84
|
-
const schema = this.store.getSchema();
|
|
85
|
-
const collectionDef = schema.collections[op2.collection];
|
|
86
|
-
if (!collectionDef) {
|
|
87
|
-
return this.store.applyRemoteOperation(op2);
|
|
88
|
-
}
|
|
89
|
-
const accessor = this.store.collection(op2.collection);
|
|
90
|
-
const currentRecord = await accessor.findById(op2.recordId);
|
|
91
|
-
if (!currentRecord) {
|
|
92
|
-
return this.store.applyRemoteOperation(op2);
|
|
93
|
-
}
|
|
94
|
-
let hasConflict = false;
|
|
95
|
-
for (const field of Object.keys(op2.data)) {
|
|
96
|
-
const expectedBase = op2.previousData[field];
|
|
97
|
-
const currentLocal = currentRecord[field];
|
|
98
|
-
if (!deepEqual(expectedBase, currentLocal)) {
|
|
99
|
-
hasConflict = true;
|
|
100
|
-
break;
|
|
109
|
+
// src/audit-bridge.ts
|
|
110
|
+
import { persistedAuditTraceFromEvent } from "@korajs/store";
|
|
111
|
+
var AUDIT_EVENT_TYPES = ["merge:completed", "merge:conflict", "constraint:violated"];
|
|
112
|
+
function isAuditEvent(event) {
|
|
113
|
+
return AUDIT_EVENT_TYPES.includes(event.type);
|
|
114
|
+
}
|
|
115
|
+
function wireAuditPersistence(store, emitter) {
|
|
116
|
+
const unsubscribers = AUDIT_EVENT_TYPES.map(
|
|
117
|
+
(eventType) => emitter.on(eventType, (event) => {
|
|
118
|
+
if (!isAuditEvent(event)) {
|
|
119
|
+
return;
|
|
101
120
|
}
|
|
121
|
+
const trace = persistedAuditTraceFromEvent(event);
|
|
122
|
+
void store.appendAuditTrace(trace).catch(() => {
|
|
123
|
+
});
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
return () => {
|
|
127
|
+
for (const unsub of unsubscribers) {
|
|
128
|
+
unsub();
|
|
102
129
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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 });
|
|
130
|
+
};
|
|
131
|
+
}
|
|
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
|
+
|
|
142
|
+
// src/sync-query-bridge.ts
|
|
143
|
+
function queryDescriptorToSyncSubset(descriptor) {
|
|
144
|
+
const where = {};
|
|
145
|
+
const skippedFields = [];
|
|
146
|
+
for (const [field, value] of Object.entries(descriptor.where)) {
|
|
147
|
+
if (value === null || value === void 0) {
|
|
148
|
+
where[field] = value;
|
|
149
|
+
continue;
|
|
128
150
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
151
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
152
|
+
where[field] = value;
|
|
153
|
+
continue;
|
|
132
154
|
}
|
|
133
|
-
|
|
134
|
-
...op2,
|
|
135
|
-
data: result.mergedData
|
|
136
|
-
};
|
|
137
|
-
return this.store.applyRemoteOperation(mergedOp);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
function buildLocalDiff(baseState, currentRecord, fields) {
|
|
141
|
-
const diff = {};
|
|
142
|
-
for (const field of fields) {
|
|
143
|
-
diff[field] = currentRecord[field];
|
|
155
|
+
skippedFields.push(field);
|
|
144
156
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (a === b) return true;
|
|
149
|
-
if (a === null || b === null) return false;
|
|
150
|
-
if (a === void 0 || b === void 0) return false;
|
|
151
|
-
if (typeof a !== typeof b) return false;
|
|
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]));
|
|
155
|
-
}
|
|
156
|
-
if (typeof a === "object" && typeof b === "object") {
|
|
157
|
-
const keysA = Object.keys(a);
|
|
158
|
-
const keysB = Object.keys(b);
|
|
159
|
-
if (keysA.length !== keysB.length) return false;
|
|
160
|
-
return keysA.every(
|
|
161
|
-
(key) => deepEqual(a[key], b[key])
|
|
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.`
|
|
162
160
|
);
|
|
163
161
|
}
|
|
164
|
-
|
|
162
|
+
if (Object.keys(where).length === 0) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
collection: descriptor.collection,
|
|
167
|
+
where
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function createSyncQuerySubscriptionHook(getSyncEngine) {
|
|
171
|
+
return (descriptor) => {
|
|
172
|
+
const subset = queryDescriptorToSyncSubset(descriptor);
|
|
173
|
+
if (!subset) {
|
|
174
|
+
return () => {
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const engine = getSyncEngine();
|
|
178
|
+
if (!engine) {
|
|
179
|
+
return () => {
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return engine.registerQuerySubset(subset);
|
|
183
|
+
};
|
|
165
184
|
}
|
|
166
185
|
|
|
167
|
-
// src/
|
|
168
|
-
function
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
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;
|
|
172
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);
|
|
173
220
|
let unsubscribeSync = null;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
let qualityInterval = null;
|
|
179
|
-
if (config.devtools) {
|
|
180
|
-
instrumenter = new Instrumenter(emitter, {
|
|
181
|
-
bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
|
|
221
|
+
if (config.sync) {
|
|
222
|
+
const transport = createSyncTransport(config.sync);
|
|
223
|
+
const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter, {
|
|
224
|
+
onMergeConflict: () => recordConflict?.()
|
|
182
225
|
});
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (config.sync.autoReconnect !== false) {
|
|
213
|
-
const engine = syncEngine;
|
|
214
|
-
emitter.on("sync:disconnected", () => {
|
|
215
|
-
if (intentionalDisconnect) return;
|
|
216
|
-
if (reconnectionManager?.isRunning()) return;
|
|
217
|
-
engine.setReconnecting(true);
|
|
218
|
-
reconnectionManager?.stop();
|
|
219
|
-
reconnectionManager?.start(async () => {
|
|
220
|
-
try {
|
|
221
|
-
await engine.start();
|
|
222
|
-
engine.setReconnecting(false);
|
|
223
|
-
return true;
|
|
224
|
-
} catch {
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
}).then(() => {
|
|
228
|
-
engine.setReconnecting(false);
|
|
229
|
-
});
|
|
230
|
-
});
|
|
226
|
+
let scopeMap = config.sync.scope ? buildScopeMap(config.schema, config.sync.scope) : void 0;
|
|
227
|
+
if (authBinding?.resolveScopeMap) {
|
|
228
|
+
scopeMap = await authBinding.resolveScopeMap() ?? scopeMap;
|
|
229
|
+
}
|
|
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);
|
|
231
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.");
|
|
274
|
+
}
|
|
275
|
+
return store;
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
async next(name, config) {
|
|
279
|
+
const store = await requireStore();
|
|
280
|
+
return store.getSequenceManager().next(name, config);
|
|
281
|
+
},
|
|
282
|
+
async current(name, config) {
|
|
283
|
+
const store = await requireStore();
|
|
284
|
+
return store.getSequenceManager().current(name, config);
|
|
285
|
+
},
|
|
286
|
+
async reset(name, config) {
|
|
287
|
+
const store = await requireStore();
|
|
288
|
+
return store.getSequenceManager().reset(name, config);
|
|
232
289
|
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
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 };
|
|
298
|
+
}
|
|
299
|
+
const instrumenter = new Instrumenter(emitter, {
|
|
300
|
+
bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
|
|
233
301
|
});
|
|
234
|
-
|
|
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(() => {
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return { instrumenter, destroyOverlay: () => destroyOverlay?.() };
|
|
310
|
+
}
|
|
311
|
+
|
|
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;
|
|
318
|
+
}
|
|
319
|
+
const offlineSyncStatus = () => OFFLINE_SYNC_STATUS;
|
|
320
|
+
const bridgeStatus = () => state.syncStatusBridge?.status ?? offlineSyncStatus();
|
|
321
|
+
return {
|
|
322
|
+
get status() {
|
|
323
|
+
return bridgeStatus();
|
|
324
|
+
},
|
|
325
|
+
subscribeStatus(listener) {
|
|
326
|
+
if (state.syncStatusBridge) {
|
|
327
|
+
return state.syncStatusBridge.subscribe(listener);
|
|
328
|
+
}
|
|
329
|
+
listener(offlineSyncStatus());
|
|
330
|
+
return () => {
|
|
331
|
+
};
|
|
332
|
+
},
|
|
235
333
|
async connect() {
|
|
236
334
|
await ready;
|
|
237
|
-
if (syncEngine) {
|
|
238
|
-
intentionalDisconnect = false;
|
|
239
|
-
reconnectionManager?.stop();
|
|
240
|
-
reconnectionManager?.reset();
|
|
241
|
-
await syncEngine.start();
|
|
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();
|
|
242
341
|
}
|
|
243
342
|
},
|
|
244
343
|
async disconnect() {
|
|
245
344
|
await ready;
|
|
246
|
-
if (syncEngine) {
|
|
247
|
-
intentionalDisconnect = true;
|
|
248
|
-
reconnectionManager?.stop();
|
|
249
|
-
await syncEngine.stop();
|
|
345
|
+
if (state.syncEngine) {
|
|
346
|
+
state.intentionalDisconnect = true;
|
|
347
|
+
state.reconnectionManager?.stop();
|
|
348
|
+
await state.syncEngine.stop();
|
|
349
|
+
state.syncStatusBridge?.refresh();
|
|
250
350
|
}
|
|
251
351
|
},
|
|
252
352
|
getStatus() {
|
|
253
|
-
if (syncEngine) {
|
|
254
|
-
return syncEngine.getStatus();
|
|
353
|
+
if (state.syncEngine) {
|
|
354
|
+
return state.syncEngine.getStatus();
|
|
255
355
|
}
|
|
256
|
-
return
|
|
257
|
-
status: "offline",
|
|
258
|
-
pendingOperations: 0,
|
|
259
|
-
lastSyncedAt: null,
|
|
260
|
-
lastSuccessfulPush: null,
|
|
261
|
-
lastSuccessfulPull: null,
|
|
262
|
-
conflicts: 0
|
|
263
|
-
};
|
|
356
|
+
return offlineSyncStatus();
|
|
264
357
|
},
|
|
265
358
|
async retryNow() {
|
|
266
359
|
await ready;
|
|
267
|
-
if (syncEngine) {
|
|
268
|
-
await syncEngine.retryNow();
|
|
360
|
+
if (state.syncEngine) {
|
|
361
|
+
await state.syncEngine.retryNow();
|
|
269
362
|
}
|
|
270
363
|
},
|
|
364
|
+
clearSchemaBlock() {
|
|
365
|
+
state.syncEngine?.clearSchemaBlock();
|
|
366
|
+
},
|
|
271
367
|
exportDiagnostics() {
|
|
272
|
-
if (syncEngine) {
|
|
273
|
-
return syncEngine.exportDiagnostics();
|
|
368
|
+
if (state.syncEngine) {
|
|
369
|
+
return state.syncEngine.exportDiagnostics();
|
|
274
370
|
}
|
|
275
371
|
return {
|
|
276
372
|
state: "disconnected",
|
|
@@ -295,62 +391,344 @@ function createApp(config) {
|
|
|
295
391
|
timestamp: Date.now()
|
|
296
392
|
};
|
|
297
393
|
}
|
|
298
|
-
}
|
|
299
|
-
|
|
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) => {
|
|
300
586
|
await ready;
|
|
587
|
+
const store = getStore();
|
|
301
588
|
if (!store) {
|
|
302
589
|
throw new Error("Store not initialized. Await app.ready before using transactions.");
|
|
303
590
|
}
|
|
304
|
-
const txContext = store.createTransaction();
|
|
305
|
-
if (mutationName !== void 0) {
|
|
306
|
-
txContext.setMutationName(mutationName);
|
|
307
|
-
}
|
|
308
591
|
const collectionNames = Object.keys(config.schema.collections);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
592
|
+
return store.transaction(async (tx) => {
|
|
593
|
+
if (mutationName !== void 0) {
|
|
594
|
+
tx.setMutationName(mutationName);
|
|
595
|
+
}
|
|
596
|
+
const proxy = {};
|
|
597
|
+
for (const name of collectionNames) {
|
|
598
|
+
Object.defineProperty(proxy, name, {
|
|
599
|
+
get() {
|
|
600
|
+
return tx.collection(name);
|
|
601
|
+
},
|
|
602
|
+
enumerable: true,
|
|
603
|
+
configurable: false
|
|
604
|
+
});
|
|
605
|
+
}
|
|
320
606
|
await fn(proxy);
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
});
|
|
618
|
+
}
|
|
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).'
|
|
325
642
|
}
|
|
326
|
-
|
|
327
|
-
} catch (error) {
|
|
328
|
-
txContext.rollback();
|
|
329
|
-
throw error;
|
|
330
|
-
}
|
|
643
|
+
);
|
|
331
644
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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");
|
|
347
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
|
|
348
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);
|
|
349
727
|
const app = {
|
|
350
728
|
ready,
|
|
351
729
|
events: emitter,
|
|
352
|
-
sync:
|
|
353
|
-
sequences,
|
|
730
|
+
sync: createSyncControl({ config, ready, state: syncState }),
|
|
731
|
+
sequences: createSequencesAccessor(ready, getStore),
|
|
354
732
|
getStore() {
|
|
355
733
|
if (!store) {
|
|
356
734
|
throw new Error("Store not initialized. Await app.ready before accessing the store.");
|
|
@@ -358,45 +736,75 @@ function createApp(config) {
|
|
|
358
736
|
return store;
|
|
359
737
|
},
|
|
360
738
|
getSyncEngine() {
|
|
361
|
-
return syncEngine;
|
|
739
|
+
return syncState.syncEngine;
|
|
740
|
+
},
|
|
741
|
+
getQueryStoreCache() {
|
|
742
|
+
return queryStoreCache;
|
|
362
743
|
},
|
|
363
|
-
|
|
744
|
+
transaction(fn) {
|
|
364
745
|
return executeTransaction(fn);
|
|
365
746
|
},
|
|
366
|
-
|
|
747
|
+
mutation(name, fn) {
|
|
367
748
|
return executeTransaction(fn, name);
|
|
368
749
|
},
|
|
369
750
|
async close() {
|
|
370
751
|
await ready;
|
|
371
|
-
intentionalDisconnect = true;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
reconnectionManager?.stop();
|
|
377
|
-
if (instrumenter) {
|
|
378
|
-
instrumenter.destroy();
|
|
379
|
-
instrumenter = null;
|
|
380
|
-
}
|
|
752
|
+
syncState.intentionalDisconnect = true;
|
|
753
|
+
teardownSyncLifecycle(syncState);
|
|
754
|
+
devtools.destroyOverlay?.();
|
|
755
|
+
devtools.instrumenter?.destroy();
|
|
381
756
|
if (unsubscribeSync) {
|
|
382
757
|
unsubscribeSync();
|
|
383
758
|
unsubscribeSync = null;
|
|
384
759
|
}
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
|
|
760
|
+
if (unsubscribeAudit) {
|
|
761
|
+
unsubscribeAudit();
|
|
762
|
+
unsubscribeAudit = null;
|
|
763
|
+
}
|
|
764
|
+
if (syncState.syncEngine) {
|
|
765
|
+
await syncState.syncEngine.stop();
|
|
766
|
+
syncState.syncEngine = null;
|
|
388
767
|
}
|
|
768
|
+
queryStoreCache.clear();
|
|
389
769
|
if (store) {
|
|
390
770
|
await store.close();
|
|
391
771
|
store = null;
|
|
392
772
|
}
|
|
393
773
|
emitter.clear();
|
|
774
|
+
},
|
|
775
|
+
async exportBackup(options) {
|
|
776
|
+
await ready;
|
|
777
|
+
if (!store) {
|
|
778
|
+
throw new Error("Store not initialized. Await app.ready before exporting backup.");
|
|
779
|
+
}
|
|
780
|
+
return store.exportBackup(options);
|
|
781
|
+
},
|
|
782
|
+
async importBackup(data, options) {
|
|
783
|
+
await ready;
|
|
784
|
+
if (!store) {
|
|
785
|
+
throw new Error("Store not initialized. Await app.ready before importing backup.");
|
|
786
|
+
}
|
|
787
|
+
return store.importBackup(data, options);
|
|
788
|
+
},
|
|
789
|
+
async replayTo(operationId) {
|
|
790
|
+
await ready;
|
|
791
|
+
if (!store) {
|
|
792
|
+
throw new Error("Store not initialized. Await app.ready before replaying operations.");
|
|
793
|
+
}
|
|
794
|
+
return store.replayTo(operationId);
|
|
795
|
+
},
|
|
796
|
+
async exportAudit(options) {
|
|
797
|
+
await ready;
|
|
798
|
+
if (!store) {
|
|
799
|
+
throw new Error("Store not initialized. Await app.ready before exporting audit data.");
|
|
800
|
+
}
|
|
801
|
+
return store.exportAudit(options);
|
|
394
802
|
}
|
|
395
803
|
};
|
|
396
804
|
for (const collectionName of Object.keys(config.schema.collections)) {
|
|
397
805
|
Object.defineProperty(app, collectionName, {
|
|
398
806
|
get() {
|
|
399
|
-
return createCollectionAccessor(collectionName,
|
|
807
|
+
return createCollectionAccessor(collectionName, getStore);
|
|
400
808
|
},
|
|
401
809
|
enumerable: true,
|
|
402
810
|
configurable: false
|
|
@@ -404,137 +812,33 @@ function createApp(config) {
|
|
|
404
812
|
}
|
|
405
813
|
return app;
|
|
406
814
|
}
|
|
407
|
-
async function initializeAsync(config, emitter, mergeEngine) {
|
|
408
|
-
const adapterType = config.store?.adapter ?? detectAdapterType();
|
|
409
|
-
const dbName = config.store?.name ?? "kora-db";
|
|
410
|
-
const adapter = await createAdapter(adapterType, dbName, config.store?.workerUrl);
|
|
411
|
-
const store = new Store({
|
|
412
|
-
schema: config.schema,
|
|
413
|
-
adapter,
|
|
414
|
-
emitter
|
|
415
|
-
});
|
|
416
|
-
await store.open();
|
|
417
|
-
let syncEngine = null;
|
|
418
|
-
let unsubscribeSync = null;
|
|
419
|
-
if (config.sync) {
|
|
420
|
-
const transport = new WebSocketTransport();
|
|
421
|
-
const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter);
|
|
422
|
-
const scopeMap = config.sync.scope ? buildScopeMap(config.schema, config.sync.scope) : void 0;
|
|
423
|
-
syncEngine = new SyncEngine({
|
|
424
|
-
transport,
|
|
425
|
-
store: mergeAwareStore,
|
|
426
|
-
config: {
|
|
427
|
-
url: config.sync.url,
|
|
428
|
-
transport: config.sync.transport,
|
|
429
|
-
auth: config.sync.auth,
|
|
430
|
-
batchSize: config.sync.batchSize,
|
|
431
|
-
schemaVersion: config.sync.schemaVersion ?? config.schema.version,
|
|
432
|
-
scopeMap
|
|
433
|
-
},
|
|
434
|
-
emitter
|
|
435
|
-
});
|
|
436
|
-
unsubscribeSync = emitter.on("operation:created", (event) => {
|
|
437
|
-
if (syncEngine) {
|
|
438
|
-
syncEngine.pushOperation(event.operation);
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
return { store, syncEngine, unsubscribeSync };
|
|
443
|
-
}
|
|
444
|
-
function createCollectionAccessor(collectionName, getStore) {
|
|
445
|
-
return {
|
|
446
|
-
async insert(data) {
|
|
447
|
-
const currentStore = getStore();
|
|
448
|
-
if (!currentStore) {
|
|
449
|
-
throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
|
|
450
|
-
}
|
|
451
|
-
return currentStore.collection(collectionName).insert(data);
|
|
452
|
-
},
|
|
453
|
-
async findById(id) {
|
|
454
|
-
const currentStore = getStore();
|
|
455
|
-
if (!currentStore) return null;
|
|
456
|
-
return currentStore.collection(collectionName).findById(id);
|
|
457
|
-
},
|
|
458
|
-
async update(id, data) {
|
|
459
|
-
const currentStore = getStore();
|
|
460
|
-
if (!currentStore) {
|
|
461
|
-
throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
|
|
462
|
-
}
|
|
463
|
-
return currentStore.collection(collectionName).update(id, data);
|
|
464
|
-
},
|
|
465
|
-
async delete(id) {
|
|
466
|
-
const currentStore = getStore();
|
|
467
|
-
if (!currentStore) {
|
|
468
|
-
throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
|
|
469
|
-
}
|
|
470
|
-
return currentStore.collection(collectionName).delete(id);
|
|
471
|
-
},
|
|
472
|
-
where(conditions) {
|
|
473
|
-
const currentStore = getStore();
|
|
474
|
-
if (!currentStore) {
|
|
475
|
-
return createPendingQueryBuilder(conditions);
|
|
476
|
-
}
|
|
477
|
-
return currentStore.collection(collectionName).where(conditions);
|
|
478
|
-
}
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
function createPendingQueryBuilder(initialWhere) {
|
|
482
|
-
const descriptor = {
|
|
483
|
-
collection: "__pending__",
|
|
484
|
-
where: { ...initialWhere },
|
|
485
|
-
orderBy: [],
|
|
486
|
-
limit: void 0,
|
|
487
|
-
offset: void 0
|
|
488
|
-
};
|
|
489
|
-
const builder = {
|
|
490
|
-
where(conditions) {
|
|
491
|
-
descriptor.where = { ...descriptor.where, ...conditions };
|
|
492
|
-
return this;
|
|
493
|
-
},
|
|
494
|
-
orderBy(field, direction = "asc") {
|
|
495
|
-
descriptor.orderBy.push({ field, direction });
|
|
496
|
-
return this;
|
|
497
|
-
},
|
|
498
|
-
limit(n) {
|
|
499
|
-
descriptor.limit = n;
|
|
500
|
-
return this;
|
|
501
|
-
},
|
|
502
|
-
offset(n) {
|
|
503
|
-
descriptor.offset = n;
|
|
504
|
-
return this;
|
|
505
|
-
},
|
|
506
|
-
async exec() {
|
|
507
|
-
return [];
|
|
508
|
-
},
|
|
509
|
-
async count() {
|
|
510
|
-
return 0;
|
|
511
|
-
},
|
|
512
|
-
subscribe(callback) {
|
|
513
|
-
void callback([]);
|
|
514
|
-
return () => {
|
|
515
|
-
};
|
|
516
|
-
},
|
|
517
|
-
getDescriptor() {
|
|
518
|
-
return { ...descriptor, where: { ...descriptor.where }, orderBy: [...descriptor.orderBy] };
|
|
519
|
-
}
|
|
520
|
-
};
|
|
521
|
-
return builder;
|
|
522
|
-
}
|
|
523
815
|
|
|
524
816
|
// src/index.ts
|
|
817
|
+
import {
|
|
818
|
+
decodeAuditExport,
|
|
819
|
+
readAuditExportManifest,
|
|
820
|
+
verifyAuditExportChecksum
|
|
821
|
+
} from "@korajs/store";
|
|
525
822
|
import { defineSchema, migrate, t } from "@korajs/core";
|
|
526
823
|
import { HybridLogicalClock } from "@korajs/core";
|
|
527
824
|
import { generateUUIDv7 } from "@korajs/core";
|
|
528
825
|
import { createOperation } from "@korajs/core";
|
|
529
|
-
import { KoraError } from "@korajs/core";
|
|
826
|
+
import { KoraError as KoraError2, AppNotReadyError as AppNotReadyError2 } from "@korajs/core";
|
|
530
827
|
import { op } from "@korajs/core";
|
|
531
828
|
import { SequenceManager, Store as Store2 } from "@korajs/store";
|
|
532
829
|
import { TransactionContext } from "@korajs/store";
|
|
830
|
+
import {
|
|
831
|
+
exportBackup,
|
|
832
|
+
readBackupManifest,
|
|
833
|
+
restoreBackup,
|
|
834
|
+
verifyBackupChecksum
|
|
835
|
+
} from "@korajs/store";
|
|
533
836
|
import { MergeEngine as MergeEngine2 } from "@korajs/merge";
|
|
534
837
|
import { SyncEngine as SyncEngine2, WebSocketTransport as WebSocketTransport2 } from "@korajs/sync";
|
|
535
838
|
export {
|
|
839
|
+
AppNotReadyError2 as AppNotReadyError,
|
|
536
840
|
HybridLogicalClock,
|
|
537
|
-
KoraError,
|
|
841
|
+
KoraError2 as KoraError,
|
|
538
842
|
MergeEngine2 as MergeEngine,
|
|
539
843
|
SequenceManager,
|
|
540
844
|
Store2 as Store,
|
|
@@ -543,10 +847,17 @@ export {
|
|
|
543
847
|
WebSocketTransport2 as WebSocketTransport,
|
|
544
848
|
createApp,
|
|
545
849
|
createOperation,
|
|
850
|
+
decodeAuditExport,
|
|
546
851
|
defineSchema,
|
|
852
|
+
exportBackup,
|
|
547
853
|
generateUUIDv7,
|
|
548
854
|
migrate,
|
|
549
855
|
op,
|
|
550
|
-
|
|
856
|
+
readAuditExportManifest,
|
|
857
|
+
readBackupManifest,
|
|
858
|
+
restoreBackup,
|
|
859
|
+
t,
|
|
860
|
+
verifyAuditExportChecksum,
|
|
861
|
+
verifyBackupChecksum
|
|
551
862
|
};
|
|
552
863
|
//# sourceMappingURL=index.js.map
|