@harperfast/harper 5.2.0-beta.4 → 5.2.1
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/copyDb.ts +21 -4
- package/bin/harper.ts +43 -55
- package/bin/help.ts +216 -0
- package/components/Application.ts +236 -46
- package/components/ApplicationScope.ts +26 -0
- package/components/EntryHandler.ts +410 -105
- package/components/RuntimeModuleTracker.ts +189 -0
- package/components/Scope.ts +120 -30
- package/components/componentLoader.ts +177 -25
- package/components/deployLifecycle.ts +119 -33
- package/components/mcp/toolRegistry.ts +10 -0
- package/components/mcp/tools/application.ts +12 -5
- package/components/mcp/tools/operations.ts +9 -0
- package/components/mcp/tools/schemas/operationDescriptions.ts +3 -0
- package/components/mcp/tools/schemas/operations.ts +9 -0
- package/components/operations.js +5 -6
- package/components/operationsValidation.js +32 -2
- package/components/scopeMount.ts +150 -0
- package/config/configUtils.ts +12 -9
- package/config-root.schema.json +14 -0
- package/dataLayer/backupManifest.ts +102 -0
- package/dataLayer/blobBackup.ts +286 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +69 -5
- package/dataLayer/hdbInfoController.ts +8 -0
- package/dataLayer/restoreMarker.ts +276 -0
- package/dataLayer/rocksdbBackup.ts +1100 -0
- package/dataLayer/schemaDescribe.ts +2 -1
- 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/copyDb.js +13 -2
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/harper.d.ts +13 -0
- package/dist/bin/harper.js +45 -56
- package/dist/bin/harper.js.map +1 -1
- package/dist/bin/help.d.ts +8 -0
- package/dist/bin/help.js +192 -0
- package/dist/bin/help.js.map +1 -0
- package/dist/components/Application.d.ts +16 -1
- package/dist/components/Application.js +210 -38
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +7 -0
- package/dist/components/ApplicationScope.js +22 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/EntryHandler.d.ts +4 -4
- package/dist/components/EntryHandler.js +386 -95
- package/dist/components/EntryHandler.js.map +1 -1
- package/dist/components/RuntimeModuleTracker.d.ts +11 -0
- package/dist/components/RuntimeModuleTracker.js +189 -0
- package/dist/components/RuntimeModuleTracker.js.map +1 -0
- package/dist/components/Scope.d.ts +38 -1
- package/dist/components/Scope.js +117 -28
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.d.ts +2 -6
- package/dist/components/componentLoader.js +160 -18
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deployLifecycle.d.ts +6 -2
- package/dist/components/deployLifecycle.js +109 -31
- package/dist/components/deployLifecycle.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 +9 -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 +6 -6
- 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/config/configUtils.js +13 -9
- package/dist/config/configUtils.js.map +1 -1
- 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 +45 -5
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.js +4 -0
- package/dist/dataLayer/hdbInfoController.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/dataLayer/schemaDescribe.js +2 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +55 -0
- package/dist/resources/DatabaseTransaction.js +282 -109
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -5
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/Resources.js +22 -4
- package/dist/resources/Resources.js.map +1 -1
- package/dist/resources/Table.d.ts +6 -5
- package/dist/resources/Table.js +114 -37
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +6 -6
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/blob.d.ts +8 -1
- package/dist/resources/blob.js +31 -14
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/databases.d.ts +59 -1
- package/dist/resources/databases.js +386 -41
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/jsResource.d.ts +4 -26
- package/dist/resources/jsResource.js +5 -59
- package/dist/resources/jsResource.js.map +1 -1
- package/dist/resources/models/Models.d.ts +11 -1
- package/dist/resources/models/Models.js +10 -1
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +9 -0
- package/dist/resources/models/backendRegistry.js +10 -0
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/openaiStream.d.ts +16 -1
- package/dist/resources/models/openaiStream.js +113 -21
- package/dist/resources/models/openaiStream.js.map +1 -1
- package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
- package/dist/resources/models/v1/chatCompletions.js +115 -0
- package/dist/resources/models/v1/chatCompletions.js.map +1 -0
- package/dist/resources/models/v1/embeddings.d.ts +11 -0
- package/dist/resources/models/v1/embeddings.js +71 -0
- package/dist/resources/models/v1/embeddings.js.map +1 -0
- package/dist/resources/models/v1/errors.d.ts +54 -0
- package/dist/resources/models/v1/errors.js +130 -0
- package/dist/resources/models/v1/errors.js.map +1 -0
- package/dist/resources/models/v1/index.d.ts +36 -0
- package/dist/resources/models/v1/index.js +75 -0
- package/dist/resources/models/v1/index.js.map +1 -0
- package/dist/resources/models/v1/models.d.ts +26 -0
- package/dist/resources/models/v1/models.js +44 -0
- package/dist/resources/models/v1/models.js.map +1 -0
- package/dist/resources/models/v1/translation.d.ts +133 -0
- package/dist/resources/models/v1/translation.js +298 -0
- package/dist/resources/models/v1/translation.js.map +1 -0
- package/dist/resources/roles.d.ts +1 -1
- package/dist/resources/roles.js +54 -7
- package/dist/resources/roles.js.map +1 -1
- package/dist/resources/transaction.js +0 -3
- package/dist/resources/transaction.js.map +1 -1
- package/dist/security/jsLoader.js +84 -33
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/role.js +4 -0
- package/dist/security/role.js.map +1 -1
- package/dist/security/superUserGuard.d.ts +7 -0
- package/dist/security/superUserGuard.js +23 -0
- package/dist/security/superUserGuard.js.map +1 -0
- package/dist/security/tokenAuthentication.d.ts +0 -1
- package/dist/security/tokenAuthentication.js +6 -2
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/security/user.d.ts +6 -1
- package/dist/security/user.js +23 -1
- package/dist/security/user.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/http.d.ts +23 -2
- package/dist/server/http.js +99 -12
- package/dist/server/http.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/operationsServer.d.ts +0 -2
- package/dist/server/operationsServer.js.map +1 -1
- package/dist/server/serverHelpers/multipartParser.js +9 -0
- package/dist/server/serverHelpers/multipartParser.js.map +1 -1
- package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
- package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
- package/dist/server/serverHelpers/registeredOperations.js +14 -13
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +27 -7
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +21 -7
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/static.js +68 -21
- package/dist/server/static.js.map +1 -1
- package/dist/server/storageReclamation.d.ts +17 -0
- package/dist/server/storageReclamation.js +90 -6
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/socketRouter.js +20 -0
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +12 -0
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/sqlEngine/diff/differential.js +7 -3
- package/dist/sqlEngine/diff/differential.js.map +1 -1
- package/dist/sqlTranslator/index.js +6 -1
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/upgrade/upgradePrompt.d.ts +2 -2
- package/dist/upgrade/upgradePrompt.js +22 -3
- package/dist/upgrade/upgradePrompt.js.map +1 -1
- package/dist/utility/OperationFunctionCaller.js +25 -6
- package/dist/utility/OperationFunctionCaller.js.map +1 -1
- package/dist/utility/errors/commonErrors.d.ts +1 -0
- package/dist/utility/errors/commonErrors.js +1 -0
- package/dist/utility/errors/commonErrors.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +13 -1
- package/dist/utility/hdbTerms.js +13 -1
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/install/installer.js +32 -0
- package/dist/utility/install/installer.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +47 -0
- package/dist/utility/logging/harper_logger.js +803 -38
- 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 +198 -193
- package/package.json +12 -7
- package/resources/DESIGN.md +2 -0
- package/resources/DatabaseTransaction.ts +310 -104
- package/resources/ResourceInterface.ts +8 -5
- package/resources/Resources.ts +22 -4
- package/resources/Table.ts +341 -263
- package/resources/analytics/write.ts +22 -20
- package/resources/blob.ts +31 -14
- package/resources/databases.ts +387 -46
- package/resources/jsResource.ts +5 -62
- package/resources/models/Models.ts +14 -1
- package/resources/models/backendRegistry.ts +10 -0
- package/resources/models/openaiStream.ts +131 -19
- package/resources/models/v1/chatCompletions.ts +128 -0
- package/resources/models/v1/embeddings.ts +70 -0
- package/resources/models/v1/errors.ts +141 -0
- package/resources/models/v1/index.ts +72 -0
- package/resources/models/v1/models.ts +53 -0
- package/resources/models/v1/translation.ts +362 -0
- package/resources/roles.ts +67 -7
- package/resources/transaction.ts +0 -3
- package/security/jsLoader.ts +84 -30
- package/security/role.ts +7 -0
- package/security/superUserGuard.ts +20 -0
- package/security/tokenAuthentication.ts +6 -3
- package/security/user.ts +26 -1
- package/server/DESIGN.md +61 -34
- package/server/REST.ts +25 -9
- package/server/fastifyRoutes.ts +20 -1
- package/server/http.ts +100 -13
- 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/operationsServer.ts +0 -2
- package/server/serverHelpers/multipartParser.ts +9 -0
- package/server/serverHelpers/operationAuthorizationState.ts +11 -0
- package/server/serverHelpers/registeredOperations.ts +19 -15
- package/server/serverHelpers/serverHandlers.js +28 -7
- package/server/serverHelpers/serverUtilities.ts +29 -7
- package/server/static.ts +87 -23
- package/server/storageReclamation.ts +104 -8
- package/server/threads/socketRouter.ts +20 -0
- package/server/threads/threadServer.js +11 -0
- package/sqlTranslator/index.ts +6 -1
- package/static/defaultConfig.yaml +3 -0
- package/studio/web/assets/{Chat-DK3GlWEb.js → Chat-aApwhRmz.js} +2 -2
- package/studio/web/assets/{Chat-DK3GlWEb.js.map → Chat-aApwhRmz.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-fBcC1Ew_.js → FloatingChat-sC0H91n9.js} +4 -4
- package/studio/web/assets/{FloatingChat-fBcC1Ew_.js.map → FloatingChat-sC0H91n9.js.map} +1 -1
- package/studio/web/assets/{apiToken-DJo1nakA.js → apiToken-DZ8JhHOI.js} +2 -2
- package/studio/web/assets/{apiToken-DJo1nakA.js.map → apiToken-DZ8JhHOI.js.map} +1 -1
- package/studio/web/assets/{applications-BDfH8urd.js → applications-9SEVLIO5.js} +2 -2
- package/studio/web/assets/{applications-BDfH8urd.js.map → applications-9SEVLIO5.js.map} +1 -1
- package/studio/web/assets/{index-BHo3c2Gk.js → index-Dy3uDGXb.js} +6 -6
- package/studio/web/assets/index-Dy3uDGXb.js.map +1 -0
- package/studio/web/assets/{index.lazy-BTo0y6UM.js → index.lazy-CN1zq4I4.js} +4 -4
- package/studio/web/assets/{index.lazy-BTo0y6UM.js.map → index.lazy-CN1zq4I4.js.map} +1 -1
- package/studio/web/assets/{notifications-CMxvWNnz.js → notifications-BFAF07xr.js} +2 -2
- package/studio/web/assets/{notifications-CMxvWNnz.js.map → notifications-BFAF07xr.js.map} +1 -1
- package/studio/web/assets/{notifications-D3GoB26g.js → notifications-CKlYVvVN.js} +2 -2
- package/studio/web/assets/{notifications-D3GoB26g.js.map → notifications-CKlYVvVN.js.map} +1 -1
- package/studio/web/assets/{profile-Doj5FVDE.js → profile-A1zhEdFG.js} +2 -2
- package/studio/web/assets/{profile-Doj5FVDE.js.map → profile-A1zhEdFG.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-yinsqJy0.js → setComponentFile-SEtBt_GV.js} +2 -2
- package/studio/web/assets/{setComponentFile-yinsqJy0.js.map → setComponentFile-SEtBt_GV.js.map} +1 -1
- package/studio/web/assets/{setup-DJwR0BHd.js → setup-BisINqdH.js} +2 -2
- package/studio/web/assets/{setup-DJwR0BHd.js.map → setup-BisINqdH.js.map} +1 -1
- package/studio/web/assets/{status-Br_AbsJs.js → status-BuQoCc7l.js} +2 -2
- package/studio/web/assets/{status-Br_AbsJs.js.map → status-BuQoCc7l.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-02XH5sVf.js → swagger-ui-react-CAi_s1PC.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-02XH5sVf.js.map → swagger-ui-react-CAi_s1PC.js.map} +1 -1
- package/studio/web/assets/{tsMode-D1DMKY7h.js → tsMode-DT74tlkM.js} +2 -2
- package/studio/web/assets/{tsMode-D1DMKY7h.js.map → tsMode-DT74tlkM.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-DtRblRgw.js → useEntityRestURL-CGRGc1n7.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-DtRblRgw.js.map → useEntityRestURL-CGRGc1n7.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/upgradePrompt.ts +22 -3
- package/utility/OperationFunctionCaller.ts +24 -3
- package/utility/errors/commonErrors.ts +2 -0
- package/utility/hdbTerms.ts +13 -1
- package/utility/install/installer.ts +37 -0
- package/utility/logging/harper_logger.ts +786 -41
- package/utility/operation_authorization.ts +31 -3
- package/studio/web/assets/index-BHo3c2Gk.js.map +0 -1
package/resources/databases.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { handleLocalTimeForGets } from './RecordEncoder.ts';
|
|
|
33
33
|
import { deleteRootBlobPathsForDB } from './blob.ts';
|
|
34
34
|
import { CUSTOM_INDEXES } from './indexes/customIndexes.ts';
|
|
35
35
|
import { OpenDBIObject } from '../utility/lmdb/OpenDBIObject.ts';
|
|
36
|
-
import { RocksDatabase, type RocksDatabaseOptions } from '@harperfast/rocksdb-js';
|
|
36
|
+
import { RocksDatabase, supportedCompression, type RocksDatabaseOptions } from '@harperfast/rocksdb-js';
|
|
37
37
|
import { PrimaryRocksDatabase } from './PrimaryRocksDatabase.ts';
|
|
38
38
|
import { replayLogs } from './replayLogs.ts';
|
|
39
39
|
import { totalmem } from 'node:os';
|
|
@@ -41,6 +41,15 @@ import { RocksIndexStore } from './RocksIndexStore.ts';
|
|
|
41
41
|
import { when } from '../utility/when.ts';
|
|
42
42
|
import { resolveRocksMemoryConfig } from '../utility/rocksMemoryConfig.ts';
|
|
43
43
|
import { isProcessRunning } from '../utility/processManagement/processManagement.js';
|
|
44
|
+
import {
|
|
45
|
+
acquireRestoreLock,
|
|
46
|
+
checkRestoreState,
|
|
47
|
+
releaseRestoreLock,
|
|
48
|
+
restoreMarkerPresent,
|
|
49
|
+
scanBlockedRestores,
|
|
50
|
+
RESTORE_META_DIR,
|
|
51
|
+
type RestoreLock,
|
|
52
|
+
} from '../dataLayer/restoreMarker.ts';
|
|
44
53
|
|
|
45
54
|
/**
|
|
46
55
|
* Check if Harper is running in read-only mode.
|
|
@@ -89,6 +98,80 @@ const DEFAULT_DATABASE_NAME = 'data';
|
|
|
89
98
|
const DEFINED_TABLES = Symbol('defined-tables');
|
|
90
99
|
const DEFAULT_COMPRESSION_THRESHOLD = (envGet(CONFIG_PARAMS.STORAGE_PAGESIZE) || 4096) - 60; // larger than this requires multiple pages
|
|
91
100
|
initSync();
|
|
101
|
+
/**
|
|
102
|
+
* The RocksDB block/blob codec for every column family this process opens (`storage.rocks.compression`),
|
|
103
|
+
* or `undefined` to leave rocksdb-js on its own default (lz4 wherever the native build has it).
|
|
104
|
+
*
|
|
105
|
+
* Resolved on the first open and then frozen, deliberately. RocksDB fixes a column family's codec
|
|
106
|
+
* for as long as it is open and rejects a reopen that disagrees, and Harper's worker threads share
|
|
107
|
+
* one process-wide column-family registry — so every open, in every thread, has to resolve the same
|
|
108
|
+
* value. Re-reading config per open does not guarantee that: on a fresh install the system families
|
|
109
|
+
* are created by `mountHdb()` before the config file exists, so the main thread would resolve
|
|
110
|
+
* nothing and the workers would resolve the configured codec, after which `__dbis__` cannot be
|
|
111
|
+
* reopened and Harper fails with "The system database failed to load". The installer stages this
|
|
112
|
+
* value before `mountHdb()` (see utility/install/installer.ts) so that first open already sees it.
|
|
113
|
+
*
|
|
114
|
+
* Unset is NOT "use the build default" for a family that already exists — see toRocksCompression.
|
|
115
|
+
*/
|
|
116
|
+
let resolvedRocksCompression: string | undefined;
|
|
117
|
+
let rocksCompressionResolved = false;
|
|
118
|
+
|
|
119
|
+
export function getRocksCompression(): string | undefined {
|
|
120
|
+
if (!rocksCompressionResolved) {
|
|
121
|
+
resolvedRocksCompression = readDatabaseCodec();
|
|
122
|
+
rocksCompressionResolved = true;
|
|
123
|
+
}
|
|
124
|
+
return resolvedRocksCompression;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Test-only: un-freezes the resolved codec. Production code never calls this — the freeze is the
|
|
129
|
+
* invariant (see getRocksCompression above) — but a test process runs many unrelated test files in
|
|
130
|
+
* one process, so whichever file happens to open a RocksDatabase first freezes this for everyone
|
|
131
|
+
* after it. Tests that need to exercise config changes call this to get back to the unresolved state.
|
|
132
|
+
*/
|
|
133
|
+
export function resetRocksCompression(): void {
|
|
134
|
+
resolvedRocksCompression = undefined;
|
|
135
|
+
rocksCompressionResolved = false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The codec every column family in this process opens under.
|
|
140
|
+
*
|
|
141
|
+
* Compression is a deployment setting, not a per-table one. RocksDB opens all of a database's
|
|
142
|
+
* column families in one call, so the codec has to be decided before the first open — which is
|
|
143
|
+
* before Harper has read any table's metadata (that catalog is itself one of the families being
|
|
144
|
+
* opened). Resolving one codec from configuration and applying it to every family is what makes
|
|
145
|
+
* that possible; it is passed with `compressionForAllColumnFamilies` so families this process
|
|
146
|
+
* never names individually adopt it too, which is what lets a database created before the codec
|
|
147
|
+
* existed start compressing.
|
|
148
|
+
*
|
|
149
|
+
* `storage.rocks.compression` names a codec outright. Otherwise `storage.compression` (default
|
|
150
|
+
* true) decides enabled-or-not and the build default fills in the algorithm. Per-table metadata
|
|
151
|
+
* still records the LMDB-era boolean, but no longer selects: a table persisted as disabled inside
|
|
152
|
+
* a deployment that enables compression would need its own codec, and it cannot have one.
|
|
153
|
+
*/
|
|
154
|
+
function readDatabaseCodec(): string | undefined {
|
|
155
|
+
const explicit = readRocksCompressionConfig();
|
|
156
|
+
if (explicit) return explicit;
|
|
157
|
+
return toRocksCompression(getDefaultCompression()) as string | undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function readRocksCompressionConfig(): string | undefined {
|
|
161
|
+
const configured = envGet(CONFIG_PARAMS.STORAGE_ROCKS_COMPRESSION);
|
|
162
|
+
if (configured === undefined || configured === null || configured === '') return undefined;
|
|
163
|
+
const requested = String(configured).trim().toLowerCase();
|
|
164
|
+
if (!requested) return undefined;
|
|
165
|
+
// Rejected here rather than at the open: an unsupported name throws inside RocksDatabase.open,
|
|
166
|
+
// which surfaces as the system database failing to load partway through startup.
|
|
167
|
+
if (!supportedCompression.includes(requested)) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`storage.rocks.compression="${requested}" is not available in this build of @harperfast/rocksdb-js. Supported: ${supportedCompression.join(', ')}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return requested;
|
|
173
|
+
}
|
|
174
|
+
|
|
92
175
|
// I don't know if this is the best place for this, but somewhere we need to specify which tables
|
|
93
176
|
// replicate by default:
|
|
94
177
|
export const NON_REPLICATING_SYSTEM_TABLES = [
|
|
@@ -167,8 +250,52 @@ export const databaseEventsEmitter = new EventEmitter<DatabaseWatcherEventMap>()
|
|
|
167
250
|
export const tables: Tables = Object.create(null);
|
|
168
251
|
export const databases: Databases = Object.create(null);
|
|
169
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Codec used to honor an "enabled, unspecified" compression setting, or `undefined` where the
|
|
255
|
+
* native build cannot provide it (in which case the request degrades to the build default rather
|
|
256
|
+
* than throwing).
|
|
257
|
+
*/
|
|
258
|
+
const DEFAULT_ENABLED_CODEC = supportedCompression.includes('lz4') ? 'lz4' : undefined;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Map a persisted (LMDB-era) compression value to what rocksdb-js accepts. Table metadata carries
|
|
262
|
+
* values where a defined falsy value (false, '') means compression was explicitly disabled, and
|
|
263
|
+
* `true` / `{ threshold, ... }` mean enabled with defaults — `storage.compression` defaults to
|
|
264
|
+
* `true` (defaultConfig.yaml), so essentially every pre-existing table asked for compression.
|
|
265
|
+
*
|
|
266
|
+
* "Enabled" resolves to an explicit codec rather than to unset. Unset is not equivalent: RocksDB
|
|
267
|
+
* persists the codec per column family and a reopen that requests nothing inherits what the family
|
|
268
|
+
* already has, applying the build default only when the family does not yet exist. Leaving these
|
|
269
|
+
* unset therefore silently ignores the operator's request on every database created before the
|
|
270
|
+
* native build carried codecs — it keeps writing uncompressed forever, while a brand-new database
|
|
271
|
+
* gets lz4. Naming the codec makes the setting mean the same thing in both cases.
|
|
272
|
+
*
|
|
273
|
+
* This governs newly written files; existing SSTs keep their codec until write traffic rewrites
|
|
274
|
+
* them (`db.compact()` will not — see getRocksCompression above).
|
|
275
|
+
*/
|
|
276
|
+
export function toRocksCompression(compression: unknown): unknown {
|
|
277
|
+
if (compression === undefined) return undefined;
|
|
278
|
+
if (!compression) return 'none';
|
|
279
|
+
// An object carrying an explicit `algorithm` is already a rocksdb-js request; anything else
|
|
280
|
+
// (`true`, or an LMDB descriptor like { startingOffset, threshold }) is "enabled, unspecified".
|
|
281
|
+
if (compression === true || (typeof compression === 'object' && !(compression as { algorithm?: unknown }).algorithm))
|
|
282
|
+
return DEFAULT_ENABLED_CODEC;
|
|
283
|
+
return compression;
|
|
284
|
+
}
|
|
285
|
+
|
|
170
286
|
function openRocksDatabase(path: string, options: RocksDatabaseOptions & { dupSort?: boolean }) {
|
|
171
287
|
options.disableWAL ??= true;
|
|
288
|
+
const legacyOptions = options as { compression?: unknown };
|
|
289
|
+
// A configured codec applies to every column family, overriding whatever per-table metadata
|
|
290
|
+
// carries — that metadata records the LMDB-era boolean, so without this there is no way to
|
|
291
|
+
// select a RocksDB codec for a deployment.
|
|
292
|
+
// One codec for every column family, and applied to every family this open touches — not just
|
|
293
|
+
// the one being named. RocksDB opens them all at once and a family's codec cannot change while
|
|
294
|
+
// it is open, so a family this process never names individually would otherwise stay on
|
|
295
|
+
// whatever it was created with, forever.
|
|
296
|
+
const databaseCodec = getRocksCompression();
|
|
297
|
+
legacyOptions.compression = databaseCodec;
|
|
298
|
+
if (databaseCodec) (options as { compressionForAllColumnFamilies?: boolean }).compressionForAllColumnFamilies = true;
|
|
172
299
|
// Apply read-only mode if enabled
|
|
173
300
|
if (isReadOnlyMode()) {
|
|
174
301
|
options.readOnly = true;
|
|
@@ -315,11 +442,18 @@ export function getDatabases(): Databases {
|
|
|
315
442
|
if (databasePath && existsSync(databasePath)) {
|
|
316
443
|
// First load all the databases from our main database folder
|
|
317
444
|
// TODO: Load any databases defined with explicit storage paths from the config
|
|
318
|
-
|
|
445
|
+
const entries = readdirSync(databasePath, { withFileTypes: true });
|
|
446
|
+
const blockedByRestore = databasesBlockedByRestore(databasePath);
|
|
447
|
+
for (const databaseEntry of entries) {
|
|
319
448
|
// in-progress migration staging dirs are not databases until atomically renamed into place
|
|
320
449
|
if (databaseEntry.name.endsWith(MIGRATING_DIR_SUFFIX)) continue;
|
|
450
|
+
// the restore-metadata directory is reserved: never load it as a database, even if a
|
|
451
|
+
// (out-of-band) RocksDB directory happens to occupy that reserved name — the API can't
|
|
452
|
+
// create it (schemaRegex forbids the backtick), but the scan opens any CURRENT+MANIFEST dir
|
|
453
|
+
if (databaseEntry.name === RESTORE_META_DIR) continue;
|
|
321
454
|
const dbName = basename(databaseEntry.name, '.mdb');
|
|
322
455
|
const dbPath = join(databasePath, databaseEntry.name);
|
|
456
|
+
if (blockedByRestore.has(dbName)) continue;
|
|
323
457
|
|
|
324
458
|
if (
|
|
325
459
|
databaseEntry.isFile() &&
|
|
@@ -376,8 +510,12 @@ export function getDatabases(): Databases {
|
|
|
376
510
|
const schemaConfig = schemaConfigs[dbName];
|
|
377
511
|
const databasePath = schemaConfig.path;
|
|
378
512
|
if (existsSync(databasePath)) {
|
|
379
|
-
|
|
380
|
-
|
|
513
|
+
const entries = readdirSync(databasePath, { withFileTypes: true });
|
|
514
|
+
const blockedByRestore = databasesBlockedByRestore(databasePath);
|
|
515
|
+
for (const databaseEntry of entries) {
|
|
516
|
+
if (databaseEntry.name.endsWith(MIGRATING_DIR_SUFFIX)) continue; // migration staging dir
|
|
517
|
+
if (databaseEntry.name === RESTORE_META_DIR) continue; // reserved restore-metadata dir
|
|
518
|
+
if (blockedByRestore.has(basename(databaseEntry.name, '.mdb'))) continue;
|
|
381
519
|
if (databaseEntry.isFile() && extname(databaseEntry.name).toLowerCase() === '.mdb') {
|
|
382
520
|
readMetaDb(join(databasePath, databaseEntry.name), basename(databaseEntry.name, '.mdb'), dbName);
|
|
383
521
|
} else {
|
|
@@ -452,6 +590,29 @@ export function getDatabases(): Databases {
|
|
|
452
590
|
return databases;
|
|
453
591
|
}
|
|
454
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Scan a databases directory's entries for restore lock/marker files and return the names of
|
|
595
|
+
* databases that must not be loaded: a held restore lock means a restore is in progress in some
|
|
596
|
+
* process; an unheld lock with a surviving `.restoring` marker means a restore was interrupted
|
|
597
|
+
* mid-purge (the directory may be partial garbage) and must be rerun. The files live *next to*
|
|
598
|
+
* the database directory, so this also covers a database whose directory is missing or empty.
|
|
599
|
+
*/
|
|
600
|
+
function databasesBlockedByRestore(databasePath: string): Set<string> {
|
|
601
|
+
const blocked = new Set<string>();
|
|
602
|
+
for (const [dbName, state] of scanBlockedRestores(databasePath)) {
|
|
603
|
+
if (state === 'in-progress') {
|
|
604
|
+
logger.warn(`A restore of database '${dbName}' is in progress; not loading it`);
|
|
605
|
+
blocked.add(dbName);
|
|
606
|
+
} else if (state === 'incomplete') {
|
|
607
|
+
logger.error(
|
|
608
|
+
`Incomplete restore of database '${dbName}' detected (a restore started but did not finish); not loading it — rerun the restore to recover`
|
|
609
|
+
);
|
|
610
|
+
blocked.add(dbName);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return blocked;
|
|
614
|
+
}
|
|
615
|
+
|
|
455
616
|
/**
|
|
456
617
|
* This is responsible for reading the internal dbi of a single database file to get a list of all the tables and
|
|
457
618
|
* their indexed or registered attributes
|
|
@@ -530,7 +691,7 @@ function initStores(
|
|
|
530
691
|
...internalDbiInit,
|
|
531
692
|
disableWAL: false,
|
|
532
693
|
name: INTERNAL_DBIS_NAME,
|
|
533
|
-
} as any)
|
|
694
|
+
} as any);
|
|
534
695
|
} else {
|
|
535
696
|
attributesDbi = rootStore.openDB(INTERNAL_DBIS_NAME, internalDbiInit as any);
|
|
536
697
|
}
|
|
@@ -916,18 +1077,12 @@ function setTable(tables, tableName, Table) {
|
|
|
916
1077
|
return Table;
|
|
917
1078
|
}
|
|
918
1079
|
/**
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
1080
|
+
* Resolve the directory that holds (or would hold) a database's storage, from the databases
|
|
1081
|
+
* config, storage path config/env, or the hdb root — without opening anything. This is the
|
|
1082
|
+
* parent directory selection used by `database()`; a RocksDB database lives at
|
|
1083
|
+
* `join(resolveDatabaseStorageRoot(...), databaseName)`.
|
|
922
1084
|
*/
|
|
923
|
-
export function
|
|
924
|
-
if (!databaseName) databaseName = DEFAULT_DATABASE_NAME;
|
|
925
|
-
getDatabases();
|
|
926
|
-
ensureDB(databaseName);
|
|
927
|
-
const definedDatabase = definedDatabases.get(databaseName);
|
|
928
|
-
if ((definedDatabase as any)?.rootStore) {
|
|
929
|
-
return (definedDatabase as any).rootStore;
|
|
930
|
-
}
|
|
1085
|
+
export function resolveDatabaseStorageRoot(databaseName: string, tableName?: string): string {
|
|
931
1086
|
const databaseConfig = envGet(CONFIG_PARAMS.DATABASES) || {};
|
|
932
1087
|
if (process.env.SCHEMAS_DATA_PATH) {
|
|
933
1088
|
databaseConfig.data = { path: process.env.SCHEMAS_DATA_PATH };
|
|
@@ -952,6 +1107,35 @@ export function database({ database: databaseName, table: tableName }) {
|
|
|
952
1107
|
`Unable to determine database storage path. Ensure STORAGE_PATH, HDB_ROOT, or a valid config path is set.`
|
|
953
1108
|
);
|
|
954
1109
|
}
|
|
1110
|
+
return databasePath;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Resolve the directory path of a RocksDB database (whether or not it exists or is loaded).
|
|
1115
|
+
*/
|
|
1116
|
+
export function resolveDatabasePath(databaseName: string): string {
|
|
1117
|
+
return join(resolveDatabaseStorageRoot(databaseName), databaseName);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Get root store for a database
|
|
1122
|
+
* @param options
|
|
1123
|
+
* @returns
|
|
1124
|
+
*/
|
|
1125
|
+
export function database({ database: databaseName, table: tableName }) {
|
|
1126
|
+
if (!databaseName) databaseName = DEFAULT_DATABASE_NAME;
|
|
1127
|
+
getDatabases();
|
|
1128
|
+
ensureDB(databaseName);
|
|
1129
|
+
const definedDatabase = definedDatabases.get(databaseName);
|
|
1130
|
+
if ((definedDatabase as any)?.rootStore) {
|
|
1131
|
+
return (definedDatabase as any).rootStore;
|
|
1132
|
+
}
|
|
1133
|
+
const databaseConfig = envGet(CONFIG_PARAMS.DATABASES) || {};
|
|
1134
|
+
if (process.env.SCHEMAS_DATA_PATH) {
|
|
1135
|
+
databaseConfig.data = { path: process.env.SCHEMAS_DATA_PATH };
|
|
1136
|
+
}
|
|
1137
|
+
const tablePath = tableName && databaseConfig[databaseName]?.tables?.[tableName]?.path;
|
|
1138
|
+
const databasePath = resolveDatabaseStorageRoot(databaseName, tableName);
|
|
955
1139
|
|
|
956
1140
|
let rootStore: RootDatabaseKind;
|
|
957
1141
|
const useRocksdb = (process.env.HARPER_STORAGE_ENGINE || envGet(CONFIG_PARAMS.STORAGE_ENGINE)) !== 'lmdb';
|
|
@@ -959,6 +1143,10 @@ export function database({ database: databaseName, table: tableName }) {
|
|
|
959
1143
|
const path = join(databasePath, tablePath ? tableName : databaseName);
|
|
960
1144
|
rootStore = rocksdbDatabaseEnvs.get(path);
|
|
961
1145
|
if (!rootStore || rootStore.status === 'closed') {
|
|
1146
|
+
// this on-demand open (create_table/create_database and friends) must not resurrect a
|
|
1147
|
+
// database that a restore is rewriting (or left half-purged) — the scan-time restore
|
|
1148
|
+
// checks don't cover this path
|
|
1149
|
+
throwIfBlockedByRestore(path, databaseName);
|
|
962
1150
|
rootStore = openRocksDatabase(path, {
|
|
963
1151
|
disableWAL: false,
|
|
964
1152
|
enableStats: true,
|
|
@@ -981,6 +1169,51 @@ export function database({ database: databaseName, table: tableName }) {
|
|
|
981
1169
|
if (definedDatabase) (definedDatabase as any).rootStore = rootStore;
|
|
982
1170
|
return rootStore;
|
|
983
1171
|
}
|
|
1172
|
+
function throwIfBlockedByRestore(dbPath: string, databaseName: string): void {
|
|
1173
|
+
const restoreState = checkRestoreState(dbPath);
|
|
1174
|
+
if (restoreState !== 'clear') {
|
|
1175
|
+
const error: any = new Error(
|
|
1176
|
+
restoreState === 'in-progress'
|
|
1177
|
+
? `Database '${databaseName}' is being restored; retry when the restore completes`
|
|
1178
|
+
: `Database '${databaseName}' has an incomplete restore; rerun restore_backup to recover it`
|
|
1179
|
+
);
|
|
1180
|
+
error.statusCode = 409;
|
|
1181
|
+
throw error;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Take the per-database restore lock for a drop, refusing (409) if a restore holds it (in-progress)
|
|
1187
|
+
* or a crashed restore left a marker (incomplete). Pushes the acquired lock onto `held` so the
|
|
1188
|
+
* caller releases it after the drop. On refusal, releases anything already held and throws.
|
|
1189
|
+
*
|
|
1190
|
+
* The lock is not reentrant within a process, so a path already in `held` must be skipped — every
|
|
1191
|
+
* table in a RocksDB database shares one root store (and one lock path), and re-acquiring it in the
|
|
1192
|
+
* same drop would spuriously 409 on the second table.
|
|
1193
|
+
*/
|
|
1194
|
+
function lockDatabaseForDrop(dbPath: string, databaseName: string, held: RestoreLock[]): void {
|
|
1195
|
+
if (held.some((h) => h.dbPath === dbPath)) return;
|
|
1196
|
+
let lock: RestoreLock;
|
|
1197
|
+
try {
|
|
1198
|
+
lock = acquireRestoreLock(dbPath);
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
for (const h of held) releaseRestoreLock(h);
|
|
1201
|
+
throw error; // 409: a restore is in progress and holds the lock
|
|
1202
|
+
}
|
|
1203
|
+
// We now hold the lock, so no restore is active. A surviving marker is therefore debris from a
|
|
1204
|
+
// crashed restore (incomplete) — refuse rather than delete a directory that still needs recovery.
|
|
1205
|
+
if (restoreMarkerPresent(dbPath)) {
|
|
1206
|
+
releaseRestoreLock(lock);
|
|
1207
|
+
for (const h of held) releaseRestoreLock(h);
|
|
1208
|
+
const error: any = new Error(
|
|
1209
|
+
`Database '${databaseName}' has an incomplete restore; rerun restore_backup to recover it`
|
|
1210
|
+
);
|
|
1211
|
+
error.statusCode = 409;
|
|
1212
|
+
throw error;
|
|
1213
|
+
}
|
|
1214
|
+
held.push(lock);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
984
1217
|
/**
|
|
985
1218
|
* Delete the database
|
|
986
1219
|
* @param databaseName
|
|
@@ -990,17 +1223,105 @@ export async function dropDatabase(databaseName) {
|
|
|
990
1223
|
const dbTables = databases[databaseName];
|
|
991
1224
|
let rootStore;
|
|
992
1225
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1226
|
+
// Hold the per-database restore lock across the entire drop so its file deletion can never
|
|
1227
|
+
// interleave with a restore's purge-and-copy on the same directory — a destroy landing after a
|
|
1228
|
+
// restore's copy would gut a "successful" restore, and vice versa. Restore takes the same lock
|
|
1229
|
+
// (before writing its marker), so both operations serialize on this one primitive rather than on
|
|
1230
|
+
// a check-then-act marker probe. Released in the finally below.
|
|
1231
|
+
const restoreLocks: RestoreLock[] = [];
|
|
1232
|
+
try {
|
|
1233
|
+
for (const tableName in dbTables) {
|
|
1234
|
+
const table = dbTables[tableName];
|
|
1235
|
+
rootStore = table.primaryStore.rootStore;
|
|
1236
|
+
if (rootStore instanceof RocksDatabase) lockDatabaseForDrop(rootStore.path, databaseName, restoreLocks);
|
|
1237
|
+
lmdbDatabaseEnvs.delete(rootStore.path);
|
|
1238
|
+
rocksdbDatabaseEnvs.delete(rootStore.path);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
for (const tableName in dbTables) {
|
|
1242
|
+
databaseEventsEmitter.emit('dropTable', tableName, databaseName);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (databaseName === 'data') {
|
|
1246
|
+
for (const tableName in tables) {
|
|
1247
|
+
delete tables[tableName];
|
|
1248
|
+
}
|
|
1249
|
+
delete tables[DEFINED_TABLES];
|
|
1250
|
+
}
|
|
1251
|
+
delete databases[databaseName];
|
|
1252
|
+
|
|
1253
|
+
databaseEventsEmitter.emit('dropDatabase', databaseName);
|
|
1254
|
+
|
|
1255
|
+
if (rootStore) {
|
|
1256
|
+
if (rootStore.status === 'open') {
|
|
1257
|
+
if (rootStore instanceof RocksDatabase) {
|
|
1258
|
+
rootStore.close();
|
|
1259
|
+
rootStore.destroy();
|
|
1260
|
+
} else {
|
|
1261
|
+
await rootStore.close();
|
|
1262
|
+
await unlink(rootStore.path);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
} else {
|
|
1266
|
+
rootStore = database({ database: databaseName, table: null });
|
|
1267
|
+
// a tableless database resolves its root store here rather than in the loop above, so take
|
|
1268
|
+
// the drop lock now (still before any destructive step)
|
|
1269
|
+
if (rootStore instanceof RocksDatabase) lockDatabaseForDrop(rootStore.path, databaseName, restoreLocks);
|
|
1270
|
+
if (rootStore instanceof RocksDatabase) {
|
|
1271
|
+
rootStore.close();
|
|
1272
|
+
rootStore.destroy();
|
|
1273
|
+
} else if (rootStore.status === 'open') {
|
|
1274
|
+
await rootStore.close();
|
|
1275
|
+
await unlink(rootStore.path);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
await deleteRootBlobPathsForDB(rootStore);
|
|
1280
|
+
} finally {
|
|
1281
|
+
for (const lock of restoreLocks) releaseRestoreLock(lock);
|
|
998
1282
|
}
|
|
1283
|
+
}
|
|
999
1284
|
|
|
1285
|
+
/**
|
|
1286
|
+
* Close a RocksDB database's store handles on the current thread and unregister it, without
|
|
1287
|
+
* touching its files. Used by the restore_backup flow: every thread must release its handles so
|
|
1288
|
+
* `backups.restore()` can purge and rewrite the (fully closed) database directory. A subsequent
|
|
1289
|
+
* `resetDatabases()`/`getDatabases()` rescan reloads it (or skips it while a restore is in
|
|
1290
|
+
* progress, per the restore marker checks in the scan).
|
|
1291
|
+
*/
|
|
1292
|
+
export function closeDatabase(databaseName: string): boolean {
|
|
1293
|
+
const dbTables = databases[databaseName];
|
|
1294
|
+
if (!dbTables) return false;
|
|
1295
|
+
const rootStores = new Set<any>();
|
|
1296
|
+
const closeStore = (store: any, description: string) => {
|
|
1297
|
+
try {
|
|
1298
|
+
store?.close?.();
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
logger.warn(`Error closing ${description} while closing database ${databaseName}:`, error);
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1000
1303
|
for (const tableName in dbTables) {
|
|
1001
|
-
|
|
1304
|
+
const table: any = dbTables[tableName];
|
|
1305
|
+
if (!table?.primaryStore) continue;
|
|
1306
|
+
if (table.primaryStore.rootStore) rootStores.add(table.primaryStore.rootStore);
|
|
1307
|
+
for (const indexName in table.indices || {}) {
|
|
1308
|
+
closeStore(table.indices[indexName], `index ${tableName}.${indexName}`);
|
|
1309
|
+
}
|
|
1310
|
+
closeStore(table.primaryStore, `table ${tableName}`);
|
|
1002
1311
|
}
|
|
1003
|
-
|
|
1312
|
+
// a database with no tables (an empty schema, or one whose tables were all dropped) still holds
|
|
1313
|
+
// an open root store, tracked only on the defined-database entry rather than any table — include
|
|
1314
|
+
// it so its handles are released too (the Set dedupes it against the per-table root stores above)
|
|
1315
|
+
const definedRoot = (definedDatabases?.get(databaseName) as any)?.rootStore;
|
|
1316
|
+
if (definedRoot) rootStores.add(definedRoot);
|
|
1317
|
+
for (const rootStore of rootStores) {
|
|
1318
|
+
closeStore(rootStore.dbisDb, 'attributes store');
|
|
1319
|
+
closeStore(rootStore, 'root store');
|
|
1320
|
+
lmdbDatabaseEnvs.delete(rootStore.path);
|
|
1321
|
+
rocksdbDatabaseEnvs.delete(rootStore.path);
|
|
1322
|
+
}
|
|
1323
|
+
const definedDatabase = definedDatabases?.get(databaseName);
|
|
1324
|
+
if (definedDatabase) (definedDatabase as any).rootStore = undefined;
|
|
1004
1325
|
if (databaseName === 'data') {
|
|
1005
1326
|
for (const tableName in tables) {
|
|
1006
1327
|
delete tables[tableName];
|
|
@@ -1008,31 +1329,41 @@ export async function dropDatabase(databaseName) {
|
|
|
1008
1329
|
delete tables[DEFINED_TABLES];
|
|
1009
1330
|
}
|
|
1010
1331
|
delete databases[databaseName];
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1011
1334
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1335
|
+
/**
|
|
1336
|
+
* Close every RocksDB (user) database this thread has open, releasing its native handles.
|
|
1337
|
+
*
|
|
1338
|
+
* rocksdb-js's registry is process-global across worker threads, and a thread that exits WITHOUT
|
|
1339
|
+
* closing leaks its handles (the process-global refCount never drops), while the only alternative,
|
|
1340
|
+
* `shutdown()`, tears down rocksdb for the entire process. So a worker thread that opens databases
|
|
1341
|
+
* and then exits — notably a job worker (jobProcess), which opens the whole database graph via
|
|
1342
|
+
* `getDatabases()` and exits when the job finishes — must close its handles explicitly, or those
|
|
1343
|
+
* handles linger process-wide (and, e.g., block an online `restore_backup` from confirming the
|
|
1344
|
+
* database is closed). The `system` database is intentionally left open: it is non-enumerable here
|
|
1345
|
+
* (skipped by the loop), is never restored online, and the exiting worker may still touch the job
|
|
1346
|
+
* table during teardown. Best-effort: closing failures are swallowed inside `closeDatabase`.
|
|
1347
|
+
*/
|
|
1348
|
+
export function closeLoadedDatabases(): void {
|
|
1349
|
+
// snapshot the names first: closeDatabase() deletes from `databases` as it goes
|
|
1350
|
+
for (const databaseName of Object.keys(databases)) {
|
|
1351
|
+
const dbTables = databases[databaseName];
|
|
1352
|
+
if (!dbTables) continue;
|
|
1353
|
+
let isRocks = false;
|
|
1354
|
+
for (const tableName in dbTables) {
|
|
1355
|
+
if (dbTables[tableName]?.primaryStore?.rootStore instanceof RocksDatabase) {
|
|
1356
|
+
isRocks = true;
|
|
1357
|
+
break;
|
|
1022
1358
|
}
|
|
1023
1359
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
if (rootStore instanceof RocksDatabase) {
|
|
1027
|
-
|
|
1028
|
-
rootStore.destroy();
|
|
1029
|
-
} else if (rootStore.status === 'open') {
|
|
1030
|
-
await rootStore.close();
|
|
1031
|
-
await unlink(rootStore.path);
|
|
1360
|
+
// a tableless database exposes no table root store, so also check the defined-database
|
|
1361
|
+
// entry — otherwise its open root store would leak on worker exit
|
|
1362
|
+
if (!isRocks && (definedDatabases?.get(databaseName) as any)?.rootStore instanceof RocksDatabase) {
|
|
1363
|
+
isRocks = true;
|
|
1032
1364
|
}
|
|
1365
|
+
if (isRocks) closeDatabase(databaseName);
|
|
1033
1366
|
}
|
|
1034
|
-
|
|
1035
|
-
await deleteRootBlobPathsForDB(rootStore);
|
|
1036
1367
|
}
|
|
1037
1368
|
// HNSW_NO_AUTOVERSION kill-switch: when set, a NEW index initializes as legacy rather than
|
|
1038
1369
|
// versioned. process.env values are strings, so a bare truthiness check would treat "0"/"false"
|
|
@@ -1122,7 +1453,11 @@ function openIndex(dbiKey: string, rootStore: RootDatabaseKind, attribute: any)
|
|
|
1122
1453
|
// Enable cache (WeakLRUCache + VT) for all custom-object index stores so the VT is
|
|
1123
1454
|
// available before resolveIndexFormat decides the format. Versioned stores need the VT
|
|
1124
1455
|
// for cached traversal; legacy stores pay a small per-write cache.delete() overhead only.
|
|
1125
|
-
dbi = openRocksDatabase(rootStore.path, {
|
|
1456
|
+
dbi = openRocksDatabase(rootStore.path, {
|
|
1457
|
+
...dbiInit,
|
|
1458
|
+
name: dbiKey,
|
|
1459
|
+
cache: isCustomObjectIndex,
|
|
1460
|
+
} as any) as any;
|
|
1126
1461
|
(dbi as any).rootStore = rootStore;
|
|
1127
1462
|
// Custom-index object stores (e.g. HNSW) write graph nodes via plain put() with no staged
|
|
1128
1463
|
// transaction timestamp, so their values carry no version and the PrimaryRocksDatabase
|
|
@@ -1345,6 +1680,10 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1345
1680
|
clearInterruptedDropEntries(rootStore.path, tableName);
|
|
1346
1681
|
}
|
|
1347
1682
|
if (rootStore instanceof RocksDatabase) {
|
|
1683
|
+
// Usually a genuinely new column family (existingTableMeta above found no catalog
|
|
1684
|
+
// entry), but an interrupted drop just completed above can leave the physical CF
|
|
1685
|
+
// behind under its old codec even though the catalog entry is gone — same fallback
|
|
1686
|
+
// as the reconcile paths covers that remnant case too.
|
|
1348
1687
|
primaryStore = openRocksDatabase(rootStore.path, { ...dbiInit, name: dbiName, cache: true } as any);
|
|
1349
1688
|
} else {
|
|
1350
1689
|
primaryStore = (rootStore as any).openDB(dbiName, dbiInit as any);
|
|
@@ -1955,7 +2294,7 @@ function completeInterruptedDrop(rootStore, attributesDbi, databaseName: string,
|
|
|
1955
2294
|
if (rootStore instanceof RocksDatabase) {
|
|
1956
2295
|
for (const columnName of (rootStore as any).columns) {
|
|
1957
2296
|
if (columnName.startsWith(tableName + '/')) {
|
|
1958
|
-
const columnStore = openRocksDatabase(rootStore.path, { name: columnName }
|
|
2297
|
+
const columnStore = openRocksDatabase(rootStore.path, { name: columnName });
|
|
1959
2298
|
try {
|
|
1960
2299
|
columnStore.dropSync();
|
|
1961
2300
|
} catch (error) {
|
|
@@ -2052,7 +2391,9 @@ export function getDefaultCompression() {
|
|
|
2052
2391
|
if (STORAGE_COMPRESSION_DICTIONARY)
|
|
2053
2392
|
LMDB_COMPRESSION_OPTS['dictionary'] = readFileSync(STORAGE_COMPRESSION_DICTIONARY);
|
|
2054
2393
|
if (STORAGE_COMPRESSION_THRESHOLD) LMDB_COMPRESSION_OPTS['threshold'] = STORAGE_COMPRESSION_THRESHOLD;
|
|
2055
|
-
|
|
2394
|
+
// normalize disabled to false so a falsy config value ('' or null) is never persisted
|
|
2395
|
+
// into table metadata as-is (openRocksDatabase maps defined-falsy to 'none')
|
|
2396
|
+
return LMDB_COMPRESSION ? LMDB_COMPRESSION_OPTS : false;
|
|
2056
2397
|
}
|
|
2057
2398
|
|
|
2058
2399
|
/**
|
package/resources/jsResource.ts
CHANGED
|
@@ -69,41 +69,13 @@ export class ResourceLoadError extends Error {
|
|
|
69
69
|
*
|
|
70
70
|
* Once a file has been loaded it cannot be unloaded without a restart.
|
|
71
71
|
*
|
|
72
|
-
* Thus, this plugin only
|
|
72
|
+
* Thus, this plugin only handles files as they are added (`add` event). All other events result in a restart request.
|
|
73
73
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* existing file as `'add'` — including ones whose contents just changed. Treating those as plain
|
|
77
|
-
* adds would silently re-run against the stale module cache and never flag a restart (harper#1817).
|
|
78
|
-
* So we track which files this scope has already loaded: a re-`add` of a known file is a redeploy of
|
|
79
|
-
* loaded code we cannot hot-swap, and is handled like a `change` — request a restart. A first-time
|
|
80
|
-
* `add` (initial load, or a genuinely new file added at runtime) still loads without a restart.
|
|
81
|
-
*
|
|
82
|
-
* A redeploy that *deletes* a loaded file is a different shape of the same problem: the fresh
|
|
83
|
-
* chokidar scan only reports what's currently on disk, so a file that's gone produces no event at
|
|
84
|
-
* all — no re-`add`, no `unlink` — and the modified-file handling above never sees it. Left
|
|
85
|
-
* unhandled, the deleted resource stays registered and active in memory (harper#1817 follow-up). So
|
|
86
|
-
* we also track which files the post-redeploy scan pass reports, and once that scan's `ready` fires,
|
|
87
|
-
* diff it against everything this scope has ever loaded: anything missing was deleted, and is
|
|
88
|
-
* handled the same way as a modified file — request a restart.
|
|
89
|
-
*
|
|
90
|
-
* That diff must only run for an actual redeploy rescan, not every time `EntryHandler` emits
|
|
91
|
-
* `ready` — it also refires after each ordinary runtime add/change once that file's read settles
|
|
92
|
-
* (its initial-scan-complete latch never resets outside a full rescan), and diffing against that
|
|
93
|
-
* would falsely treat every other already-loaded file as deleted. So the diff window is gated by
|
|
94
|
-
* the scope's own `deploy:start`/`deploy:end` bracket (see `Scope`): `deploy:start` pauses the
|
|
95
|
-
* watcher and opens the window (and is where we reset the scan-file tracking, since no file events
|
|
96
|
-
* can land while paused), and the first `ready` afterward — the resumed watcher's fresh scan
|
|
97
|
-
* completing — closes it and runs the diff.
|
|
74
|
+
* EntryHandler preserves file identity across a deploy pause/resume and emits `change` or `unlink`
|
|
75
|
+
* for loaded files that changed or disappeared, so those events request the required restart.
|
|
98
76
|
*/
|
|
99
77
|
export async function handleApplication(scope: Scope) {
|
|
100
|
-
|
|
101
|
-
// Files reported as `add` since the most recent `deploy:start`, populated only while
|
|
102
|
-
// `awaitingPostRedeployScan` is true — see the gating note above.
|
|
103
|
-
let currentScanFiles = new Set<string>();
|
|
104
|
-
let awaitingPostRedeployScan = false;
|
|
105
|
-
|
|
106
|
-
const entryHandler = scope.handleEntry(async function handleResourceEntry(entryEvent) {
|
|
78
|
+
scope.handleEntry(async function handleResourceEntry(entryEvent) {
|
|
107
79
|
if (entryEvent.entryType !== 'file') {
|
|
108
80
|
scope.logger.warn(
|
|
109
81
|
`jsResource plugin cannot handle entry type ${entryEvent.entryType}. Modify the 'files' option in ${scope.configFilePath} to only include files.`
|
|
@@ -111,13 +83,7 @@ export async function handleApplication(scope: Scope) {
|
|
|
111
83
|
return;
|
|
112
84
|
}
|
|
113
85
|
|
|
114
|
-
if (
|
|
115
|
-
// Recorded unconditionally — before the loaded/re-add branch below — so the post-scan
|
|
116
|
-
// deletion diff sees every file this scan reported, whether newly loaded or already known.
|
|
117
|
-
currentScanFiles.add(entryEvent.absolutePath);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (entryEvent.eventType !== 'add' || loadedResourceFiles.has(entryEvent.absolutePath)) {
|
|
86
|
+
if (entryEvent.eventType !== 'add') {
|
|
121
87
|
scope.requestRestart();
|
|
122
88
|
return;
|
|
123
89
|
}
|
|
@@ -133,9 +99,6 @@ export async function handleApplication(scope: Scope) {
|
|
|
133
99
|
scope.logger.debug?.(`Registered root resource: ${path}`);
|
|
134
100
|
}
|
|
135
101
|
recurseForResources(scope, resourceModule, root);
|
|
136
|
-
// Record the load so a later re-`add` of this same file (a redeploy re-scan) is treated
|
|
137
|
-
// as a change and requests a restart rather than silently re-serving stale cached code.
|
|
138
|
-
loadedResourceFiles.add(entryEvent.absolutePath);
|
|
139
102
|
// A JS resource that extends an exported @table is the one carrying author opt-ins
|
|
140
103
|
// (`static mcpTools`/`mcpPrompts`), and it registers here — after the schema-derived
|
|
141
104
|
// table class and after the MCP component's boot scan. Signal so listing surfaces
|
|
@@ -146,26 +109,6 @@ export async function handleApplication(scope: Scope) {
|
|
|
146
109
|
throw new ResourceLoadError(entryEvent.absolutePath, error);
|
|
147
110
|
}
|
|
148
111
|
});
|
|
149
|
-
|
|
150
|
-
// Optional chaining: a mock/test scope may not implement EventEmitter, and Scope#handleEntry
|
|
151
|
-
// itself can return undefined (e.g. MissingDefaultFilesOptionError). In real use `scope` is
|
|
152
|
-
// always an EventEmitter and `entryHandler` is always the EntryHandler backing this watcher.
|
|
153
|
-
scope.on?.('deploy:start', () => {
|
|
154
|
-
awaitingPostRedeployScan = true;
|
|
155
|
-
currentScanFiles = new Set();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
entryHandler?.on?.('ready', () => {
|
|
159
|
-
if (!awaitingPostRedeployScan) return;
|
|
160
|
-
awaitingPostRedeployScan = false;
|
|
161
|
-
for (const loadedFile of loadedResourceFiles) {
|
|
162
|
-
if (!currentScanFiles.has(loadedFile)) {
|
|
163
|
-
// Known file that the just-completed scan never reported — deleted during the redeploy.
|
|
164
|
-
loadedResourceFiles.delete(loadedFile);
|
|
165
|
-
scope.requestRestart();
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
112
|
}
|
|
170
113
|
|
|
171
114
|
function recurseForResources(scope: Scope, resourceModule: any, prefix: string) {
|