taladb 0.11.2 → 0.11.4

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.mjs CHANGED
@@ -1,3 +1,89 @@
1
+ // src/vector-client.ts
2
+ function integer(value, name, min = 0, max = 4294967295) {
3
+ if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`${name} must be an integer between ${min} and ${max}`);
4
+ }
5
+ function vectorValues(vector) {
6
+ const values = Array.from(vector);
7
+ if (values.length === 0 || values.some((v) => !Number.isFinite(v))) throw new Error("vector must contain finite numbers");
8
+ return values;
9
+ }
10
+ function queryOptions(options) {
11
+ if (options.efSearch !== void 0) integer(options.efSearch, "efSearch", 1);
12
+ if (options.offset !== void 0) integer(options.offset, "offset");
13
+ if (options.groupSize !== void 0) integer(options.groupSize, "groupSize", 1);
14
+ if (options.oversampling !== void 0) integer(options.oversampling, "oversampling", 1, 100);
15
+ if (options.scoreThreshold !== void 0 && !Number.isFinite(options.scoreThreshold)) throw new Error("scoreThreshold must be finite");
16
+ return options;
17
+ }
18
+ function abortError() {
19
+ const error = new Error("Vector index rebuild cancelled");
20
+ error.name = "AbortError";
21
+ return error;
22
+ }
23
+ function createVectorClient(send) {
24
+ const client = {
25
+ searchVectors: async (field, vector, topK, filter, options = {}) => {
26
+ integer(topK, "topK");
27
+ return send({ op: "search", field, query: vectorValues(vector), topK, filter, options: queryOptions(options) });
28
+ },
29
+ findWithin: (field, vector, scoreThreshold, filter) => client.searchVectors(field, vector, 4294967295, filter, { mode: "exact", scoreThreshold }),
30
+ vectorIndexStatus: (field) => send({ op: "status", field }),
31
+ beginVectorBuild: (field, options) => send({ op: "beginBuild", field, options }),
32
+ stepVectorBuild: async (field, id, batchSize = 32) => {
33
+ integer(batchSize, "batchSize", 1, 1024);
34
+ return send({ op: "stepBuild", field, id, batchSize });
35
+ },
36
+ cancelVectorBuild: (field, id) => send({ op: "cancelBuild", field, id }),
37
+ rebuildVectorIndex: async (field, options = {}) => {
38
+ const { signal, onProgress, batchSize = 32, ...graphOptions } = options;
39
+ integer(batchSize, "batchSize", 1, 1024);
40
+ if (signal?.aborted) throw abortError();
41
+ let progress = await client.beginVectorBuild(field, Object.keys(graphOptions).length ? graphOptions : void 0);
42
+ try {
43
+ onProgress?.(progress);
44
+ while (progress.state === "building") {
45
+ await new Promise((resolve) => setTimeout(resolve, 0));
46
+ if (signal?.aborted) throw abortError();
47
+ progress = await client.stepVectorBuild(field, progress.id, batchSize);
48
+ onProgress?.(progress);
49
+ }
50
+ if (progress.state === "failed") throw new Error(progress.error ?? "Vector rebuild failed");
51
+ if (progress.state === "cancelled") throw abortError();
52
+ return progress;
53
+ } catch (error) {
54
+ if (progress.state === "building") await client.cancelVectorBuild(field, progress.id);
55
+ throw error;
56
+ }
57
+ },
58
+ measureVectorRecall: async (field, queries, topK, filter, options = {}) => {
59
+ integer(topK, "topK", 1);
60
+ return send({ op: "recall", field, queries: queries.map(vectorValues), topK, filter, options: queryOptions(options) });
61
+ }
62
+ };
63
+ return client;
64
+ }
65
+ function vectorIndexRequest(field, options) {
66
+ integer(options.dimensions, "dimensions", 1);
67
+ if (options.metric && !["cosine", "dot", "euclidean"].includes(options.metric)) throw new Error("invalid vector metric");
68
+ if (options.indexType && !["flat", "hnsw"].includes(options.indexType)) throw new Error("invalid vector indexType");
69
+ if (options.quantization && options.quantization !== "none" && options.indexType !== "hnsw") throw new Error("quantization requires an HNSW index");
70
+ if (options.indexType === "hnsw") {
71
+ const m = options.hnswM ?? 32;
72
+ const efConstruction = options.hnswEfConstruction ?? 200;
73
+ integer(m, "hnswM", 2, 128);
74
+ integer(efConstruction, "hnswEfConstruction", m, 1e5);
75
+ if (options.metric === "dot") throw new Error("HNSW requires cosine or euclidean");
76
+ if (options.quantization === "binary" && options.metric && options.metric !== "cosine") throw new Error("binary quantization requires cosine");
77
+ }
78
+ return {
79
+ op: "create",
80
+ field,
81
+ dimensions: options.dimensions,
82
+ metric: options.metric,
83
+ options: options.indexType === "hnsw" ? { m: options.hnswM ?? 32, efConstruction: options.hnswEfConstruction ?? 200, quantization: options.quantization ?? "none" } : null
84
+ };
85
+ }
86
+
1
87
  // src/webhook.ts
2
88
  var METHOD = {
3
89
  insert: "POST",
@@ -739,7 +825,9 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
739
825
  }
740
826
  function wrapCollection(name, opts) {
741
827
  const s = JSON.stringify;
828
+ const vectorCommand = async (request) => JSON.parse(await proxy.send("vectorCommand", { collection: name, requestJson: JSON.stringify(request) }));
742
829
  const wrapped = {
830
+ ...createVectorClient(vectorCommand),
743
831
  insert: (doc) => proxy.send("insert", { collection: name, docJson: s(doc) }),
744
832
  insertMany: async (docs) => {
745
833
  const json = await proxy.send("insertMany", {
@@ -791,25 +879,21 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
791
879
  dropCompoundIndex: (fields) => proxy.send("dropCompoundIndex", { collection: name, fieldsJson: JSON.stringify(fields) }),
792
880
  createFtsIndex: (field) => proxy.send("createFtsIndex", { collection: name, field }),
793
881
  dropFtsIndex: (field) => proxy.send("dropFtsIndex", { collection: name, field }),
794
- createVectorIndex: (field, options) => {
795
- if (options.indexType === "hnsw") return Promise.reject(new Error("HNSW vector indexes are not available in the browser (requires native threads). Use Node.js or React Native."));
796
- return proxy.send("createVectorIndex", {
797
- collection: name,
798
- field,
799
- dimensions: options.dimensions,
800
- metric: options.metric,
801
- indexType: null,
802
- hnswM: null,
803
- hnswEfConstruction: null
804
- });
882
+ createVectorIndex: async (field, options) => {
883
+ const request = vectorIndexRequest(field, options);
884
+ await vectorCommand({ ...request, deferBuild: true });
885
+ if (request.options) await createVectorClient(vectorCommand).rebuildVectorIndex(field, request.options);
805
886
  },
806
887
  dropVectorIndex: (field) => proxy.send("dropVectorIndex", { collection: name, field }),
807
- upgradeVectorIndex: (_field) => Promise.reject(new Error("HNSW vector indexes are not available in the browser (requires native threads). Use Node.js or React Native.")),
888
+ upgradeVectorIndex: async (field) => {
889
+ await createVectorClient(vectorCommand).rebuildVectorIndex(field);
890
+ },
808
891
  listIndexes: async () => {
809
892
  const json = await proxy.send("listIndexes", { collection: name });
810
893
  return JSON.parse(json);
811
894
  },
812
- findNearest: async (field, vector, topK, filter) => {
895
+ findNearest: async (field, vector, topK, filter, options) => {
896
+ if (options) return (await createVectorClient(vectorCommand).searchVectors(field, vector, topK, filter, options)).hits;
813
897
  const json = await proxy.send("findNearest", {
814
898
  collection: name,
815
899
  field,
@@ -867,7 +951,7 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
867
951
  function nudgedPoller(collection, fetchJson, callback, onError) {
868
952
  let active = true;
869
953
  let lastJson = "";
870
- let lastGeneration = -1;
954
+ let lastGeneration = null;
871
955
  let generationSupported = true;
872
956
  let timer = null;
873
957
  let running = false;
@@ -932,6 +1016,7 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
932
1016
  await proxy.send("flush");
933
1017
  },
934
1018
  isPrimary: () => proxy.send("isPrimary"),
1019
+ storageInfo: () => proxy.send("capabilities"),
935
1020
  close: async () => {
936
1021
  channel?.close();
937
1022
  try {
@@ -944,14 +1029,16 @@ async function createBrowserDB(dbName, webhook, config, passphrase, migrations)
944
1029
  listCollectionNames: async () => JSON.parse(await proxy.send("listCollections"))
945
1030
  };
946
1031
  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
- );
1032
+ await navigator.locks.request(`taladb:migrations:${dbName}`, async () => {
1033
+ await runMigrations(
1034
+ handle,
1035
+ async () => proxy.send("userVersion"),
1036
+ async (v) => {
1037
+ await proxy.send("setUserVersion", { version: v });
1038
+ },
1039
+ migrations
1040
+ );
1041
+ });
955
1042
  }
956
1043
  return handle;
957
1044
  }
@@ -967,7 +1054,9 @@ async function createNodeDB(dbName, webhook, config, passphrase, migrations) {
967
1054
  const db = TalaDBNode.open(dbName, configJson, passphrase ?? null);
968
1055
  function wrapCollection(name, opts) {
969
1056
  const col = db.collection(name);
1057
+ const vectorCommand = async (request) => col.vectorCommandAsync ? col.vectorCommandAsync(request) : col.vectorCommand(request);
970
1058
  const wrapped = {
1059
+ ...createVectorClient(vectorCommand),
971
1060
  insert: async (doc) => col.insertAsync ? col.insertAsync(doc) : col.insert(doc),
972
1061
  insertMany: async (docs) => col.insertManyAsync ? col.insertManyAsync(docs) : col.insertMany(docs),
973
1062
  find: async (filter) => col.findAsync ? col.findAsync(filter ?? null) : col.find(filter ?? null),
@@ -984,14 +1073,19 @@ async function createNodeDB(dbName, webhook, config, passphrase, migrations) {
984
1073
  dropCompoundIndex: async (fields) => col.dropCompoundIndex(fields),
985
1074
  createFtsIndex: async (field) => col.createFtsIndex(field),
986
1075
  dropFtsIndex: async (field) => col.dropFtsIndex(field),
987
- createVectorIndex: async (field, options) => col.createVectorIndex(field, options.dimensions, options.metric ?? null, options.indexType ?? null, options.hnswM ?? null, options.hnswEfConstruction ?? null),
1076
+ createVectorIndex: async (field, options) => {
1077
+ const request = vectorIndexRequest(field, options);
1078
+ await vectorCommand({ ...request, deferBuild: true });
1079
+ if (request.options) await createVectorClient(vectorCommand).rebuildVectorIndex(field, request.options);
1080
+ },
988
1081
  dropVectorIndex: async (field) => col.dropVectorIndex(field),
989
1082
  upgradeVectorIndex: async (field) => col.upgradeVectorIndex(field),
990
1083
  listIndexes: async () => {
991
1084
  const json = col.listIndexes();
992
1085
  return JSON.parse(json);
993
1086
  },
994
- findNearest: async (field, vector, topK, filter) => {
1087
+ findNearest: async (field, vector, topK, filter, options) => {
1088
+ if (options) return (await createVectorClient(vectorCommand).searchVectors(field, vector, topK, filter, options)).hits;
995
1089
  const raw = await col.findNearest(field, vector, topK, filter ?? null);
996
1090
  return raw;
997
1091
  },
@@ -1039,78 +1133,109 @@ async function createNativeDB(_dbName, webhook, migrations) {
1039
1133
  );
1040
1134
  }
1041
1135
  const native = maybeNative;
1136
+ let nativeQueue = Promise.resolve();
1137
+ let queued = 0;
1138
+ let closed = false;
1139
+ const call = (op, ...args) => {
1140
+ if (queued >= 128) return Promise.reject(new Error("TalaDB native request queue is full"));
1141
+ queued++;
1142
+ const result = nativeQueue.then(async () => {
1143
+ if (closed) throw new Error("TalaDB database is closed");
1144
+ if (op === "close") {
1145
+ closed = true;
1146
+ native.close();
1147
+ return;
1148
+ }
1149
+ if (op === "findNearest" && native.findNearestAsync) {
1150
+ return native.findNearestAsync(...args);
1151
+ }
1152
+ if (native.callAsync) return native.callAsync(op, args);
1153
+ if (op === "find" && native.findAsync) {
1154
+ return native.findAsync(...args);
1155
+ }
1156
+ const method = native[op];
1157
+ if (!method) throw new Error(`${op} requires a newer @taladb/react-native binary; rebuild the native module`);
1158
+ return method(...args);
1159
+ });
1160
+ nativeQueue = result.catch(() => {
1161
+ }).finally(() => {
1162
+ queued--;
1163
+ });
1164
+ return result;
1165
+ };
1042
1166
  function wrapCollection(name, opts) {
1167
+ const vectorCommand = async (request) => call("vectorCommand", name, request);
1043
1168
  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),
1169
+ ...createVectorClient(vectorCommand),
1170
+ insert: async (doc) => await call("insert", name, doc),
1171
+ insertMany: async (docs) => await call("insertMany", name, docs),
1172
+ find: async (filter) => await call("find", name, filter ?? {}),
1173
+ findOne: async (filter) => await call("findOne", name, filter ?? {}),
1174
+ updateOne: async (filter, update) => await call("updateOne", name, filter, update),
1175
+ updateMany: async (filter, update) => await call("updateMany", name, filter, update),
1176
+ deleteOne: async (filter) => await call("deleteOne", name, filter),
1177
+ deleteMany: async (filter) => await call("deleteMany", name, filter),
1178
+ count: async (filter) => await call("count", name, filter ?? {}),
1179
+ aggregate: async (pipeline) => await call("aggregate", name, pipeline),
1180
+ createIndex: async (field) => await call("createIndex", name, field),
1181
+ dropIndex: async (field) => await call("dropIndex", name, field),
1182
+ createCompoundIndex: async (fields) => await call("createCompoundIndex", name, fields),
1183
+ dropCompoundIndex: async (fields) => await call("dropCompoundIndex", name, fields),
1184
+ createFtsIndex: async (field) => await call("createFtsIndex", name, field),
1185
+ dropFtsIndex: async (field) => await call("dropFtsIndex", name, field),
1060
1186
  createVectorIndex: async (field, options) => {
1061
- const opts2 = {};
1062
- if (options.metric) opts2.metric = options.metric;
1063
- if (options.hnswM || options.hnswEfConstruction) {
1064
- opts2.hnsw = { m: options.hnswM, efConstruction: options.hnswEfConstruction };
1065
- }
1066
- return native.createVectorIndex(name, field, options.dimensions, opts2);
1187
+ const request = vectorIndexRequest(field, options);
1188
+ await vectorCommand({ ...request, deferBuild: true });
1189
+ if (request.options) await createVectorClient(vectorCommand).rebuildVectorIndex(field, request.options);
1067
1190
  },
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: [] }),
1073
- findNearest: async (field, vector, topK, filter) => {
1074
- const raw = native.findNearest(name, field, vector, topK, filter ?? null);
1191
+ dropVectorIndex: async (field) => await call("dropVectorIndex", name, field),
1192
+ upgradeVectorIndex: async (field) => await call("upgradeVectorIndex", name, field),
1193
+ listIndexes: async () => call("listIndexes", name),
1194
+ findNearest: async (field, vector, topK, filter, options) => {
1195
+ if (options) return (await createVectorClient(vectorCommand).searchVectors(field, vector, topK, filter, options)).hits;
1196
+ const raw = await call("findNearest", name, field, vector, topK, filter ?? null);
1075
1197
  return raw;
1076
1198
  },
1077
1199
  searchText: async (field, query, topK, filter, options) => {
1078
- if (!native.searchText) {
1200
+ if (!native.callAsync && !native.searchText) {
1079
1201
  throw new Error("searchText requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
1080
1202
  }
1081
- return native.searchText(name, field, query, topK, filter ?? null, options ?? null);
1203
+ return await call("searchText", name, field, query, topK, filter ?? null, options ?? null);
1082
1204
  },
1083
1205
  hybridSearch: async (text, vector, topK, filter, options) => {
1084
- if (!native.hybridSearch) {
1206
+ if (!native.callAsync && !native.hybridSearch) {
1085
1207
  throw new Error("hybridSearch requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
1086
1208
  }
1087
- return native.hybridSearch(name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
1209
+ return await call("hybridSearch", name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
1088
1210
  },
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)
1211
+ subscribe: (filter, callback, onError) => makePoller(async () => await call("find", name, filter ?? {}), callback, onError),
1212
+ subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => await call("aggregate", name, pipeline), callback, onError)
1091
1213
  };
1092
1214
  return decorateCollection(wrapped, name, opts, webhook);
1093
1215
  }
1094
1216
  const handle = {
1095
1217
  collection: (name, opts) => wrapCollection(name, opts),
1096
- compact: async () => native.compact(),
1097
- close: async () => native.close(),
1098
- flush: native.flush ? async () => {
1099
- native.flush();
1218
+ compact: async () => await call("compact"),
1219
+ close: async () => await call("close"),
1220
+ flush: native.callAsync || native.flush ? async () => {
1221
+ await call("flush");
1222
+ } : void 0,
1223
+ rebuildVectorIndexes: native.callAsync || native.rebuildVectorIndexes ? async () => {
1224
+ await call("rebuildVectorIndexes");
1100
1225
  } : void 0,
1101
1226
  // One process owns the file — there is no other tab to defer to.
1102
1227
  isPrimary: async () => true
1103
1228
  };
1104
1229
  if (migrations?.length) {
1105
- if (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function") {
1230
+ if (!native.callAsync && (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function")) {
1106
1231
  throw new Error(
1107
1232
  "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
1233
  );
1109
1234
  }
1110
1235
  await runMigrations(
1111
1236
  handle,
1112
- async () => native.userVersion(),
1113
- async (v) => native.setUserVersion(v),
1237
+ async () => call("userVersion"),
1238
+ async (v) => call("setUserVersion", v),
1114
1239
  migrations
1115
1240
  );
1116
1241
  }
@@ -1185,6 +1310,7 @@ function attachWebhook(db, webhook) {
1185
1310
  export {
1186
1311
  TalaDbValidationError,
1187
1312
  applySchema,
1313
+ createVectorClient,
1188
1314
  createWebhookDispatcher,
1189
1315
  decorateCollection,
1190
1316
  deriveDocId,