@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.
- package/bin/backup.ts +169 -0
- package/bin/cliOperations.ts +92 -73
- package/bin/harper.ts +25 -6
- package/components/Scope.ts +52 -6
- package/components/componentLoader.ts +107 -9
- package/components/mcp/toolRegistry.ts +10 -0
- package/components/mcp/tools/application.ts +12 -5
- package/components/mcp/tools/operations.ts +3 -0
- package/components/mcp/tools/schemas/operationDescriptions.ts +3 -0
- package/components/mcp/tools/schemas/operations.ts +9 -0
- package/components/operations.js +1 -0
- package/components/operationsValidation.js +32 -2
- package/components/scopeMount.ts +150 -0
- package/config-root.schema.json +4 -0
- package/dataLayer/backupManifest.ts +102 -0
- package/dataLayer/blobBackup.ts +286 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +43 -0
- package/dataLayer/restoreMarker.ts +276 -0
- package/dataLayer/rocksdbBackup.ts +1100 -0
- package/dist/bin/backup.d.ts +9 -0
- package/dist/bin/backup.js +192 -0
- package/dist/bin/backup.js.map +1 -0
- package/dist/bin/cliOperations.d.ts +13 -0
- package/dist/bin/cliOperations.js +89 -70
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/harper.d.ts +7 -0
- package/dist/bin/harper.js +27 -6
- package/dist/bin/harper.js.map +1 -1
- package/dist/components/Scope.d.ts +37 -1
- package/dist/components/Scope.js +48 -6
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.d.ts +2 -6
- package/dist/components/componentLoader.js +91 -8
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +7 -0
- package/dist/components/mcp/toolRegistry.js +10 -0
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/application.js +11 -5
- package/dist/components/mcp/tools/application.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +3 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -0
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operations.js +9 -0
- package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
- package/dist/components/operations.js +2 -0
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +34 -2
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/scopeMount.d.ts +86 -0
- package/dist/components/scopeMount.js +131 -0
- package/dist/components/scopeMount.js.map +1 -0
- package/dist/dataLayer/backupManifest.d.ts +26 -0
- package/dist/dataLayer/backupManifest.js +97 -0
- package/dist/dataLayer/backupManifest.js.map +1 -0
- package/dist/dataLayer/blobBackup.d.ts +87 -0
- package/dist/dataLayer/blobBackup.js +282 -0
- package/dist/dataLayer/blobBackup.js.map +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +3 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +33 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +122 -0
- package/dist/dataLayer/restoreMarker.js +261 -0
- package/dist/dataLayer/restoreMarker.js.map +1 -0
- package/dist/dataLayer/rocksdbBackup.d.ts +127 -0
- package/dist/dataLayer/rocksdbBackup.js +1039 -0
- package/dist/dataLayer/rocksdbBackup.js.map +1 -0
- package/dist/resources/DatabaseTransaction.js +0 -6
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +0 -3
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/Table.js +22 -22
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/blob.d.ts +8 -0
- package/dist/resources/blob.js +16 -7
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/databases.d.ts +42 -1
- package/dist/resources/databases.js +276 -40
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/transaction.js +0 -3
- package/dist/resources/transaction.js.map +1 -1
- package/dist/server/REST.js +25 -9
- package/dist/server/REST.js.map +1 -1
- package/dist/server/fastifyRoutes.js +15 -1
- package/dist/server/fastifyRoutes.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +7 -1
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobProcess.js +20 -1
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobRunner.js +10 -0
- package/dist/server/jobs/jobRunner.js.map +1 -1
- package/dist/server/jobs/jobs.js +11 -0
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/middlewareChain.d.ts +10 -1
- package/dist/server/middlewareChain.js +81 -21
- package/dist/server/middlewareChain.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +8 -4
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +11 -0
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/static.js +11 -3
- package/dist/server/static.js.map +1 -1
- package/dist/utility/OperationFunctionCaller.js +25 -6
- package/dist/utility/OperationFunctionCaller.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +11 -1
- package/dist/utility/hdbTerms.js +11 -1
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +40 -0
- package/dist/utility/logging/harper_logger.js +746 -14
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/operation_authorization.js +13 -3
- package/dist/utility/operation_authorization.js.map +1 -1
- package/npm-shrinkwrap.json +3 -2
- package/package.json +4 -1
- package/resources/DESIGN.md +2 -0
- package/resources/DatabaseTransaction.ts +0 -3
- package/resources/ResourceInterface.ts +0 -3
- package/resources/Table.ts +20 -20
- package/resources/blob.ts +16 -6
- package/resources/databases.ts +272 -42
- package/resources/transaction.ts +0 -3
- package/server/DESIGN.md +16 -0
- package/server/REST.ts +25 -9
- package/server/fastifyRoutes.ts +20 -1
- package/server/itc/serverHandlers.js +7 -1
- package/server/jobs/jobProcess.ts +18 -1
- package/server/jobs/jobRunner.ts +10 -0
- package/server/jobs/jobs.ts +11 -0
- package/server/middlewareChain.ts +79 -20
- package/server/serverHelpers/serverHandlers.js +8 -4
- package/server/serverHelpers/serverUtilities.ts +19 -0
- package/server/static.ts +12 -3
- package/static/defaultConfig.yaml +1 -0
- package/studio/web/assets/{Chat-DK3GlWEb.js → Chat-DoVWScmq.js} +2 -2
- package/studio/web/assets/{Chat-DK3GlWEb.js.map → Chat-DoVWScmq.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-fBcC1Ew_.js → FloatingChat-UZ2NsUOZ.js} +4 -4
- package/studio/web/assets/{FloatingChat-fBcC1Ew_.js.map → FloatingChat-UZ2NsUOZ.js.map} +1 -1
- package/studio/web/assets/{apiToken-DJo1nakA.js → apiToken-BUI_04o7.js} +2 -2
- package/studio/web/assets/{apiToken-DJo1nakA.js.map → apiToken-BUI_04o7.js.map} +1 -1
- package/studio/web/assets/{applications-BDfH8urd.js → applications-D03NA7wW.js} +2 -2
- package/studio/web/assets/{applications-BDfH8urd.js.map → applications-D03NA7wW.js.map} +1 -1
- package/studio/web/assets/{index-BHo3c2Gk.js → index-Bh_CNAHr.js} +6 -6
- package/studio/web/assets/index-Bh_CNAHr.js.map +1 -0
- package/studio/web/assets/{index.lazy-BTo0y6UM.js → index.lazy-Dx3MpyDC.js} +4 -4
- package/studio/web/assets/{index.lazy-BTo0y6UM.js.map → index.lazy-Dx3MpyDC.js.map} +1 -1
- package/studio/web/assets/{notifications-CMxvWNnz.js → notifications-0edoFTsb.js} +2 -2
- package/studio/web/assets/{notifications-CMxvWNnz.js.map → notifications-0edoFTsb.js.map} +1 -1
- package/studio/web/assets/{notifications-D3GoB26g.js → notifications-CwKhipK7.js} +2 -2
- package/studio/web/assets/{notifications-D3GoB26g.js.map → notifications-CwKhipK7.js.map} +1 -1
- package/studio/web/assets/{profile-Doj5FVDE.js → profile-DUfEPQtx.js} +2 -2
- package/studio/web/assets/{profile-Doj5FVDE.js.map → profile-DUfEPQtx.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-yinsqJy0.js → setComponentFile-DMPo4UjC.js} +2 -2
- package/studio/web/assets/{setComponentFile-yinsqJy0.js.map → setComponentFile-DMPo4UjC.js.map} +1 -1
- package/studio/web/assets/{setup-DJwR0BHd.js → setup-B56Oz1_u.js} +2 -2
- package/studio/web/assets/{setup-DJwR0BHd.js.map → setup-B56Oz1_u.js.map} +1 -1
- package/studio/web/assets/{status-Br_AbsJs.js → status-BAod7p3o.js} +2 -2
- package/studio/web/assets/{status-Br_AbsJs.js.map → status-BAod7p3o.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-02XH5sVf.js → swagger-ui-react-lQrBxfwM.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-02XH5sVf.js.map → swagger-ui-react-lQrBxfwM.js.map} +1 -1
- package/studio/web/assets/{tsMode-D1DMKY7h.js → tsMode-CrHCRjTK.js} +2 -2
- package/studio/web/assets/{tsMode-D1DMKY7h.js.map → tsMode-CrHCRjTK.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-DtRblRgw.js → useEntityRestURL-DoaBMEvU.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-DtRblRgw.js.map → useEntityRestURL-DoaBMEvU.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/OperationFunctionCaller.ts +24 -3
- package/utility/hdbTerms.ts +11 -1
- package/utility/logging/harper_logger.ts +729 -15
- package/utility/operation_authorization.ts +31 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
903
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
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.
|