taladb 0.11.2 → 0.11.3

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.
@@ -838,7 +838,7 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
838
838
  function nudgedPoller(collection, fetchJson, callback, onError) {
839
839
  let active = true;
840
840
  let lastJson = "";
841
- let lastGeneration = -1;
841
+ let lastGeneration = null;
842
842
  let generationSupported = true;
843
843
  let timer = null;
844
844
  let running = false;
@@ -903,6 +903,7 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
903
903
  await proxy.send("flush");
904
904
  },
905
905
  isPrimary: () => proxy.send("isPrimary"),
906
+ storageInfo: () => proxy.send("capabilities"),
906
907
  close: async () => {
907
908
  channel?.close();
908
909
  try {
@@ -915,14 +916,16 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
915
916
  listCollectionNames: async () => JSON.parse(await proxy.send("listCollections"))
916
917
  };
917
918
  if (migrations?.length) {
918
- await runMigrations(
919
- handle,
920
- async () => proxy.send("userVersion"),
921
- async (v) => {
922
- await proxy.send("setUserVersion", { version: v });
923
- },
924
- migrations
925
- );
919
+ await navigator.locks.request(`taladb:migrations:${dbName}`, async () => {
920
+ await runMigrations(
921
+ handle,
922
+ async () => proxy.send("userVersion"),
923
+ async (v) => {
924
+ await proxy.send("setUserVersion", { version: v });
925
+ },
926
+ migrations
927
+ );
928
+ });
926
929
  }
927
930
  return handle;
928
931
  }
@@ -1010,78 +1013,106 @@ async function createNativeDB(_dbName, webhook, migrations) {
1010
1013
  );
1011
1014
  }
1012
1015
  const native = maybeNative;
1016
+ let nativeQueue = Promise.resolve();
1017
+ let queued = 0;
1018
+ let closed = false;
1019
+ const call = (op, ...args) => {
1020
+ if (queued >= 128) return Promise.reject(new Error("TalaDB native request queue is full"));
1021
+ queued++;
1022
+ const result = nativeQueue.then(async () => {
1023
+ if (closed) throw new Error("TalaDB database is closed");
1024
+ if (op === "close") {
1025
+ closed = true;
1026
+ native.close();
1027
+ return;
1028
+ }
1029
+ if (op === "findNearest" && native.findNearestAsync) {
1030
+ return native.findNearestAsync(...args);
1031
+ }
1032
+ if (native.callAsync) return native.callAsync(op, args);
1033
+ if (op === "find" && native.findAsync) {
1034
+ return native.findAsync(...args);
1035
+ }
1036
+ const method = native[op];
1037
+ if (!method) throw new Error(`${op} requires a newer @taladb/react-native binary; rebuild the native module`);
1038
+ return method(...args);
1039
+ });
1040
+ nativeQueue = result.catch(() => {
1041
+ }).finally(() => {
1042
+ queued--;
1043
+ });
1044
+ return result;
1045
+ };
1013
1046
  function wrapCollection(name, opts) {
1014
1047
  const wrapped = {
1015
- insert: async (doc) => native.insert(name, doc),
1016
- insertMany: async (docs) => native.insertMany(name, docs),
1017
- find: async (filter) => native.find(name, filter ?? {}),
1018
- findOne: async (filter) => native.findOne(name, filter ?? {}),
1019
- updateOne: async (filter, update) => native.updateOne(name, filter, update),
1020
- updateMany: async (filter, update) => native.updateMany(name, filter, update),
1021
- deleteOne: async (filter) => native.deleteOne(name, filter),
1022
- deleteMany: async (filter) => native.deleteMany(name, filter),
1023
- count: async (filter) => native.count(name, filter ?? {}),
1024
- aggregate: async (pipeline) => native.aggregate(name, pipeline),
1025
- createIndex: async (field) => native.createIndex(name, field),
1026
- dropIndex: async (field) => native.dropIndex(name, field),
1027
- createCompoundIndex: async (fields) => native.createCompoundIndex(name, fields),
1028
- dropCompoundIndex: async (fields) => native.dropCompoundIndex(name, fields),
1029
- createFtsIndex: async (field) => native.createFtsIndex(name, field),
1030
- dropFtsIndex: async (field) => native.dropFtsIndex(name, field),
1048
+ insert: async (doc) => await call("insert", name, doc),
1049
+ insertMany: async (docs) => await call("insertMany", name, docs),
1050
+ find: async (filter) => await call("find", name, filter ?? {}),
1051
+ findOne: async (filter) => await call("findOne", name, filter ?? {}),
1052
+ updateOne: async (filter, update) => await call("updateOne", name, filter, update),
1053
+ updateMany: async (filter, update) => await call("updateMany", name, filter, update),
1054
+ deleteOne: async (filter) => await call("deleteOne", name, filter),
1055
+ deleteMany: async (filter) => await call("deleteMany", name, filter),
1056
+ count: async (filter) => await call("count", name, filter ?? {}),
1057
+ aggregate: async (pipeline) => await call("aggregate", name, pipeline),
1058
+ createIndex: async (field) => await call("createIndex", name, field),
1059
+ dropIndex: async (field) => await call("dropIndex", name, field),
1060
+ createCompoundIndex: async (fields) => await call("createCompoundIndex", name, fields),
1061
+ dropCompoundIndex: async (fields) => await call("dropCompoundIndex", name, fields),
1062
+ createFtsIndex: async (field) => await call("createFtsIndex", name, field),
1063
+ dropFtsIndex: async (field) => await call("dropFtsIndex", name, field),
1031
1064
  createVectorIndex: async (field, options) => {
1032
1065
  const opts2 = {};
1033
1066
  if (options.metric) opts2.metric = options.metric;
1034
- if (options.hnswM || options.hnswEfConstruction) {
1035
- opts2.hnsw = { m: options.hnswM, efConstruction: options.hnswEfConstruction };
1067
+ if (options.indexType === "hnsw") {
1068
+ opts2.hnsw = { m: options.hnswM ?? 32, ef_construction: options.hnswEfConstruction ?? 200 };
1036
1069
  }
1037
- return native.createVectorIndex(name, field, options.dimensions, opts2);
1070
+ return await call("createVectorIndex", name, field, options.dimensions, opts2);
1038
1071
  },
1039
- dropVectorIndex: async (field) => native.dropVectorIndex(name, field),
1040
- upgradeVectorIndex: async (field) => native.upgradeVectorIndex(name, field),
1041
- // The JSI HostObject does not expose index introspection yet; return a
1042
- // correctly-shaped empty result rather than `{}` cast to the interface.
1043
- listIndexes: async () => ({ btree: [], fts: [], vector: [] }),
1072
+ dropVectorIndex: async (field) => await call("dropVectorIndex", name, field),
1073
+ upgradeVectorIndex: async (field) => await call("upgradeVectorIndex", name, field),
1074
+ listIndexes: async () => call("listIndexes", name),
1044
1075
  findNearest: async (field, vector, topK, filter) => {
1045
- const raw = native.findNearest(name, field, vector, topK, filter ?? null);
1076
+ const raw = await call("findNearest", name, field, vector, topK, filter ?? null);
1046
1077
  return raw;
1047
1078
  },
1048
1079
  searchText: async (field, query, topK, filter, options) => {
1049
- if (!native.searchText) {
1080
+ if (!native.callAsync && !native.searchText) {
1050
1081
  throw new Error("searchText requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
1051
1082
  }
1052
- return native.searchText(name, field, query, topK, filter ?? null, options ?? null);
1083
+ return await call("searchText", name, field, query, topK, filter ?? null, options ?? null);
1053
1084
  },
1054
1085
  hybridSearch: async (text, vector, topK, filter, options) => {
1055
- if (!native.hybridSearch) {
1086
+ if (!native.callAsync && !native.hybridSearch) {
1056
1087
  throw new Error("hybridSearch requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
1057
1088
  }
1058
- return native.hybridSearch(name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
1089
+ return await call("hybridSearch", name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
1059
1090
  },
1060
- subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError),
1061
- subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => native.aggregate(name, pipeline), callback, onError)
1091
+ subscribe: (filter, callback, onError) => makePoller(async () => await call("find", name, filter ?? {}), callback, onError),
1092
+ subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => await call("aggregate", name, pipeline), callback, onError)
1062
1093
  };
1063
1094
  return decorateCollection(wrapped, name, opts, webhook);
1064
1095
  }
1065
1096
  const handle = {
1066
1097
  collection: (name, opts) => wrapCollection(name, opts),
1067
- compact: async () => native.compact(),
1068
- close: async () => native.close(),
1069
- flush: native.flush ? async () => {
1070
- native.flush();
1098
+ compact: async () => await call("compact"),
1099
+ close: async () => await call("close"),
1100
+ flush: native.callAsync || native.flush ? async () => {
1101
+ await call("flush");
1071
1102
  } : void 0,
1072
1103
  // One process owns the file — there is no other tab to defer to.
1073
1104
  isPrimary: async () => true
1074
1105
  };
1075
1106
  if (migrations?.length) {
1076
- if (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function") {
1107
+ if (!native.callAsync && (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function")) {
1077
1108
  throw new Error(
1078
1109
  "openDB({ migrations }) is not available on this @taladb/react-native binary yet (the JSI HostObject does not expose userVersion/setUserVersion). Update the native module."
1079
1110
  );
1080
1111
  }
1081
1112
  await runMigrations(
1082
1113
  handle,
1083
- async () => native.userVersion(),
1084
- async (v) => native.setUserVersion(v),
1114
+ async () => call("userVersion"),
1115
+ async (v) => call("setUserVersion", v),
1085
1116
  migrations
1086
1117
  );
1087
1118
  }
package/dist/index.d.mts CHANGED
@@ -108,7 +108,7 @@ interface VectorIndexOptions {
108
108
  * Requires the `vector-hnsw` feature to be compiled in.
109
109
  */
110
110
  indexType?: 'flat' | 'hnsw';
111
- /** HNSW connectivity parameter M (default 16). Higher = better recall, more memory. */
111
+ /** HNSW connectivity parameter M. This implementation supports only 32 (the default). */
112
112
  hnswM?: number;
113
113
  /** HNSW build-time quality parameter ef_construction (default 200). */
114
114
  hnswEfConstruction?: number;
@@ -681,33 +681,20 @@ interface TalaDB {
681
681
  */
682
682
  flush?(): Promise<void>;
683
683
  /**
684
- * Whether this tab's writes land authoritatively, without being forwarded to
685
- * another tab.
686
- *
687
- * **Browser only.** The first tab to open a database becomes the primary and
688
- * owns the storage; later tabs run an in-memory copy and forward their writes
689
- * to it over BroadcastChannel. Both the OPFS owner and — where OPFS is
690
- * unavailable — the tab that publishes the IndexedDB snapshot report `true`.
691
- *
692
- * Use it for work that must not run in more than one tab at a time, or that
693
- * depends on reading its own writes back immediately: a background queue
694
- * drainer, a scheduled cleanup pass, an outbound sync loop. A secondary tab
695
- * sees other tabs' writes up to ~500 ms late, and its own writes only once
696
- * the primary has applied them.
697
- *
698
- * Primary status changes during a session — closing the owning tab promotes
699
- * another — so re-check it rather than caching the answer.
700
- *
701
- * Always `true` on Node.js and React Native, where a single process owns the
702
- * database. May be absent on older `@taladb/web` builds — treat absence as
703
- * `true`.
704
- *
705
- * @example
706
- * if (await db.isPrimary?.() ?? true) {
707
- * await drainOutbox();
708
- * }
684
+ * Whether this browser tab owns the database storage. All tabs execute reads
685
+ * and writes through that owner and await its result. Ownership can change
686
+ * after the owning tab closes. Node.js and React Native return true.
709
687
  */
710
688
  isPrimary?(): Promise<boolean>;
689
+ /** Actual browser storage/durability capabilities, including fallback errors. */
690
+ storageInfo?(): Promise<{
691
+ storage: 'opfs' | 'indexeddb';
692
+ durableWrites: boolean;
693
+ maxSnapshotBytes: number | null;
694
+ storageError: string | null;
695
+ hnsw: boolean;
696
+ owner: boolean;
697
+ }>;
711
698
  /**
712
699
  * Change-webhook delivery counters, when the webhook is enabled. All zero
713
700
  * (and `pending: 0`) when it is not.
package/dist/index.d.ts CHANGED
@@ -108,7 +108,7 @@ interface VectorIndexOptions {
108
108
  * Requires the `vector-hnsw` feature to be compiled in.
109
109
  */
110
110
  indexType?: 'flat' | 'hnsw';
111
- /** HNSW connectivity parameter M (default 16). Higher = better recall, more memory. */
111
+ /** HNSW connectivity parameter M. This implementation supports only 32 (the default). */
112
112
  hnswM?: number;
113
113
  /** HNSW build-time quality parameter ef_construction (default 200). */
114
114
  hnswEfConstruction?: number;
@@ -681,33 +681,20 @@ interface TalaDB {
681
681
  */
682
682
  flush?(): Promise<void>;
683
683
  /**
684
- * Whether this tab's writes land authoritatively, without being forwarded to
685
- * another tab.
686
- *
687
- * **Browser only.** The first tab to open a database becomes the primary and
688
- * owns the storage; later tabs run an in-memory copy and forward their writes
689
- * to it over BroadcastChannel. Both the OPFS owner and — where OPFS is
690
- * unavailable — the tab that publishes the IndexedDB snapshot report `true`.
691
- *
692
- * Use it for work that must not run in more than one tab at a time, or that
693
- * depends on reading its own writes back immediately: a background queue
694
- * drainer, a scheduled cleanup pass, an outbound sync loop. A secondary tab
695
- * sees other tabs' writes up to ~500 ms late, and its own writes only once
696
- * the primary has applied them.
697
- *
698
- * Primary status changes during a session — closing the owning tab promotes
699
- * another — so re-check it rather than caching the answer.
700
- *
701
- * Always `true` on Node.js and React Native, where a single process owns the
702
- * database. May be absent on older `@taladb/web` builds — treat absence as
703
- * `true`.
704
- *
705
- * @example
706
- * if (await db.isPrimary?.() ?? true) {
707
- * await drainOutbox();
708
- * }
684
+ * Whether this browser tab owns the database storage. All tabs execute reads
685
+ * and writes through that owner and await its result. Ownership can change
686
+ * after the owning tab closes. Node.js and React Native return true.
709
687
  */
710
688
  isPrimary?(): Promise<boolean>;
689
+ /** Actual browser storage/durability capabilities, including fallback errors. */
690
+ storageInfo?(): Promise<{
691
+ storage: 'opfs' | 'indexeddb';
692
+ durableWrites: boolean;
693
+ maxSnapshotBytes: number | null;
694
+ storageError: string | null;
695
+ hnsw: boolean;
696
+ owner: boolean;
697
+ }>;
711
698
  /**
712
699
  * Change-webhook delivery counters, when the webhook is enabled. All zero
713
700
  * (and `pending: 0`) when it is not.
package/dist/index.js CHANGED
@@ -912,7 +912,7 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
912
912
  function nudgedPoller(collection, fetchJson, callback, onError) {
913
913
  let active = true;
914
914
  let lastJson = "";
915
- let lastGeneration = -1;
915
+ let lastGeneration = null;
916
916
  let generationSupported = true;
917
917
  let timer = null;
918
918
  let running = false;
@@ -977,6 +977,7 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
977
977
  await proxy.send("flush");
978
978
  },
979
979
  isPrimary: () => proxy.send("isPrimary"),
980
+ storageInfo: () => proxy.send("capabilities"),
980
981
  close: async () => {
981
982
  channel?.close();
982
983
  try {
@@ -989,14 +990,16 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
989
990
  listCollectionNames: async () => JSON.parse(await proxy.send("listCollections"))
990
991
  };
991
992
  if (migrations?.length) {
992
- await runMigrations(
993
- handle,
994
- async () => proxy.send("userVersion"),
995
- async (v) => {
996
- await proxy.send("setUserVersion", { version: v });
997
- },
998
- migrations
999
- );
993
+ await navigator.locks.request(`taladb:migrations:${dbName}`, async () => {
994
+ await runMigrations(
995
+ handle,
996
+ async () => proxy.send("userVersion"),
997
+ async (v) => {
998
+ await proxy.send("setUserVersion", { version: v });
999
+ },
1000
+ migrations
1001
+ );
1002
+ });
1000
1003
  }
1001
1004
  return handle;
1002
1005
  }
@@ -1084,78 +1087,106 @@ async function createNativeDB(_dbName, webhook, migrations) {
1084
1087
  );
1085
1088
  }
1086
1089
  const native = maybeNative;
1090
+ let nativeQueue = Promise.resolve();
1091
+ let queued = 0;
1092
+ let closed = false;
1093
+ const call = (op, ...args) => {
1094
+ if (queued >= 128) return Promise.reject(new Error("TalaDB native request queue is full"));
1095
+ queued++;
1096
+ const result = nativeQueue.then(async () => {
1097
+ if (closed) throw new Error("TalaDB database is closed");
1098
+ if (op === "close") {
1099
+ closed = true;
1100
+ native.close();
1101
+ return;
1102
+ }
1103
+ if (op === "findNearest" && native.findNearestAsync) {
1104
+ return native.findNearestAsync(...args);
1105
+ }
1106
+ if (native.callAsync) return native.callAsync(op, args);
1107
+ if (op === "find" && native.findAsync) {
1108
+ return native.findAsync(...args);
1109
+ }
1110
+ const method = native[op];
1111
+ if (!method) throw new Error(`${op} requires a newer @taladb/react-native binary; rebuild the native module`);
1112
+ return method(...args);
1113
+ });
1114
+ nativeQueue = result.catch(() => {
1115
+ }).finally(() => {
1116
+ queued--;
1117
+ });
1118
+ return result;
1119
+ };
1087
1120
  function wrapCollection(name, opts) {
1088
1121
  const wrapped = {
1089
- insert: async (doc) => native.insert(name, doc),
1090
- insertMany: async (docs) => native.insertMany(name, docs),
1091
- find: async (filter) => native.find(name, filter ?? {}),
1092
- findOne: async (filter) => native.findOne(name, filter ?? {}),
1093
- updateOne: async (filter, update) => native.updateOne(name, filter, update),
1094
- updateMany: async (filter, update) => native.updateMany(name, filter, update),
1095
- deleteOne: async (filter) => native.deleteOne(name, filter),
1096
- deleteMany: async (filter) => native.deleteMany(name, filter),
1097
- count: async (filter) => native.count(name, filter ?? {}),
1098
- aggregate: async (pipeline) => native.aggregate(name, pipeline),
1099
- createIndex: async (field) => native.createIndex(name, field),
1100
- dropIndex: async (field) => native.dropIndex(name, field),
1101
- createCompoundIndex: async (fields) => native.createCompoundIndex(name, fields),
1102
- dropCompoundIndex: async (fields) => native.dropCompoundIndex(name, fields),
1103
- createFtsIndex: async (field) => native.createFtsIndex(name, field),
1104
- dropFtsIndex: async (field) => native.dropFtsIndex(name, field),
1122
+ insert: async (doc) => await call("insert", name, doc),
1123
+ insertMany: async (docs) => await call("insertMany", name, docs),
1124
+ find: async (filter) => await call("find", name, filter ?? {}),
1125
+ findOne: async (filter) => await call("findOne", name, filter ?? {}),
1126
+ updateOne: async (filter, update) => await call("updateOne", name, filter, update),
1127
+ updateMany: async (filter, update) => await call("updateMany", name, filter, update),
1128
+ deleteOne: async (filter) => await call("deleteOne", name, filter),
1129
+ deleteMany: async (filter) => await call("deleteMany", name, filter),
1130
+ count: async (filter) => await call("count", name, filter ?? {}),
1131
+ aggregate: async (pipeline) => await call("aggregate", name, pipeline),
1132
+ createIndex: async (field) => await call("createIndex", name, field),
1133
+ dropIndex: async (field) => await call("dropIndex", name, field),
1134
+ createCompoundIndex: async (fields) => await call("createCompoundIndex", name, fields),
1135
+ dropCompoundIndex: async (fields) => await call("dropCompoundIndex", name, fields),
1136
+ createFtsIndex: async (field) => await call("createFtsIndex", name, field),
1137
+ dropFtsIndex: async (field) => await call("dropFtsIndex", name, field),
1105
1138
  createVectorIndex: async (field, options) => {
1106
1139
  const opts2 = {};
1107
1140
  if (options.metric) opts2.metric = options.metric;
1108
- if (options.hnswM || options.hnswEfConstruction) {
1109
- opts2.hnsw = { m: options.hnswM, efConstruction: options.hnswEfConstruction };
1141
+ if (options.indexType === "hnsw") {
1142
+ opts2.hnsw = { m: options.hnswM ?? 32, ef_construction: options.hnswEfConstruction ?? 200 };
1110
1143
  }
1111
- return native.createVectorIndex(name, field, options.dimensions, opts2);
1144
+ return await call("createVectorIndex", name, field, options.dimensions, opts2);
1112
1145
  },
1113
- dropVectorIndex: async (field) => native.dropVectorIndex(name, field),
1114
- upgradeVectorIndex: async (field) => native.upgradeVectorIndex(name, field),
1115
- // The JSI HostObject does not expose index introspection yet; return a
1116
- // correctly-shaped empty result rather than `{}` cast to the interface.
1117
- listIndexes: async () => ({ btree: [], fts: [], vector: [] }),
1146
+ dropVectorIndex: async (field) => await call("dropVectorIndex", name, field),
1147
+ upgradeVectorIndex: async (field) => await call("upgradeVectorIndex", name, field),
1148
+ listIndexes: async () => call("listIndexes", name),
1118
1149
  findNearest: async (field, vector, topK, filter) => {
1119
- const raw = native.findNearest(name, field, vector, topK, filter ?? null);
1150
+ const raw = await call("findNearest", name, field, vector, topK, filter ?? null);
1120
1151
  return raw;
1121
1152
  },
1122
1153
  searchText: async (field, query, topK, filter, options) => {
1123
- if (!native.searchText) {
1154
+ if (!native.callAsync && !native.searchText) {
1124
1155
  throw new Error("searchText requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
1125
1156
  }
1126
- return native.searchText(name, field, query, topK, filter ?? null, options ?? null);
1157
+ return await call("searchText", name, field, query, topK, filter ?? null, options ?? null);
1127
1158
  },
1128
1159
  hybridSearch: async (text, vector, topK, filter, options) => {
1129
- if (!native.hybridSearch) {
1160
+ if (!native.callAsync && !native.hybridSearch) {
1130
1161
  throw new Error("hybridSearch requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
1131
1162
  }
1132
- return native.hybridSearch(name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
1163
+ return await call("hybridSearch", name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
1133
1164
  },
1134
- subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError),
1135
- subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => native.aggregate(name, pipeline), callback, onError)
1165
+ subscribe: (filter, callback, onError) => makePoller(async () => await call("find", name, filter ?? {}), callback, onError),
1166
+ subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => await call("aggregate", name, pipeline), callback, onError)
1136
1167
  };
1137
1168
  return decorateCollection(wrapped, name, opts, webhook);
1138
1169
  }
1139
1170
  const handle = {
1140
1171
  collection: (name, opts) => wrapCollection(name, opts),
1141
- compact: async () => native.compact(),
1142
- close: async () => native.close(),
1143
- flush: native.flush ? async () => {
1144
- native.flush();
1172
+ compact: async () => await call("compact"),
1173
+ close: async () => await call("close"),
1174
+ flush: native.callAsync || native.flush ? async () => {
1175
+ await call("flush");
1145
1176
  } : void 0,
1146
1177
  // One process owns the file — there is no other tab to defer to.
1147
1178
  isPrimary: async () => true
1148
1179
  };
1149
1180
  if (migrations?.length) {
1150
- if (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function") {
1181
+ if (!native.callAsync && (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function")) {
1151
1182
  throw new Error(
1152
1183
  "openDB({ migrations }) is not available on this @taladb/react-native binary yet (the JSI HostObject does not expose userVersion/setUserVersion). Update the native module."
1153
1184
  );
1154
1185
  }
1155
1186
  await runMigrations(
1156
1187
  handle,
1157
- async () => native.userVersion(),
1158
- async (v) => native.setUserVersion(v),
1188
+ async () => call("userVersion"),
1189
+ async (v) => call("setUserVersion", v),
1159
1190
  migrations
1160
1191
  );
1161
1192
  }
package/dist/index.mjs CHANGED
@@ -867,7 +867,7 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
867
867
  function nudgedPoller(collection, fetchJson, callback, onError) {
868
868
  let active = true;
869
869
  let lastJson = "";
870
- let lastGeneration = -1;
870
+ let lastGeneration = null;
871
871
  let generationSupported = true;
872
872
  let timer = null;
873
873
  let running = false;
@@ -932,6 +932,7 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
932
932
  await proxy.send("flush");
933
933
  },
934
934
  isPrimary: () => proxy.send("isPrimary"),
935
+ storageInfo: () => proxy.send("capabilities"),
935
936
  close: async () => {
936
937
  channel?.close();
937
938
  try {
@@ -944,14 +945,16 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
944
945
  listCollectionNames: async () => JSON.parse(await proxy.send("listCollections"))
945
946
  };
946
947
  if (migrations?.length) {
947
- await runMigrations(
948
- handle,
949
- async () => proxy.send("userVersion"),
950
- async (v) => {
951
- await proxy.send("setUserVersion", { version: v });
952
- },
953
- migrations
954
- );
948
+ await navigator.locks.request(`taladb:migrations:${dbName}`, async () => {
949
+ await runMigrations(
950
+ handle,
951
+ async () => proxy.send("userVersion"),
952
+ async (v) => {
953
+ await proxy.send("setUserVersion", { version: v });
954
+ },
955
+ migrations
956
+ );
957
+ });
955
958
  }
956
959
  return handle;
957
960
  }
@@ -1039,78 +1042,106 @@ async function createNativeDB(_dbName, webhook, migrations) {
1039
1042
  );
1040
1043
  }
1041
1044
  const native = maybeNative;
1045
+ let nativeQueue = Promise.resolve();
1046
+ let queued = 0;
1047
+ let closed = false;
1048
+ const call = (op, ...args) => {
1049
+ if (queued >= 128) return Promise.reject(new Error("TalaDB native request queue is full"));
1050
+ queued++;
1051
+ const result = nativeQueue.then(async () => {
1052
+ if (closed) throw new Error("TalaDB database is closed");
1053
+ if (op === "close") {
1054
+ closed = true;
1055
+ native.close();
1056
+ return;
1057
+ }
1058
+ if (op === "findNearest" && native.findNearestAsync) {
1059
+ return native.findNearestAsync(...args);
1060
+ }
1061
+ if (native.callAsync) return native.callAsync(op, args);
1062
+ if (op === "find" && native.findAsync) {
1063
+ return native.findAsync(...args);
1064
+ }
1065
+ const method = native[op];
1066
+ if (!method) throw new Error(`${op} requires a newer @taladb/react-native binary; rebuild the native module`);
1067
+ return method(...args);
1068
+ });
1069
+ nativeQueue = result.catch(() => {
1070
+ }).finally(() => {
1071
+ queued--;
1072
+ });
1073
+ return result;
1074
+ };
1042
1075
  function wrapCollection(name, opts) {
1043
1076
  const wrapped = {
1044
- insert: async (doc) => native.insert(name, doc),
1045
- insertMany: async (docs) => native.insertMany(name, docs),
1046
- find: async (filter) => native.find(name, filter ?? {}),
1047
- findOne: async (filter) => native.findOne(name, filter ?? {}),
1048
- updateOne: async (filter, update) => native.updateOne(name, filter, update),
1049
- updateMany: async (filter, update) => native.updateMany(name, filter, update),
1050
- deleteOne: async (filter) => native.deleteOne(name, filter),
1051
- deleteMany: async (filter) => native.deleteMany(name, filter),
1052
- count: async (filter) => native.count(name, filter ?? {}),
1053
- aggregate: async (pipeline) => native.aggregate(name, pipeline),
1054
- createIndex: async (field) => native.createIndex(name, field),
1055
- dropIndex: async (field) => native.dropIndex(name, field),
1056
- createCompoundIndex: async (fields) => native.createCompoundIndex(name, fields),
1057
- dropCompoundIndex: async (fields) => native.dropCompoundIndex(name, fields),
1058
- createFtsIndex: async (field) => native.createFtsIndex(name, field),
1059
- dropFtsIndex: async (field) => native.dropFtsIndex(name, field),
1077
+ insert: async (doc) => await call("insert", name, doc),
1078
+ insertMany: async (docs) => await call("insertMany", name, docs),
1079
+ find: async (filter) => await call("find", name, filter ?? {}),
1080
+ findOne: async (filter) => await call("findOne", name, filter ?? {}),
1081
+ updateOne: async (filter, update) => await call("updateOne", name, filter, update),
1082
+ updateMany: async (filter, update) => await call("updateMany", name, filter, update),
1083
+ deleteOne: async (filter) => await call("deleteOne", name, filter),
1084
+ deleteMany: async (filter) => await call("deleteMany", name, filter),
1085
+ count: async (filter) => await call("count", name, filter ?? {}),
1086
+ aggregate: async (pipeline) => await call("aggregate", name, pipeline),
1087
+ createIndex: async (field) => await call("createIndex", name, field),
1088
+ dropIndex: async (field) => await call("dropIndex", name, field),
1089
+ createCompoundIndex: async (fields) => await call("createCompoundIndex", name, fields),
1090
+ dropCompoundIndex: async (fields) => await call("dropCompoundIndex", name, fields),
1091
+ createFtsIndex: async (field) => await call("createFtsIndex", name, field),
1092
+ dropFtsIndex: async (field) => await call("dropFtsIndex", name, field),
1060
1093
  createVectorIndex: async (field, options) => {
1061
1094
  const opts2 = {};
1062
1095
  if (options.metric) opts2.metric = options.metric;
1063
- if (options.hnswM || options.hnswEfConstruction) {
1064
- opts2.hnsw = { m: options.hnswM, efConstruction: options.hnswEfConstruction };
1096
+ if (options.indexType === "hnsw") {
1097
+ opts2.hnsw = { m: options.hnswM ?? 32, ef_construction: options.hnswEfConstruction ?? 200 };
1065
1098
  }
1066
- return native.createVectorIndex(name, field, options.dimensions, opts2);
1099
+ return await call("createVectorIndex", name, field, options.dimensions, opts2);
1067
1100
  },
1068
- dropVectorIndex: async (field) => native.dropVectorIndex(name, field),
1069
- upgradeVectorIndex: async (field) => native.upgradeVectorIndex(name, field),
1070
- // The JSI HostObject does not expose index introspection yet; return a
1071
- // correctly-shaped empty result rather than `{}` cast to the interface.
1072
- listIndexes: async () => ({ btree: [], fts: [], vector: [] }),
1101
+ dropVectorIndex: async (field) => await call("dropVectorIndex", name, field),
1102
+ upgradeVectorIndex: async (field) => await call("upgradeVectorIndex", name, field),
1103
+ listIndexes: async () => call("listIndexes", name),
1073
1104
  findNearest: async (field, vector, topK, filter) => {
1074
- const raw = native.findNearest(name, field, vector, topK, filter ?? null);
1105
+ const raw = await call("findNearest", name, field, vector, topK, filter ?? null);
1075
1106
  return raw;
1076
1107
  },
1077
1108
  searchText: async (field, query, topK, filter, options) => {
1078
- if (!native.searchText) {
1109
+ if (!native.callAsync && !native.searchText) {
1079
1110
  throw new Error("searchText requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
1080
1111
  }
1081
- return native.searchText(name, field, query, topK, filter ?? null, options ?? null);
1112
+ return await call("searchText", name, field, query, topK, filter ?? null, options ?? null);
1082
1113
  },
1083
1114
  hybridSearch: async (text, vector, topK, filter, options) => {
1084
- if (!native.hybridSearch) {
1115
+ if (!native.callAsync && !native.hybridSearch) {
1085
1116
  throw new Error("hybridSearch requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
1086
1117
  }
1087
- return native.hybridSearch(name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
1118
+ return await call("hybridSearch", name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
1088
1119
  },
1089
- subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError),
1090
- subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => native.aggregate(name, pipeline), callback, onError)
1120
+ subscribe: (filter, callback, onError) => makePoller(async () => await call("find", name, filter ?? {}), callback, onError),
1121
+ subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => await call("aggregate", name, pipeline), callback, onError)
1091
1122
  };
1092
1123
  return decorateCollection(wrapped, name, opts, webhook);
1093
1124
  }
1094
1125
  const handle = {
1095
1126
  collection: (name, opts) => wrapCollection(name, opts),
1096
- compact: async () => native.compact(),
1097
- close: async () => native.close(),
1098
- flush: native.flush ? async () => {
1099
- native.flush();
1127
+ compact: async () => await call("compact"),
1128
+ close: async () => await call("close"),
1129
+ flush: native.callAsync || native.flush ? async () => {
1130
+ await call("flush");
1100
1131
  } : void 0,
1101
1132
  // One process owns the file — there is no other tab to defer to.
1102
1133
  isPrimary: async () => true
1103
1134
  };
1104
1135
  if (migrations?.length) {
1105
- if (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function") {
1136
+ if (!native.callAsync && (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function")) {
1106
1137
  throw new Error(
1107
1138
  "openDB({ migrations }) is not available on this @taladb/react-native binary yet (the JSI HostObject does not expose userVersion/setUserVersion). Update the native module."
1108
1139
  );
1109
1140
  }
1110
1141
  await runMigrations(
1111
1142
  handle,
1112
- async () => native.userVersion(),
1113
- async (v) => native.setUserVersion(v),
1143
+ async () => call("userVersion"),
1144
+ async (v) => call("setUserVersion", v),
1114
1145
  migrations
1115
1146
  );
1116
1147
  }
@@ -838,7 +838,7 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
838
838
  function nudgedPoller(collection, fetchJson, callback, onError) {
839
839
  let active = true;
840
840
  let lastJson = "";
841
- let lastGeneration = -1;
841
+ let lastGeneration = null;
842
842
  let generationSupported = true;
843
843
  let timer = null;
844
844
  let running = false;
@@ -903,6 +903,7 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
903
903
  await proxy.send("flush");
904
904
  },
905
905
  isPrimary: () => proxy.send("isPrimary"),
906
+ storageInfo: () => proxy.send("capabilities"),
906
907
  close: async () => {
907
908
  channel?.close();
908
909
  try {
@@ -915,14 +916,16 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
915
916
  listCollectionNames: async () => JSON.parse(await proxy.send("listCollections"))
916
917
  };
917
918
  if (migrations?.length) {
918
- await runMigrations(
919
- handle,
920
- async () => proxy.send("userVersion"),
921
- async (v) => {
922
- await proxy.send("setUserVersion", { version: v });
923
- },
924
- migrations
925
- );
919
+ await navigator.locks.request(`taladb:migrations:${dbName}`, async () => {
920
+ await runMigrations(
921
+ handle,
922
+ async () => proxy.send("userVersion"),
923
+ async (v) => {
924
+ await proxy.send("setUserVersion", { version: v });
925
+ },
926
+ migrations
927
+ );
928
+ });
926
929
  }
927
930
  return handle;
928
931
  }
@@ -1010,78 +1013,106 @@ async function createNativeDB(_dbName, webhook, migrations) {
1010
1013
  );
1011
1014
  }
1012
1015
  const native = maybeNative;
1016
+ let nativeQueue = Promise.resolve();
1017
+ let queued = 0;
1018
+ let closed = false;
1019
+ const call = (op, ...args) => {
1020
+ if (queued >= 128) return Promise.reject(new Error("TalaDB native request queue is full"));
1021
+ queued++;
1022
+ const result = nativeQueue.then(async () => {
1023
+ if (closed) throw new Error("TalaDB database is closed");
1024
+ if (op === "close") {
1025
+ closed = true;
1026
+ native.close();
1027
+ return;
1028
+ }
1029
+ if (op === "findNearest" && native.findNearestAsync) {
1030
+ return native.findNearestAsync(...args);
1031
+ }
1032
+ if (native.callAsync) return native.callAsync(op, args);
1033
+ if (op === "find" && native.findAsync) {
1034
+ return native.findAsync(...args);
1035
+ }
1036
+ const method = native[op];
1037
+ if (!method) throw new Error(`${op} requires a newer @taladb/react-native binary; rebuild the native module`);
1038
+ return method(...args);
1039
+ });
1040
+ nativeQueue = result.catch(() => {
1041
+ }).finally(() => {
1042
+ queued--;
1043
+ });
1044
+ return result;
1045
+ };
1013
1046
  function wrapCollection(name, opts) {
1014
1047
  const wrapped = {
1015
- insert: async (doc) => native.insert(name, doc),
1016
- insertMany: async (docs) => native.insertMany(name, docs),
1017
- find: async (filter) => native.find(name, filter ?? {}),
1018
- findOne: async (filter) => native.findOne(name, filter ?? {}),
1019
- updateOne: async (filter, update) => native.updateOne(name, filter, update),
1020
- updateMany: async (filter, update) => native.updateMany(name, filter, update),
1021
- deleteOne: async (filter) => native.deleteOne(name, filter),
1022
- deleteMany: async (filter) => native.deleteMany(name, filter),
1023
- count: async (filter) => native.count(name, filter ?? {}),
1024
- aggregate: async (pipeline) => native.aggregate(name, pipeline),
1025
- createIndex: async (field) => native.createIndex(name, field),
1026
- dropIndex: async (field) => native.dropIndex(name, field),
1027
- createCompoundIndex: async (fields) => native.createCompoundIndex(name, fields),
1028
- dropCompoundIndex: async (fields) => native.dropCompoundIndex(name, fields),
1029
- createFtsIndex: async (field) => native.createFtsIndex(name, field),
1030
- dropFtsIndex: async (field) => native.dropFtsIndex(name, field),
1048
+ insert: async (doc) => await call("insert", name, doc),
1049
+ insertMany: async (docs) => await call("insertMany", name, docs),
1050
+ find: async (filter) => await call("find", name, filter ?? {}),
1051
+ findOne: async (filter) => await call("findOne", name, filter ?? {}),
1052
+ updateOne: async (filter, update) => await call("updateOne", name, filter, update),
1053
+ updateMany: async (filter, update) => await call("updateMany", name, filter, update),
1054
+ deleteOne: async (filter) => await call("deleteOne", name, filter),
1055
+ deleteMany: async (filter) => await call("deleteMany", name, filter),
1056
+ count: async (filter) => await call("count", name, filter ?? {}),
1057
+ aggregate: async (pipeline) => await call("aggregate", name, pipeline),
1058
+ createIndex: async (field) => await call("createIndex", name, field),
1059
+ dropIndex: async (field) => await call("dropIndex", name, field),
1060
+ createCompoundIndex: async (fields) => await call("createCompoundIndex", name, fields),
1061
+ dropCompoundIndex: async (fields) => await call("dropCompoundIndex", name, fields),
1062
+ createFtsIndex: async (field) => await call("createFtsIndex", name, field),
1063
+ dropFtsIndex: async (field) => await call("dropFtsIndex", name, field),
1031
1064
  createVectorIndex: async (field, options) => {
1032
1065
  const opts2 = {};
1033
1066
  if (options.metric) opts2.metric = options.metric;
1034
- if (options.hnswM || options.hnswEfConstruction) {
1035
- opts2.hnsw = { m: options.hnswM, efConstruction: options.hnswEfConstruction };
1067
+ if (options.indexType === "hnsw") {
1068
+ opts2.hnsw = { m: options.hnswM ?? 32, ef_construction: options.hnswEfConstruction ?? 200 };
1036
1069
  }
1037
- return native.createVectorIndex(name, field, options.dimensions, opts2);
1070
+ return await call("createVectorIndex", name, field, options.dimensions, opts2);
1038
1071
  },
1039
- dropVectorIndex: async (field) => native.dropVectorIndex(name, field),
1040
- upgradeVectorIndex: async (field) => native.upgradeVectorIndex(name, field),
1041
- // The JSI HostObject does not expose index introspection yet; return a
1042
- // correctly-shaped empty result rather than `{}` cast to the interface.
1043
- listIndexes: async () => ({ btree: [], fts: [], vector: [] }),
1072
+ dropVectorIndex: async (field) => await call("dropVectorIndex", name, field),
1073
+ upgradeVectorIndex: async (field) => await call("upgradeVectorIndex", name, field),
1074
+ listIndexes: async () => call("listIndexes", name),
1044
1075
  findNearest: async (field, vector, topK, filter) => {
1045
- const raw = native.findNearest(name, field, vector, topK, filter ?? null);
1076
+ const raw = await call("findNearest", name, field, vector, topK, filter ?? null);
1046
1077
  return raw;
1047
1078
  },
1048
1079
  searchText: async (field, query, topK, filter, options) => {
1049
- if (!native.searchText) {
1080
+ if (!native.callAsync && !native.searchText) {
1050
1081
  throw new Error("searchText requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
1051
1082
  }
1052
- return native.searchText(name, field, query, topK, filter ?? null, options ?? null);
1083
+ return await call("searchText", name, field, query, topK, filter ?? null, options ?? null);
1053
1084
  },
1054
1085
  hybridSearch: async (text, vector, topK, filter, options) => {
1055
- if (!native.hybridSearch) {
1086
+ if (!native.callAsync && !native.hybridSearch) {
1056
1087
  throw new Error("hybridSearch requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
1057
1088
  }
1058
- return native.hybridSearch(name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
1089
+ return await call("hybridSearch", name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
1059
1090
  },
1060
- subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError),
1061
- subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => native.aggregate(name, pipeline), callback, onError)
1091
+ subscribe: (filter, callback, onError) => makePoller(async () => await call("find", name, filter ?? {}), callback, onError),
1092
+ subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => await call("aggregate", name, pipeline), callback, onError)
1062
1093
  };
1063
1094
  return decorateCollection(wrapped, name, opts, webhook);
1064
1095
  }
1065
1096
  const handle = {
1066
1097
  collection: (name, opts) => wrapCollection(name, opts),
1067
- compact: async () => native.compact(),
1068
- close: async () => native.close(),
1069
- flush: native.flush ? async () => {
1070
- native.flush();
1098
+ compact: async () => await call("compact"),
1099
+ close: async () => await call("close"),
1100
+ flush: native.callAsync || native.flush ? async () => {
1101
+ await call("flush");
1071
1102
  } : void 0,
1072
1103
  // One process owns the file — there is no other tab to defer to.
1073
1104
  isPrimary: async () => true
1074
1105
  };
1075
1106
  if (migrations?.length) {
1076
- if (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function") {
1107
+ if (!native.callAsync && (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function")) {
1077
1108
  throw new Error(
1078
1109
  "openDB({ migrations }) is not available on this @taladb/react-native binary yet (the JSI HostObject does not expose userVersion/setUserVersion). Update the native module."
1079
1110
  );
1080
1111
  }
1081
1112
  await runMigrations(
1082
1113
  handle,
1083
- async () => native.userVersion(),
1084
- async (v) => native.setUserVersion(v),
1114
+ async () => call("userVersion"),
1115
+ async (v) => call("setUserVersion", v),
1085
1116
  migrations
1086
1117
  );
1087
1118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taladb",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
4
4
  "description": "The embedded vector database for on-device AI — documents, similarity search, and offline sync for browser, React Native, and Node.js",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -62,8 +62,8 @@
62
62
  "vitest": "^3.2.0"
63
63
  },
64
64
  "peerDependencies": {
65
- "@taladb/web": "0.11.2",
66
- "@taladb/node": "0.11.2"
65
+ "@taladb/web": "0.11.3",
66
+ "@taladb/node": "0.11.3"
67
67
  },
68
68
  "peerDependenciesMeta": {
69
69
  "@taladb/web": {