korajs 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -17
- package/dist/index.cjs +554 -539
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -52
- package/dist/index.d.ts +9 -52
- package/dist/index.js +538 -528
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +47 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +3 -0
- package/dist/react.d.ts +3 -0
- package/dist/react.js +24 -0
- package/dist/react.js.map +1 -0
- package/dist/svelte.cjs +71 -0
- package/dist/svelte.cjs.map +1 -0
- package/dist/svelte.d.cts +1 -0
- package/dist/svelte.d.ts +1 -0
- package/dist/svelte.js +48 -0
- package/dist/svelte.js.map +1 -0
- package/dist/vue.cjs +57 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.d.cts +1 -0
- package/dist/vue.d.ts +1 -0
- package/dist/vue.js +34 -0
- package/dist/vue.js.map +1 -0
- package/package.json +71 -7
package/dist/index.cjs
CHANGED
|
@@ -30,38 +30,84 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AppNotReadyError: () => import_core10.AppNotReadyError,
|
|
33
34
|
HybridLogicalClock: () => import_core7.HybridLogicalClock,
|
|
34
35
|
KoraError: () => import_core10.KoraError,
|
|
35
36
|
MergeEngine: () => import_merge3.MergeEngine,
|
|
36
|
-
SequenceManager: () =>
|
|
37
|
-
Store: () =>
|
|
38
|
-
SyncEngine: () =>
|
|
39
|
-
TransactionContext: () =>
|
|
40
|
-
WebSocketTransport: () =>
|
|
37
|
+
SequenceManager: () => import_store7.SequenceManager,
|
|
38
|
+
Store: () => import_store7.Store,
|
|
39
|
+
SyncEngine: () => import_sync7.SyncEngine,
|
|
40
|
+
TransactionContext: () => import_store8.TransactionContext,
|
|
41
|
+
WebSocketTransport: () => import_sync7.WebSocketTransport,
|
|
41
42
|
createApp: () => createApp,
|
|
42
43
|
createOperation: () => import_core9.createOperation,
|
|
43
|
-
decodeAuditExport: () =>
|
|
44
|
+
decodeAuditExport: () => import_store6.decodeAuditExport,
|
|
44
45
|
defineSchema: () => import_core6.defineSchema,
|
|
45
|
-
exportBackup: () =>
|
|
46
|
+
exportBackup: () => import_store9.exportBackup,
|
|
46
47
|
generateUUIDv7: () => import_core8.generateUUIDv7,
|
|
47
48
|
migrate: () => import_core6.migrate,
|
|
48
49
|
op: () => import_core11.op,
|
|
49
|
-
readAuditExportManifest: () =>
|
|
50
|
-
readBackupManifest: () =>
|
|
51
|
-
restoreBackup: () =>
|
|
50
|
+
readAuditExportManifest: () => import_store6.readAuditExportManifest,
|
|
51
|
+
readBackupManifest: () => import_store9.readBackupManifest,
|
|
52
|
+
restoreBackup: () => import_store9.restoreBackup,
|
|
52
53
|
t: () => import_core6.t,
|
|
53
|
-
verifyAuditExportChecksum: () =>
|
|
54
|
-
verifyBackupChecksum: () =>
|
|
54
|
+
verifyAuditExportChecksum: () => import_store6.verifyAuditExportChecksum,
|
|
55
|
+
verifyBackupChecksum: () => import_store9.verifyBackupChecksum
|
|
55
56
|
});
|
|
56
57
|
module.exports = __toCommonJS(index_exports);
|
|
57
58
|
|
|
58
59
|
// src/create-app.ts
|
|
59
|
-
var import_core5 = require("@korajs/core");
|
|
60
60
|
var import_internal5 = require("@korajs/core/internal");
|
|
61
|
-
var import_devtools = require("@korajs/devtools");
|
|
62
61
|
var import_merge2 = require("@korajs/merge");
|
|
62
|
+
var import_store5 = require("@korajs/store");
|
|
63
|
+
|
|
64
|
+
// src/collection-accessor.ts
|
|
65
|
+
var import_core = require("@korajs/core");
|
|
66
|
+
function createCollectionAccessor(collectionName, getStore) {
|
|
67
|
+
const notReady = (action) => new import_core.AppNotReadyError(
|
|
68
|
+
`Cannot ${action} on collection "${collectionName}" before app.ready. Await app.ready or use <KoraProvider app={app}>.`
|
|
69
|
+
);
|
|
70
|
+
return {
|
|
71
|
+
async insert(data) {
|
|
72
|
+
const currentStore = getStore();
|
|
73
|
+
if (!currentStore) {
|
|
74
|
+
throw notReady("insert into");
|
|
75
|
+
}
|
|
76
|
+
return currentStore.collection(collectionName).insert(data);
|
|
77
|
+
},
|
|
78
|
+
async findById(id) {
|
|
79
|
+
const currentStore = getStore();
|
|
80
|
+
if (!currentStore) return null;
|
|
81
|
+
return currentStore.collection(collectionName).findById(id);
|
|
82
|
+
},
|
|
83
|
+
async update(id, data) {
|
|
84
|
+
const currentStore = getStore();
|
|
85
|
+
if (!currentStore) {
|
|
86
|
+
throw notReady("update");
|
|
87
|
+
}
|
|
88
|
+
return currentStore.collection(collectionName).update(id, data);
|
|
89
|
+
},
|
|
90
|
+
async delete(id) {
|
|
91
|
+
const currentStore = getStore();
|
|
92
|
+
if (!currentStore) {
|
|
93
|
+
throw notReady("delete from");
|
|
94
|
+
}
|
|
95
|
+
return currentStore.collection(collectionName).delete(id);
|
|
96
|
+
},
|
|
97
|
+
where(conditions) {
|
|
98
|
+
const currentStore = getStore();
|
|
99
|
+
if (!currentStore) {
|
|
100
|
+
throw notReady("query");
|
|
101
|
+
}
|
|
102
|
+
return currentStore.collection(collectionName).where(conditions);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/initialize-app.ts
|
|
108
|
+
var import_core4 = require("@korajs/core");
|
|
63
109
|
var import_store4 = require("@korajs/store");
|
|
64
|
-
var
|
|
110
|
+
var import_sync3 = require("@korajs/sync");
|
|
65
111
|
|
|
66
112
|
// src/adapter-resolver.ts
|
|
67
113
|
function importOptionalPeer(specifier) {
|
|
@@ -111,17 +157,6 @@ async function createAdapter(type, dbName, workerUrl, emitter, workerResponseTim
|
|
|
111
157
|
}
|
|
112
158
|
}
|
|
113
159
|
|
|
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";
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
|
|
125
160
|
// src/apply-pipeline.ts
|
|
126
161
|
var import_core3 = require("@korajs/core");
|
|
127
162
|
var import_internal2 = require("@korajs/core/internal");
|
|
@@ -706,6 +741,15 @@ function wireAuditPersistence(store, emitter) {
|
|
|
706
741
|
};
|
|
707
742
|
}
|
|
708
743
|
|
|
744
|
+
// src/create-sync-transport.ts
|
|
745
|
+
var import_sync = require("@korajs/sync");
|
|
746
|
+
function createSyncTransport(sync) {
|
|
747
|
+
if (sync.transport === "http") {
|
|
748
|
+
return new import_sync.HttpLongPollingTransport();
|
|
749
|
+
}
|
|
750
|
+
return new import_sync.WebSocketTransport();
|
|
751
|
+
}
|
|
752
|
+
|
|
709
753
|
// src/merge-aware-sync-store.ts
|
|
710
754
|
var MergeAwareSyncStore = class {
|
|
711
755
|
constructor(store, mergeEngine, emitter, options) {
|
|
@@ -777,7 +821,7 @@ function operationFromQueuePayload(payload) {
|
|
|
777
821
|
|
|
778
822
|
// src/store-sync-state.ts
|
|
779
823
|
var import_store3 = require("@korajs/store");
|
|
780
|
-
var
|
|
824
|
+
var import_sync2 = require("@korajs/sync");
|
|
781
825
|
var StoreSyncStatePersistence = class {
|
|
782
826
|
constructor(store, scope) {
|
|
783
827
|
this.store = store;
|
|
@@ -800,19 +844,20 @@ var StoreSyncStatePersistence = class {
|
|
|
800
844
|
}
|
|
801
845
|
async getUnsyncedOperations(serverVector) {
|
|
802
846
|
const ops = await this.store.getUnsyncedOperations(serverVector);
|
|
803
|
-
return ops.filter((op2) => (0,
|
|
847
|
+
return ops.filter((op2) => (0, import_sync2.operationMatchesScope)(op2, this.scope));
|
|
804
848
|
}
|
|
805
849
|
loadDeltaCursor() {
|
|
806
|
-
return this.store.loadDeltaCursor().then((encoded) => (0,
|
|
850
|
+
return this.store.loadDeltaCursor().then((encoded) => (0, import_sync2.decodeDeltaCursor)(encoded));
|
|
807
851
|
}
|
|
808
852
|
async saveDeltaCursor(cursor) {
|
|
809
|
-
await this.store.saveDeltaCursor(cursor ? (0,
|
|
853
|
+
await this.store.saveDeltaCursor(cursor ? (0, import_sync2.encodeDeltaCursor)(cursor) : null);
|
|
810
854
|
}
|
|
811
855
|
};
|
|
812
856
|
|
|
813
857
|
// src/sync-query-bridge.ts
|
|
814
858
|
function queryDescriptorToSyncSubset(descriptor) {
|
|
815
859
|
const where = {};
|
|
860
|
+
const skippedFields = [];
|
|
816
861
|
for (const [field, value] of Object.entries(descriptor.where)) {
|
|
817
862
|
if (value === null || value === void 0) {
|
|
818
863
|
where[field] = value;
|
|
@@ -820,7 +865,14 @@ function queryDescriptorToSyncSubset(descriptor) {
|
|
|
820
865
|
}
|
|
821
866
|
if (typeof value !== "object" || Array.isArray(value)) {
|
|
822
867
|
where[field] = value;
|
|
868
|
+
continue;
|
|
823
869
|
}
|
|
870
|
+
skippedFields.push(field);
|
|
871
|
+
}
|
|
872
|
+
if (skippedFields.length > 0 && typeof console !== "undefined") {
|
|
873
|
+
console.warn(
|
|
874
|
+
`[Kora] Sync query subset omitted non-equality filters on ${descriptor.collection}: ${skippedFields.join(", ")}. Only plain equality WHERE clauses are registered for incremental sync.`
|
|
875
|
+
);
|
|
824
876
|
}
|
|
825
877
|
if (Object.keys(where).length === 0) {
|
|
826
878
|
return null;
|
|
@@ -846,297 +898,148 @@ function createSyncQuerySubscriptionHook(getSyncEngine) {
|
|
|
846
898
|
};
|
|
847
899
|
}
|
|
848
900
|
|
|
849
|
-
// src/
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
901
|
+
// src/initialize-app.ts
|
|
902
|
+
async function initializeApp(config, emitter, mergeEngine) {
|
|
903
|
+
const adapterType = config.store?.adapter ?? detectAdapterType();
|
|
904
|
+
const dbName = config.store?.name ?? "kora-db";
|
|
905
|
+
const adapter = await createAdapter(
|
|
906
|
+
adapterType,
|
|
907
|
+
dbName,
|
|
908
|
+
config.store?.workerUrl,
|
|
909
|
+
emitter,
|
|
910
|
+
config.store?.workerResponseTimeoutMs,
|
|
911
|
+
config.store?.sharedWorkerUrl
|
|
912
|
+
);
|
|
913
|
+
const authBinding = config.sync?.authClient ?? null;
|
|
914
|
+
const authNodeId = authBinding?.resolveNodeId ? await authBinding.resolveNodeId() : void 0;
|
|
915
|
+
let syncEngine = null;
|
|
916
|
+
const store = new import_store4.Store({
|
|
917
|
+
schema: config.schema,
|
|
918
|
+
adapter,
|
|
919
|
+
emitter,
|
|
920
|
+
dbName,
|
|
921
|
+
nodeId: authNodeId,
|
|
922
|
+
isolation: authNodeId ? "shared" : config.store?.isolation,
|
|
923
|
+
...config.sync ? { onQuerySubscribed: createSyncQuerySubscriptionHook(() => syncEngine) } : {}
|
|
924
|
+
});
|
|
925
|
+
await store.open();
|
|
926
|
+
let recordConflict;
|
|
927
|
+
const applyPipeline = new ApplyPipeline({
|
|
928
|
+
store,
|
|
929
|
+
mergeEngine,
|
|
930
|
+
emitter,
|
|
931
|
+
onMergeConflict: () => recordConflict?.()
|
|
932
|
+
});
|
|
933
|
+
store.setLocalMutationHandler(applyPipeline);
|
|
934
|
+
const unsubscribeAudit = wireAuditPersistence(store, emitter);
|
|
935
|
+
let unsubscribeSync = null;
|
|
936
|
+
if (config.sync) {
|
|
937
|
+
const transport = createSyncTransport(config.sync);
|
|
938
|
+
const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter, {
|
|
939
|
+
onMergeConflict: () => recordConflict?.()
|
|
940
|
+
});
|
|
941
|
+
let scopeMap = config.sync.scope ? (0, import_core4.buildScopeMap)(config.schema, config.sync.scope) : void 0;
|
|
942
|
+
if (authBinding?.resolveScopeMap) {
|
|
943
|
+
scopeMap = await authBinding.resolveScopeMap() ?? scopeMap;
|
|
880
944
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
945
|
+
const syncAuth = authBinding?.auth ?? config.sync.auth;
|
|
946
|
+
const encryptor = config.sync.encryption?.enabled === true ? await import_sync3.SyncEncryptor.create(config.sync.encryption) : void 0;
|
|
947
|
+
syncEngine = new import_sync3.SyncEngine({
|
|
948
|
+
transport,
|
|
949
|
+
store: mergeAwareStore,
|
|
950
|
+
config: {
|
|
951
|
+
url: config.sync.url,
|
|
952
|
+
transport: config.sync.transport,
|
|
953
|
+
auth: syncAuth,
|
|
954
|
+
batchSize: config.sync.batchSize,
|
|
955
|
+
schemaVersion: config.sync.schemaVersion ?? config.schema.version,
|
|
956
|
+
scopeMap,
|
|
957
|
+
encryption: config.sync.encryption,
|
|
958
|
+
strictHandshake: config.sync.strictHandshake,
|
|
959
|
+
operationTransforms: config.sync.operationTransforms
|
|
960
|
+
},
|
|
961
|
+
emitter,
|
|
962
|
+
queueStorage: new StoreQueueStorage(adapter),
|
|
963
|
+
syncState: new StoreSyncStatePersistence(store, scopeMap),
|
|
964
|
+
encryptor
|
|
965
|
+
});
|
|
966
|
+
recordConflict = () => syncEngine?.recordConflict();
|
|
967
|
+
unsubscribeSync = emitter.on("operation:created", (event) => {
|
|
968
|
+
if (syncEngine) {
|
|
969
|
+
syncEngine.pushOperation(event.operation);
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
store,
|
|
975
|
+
syncEngine,
|
|
976
|
+
unsubscribeSync,
|
|
977
|
+
unsubscribeAudit,
|
|
978
|
+
authBinding
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// src/sequences-accessor.ts
|
|
983
|
+
function createSequencesAccessor(ready, getStore) {
|
|
984
|
+
const requireStore = async () => {
|
|
985
|
+
await ready;
|
|
986
|
+
const store = getStore();
|
|
987
|
+
if (!store) {
|
|
988
|
+
throw new Error("Store not initialized. Await app.ready before using sequences.");
|
|
884
989
|
}
|
|
990
|
+
return store;
|
|
885
991
|
};
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
const bridge = {
|
|
891
|
-
get status() {
|
|
892
|
-
return currentStatus;
|
|
992
|
+
return {
|
|
993
|
+
async next(name, config) {
|
|
994
|
+
const store = await requireStore();
|
|
995
|
+
return store.getSequenceManager().next(name, config);
|
|
893
996
|
},
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
return () => {
|
|
898
|
-
listeners.delete(listener);
|
|
899
|
-
};
|
|
997
|
+
async current(name, config) {
|
|
998
|
+
const store = await requireStore();
|
|
999
|
+
return store.getSequenceManager().current(name, config);
|
|
900
1000
|
},
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
unsub();
|
|
905
|
-
}
|
|
906
|
-
unsubs.length = 0;
|
|
907
|
-
listeners.clear();
|
|
1001
|
+
async reset(name, config) {
|
|
1002
|
+
const store = await requireStore();
|
|
1003
|
+
return store.getSequenceManager().reset(name, config);
|
|
908
1004
|
}
|
|
909
1005
|
};
|
|
910
|
-
refresh();
|
|
911
|
-
return bridge;
|
|
912
1006
|
}
|
|
913
1007
|
|
|
914
|
-
// src/
|
|
915
|
-
var
|
|
916
|
-
function
|
|
917
|
-
if (!config.
|
|
918
|
-
|
|
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
|
-
);
|
|
1008
|
+
// src/setup-devtools.ts
|
|
1009
|
+
var import_devtools = require("@korajs/devtools");
|
|
1010
|
+
function setupDevtools(config, emitter) {
|
|
1011
|
+
if (!config.devtools) {
|
|
1012
|
+
return { instrumenter: null, destroyOverlay: null };
|
|
947
1013
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1014
|
+
const instrumenter = new import_devtools.Instrumenter(emitter, {
|
|
1015
|
+
bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
|
|
1016
|
+
});
|
|
1017
|
+
let destroyOverlay = null;
|
|
1018
|
+
if (typeof globalThis !== "undefined" && "window" in globalThis) {
|
|
1019
|
+
void import("@korajs/devtools/overlay").then(({ mountKoraDevtoolsOverlay }) => {
|
|
1020
|
+
destroyOverlay = mountKoraDevtoolsOverlay(instrumenter);
|
|
1021
|
+
}).catch(() => {
|
|
953
1022
|
});
|
|
954
1023
|
}
|
|
955
|
-
|
|
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
|
-
}
|
|
1024
|
+
return { instrumenter, destroyOverlay: () => destroyOverlay?.() };
|
|
975
1025
|
}
|
|
976
1026
|
|
|
977
|
-
// src/
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
}
|
|
1003
|
-
let store = null;
|
|
1004
|
-
let syncEngine = null;
|
|
1005
|
-
let unsubscribeSync = null;
|
|
1006
|
-
let unsubscribeAudit = null;
|
|
1007
|
-
let reconnectionManager = null;
|
|
1008
|
-
let connectionMonitor = null;
|
|
1009
|
-
let instrumenter = null;
|
|
1010
|
-
let intentionalDisconnect = false;
|
|
1011
|
-
let qualityInterval = null;
|
|
1012
|
-
let syncStatusBridge = null;
|
|
1013
|
-
let destroyDevtoolsOverlay = null;
|
|
1014
|
-
if (config.devtools) {
|
|
1015
|
-
instrumenter = new import_devtools.Instrumenter(emitter, {
|
|
1016
|
-
bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
|
|
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
|
-
}
|
|
1027
|
+
// src/sync-control.ts
|
|
1028
|
+
var import_sync4 = require("@korajs/sync");
|
|
1029
|
+
function createSyncControl(options) {
|
|
1030
|
+
const { config, ready, state } = options;
|
|
1031
|
+
if (!config.sync) {
|
|
1032
|
+
return null;
|
|
1026
1033
|
}
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
-
}
|
|
1061
|
-
if (config.sync && syncEngine) {
|
|
1062
|
-
connectionMonitor = new import_sync2.ConnectionMonitor();
|
|
1063
|
-
reconnectionManager = new import_sync2.ReconnectionManager({
|
|
1064
|
-
initialDelay: config.sync.reconnectInterval,
|
|
1065
|
-
maxDelay: config.sync.maxReconnectInterval
|
|
1066
|
-
});
|
|
1067
|
-
emitter.on("sync:sent", () => connectionMonitor?.recordActivity());
|
|
1068
|
-
emitter.on("sync:received", () => connectionMonitor?.recordActivity());
|
|
1069
|
-
emitter.on("sync:acknowledged", () => connectionMonitor?.recordActivity());
|
|
1070
|
-
emitter.on("sync:connected", () => {
|
|
1071
|
-
if (qualityInterval !== null) clearInterval(qualityInterval);
|
|
1072
|
-
qualityInterval = setInterval(() => {
|
|
1073
|
-
if (connectionMonitor) {
|
|
1074
|
-
emitter.emit({ type: "connection:quality", quality: connectionMonitor.getQuality() });
|
|
1075
|
-
}
|
|
1076
|
-
}, 5e3);
|
|
1077
|
-
});
|
|
1078
|
-
emitter.on("sync:disconnected", () => {
|
|
1079
|
-
connectionMonitor?.reset();
|
|
1080
|
-
if (qualityInterval !== null) {
|
|
1081
|
-
clearInterval(qualityInterval);
|
|
1082
|
-
qualityInterval = null;
|
|
1083
|
-
}
|
|
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
|
-
});
|
|
1099
|
-
if (config.sync.autoReconnect !== false) {
|
|
1100
|
-
const engine = syncEngine;
|
|
1101
|
-
emitter.on("sync:disconnected", () => {
|
|
1102
|
-
if (intentionalDisconnect || engine.isSchemaBlocked()) return;
|
|
1103
|
-
if (reconnectionManager?.isRunning()) return;
|
|
1104
|
-
engine.setReconnecting(true);
|
|
1105
|
-
reconnectionManager?.stop();
|
|
1106
|
-
reconnectionManager?.start(async () => {
|
|
1107
|
-
try {
|
|
1108
|
-
await engine.start();
|
|
1109
|
-
engine.setReconnecting(false);
|
|
1110
|
-
return true;
|
|
1111
|
-
} catch {
|
|
1112
|
-
return false;
|
|
1113
|
-
}
|
|
1114
|
-
}).then(() => {
|
|
1115
|
-
engine.setReconnecting(false);
|
|
1116
|
-
});
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
if (config.sync.autoConnect === true && syncEngine) {
|
|
1120
|
-
void syncEngine.start().catch(() => {
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
});
|
|
1125
|
-
const offlineSyncStatus = () => ({
|
|
1126
|
-
status: "offline",
|
|
1127
|
-
pendingOperations: 0,
|
|
1128
|
-
lastSyncedAt: null,
|
|
1129
|
-
lastSuccessfulPush: null,
|
|
1130
|
-
lastSuccessfulPull: null,
|
|
1131
|
-
conflicts: 0
|
|
1132
|
-
});
|
|
1133
|
-
const syncControl = config.sync ? {
|
|
1034
|
+
const offlineSyncStatus = () => import_sync4.OFFLINE_SYNC_STATUS;
|
|
1035
|
+
const bridgeStatus = () => state.syncStatusBridge?.status ?? offlineSyncStatus();
|
|
1036
|
+
return {
|
|
1134
1037
|
get status() {
|
|
1135
|
-
return
|
|
1038
|
+
return bridgeStatus();
|
|
1136
1039
|
},
|
|
1137
1040
|
subscribeStatus(listener) {
|
|
1138
|
-
if (syncStatusBridge) {
|
|
1139
|
-
return syncStatusBridge.subscribe(listener);
|
|
1041
|
+
if (state.syncStatusBridge) {
|
|
1042
|
+
return state.syncStatusBridge.subscribe(listener);
|
|
1140
1043
|
}
|
|
1141
1044
|
listener(offlineSyncStatus());
|
|
1142
1045
|
return () => {
|
|
@@ -1144,41 +1047,41 @@ function createApp(config) {
|
|
|
1144
1047
|
},
|
|
1145
1048
|
async connect() {
|
|
1146
1049
|
await ready;
|
|
1147
|
-
if (syncEngine) {
|
|
1148
|
-
intentionalDisconnect = false;
|
|
1149
|
-
reconnectionManager?.stop();
|
|
1150
|
-
reconnectionManager?.reset();
|
|
1151
|
-
await syncEngine.start();
|
|
1152
|
-
syncStatusBridge?.refresh();
|
|
1050
|
+
if (state.syncEngine) {
|
|
1051
|
+
state.intentionalDisconnect = false;
|
|
1052
|
+
state.reconnectionManager?.stop();
|
|
1053
|
+
state.reconnectionManager?.reset();
|
|
1054
|
+
await state.syncEngine.start();
|
|
1055
|
+
state.syncStatusBridge?.refresh();
|
|
1153
1056
|
}
|
|
1154
1057
|
},
|
|
1155
1058
|
async disconnect() {
|
|
1156
1059
|
await ready;
|
|
1157
|
-
if (syncEngine) {
|
|
1158
|
-
intentionalDisconnect = true;
|
|
1159
|
-
reconnectionManager?.stop();
|
|
1160
|
-
await syncEngine.stop();
|
|
1161
|
-
syncStatusBridge?.refresh();
|
|
1060
|
+
if (state.syncEngine) {
|
|
1061
|
+
state.intentionalDisconnect = true;
|
|
1062
|
+
state.reconnectionManager?.stop();
|
|
1063
|
+
await state.syncEngine.stop();
|
|
1064
|
+
state.syncStatusBridge?.refresh();
|
|
1162
1065
|
}
|
|
1163
1066
|
},
|
|
1164
1067
|
getStatus() {
|
|
1165
|
-
if (syncEngine) {
|
|
1166
|
-
return syncEngine.getStatus();
|
|
1068
|
+
if (state.syncEngine) {
|
|
1069
|
+
return state.syncEngine.getStatus();
|
|
1167
1070
|
}
|
|
1168
1071
|
return offlineSyncStatus();
|
|
1169
1072
|
},
|
|
1170
1073
|
async retryNow() {
|
|
1171
1074
|
await ready;
|
|
1172
|
-
if (syncEngine) {
|
|
1173
|
-
await syncEngine.retryNow();
|
|
1075
|
+
if (state.syncEngine) {
|
|
1076
|
+
await state.syncEngine.retryNow();
|
|
1174
1077
|
}
|
|
1175
1078
|
},
|
|
1176
1079
|
clearSchemaBlock() {
|
|
1177
|
-
syncEngine?.clearSchemaBlock();
|
|
1080
|
+
state.syncEngine?.clearSchemaBlock();
|
|
1178
1081
|
},
|
|
1179
1082
|
exportDiagnostics() {
|
|
1180
|
-
if (syncEngine) {
|
|
1181
|
-
return syncEngine.exportDiagnostics();
|
|
1083
|
+
if (state.syncEngine) {
|
|
1084
|
+
return state.syncEngine.exportDiagnostics();
|
|
1182
1085
|
}
|
|
1183
1086
|
return {
|
|
1184
1087
|
state: "disconnected",
|
|
@@ -1203,11 +1106,199 @@ function createApp(config) {
|
|
|
1203
1106
|
timestamp: Date.now()
|
|
1204
1107
|
};
|
|
1205
1108
|
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/auth-sync-coordinator.ts
|
|
1113
|
+
var AuthSyncCoordinator = class {
|
|
1114
|
+
constructor(getEngine, authBinding) {
|
|
1115
|
+
this.getEngine = getEngine;
|
|
1116
|
+
this.authBinding = authBinding;
|
|
1117
|
+
}
|
|
1118
|
+
getEngine;
|
|
1119
|
+
authBinding;
|
|
1120
|
+
inFlight = null;
|
|
1121
|
+
pending = false;
|
|
1122
|
+
disposed = false;
|
|
1123
|
+
scheduleReconnect() {
|
|
1124
|
+
if (this.disposed) {
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (this.inFlight) {
|
|
1128
|
+
this.pending = true;
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
this.inFlight = this.run().finally(() => {
|
|
1132
|
+
this.inFlight = null;
|
|
1133
|
+
if (this.pending && !this.disposed) {
|
|
1134
|
+
this.pending = false;
|
|
1135
|
+
this.scheduleReconnect();
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
destroy() {
|
|
1140
|
+
this.disposed = true;
|
|
1141
|
+
this.pending = false;
|
|
1142
|
+
}
|
|
1143
|
+
async run() {
|
|
1144
|
+
const engine = this.getEngine();
|
|
1145
|
+
if (!engine) {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const headers = await this.authBinding.auth();
|
|
1149
|
+
if (!headers.token) {
|
|
1150
|
+
await engine.stop();
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
if (this.authBinding.resolveScopeMap) {
|
|
1154
|
+
const nextScope = await this.authBinding.resolveScopeMap();
|
|
1155
|
+
engine.updateScope(nextScope);
|
|
1156
|
+
}
|
|
1157
|
+
const status = engine.getStatus().status;
|
|
1158
|
+
if (status !== "offline") {
|
|
1159
|
+
await engine.stop();
|
|
1160
|
+
}
|
|
1161
|
+
await engine.start();
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
// src/sync-status-bridge.ts
|
|
1166
|
+
var import_sync5 = require("@korajs/sync");
|
|
1167
|
+
function createSyncStatusBridge(emitter, getSyncEngine) {
|
|
1168
|
+
const controller = (0, import_sync5.createSyncStatusController)({
|
|
1169
|
+
getSyncEngine,
|
|
1170
|
+
subscribeSyncStatus: null,
|
|
1171
|
+
events: emitter
|
|
1172
|
+
});
|
|
1173
|
+
return {
|
|
1174
|
+
get status() {
|
|
1175
|
+
return controller.getSnapshot();
|
|
1176
|
+
},
|
|
1177
|
+
subscribe(listener) {
|
|
1178
|
+
return controller.subscribe(() => {
|
|
1179
|
+
listener(controller.getSnapshot());
|
|
1180
|
+
});
|
|
1181
|
+
},
|
|
1182
|
+
refresh: () => controller.refresh(),
|
|
1183
|
+
destroy: () => controller.destroy()
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/sync-lifecycle.ts
|
|
1188
|
+
var import_sync6 = require("@korajs/sync");
|
|
1189
|
+
function wireSyncLifecycleAfterReady(config, emitter, state, init) {
|
|
1190
|
+
state.syncEngine = init.syncEngine;
|
|
1191
|
+
if (!config.sync) {
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
state.syncStatusBridge = createSyncStatusBridge(emitter, () => state.syncEngine);
|
|
1195
|
+
state.syncStatusBridge.refresh();
|
|
1196
|
+
if (state.syncEngine && init.authBinding?.subscribe) {
|
|
1197
|
+
state.authSyncCoordinator = new AuthSyncCoordinator(() => state.syncEngine, init.authBinding);
|
|
1198
|
+
init.authBinding.subscribe(() => {
|
|
1199
|
+
state.authSyncCoordinator?.scheduleReconnect();
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
if (!state.syncEngine) {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const syncEngine = state.syncEngine;
|
|
1206
|
+
state.connectionMonitor = new import_sync6.ConnectionMonitor();
|
|
1207
|
+
state.reconnectionManager = new import_sync6.ReconnectionManager({
|
|
1208
|
+
initialDelay: config.sync.reconnectInterval,
|
|
1209
|
+
maxDelay: config.sync.maxReconnectInterval
|
|
1210
|
+
});
|
|
1211
|
+
emitter.on("sync:sent", () => state.connectionMonitor?.recordActivity());
|
|
1212
|
+
emitter.on("sync:received", () => state.connectionMonitor?.recordActivity());
|
|
1213
|
+
emitter.on("sync:acknowledged", () => state.connectionMonitor?.recordActivity());
|
|
1214
|
+
emitter.on("sync:connected", () => {
|
|
1215
|
+
if (state.qualityInterval !== null) {
|
|
1216
|
+
clearInterval(state.qualityInterval);
|
|
1217
|
+
}
|
|
1218
|
+
state.qualityInterval = setInterval(() => {
|
|
1219
|
+
if (state.connectionMonitor) {
|
|
1220
|
+
emitter.emit({
|
|
1221
|
+
type: "connection:quality",
|
|
1222
|
+
quality: state.connectionMonitor.getQuality()
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
}, 5e3);
|
|
1226
|
+
});
|
|
1227
|
+
emitter.on("sync:disconnected", () => {
|
|
1228
|
+
state.connectionMonitor?.reset();
|
|
1229
|
+
if (state.qualityInterval !== null) {
|
|
1230
|
+
clearInterval(state.qualityInterval);
|
|
1231
|
+
state.qualityInterval = null;
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
const browserGlobal = globalThis;
|
|
1235
|
+
if (typeof browserGlobal.addEventListener === "function") {
|
|
1236
|
+
const onOnline = () => {
|
|
1237
|
+
if (state.intentionalDisconnect || config.sync?.autoReconnect === false) {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
state.reconnectionManager?.wake();
|
|
1241
|
+
state.reconnectionManager?.reset();
|
|
1242
|
+
void syncEngine.retryNow();
|
|
1243
|
+
};
|
|
1244
|
+
browserGlobal.addEventListener("online", onOnline);
|
|
1245
|
+
state.removeOnlineListener = () => {
|
|
1246
|
+
browserGlobal.removeEventListener?.("online", onOnline);
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
emitter.on("sync:schema-mismatch", () => {
|
|
1250
|
+
state.reconnectionManager?.stop();
|
|
1251
|
+
state.intentionalDisconnect = true;
|
|
1252
|
+
});
|
|
1253
|
+
if (config.sync.autoReconnect !== false) {
|
|
1254
|
+
emitter.on("sync:disconnected", () => {
|
|
1255
|
+
if (state.intentionalDisconnect || syncEngine.isSchemaBlocked()) {
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
if (state.reconnectionManager?.isRunning()) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
syncEngine.setReconnecting(true);
|
|
1262
|
+
state.reconnectionManager?.stop();
|
|
1263
|
+
state.reconnectionManager?.start(async () => {
|
|
1264
|
+
try {
|
|
1265
|
+
await syncEngine.start();
|
|
1266
|
+
syncEngine.setReconnecting(false);
|
|
1267
|
+
return true;
|
|
1268
|
+
} catch {
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
}).then(() => {
|
|
1272
|
+
syncEngine.setReconnecting(false);
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
if (config.sync.autoConnect === true) {
|
|
1277
|
+
void syncEngine.start().catch(() => {
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
function teardownSyncLifecycle(state) {
|
|
1282
|
+
if (state.qualityInterval !== null) {
|
|
1283
|
+
clearInterval(state.qualityInterval);
|
|
1284
|
+
state.qualityInterval = null;
|
|
1285
|
+
}
|
|
1286
|
+
state.reconnectionManager?.stop();
|
|
1287
|
+
state.syncStatusBridge?.destroy();
|
|
1288
|
+
state.syncStatusBridge = null;
|
|
1289
|
+
state.authSyncCoordinator?.destroy();
|
|
1290
|
+
state.authSyncCoordinator = null;
|
|
1291
|
+
state.removeOnlineListener?.();
|
|
1292
|
+
state.removeOnlineListener = null;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/transaction-executor.ts
|
|
1296
|
+
function createTransactionExecutor(config, ready, getStore) {
|
|
1297
|
+
return async (fn, mutationName) => {
|
|
1298
|
+
await ready;
|
|
1299
|
+
const store = getStore();
|
|
1300
|
+
if (!store) {
|
|
1301
|
+
throw new Error("Store not initialized. Await app.ready before using transactions.");
|
|
1211
1302
|
}
|
|
1212
1303
|
const collectionNames = Object.keys(config.schema.collections);
|
|
1213
1304
|
return store.transaction(async (tx) => {
|
|
@@ -1226,29 +1317,130 @@ function createApp(config) {
|
|
|
1226
1317
|
}
|
|
1227
1318
|
await fn(proxy);
|
|
1228
1319
|
});
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// src/validate-config.ts
|
|
1324
|
+
var import_core5 = require("@korajs/core");
|
|
1325
|
+
function validateCreateAppConfig(config) {
|
|
1326
|
+
if (!config.schema) {
|
|
1327
|
+
throw new import_core5.SchemaValidationError("createApp requires a schema.", {
|
|
1328
|
+
fix: "Pass schema: defineSchema({ version: 1, collections: { ... } })"
|
|
1329
|
+
});
|
|
1229
1330
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1331
|
+
if (config.schema.version < 1) {
|
|
1332
|
+
throw new import_core5.SchemaValidationError("Schema version must be at least 1.", {
|
|
1333
|
+
version: config.schema.version
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
const collectionNames = Object.keys(config.schema.collections);
|
|
1337
|
+
if (collectionNames.length === 0) {
|
|
1338
|
+
throw new import_core5.SchemaValidationError("Schema must define at least one collection.", {
|
|
1339
|
+
fix: "Add entries under collections in defineSchema()."
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
if (config.sync) {
|
|
1343
|
+
validateSyncUrl(config.sync.url, config.sync.transport ?? "websocket");
|
|
1344
|
+
}
|
|
1345
|
+
const adapter = config.store?.adapter ?? detectAdapterType();
|
|
1346
|
+
const isBrowser = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
|
|
1347
|
+
if (isBrowser && (adapter === "sqlite-wasm" || adapter === "indexeddb") && !config.store?.workerUrl) {
|
|
1348
|
+
throw new import_core5.KoraError(
|
|
1349
|
+
"Browser storage requires store.workerUrl pointing to the SQLite WASM worker script.",
|
|
1350
|
+
"MISSING_WORKER_URL",
|
|
1351
|
+
{
|
|
1352
|
+
adapter,
|
|
1353
|
+
fix: 'Add store: { workerUrl: "/sqlite-wasm-worker.js" } (see create-kora-app templates).'
|
|
1354
|
+
}
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function validateSyncUrl(url, transport) {
|
|
1359
|
+
if (!url || url.trim().length === 0) {
|
|
1360
|
+
throw new import_core5.KoraError("sync.url is required when sync is configured.", "INVALID_SYNC_URL", {
|
|
1361
|
+
fix: 'Pass sync: { url: "wss://your-server/kora" }.'
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
const parsed = new URL(url);
|
|
1366
|
+
if (transport === "http") {
|
|
1367
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1368
|
+
throw new Error("bad protocol");
|
|
1369
|
+
}
|
|
1370
|
+
} else if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
|
1371
|
+
throw new Error("bad protocol");
|
|
1245
1372
|
}
|
|
1373
|
+
} catch {
|
|
1374
|
+
throw new import_core5.KoraError(
|
|
1375
|
+
`Invalid sync URL "${url}" for transport "${transport}".`,
|
|
1376
|
+
"INVALID_SYNC_URL",
|
|
1377
|
+
{
|
|
1378
|
+
url,
|
|
1379
|
+
transport,
|
|
1380
|
+
fix: transport === "http" ? "Use an absolute http:// or https:// URL." : "Use an absolute ws:// or wss:// URL."
|
|
1381
|
+
}
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// src/wire-sync-event-forwarding.ts
|
|
1387
|
+
var SYNC_EVENT_TYPES = [
|
|
1388
|
+
"sync:connected",
|
|
1389
|
+
"sync:disconnected",
|
|
1390
|
+
"sync:schema-mismatch",
|
|
1391
|
+
"sync:auth-failed",
|
|
1392
|
+
"sync:sent",
|
|
1393
|
+
"sync:received",
|
|
1394
|
+
"sync:acknowledged",
|
|
1395
|
+
"sync:apply-failed",
|
|
1396
|
+
"sync:diagnostics",
|
|
1397
|
+
"sync:bandwidth",
|
|
1398
|
+
"sync:initial-sync-progress"
|
|
1399
|
+
];
|
|
1400
|
+
function wireSyncEventForwarding(emitter, handler) {
|
|
1401
|
+
for (const type of SYNC_EVENT_TYPES) {
|
|
1402
|
+
emitter.on(type, (event) => {
|
|
1403
|
+
handler(event);
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/create-app.ts
|
|
1409
|
+
function createApp(config) {
|
|
1410
|
+
validateCreateAppConfig(config);
|
|
1411
|
+
const emitter = new import_internal5.SimpleEventEmitter();
|
|
1412
|
+
const mergeEngine = new import_merge2.MergeEngine();
|
|
1413
|
+
if (config.onSyncEvent) {
|
|
1414
|
+
wireSyncEventForwarding(emitter, config.onSyncEvent);
|
|
1415
|
+
}
|
|
1416
|
+
let store = null;
|
|
1417
|
+
let unsubscribeSync = null;
|
|
1418
|
+
let unsubscribeAudit = null;
|
|
1419
|
+
const syncState = {
|
|
1420
|
+
syncEngine: null,
|
|
1421
|
+
syncStatusBridge: null,
|
|
1422
|
+
authSyncCoordinator: null,
|
|
1423
|
+
reconnectionManager: null,
|
|
1424
|
+
connectionMonitor: null,
|
|
1425
|
+
qualityInterval: null,
|
|
1426
|
+
intentionalDisconnect: false,
|
|
1427
|
+
removeOnlineListener: null
|
|
1246
1428
|
};
|
|
1429
|
+
const devtools = setupDevtools(config, emitter);
|
|
1430
|
+
const queryStoreCache = new import_store5.QueryStoreCache(config.store?.name ?? "kora-db");
|
|
1431
|
+
const ready = initializeApp(config, emitter, mergeEngine).then((init) => {
|
|
1432
|
+
store = init.store;
|
|
1433
|
+
unsubscribeSync = init.unsubscribeSync;
|
|
1434
|
+
unsubscribeAudit = init.unsubscribeAudit;
|
|
1435
|
+
wireSyncLifecycleAfterReady(config, emitter, syncState, init);
|
|
1436
|
+
});
|
|
1437
|
+
const getStore = () => store;
|
|
1438
|
+
const executeTransaction = createTransactionExecutor(config, ready, getStore);
|
|
1247
1439
|
const app = {
|
|
1248
1440
|
ready,
|
|
1249
1441
|
events: emitter,
|
|
1250
|
-
sync:
|
|
1251
|
-
sequences,
|
|
1442
|
+
sync: createSyncControl({ config, ready, state: syncState }),
|
|
1443
|
+
sequences: createSequencesAccessor(ready, getStore),
|
|
1252
1444
|
getStore() {
|
|
1253
1445
|
if (!store) {
|
|
1254
1446
|
throw new Error("Store not initialized. Await app.ready before accessing the store.");
|
|
@@ -1256,30 +1448,23 @@ function createApp(config) {
|
|
|
1256
1448
|
return store;
|
|
1257
1449
|
},
|
|
1258
1450
|
getSyncEngine() {
|
|
1259
|
-
return syncEngine;
|
|
1451
|
+
return syncState.syncEngine;
|
|
1452
|
+
},
|
|
1453
|
+
getQueryStoreCache() {
|
|
1454
|
+
return queryStoreCache;
|
|
1260
1455
|
},
|
|
1261
|
-
|
|
1456
|
+
transaction(fn) {
|
|
1262
1457
|
return executeTransaction(fn);
|
|
1263
1458
|
},
|
|
1264
|
-
|
|
1459
|
+
mutation(name, fn) {
|
|
1265
1460
|
return executeTransaction(fn, name);
|
|
1266
1461
|
},
|
|
1267
1462
|
async close() {
|
|
1268
1463
|
await ready;
|
|
1269
|
-
intentionalDisconnect = true;
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
}
|
|
1274
|
-
reconnectionManager?.stop();
|
|
1275
|
-
if (destroyDevtoolsOverlay) {
|
|
1276
|
-
destroyDevtoolsOverlay();
|
|
1277
|
-
destroyDevtoolsOverlay = null;
|
|
1278
|
-
}
|
|
1279
|
-
if (instrumenter) {
|
|
1280
|
-
instrumenter.destroy();
|
|
1281
|
-
instrumenter = null;
|
|
1282
|
-
}
|
|
1464
|
+
syncState.intentionalDisconnect = true;
|
|
1465
|
+
teardownSyncLifecycle(syncState);
|
|
1466
|
+
devtools.destroyOverlay?.();
|
|
1467
|
+
devtools.instrumenter?.destroy();
|
|
1283
1468
|
if (unsubscribeSync) {
|
|
1284
1469
|
unsubscribeSync();
|
|
1285
1470
|
unsubscribeSync = null;
|
|
@@ -1288,14 +1473,11 @@ function createApp(config) {
|
|
|
1288
1473
|
unsubscribeAudit();
|
|
1289
1474
|
unsubscribeAudit = null;
|
|
1290
1475
|
}
|
|
1291
|
-
if (syncEngine) {
|
|
1292
|
-
await syncEngine.stop();
|
|
1293
|
-
syncEngine = null;
|
|
1294
|
-
}
|
|
1295
|
-
if (syncStatusBridge) {
|
|
1296
|
-
syncStatusBridge.destroy();
|
|
1297
|
-
syncStatusBridge = null;
|
|
1476
|
+
if (syncState.syncEngine) {
|
|
1477
|
+
await syncState.syncEngine.stop();
|
|
1478
|
+
syncState.syncEngine = null;
|
|
1298
1479
|
}
|
|
1480
|
+
queryStoreCache.clear();
|
|
1299
1481
|
if (store) {
|
|
1300
1482
|
await store.close();
|
|
1301
1483
|
store = null;
|
|
@@ -1334,7 +1516,7 @@ function createApp(config) {
|
|
|
1334
1516
|
for (const collectionName of Object.keys(config.schema.collections)) {
|
|
1335
1517
|
Object.defineProperty(app, collectionName, {
|
|
1336
1518
|
get() {
|
|
1337
|
-
return createCollectionAccessor(collectionName,
|
|
1519
|
+
return createCollectionAccessor(collectionName, getStore);
|
|
1338
1520
|
},
|
|
1339
1521
|
enumerable: true,
|
|
1340
1522
|
configurable: false
|
|
@@ -1342,190 +1524,23 @@ function createApp(config) {
|
|
|
1342
1524
|
}
|
|
1343
1525
|
return app;
|
|
1344
1526
|
}
|
|
1345
|
-
async function initializeAsync(config, emitter, mergeEngine) {
|
|
1346
|
-
const adapterType = config.store?.adapter ?? detectAdapterType();
|
|
1347
|
-
const dbName = config.store?.name ?? "kora-db";
|
|
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({
|
|
1360
|
-
schema: config.schema,
|
|
1361
|
-
adapter,
|
|
1362
|
-
emitter,
|
|
1363
|
-
dbName,
|
|
1364
|
-
nodeId: authNodeId,
|
|
1365
|
-
isolation: authNodeId ? "shared" : config.store?.isolation,
|
|
1366
|
-
...config.sync ? { onQuerySubscribed: createSyncQuerySubscriptionHook(() => syncEngine) } : {}
|
|
1367
|
-
});
|
|
1368
|
-
await store.open();
|
|
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);
|
|
1378
|
-
let unsubscribeSync = null;
|
|
1379
|
-
if (config.sync) {
|
|
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({
|
|
1391
|
-
transport,
|
|
1392
|
-
store: mergeAwareStore,
|
|
1393
|
-
config: {
|
|
1394
|
-
url: config.sync.url,
|
|
1395
|
-
transport: config.sync.transport,
|
|
1396
|
-
auth: syncAuth,
|
|
1397
|
-
batchSize: config.sync.batchSize,
|
|
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
|
|
1403
|
-
},
|
|
1404
|
-
emitter,
|
|
1405
|
-
queueStorage: new StoreQueueStorage(adapter),
|
|
1406
|
-
syncState: new StoreSyncStatePersistence(store, scopeMap),
|
|
1407
|
-
encryptor
|
|
1408
|
-
});
|
|
1409
|
-
recordConflict = () => syncEngine?.recordConflict();
|
|
1410
|
-
unsubscribeSync = emitter.on("operation:created", (event) => {
|
|
1411
|
-
if (syncEngine) {
|
|
1412
|
-
syncEngine.pushOperation(event.operation);
|
|
1413
|
-
}
|
|
1414
|
-
});
|
|
1415
|
-
}
|
|
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();
|
|
1429
|
-
}
|
|
1430
|
-
function createCollectionAccessor(collectionName, getStore) {
|
|
1431
|
-
return {
|
|
1432
|
-
async insert(data) {
|
|
1433
|
-
const currentStore = getStore();
|
|
1434
|
-
if (!currentStore) {
|
|
1435
|
-
throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
|
|
1436
|
-
}
|
|
1437
|
-
return currentStore.collection(collectionName).insert(data);
|
|
1438
|
-
},
|
|
1439
|
-
async findById(id) {
|
|
1440
|
-
const currentStore = getStore();
|
|
1441
|
-
if (!currentStore) return null;
|
|
1442
|
-
return currentStore.collection(collectionName).findById(id);
|
|
1443
|
-
},
|
|
1444
|
-
async update(id, data) {
|
|
1445
|
-
const currentStore = getStore();
|
|
1446
|
-
if (!currentStore) {
|
|
1447
|
-
throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
|
|
1448
|
-
}
|
|
1449
|
-
return currentStore.collection(collectionName).update(id, data);
|
|
1450
|
-
},
|
|
1451
|
-
async delete(id) {
|
|
1452
|
-
const currentStore = getStore();
|
|
1453
|
-
if (!currentStore) {
|
|
1454
|
-
throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
|
|
1455
|
-
}
|
|
1456
|
-
return currentStore.collection(collectionName).delete(id);
|
|
1457
|
-
},
|
|
1458
|
-
where(conditions) {
|
|
1459
|
-
const currentStore = getStore();
|
|
1460
|
-
if (!currentStore) {
|
|
1461
|
-
return createPendingQueryBuilder(conditions);
|
|
1462
|
-
}
|
|
1463
|
-
return currentStore.collection(collectionName).where(conditions);
|
|
1464
|
-
}
|
|
1465
|
-
};
|
|
1466
|
-
}
|
|
1467
|
-
function createPendingQueryBuilder(initialWhere) {
|
|
1468
|
-
const descriptor = {
|
|
1469
|
-
collection: "__pending__",
|
|
1470
|
-
where: { ...initialWhere },
|
|
1471
|
-
orderBy: [],
|
|
1472
|
-
limit: void 0,
|
|
1473
|
-
offset: void 0
|
|
1474
|
-
};
|
|
1475
|
-
const builder = {
|
|
1476
|
-
where(conditions) {
|
|
1477
|
-
descriptor.where = { ...descriptor.where, ...conditions };
|
|
1478
|
-
return this;
|
|
1479
|
-
},
|
|
1480
|
-
orderBy(field, direction = "asc") {
|
|
1481
|
-
descriptor.orderBy.push({ field, direction });
|
|
1482
|
-
return this;
|
|
1483
|
-
},
|
|
1484
|
-
limit(n) {
|
|
1485
|
-
descriptor.limit = n;
|
|
1486
|
-
return this;
|
|
1487
|
-
},
|
|
1488
|
-
offset(n) {
|
|
1489
|
-
descriptor.offset = n;
|
|
1490
|
-
return this;
|
|
1491
|
-
},
|
|
1492
|
-
async exec() {
|
|
1493
|
-
throw new AppNotReadyError(
|
|
1494
|
-
"Cannot execute a query before app.ready. Await app.ready or use <KoraProvider app={app}>."
|
|
1495
|
-
);
|
|
1496
|
-
},
|
|
1497
|
-
async count() {
|
|
1498
|
-
throw new AppNotReadyError(
|
|
1499
|
-
"Cannot count query results before app.ready. Await app.ready or use <KoraProvider app={app}>."
|
|
1500
|
-
);
|
|
1501
|
-
},
|
|
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
|
-
);
|
|
1506
|
-
},
|
|
1507
|
-
getDescriptor() {
|
|
1508
|
-
return { ...descriptor, where: { ...descriptor.where }, orderBy: [...descriptor.orderBy] };
|
|
1509
|
-
}
|
|
1510
|
-
};
|
|
1511
|
-
return builder;
|
|
1512
|
-
}
|
|
1513
1527
|
|
|
1514
1528
|
// src/index.ts
|
|
1515
|
-
var
|
|
1529
|
+
var import_store6 = require("@korajs/store");
|
|
1516
1530
|
var import_core6 = require("@korajs/core");
|
|
1517
1531
|
var import_core7 = require("@korajs/core");
|
|
1518
1532
|
var import_core8 = require("@korajs/core");
|
|
1519
1533
|
var import_core9 = require("@korajs/core");
|
|
1520
1534
|
var import_core10 = require("@korajs/core");
|
|
1521
1535
|
var import_core11 = require("@korajs/core");
|
|
1522
|
-
var import_store6 = require("@korajs/store");
|
|
1523
1536
|
var import_store7 = require("@korajs/store");
|
|
1524
1537
|
var import_store8 = require("@korajs/store");
|
|
1538
|
+
var import_store9 = require("@korajs/store");
|
|
1525
1539
|
var import_merge3 = require("@korajs/merge");
|
|
1526
|
-
var
|
|
1540
|
+
var import_sync7 = require("@korajs/sync");
|
|
1527
1541
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1528
1542
|
0 && (module.exports = {
|
|
1543
|
+
AppNotReadyError,
|
|
1529
1544
|
HybridLogicalClock,
|
|
1530
1545
|
KoraError,
|
|
1531
1546
|
MergeEngine,
|