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