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.cjs
CHANGED
|
@@ -30,33 +30,90 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
AppNotReadyError: () => import_core10.AppNotReadyError,
|
|
34
|
+
HybridLogicalClock: () => import_core7.HybridLogicalClock,
|
|
35
|
+
KoraError: () => import_core10.KoraError,
|
|
36
|
+
MergeEngine: () => import_merge3.MergeEngine,
|
|
37
|
+
SequenceManager: () => import_store7.SequenceManager,
|
|
38
|
+
Store: () => import_store7.Store,
|
|
39
|
+
SyncEngine: () => import_sync7.SyncEngine,
|
|
40
|
+
TransactionContext: () => import_store8.TransactionContext,
|
|
41
|
+
WebSocketTransport: () => import_sync7.WebSocketTransport,
|
|
41
42
|
createApp: () => createApp,
|
|
42
|
-
createOperation: () =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
createOperation: () => import_core9.createOperation,
|
|
44
|
+
decodeAuditExport: () => import_store6.decodeAuditExport,
|
|
45
|
+
defineSchema: () => import_core6.defineSchema,
|
|
46
|
+
exportBackup: () => import_store9.exportBackup,
|
|
47
|
+
generateUUIDv7: () => import_core8.generateUUIDv7,
|
|
48
|
+
migrate: () => import_core6.migrate,
|
|
49
|
+
op: () => import_core11.op,
|
|
50
|
+
readAuditExportManifest: () => import_store6.readAuditExportManifest,
|
|
51
|
+
readBackupManifest: () => import_store9.readBackupManifest,
|
|
52
|
+
restoreBackup: () => import_store9.restoreBackup,
|
|
53
|
+
t: () => import_core6.t,
|
|
54
|
+
verifyAuditExportChecksum: () => import_store6.verifyAuditExportChecksum,
|
|
55
|
+
verifyBackupChecksum: () => import_store9.verifyBackupChecksum
|
|
48
56
|
});
|
|
49
57
|
module.exports = __toCommonJS(index_exports);
|
|
50
58
|
|
|
51
59
|
// src/create-app.ts
|
|
60
|
+
var import_internal5 = require("@korajs/core/internal");
|
|
61
|
+
var import_merge2 = require("@korajs/merge");
|
|
62
|
+
var import_store5 = require("@korajs/store");
|
|
63
|
+
|
|
64
|
+
// src/collection-accessor.ts
|
|
52
65
|
var import_core = require("@korajs/core");
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
function createCollectionAccessor(collectionName, getStore) {
|
|
67
|
+
const notReady = (action) => new import_core.AppNotReadyError(
|
|
68
|
+
`Cannot ${action} on collection "${collectionName}" before app.ready. Await app.ready or use <KoraProvider app={app}>.`
|
|
69
|
+
);
|
|
70
|
+
return {
|
|
71
|
+
async insert(data) {
|
|
72
|
+
const currentStore = getStore();
|
|
73
|
+
if (!currentStore) {
|
|
74
|
+
throw notReady("insert into");
|
|
75
|
+
}
|
|
76
|
+
return currentStore.collection(collectionName).insert(data);
|
|
77
|
+
},
|
|
78
|
+
async findById(id) {
|
|
79
|
+
const currentStore = getStore();
|
|
80
|
+
if (!currentStore) return null;
|
|
81
|
+
return currentStore.collection(collectionName).findById(id);
|
|
82
|
+
},
|
|
83
|
+
async update(id, data) {
|
|
84
|
+
const currentStore = getStore();
|
|
85
|
+
if (!currentStore) {
|
|
86
|
+
throw notReady("update");
|
|
87
|
+
}
|
|
88
|
+
return currentStore.collection(collectionName).update(id, data);
|
|
89
|
+
},
|
|
90
|
+
async delete(id) {
|
|
91
|
+
const currentStore = getStore();
|
|
92
|
+
if (!currentStore) {
|
|
93
|
+
throw notReady("delete from");
|
|
94
|
+
}
|
|
95
|
+
return currentStore.collection(collectionName).delete(id);
|
|
96
|
+
},
|
|
97
|
+
where(conditions) {
|
|
98
|
+
const currentStore = getStore();
|
|
99
|
+
if (!currentStore) {
|
|
100
|
+
throw notReady("query");
|
|
101
|
+
}
|
|
102
|
+
return currentStore.collection(collectionName).where(conditions);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/initialize-app.ts
|
|
108
|
+
var import_core4 = require("@korajs/core");
|
|
109
|
+
var import_store4 = require("@korajs/store");
|
|
110
|
+
var import_sync3 = require("@korajs/sync");
|
|
58
111
|
|
|
59
112
|
// src/adapter-resolver.ts
|
|
113
|
+
function importOptionalPeer(specifier) {
|
|
114
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
115
|
+
return dynamicImport(specifier);
|
|
116
|
+
}
|
|
60
117
|
function detectAdapterType() {
|
|
61
118
|
if (typeof globalThis !== "undefined" && typeof globalThis.__TAURI_INTERNALS__ !== "undefined") {
|
|
62
119
|
return "tauri-sqlite";
|
|
@@ -72,13 +129,10 @@ function detectAdapterType() {
|
|
|
72
129
|
}
|
|
73
130
|
return "better-sqlite3";
|
|
74
131
|
}
|
|
75
|
-
async function createAdapter(type, dbName, workerUrl) {
|
|
132
|
+
async function createAdapter(type, dbName, workerUrl, emitter, workerResponseTimeoutMs, sharedWorkerUrl) {
|
|
76
133
|
switch (type) {
|
|
77
134
|
case "tauri-sqlite": {
|
|
78
|
-
const { TauriSqliteAdapter } = await
|
|
79
|
-
/* @vite-ignore */
|
|
80
|
-
"@korajs/tauri"
|
|
81
|
-
);
|
|
135
|
+
const { TauriSqliteAdapter } = await importOptionalPeer("@korajs/tauri");
|
|
82
136
|
return new TauriSqliteAdapter({ path: `${dbName}.db` });
|
|
83
137
|
}
|
|
84
138
|
case "better-sqlite3": {
|
|
@@ -90,11 +144,11 @@ async function createAdapter(type, dbName, workerUrl) {
|
|
|
90
144
|
}
|
|
91
145
|
case "sqlite-wasm": {
|
|
92
146
|
const { SqliteWasmAdapter } = await import("@korajs/store/sqlite-wasm");
|
|
93
|
-
return new SqliteWasmAdapter({ dbName, workerUrl });
|
|
147
|
+
return new SqliteWasmAdapter({ dbName, workerUrl, sharedWorkerUrl, workerResponseTimeoutMs });
|
|
94
148
|
}
|
|
95
149
|
case "indexeddb": {
|
|
96
150
|
const { IndexedDbAdapter } = await import("@korajs/store/indexeddb");
|
|
97
|
-
return new IndexedDbAdapter({ dbName, workerUrl });
|
|
151
|
+
return new IndexedDbAdapter({ dbName, workerUrl, emitter, workerResponseTimeoutMs });
|
|
98
152
|
}
|
|
99
153
|
default: {
|
|
100
154
|
const _exhaustive = type;
|
|
@@ -103,85 +157,525 @@ async function createAdapter(type, dbName, workerUrl) {
|
|
|
103
157
|
}
|
|
104
158
|
}
|
|
105
159
|
|
|
106
|
-
// src/
|
|
107
|
-
var
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
160
|
+
// src/apply-pipeline.ts
|
|
161
|
+
var import_core3 = require("@korajs/core");
|
|
162
|
+
var import_internal2 = require("@korajs/core/internal");
|
|
163
|
+
var import_merge = require("@korajs/merge");
|
|
164
|
+
var import_store = require("@korajs/store");
|
|
165
|
+
var import_internal3 = require("@korajs/store/internal");
|
|
166
|
+
|
|
167
|
+
// src/build-side-effect-entry.ts
|
|
168
|
+
var import_core2 = require("@korajs/core");
|
|
169
|
+
var import_internal = require("@korajs/store/internal");
|
|
170
|
+
async function buildSideEffectEntry(ctx, effect, parentOpId, transactionId, mutationName) {
|
|
171
|
+
const causalDeps = [parentOpId];
|
|
172
|
+
const baseInput = {
|
|
173
|
+
nodeId: ctx.nodeId,
|
|
174
|
+
collection: effect.collection,
|
|
175
|
+
recordId: effect.recordId,
|
|
176
|
+
sequenceNumber: await ctx.allocateSequenceNumber(),
|
|
177
|
+
causalDeps,
|
|
178
|
+
schemaVersion: ctx.schema.version,
|
|
179
|
+
...transactionId !== void 0 ? { transactionId } : {},
|
|
180
|
+
...mutationName !== void 0 ? { mutationName } : {}
|
|
181
|
+
};
|
|
182
|
+
if (effect.type === "delete") {
|
|
183
|
+
const operation2 = await (0, import_core2.createOperation)(
|
|
184
|
+
{
|
|
185
|
+
...baseInput,
|
|
186
|
+
type: "delete",
|
|
187
|
+
data: null,
|
|
188
|
+
previousData: effect.previousData
|
|
189
|
+
},
|
|
190
|
+
ctx.clock
|
|
191
|
+
);
|
|
192
|
+
ctx.causalTracker?.afterOperation(effect.collection, operation2.id, ctx.inTransaction);
|
|
193
|
+
const version2 = (0, import_internal.serializeRowVersion)(operation2.timestamp);
|
|
194
|
+
const deleteQuery = (0, import_internal.buildSoftDeleteQuery)(
|
|
195
|
+
effect.collection,
|
|
196
|
+
effect.recordId,
|
|
197
|
+
operation2.timestamp.wallTime,
|
|
198
|
+
version2
|
|
199
|
+
);
|
|
200
|
+
const opInsert2 = (0, import_internal.buildInsertQuery)(
|
|
201
|
+
`_kora_ops_${effect.collection}`,
|
|
202
|
+
(0, import_internal.serializeOperation)(operation2)
|
|
203
|
+
);
|
|
204
|
+
return {
|
|
205
|
+
operation: operation2,
|
|
206
|
+
collection: effect.collection,
|
|
207
|
+
commands: [
|
|
208
|
+
{ sql: deleteQuery.sql, params: deleteQuery.params },
|
|
209
|
+
{ sql: opInsert2.sql, params: opInsert2.params },
|
|
210
|
+
{
|
|
211
|
+
sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
212
|
+
params: [ctx.nodeId, operation2.sequenceNumber]
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
};
|
|
112
216
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
217
|
+
const operation = await (0, import_core2.createOperation)(
|
|
218
|
+
{
|
|
219
|
+
...baseInput,
|
|
220
|
+
type: "update",
|
|
221
|
+
data: effect.data,
|
|
222
|
+
previousData: effect.previousData
|
|
223
|
+
},
|
|
224
|
+
ctx.clock
|
|
225
|
+
);
|
|
226
|
+
ctx.causalTracker?.afterOperation(effect.collection, operation.id, ctx.inTransaction);
|
|
227
|
+
const definition = ctx.schema.collections[effect.collection];
|
|
228
|
+
const serializedChanges = effect.data && definition ? (0, import_internal.serializeRecord)(effect.data, definition.fields) : effect.data ?? {};
|
|
229
|
+
const version = (0, import_internal.serializeRowVersion)(operation.timestamp);
|
|
230
|
+
const updateQuery = (0, import_internal.buildUpdateQuery)(effect.collection, effect.recordId, {
|
|
231
|
+
...serializedChanges,
|
|
232
|
+
_updated_at: operation.timestamp.wallTime,
|
|
233
|
+
_version: version
|
|
234
|
+
});
|
|
235
|
+
const opInsert = (0, import_internal.buildInsertQuery)(
|
|
236
|
+
`_kora_ops_${effect.collection}`,
|
|
237
|
+
(0, import_internal.serializeOperation)(operation)
|
|
238
|
+
);
|
|
239
|
+
return {
|
|
240
|
+
operation,
|
|
241
|
+
collection: effect.collection,
|
|
242
|
+
commands: [
|
|
243
|
+
{ sql: updateQuery.sql, params: updateQuery.params },
|
|
244
|
+
{ sql: opInsert.sql, params: opInsert.params },
|
|
245
|
+
{
|
|
246
|
+
sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
|
|
247
|
+
params: [ctx.nodeId, operation.sequenceNumber]
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/apply-pipeline.ts
|
|
254
|
+
var ApplyPipeline = class {
|
|
255
|
+
constructor(deps) {
|
|
256
|
+
this.deps = deps;
|
|
257
|
+
this.relationLookupMap = (0, import_merge.buildMergeRelationLookup)(deps.store.getSchema());
|
|
118
258
|
}
|
|
119
|
-
|
|
120
|
-
|
|
259
|
+
deps;
|
|
260
|
+
relationLookupMap;
|
|
261
|
+
/** Local insert — same persistence path as remote apply side effects. */
|
|
262
|
+
async insert(collection, data) {
|
|
263
|
+
return (0, import_internal3.executeInsert)(this.deps.store.createMutationContext(collection), data);
|
|
121
264
|
}
|
|
122
|
-
|
|
123
|
-
|
|
265
|
+
/** Local update. */
|
|
266
|
+
async update(collection, id, data) {
|
|
267
|
+
return (0, import_internal3.executeUpdate)(this.deps.store.createMutationContext(collection), id, data);
|
|
124
268
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Local delete with merge-package referential integrity (same rules as remote delete).
|
|
271
|
+
*/
|
|
272
|
+
async delete(collection, id) {
|
|
273
|
+
const ctx = this.deps.store.createMutationContext(collection);
|
|
274
|
+
const causalDeps = (0, import_internal3.resolveCausalDeps)(ctx);
|
|
275
|
+
const operation = await (0, import_core3.createOperation)(
|
|
276
|
+
{
|
|
277
|
+
nodeId: ctx.nodeId,
|
|
278
|
+
type: "delete",
|
|
279
|
+
collection,
|
|
280
|
+
recordId: id,
|
|
281
|
+
data: null,
|
|
282
|
+
previousData: null,
|
|
283
|
+
sequenceNumber: await ctx.allocateSequenceNumber(),
|
|
284
|
+
causalDeps,
|
|
285
|
+
schemaVersion: ctx.schema.version
|
|
286
|
+
},
|
|
287
|
+
ctx.clock
|
|
288
|
+
);
|
|
289
|
+
ctx.causalTracker?.afterOperation(collection, operation.id, ctx.inTransaction);
|
|
290
|
+
const refCtx = createReferentialMergeContext(this.deps.store);
|
|
291
|
+
const check = await (0, import_merge.checkReferentialIntegrityOnDelete)(
|
|
292
|
+
operation,
|
|
293
|
+
this.deps.store.getSchema(),
|
|
294
|
+
refCtx,
|
|
295
|
+
this.relationLookupMap
|
|
296
|
+
);
|
|
297
|
+
for (const trace of check.traces) {
|
|
298
|
+
this.deps.emitter?.emit({ type: "merge:conflict", trace });
|
|
299
|
+
}
|
|
300
|
+
if (!check.allowed) {
|
|
301
|
+
throw new import_core3.KoraError(
|
|
302
|
+
`Cannot delete record "${id}" from "${collection}": referential restrict policy violated`,
|
|
303
|
+
"REFERENTIAL_INTEGRITY",
|
|
304
|
+
{ collection, recordId: id }
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
await (0, import_internal3.executeDelete)(ctx, id, {
|
|
308
|
+
skipReferentialEnforcement: true,
|
|
309
|
+
operation
|
|
310
|
+
});
|
|
311
|
+
if (check.sideEffectOps.length > 0) {
|
|
312
|
+
await applySideEffectOps(this.deps.store, check.sideEffectOps, operation.id);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Commit a buffered transaction: merge-package delete enforcement, causal ordering, single DB txn.
|
|
317
|
+
*/
|
|
318
|
+
async commitTransaction(batch) {
|
|
319
|
+
const store = this.deps.store;
|
|
320
|
+
const supplemental = [];
|
|
321
|
+
for (const entry of batch.entries) {
|
|
322
|
+
if (entry.operation.type !== "delete") {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const refCtx = createReferentialMergeContext(store);
|
|
326
|
+
const check = await (0, import_merge.checkReferentialIntegrityOnDelete)(
|
|
327
|
+
entry.operation,
|
|
328
|
+
store.getSchema(),
|
|
329
|
+
refCtx,
|
|
330
|
+
this.relationLookupMap
|
|
331
|
+
);
|
|
332
|
+
for (const trace of check.traces) {
|
|
333
|
+
this.deps.emitter?.emit({ type: "merge:conflict", trace });
|
|
334
|
+
}
|
|
335
|
+
if (!check.allowed) {
|
|
336
|
+
throw new import_core3.KoraError(
|
|
337
|
+
`Cannot delete record "${entry.operation.recordId}" from "${entry.collection}": referential restrict policy violated`,
|
|
338
|
+
"REFERENTIAL_INTEGRITY",
|
|
339
|
+
{ collection: entry.collection, recordId: entry.operation.recordId }
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
for (const effect of check.sideEffectOps) {
|
|
343
|
+
const ctx2 = store.createMutationContext(effect.collection, { inTransaction: true });
|
|
344
|
+
supplemental.push(
|
|
345
|
+
await buildSideEffectEntry(
|
|
346
|
+
ctx2,
|
|
347
|
+
effect,
|
|
348
|
+
entry.operation.id,
|
|
349
|
+
batch.transactionId,
|
|
350
|
+
batch.mutationName
|
|
351
|
+
)
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const allEntries = [...batch.entries, ...supplemental];
|
|
356
|
+
const sortedOps = (0, import_internal2.topologicalSort)(allEntries.map((e) => e.operation));
|
|
357
|
+
const commandsByOpId = new Map(allEntries.map((e) => [e.operation.id, e.commands]));
|
|
358
|
+
const ctx = store.createMutationContext(
|
|
359
|
+
batch.entries[0]?.collection ?? Object.keys(store.getSchema().collections)[0] ?? "todos",
|
|
360
|
+
{ inTransaction: true }
|
|
361
|
+
);
|
|
362
|
+
await ctx.adapter.transaction(async (tx) => {
|
|
363
|
+
for (const op2 of sortedOps) {
|
|
364
|
+
const commands = commandsByOpId.get(op2.id);
|
|
365
|
+
if (!commands) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
for (const cmd of commands) {
|
|
369
|
+
await tx.execute(cmd.sql, cmd.params);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
const affectedCollections = /* @__PURE__ */ new Set();
|
|
374
|
+
for (const entry of allEntries) {
|
|
375
|
+
affectedCollections.add(entry.collection);
|
|
376
|
+
}
|
|
377
|
+
return { operations: sortedOps, affectedCollections };
|
|
378
|
+
}
|
|
379
|
+
async applyRemote(op2) {
|
|
380
|
+
return this.apply(op2, { mode: "remote", schema: this.deps.store.getSchema() });
|
|
381
|
+
}
|
|
382
|
+
async apply(op2, context) {
|
|
383
|
+
if (context.mode === "local") {
|
|
384
|
+
return this.deps.store.applyRemoteOperation(op2);
|
|
385
|
+
}
|
|
386
|
+
if (op2.type === "delete") {
|
|
387
|
+
return this.applyRemoteDelete(op2);
|
|
388
|
+
}
|
|
389
|
+
if (op2.type === "insert" && op2.data) {
|
|
390
|
+
return this.applyRemoteInsert(op2);
|
|
391
|
+
}
|
|
392
|
+
if (op2.type === "update" && op2.data && op2.previousData) {
|
|
393
|
+
return this.applyRemoteUpdate(op2);
|
|
394
|
+
}
|
|
395
|
+
return this.deps.store.applyRemoteOperation(op2);
|
|
396
|
+
}
|
|
397
|
+
async applyRemoteDelete(op2) {
|
|
398
|
+
const blocked = await this.resolveRemoteDeleteVsLocalUpdate(op2);
|
|
399
|
+
if (blocked) {
|
|
400
|
+
return "skipped";
|
|
401
|
+
}
|
|
402
|
+
const refCtx = createReferentialMergeContext(this.deps.store);
|
|
403
|
+
const check = await (0, import_merge.checkReferentialIntegrityOnDelete)(
|
|
404
|
+
op2,
|
|
405
|
+
this.deps.store.getSchema(),
|
|
406
|
+
refCtx,
|
|
407
|
+
this.relationLookupMap
|
|
408
|
+
);
|
|
409
|
+
for (const trace of check.traces) {
|
|
410
|
+
this.deps.emitter?.emit({ type: "merge:conflict", trace });
|
|
411
|
+
}
|
|
412
|
+
if (!check.allowed) {
|
|
413
|
+
return "rejected";
|
|
414
|
+
}
|
|
415
|
+
const result = await this.deps.store.applyRemoteOperation(op2);
|
|
416
|
+
if (result !== "applied") {
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
if (check.sideEffectOps.length > 0) {
|
|
420
|
+
await applySideEffectOps(this.deps.store, check.sideEffectOps, op2.id);
|
|
421
|
+
}
|
|
422
|
+
return "applied";
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* When a local update is newer than a remote delete, keep the record alive.
|
|
426
|
+
*/
|
|
427
|
+
async resolveRemoteDeleteVsLocalUpdate(op2) {
|
|
428
|
+
const collectionDef = this.deps.store.getSchema().collections[op2.collection];
|
|
429
|
+
if (!collectionDef) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
const localOp = await this.deps.store.getLatestLocalOperationForRecord(
|
|
433
|
+
op2.collection,
|
|
434
|
+
op2.recordId
|
|
435
|
+
);
|
|
436
|
+
if (!localOp || localOp.type !== "update") {
|
|
437
|
+
return false;
|
|
128
438
|
}
|
|
129
|
-
const
|
|
439
|
+
const baseState = localOp.previousData ?? {};
|
|
440
|
+
const mergeResult = await this.deps.mergeEngine.merge({
|
|
441
|
+
local: localOp,
|
|
442
|
+
remote: op2,
|
|
443
|
+
baseState,
|
|
444
|
+
collectionDef
|
|
445
|
+
});
|
|
446
|
+
this.emitMergeLifecycle(op2, localOp, mergeResult);
|
|
447
|
+
return mergeResult.appliedOperation === "local";
|
|
448
|
+
}
|
|
449
|
+
async applyRemoteUpdate(op2) {
|
|
450
|
+
const schema = this.deps.store.getSchema();
|
|
130
451
|
const collectionDef = schema.collections[op2.collection];
|
|
131
452
|
if (!collectionDef) {
|
|
132
|
-
return this.store.applyRemoteOperation(op2);
|
|
453
|
+
return this.deps.store.applyRemoteOperation(op2);
|
|
133
454
|
}
|
|
134
|
-
const accessor = this.store.collection(op2.collection);
|
|
455
|
+
const accessor = this.deps.store.collection(op2.collection);
|
|
135
456
|
const currentRecord = await accessor.findById(op2.recordId);
|
|
136
457
|
if (!currentRecord) {
|
|
137
|
-
|
|
458
|
+
const tombstoneResult = await this.applyRemoteUpdateOnDeletedRow(op2, collectionDef);
|
|
459
|
+
if (tombstoneResult !== null) {
|
|
460
|
+
return tombstoneResult;
|
|
461
|
+
}
|
|
462
|
+
return this.deps.store.applyRemoteOperation(op2);
|
|
138
463
|
}
|
|
139
464
|
let hasConflict = false;
|
|
140
|
-
for (const field of Object.keys(op2.data)) {
|
|
141
|
-
const
|
|
465
|
+
for (const field of Object.keys(op2.data ?? {})) {
|
|
466
|
+
const fieldDef = collectionDef.fields[field];
|
|
467
|
+
const expectedBase = op2.previousData?.[field];
|
|
142
468
|
const currentLocal = currentRecord[field];
|
|
469
|
+
if (fieldDef?.kind === "richtext") {
|
|
470
|
+
if (!(0, import_store.richtextStatesEqual)(currentLocal, expectedBase)) {
|
|
471
|
+
hasConflict = true;
|
|
472
|
+
}
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
143
475
|
if (!deepEqual(expectedBase, currentLocal)) {
|
|
144
476
|
hasConflict = true;
|
|
145
477
|
break;
|
|
146
478
|
}
|
|
147
479
|
}
|
|
148
480
|
if (!hasConflict) {
|
|
149
|
-
return this.store.applyRemoteOperation(op2);
|
|
481
|
+
return this.deps.store.applyRemoteOperation(op2);
|
|
150
482
|
}
|
|
151
|
-
this.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
483
|
+
return this.applyMergedUpdate(op2, collectionDef, currentRecord, op2.previousData ?? {});
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Remote update vs local soft-delete: merge before materializing to avoid zombie rows.
|
|
487
|
+
*/
|
|
488
|
+
async applyRemoteUpdateOnDeletedRow(op2, collectionDef) {
|
|
489
|
+
const snapshot = await this.deps.store.findMaterializedRow(op2.collection, op2.recordId);
|
|
490
|
+
if (!snapshot?.deleted) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
const localOp = await this.deps.store.getLatestLocalOperationForRecord(
|
|
494
|
+
op2.collection,
|
|
495
|
+
op2.recordId
|
|
496
|
+
);
|
|
497
|
+
if (!localOp || localOp.type !== "delete") {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
const mergeResult = await this.deps.mergeEngine.merge({
|
|
501
|
+
local: localOp,
|
|
502
|
+
remote: op2,
|
|
503
|
+
baseState: op2.previousData ?? {},
|
|
504
|
+
collectionDef
|
|
505
|
+
});
|
|
506
|
+
this.emitMergeLifecycle(op2, localOp, mergeResult);
|
|
507
|
+
if (mergeResult.appliedOperation === "local") {
|
|
508
|
+
return "skipped";
|
|
509
|
+
}
|
|
510
|
+
const mergedOp = {
|
|
511
|
+
...op2,
|
|
512
|
+
data: { ...mergeResult.mergedData },
|
|
513
|
+
timestamp: maxTimestamp(op2.timestamp, localOp.timestamp)
|
|
514
|
+
};
|
|
515
|
+
const applyResult = await this.deps.store.applyRemoteOperation(mergedOp, {
|
|
516
|
+
reactivateIfDeleted: true
|
|
155
517
|
});
|
|
156
|
-
|
|
518
|
+
if (applyResult === "applied" && mergeResult.sideEffects.length > 0) {
|
|
519
|
+
await applySideEffectOps(this.deps.store, mergeResult.sideEffects, op2.id);
|
|
520
|
+
}
|
|
521
|
+
return applyResult;
|
|
522
|
+
}
|
|
523
|
+
async applyMergedUpdate(op2, collectionDef, currentRecord, baseState, applyOptions) {
|
|
524
|
+
const localTimestamp = await resolveLocalTimestamp(
|
|
525
|
+
this.deps.store,
|
|
526
|
+
op2.collection,
|
|
527
|
+
op2.recordId,
|
|
528
|
+
currentRecord,
|
|
529
|
+
this.deps.store.getNodeId()
|
|
530
|
+
);
|
|
157
531
|
const localOp = {
|
|
158
532
|
...op2,
|
|
159
|
-
|
|
160
|
-
data: buildLocalDiff(op2.previousData, currentRecord, Object.keys(op2.data)),
|
|
533
|
+
data: buildLocalDiff(baseState, currentRecord, Object.keys(op2.data ?? {})),
|
|
161
534
|
previousData: op2.previousData,
|
|
162
|
-
nodeId: this.store.getNodeId()
|
|
535
|
+
nodeId: this.deps.store.getNodeId(),
|
|
536
|
+
timestamp: localTimestamp
|
|
537
|
+
};
|
|
538
|
+
const constraintContext = createConstraintContext(this.deps.store);
|
|
539
|
+
const mergeResult = await this.deps.mergeEngine.merge(
|
|
540
|
+
{
|
|
541
|
+
local: localOp,
|
|
542
|
+
remote: op2,
|
|
543
|
+
baseState,
|
|
544
|
+
collectionDef
|
|
545
|
+
},
|
|
546
|
+
constraintContext
|
|
547
|
+
);
|
|
548
|
+
this.emitMergeLifecycle(op2, localOp, mergeResult);
|
|
549
|
+
const mergedOp = {
|
|
550
|
+
...op2,
|
|
551
|
+
data: mergeResult.mergedData,
|
|
552
|
+
timestamp: maxTimestamp(op2.timestamp, localTimestamp)
|
|
163
553
|
};
|
|
164
|
-
const
|
|
554
|
+
const applyResult = await this.deps.store.applyRemoteOperation(mergedOp, applyOptions);
|
|
555
|
+
if (applyResult === "applied" && mergeResult.sideEffects.length > 0) {
|
|
556
|
+
await applySideEffectOps(this.deps.store, mergeResult.sideEffects, op2.id);
|
|
557
|
+
}
|
|
558
|
+
return applyResult;
|
|
559
|
+
}
|
|
560
|
+
async applyRemoteInsert(op2) {
|
|
561
|
+
const collectionDef = this.deps.store.getSchema().collections[op2.collection];
|
|
562
|
+
if (!collectionDef || !op2.data) {
|
|
563
|
+
return this.deps.store.applyRemoteOperation(op2);
|
|
564
|
+
}
|
|
565
|
+
const snapshot = await this.deps.store.findMaterializedRow(op2.collection, op2.recordId);
|
|
566
|
+
if (!snapshot || snapshot.deleted) {
|
|
567
|
+
return this.deps.store.applyRemoteOperation(op2);
|
|
568
|
+
}
|
|
569
|
+
const localOp = await resolveLocalOpForRecord(
|
|
570
|
+
this.deps.store,
|
|
571
|
+
op2.collection,
|
|
572
|
+
op2.recordId,
|
|
573
|
+
snapshot.record
|
|
574
|
+
);
|
|
575
|
+
const mergeResult = await this.deps.mergeEngine.merge({
|
|
165
576
|
local: localOp,
|
|
166
577
|
remote: op2,
|
|
167
|
-
baseState,
|
|
578
|
+
baseState: {},
|
|
168
579
|
collectionDef
|
|
580
|
+
});
|
|
581
|
+
this.emitMergeLifecycle(op2, localOp, mergeResult);
|
|
582
|
+
const mergedOp = {
|
|
583
|
+
...op2,
|
|
584
|
+
data: mergeResult.mergedData,
|
|
585
|
+
timestamp: maxTimestamp(op2.timestamp, localOp.timestamp)
|
|
169
586
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
587
|
+
return this.deps.store.applyRemoteOperation(mergedOp);
|
|
588
|
+
}
|
|
589
|
+
emitMergeLifecycle(remote, local, mergeResult) {
|
|
590
|
+
this.deps.emitter?.emit({
|
|
591
|
+
type: "merge:started",
|
|
592
|
+
operationA: remote,
|
|
593
|
+
operationB: local
|
|
594
|
+
});
|
|
595
|
+
const hadMergeConflict = mergeResult.traces.some(
|
|
596
|
+
(t2) => t2.strategy !== "no-conflict-local" && t2.strategy !== "no-conflict-remote" && t2.strategy !== "no-conflict-unchanged"
|
|
597
|
+
);
|
|
598
|
+
if (hadMergeConflict) {
|
|
599
|
+
this.deps.onMergeConflict?.();
|
|
600
|
+
}
|
|
601
|
+
for (const trace of mergeResult.traces) {
|
|
602
|
+
if (trace.strategy !== "no-conflict-local" && trace.strategy !== "no-conflict-remote" && trace.strategy !== "no-conflict-unchanged") {
|
|
603
|
+
this.deps.emitter?.emit({ type: "merge:conflict", trace });
|
|
604
|
+
}
|
|
173
605
|
}
|
|
174
|
-
const firstTrace =
|
|
606
|
+
const firstTrace = mergeResult.traces[0];
|
|
175
607
|
if (firstTrace) {
|
|
176
|
-
this.emitter?.emit({ type: "merge:completed", trace: firstTrace });
|
|
608
|
+
this.deps.emitter?.emit({ type: "merge:completed", trace: firstTrace });
|
|
177
609
|
}
|
|
178
|
-
const mergedOp = {
|
|
179
|
-
...op2,
|
|
180
|
-
data: result.mergedData
|
|
181
|
-
};
|
|
182
|
-
return this.store.applyRemoteOperation(mergedOp);
|
|
183
610
|
}
|
|
184
611
|
};
|
|
612
|
+
async function resolveLocalOpForRecord(store, collection, recordId, record) {
|
|
613
|
+
const localLatest = await store.getLatestLocalOperationForRecord(collection, recordId);
|
|
614
|
+
if (localLatest) {
|
|
615
|
+
return localLatest;
|
|
616
|
+
}
|
|
617
|
+
const anyLatest = await store.getLatestOperationForRecord(collection, recordId);
|
|
618
|
+
if (anyLatest) {
|
|
619
|
+
return anyLatest;
|
|
620
|
+
}
|
|
621
|
+
return syntheticInsertFromSnapshot(
|
|
622
|
+
record,
|
|
623
|
+
collection,
|
|
624
|
+
store.getNodeId(),
|
|
625
|
+
store.getSchema().version
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
function syntheticInsertFromSnapshot(record, collection, nodeId, schemaVersion) {
|
|
629
|
+
const { id, createdAt, updatedAt, ...fields } = record;
|
|
630
|
+
return {
|
|
631
|
+
id: `synthetic-local-${id}`,
|
|
632
|
+
nodeId,
|
|
633
|
+
type: "insert",
|
|
634
|
+
collection,
|
|
635
|
+
recordId: id,
|
|
636
|
+
data: fields,
|
|
637
|
+
previousData: null,
|
|
638
|
+
timestamp: { wallTime: updatedAt, logical: 0, nodeId },
|
|
639
|
+
sequenceNumber: 0,
|
|
640
|
+
causalDeps: [],
|
|
641
|
+
schemaVersion
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function createConstraintContext(store) {
|
|
645
|
+
return {
|
|
646
|
+
async queryRecords(collection, where) {
|
|
647
|
+
const rows = await store.collection(collection).where(where).exec();
|
|
648
|
+
return rows;
|
|
649
|
+
},
|
|
650
|
+
async countRecords(collection, where) {
|
|
651
|
+
return store.collection(collection).where(where).count();
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function createReferentialMergeContext(store) {
|
|
656
|
+
return {
|
|
657
|
+
async queryRecords(collection, where) {
|
|
658
|
+
const rows = await store.collection(collection).where(where).exec();
|
|
659
|
+
return rows;
|
|
660
|
+
},
|
|
661
|
+
async recordExists(collection, recordId) {
|
|
662
|
+
const row = await store.collection(collection).findById(recordId);
|
|
663
|
+
return row !== null;
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
async function applySideEffectOps(store, sideEffects, parentOpId) {
|
|
668
|
+
for (const effect of sideEffects) {
|
|
669
|
+
const ctx = store.createMutationContext(effect.collection, {
|
|
670
|
+
extraCausalDeps: [parentOpId]
|
|
671
|
+
});
|
|
672
|
+
if (effect.type === "delete") {
|
|
673
|
+
await (0, import_internal3.executeDelete)(ctx, effect.recordId, { skipReferentialEnforcement: true });
|
|
674
|
+
} else if (effect.type === "update" && effect.data) {
|
|
675
|
+
await (0, import_internal3.executeUpdate)(ctx, effect.recordId, effect.data);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
185
679
|
function buildLocalDiff(baseState, currentRecord, fields) {
|
|
186
680
|
const diff = {};
|
|
187
681
|
for (const field of fields) {
|
|
@@ -189,6 +683,20 @@ function buildLocalDiff(baseState, currentRecord, fields) {
|
|
|
189
683
|
}
|
|
190
684
|
return diff;
|
|
191
685
|
}
|
|
686
|
+
async function resolveLocalTimestamp(store, collection, recordId, currentRecord, nodeId) {
|
|
687
|
+
const latestLocal = await store.getLatestLocalOperationForRecord(collection, recordId);
|
|
688
|
+
if (latestLocal) {
|
|
689
|
+
return latestLocal.timestamp;
|
|
690
|
+
}
|
|
691
|
+
const updatedAt = currentRecord.updatedAt;
|
|
692
|
+
if (typeof updatedAt === "number") {
|
|
693
|
+
return { wallTime: updatedAt, logical: 0, nodeId };
|
|
694
|
+
}
|
|
695
|
+
return { wallTime: Date.now(), logical: 0, nodeId };
|
|
696
|
+
}
|
|
697
|
+
function maxTimestamp(a, b) {
|
|
698
|
+
return import_core3.HybridLogicalClock.compare(a, b) >= 0 ? a : b;
|
|
699
|
+
}
|
|
192
700
|
function deepEqual(a, b) {
|
|
193
701
|
if (a === b) return true;
|
|
194
702
|
if (a === null || b === null) return false;
|
|
@@ -209,113 +717,371 @@ function deepEqual(a, b) {
|
|
|
209
717
|
return false;
|
|
210
718
|
}
|
|
211
719
|
|
|
212
|
-
// src/
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
720
|
+
// src/audit-bridge.ts
|
|
721
|
+
var import_store2 = require("@korajs/store");
|
|
722
|
+
var AUDIT_EVENT_TYPES = ["merge:completed", "merge:conflict", "constraint:violated"];
|
|
723
|
+
function isAuditEvent(event) {
|
|
724
|
+
return AUDIT_EVENT_TYPES.includes(event.type);
|
|
725
|
+
}
|
|
726
|
+
function wireAuditPersistence(store, emitter) {
|
|
727
|
+
const unsubscribers = AUDIT_EVENT_TYPES.map(
|
|
728
|
+
(eventType) => emitter.on(eventType, (event) => {
|
|
729
|
+
if (!isAuditEvent(event)) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const trace = (0, import_store2.persistedAuditTraceFromEvent)(event);
|
|
733
|
+
void store.appendAuditTrace(trace).catch(() => {
|
|
734
|
+
});
|
|
735
|
+
})
|
|
736
|
+
);
|
|
737
|
+
return () => {
|
|
738
|
+
for (const unsub of unsubscribers) {
|
|
739
|
+
unsub();
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/create-sync-transport.ts
|
|
745
|
+
var import_sync = require("@korajs/sync");
|
|
746
|
+
function createSyncTransport(sync) {
|
|
747
|
+
if (sync.transport === "http") {
|
|
748
|
+
return new import_sync.HttpLongPollingTransport();
|
|
749
|
+
}
|
|
750
|
+
return new import_sync.WebSocketTransport();
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/merge-aware-sync-store.ts
|
|
754
|
+
var MergeAwareSyncStore = class {
|
|
755
|
+
constructor(store, mergeEngine, emitter, options) {
|
|
756
|
+
this.store = store;
|
|
757
|
+
this.pipeline = new ApplyPipeline({
|
|
758
|
+
store,
|
|
759
|
+
mergeEngine,
|
|
760
|
+
emitter,
|
|
761
|
+
onMergeConflict: options?.onMergeConflict
|
|
227
762
|
});
|
|
228
763
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
764
|
+
store;
|
|
765
|
+
pipeline;
|
|
766
|
+
getVersionVector() {
|
|
767
|
+
return this.store.getVersionVector();
|
|
768
|
+
}
|
|
769
|
+
getNodeId() {
|
|
770
|
+
return this.store.getNodeId();
|
|
771
|
+
}
|
|
772
|
+
async getOperationRange(nodeId, fromSeq, toSeq) {
|
|
773
|
+
return this.store.getOperationRange(nodeId, fromSeq, toSeq);
|
|
774
|
+
}
|
|
775
|
+
async applyRemoteOperation(op2) {
|
|
776
|
+
return this.pipeline.applyRemote(op2);
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
// src/store-queue-storage.ts
|
|
781
|
+
var import_internal4 = require("@korajs/store/internal");
|
|
782
|
+
var StoreQueueStorage = class {
|
|
783
|
+
constructor(adapter) {
|
|
784
|
+
this.adapter = adapter;
|
|
785
|
+
}
|
|
786
|
+
adapter;
|
|
787
|
+
async load() {
|
|
788
|
+
const rows = await this.adapter.query(
|
|
789
|
+
"SELECT id, payload FROM _kora_sync_queue ORDER BY rowid ASC"
|
|
790
|
+
);
|
|
791
|
+
return rows.map((row) => operationFromQueuePayload(row.payload));
|
|
792
|
+
}
|
|
793
|
+
async enqueue(op2) {
|
|
794
|
+
const row = (0, import_internal4.serializeOperation)(op2);
|
|
795
|
+
const payload = { ...row, _collection: op2.collection };
|
|
796
|
+
await this.adapter.execute(
|
|
797
|
+
"INSERT OR REPLACE INTO _kora_sync_queue (id, payload) VALUES (?, ?)",
|
|
798
|
+
[op2.id, JSON.stringify(payload)]
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
async dequeue(ids) {
|
|
802
|
+
if (ids.length === 0) return;
|
|
803
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
804
|
+
await this.adapter.execute(`DELETE FROM _kora_sync_queue WHERE id IN (${placeholders})`, ids);
|
|
805
|
+
}
|
|
806
|
+
async count() {
|
|
807
|
+
const rows = await this.adapter.query(
|
|
808
|
+
"SELECT COUNT(*) as cnt FROM _kora_sync_queue"
|
|
809
|
+
);
|
|
810
|
+
return rows[0]?.cnt ?? 0;
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
function operationFromQueuePayload(payload) {
|
|
814
|
+
const parsed = JSON.parse(payload);
|
|
815
|
+
const op2 = (0, import_internal4.deserializeOperation)(parsed);
|
|
816
|
+
return {
|
|
817
|
+
...op2,
|
|
818
|
+
collection: parsed._collection
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// src/store-sync-state.ts
|
|
823
|
+
var import_store3 = require("@korajs/store");
|
|
824
|
+
var import_sync2 = require("@korajs/sync");
|
|
825
|
+
var StoreSyncStatePersistence = class {
|
|
826
|
+
constructor(store, scope) {
|
|
827
|
+
this.store = store;
|
|
828
|
+
this.scope = scope;
|
|
829
|
+
}
|
|
830
|
+
store;
|
|
831
|
+
scope;
|
|
832
|
+
loadLastAckedServerVector() {
|
|
833
|
+
return this.store.loadLastAckedServerVector();
|
|
834
|
+
}
|
|
835
|
+
saveLastAckedServerVector(vector) {
|
|
836
|
+
return this.store.saveLastAckedServerVector(vector);
|
|
837
|
+
}
|
|
838
|
+
mergeServerVectors(a, b) {
|
|
839
|
+
return this.store.mergeServerVectors(a, b);
|
|
840
|
+
}
|
|
841
|
+
async countUnsyncedOperations(serverVector) {
|
|
842
|
+
const ops = await this.getUnsyncedOperations(serverVector);
|
|
843
|
+
return ops.length;
|
|
844
|
+
}
|
|
845
|
+
async getUnsyncedOperations(serverVector) {
|
|
846
|
+
const ops = await this.store.getUnsyncedOperations(serverVector);
|
|
847
|
+
return ops.filter((op2) => (0, import_sync2.operationMatchesScope)(op2, this.scope));
|
|
848
|
+
}
|
|
849
|
+
loadDeltaCursor() {
|
|
850
|
+
return this.store.loadDeltaCursor().then((encoded) => (0, import_sync2.decodeDeltaCursor)(encoded));
|
|
851
|
+
}
|
|
852
|
+
async saveDeltaCursor(cursor) {
|
|
853
|
+
await this.store.saveDeltaCursor(cursor ? (0, import_sync2.encodeDeltaCursor)(cursor) : null);
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
// src/sync-query-bridge.ts
|
|
858
|
+
function queryDescriptorToSyncSubset(descriptor) {
|
|
859
|
+
const where = {};
|
|
860
|
+
const skippedFields = [];
|
|
861
|
+
for (const [field, value] of Object.entries(descriptor.where)) {
|
|
862
|
+
if (value === null || value === void 0) {
|
|
863
|
+
where[field] = value;
|
|
864
|
+
continue;
|
|
277
865
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
866
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
867
|
+
where[field] = value;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
skippedFields.push(field);
|
|
871
|
+
}
|
|
872
|
+
if (skippedFields.length > 0 && typeof console !== "undefined") {
|
|
873
|
+
console.warn(
|
|
874
|
+
`[Kora] Sync query subset omitted non-equality filters on ${descriptor.collection}: ${skippedFields.join(", ")}. Only plain equality WHERE clauses are registered for incremental sync.`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
if (Object.keys(where).length === 0) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
collection: descriptor.collection,
|
|
882
|
+
where
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function createSyncQuerySubscriptionHook(getSyncEngine) {
|
|
886
|
+
return (descriptor) => {
|
|
887
|
+
const subset = queryDescriptorToSyncSubset(descriptor);
|
|
888
|
+
if (!subset) {
|
|
889
|
+
return () => {
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
const engine = getSyncEngine();
|
|
893
|
+
if (!engine) {
|
|
894
|
+
return () => {
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
return engine.registerQuerySubset(subset);
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/initialize-app.ts
|
|
902
|
+
async function initializeApp(config, emitter, mergeEngine) {
|
|
903
|
+
const adapterType = config.store?.adapter ?? detectAdapterType();
|
|
904
|
+
const dbName = config.store?.name ?? "kora-db";
|
|
905
|
+
const adapter = await createAdapter(
|
|
906
|
+
adapterType,
|
|
907
|
+
dbName,
|
|
908
|
+
config.store?.workerUrl,
|
|
909
|
+
emitter,
|
|
910
|
+
config.store?.workerResponseTimeoutMs,
|
|
911
|
+
config.store?.sharedWorkerUrl
|
|
912
|
+
);
|
|
913
|
+
const authBinding = config.sync?.authClient ?? null;
|
|
914
|
+
const authNodeId = authBinding?.resolveNodeId ? await authBinding.resolveNodeId() : void 0;
|
|
915
|
+
let syncEngine = null;
|
|
916
|
+
const store = new import_store4.Store({
|
|
917
|
+
schema: config.schema,
|
|
918
|
+
adapter,
|
|
919
|
+
emitter,
|
|
920
|
+
dbName,
|
|
921
|
+
nodeId: authNodeId,
|
|
922
|
+
isolation: authNodeId ? "shared" : config.store?.isolation,
|
|
923
|
+
...config.sync ? { onQuerySubscribed: createSyncQuerySubscriptionHook(() => syncEngine) } : {}
|
|
924
|
+
});
|
|
925
|
+
await store.open();
|
|
926
|
+
let recordConflict;
|
|
927
|
+
const applyPipeline = new ApplyPipeline({
|
|
928
|
+
store,
|
|
929
|
+
mergeEngine,
|
|
930
|
+
emitter,
|
|
931
|
+
onMergeConflict: () => recordConflict?.()
|
|
932
|
+
});
|
|
933
|
+
store.setLocalMutationHandler(applyPipeline);
|
|
934
|
+
const unsubscribeAudit = wireAuditPersistence(store, emitter);
|
|
935
|
+
let unsubscribeSync = null;
|
|
936
|
+
if (config.sync) {
|
|
937
|
+
const transport = createSyncTransport(config.sync);
|
|
938
|
+
const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter, {
|
|
939
|
+
onMergeConflict: () => recordConflict?.()
|
|
940
|
+
});
|
|
941
|
+
let scopeMap = config.sync.scope ? (0, import_core4.buildScopeMap)(config.schema, config.sync.scope) : void 0;
|
|
942
|
+
if (authBinding?.resolveScopeMap) {
|
|
943
|
+
scopeMap = await authBinding.resolveScopeMap() ?? scopeMap;
|
|
944
|
+
}
|
|
945
|
+
const syncAuth = authBinding?.auth ?? config.sync.auth;
|
|
946
|
+
const encryptor = config.sync.encryption?.enabled === true ? await import_sync3.SyncEncryptor.create(config.sync.encryption) : void 0;
|
|
947
|
+
syncEngine = new import_sync3.SyncEngine({
|
|
948
|
+
transport,
|
|
949
|
+
store: mergeAwareStore,
|
|
950
|
+
config: {
|
|
951
|
+
url: config.sync.url,
|
|
952
|
+
transport: config.sync.transport,
|
|
953
|
+
auth: syncAuth,
|
|
954
|
+
batchSize: config.sync.batchSize,
|
|
955
|
+
schemaVersion: config.sync.schemaVersion ?? config.schema.version,
|
|
956
|
+
scopeMap,
|
|
957
|
+
encryption: config.sync.encryption,
|
|
958
|
+
strictHandshake: config.sync.strictHandshake,
|
|
959
|
+
operationTransforms: config.sync.operationTransforms
|
|
960
|
+
},
|
|
961
|
+
emitter,
|
|
962
|
+
queueStorage: new StoreQueueStorage(adapter),
|
|
963
|
+
syncState: new StoreSyncStatePersistence(store, scopeMap),
|
|
964
|
+
encryptor
|
|
965
|
+
});
|
|
966
|
+
recordConflict = () => syncEngine?.recordConflict();
|
|
967
|
+
unsubscribeSync = emitter.on("operation:created", (event) => {
|
|
968
|
+
if (syncEngine) {
|
|
969
|
+
syncEngine.pushOperation(event.operation);
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
store,
|
|
975
|
+
syncEngine,
|
|
976
|
+
unsubscribeSync,
|
|
977
|
+
unsubscribeAudit,
|
|
978
|
+
authBinding
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// src/sequences-accessor.ts
|
|
983
|
+
function createSequencesAccessor(ready, getStore) {
|
|
984
|
+
const requireStore = async () => {
|
|
985
|
+
await ready;
|
|
986
|
+
const store = getStore();
|
|
987
|
+
if (!store) {
|
|
988
|
+
throw new Error("Store not initialized. Await app.ready before using sequences.");
|
|
989
|
+
}
|
|
990
|
+
return store;
|
|
991
|
+
};
|
|
992
|
+
return {
|
|
993
|
+
async next(name, config) {
|
|
994
|
+
const store = await requireStore();
|
|
995
|
+
return store.getSequenceManager().next(name, config);
|
|
996
|
+
},
|
|
997
|
+
async current(name, config) {
|
|
998
|
+
const store = await requireStore();
|
|
999
|
+
return store.getSequenceManager().current(name, config);
|
|
1000
|
+
},
|
|
1001
|
+
async reset(name, config) {
|
|
1002
|
+
const store = await requireStore();
|
|
1003
|
+
return store.getSequenceManager().reset(name, config);
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/setup-devtools.ts
|
|
1009
|
+
var import_devtools = require("@korajs/devtools");
|
|
1010
|
+
function setupDevtools(config, emitter) {
|
|
1011
|
+
if (!config.devtools) {
|
|
1012
|
+
return { instrumenter: null, destroyOverlay: null };
|
|
1013
|
+
}
|
|
1014
|
+
const instrumenter = new import_devtools.Instrumenter(emitter, {
|
|
1015
|
+
bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
|
|
1016
|
+
});
|
|
1017
|
+
let destroyOverlay = null;
|
|
1018
|
+
if (typeof globalThis !== "undefined" && "window" in globalThis) {
|
|
1019
|
+
void import("@korajs/devtools/overlay").then(({ mountKoraDevtoolsOverlay }) => {
|
|
1020
|
+
destroyOverlay = mountKoraDevtoolsOverlay(instrumenter);
|
|
1021
|
+
}).catch(() => {
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
return { instrumenter, destroyOverlay: () => destroyOverlay?.() };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// src/sync-control.ts
|
|
1028
|
+
var import_sync4 = require("@korajs/sync");
|
|
1029
|
+
function createSyncControl(options) {
|
|
1030
|
+
const { config, ready, state } = options;
|
|
1031
|
+
if (!config.sync) {
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
const offlineSyncStatus = () => import_sync4.OFFLINE_SYNC_STATUS;
|
|
1035
|
+
const bridgeStatus = () => state.syncStatusBridge?.status ?? offlineSyncStatus();
|
|
1036
|
+
return {
|
|
1037
|
+
get status() {
|
|
1038
|
+
return bridgeStatus();
|
|
1039
|
+
},
|
|
1040
|
+
subscribeStatus(listener) {
|
|
1041
|
+
if (state.syncStatusBridge) {
|
|
1042
|
+
return state.syncStatusBridge.subscribe(listener);
|
|
1043
|
+
}
|
|
1044
|
+
listener(offlineSyncStatus());
|
|
1045
|
+
return () => {
|
|
1046
|
+
};
|
|
1047
|
+
},
|
|
1048
|
+
async connect() {
|
|
1049
|
+
await ready;
|
|
1050
|
+
if (state.syncEngine) {
|
|
1051
|
+
state.intentionalDisconnect = false;
|
|
1052
|
+
state.reconnectionManager?.stop();
|
|
1053
|
+
state.reconnectionManager?.reset();
|
|
1054
|
+
await state.syncEngine.start();
|
|
1055
|
+
state.syncStatusBridge?.refresh();
|
|
1056
|
+
}
|
|
288
1057
|
},
|
|
289
1058
|
async disconnect() {
|
|
290
1059
|
await ready;
|
|
291
|
-
if (syncEngine) {
|
|
292
|
-
intentionalDisconnect = true;
|
|
293
|
-
reconnectionManager?.stop();
|
|
294
|
-
await syncEngine.stop();
|
|
1060
|
+
if (state.syncEngine) {
|
|
1061
|
+
state.intentionalDisconnect = true;
|
|
1062
|
+
state.reconnectionManager?.stop();
|
|
1063
|
+
await state.syncEngine.stop();
|
|
1064
|
+
state.syncStatusBridge?.refresh();
|
|
295
1065
|
}
|
|
296
1066
|
},
|
|
297
1067
|
getStatus() {
|
|
298
|
-
if (syncEngine) {
|
|
299
|
-
return syncEngine.getStatus();
|
|
1068
|
+
if (state.syncEngine) {
|
|
1069
|
+
return state.syncEngine.getStatus();
|
|
300
1070
|
}
|
|
301
|
-
return
|
|
302
|
-
status: "offline",
|
|
303
|
-
pendingOperations: 0,
|
|
304
|
-
lastSyncedAt: null,
|
|
305
|
-
lastSuccessfulPush: null,
|
|
306
|
-
lastSuccessfulPull: null,
|
|
307
|
-
conflicts: 0
|
|
308
|
-
};
|
|
1071
|
+
return offlineSyncStatus();
|
|
309
1072
|
},
|
|
310
1073
|
async retryNow() {
|
|
311
1074
|
await ready;
|
|
312
|
-
if (syncEngine) {
|
|
313
|
-
await syncEngine.retryNow();
|
|
1075
|
+
if (state.syncEngine) {
|
|
1076
|
+
await state.syncEngine.retryNow();
|
|
314
1077
|
}
|
|
315
1078
|
},
|
|
1079
|
+
clearSchemaBlock() {
|
|
1080
|
+
state.syncEngine?.clearSchemaBlock();
|
|
1081
|
+
},
|
|
316
1082
|
exportDiagnostics() {
|
|
317
|
-
if (syncEngine) {
|
|
318
|
-
return syncEngine.exportDiagnostics();
|
|
1083
|
+
if (state.syncEngine) {
|
|
1084
|
+
return state.syncEngine.exportDiagnostics();
|
|
319
1085
|
}
|
|
320
1086
|
return {
|
|
321
1087
|
state: "disconnected",
|
|
@@ -340,62 +1106,341 @@ function createApp(config) {
|
|
|
340
1106
|
timestamp: Date.now()
|
|
341
1107
|
};
|
|
342
1108
|
}
|
|
343
|
-
}
|
|
344
|
-
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/auth-sync-coordinator.ts
|
|
1113
|
+
var AuthSyncCoordinator = class {
|
|
1114
|
+
constructor(getEngine, authBinding) {
|
|
1115
|
+
this.getEngine = getEngine;
|
|
1116
|
+
this.authBinding = authBinding;
|
|
1117
|
+
}
|
|
1118
|
+
getEngine;
|
|
1119
|
+
authBinding;
|
|
1120
|
+
inFlight = null;
|
|
1121
|
+
pending = false;
|
|
1122
|
+
disposed = false;
|
|
1123
|
+
scheduleReconnect() {
|
|
1124
|
+
if (this.disposed) {
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (this.inFlight) {
|
|
1128
|
+
this.pending = true;
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
this.inFlight = this.run().finally(() => {
|
|
1132
|
+
this.inFlight = null;
|
|
1133
|
+
if (this.pending && !this.disposed) {
|
|
1134
|
+
this.pending = false;
|
|
1135
|
+
this.scheduleReconnect();
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
destroy() {
|
|
1140
|
+
this.disposed = true;
|
|
1141
|
+
this.pending = false;
|
|
1142
|
+
}
|
|
1143
|
+
async run() {
|
|
1144
|
+
const engine = this.getEngine();
|
|
1145
|
+
if (!engine) {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const headers = await this.authBinding.auth();
|
|
1149
|
+
if (!headers.token) {
|
|
1150
|
+
await engine.stop();
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
if (this.authBinding.resolveScopeMap) {
|
|
1154
|
+
const nextScope = await this.authBinding.resolveScopeMap();
|
|
1155
|
+
engine.updateScope(nextScope);
|
|
1156
|
+
}
|
|
1157
|
+
const status = engine.getStatus().status;
|
|
1158
|
+
if (status !== "offline") {
|
|
1159
|
+
await engine.stop();
|
|
1160
|
+
}
|
|
1161
|
+
await engine.start();
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
// src/sync-status-bridge.ts
|
|
1166
|
+
var import_sync5 = require("@korajs/sync");
|
|
1167
|
+
function createSyncStatusBridge(emitter, getSyncEngine) {
|
|
1168
|
+
const controller = (0, import_sync5.createSyncStatusController)({
|
|
1169
|
+
getSyncEngine,
|
|
1170
|
+
subscribeSyncStatus: null,
|
|
1171
|
+
events: emitter
|
|
1172
|
+
});
|
|
1173
|
+
return {
|
|
1174
|
+
get status() {
|
|
1175
|
+
return controller.getSnapshot();
|
|
1176
|
+
},
|
|
1177
|
+
subscribe(listener) {
|
|
1178
|
+
return controller.subscribe(() => {
|
|
1179
|
+
listener(controller.getSnapshot());
|
|
1180
|
+
});
|
|
1181
|
+
},
|
|
1182
|
+
refresh: () => controller.refresh(),
|
|
1183
|
+
destroy: () => controller.destroy()
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/sync-lifecycle.ts
|
|
1188
|
+
var import_sync6 = require("@korajs/sync");
|
|
1189
|
+
function wireSyncLifecycleAfterReady(config, emitter, state, init) {
|
|
1190
|
+
state.syncEngine = init.syncEngine;
|
|
1191
|
+
if (!config.sync) {
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
state.syncStatusBridge = createSyncStatusBridge(emitter, () => state.syncEngine);
|
|
1195
|
+
state.syncStatusBridge.refresh();
|
|
1196
|
+
if (state.syncEngine && init.authBinding?.subscribe) {
|
|
1197
|
+
state.authSyncCoordinator = new AuthSyncCoordinator(() => state.syncEngine, init.authBinding);
|
|
1198
|
+
init.authBinding.subscribe(() => {
|
|
1199
|
+
state.authSyncCoordinator?.scheduleReconnect();
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
if (!state.syncEngine) {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const syncEngine = state.syncEngine;
|
|
1206
|
+
state.connectionMonitor = new import_sync6.ConnectionMonitor();
|
|
1207
|
+
state.reconnectionManager = new import_sync6.ReconnectionManager({
|
|
1208
|
+
initialDelay: config.sync.reconnectInterval,
|
|
1209
|
+
maxDelay: config.sync.maxReconnectInterval
|
|
1210
|
+
});
|
|
1211
|
+
emitter.on("sync:sent", () => state.connectionMonitor?.recordActivity());
|
|
1212
|
+
emitter.on("sync:received", () => state.connectionMonitor?.recordActivity());
|
|
1213
|
+
emitter.on("sync:acknowledged", () => state.connectionMonitor?.recordActivity());
|
|
1214
|
+
emitter.on("sync:connected", () => {
|
|
1215
|
+
if (state.qualityInterval !== null) {
|
|
1216
|
+
clearInterval(state.qualityInterval);
|
|
1217
|
+
}
|
|
1218
|
+
state.qualityInterval = setInterval(() => {
|
|
1219
|
+
if (state.connectionMonitor) {
|
|
1220
|
+
emitter.emit({
|
|
1221
|
+
type: "connection:quality",
|
|
1222
|
+
quality: state.connectionMonitor.getQuality()
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
}, 5e3);
|
|
1226
|
+
});
|
|
1227
|
+
emitter.on("sync:disconnected", () => {
|
|
1228
|
+
state.connectionMonitor?.reset();
|
|
1229
|
+
if (state.qualityInterval !== null) {
|
|
1230
|
+
clearInterval(state.qualityInterval);
|
|
1231
|
+
state.qualityInterval = null;
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
const browserGlobal = globalThis;
|
|
1235
|
+
if (typeof browserGlobal.addEventListener === "function") {
|
|
1236
|
+
const onOnline = () => {
|
|
1237
|
+
if (state.intentionalDisconnect || config.sync?.autoReconnect === false) {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
state.reconnectionManager?.wake();
|
|
1241
|
+
state.reconnectionManager?.reset();
|
|
1242
|
+
void syncEngine.retryNow();
|
|
1243
|
+
};
|
|
1244
|
+
browserGlobal.addEventListener("online", onOnline);
|
|
1245
|
+
state.removeOnlineListener = () => {
|
|
1246
|
+
browserGlobal.removeEventListener?.("online", onOnline);
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
emitter.on("sync:schema-mismatch", () => {
|
|
1250
|
+
state.reconnectionManager?.stop();
|
|
1251
|
+
state.intentionalDisconnect = true;
|
|
1252
|
+
});
|
|
1253
|
+
if (config.sync.autoReconnect !== false) {
|
|
1254
|
+
emitter.on("sync:disconnected", () => {
|
|
1255
|
+
if (state.intentionalDisconnect || syncEngine.isSchemaBlocked()) {
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
if (state.reconnectionManager?.isRunning()) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
syncEngine.setReconnecting(true);
|
|
1262
|
+
state.reconnectionManager?.stop();
|
|
1263
|
+
state.reconnectionManager?.start(async () => {
|
|
1264
|
+
try {
|
|
1265
|
+
await syncEngine.start();
|
|
1266
|
+
syncEngine.setReconnecting(false);
|
|
1267
|
+
return true;
|
|
1268
|
+
} catch {
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
}).then(() => {
|
|
1272
|
+
syncEngine.setReconnecting(false);
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
if (config.sync.autoConnect === true) {
|
|
1277
|
+
void syncEngine.start().catch(() => {
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
function teardownSyncLifecycle(state) {
|
|
1282
|
+
if (state.qualityInterval !== null) {
|
|
1283
|
+
clearInterval(state.qualityInterval);
|
|
1284
|
+
state.qualityInterval = null;
|
|
1285
|
+
}
|
|
1286
|
+
state.reconnectionManager?.stop();
|
|
1287
|
+
state.syncStatusBridge?.destroy();
|
|
1288
|
+
state.syncStatusBridge = null;
|
|
1289
|
+
state.authSyncCoordinator?.destroy();
|
|
1290
|
+
state.authSyncCoordinator = null;
|
|
1291
|
+
state.removeOnlineListener?.();
|
|
1292
|
+
state.removeOnlineListener = null;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/transaction-executor.ts
|
|
1296
|
+
function createTransactionExecutor(config, ready, getStore) {
|
|
1297
|
+
return async (fn, mutationName) => {
|
|
345
1298
|
await ready;
|
|
1299
|
+
const store = getStore();
|
|
346
1300
|
if (!store) {
|
|
347
1301
|
throw new Error("Store not initialized. Await app.ready before using transactions.");
|
|
348
1302
|
}
|
|
349
|
-
const txContext = store.createTransaction();
|
|
350
|
-
if (mutationName !== void 0) {
|
|
351
|
-
txContext.setMutationName(mutationName);
|
|
352
|
-
}
|
|
353
1303
|
const collectionNames = Object.keys(config.schema.collections);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
1304
|
+
return store.transaction(async (tx) => {
|
|
1305
|
+
if (mutationName !== void 0) {
|
|
1306
|
+
tx.setMutationName(mutationName);
|
|
1307
|
+
}
|
|
1308
|
+
const proxy = {};
|
|
1309
|
+
for (const name of collectionNames) {
|
|
1310
|
+
Object.defineProperty(proxy, name, {
|
|
1311
|
+
get() {
|
|
1312
|
+
return tx.collection(name);
|
|
1313
|
+
},
|
|
1314
|
+
enumerable: true,
|
|
1315
|
+
configurable: false
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
365
1318
|
await fn(proxy);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
1319
|
+
});
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// src/validate-config.ts
|
|
1324
|
+
var import_core5 = require("@korajs/core");
|
|
1325
|
+
function validateCreateAppConfig(config) {
|
|
1326
|
+
if (!config.schema) {
|
|
1327
|
+
throw new import_core5.SchemaValidationError("createApp requires a schema.", {
|
|
1328
|
+
fix: "Pass schema: defineSchema({ version: 1, collections: { ... } })"
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
if (config.schema.version < 1) {
|
|
1332
|
+
throw new import_core5.SchemaValidationError("Schema version must be at least 1.", {
|
|
1333
|
+
version: config.schema.version
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
const collectionNames = Object.keys(config.schema.collections);
|
|
1337
|
+
if (collectionNames.length === 0) {
|
|
1338
|
+
throw new import_core5.SchemaValidationError("Schema must define at least one collection.", {
|
|
1339
|
+
fix: "Add entries under collections in defineSchema()."
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
if (config.sync) {
|
|
1343
|
+
validateSyncUrl(config.sync.url, config.sync.transport ?? "websocket");
|
|
1344
|
+
}
|
|
1345
|
+
const adapter = config.store?.adapter ?? detectAdapterType();
|
|
1346
|
+
const isBrowser = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
|
|
1347
|
+
if (isBrowser && (adapter === "sqlite-wasm" || adapter === "indexeddb") && !config.store?.workerUrl) {
|
|
1348
|
+
throw new import_core5.KoraError(
|
|
1349
|
+
"Browser storage requires store.workerUrl pointing to the SQLite WASM worker script.",
|
|
1350
|
+
"MISSING_WORKER_URL",
|
|
1351
|
+
{
|
|
1352
|
+
adapter,
|
|
1353
|
+
fix: 'Add store: { workerUrl: "/sqlite-wasm-worker.js" } (see create-kora-app templates).'
|
|
370
1354
|
}
|
|
371
|
-
|
|
372
|
-
} catch (error) {
|
|
373
|
-
txContext.rollback();
|
|
374
|
-
throw error;
|
|
375
|
-
}
|
|
1355
|
+
);
|
|
376
1356
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
1357
|
+
}
|
|
1358
|
+
function validateSyncUrl(url, transport) {
|
|
1359
|
+
if (!url || url.trim().length === 0) {
|
|
1360
|
+
throw new import_core5.KoraError("sync.url is required when sync is configured.", "INVALID_SYNC_URL", {
|
|
1361
|
+
fix: 'Pass sync: { url: "wss://your-server/kora" }.'
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
const parsed = new URL(url);
|
|
1366
|
+
if (transport === "http") {
|
|
1367
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1368
|
+
throw new Error("bad protocol");
|
|
1369
|
+
}
|
|
1370
|
+
} else if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
|
1371
|
+
throw new Error("bad protocol");
|
|
392
1372
|
}
|
|
1373
|
+
} catch {
|
|
1374
|
+
throw new import_core5.KoraError(
|
|
1375
|
+
`Invalid sync URL "${url}" for transport "${transport}".`,
|
|
1376
|
+
"INVALID_SYNC_URL",
|
|
1377
|
+
{
|
|
1378
|
+
url,
|
|
1379
|
+
transport,
|
|
1380
|
+
fix: transport === "http" ? "Use an absolute http:// or https:// URL." : "Use an absolute ws:// or wss:// URL."
|
|
1381
|
+
}
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// src/wire-sync-event-forwarding.ts
|
|
1387
|
+
var SYNC_EVENT_TYPES = [
|
|
1388
|
+
"sync:connected",
|
|
1389
|
+
"sync:disconnected",
|
|
1390
|
+
"sync:schema-mismatch",
|
|
1391
|
+
"sync:auth-failed",
|
|
1392
|
+
"sync:sent",
|
|
1393
|
+
"sync:received",
|
|
1394
|
+
"sync:acknowledged",
|
|
1395
|
+
"sync:apply-failed",
|
|
1396
|
+
"sync:diagnostics",
|
|
1397
|
+
"sync:bandwidth",
|
|
1398
|
+
"sync:initial-sync-progress"
|
|
1399
|
+
];
|
|
1400
|
+
function wireSyncEventForwarding(emitter, handler) {
|
|
1401
|
+
for (const type of SYNC_EVENT_TYPES) {
|
|
1402
|
+
emitter.on(type, (event) => {
|
|
1403
|
+
handler(event);
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/create-app.ts
|
|
1409
|
+
function createApp(config) {
|
|
1410
|
+
validateCreateAppConfig(config);
|
|
1411
|
+
const emitter = new import_internal5.SimpleEventEmitter();
|
|
1412
|
+
const mergeEngine = new import_merge2.MergeEngine();
|
|
1413
|
+
if (config.onSyncEvent) {
|
|
1414
|
+
wireSyncEventForwarding(emitter, config.onSyncEvent);
|
|
1415
|
+
}
|
|
1416
|
+
let store = null;
|
|
1417
|
+
let unsubscribeSync = null;
|
|
1418
|
+
let unsubscribeAudit = null;
|
|
1419
|
+
const syncState = {
|
|
1420
|
+
syncEngine: null,
|
|
1421
|
+
syncStatusBridge: null,
|
|
1422
|
+
authSyncCoordinator: null,
|
|
1423
|
+
reconnectionManager: null,
|
|
1424
|
+
connectionMonitor: null,
|
|
1425
|
+
qualityInterval: null,
|
|
1426
|
+
intentionalDisconnect: false,
|
|
1427
|
+
removeOnlineListener: null
|
|
393
1428
|
};
|
|
1429
|
+
const devtools = setupDevtools(config, emitter);
|
|
1430
|
+
const queryStoreCache = new import_store5.QueryStoreCache(config.store?.name ?? "kora-db");
|
|
1431
|
+
const ready = initializeApp(config, emitter, mergeEngine).then((init) => {
|
|
1432
|
+
store = init.store;
|
|
1433
|
+
unsubscribeSync = init.unsubscribeSync;
|
|
1434
|
+
unsubscribeAudit = init.unsubscribeAudit;
|
|
1435
|
+
wireSyncLifecycleAfterReady(config, emitter, syncState, init);
|
|
1436
|
+
});
|
|
1437
|
+
const getStore = () => store;
|
|
1438
|
+
const executeTransaction = createTransactionExecutor(config, ready, getStore);
|
|
394
1439
|
const app = {
|
|
395
1440
|
ready,
|
|
396
1441
|
events: emitter,
|
|
397
|
-
sync:
|
|
398
|
-
sequences,
|
|
1442
|
+
sync: createSyncControl({ config, ready, state: syncState }),
|
|
1443
|
+
sequences: createSequencesAccessor(ready, getStore),
|
|
399
1444
|
getStore() {
|
|
400
1445
|
if (!store) {
|
|
401
1446
|
throw new Error("Store not initialized. Await app.ready before accessing the store.");
|
|
@@ -403,45 +1448,75 @@ function createApp(config) {
|
|
|
403
1448
|
return store;
|
|
404
1449
|
},
|
|
405
1450
|
getSyncEngine() {
|
|
406
|
-
return syncEngine;
|
|
1451
|
+
return syncState.syncEngine;
|
|
1452
|
+
},
|
|
1453
|
+
getQueryStoreCache() {
|
|
1454
|
+
return queryStoreCache;
|
|
407
1455
|
},
|
|
408
|
-
|
|
1456
|
+
transaction(fn) {
|
|
409
1457
|
return executeTransaction(fn);
|
|
410
1458
|
},
|
|
411
|
-
|
|
1459
|
+
mutation(name, fn) {
|
|
412
1460
|
return executeTransaction(fn, name);
|
|
413
1461
|
},
|
|
414
1462
|
async close() {
|
|
415
1463
|
await ready;
|
|
416
|
-
intentionalDisconnect = true;
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
reconnectionManager?.stop();
|
|
422
|
-
if (instrumenter) {
|
|
423
|
-
instrumenter.destroy();
|
|
424
|
-
instrumenter = null;
|
|
425
|
-
}
|
|
1464
|
+
syncState.intentionalDisconnect = true;
|
|
1465
|
+
teardownSyncLifecycle(syncState);
|
|
1466
|
+
devtools.destroyOverlay?.();
|
|
1467
|
+
devtools.instrumenter?.destroy();
|
|
426
1468
|
if (unsubscribeSync) {
|
|
427
1469
|
unsubscribeSync();
|
|
428
1470
|
unsubscribeSync = null;
|
|
429
1471
|
}
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
|
|
1472
|
+
if (unsubscribeAudit) {
|
|
1473
|
+
unsubscribeAudit();
|
|
1474
|
+
unsubscribeAudit = null;
|
|
1475
|
+
}
|
|
1476
|
+
if (syncState.syncEngine) {
|
|
1477
|
+
await syncState.syncEngine.stop();
|
|
1478
|
+
syncState.syncEngine = null;
|
|
433
1479
|
}
|
|
1480
|
+
queryStoreCache.clear();
|
|
434
1481
|
if (store) {
|
|
435
1482
|
await store.close();
|
|
436
1483
|
store = null;
|
|
437
1484
|
}
|
|
438
1485
|
emitter.clear();
|
|
1486
|
+
},
|
|
1487
|
+
async exportBackup(options) {
|
|
1488
|
+
await ready;
|
|
1489
|
+
if (!store) {
|
|
1490
|
+
throw new Error("Store not initialized. Await app.ready before exporting backup.");
|
|
1491
|
+
}
|
|
1492
|
+
return store.exportBackup(options);
|
|
1493
|
+
},
|
|
1494
|
+
async importBackup(data, options) {
|
|
1495
|
+
await ready;
|
|
1496
|
+
if (!store) {
|
|
1497
|
+
throw new Error("Store not initialized. Await app.ready before importing backup.");
|
|
1498
|
+
}
|
|
1499
|
+
return store.importBackup(data, options);
|
|
1500
|
+
},
|
|
1501
|
+
async replayTo(operationId) {
|
|
1502
|
+
await ready;
|
|
1503
|
+
if (!store) {
|
|
1504
|
+
throw new Error("Store not initialized. Await app.ready before replaying operations.");
|
|
1505
|
+
}
|
|
1506
|
+
return store.replayTo(operationId);
|
|
1507
|
+
},
|
|
1508
|
+
async exportAudit(options) {
|
|
1509
|
+
await ready;
|
|
1510
|
+
if (!store) {
|
|
1511
|
+
throw new Error("Store not initialized. Await app.ready before exporting audit data.");
|
|
1512
|
+
}
|
|
1513
|
+
return store.exportAudit(options);
|
|
439
1514
|
}
|
|
440
1515
|
};
|
|
441
1516
|
for (const collectionName of Object.keys(config.schema.collections)) {
|
|
442
1517
|
Object.defineProperty(app, collectionName, {
|
|
443
1518
|
get() {
|
|
444
|
-
return createCollectionAccessor(collectionName,
|
|
1519
|
+
return createCollectionAccessor(collectionName, getStore);
|
|
445
1520
|
},
|
|
446
1521
|
enumerable: true,
|
|
447
1522
|
configurable: false
|
|
@@ -449,136 +1524,23 @@ function createApp(config) {
|
|
|
449
1524
|
}
|
|
450
1525
|
return app;
|
|
451
1526
|
}
|
|
452
|
-
async function initializeAsync(config, emitter, mergeEngine) {
|
|
453
|
-
const adapterType = config.store?.adapter ?? detectAdapterType();
|
|
454
|
-
const dbName = config.store?.name ?? "kora-db";
|
|
455
|
-
const adapter = await createAdapter(adapterType, dbName, config.store?.workerUrl);
|
|
456
|
-
const store = new import_store.Store({
|
|
457
|
-
schema: config.schema,
|
|
458
|
-
adapter,
|
|
459
|
-
emitter
|
|
460
|
-
});
|
|
461
|
-
await store.open();
|
|
462
|
-
let syncEngine = null;
|
|
463
|
-
let unsubscribeSync = null;
|
|
464
|
-
if (config.sync) {
|
|
465
|
-
const transport = new import_sync.WebSocketTransport();
|
|
466
|
-
const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter);
|
|
467
|
-
const scopeMap = config.sync.scope ? (0, import_core.buildScopeMap)(config.schema, config.sync.scope) : void 0;
|
|
468
|
-
syncEngine = new import_sync.SyncEngine({
|
|
469
|
-
transport,
|
|
470
|
-
store: mergeAwareStore,
|
|
471
|
-
config: {
|
|
472
|
-
url: config.sync.url,
|
|
473
|
-
transport: config.sync.transport,
|
|
474
|
-
auth: config.sync.auth,
|
|
475
|
-
batchSize: config.sync.batchSize,
|
|
476
|
-
schemaVersion: config.sync.schemaVersion ?? config.schema.version,
|
|
477
|
-
scopeMap
|
|
478
|
-
},
|
|
479
|
-
emitter
|
|
480
|
-
});
|
|
481
|
-
unsubscribeSync = emitter.on("operation:created", (event) => {
|
|
482
|
-
if (syncEngine) {
|
|
483
|
-
syncEngine.pushOperation(event.operation);
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
return { store, syncEngine, unsubscribeSync };
|
|
488
|
-
}
|
|
489
|
-
function createCollectionAccessor(collectionName, getStore) {
|
|
490
|
-
return {
|
|
491
|
-
async insert(data) {
|
|
492
|
-
const currentStore = getStore();
|
|
493
|
-
if (!currentStore) {
|
|
494
|
-
throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
|
|
495
|
-
}
|
|
496
|
-
return currentStore.collection(collectionName).insert(data);
|
|
497
|
-
},
|
|
498
|
-
async findById(id) {
|
|
499
|
-
const currentStore = getStore();
|
|
500
|
-
if (!currentStore) return null;
|
|
501
|
-
return currentStore.collection(collectionName).findById(id);
|
|
502
|
-
},
|
|
503
|
-
async update(id, data) {
|
|
504
|
-
const currentStore = getStore();
|
|
505
|
-
if (!currentStore) {
|
|
506
|
-
throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
|
|
507
|
-
}
|
|
508
|
-
return currentStore.collection(collectionName).update(id, data);
|
|
509
|
-
},
|
|
510
|
-
async delete(id) {
|
|
511
|
-
const currentStore = getStore();
|
|
512
|
-
if (!currentStore) {
|
|
513
|
-
throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
|
|
514
|
-
}
|
|
515
|
-
return currentStore.collection(collectionName).delete(id);
|
|
516
|
-
},
|
|
517
|
-
where(conditions) {
|
|
518
|
-
const currentStore = getStore();
|
|
519
|
-
if (!currentStore) {
|
|
520
|
-
return createPendingQueryBuilder(conditions);
|
|
521
|
-
}
|
|
522
|
-
return currentStore.collection(collectionName).where(conditions);
|
|
523
|
-
}
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
function createPendingQueryBuilder(initialWhere) {
|
|
527
|
-
const descriptor = {
|
|
528
|
-
collection: "__pending__",
|
|
529
|
-
where: { ...initialWhere },
|
|
530
|
-
orderBy: [],
|
|
531
|
-
limit: void 0,
|
|
532
|
-
offset: void 0
|
|
533
|
-
};
|
|
534
|
-
const builder = {
|
|
535
|
-
where(conditions) {
|
|
536
|
-
descriptor.where = { ...descriptor.where, ...conditions };
|
|
537
|
-
return this;
|
|
538
|
-
},
|
|
539
|
-
orderBy(field, direction = "asc") {
|
|
540
|
-
descriptor.orderBy.push({ field, direction });
|
|
541
|
-
return this;
|
|
542
|
-
},
|
|
543
|
-
limit(n) {
|
|
544
|
-
descriptor.limit = n;
|
|
545
|
-
return this;
|
|
546
|
-
},
|
|
547
|
-
offset(n) {
|
|
548
|
-
descriptor.offset = n;
|
|
549
|
-
return this;
|
|
550
|
-
},
|
|
551
|
-
async exec() {
|
|
552
|
-
return [];
|
|
553
|
-
},
|
|
554
|
-
async count() {
|
|
555
|
-
return 0;
|
|
556
|
-
},
|
|
557
|
-
subscribe(callback) {
|
|
558
|
-
void callback([]);
|
|
559
|
-
return () => {
|
|
560
|
-
};
|
|
561
|
-
},
|
|
562
|
-
getDescriptor() {
|
|
563
|
-
return { ...descriptor, where: { ...descriptor.where }, orderBy: [...descriptor.orderBy] };
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
return builder;
|
|
567
|
-
}
|
|
568
1527
|
|
|
569
1528
|
// src/index.ts
|
|
570
|
-
var
|
|
571
|
-
var import_core3 = require("@korajs/core");
|
|
572
|
-
var import_core4 = require("@korajs/core");
|
|
573
|
-
var import_core5 = require("@korajs/core");
|
|
1529
|
+
var import_store6 = require("@korajs/store");
|
|
574
1530
|
var import_core6 = require("@korajs/core");
|
|
575
1531
|
var import_core7 = require("@korajs/core");
|
|
576
|
-
var
|
|
577
|
-
var
|
|
578
|
-
var
|
|
579
|
-
var
|
|
1532
|
+
var import_core8 = require("@korajs/core");
|
|
1533
|
+
var import_core9 = require("@korajs/core");
|
|
1534
|
+
var import_core10 = require("@korajs/core");
|
|
1535
|
+
var import_core11 = require("@korajs/core");
|
|
1536
|
+
var import_store7 = require("@korajs/store");
|
|
1537
|
+
var import_store8 = require("@korajs/store");
|
|
1538
|
+
var import_store9 = require("@korajs/store");
|
|
1539
|
+
var import_merge3 = require("@korajs/merge");
|
|
1540
|
+
var import_sync7 = require("@korajs/sync");
|
|
580
1541
|
// Annotate the CommonJS export names for ESM import in node:
|
|
581
1542
|
0 && (module.exports = {
|
|
1543
|
+
AppNotReadyError,
|
|
582
1544
|
HybridLogicalClock,
|
|
583
1545
|
KoraError,
|
|
584
1546
|
MergeEngine,
|
|
@@ -589,10 +1551,17 @@ var import_sync2 = require("@korajs/sync");
|
|
|
589
1551
|
WebSocketTransport,
|
|
590
1552
|
createApp,
|
|
591
1553
|
createOperation,
|
|
1554
|
+
decodeAuditExport,
|
|
592
1555
|
defineSchema,
|
|
1556
|
+
exportBackup,
|
|
593
1557
|
generateUUIDv7,
|
|
594
1558
|
migrate,
|
|
595
1559
|
op,
|
|
596
|
-
|
|
1560
|
+
readAuditExportManifest,
|
|
1561
|
+
readBackupManifest,
|
|
1562
|
+
restoreBackup,
|
|
1563
|
+
t,
|
|
1564
|
+
verifyAuditExportChecksum,
|
|
1565
|
+
verifyBackupChecksum
|
|
597
1566
|
});
|
|
598
1567
|
//# sourceMappingURL=index.cjs.map
|