mlIDbStorage.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. import {
  2. DEFAULT_ML_SEARCH_CONFIG,
  3. DEFAULT_ML_SYNC_CONFIG,
  4. DEFAULT_ML_SYNC_JOB_CONFIG,
  5. MAX_ML_SYNC_ERROR_COUNT,
  6. } from 'constants/mlConfig';
  7. import {
  8. openDB,
  9. deleteDB,
  10. DBSchema,
  11. IDBPDatabase,
  12. IDBPTransaction,
  13. StoreNames,
  14. } from 'idb';
  15. import { Config } from 'types/common/config';
  16. import {
  17. Face,
  18. MlFileData,
  19. MLLibraryData,
  20. Person,
  21. RealWorldObject,
  22. Thing,
  23. } from 'types/machineLearning';
  24. import { IndexStatus } from 'types/machineLearning/ui';
  25. import { runningInBrowser, runningInElectron } from 'utils/common';
  26. import { addLogLine } from 'utils/logging';
  27. import { logError } from 'utils/sentry';
  28. export const ML_SYNC_JOB_CONFIG_NAME = 'ml-sync-job';
  29. export const ML_SYNC_CONFIG_NAME = 'ml-sync';
  30. export const ML_SEARCH_CONFIG_NAME = 'ml-search';
  31. const MLDATA_DB_NAME = 'mldata';
  32. interface MLDb extends DBSchema {
  33. files: {
  34. key: number;
  35. value: MlFileData;
  36. indexes: { mlVersion: [number, number] };
  37. };
  38. people: {
  39. key: number;
  40. value: Person;
  41. };
  42. things: {
  43. key: number;
  44. value: Thing;
  45. };
  46. versions: {
  47. key: string;
  48. value: number;
  49. };
  50. library: {
  51. key: string;
  52. value: MLLibraryData;
  53. };
  54. configs: {
  55. key: string;
  56. value: Config;
  57. };
  58. }
  59. class MLIDbStorage {
  60. public _db: Promise<IDBPDatabase<MLDb>>;
  61. constructor() {
  62. if (!runningInBrowser() || !runningInElectron()) {
  63. return;
  64. }
  65. this.db;
  66. }
  67. private openDB(): Promise<IDBPDatabase<MLDb>> {
  68. return openDB<MLDb>(MLDATA_DB_NAME, 3, {
  69. terminated: async () => {
  70. console.error('ML Indexed DB terminated');
  71. logError(new Error(), 'ML Indexed DB terminated');
  72. this._db = undefined;
  73. // TODO: remove if there is chance of this going into recursion in some case
  74. await this.db;
  75. },
  76. blocked() {
  77. // TODO: make sure we dont allow multiple tabs of app
  78. console.error('ML Indexed DB blocked');
  79. logError(new Error(), 'ML Indexed DB blocked');
  80. },
  81. blocking() {
  82. // TODO: make sure we dont allow multiple tabs of app
  83. console.error('ML Indexed DB blocking');
  84. logError(new Error(), 'ML Indexed DB blocking');
  85. },
  86. async upgrade(db, oldVersion, newVersion, tx) {
  87. if (oldVersion < 1) {
  88. const filesStore = db.createObjectStore('files', {
  89. keyPath: 'fileId',
  90. });
  91. filesStore.createIndex('mlVersion', [
  92. 'mlVersion',
  93. 'errorCount',
  94. ]);
  95. db.createObjectStore('people', {
  96. keyPath: 'id',
  97. });
  98. db.createObjectStore('things', {
  99. keyPath: 'id',
  100. });
  101. db.createObjectStore('versions');
  102. db.createObjectStore('library');
  103. }
  104. if (oldVersion < 2) {
  105. // TODO: update configs if version is updated in defaults
  106. db.createObjectStore('configs');
  107. await tx
  108. .objectStore('configs')
  109. .add(
  110. DEFAULT_ML_SYNC_JOB_CONFIG,
  111. ML_SYNC_JOB_CONFIG_NAME
  112. );
  113. await tx
  114. .objectStore('configs')
  115. .add(DEFAULT_ML_SYNC_CONFIG, ML_SYNC_CONFIG_NAME);
  116. }
  117. if (oldVersion < 3) {
  118. await tx
  119. .objectStore('configs')
  120. .add(DEFAULT_ML_SEARCH_CONFIG, ML_SEARCH_CONFIG_NAME);
  121. }
  122. addLogLine(
  123. `Ml DB upgraded to version: ${newVersion} from version: ${oldVersion}`
  124. );
  125. },
  126. });
  127. }
  128. public get db(): Promise<IDBPDatabase<MLDb>> {
  129. if (!this._db) {
  130. this._db = this.openDB();
  131. addLogLine('Opening Ml DB');
  132. }
  133. return this._db;
  134. }
  135. public async clearMLDB() {
  136. const db = await this.db;
  137. db.close();
  138. await deleteDB(MLDATA_DB_NAME);
  139. addLogLine('Cleared Ml DB');
  140. this._db = undefined;
  141. await this.db;
  142. }
  143. public async getAllFileIds() {
  144. const db = await this.db;
  145. return db.getAllKeys('files');
  146. }
  147. public async putAllFilesInTx(mlFiles: Array<MlFileData>) {
  148. const db = await this.db;
  149. const tx = db.transaction('files', 'readwrite');
  150. await Promise.all(mlFiles.map((mlFile) => tx.store.put(mlFile)));
  151. await tx.done;
  152. }
  153. public async removeAllFilesInTx(fileIds: Array<number>) {
  154. const db = await this.db;
  155. const tx = db.transaction('files', 'readwrite');
  156. await Promise.all(fileIds.map((fileId) => tx.store.delete(fileId)));
  157. await tx.done;
  158. }
  159. public async newTransaction<
  160. Name extends StoreNames<MLDb>,
  161. Mode extends IDBTransactionMode = 'readonly'
  162. >(storeNames: Name, mode?: Mode) {
  163. const db = await this.db;
  164. return db.transaction(storeNames, mode);
  165. }
  166. public async commit(tx: IDBPTransaction<MLDb>) {
  167. return tx.done;
  168. }
  169. public async getAllFileIdsForUpdate(
  170. tx: IDBPTransaction<MLDb, ['files'], 'readwrite'>
  171. ) {
  172. return tx.store.getAllKeys();
  173. }
  174. public async getFileIds(
  175. count: number,
  176. limitMlVersion: number,
  177. maxErrorCount: number
  178. ) {
  179. const db = await this.db;
  180. const tx = db.transaction('files', 'readonly');
  181. const index = tx.store.index('mlVersion');
  182. let cursor = await index.openKeyCursor(
  183. IDBKeyRange.upperBound([limitMlVersion], true)
  184. );
  185. const fileIds: number[] = [];
  186. while (cursor && fileIds.length < count) {
  187. if (
  188. cursor.key[0] < limitMlVersion &&
  189. cursor.key[1] <= maxErrorCount
  190. ) {
  191. fileIds.push(cursor.primaryKey);
  192. }
  193. cursor = await cursor.continue();
  194. }
  195. await tx.done;
  196. return fileIds;
  197. }
  198. public async getFile(fileId: number) {
  199. const db = await this.db;
  200. return db.get('files', fileId);
  201. }
  202. public async getAllFiles() {
  203. const db = await this.db;
  204. return db.getAll('files');
  205. }
  206. public async putFile(mlFile: MlFileData) {
  207. const db = await this.db;
  208. return db.put('files', mlFile);
  209. }
  210. public async upsertFileInTx(
  211. fileId: number,
  212. upsert: (mlFile: MlFileData) => MlFileData
  213. ) {
  214. const db = await this.db;
  215. const tx = db.transaction('files', 'readwrite');
  216. const existing = await tx.store.get(fileId);
  217. const updated = upsert(existing);
  218. await tx.store.put(updated);
  219. await tx.done;
  220. return updated;
  221. }
  222. public async putAllFiles(
  223. mlFiles: Array<MlFileData>,
  224. tx: IDBPTransaction<MLDb, ['files'], 'readwrite'>
  225. ) {
  226. await Promise.all(mlFiles.map((mlFile) => tx.store.put(mlFile)));
  227. }
  228. public async removeAllFiles(
  229. fileIds: Array<number>,
  230. tx: IDBPTransaction<MLDb, ['files'], 'readwrite'>
  231. ) {
  232. await Promise.all(fileIds.map((fileId) => tx.store.delete(fileId)));
  233. }
  234. public async getAllFacesMap() {
  235. const startTime = Date.now();
  236. const db = await this.db;
  237. const allFiles = await db.getAll('files');
  238. const allFacesMap = new Map<number, Array<Face>>();
  239. allFiles.forEach(
  240. (mlFileData) =>
  241. mlFileData.faces &&
  242. allFacesMap.set(mlFileData.fileId, mlFileData.faces)
  243. );
  244. addLogLine('getAllFacesMap', Date.now() - startTime, 'ms');
  245. return allFacesMap;
  246. }
  247. public async updateFaces(allFacesMap: Map<number, Face[]>) {
  248. const startTime = Date.now();
  249. const db = await this.db;
  250. const tx = db.transaction('files', 'readwrite');
  251. let cursor = await tx.store.openCursor();
  252. while (cursor) {
  253. if (allFacesMap.has(cursor.key)) {
  254. const mlFileData = { ...cursor.value };
  255. mlFileData.faces = allFacesMap.get(cursor.key);
  256. cursor.update(mlFileData);
  257. }
  258. cursor = await cursor.continue();
  259. }
  260. await tx.done;
  261. addLogLine('updateFaces', Date.now() - startTime, 'ms');
  262. }
  263. public async getAllObjectsMap() {
  264. const startTime = Date.now();
  265. const db = await this.db;
  266. const allFiles = await db.getAll('files');
  267. const allObjectsMap = new Map<number, Array<RealWorldObject>>();
  268. allFiles.forEach(
  269. (mlFileData) =>
  270. mlFileData.objects &&
  271. allObjectsMap.set(mlFileData.fileId, mlFileData.objects)
  272. );
  273. addLogLine('allObjectsMap', Date.now() - startTime, 'ms');
  274. return allObjectsMap;
  275. }
  276. public async getPerson(id: number) {
  277. const db = await this.db;
  278. return db.get('people', id);
  279. }
  280. public async getAllPeople() {
  281. const db = await this.db;
  282. return db.getAll('people');
  283. }
  284. public async putPerson(person: Person) {
  285. const db = await this.db;
  286. return db.put('people', person);
  287. }
  288. public async clearAllPeople() {
  289. const db = await this.db;
  290. return db.clear('people');
  291. }
  292. public async getAllThings() {
  293. const db = await this.db;
  294. return db.getAll('things');
  295. }
  296. public async putThing(thing: Thing) {
  297. const db = await this.db;
  298. return db.put('things', thing);
  299. }
  300. public async clearAllThings() {
  301. const db = await this.db;
  302. return db.clear('things');
  303. }
  304. public async getIndexVersion(index: string) {
  305. const db = await this.db;
  306. return db.get('versions', index);
  307. }
  308. public async incrementIndexVersion(index: StoreNames<MLDb>) {
  309. if (index === 'versions') {
  310. throw new Error('versions store can not be versioned');
  311. }
  312. const db = await this.db;
  313. const tx = db.transaction(['versions', index], 'readwrite');
  314. let version = await tx.objectStore('versions').get(index);
  315. version = (version || 0) + 1;
  316. tx.objectStore('versions').put(version, index);
  317. await tx.done;
  318. return version;
  319. }
  320. public async setIndexVersion(index: string, version: number) {
  321. const db = await this.db;
  322. return db.put('versions', version, index);
  323. }
  324. public async getLibraryData() {
  325. const db = await this.db;
  326. return db.get('library', 'data');
  327. }
  328. public async putLibraryData(data: MLLibraryData) {
  329. const db = await this.db;
  330. return db.put('library', data, 'data');
  331. }
  332. public async getConfig<T extends Config>(name: string, def: T) {
  333. const db = await this.db;
  334. const tx = db.transaction('configs', 'readwrite');
  335. let config = (await tx.store.get(name)) as T;
  336. if (!config) {
  337. config = def;
  338. await tx.store.put(def, name);
  339. }
  340. await tx.done;
  341. return config;
  342. }
  343. public async putConfig(name: string, data: Config) {
  344. const db = await this.db;
  345. return db.put('configs', data, name);
  346. }
  347. public async getIndexStatus(latestMlVersion: number): Promise<IndexStatus> {
  348. const db = await this.db;
  349. const tx = db.transaction(['files', 'versions'], 'readonly');
  350. const mlVersionIdx = tx.objectStore('files').index('mlVersion');
  351. let outOfSyncCursor = await mlVersionIdx.openKeyCursor(
  352. IDBKeyRange.upperBound([latestMlVersion], true)
  353. );
  354. let outOfSyncFilesExists = false;
  355. while (outOfSyncCursor && !outOfSyncFilesExists) {
  356. if (
  357. outOfSyncCursor.key[0] < latestMlVersion &&
  358. outOfSyncCursor.key[1] <= MAX_ML_SYNC_ERROR_COUNT
  359. ) {
  360. outOfSyncFilesExists = true;
  361. }
  362. outOfSyncCursor = await outOfSyncCursor.continue();
  363. }
  364. const nSyncedFiles = await mlVersionIdx.count(
  365. IDBKeyRange.lowerBound([latestMlVersion])
  366. );
  367. const nTotalFiles = await mlVersionIdx.count();
  368. const filesIndexVersion = await tx.objectStore('versions').get('files');
  369. const peopleIndexVersion = await tx
  370. .objectStore('versions')
  371. .get('people');
  372. const filesIndexVersionExists =
  373. filesIndexVersion !== null && filesIndexVersion !== undefined;
  374. const peopleIndexVersionExists =
  375. peopleIndexVersion !== null && peopleIndexVersion !== undefined;
  376. await tx.done;
  377. return {
  378. outOfSyncFilesExists,
  379. nSyncedFiles,
  380. nTotalFiles,
  381. localFilesSynced: filesIndexVersionExists,
  382. peopleIndexSynced:
  383. peopleIndexVersionExists &&
  384. peopleIndexVersion === filesIndexVersion,
  385. };
  386. }
  387. // for debug purpose
  388. public async getAllMLData() {
  389. const db = await this.db;
  390. const tx = db.transaction(db.objectStoreNames, 'readonly');
  391. const allMLData: any = {};
  392. for (const store of tx.objectStoreNames) {
  393. const keys = await tx.objectStore(store).getAllKeys();
  394. const data = await tx.objectStore(store).getAll();
  395. allMLData[store] = {};
  396. for (let i = 0; i < keys.length; i++) {
  397. allMLData[store][keys[i]] = data[i];
  398. }
  399. }
  400. await tx.done;
  401. const files = allMLData['files'];
  402. for (const fileId of Object.keys(files)) {
  403. const fileData = files[fileId];
  404. fileData.faces?.forEach(
  405. (f) => (f.embedding = Array.from(f.embedding))
  406. );
  407. }
  408. return allMLData;
  409. }
  410. // for debug purpose, this will overwrite all data
  411. public async putAllMLData(allMLData: Map<string, any>) {
  412. const db = await this.db;
  413. const tx = db.transaction(db.objectStoreNames, 'readwrite');
  414. for (const store of tx.objectStoreNames) {
  415. const records = allMLData[store];
  416. if (!records) {
  417. continue;
  418. }
  419. const txStore = tx.objectStore(store);
  420. if (store === 'files') {
  421. const files = records;
  422. for (const fileId of Object.keys(files)) {
  423. const fileData = files[fileId];
  424. fileData.faces?.forEach(
  425. (f) => (f.embedding = Float32Array.from(f.embedding))
  426. );
  427. }
  428. }
  429. await txStore.clear();
  430. for (const key of Object.keys(records)) {
  431. if (txStore.keyPath) {
  432. txStore.put(records[key]);
  433. } else {
  434. txStore.put(records[key], key);
  435. }
  436. }
  437. }
  438. await tx.done;
  439. }
  440. }
  441. export default new MLIDbStorage();