@harperfast/harper 5.2.0-beta.4 → 5.2.0

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.
Files changed (169) hide show
  1. package/bin/backup.ts +169 -0
  2. package/bin/cliOperations.ts +92 -73
  3. package/bin/harper.ts +25 -6
  4. package/components/Scope.ts +52 -6
  5. package/components/componentLoader.ts +107 -9
  6. package/components/mcp/toolRegistry.ts +10 -0
  7. package/components/mcp/tools/application.ts +12 -5
  8. package/components/mcp/tools/operations.ts +3 -0
  9. package/components/mcp/tools/schemas/operationDescriptions.ts +3 -0
  10. package/components/mcp/tools/schemas/operations.ts +9 -0
  11. package/components/operations.js +1 -0
  12. package/components/operationsValidation.js +32 -2
  13. package/components/scopeMount.ts +150 -0
  14. package/config-root.schema.json +4 -0
  15. package/dataLayer/backupManifest.ts +102 -0
  16. package/dataLayer/blobBackup.ts +286 -0
  17. package/dataLayer/harperBridge/ResourceBridge.ts +43 -0
  18. package/dataLayer/restoreMarker.ts +276 -0
  19. package/dataLayer/rocksdbBackup.ts +1100 -0
  20. package/dist/bin/backup.d.ts +9 -0
  21. package/dist/bin/backup.js +192 -0
  22. package/dist/bin/backup.js.map +1 -0
  23. package/dist/bin/cliOperations.d.ts +13 -0
  24. package/dist/bin/cliOperations.js +89 -70
  25. package/dist/bin/cliOperations.js.map +1 -1
  26. package/dist/bin/harper.d.ts +7 -0
  27. package/dist/bin/harper.js +27 -6
  28. package/dist/bin/harper.js.map +1 -1
  29. package/dist/components/Scope.d.ts +37 -1
  30. package/dist/components/Scope.js +48 -6
  31. package/dist/components/Scope.js.map +1 -1
  32. package/dist/components/componentLoader.d.ts +2 -6
  33. package/dist/components/componentLoader.js +91 -8
  34. package/dist/components/componentLoader.js.map +1 -1
  35. package/dist/components/mcp/toolRegistry.d.ts +7 -0
  36. package/dist/components/mcp/toolRegistry.js +10 -0
  37. package/dist/components/mcp/toolRegistry.js.map +1 -1
  38. package/dist/components/mcp/tools/application.js +11 -5
  39. package/dist/components/mcp/tools/application.js.map +1 -1
  40. package/dist/components/mcp/tools/operations.js +3 -0
  41. package/dist/components/mcp/tools/operations.js.map +1 -1
  42. package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -0
  43. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  44. package/dist/components/mcp/tools/schemas/operations.js +9 -0
  45. package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
  46. package/dist/components/operations.js +2 -0
  47. package/dist/components/operations.js.map +1 -1
  48. package/dist/components/operationsValidation.js +34 -2
  49. package/dist/components/operationsValidation.js.map +1 -1
  50. package/dist/components/scopeMount.d.ts +86 -0
  51. package/dist/components/scopeMount.js +131 -0
  52. package/dist/components/scopeMount.js.map +1 -0
  53. package/dist/dataLayer/backupManifest.d.ts +26 -0
  54. package/dist/dataLayer/backupManifest.js +97 -0
  55. package/dist/dataLayer/backupManifest.js.map +1 -0
  56. package/dist/dataLayer/blobBackup.d.ts +87 -0
  57. package/dist/dataLayer/blobBackup.js +282 -0
  58. package/dist/dataLayer/blobBackup.js.map +1 -0
  59. package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +3 -0
  60. package/dist/dataLayer/harperBridge/ResourceBridge.js +33 -0
  61. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  62. package/dist/dataLayer/restoreMarker.d.ts +122 -0
  63. package/dist/dataLayer/restoreMarker.js +261 -0
  64. package/dist/dataLayer/restoreMarker.js.map +1 -0
  65. package/dist/dataLayer/rocksdbBackup.d.ts +127 -0
  66. package/dist/dataLayer/rocksdbBackup.js +1039 -0
  67. package/dist/dataLayer/rocksdbBackup.js.map +1 -0
  68. package/dist/resources/DatabaseTransaction.js +0 -6
  69. package/dist/resources/DatabaseTransaction.js.map +1 -1
  70. package/dist/resources/ResourceInterface.d.ts +0 -3
  71. package/dist/resources/ResourceInterface.js.map +1 -1
  72. package/dist/resources/Table.js +22 -22
  73. package/dist/resources/Table.js.map +1 -1
  74. package/dist/resources/blob.d.ts +8 -0
  75. package/dist/resources/blob.js +16 -7
  76. package/dist/resources/blob.js.map +1 -1
  77. package/dist/resources/databases.d.ts +42 -1
  78. package/dist/resources/databases.js +276 -40
  79. package/dist/resources/databases.js.map +1 -1
  80. package/dist/resources/transaction.js +0 -3
  81. package/dist/resources/transaction.js.map +1 -1
  82. package/dist/server/REST.js +25 -9
  83. package/dist/server/REST.js.map +1 -1
  84. package/dist/server/fastifyRoutes.js +15 -1
  85. package/dist/server/fastifyRoutes.js.map +1 -1
  86. package/dist/server/itc/serverHandlers.js +7 -1
  87. package/dist/server/itc/serverHandlers.js.map +1 -1
  88. package/dist/server/jobs/jobProcess.js +20 -1
  89. package/dist/server/jobs/jobProcess.js.map +1 -1
  90. package/dist/server/jobs/jobRunner.js +10 -0
  91. package/dist/server/jobs/jobRunner.js.map +1 -1
  92. package/dist/server/jobs/jobs.js +11 -0
  93. package/dist/server/jobs/jobs.js.map +1 -1
  94. package/dist/server/middlewareChain.d.ts +10 -1
  95. package/dist/server/middlewareChain.js +81 -21
  96. package/dist/server/middlewareChain.js.map +1 -1
  97. package/dist/server/serverHelpers/serverHandlers.js +8 -4
  98. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  99. package/dist/server/serverHelpers/serverUtilities.js +11 -0
  100. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  101. package/dist/server/static.js +11 -3
  102. package/dist/server/static.js.map +1 -1
  103. package/dist/utility/OperationFunctionCaller.js +25 -6
  104. package/dist/utility/OperationFunctionCaller.js.map +1 -1
  105. package/dist/utility/hdbTerms.d.ts +11 -1
  106. package/dist/utility/hdbTerms.js +11 -1
  107. package/dist/utility/hdbTerms.js.map +1 -1
  108. package/dist/utility/logging/harper_logger.d.ts +40 -0
  109. package/dist/utility/logging/harper_logger.js +746 -14
  110. package/dist/utility/logging/harper_logger.js.map +1 -1
  111. package/dist/utility/operation_authorization.js +13 -3
  112. package/dist/utility/operation_authorization.js.map +1 -1
  113. package/npm-shrinkwrap.json +3 -2
  114. package/package.json +4 -1
  115. package/resources/DESIGN.md +2 -0
  116. package/resources/DatabaseTransaction.ts +0 -3
  117. package/resources/ResourceInterface.ts +0 -3
  118. package/resources/Table.ts +20 -20
  119. package/resources/blob.ts +16 -6
  120. package/resources/databases.ts +272 -42
  121. package/resources/transaction.ts +0 -3
  122. package/server/DESIGN.md +16 -0
  123. package/server/REST.ts +25 -9
  124. package/server/fastifyRoutes.ts +20 -1
  125. package/server/itc/serverHandlers.js +7 -1
  126. package/server/jobs/jobProcess.ts +18 -1
  127. package/server/jobs/jobRunner.ts +10 -0
  128. package/server/jobs/jobs.ts +11 -0
  129. package/server/middlewareChain.ts +79 -20
  130. package/server/serverHelpers/serverHandlers.js +8 -4
  131. package/server/serverHelpers/serverUtilities.ts +19 -0
  132. package/server/static.ts +12 -3
  133. package/static/defaultConfig.yaml +1 -0
  134. package/studio/web/assets/{Chat-DK3GlWEb.js → Chat-DoVWScmq.js} +2 -2
  135. package/studio/web/assets/{Chat-DK3GlWEb.js.map → Chat-DoVWScmq.js.map} +1 -1
  136. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js → FloatingChat-UZ2NsUOZ.js} +4 -4
  137. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js.map → FloatingChat-UZ2NsUOZ.js.map} +1 -1
  138. package/studio/web/assets/{apiToken-DJo1nakA.js → apiToken-BUI_04o7.js} +2 -2
  139. package/studio/web/assets/{apiToken-DJo1nakA.js.map → apiToken-BUI_04o7.js.map} +1 -1
  140. package/studio/web/assets/{applications-BDfH8urd.js → applications-D03NA7wW.js} +2 -2
  141. package/studio/web/assets/{applications-BDfH8urd.js.map → applications-D03NA7wW.js.map} +1 -1
  142. package/studio/web/assets/{index-BHo3c2Gk.js → index-Bh_CNAHr.js} +6 -6
  143. package/studio/web/assets/index-Bh_CNAHr.js.map +1 -0
  144. package/studio/web/assets/{index.lazy-BTo0y6UM.js → index.lazy-Dx3MpyDC.js} +4 -4
  145. package/studio/web/assets/{index.lazy-BTo0y6UM.js.map → index.lazy-Dx3MpyDC.js.map} +1 -1
  146. package/studio/web/assets/{notifications-CMxvWNnz.js → notifications-0edoFTsb.js} +2 -2
  147. package/studio/web/assets/{notifications-CMxvWNnz.js.map → notifications-0edoFTsb.js.map} +1 -1
  148. package/studio/web/assets/{notifications-D3GoB26g.js → notifications-CwKhipK7.js} +2 -2
  149. package/studio/web/assets/{notifications-D3GoB26g.js.map → notifications-CwKhipK7.js.map} +1 -1
  150. package/studio/web/assets/{profile-Doj5FVDE.js → profile-DUfEPQtx.js} +2 -2
  151. package/studio/web/assets/{profile-Doj5FVDE.js.map → profile-DUfEPQtx.js.map} +1 -1
  152. package/studio/web/assets/{setComponentFile-yinsqJy0.js → setComponentFile-DMPo4UjC.js} +2 -2
  153. package/studio/web/assets/{setComponentFile-yinsqJy0.js.map → setComponentFile-DMPo4UjC.js.map} +1 -1
  154. package/studio/web/assets/{setup-DJwR0BHd.js → setup-B56Oz1_u.js} +2 -2
  155. package/studio/web/assets/{setup-DJwR0BHd.js.map → setup-B56Oz1_u.js.map} +1 -1
  156. package/studio/web/assets/{status-Br_AbsJs.js → status-BAod7p3o.js} +2 -2
  157. package/studio/web/assets/{status-Br_AbsJs.js.map → status-BAod7p3o.js.map} +1 -1
  158. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js → swagger-ui-react-lQrBxfwM.js} +2 -2
  159. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js.map → swagger-ui-react-lQrBxfwM.js.map} +1 -1
  160. package/studio/web/assets/{tsMode-D1DMKY7h.js → tsMode-CrHCRjTK.js} +2 -2
  161. package/studio/web/assets/{tsMode-D1DMKY7h.js.map → tsMode-CrHCRjTK.js.map} +1 -1
  162. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js → useEntityRestURL-DoaBMEvU.js} +2 -2
  163. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js.map → useEntityRestURL-DoaBMEvU.js.map} +1 -1
  164. package/studio/web/index.html +1 -1
  165. package/utility/OperationFunctionCaller.ts +24 -3
  166. package/utility/hdbTerms.ts +11 -1
  167. package/utility/logging/harper_logger.ts +729 -15
  168. package/utility/operation_authorization.ts +31 -3
  169. package/studio/web/assets/index-BHo3c2Gk.js.map +0 -1
@@ -38,12 +38,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.databases = exports.tables = exports.databaseEventsEmitter = exports.NON_REPLICATING_SYSTEM_TABLES = void 0;
40
40
  exports.isReadOnlyMode = isReadOnlyMode;
41
+ exports.toRocksCompression = toRocksCompression;
41
42
  exports.getTables = getTables;
42
43
  exports.getDatabases = getDatabases;
43
44
  exports.readMetaDb = readMetaDb;
44
45
  exports.resetDatabases = resetDatabases;
46
+ exports.resolveDatabaseStorageRoot = resolveDatabaseStorageRoot;
47
+ exports.resolveDatabasePath = resolveDatabasePath;
45
48
  exports.database = database;
46
49
  exports.dropDatabase = dropDatabase;
50
+ exports.closeDatabase = closeDatabase;
51
+ exports.closeLoadedDatabases = closeLoadedDatabases;
47
52
  exports.table = table;
48
53
  exports.canonicalizeIndexOptions = canonicalizeIndexOptions;
49
54
  exports.dropTableMeta = dropTableMeta;
@@ -86,6 +91,7 @@ const RocksIndexStore_ts_1 = require("./RocksIndexStore.js");
86
91
  const when_ts_1 = require("../utility/when.js");
87
92
  const rocksMemoryConfig_ts_1 = require("../utility/rocksMemoryConfig.js");
88
93
  const processManagement_js_1 = require("../utility/processManagement/processManagement.js");
94
+ const restoreMarker_ts_1 = require("../dataLayer/restoreMarker.js");
89
95
  /**
90
96
  * Check if Harper is running in read-only mode.
91
97
  * Read-only mode can be enabled via:
@@ -148,8 +154,24 @@ exports.NON_REPLICATING_SYSTEM_TABLES = [
148
154
  exports.databaseEventsEmitter = new node_events_1.EventEmitter();
149
155
  exports.tables = Object.create(null);
150
156
  exports.databases = Object.create(null);
157
+ /**
158
+ * Map a persisted (LMDB-era) compression value to what rocksdb-js accepts. Table metadata
159
+ * carries values where a defined falsy value (false, '') means compression was explicitly
160
+ * disabled and true/{ threshold, ... } mean enabled with defaults. rocksdb-js >= 2.6
161
+ * validates `compression` strictly and treats UNSET as "use the build default (lz4)", so
162
+ * disabled must be mapped to 'none' explicitly, and true to unset.
163
+ */
164
+ function toRocksCompression(compression) {
165
+ if (compression === true)
166
+ return undefined;
167
+ if (compression !== undefined && !compression)
168
+ return 'none';
169
+ return compression;
170
+ }
151
171
  function openRocksDatabase(path, options) {
152
172
  options.disableWAL ??= true;
173
+ const legacyOptions = options;
174
+ legacyOptions.compression = toRocksCompression(legacyOptions.compression);
153
175
  // Apply read-only mode if enabled
154
176
  if (isReadOnlyMode()) {
155
177
  options.readOnly = true;
@@ -284,12 +306,21 @@ function getDatabases() {
284
306
  if (databasePath && (0, node_fs_1.existsSync)(databasePath)) {
285
307
  // First load all the databases from our main database folder
286
308
  // TODO: Load any databases defined with explicit storage paths from the config
287
- for (const databaseEntry of (0, node_fs_1.readdirSync)(databasePath, { withFileTypes: true })) {
309
+ const entries = (0, node_fs_1.readdirSync)(databasePath, { withFileTypes: true });
310
+ const blockedByRestore = databasesBlockedByRestore(databasePath);
311
+ for (const databaseEntry of entries) {
288
312
  // in-progress migration staging dirs are not databases until atomically renamed into place
289
313
  if (databaseEntry.name.endsWith(hdbTerms_ts_1.MIGRATING_DIR_SUFFIX))
290
314
  continue;
315
+ // the restore-metadata directory is reserved: never load it as a database, even if a
316
+ // (out-of-band) RocksDB directory happens to occupy that reserved name — the API can't
317
+ // create it (schemaRegex forbids the backtick), but the scan opens any CURRENT+MANIFEST dir
318
+ if (databaseEntry.name === restoreMarker_ts_1.RESTORE_META_DIR)
319
+ continue;
291
320
  const dbName = (0, path_1.basename)(databaseEntry.name, '.mdb');
292
321
  const dbPath = (0, path_1.join)(databasePath, databaseEntry.name);
322
+ if (blockedByRestore.has(dbName))
323
+ continue;
293
324
  if (databaseEntry.isFile() &&
294
325
  (0, path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb' &&
295
326
  !schemaConfigs[dbName]?.path) {
@@ -334,8 +365,14 @@ function getDatabases() {
334
365
  const schemaConfig = schemaConfigs[dbName];
335
366
  const databasePath = schemaConfig.path;
336
367
  if ((0, node_fs_1.existsSync)(databasePath)) {
337
- for (const databaseEntry of (0, node_fs_1.readdirSync)(databasePath, { withFileTypes: true })) {
368
+ const entries = (0, node_fs_1.readdirSync)(databasePath, { withFileTypes: true });
369
+ const blockedByRestore = databasesBlockedByRestore(databasePath);
370
+ for (const databaseEntry of entries) {
338
371
  if (databaseEntry.name.endsWith(hdbTerms_ts_1.MIGRATING_DIR_SUFFIX))
372
+ continue; // migration staging dir
373
+ if (databaseEntry.name === restoreMarker_ts_1.RESTORE_META_DIR)
374
+ continue; // reserved restore-metadata dir
375
+ if (blockedByRestore.has((0, path_1.basename)(databaseEntry.name, '.mdb')))
339
376
  continue;
340
377
  if (databaseEntry.isFile() && (0, path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb') {
341
378
  readMetaDb((0, path_1.join)(databasePath, databaseEntry.name), (0, path_1.basename)(databaseEntry.name, '.mdb'), dbName);
@@ -413,6 +450,27 @@ function getDatabases() {
413
450
  }
414
451
  return exports.databases;
415
452
  }
453
+ /**
454
+ * Scan a databases directory's entries for restore lock/marker files and return the names of
455
+ * databases that must not be loaded: a held restore lock means a restore is in progress in some
456
+ * process; an unheld lock with a surviving `.restoring` marker means a restore was interrupted
457
+ * mid-purge (the directory may be partial garbage) and must be rerun. The files live *next to*
458
+ * the database directory, so this also covers a database whose directory is missing or empty.
459
+ */
460
+ function databasesBlockedByRestore(databasePath) {
461
+ const blocked = new Set();
462
+ for (const [dbName, state] of (0, restoreMarker_ts_1.scanBlockedRestores)(databasePath)) {
463
+ if (state === 'in-progress') {
464
+ logger.warn(`A restore of database '${dbName}' is in progress; not loading it`);
465
+ blocked.add(dbName);
466
+ }
467
+ else if (state === 'incomplete') {
468
+ logger.error(`Incomplete restore of database '${dbName}' detected (a restore started but did not finish); not loading it — rerun the restore to recover`);
469
+ blocked.add(dbName);
470
+ }
471
+ }
472
+ return blocked;
473
+ }
416
474
  /**
417
475
  * This is responsible for reading the internal dbi of a single database file to get a list of all the tables and
418
476
  * their indexed or registered attributes
@@ -823,19 +881,12 @@ function setTable(tables, tableName, Table) {
823
881
  return Table;
824
882
  }
825
883
  /**
826
- * Get root store for a database
827
- * @param options
828
- * @returns
884
+ * Resolve the directory that holds (or would hold) a database's storage, from the databases
885
+ * config, storage path config/env, or the hdb root — without opening anything. This is the
886
+ * parent directory selection used by `database()`; a RocksDB database lives at
887
+ * `join(resolveDatabaseStorageRoot(...), databaseName)`.
829
888
  */
830
- function database({ database: databaseName, table: tableName }) {
831
- if (!databaseName)
832
- databaseName = DEFAULT_DATABASE_NAME;
833
- getDatabases();
834
- ensureDB(databaseName);
835
- const definedDatabase = definedDatabases.get(databaseName);
836
- if (definedDatabase?.rootStore) {
837
- return definedDatabase.rootStore;
838
- }
889
+ function resolveDatabaseStorageRoot(databaseName, tableName) {
839
890
  const databaseConfig = (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.DATABASES) || {};
840
891
  if (process.env.SCHEMAS_DATA_PATH) {
841
892
  databaseConfig.data = { path: process.env.SCHEMAS_DATA_PATH };
@@ -854,12 +905,44 @@ function database({ database: databaseName, table: tableName }) {
854
905
  if (!databasePath) {
855
906
  throw new Error(`Unable to determine database storage path. Ensure STORAGE_PATH, HDB_ROOT, or a valid config path is set.`);
856
907
  }
908
+ return databasePath;
909
+ }
910
+ /**
911
+ * Resolve the directory path of a RocksDB database (whether or not it exists or is loaded).
912
+ */
913
+ function resolveDatabasePath(databaseName) {
914
+ return (0, path_1.join)(resolveDatabaseStorageRoot(databaseName), databaseName);
915
+ }
916
+ /**
917
+ * Get root store for a database
918
+ * @param options
919
+ * @returns
920
+ */
921
+ function database({ database: databaseName, table: tableName }) {
922
+ if (!databaseName)
923
+ databaseName = DEFAULT_DATABASE_NAME;
924
+ getDatabases();
925
+ ensureDB(databaseName);
926
+ const definedDatabase = definedDatabases.get(databaseName);
927
+ if (definedDatabase?.rootStore) {
928
+ return definedDatabase.rootStore;
929
+ }
930
+ const databaseConfig = (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.DATABASES) || {};
931
+ if (process.env.SCHEMAS_DATA_PATH) {
932
+ databaseConfig.data = { path: process.env.SCHEMAS_DATA_PATH };
933
+ }
934
+ const tablePath = tableName && databaseConfig[databaseName]?.tables?.[tableName]?.path;
935
+ const databasePath = resolveDatabaseStorageRoot(databaseName, tableName);
857
936
  let rootStore;
858
937
  const useRocksdb = (process.env.HARPER_STORAGE_ENGINE || (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_ENGINE)) !== 'lmdb';
859
938
  if (useRocksdb) {
860
939
  const path = (0, path_1.join)(databasePath, tablePath ? tableName : databaseName);
861
940
  rootStore = rocksdbDatabaseEnvs.get(path);
862
941
  if (!rootStore || rootStore.status === 'closed') {
942
+ // this on-demand open (create_table/create_database and friends) must not resurrect a
943
+ // database that a restore is rewriting (or left half-purged) — the scan-time restore
944
+ // checks don't cover this path
945
+ throwIfBlockedByRestore(path, databaseName);
863
946
  rootStore = openRocksDatabase(path, {
864
947
  disableWAL: false,
865
948
  enableStats: true,
@@ -884,6 +967,49 @@ function database({ database: databaseName, table: tableName }) {
884
967
  definedDatabase.rootStore = rootStore;
885
968
  return rootStore;
886
969
  }
970
+ function throwIfBlockedByRestore(dbPath, databaseName) {
971
+ const restoreState = (0, restoreMarker_ts_1.checkRestoreState)(dbPath);
972
+ if (restoreState !== 'clear') {
973
+ const error = new Error(restoreState === 'in-progress'
974
+ ? `Database '${databaseName}' is being restored; retry when the restore completes`
975
+ : `Database '${databaseName}' has an incomplete restore; rerun restore_backup to recover it`);
976
+ error.statusCode = 409;
977
+ throw error;
978
+ }
979
+ }
980
+ /**
981
+ * Take the per-database restore lock for a drop, refusing (409) if a restore holds it (in-progress)
982
+ * or a crashed restore left a marker (incomplete). Pushes the acquired lock onto `held` so the
983
+ * caller releases it after the drop. On refusal, releases anything already held and throws.
984
+ *
985
+ * The lock is not reentrant within a process, so a path already in `held` must be skipped — every
986
+ * table in a RocksDB database shares one root store (and one lock path), and re-acquiring it in the
987
+ * same drop would spuriously 409 on the second table.
988
+ */
989
+ function lockDatabaseForDrop(dbPath, databaseName, held) {
990
+ if (held.some((h) => h.dbPath === dbPath))
991
+ return;
992
+ let lock;
993
+ try {
994
+ lock = (0, restoreMarker_ts_1.acquireRestoreLock)(dbPath);
995
+ }
996
+ catch (error) {
997
+ for (const h of held)
998
+ (0, restoreMarker_ts_1.releaseRestoreLock)(h);
999
+ throw error; // 409: a restore is in progress and holds the lock
1000
+ }
1001
+ // We now hold the lock, so no restore is active. A surviving marker is therefore debris from a
1002
+ // crashed restore (incomplete) — refuse rather than delete a directory that still needs recovery.
1003
+ if ((0, restoreMarker_ts_1.restoreMarkerPresent)(dbPath)) {
1004
+ (0, restoreMarker_ts_1.releaseRestoreLock)(lock);
1005
+ for (const h of held)
1006
+ (0, restoreMarker_ts_1.releaseRestoreLock)(h);
1007
+ const error = new Error(`Database '${databaseName}' has an incomplete restore; rerun restore_backup to recover it`);
1008
+ error.statusCode = 409;
1009
+ throw error;
1010
+ }
1011
+ held.push(lock);
1012
+ }
887
1013
  /**
888
1014
  * Delete the database
889
1015
  * @param databaseName
@@ -893,15 +1019,112 @@ async function dropDatabase(databaseName) {
893
1019
  throw new Error('Database does not exist');
894
1020
  const dbTables = exports.databases[databaseName];
895
1021
  let rootStore;
1022
+ // Hold the per-database restore lock across the entire drop so its file deletion can never
1023
+ // interleave with a restore's purge-and-copy on the same directory — a destroy landing after a
1024
+ // restore's copy would gut a "successful" restore, and vice versa. Restore takes the same lock
1025
+ // (before writing its marker), so both operations serialize on this one primitive rather than on
1026
+ // a check-then-act marker probe. Released in the finally below.
1027
+ const restoreLocks = [];
1028
+ try {
1029
+ for (const tableName in dbTables) {
1030
+ const table = dbTables[tableName];
1031
+ rootStore = table.primaryStore.rootStore;
1032
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase)
1033
+ lockDatabaseForDrop(rootStore.path, databaseName, restoreLocks);
1034
+ lmdbDatabaseEnvs.delete(rootStore.path);
1035
+ rocksdbDatabaseEnvs.delete(rootStore.path);
1036
+ }
1037
+ for (const tableName in dbTables) {
1038
+ exports.databaseEventsEmitter.emit('dropTable', tableName, databaseName);
1039
+ }
1040
+ if (databaseName === 'data') {
1041
+ for (const tableName in exports.tables) {
1042
+ delete exports.tables[tableName];
1043
+ }
1044
+ delete exports.tables[DEFINED_TABLES];
1045
+ }
1046
+ delete exports.databases[databaseName];
1047
+ exports.databaseEventsEmitter.emit('dropDatabase', databaseName);
1048
+ if (rootStore) {
1049
+ if (rootStore.status === 'open') {
1050
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1051
+ rootStore.close();
1052
+ rootStore.destroy();
1053
+ }
1054
+ else {
1055
+ await rootStore.close();
1056
+ await (0, promises_1.unlink)(rootStore.path);
1057
+ }
1058
+ }
1059
+ }
1060
+ else {
1061
+ rootStore = database({ database: databaseName, table: null });
1062
+ // a tableless database resolves its root store here rather than in the loop above, so take
1063
+ // the drop lock now (still before any destructive step)
1064
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase)
1065
+ lockDatabaseForDrop(rootStore.path, databaseName, restoreLocks);
1066
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1067
+ rootStore.close();
1068
+ rootStore.destroy();
1069
+ }
1070
+ else if (rootStore.status === 'open') {
1071
+ await rootStore.close();
1072
+ await (0, promises_1.unlink)(rootStore.path);
1073
+ }
1074
+ }
1075
+ await (0, blob_ts_1.deleteRootBlobPathsForDB)(rootStore);
1076
+ }
1077
+ finally {
1078
+ for (const lock of restoreLocks)
1079
+ (0, restoreMarker_ts_1.releaseRestoreLock)(lock);
1080
+ }
1081
+ }
1082
+ /**
1083
+ * Close a RocksDB database's store handles on the current thread and unregister it, without
1084
+ * touching its files. Used by the restore_backup flow: every thread must release its handles so
1085
+ * `backups.restore()` can purge and rewrite the (fully closed) database directory. A subsequent
1086
+ * `resetDatabases()`/`getDatabases()` rescan reloads it (or skips it while a restore is in
1087
+ * progress, per the restore marker checks in the scan).
1088
+ */
1089
+ function closeDatabase(databaseName) {
1090
+ const dbTables = exports.databases[databaseName];
1091
+ if (!dbTables)
1092
+ return false;
1093
+ const rootStores = new Set();
1094
+ const closeStore = (store, description) => {
1095
+ try {
1096
+ store?.close?.();
1097
+ }
1098
+ catch (error) {
1099
+ logger.warn(`Error closing ${description} while closing database ${databaseName}:`, error);
1100
+ }
1101
+ };
896
1102
  for (const tableName in dbTables) {
897
1103
  const table = dbTables[tableName];
898
- rootStore = table.primaryStore.rootStore;
1104
+ if (!table?.primaryStore)
1105
+ continue;
1106
+ if (table.primaryStore.rootStore)
1107
+ rootStores.add(table.primaryStore.rootStore);
1108
+ for (const indexName in table.indices || {}) {
1109
+ closeStore(table.indices[indexName], `index ${tableName}.${indexName}`);
1110
+ }
1111
+ closeStore(table.primaryStore, `table ${tableName}`);
1112
+ }
1113
+ // a database with no tables (an empty schema, or one whose tables were all dropped) still holds
1114
+ // an open root store, tracked only on the defined-database entry rather than any table — include
1115
+ // it so its handles are released too (the Set dedupes it against the per-table root stores above)
1116
+ const definedRoot = definedDatabases?.get(databaseName)?.rootStore;
1117
+ if (definedRoot)
1118
+ rootStores.add(definedRoot);
1119
+ for (const rootStore of rootStores) {
1120
+ closeStore(rootStore.dbisDb, 'attributes store');
1121
+ closeStore(rootStore, 'root store');
899
1122
  lmdbDatabaseEnvs.delete(rootStore.path);
900
1123
  rocksdbDatabaseEnvs.delete(rootStore.path);
901
1124
  }
902
- for (const tableName in dbTables) {
903
- exports.databaseEventsEmitter.emit('dropTable', tableName, databaseName);
904
- }
1125
+ const definedDatabase = definedDatabases?.get(databaseName);
1126
+ if (definedDatabase)
1127
+ definedDatabase.rootStore = undefined;
905
1128
  if (databaseName === 'data') {
906
1129
  for (const tableName in exports.tables) {
907
1130
  delete exports.tables[tableName];
@@ -909,31 +1132,42 @@ async function dropDatabase(databaseName) {
909
1132
  delete exports.tables[DEFINED_TABLES];
910
1133
  }
911
1134
  delete exports.databases[databaseName];
912
- exports.databaseEventsEmitter.emit('dropDatabase', databaseName);
913
- if (rootStore) {
914
- if (rootStore.status === 'open') {
915
- if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
916
- rootStore.close();
917
- rootStore.destroy();
918
- }
919
- else {
920
- await rootStore.close();
921
- await (0, promises_1.unlink)(rootStore.path);
1135
+ return true;
1136
+ }
1137
+ /**
1138
+ * Close every RocksDB (user) database this thread has open, releasing its native handles.
1139
+ *
1140
+ * rocksdb-js's registry is process-global across worker threads, and a thread that exits WITHOUT
1141
+ * closing leaks its handles (the process-global refCount never drops), while the only alternative,
1142
+ * `shutdown()`, tears down rocksdb for the entire process. So a worker thread that opens databases
1143
+ * and then exits — notably a job worker (jobProcess), which opens the whole database graph via
1144
+ * `getDatabases()` and exits when the job finishes — must close its handles explicitly, or those
1145
+ * handles linger process-wide (and, e.g., block an online `restore_backup` from confirming the
1146
+ * database is closed). The `system` database is intentionally left open: it is non-enumerable here
1147
+ * (skipped by the loop), is never restored online, and the exiting worker may still touch the job
1148
+ * table during teardown. Best-effort: closing failures are swallowed inside `closeDatabase`.
1149
+ */
1150
+ function closeLoadedDatabases() {
1151
+ // snapshot the names first: closeDatabase() deletes from `databases` as it goes
1152
+ for (const databaseName of Object.keys(exports.databases)) {
1153
+ const dbTables = exports.databases[databaseName];
1154
+ if (!dbTables)
1155
+ continue;
1156
+ let isRocks = false;
1157
+ for (const tableName in dbTables) {
1158
+ if (dbTables[tableName]?.primaryStore?.rootStore instanceof rocksdb_js_1.RocksDatabase) {
1159
+ isRocks = true;
1160
+ break;
922
1161
  }
923
1162
  }
924
- }
925
- else {
926
- rootStore = database({ database: databaseName, table: null });
927
- if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
928
- rootStore.close();
929
- rootStore.destroy();
930
- }
931
- else if (rootStore.status === 'open') {
932
- await rootStore.close();
933
- await (0, promises_1.unlink)(rootStore.path);
1163
+ // a tableless database exposes no table root store, so also check the defined-database
1164
+ // entry — otherwise its open root store would leak on worker exit
1165
+ if (!isRocks && definedDatabases?.get(databaseName)?.rootStore instanceof rocksdb_js_1.RocksDatabase) {
1166
+ isRocks = true;
934
1167
  }
1168
+ if (isRocks)
1169
+ closeDatabase(databaseName);
935
1170
  }
936
- await (0, blob_ts_1.deleteRootBlobPathsForDB)(rootStore);
937
1171
  }
938
1172
  // HNSW_NO_AUTOVERSION kill-switch: when set, a NEW index initializes as legacy rather than
939
1173
  // versioned. process.env values are strings, so a bare truthiness check would treat "0"/"false"
@@ -1950,7 +2184,9 @@ function getDefaultCompression() {
1950
2184
  LMDB_COMPRESSION_OPTS['dictionary'] = (0, node_fs_1.readFileSync)(STORAGE_COMPRESSION_DICTIONARY);
1951
2185
  if (STORAGE_COMPRESSION_THRESHOLD)
1952
2186
  LMDB_COMPRESSION_OPTS['threshold'] = STORAGE_COMPRESSION_THRESHOLD;
1953
- return LMDB_COMPRESSION && LMDB_COMPRESSION_OPTS;
2187
+ // normalize disabled to false so a falsy config value ('' or null) is never persisted
2188
+ // into table metadata as-is (openRocksDatabase maps defined-falsy to 'none')
2189
+ return LMDB_COMPRESSION ? LMDB_COMPRESSION_OPTS : false;
1954
2190
  }
1955
2191
  /**
1956
2192
  * Force all RocksDB databases to flush to disk.