korajs 1.0.0-beta.5 → 1.0.0-beta.6

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/index.cjs CHANGED
@@ -212,7 +212,7 @@ function detectAdapterType() {
212
212
  }
213
213
  return "better-sqlite3";
214
214
  }
215
- async function createAdapter(type, dbName, workerUrl, emitter, workerResponseTimeoutMs, sharedWorkerUrl) {
215
+ async function createAdapter(type, dbName, workerUrl, emitter, workerResponseTimeoutMs, sharedWorkerUrl, suppressNonPersistentDiagnostic = false) {
216
216
  switch (type) {
217
217
  case "tauri-sqlite": {
218
218
  const { TauriSqliteAdapter } = await importOptionalPeer("@korajs/tauri");
@@ -233,7 +233,8 @@ async function createAdapter(type, dbName, workerUrl, emitter, workerResponseTim
233
233
  workerUrl,
234
234
  sharedWorkerUrl,
235
235
  workerResponseTimeoutMs,
236
- emitter
236
+ emitter,
237
+ emitNonPersistentDiagnostic: !suppressNonPersistentDiagnostic
237
238
  });
238
239
  }
239
240
  case "indexeddb": {
@@ -1055,6 +1056,51 @@ function createSyncTransport(sync) {
1055
1056
  return new import_sync.WebSocketTransport();
1056
1057
  }
1057
1058
 
1059
+ // src/local-operation-bus.ts
1060
+ var MESSAGE_TYPE = "kora-local-operation";
1061
+ function wireLocalOperationBus(dbName, store, emitter) {
1062
+ if (typeof BroadcastChannel === "undefined") {
1063
+ return () => {
1064
+ };
1065
+ }
1066
+ const originId = createOriginId();
1067
+ const channel = new BroadcastChannel(`kora-local-ops-${dbName}`);
1068
+ const onMessage = (event) => {
1069
+ const message = event.data;
1070
+ if (message?.type !== MESSAGE_TYPE || message.originId === originId || !isOperationLike(message.operation)) {
1071
+ return;
1072
+ }
1073
+ store.notifyExternalOperation(message.operation);
1074
+ };
1075
+ channel.addEventListener("message", onMessage);
1076
+ const unsubscribe = emitter.on("operation:created", (event) => {
1077
+ const message = {
1078
+ type: MESSAGE_TYPE,
1079
+ originId,
1080
+ operation: event.operation
1081
+ };
1082
+ channel.postMessage(message);
1083
+ });
1084
+ return () => {
1085
+ unsubscribe();
1086
+ channel.removeEventListener("message", onMessage);
1087
+ channel.close();
1088
+ };
1089
+ }
1090
+ function createOriginId() {
1091
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1092
+ return crypto.randomUUID();
1093
+ }
1094
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
1095
+ }
1096
+ function isOperationLike(value) {
1097
+ if (!value || typeof value !== "object") {
1098
+ return false;
1099
+ }
1100
+ const candidate = value;
1101
+ return typeof candidate.id === "string" && typeof candidate.collection === "string" && typeof candidate.recordId === "string" && typeof candidate.nodeId === "string" && typeof candidate.sequenceNumber === "number";
1102
+ }
1103
+
1058
1104
  // src/merge-aware-sync-store.ts
1059
1105
  var MergeAwareSyncStore = class {
1060
1106
  constructor(store, mergeEngine, emitter, options) {
@@ -1279,22 +1325,23 @@ function createSyncQuerySubscriptionHook(getSyncEngine) {
1279
1325
  async function initializeApp(config, emitter, mergeEngine) {
1280
1326
  const adapterType = config.store?.adapter ?? detectAdapterType();
1281
1327
  const dbName = config.store?.name ?? "kora-db";
1282
- const adapter = await createAdapter(
1328
+ let adapter = await createAdapter(
1283
1329
  adapterType,
1284
1330
  dbName,
1285
1331
  config.store?.workerUrl,
1286
1332
  emitter,
1287
1333
  config.store?.workerResponseTimeoutMs,
1288
- config.store?.sharedWorkerUrl
1334
+ config.store?.sharedWorkerUrl,
1335
+ adapterType === "sqlite-wasm"
1289
1336
  );
1290
1337
  const authBinding = config.sync?.authClient ?? null;
1291
1338
  const authNodeId = authBinding?.resolveNodeId ? await authBinding.resolveNodeId() : void 0;
1292
1339
  let syncEngine = null;
1293
1340
  const encryptionKey = config.sync?.encryption?.key;
1294
1341
  const secretKeyProvider = encryptionKey ? typeof encryptionKey === "string" ? () => encryptionKey : () => encryptionKey() : void 0;
1295
- const store = new import_store8.Store({
1342
+ const buildStore = (storeAdapter) => new import_store8.Store({
1296
1343
  schema: config.schema,
1297
- adapter,
1344
+ adapter: storeAdapter,
1298
1345
  emitter,
1299
1346
  dbName,
1300
1347
  nodeId: authNodeId,
@@ -1302,7 +1349,44 @@ async function initializeApp(config, emitter, mergeEngine) {
1302
1349
  ...secretKeyProvider ? { secretKeyProvider } : {},
1303
1350
  ...config.sync ? { onQuerySubscribed: createSyncQuerySubscriptionHook(() => syncEngine) } : {}
1304
1351
  });
1352
+ let store = buildStore(adapter);
1305
1353
  await store.open();
1354
+ if (adapterType === "sqlite-wasm" && adapter.getStorageOpenState?.()?.persistent === false) {
1355
+ const fallbackReason = adapter.getStorageOpenState()?.fallbackReason ?? "unsupported";
1356
+ await store.close();
1357
+ try {
1358
+ adapter = await createAdapter(
1359
+ "indexeddb",
1360
+ dbName,
1361
+ config.store?.workerUrl,
1362
+ emitter,
1363
+ config.store?.workerResponseTimeoutMs
1364
+ );
1365
+ store = buildStore(adapter);
1366
+ await store.open();
1367
+ emitter.emit({
1368
+ type: "store:storage-fallback",
1369
+ dbName,
1370
+ from: "opfs",
1371
+ to: "indexeddb",
1372
+ reason: fallbackReason,
1373
+ message: `OPFS persistence is unavailable (${fallbackReason}) for database "${dbName}"; Kora is using durable IndexedDB instead.`
1374
+ });
1375
+ } catch {
1376
+ adapter = await createAdapter(
1377
+ "sqlite-wasm",
1378
+ dbName,
1379
+ config.store?.workerUrl,
1380
+ emitter,
1381
+ config.store?.workerResponseTimeoutMs,
1382
+ config.store?.sharedWorkerUrl,
1383
+ true
1384
+ );
1385
+ store = buildStore(adapter);
1386
+ await store.open();
1387
+ emitOpfsUnavailable(emitter, dbName, fallbackReason);
1388
+ }
1389
+ }
1306
1390
  let recordConflict;
1307
1391
  const applyPipeline = new ApplyPipeline({
1308
1392
  store,
@@ -1312,6 +1396,7 @@ async function initializeApp(config, emitter, mergeEngine) {
1312
1396
  });
1313
1397
  store.setLocalMutationHandler(applyPipeline);
1314
1398
  const unsubscribeAudit = wireAuditPersistence(store, emitter);
1399
+ const unsubscribeLocalOperations = wireLocalOperationBus(dbName, store, emitter);
1315
1400
  const blobStore = await resolveBlobStore(config.blob, dbName);
1316
1401
  let blobChunkProvider = null;
1317
1402
  let unsubscribeSync = null;
@@ -1366,11 +1451,20 @@ async function initializeApp(config, emitter, mergeEngine) {
1366
1451
  syncEngine,
1367
1452
  unsubscribeSync,
1368
1453
  unsubscribeAudit,
1454
+ unsubscribeLocalOperations,
1369
1455
  authBinding,
1370
1456
  blobStore,
1371
1457
  blobChunkProvider
1372
1458
  };
1373
1459
  }
1460
+ function emitOpfsUnavailable(emitter, dbName, reason) {
1461
+ emitter.emit({
1462
+ type: "store:opfs-unavailable",
1463
+ dbName,
1464
+ reason,
1465
+ message: `OPFS persistence is unavailable (${reason}) for database "${dbName}", and IndexedDB fallback could not open; the store is running in memory and data will not survive a reload.`
1466
+ });
1467
+ }
1374
1468
 
1375
1469
  // src/sequences-accessor.ts
1376
1470
  function createSequencesAccessor(ready, getStore) {
@@ -1831,6 +1925,7 @@ function createApp(config) {
1831
1925
  let blobApi = null;
1832
1926
  let unsubscribeSync = null;
1833
1927
  let unsubscribeAudit = null;
1928
+ let unsubscribeLocalOperations = null;
1834
1929
  const syncState = {
1835
1930
  syncEngine: null,
1836
1931
  syncStatusBridge: null,
@@ -1847,6 +1942,7 @@ function createApp(config) {
1847
1942
  store = init.store;
1848
1943
  unsubscribeSync = init.unsubscribeSync;
1849
1944
  unsubscribeAudit = init.unsubscribeAudit;
1945
+ unsubscribeLocalOperations = init.unsubscribeLocalOperations;
1850
1946
  blobApi = createBlobApi(
1851
1947
  init.blobStore,
1852
1948
  init.blobChunkProvider,
@@ -1929,6 +2025,10 @@ function createApp(config) {
1929
2025
  unsubscribeAudit();
1930
2026
  unsubscribeAudit = null;
1931
2027
  }
2028
+ if (unsubscribeLocalOperations) {
2029
+ unsubscribeLocalOperations();
2030
+ unsubscribeLocalOperations = null;
2031
+ }
1932
2032
  if (syncState.syncEngine) {
1933
2033
  await syncState.syncEngine.stop();
1934
2034
  syncState.syncEngine = null;