korajs 0.4.0 → 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/index.cjs +1090 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +105 -7
- package/dist/index.d.ts +105 -7
- package/dist/index.js +450 -149
- 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,33 +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
|
-
SequenceManager: () =>
|
|
37
|
-
Store: () =>
|
|
38
|
-
SyncEngine: () =>
|
|
39
|
-
TransactionContext: () =>
|
|
40
|
-
WebSocketTransport: () =>
|
|
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,
|
|
41
41
|
createApp: () => createApp,
|
|
42
|
-
createOperation: () =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
48
55
|
});
|
|
49
56
|
module.exports = __toCommonJS(index_exports);
|
|
50
57
|
|
|
51
58
|
// src/create-app.ts
|
|
52
|
-
var
|
|
53
|
-
var
|
|
59
|
+
var import_core5 = require("@korajs/core");
|
|
60
|
+
var import_internal5 = require("@korajs/core/internal");
|
|
54
61
|
var import_devtools = require("@korajs/devtools");
|
|
55
|
-
var
|
|
56
|
-
var
|
|
57
|
-
var
|
|
62
|
+
var import_merge2 = require("@korajs/merge");
|
|
63
|
+
var import_store4 = require("@korajs/store");
|
|
64
|
+
var import_sync2 = require("@korajs/sync");
|
|
58
65
|
|
|
59
66
|
// src/adapter-resolver.ts
|
|
67
|
+
function importOptionalPeer(specifier) {
|
|
68
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
69
|
+
return dynamicImport(specifier);
|
|
70
|
+
}
|
|
60
71
|
function detectAdapterType() {
|
|
61
72
|
if (typeof globalThis !== "undefined" && typeof globalThis.__TAURI_INTERNALS__ !== "undefined") {
|
|
62
73
|
return "tauri-sqlite";
|
|
@@ -72,13 +83,10 @@ function detectAdapterType() {
|
|
|
72
83
|
}
|
|
73
84
|
return "better-sqlite3";
|
|
74
85
|
}
|
|
75
|
-
async function createAdapter(type, dbName, workerUrl) {
|
|
86
|
+
async function createAdapter(type, dbName, workerUrl, emitter, workerResponseTimeoutMs, sharedWorkerUrl) {
|
|
76
87
|
switch (type) {
|
|
77
88
|
case "tauri-sqlite": {
|
|
78
|
-
const { TauriSqliteAdapter } = await
|
|
79
|
-
/* @vite-ignore */
|
|
80
|
-
"@korajs/tauri"
|
|
81
|
-
);
|
|
89
|
+
const { TauriSqliteAdapter } = await importOptionalPeer("@korajs/tauri");
|
|
82
90
|
return new TauriSqliteAdapter({ path: `${dbName}.db` });
|
|
83
91
|
}
|
|
84
92
|
case "better-sqlite3": {
|
|
@@ -90,11 +98,11 @@ async function createAdapter(type, dbName, workerUrl) {
|
|
|
90
98
|
}
|
|
91
99
|
case "sqlite-wasm": {
|
|
92
100
|
const { SqliteWasmAdapter } = await import("@korajs/store/sqlite-wasm");
|
|
93
|
-
return new SqliteWasmAdapter({ dbName, workerUrl });
|
|
101
|
+
return new SqliteWasmAdapter({ dbName, workerUrl, sharedWorkerUrl, workerResponseTimeoutMs });
|
|
94
102
|
}
|
|
95
103
|
case "indexeddb": {
|
|
96
104
|
const { IndexedDbAdapter } = await import("@korajs/store/indexeddb");
|
|
97
|
-
return new IndexedDbAdapter({ dbName, workerUrl });
|
|
105
|
+
return new IndexedDbAdapter({ dbName, workerUrl, emitter, workerResponseTimeoutMs });
|
|
98
106
|
}
|
|
99
107
|
default: {
|
|
100
108
|
const _exhaustive = type;
|
|
@@ -103,85 +111,536 @@ async function createAdapter(type, dbName, workerUrl) {
|
|
|
103
111
|
}
|
|
104
112
|
}
|
|
105
113
|
|
|
106
|
-
// src/
|
|
107
|
-
var
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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";
|
|
112
122
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
};
|
|
118
181
|
}
|
|
119
|
-
|
|
120
|
-
|
|
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());
|
|
121
223
|
}
|
|
122
|
-
|
|
123
|
-
|
|
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);
|
|
124
229
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
230
|
+
/** Local update. */
|
|
231
|
+
async update(collection, id, data) {
|
|
232
|
+
return (0, import_internal3.executeUpdate)(this.deps.store.createMutationContext(collection), id, data);
|
|
233
|
+
}
|
|
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
|
+
}
|
|
128
319
|
}
|
|
129
|
-
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();
|
|
130
416
|
const collectionDef = schema.collections[op2.collection];
|
|
131
417
|
if (!collectionDef) {
|
|
132
|
-
return this.store.applyRemoteOperation(op2);
|
|
418
|
+
return this.deps.store.applyRemoteOperation(op2);
|
|
133
419
|
}
|
|
134
|
-
const accessor = this.store.collection(op2.collection);
|
|
420
|
+
const accessor = this.deps.store.collection(op2.collection);
|
|
135
421
|
const currentRecord = await accessor.findById(op2.recordId);
|
|
136
422
|
if (!currentRecord) {
|
|
137
|
-
|
|
423
|
+
const tombstoneResult = await this.applyRemoteUpdateOnDeletedRow(op2, collectionDef);
|
|
424
|
+
if (tombstoneResult !== null) {
|
|
425
|
+
return tombstoneResult;
|
|
426
|
+
}
|
|
427
|
+
return this.deps.store.applyRemoteOperation(op2);
|
|
138
428
|
}
|
|
139
429
|
let hasConflict = false;
|
|
140
|
-
for (const field of Object.keys(op2.data)) {
|
|
141
|
-
const
|
|
430
|
+
for (const field of Object.keys(op2.data ?? {})) {
|
|
431
|
+
const fieldDef = collectionDef.fields[field];
|
|
432
|
+
const expectedBase = op2.previousData?.[field];
|
|
142
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
|
+
}
|
|
143
440
|
if (!deepEqual(expectedBase, currentLocal)) {
|
|
144
441
|
hasConflict = true;
|
|
145
442
|
break;
|
|
146
443
|
}
|
|
147
444
|
}
|
|
148
445
|
if (!hasConflict) {
|
|
149
|
-
return this.store.applyRemoteOperation(op2);
|
|
446
|
+
return this.deps.store.applyRemoteOperation(op2);
|
|
150
447
|
}
|
|
151
|
-
this.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
155
470
|
});
|
|
156
|
-
|
|
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
|
|
482
|
+
});
|
|
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
|
+
);
|
|
157
496
|
const localOp = {
|
|
158
497
|
...op2,
|
|
159
|
-
|
|
160
|
-
data: buildLocalDiff(op2.previousData, currentRecord, Object.keys(op2.data)),
|
|
498
|
+
data: buildLocalDiff(baseState, currentRecord, Object.keys(op2.data ?? {})),
|
|
161
499
|
previousData: op2.previousData,
|
|
162
|
-
nodeId: this.store.getNodeId()
|
|
500
|
+
nodeId: this.deps.store.getNodeId(),
|
|
501
|
+
timestamp: localTimestamp
|
|
502
|
+
};
|
|
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)
|
|
163
518
|
};
|
|
164
|
-
const
|
|
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({
|
|
165
541
|
local: localOp,
|
|
166
542
|
remote: op2,
|
|
167
|
-
baseState,
|
|
543
|
+
baseState: {},
|
|
168
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)
|
|
169
551
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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?.();
|
|
173
565
|
}
|
|
174
|
-
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];
|
|
175
572
|
if (firstTrace) {
|
|
176
|
-
this.emitter?.emit({ type: "merge:completed", trace: firstTrace });
|
|
573
|
+
this.deps.emitter?.emit({ type: "merge:completed", trace: firstTrace });
|
|
177
574
|
}
|
|
178
|
-
const mergedOp = {
|
|
179
|
-
...op2,
|
|
180
|
-
data: result.mergedData
|
|
181
|
-
};
|
|
182
|
-
return this.store.applyRemoteOperation(mergedOp);
|
|
183
575
|
}
|
|
184
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
|
+
}
|
|
185
644
|
function buildLocalDiff(baseState, currentRecord, fields) {
|
|
186
645
|
const diff = {};
|
|
187
646
|
for (const field of fields) {
|
|
@@ -189,6 +648,20 @@ function buildLocalDiff(baseState, currentRecord, fields) {
|
|
|
189
648
|
}
|
|
190
649
|
return diff;
|
|
191
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
|
+
}
|
|
192
665
|
function deepEqual(a, b) {
|
|
193
666
|
if (a === b) return true;
|
|
194
667
|
if (a === null || b === null) return false;
|
|
@@ -209,30 +682,385 @@ function deepEqual(a, b) {
|
|
|
209
682
|
return false;
|
|
210
683
|
}
|
|
211
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
|
+
|
|
212
977
|
// src/create-app.ts
|
|
213
978
|
function createApp(config) {
|
|
214
|
-
|
|
215
|
-
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
|
+
}
|
|
216
1003
|
let store = null;
|
|
217
1004
|
let syncEngine = null;
|
|
218
1005
|
let unsubscribeSync = null;
|
|
1006
|
+
let unsubscribeAudit = null;
|
|
219
1007
|
let reconnectionManager = null;
|
|
220
1008
|
let connectionMonitor = null;
|
|
221
1009
|
let instrumenter = null;
|
|
222
1010
|
let intentionalDisconnect = false;
|
|
223
1011
|
let qualityInterval = null;
|
|
1012
|
+
let syncStatusBridge = null;
|
|
1013
|
+
let destroyDevtoolsOverlay = null;
|
|
224
1014
|
if (config.devtools) {
|
|
225
1015
|
instrumenter = new import_devtools.Instrumenter(emitter, {
|
|
226
1016
|
bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
|
|
227
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
|
+
}
|
|
228
1026
|
}
|
|
229
1027
|
const ready = initializeAsync(config, emitter, mergeEngine).then((result) => {
|
|
230
1028
|
store = result.store;
|
|
231
1029
|
syncEngine = result.syncEngine;
|
|
232
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
|
+
}
|
|
233
1061
|
if (config.sync && syncEngine) {
|
|
234
|
-
connectionMonitor = new
|
|
235
|
-
reconnectionManager = new
|
|
1062
|
+
connectionMonitor = new import_sync2.ConnectionMonitor();
|
|
1063
|
+
reconnectionManager = new import_sync2.ReconnectionManager({
|
|
236
1064
|
initialDelay: config.sync.reconnectInterval,
|
|
237
1065
|
maxDelay: config.sync.maxReconnectInterval
|
|
238
1066
|
});
|
|
@@ -254,10 +1082,24 @@ function createApp(config) {
|
|
|
254
1082
|
qualityInterval = null;
|
|
255
1083
|
}
|
|
256
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
|
+
});
|
|
257
1099
|
if (config.sync.autoReconnect !== false) {
|
|
258
1100
|
const engine = syncEngine;
|
|
259
1101
|
emitter.on("sync:disconnected", () => {
|
|
260
|
-
if (intentionalDisconnect) return;
|
|
1102
|
+
if (intentionalDisconnect || engine.isSchemaBlocked()) return;
|
|
261
1103
|
if (reconnectionManager?.isRunning()) return;
|
|
262
1104
|
engine.setReconnecting(true);
|
|
263
1105
|
reconnectionManager?.stop();
|
|
@@ -274,9 +1116,32 @@ function createApp(config) {
|
|
|
274
1116
|
});
|
|
275
1117
|
});
|
|
276
1118
|
}
|
|
1119
|
+
if (config.sync.autoConnect === true && syncEngine) {
|
|
1120
|
+
void syncEngine.start().catch(() => {
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
277
1123
|
}
|
|
278
1124
|
});
|
|
1125
|
+
const offlineSyncStatus = () => ({
|
|
1126
|
+
status: "offline",
|
|
1127
|
+
pendingOperations: 0,
|
|
1128
|
+
lastSyncedAt: null,
|
|
1129
|
+
lastSuccessfulPush: null,
|
|
1130
|
+
lastSuccessfulPull: null,
|
|
1131
|
+
conflicts: 0
|
|
1132
|
+
});
|
|
279
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
|
+
},
|
|
280
1145
|
async connect() {
|
|
281
1146
|
await ready;
|
|
282
1147
|
if (syncEngine) {
|
|
@@ -284,6 +1149,7 @@ function createApp(config) {
|
|
|
284
1149
|
reconnectionManager?.stop();
|
|
285
1150
|
reconnectionManager?.reset();
|
|
286
1151
|
await syncEngine.start();
|
|
1152
|
+
syncStatusBridge?.refresh();
|
|
287
1153
|
}
|
|
288
1154
|
},
|
|
289
1155
|
async disconnect() {
|
|
@@ -292,20 +1158,14 @@ function createApp(config) {
|
|
|
292
1158
|
intentionalDisconnect = true;
|
|
293
1159
|
reconnectionManager?.stop();
|
|
294
1160
|
await syncEngine.stop();
|
|
1161
|
+
syncStatusBridge?.refresh();
|
|
295
1162
|
}
|
|
296
1163
|
},
|
|
297
1164
|
getStatus() {
|
|
298
1165
|
if (syncEngine) {
|
|
299
1166
|
return syncEngine.getStatus();
|
|
300
1167
|
}
|
|
301
|
-
return
|
|
302
|
-
status: "offline",
|
|
303
|
-
pendingOperations: 0,
|
|
304
|
-
lastSyncedAt: null,
|
|
305
|
-
lastSuccessfulPush: null,
|
|
306
|
-
lastSuccessfulPull: null,
|
|
307
|
-
conflicts: 0
|
|
308
|
-
};
|
|
1168
|
+
return offlineSyncStatus();
|
|
309
1169
|
},
|
|
310
1170
|
async retryNow() {
|
|
311
1171
|
await ready;
|
|
@@ -313,6 +1173,9 @@ function createApp(config) {
|
|
|
313
1173
|
await syncEngine.retryNow();
|
|
314
1174
|
}
|
|
315
1175
|
},
|
|
1176
|
+
clearSchemaBlock() {
|
|
1177
|
+
syncEngine?.clearSchemaBlock();
|
|
1178
|
+
},
|
|
316
1179
|
exportDiagnostics() {
|
|
317
1180
|
if (syncEngine) {
|
|
318
1181
|
return syncEngine.exportDiagnostics();
|
|
@@ -346,33 +1209,23 @@ function createApp(config) {
|
|
|
346
1209
|
if (!store) {
|
|
347
1210
|
throw new Error("Store not initialized. Await app.ready before using transactions.");
|
|
348
1211
|
}
|
|
349
|
-
const txContext = store.createTransaction();
|
|
350
|
-
if (mutationName !== void 0) {
|
|
351
|
-
txContext.setMutationName(mutationName);
|
|
352
|
-
}
|
|
353
1212
|
const collectionNames = Object.keys(config.schema.collections);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
get() {
|
|
358
|
-
return txContext.collection(name);
|
|
359
|
-
},
|
|
360
|
-
enumerable: true,
|
|
361
|
-
configurable: false
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
try {
|
|
365
|
-
await fn(proxy);
|
|
366
|
-
const { operations } = await txContext.commit();
|
|
367
|
-
for (const op2 of operations) {
|
|
368
|
-
store.getSubscriptionManager().notify(op2.collection, op2);
|
|
369
|
-
emitter.emit({ type: "operation:created", operation: op2 });
|
|
1213
|
+
return store.transaction(async (tx) => {
|
|
1214
|
+
if (mutationName !== void 0) {
|
|
1215
|
+
tx.setMutationName(mutationName);
|
|
370
1216
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
+
});
|
|
376
1229
|
}
|
|
377
1230
|
const sequences = {
|
|
378
1231
|
async next(name, config2) {
|
|
@@ -419,6 +1272,10 @@ function createApp(config) {
|
|
|
419
1272
|
qualityInterval = null;
|
|
420
1273
|
}
|
|
421
1274
|
reconnectionManager?.stop();
|
|
1275
|
+
if (destroyDevtoolsOverlay) {
|
|
1276
|
+
destroyDevtoolsOverlay();
|
|
1277
|
+
destroyDevtoolsOverlay = null;
|
|
1278
|
+
}
|
|
422
1279
|
if (instrumenter) {
|
|
423
1280
|
instrumenter.destroy();
|
|
424
1281
|
instrumenter = null;
|
|
@@ -427,15 +1284,51 @@ function createApp(config) {
|
|
|
427
1284
|
unsubscribeSync();
|
|
428
1285
|
unsubscribeSync = null;
|
|
429
1286
|
}
|
|
1287
|
+
if (unsubscribeAudit) {
|
|
1288
|
+
unsubscribeAudit();
|
|
1289
|
+
unsubscribeAudit = null;
|
|
1290
|
+
}
|
|
430
1291
|
if (syncEngine) {
|
|
431
1292
|
await syncEngine.stop();
|
|
432
1293
|
syncEngine = null;
|
|
433
1294
|
}
|
|
1295
|
+
if (syncStatusBridge) {
|
|
1296
|
+
syncStatusBridge.destroy();
|
|
1297
|
+
syncStatusBridge = null;
|
|
1298
|
+
}
|
|
434
1299
|
if (store) {
|
|
435
1300
|
await store.close();
|
|
436
1301
|
store = null;
|
|
437
1302
|
}
|
|
438
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);
|
|
439
1332
|
}
|
|
440
1333
|
};
|
|
441
1334
|
for (const collectionName of Object.keys(config.schema.collections)) {
|
|
@@ -452,39 +1345,87 @@ function createApp(config) {
|
|
|
452
1345
|
async function initializeAsync(config, emitter, mergeEngine) {
|
|
453
1346
|
const adapterType = config.store?.adapter ?? detectAdapterType();
|
|
454
1347
|
const dbName = config.store?.name ?? "kora-db";
|
|
455
|
-
const adapter = await createAdapter(
|
|
456
|
-
|
|
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({
|
|
457
1360
|
schema: config.schema,
|
|
458
1361
|
adapter,
|
|
459
|
-
emitter
|
|
1362
|
+
emitter,
|
|
1363
|
+
dbName,
|
|
1364
|
+
nodeId: authNodeId,
|
|
1365
|
+
isolation: authNodeId ? "shared" : config.store?.isolation,
|
|
1366
|
+
...config.sync ? { onQuerySubscribed: createSyncQuerySubscriptionHook(() => syncEngine) } : {}
|
|
460
1367
|
});
|
|
461
1368
|
await store.open();
|
|
462
|
-
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);
|
|
463
1378
|
let unsubscribeSync = null;
|
|
464
1379
|
if (config.sync) {
|
|
465
|
-
const transport =
|
|
466
|
-
const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter
|
|
467
|
-
|
|
468
|
-
|
|
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({
|
|
469
1391
|
transport,
|
|
470
1392
|
store: mergeAwareStore,
|
|
471
1393
|
config: {
|
|
472
1394
|
url: config.sync.url,
|
|
473
1395
|
transport: config.sync.transport,
|
|
474
|
-
auth:
|
|
1396
|
+
auth: syncAuth,
|
|
475
1397
|
batchSize: config.sync.batchSize,
|
|
476
1398
|
schemaVersion: config.sync.schemaVersion ?? config.schema.version,
|
|
477
|
-
scopeMap
|
|
1399
|
+
scopeMap,
|
|
1400
|
+
encryption: config.sync.encryption,
|
|
1401
|
+
strictHandshake: config.sync.strictHandshake,
|
|
1402
|
+
operationTransforms: config.sync.operationTransforms
|
|
478
1403
|
},
|
|
479
|
-
emitter
|
|
1404
|
+
emitter,
|
|
1405
|
+
queueStorage: new StoreQueueStorage(adapter),
|
|
1406
|
+
syncState: new StoreSyncStatePersistence(store, scopeMap),
|
|
1407
|
+
encryptor
|
|
480
1408
|
});
|
|
1409
|
+
recordConflict = () => syncEngine?.recordConflict();
|
|
481
1410
|
unsubscribeSync = emitter.on("operation:created", (event) => {
|
|
482
1411
|
if (syncEngine) {
|
|
483
1412
|
syncEngine.pushOperation(event.operation);
|
|
484
1413
|
}
|
|
485
1414
|
});
|
|
486
1415
|
}
|
|
487
|
-
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();
|
|
488
1429
|
}
|
|
489
1430
|
function createCollectionAccessor(collectionName, getStore) {
|
|
490
1431
|
return {
|
|
@@ -549,15 +1490,19 @@ function createPendingQueryBuilder(initialWhere) {
|
|
|
549
1490
|
return this;
|
|
550
1491
|
},
|
|
551
1492
|
async exec() {
|
|
552
|
-
|
|
1493
|
+
throw new AppNotReadyError(
|
|
1494
|
+
"Cannot execute a query before app.ready. Await app.ready or use <KoraProvider app={app}>."
|
|
1495
|
+
);
|
|
553
1496
|
},
|
|
554
1497
|
async count() {
|
|
555
|
-
|
|
1498
|
+
throw new AppNotReadyError(
|
|
1499
|
+
"Cannot count query results before app.ready. Await app.ready or use <KoraProvider app={app}>."
|
|
1500
|
+
);
|
|
556
1501
|
},
|
|
557
|
-
subscribe(
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
+
);
|
|
561
1506
|
},
|
|
562
1507
|
getDescriptor() {
|
|
563
1508
|
return { ...descriptor, where: { ...descriptor.where }, orderBy: [...descriptor.orderBy] };
|
|
@@ -567,16 +1512,18 @@ function createPendingQueryBuilder(initialWhere) {
|
|
|
567
1512
|
}
|
|
568
1513
|
|
|
569
1514
|
// 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");
|
|
1515
|
+
var import_store5 = require("@korajs/store");
|
|
574
1516
|
var import_core6 = require("@korajs/core");
|
|
575
1517
|
var import_core7 = require("@korajs/core");
|
|
576
|
-
var
|
|
577
|
-
var
|
|
578
|
-
var
|
|
579
|
-
var
|
|
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");
|
|
580
1527
|
// Annotate the CommonJS export names for ESM import in node:
|
|
581
1528
|
0 && (module.exports = {
|
|
582
1529
|
HybridLogicalClock,
|
|
@@ -589,10 +1536,17 @@ var import_sync2 = require("@korajs/sync");
|
|
|
589
1536
|
WebSocketTransport,
|
|
590
1537
|
createApp,
|
|
591
1538
|
createOperation,
|
|
1539
|
+
decodeAuditExport,
|
|
592
1540
|
defineSchema,
|
|
1541
|
+
exportBackup,
|
|
593
1542
|
generateUUIDv7,
|
|
594
1543
|
migrate,
|
|
595
1544
|
op,
|
|
596
|
-
|
|
1545
|
+
readAuditExportManifest,
|
|
1546
|
+
readBackupManifest,
|
|
1547
|
+
restoreBackup,
|
|
1548
|
+
t,
|
|
1549
|
+
verifyAuditExportChecksum,
|
|
1550
|
+
verifyBackupChecksum
|
|
597
1551
|
});
|
|
598
1552
|
//# sourceMappingURL=index.cjs.map
|