@harperfast/harper 5.2.10 → 5.3.0-alpha.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/agent/mcpTools.ts +1 -1
- package/agent/session.ts +25 -14
- package/bin/cliOperations.ts +46 -9
- package/bin/copyDb.ts +282 -59
- package/bin/deploySetup.ts +16 -5
- package/bin/harper.ts +1 -1
- package/bin/help.ts +4 -1
- package/bin/lite.ts +4 -1
- package/bin/restart.ts +59 -3
- package/bin/run.ts +6 -11
- package/bin/upgrade.js +7 -3
- package/bin/workloadIdentity.ts +119 -0
- package/components/Application.ts +2414 -242
- package/components/ApplicationScope.ts +8 -0
- package/components/EntryHandler.ts +59 -39
- package/components/OptionsWatcher.ts +150 -74
- package/components/RuntimeModuleTracker.ts +38 -7
- package/components/Scope.ts +37 -15
- package/components/awaitRestart.ts +84 -0
- package/components/componentLoader.ts +340 -30
- package/components/componentPreparationLock.ts +16 -5
- package/components/mcp/adapters/harperHttp.ts +4 -0
- package/components/mcp/listChanged.ts +4 -0
- package/components/mcp/toolRegistry.ts +2 -0
- package/components/mcp/tools/operations.ts +9 -0
- package/components/mcp/tools/schemas/operationDescriptions.ts +2 -2
- package/components/operations.js +270 -78
- package/components/operationsValidation.js +49 -1
- package/components/status/ComponentStatusRegistry.ts +59 -0
- package/config/RootConfigWatcher.ts +80 -34
- package/config/configUtils.ts +291 -34
- package/config/harperConfigEnvVars.ts +170 -27
- package/config-root.schema.json +29 -0
- package/dataLayer/blobBackup.ts +160 -50
- package/dataLayer/delete.ts +6 -1
- package/dataLayer/harperBridge/ResourceBridge.ts +52 -8
- package/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
- package/dataLayer/hdbInfoController.ts +34 -1
- package/dataLayer/insert.ts +44 -1
- package/dataLayer/rocksdbBackup.ts +53 -10
- package/dataLayer/schema.ts +11 -1
- package/dataLayer/schemaDescribe.ts +8 -1
- package/dist/agent/mcpTools.js +1 -1
- package/dist/agent/mcpTools.js.map +1 -1
- package/dist/agent/session.d.ts +22 -0
- package/dist/agent/session.js +26 -15
- package/dist/agent/session.js.map +1 -1
- package/dist/bin/cliOperations.js +48 -9
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/copyDb.d.ts +12 -1
- package/dist/bin/copyDb.js +248 -60
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/deploySetup.d.ts +2 -0
- package/dist/bin/deploySetup.js +11 -3
- package/dist/bin/deploySetup.js.map +1 -1
- package/dist/bin/harper.js +1 -1
- package/dist/bin/harper.js.map +1 -1
- package/dist/bin/help.js +4 -1
- package/dist/bin/help.js.map +1 -1
- package/dist/bin/lite.js +4 -1
- package/dist/bin/lite.js.map +1 -1
- package/dist/bin/restart.js +54 -5
- package/dist/bin/restart.js.map +1 -1
- package/dist/bin/run.js +4 -10
- package/dist/bin/run.js.map +1 -1
- package/dist/bin/upgrade.js +4 -3
- package/dist/bin/upgrade.js.map +1 -1
- package/dist/bin/workloadIdentity.d.ts +18 -0
- package/dist/bin/workloadIdentity.js +100 -0
- package/dist/bin/workloadIdentity.js.map +1 -0
- package/dist/components/Application.d.ts +139 -16
- package/dist/components/Application.js +2215 -267
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +8 -0
- package/dist/components/ApplicationScope.js +7 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/EntryHandler.js +26 -10
- package/dist/components/EntryHandler.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +1 -0
- package/dist/components/OptionsWatcher.js +141 -74
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/RuntimeModuleTracker.js +40 -6
- package/dist/components/RuntimeModuleTracker.js.map +1 -1
- package/dist/components/Scope.js +38 -13
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/awaitRestart.d.ts +33 -0
- package/dist/components/awaitRestart.js +61 -0
- package/dist/components/awaitRestart.js.map +1 -0
- package/dist/components/componentLoader.d.ts +38 -1
- package/dist/components/componentLoader.js +279 -22
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/componentPreparationLock.d.ts +5 -0
- package/dist/components/componentPreparationLock.js +14 -6
- package/dist/components/componentPreparationLock.js.map +1 -1
- package/dist/components/mcp/adapters/harperHttp.js +4 -0
- package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
- package/dist/components/mcp/listChanged.js +5 -0
- package/dist/components/mcp/listChanged.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +1 -0
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/operations.d.ts +5 -0
- 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 -2
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/operations.js +231 -77
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +49 -1
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/status/ComponentStatusRegistry.d.ts +0 -4
- package/dist/components/status/ComponentStatusRegistry.js +63 -0
- package/dist/components/status/ComponentStatusRegistry.js.map +1 -1
- package/dist/config/RootConfigWatcher.d.ts +7 -1
- package/dist/config/RootConfigWatcher.js +64 -27
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configUtils.d.ts +9 -1
- package/dist/config/configUtils.js +254 -33
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/harperConfigEnvVars.d.ts +16 -0
- package/dist/config/harperConfigEnvVars.js +162 -25
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/dataLayer/blobBackup.d.ts +49 -20
- package/dist/dataLayer/blobBackup.js +139 -50
- package/dist/dataLayer/blobBackup.js.map +1 -1
- package/dist/dataLayer/delete.js +1 -1
- package/dist/dataLayer/delete.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +14 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +51 -10
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.d.ts +3 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.d.ts +10 -0
- package/dist/dataLayer/hdbInfoController.js +30 -1
- package/dist/dataLayer/hdbInfoController.js.map +1 -1
- package/dist/dataLayer/insert.d.ts +9 -1
- package/dist/dataLayer/insert.js +30 -0
- package/dist/dataLayer/insert.js.map +1 -1
- package/dist/dataLayer/rocksdbBackup.d.ts +2 -2
- package/dist/dataLayer/rocksdbBackup.js +45 -8
- package/dist/dataLayer/rocksdbBackup.js.map +1 -1
- package/dist/dataLayer/schema.js +8 -0
- package/dist/dataLayer/schema.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +8 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +52 -0
- package/dist/resources/DatabaseTransaction.d.ts +55 -3
- package/dist/resources/DatabaseTransaction.js +460 -134
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.js +22 -4
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.d.ts +1 -0
- package/dist/resources/PrimaryRocksDatabase.js +30 -2
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +20 -0
- package/dist/resources/RecordEncoder.js +94 -9
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/RequestTarget.d.ts +2 -0
- package/dist/resources/RequestTarget.js.map +1 -1
- package/dist/resources/Resource.js +20 -11
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +20 -1
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/RocksIndexStore.d.ts +6 -1
- package/dist/resources/RocksIndexStore.js +24 -10
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +14 -1
- package/dist/resources/RocksTransactionLogStore.js +57 -17
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +112 -8
- package/dist/resources/Table.js +1071 -197
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/auditStore.d.ts +11 -2
- package/dist/resources/auditStore.js +183 -18
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +129 -9
- package/dist/resources/blob.js +992 -114
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/branchDatabase.d.ts +48 -0
- package/dist/resources/branchDatabase.js +892 -0
- package/dist/resources/branchDatabase.js.map +1 -0
- package/dist/resources/crdt.js +50 -12
- package/dist/resources/crdt.js.map +1 -1
- package/dist/resources/databases.d.ts +150 -9
- package/dist/resources/databases.js +1247 -525
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/defineTable.d.ts +10 -2
- package/dist/resources/defineTable.js +9 -1
- package/dist/resources/defineTable.js.map +1 -1
- package/dist/resources/graphql.d.ts +1 -1
- package/dist/resources/graphql.js +50 -15
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +1 -0
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +6 -2
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/longLivedTransactions.d.ts +71 -0
- package/dist/resources/longLivedTransactions.js +358 -0
- package/dist/resources/longLivedTransactions.js.map +1 -0
- package/dist/resources/models/backendRegistry.d.ts +26 -0
- package/dist/resources/models/backendRegistry.js +60 -2
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +33 -1
- package/dist/resources/models/bootstrap.js +416 -31
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/recordLock.d.ts +80 -0
- package/dist/resources/recordLock.js +184 -0
- package/dist/resources/recordLock.js.map +1 -0
- package/dist/resources/replayLogs.d.ts +14 -1
- package/dist/resources/replayLogs.js +152 -24
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/replayLogsGuards.d.ts +94 -7
- package/dist/resources/replayLogsGuards.js +111 -7
- package/dist/resources/replayLogsGuards.js.map +1 -1
- package/dist/resources/search.js +158 -32
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/transactionBroadcast.d.ts +1 -1
- package/dist/resources/transactionBroadcast.js +2 -2
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/auth.js +61 -30
- package/dist/security/auth.js.map +1 -1
- package/dist/security/authn/oidc/claims.d.ts +22 -0
- package/dist/security/authn/oidc/claims.js +71 -0
- package/dist/security/authn/oidc/claims.js.map +1 -0
- package/dist/security/authn/oidc/identityToken.d.ts +27 -0
- package/dist/security/authn/oidc/identityToken.js +111 -0
- package/dist/security/authn/oidc/identityToken.js.map +1 -0
- package/dist/security/authn/oidc/jwks.d.ts +25 -0
- package/dist/security/authn/oidc/jwks.js +261 -0
- package/dist/security/authn/oidc/jwks.js.map +1 -0
- package/dist/security/authn/oidc/providers/generic.d.ts +13 -0
- package/dist/security/authn/oidc/providers/generic.js +34 -0
- package/dist/security/authn/oidc/providers/generic.js.map +1 -0
- package/dist/security/authn/oidc/providers/githubActions.d.ts +11 -0
- package/dist/security/authn/oidc/providers/githubActions.js +129 -0
- package/dist/security/authn/oidc/providers/githubActions.js.map +1 -0
- package/dist/security/authn/oidc/providers/index.d.ts +37 -0
- package/dist/security/authn/oidc/providers/index.js +24 -0
- package/dist/security/authn/oidc/providers/index.js.map +1 -0
- package/dist/security/authn/oidc/tokenExchange.d.ts +12 -0
- package/dist/security/authn/oidc/tokenExchange.js +306 -0
- package/dist/security/authn/oidc/tokenExchange.js.map +1 -0
- package/dist/security/authn/oidc/trustPolicyOperations.d.ts +49 -0
- package/dist/security/authn/oidc/trustPolicyOperations.js +358 -0
- package/dist/security/authn/oidc/trustPolicyOperations.js.map +1 -0
- package/dist/security/authn/oidc/types.d.ts +38 -0
- package/dist/security/authn/oidc/types.js +6 -0
- package/dist/security/authn/oidc/types.js.map +1 -0
- package/dist/security/certificateVerification/index.js +40 -11
- package/dist/security/certificateVerification/index.js.map +1 -1
- package/dist/security/certificateVerification/trustedIssuers.d.ts +24 -0
- package/dist/security/certificateVerification/trustedIssuers.js +79 -0
- package/dist/security/certificateVerification/trustedIssuers.js.map +1 -0
- package/dist/security/certificateVerification/types.d.ts +1 -0
- package/dist/security/credentialProvenance.d.ts +35 -0
- package/dist/security/credentialProvenance.js +51 -0
- package/dist/security/credentialProvenance.js.map +1 -0
- package/dist/security/credentialRejection.d.ts +4 -0
- package/dist/security/credentialRejection.js +24 -0
- package/dist/security/credentialRejection.js.map +1 -0
- package/dist/security/deferredAuthentication.d.ts +36 -0
- package/dist/security/deferredAuthentication.js +70 -0
- package/dist/security/deferredAuthentication.js.map +1 -0
- package/dist/security/impersonation.d.ts +21 -0
- package/dist/security/impersonation.js +108 -9
- package/dist/security/impersonation.js.map +1 -1
- package/dist/security/jsLoader.d.ts +6 -0
- package/dist/security/jsLoader.js +75 -15
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/keys.js +301 -71
- package/dist/security/keys.js.map +1 -1
- package/dist/security/operationScope.d.ts +21 -0
- package/dist/security/operationScope.js +36 -0
- package/dist/security/operationScope.js.map +1 -0
- package/dist/security/permissionsTranslator.js +21 -0
- package/dist/security/permissionsTranslator.js.map +1 -1
- package/dist/security/tokenAuthentication.d.ts +19 -1
- package/dist/security/tokenAuthentication.js +191 -10
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/security/user.js +4 -3
- package/dist/security/user.js.map +1 -1
- package/dist/server/DurableSubscriptionsSession.d.ts +2 -2
- package/dist/server/DurableSubscriptionsSession.js +67 -10
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/REST.js +73 -0
- package/dist/server/REST.js.map +1 -1
- package/dist/server/graphqlQuerying.js +4 -0
- package/dist/server/graphqlQuerying.js.map +1 -1
- package/dist/server/http.d.ts +11 -0
- package/dist/server/http.js +88 -15
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +8 -1
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobProcess.js +6 -2
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobs.js +4 -1
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/liveSubscriptionAuth.d.ts +26 -4
- package/dist/server/liveSubscriptionAuth.js +105 -39
- package/dist/server/liveSubscriptionAuth.js.map +1 -1
- package/dist/server/loadRootComponents.js +49 -10
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/mqtt.d.ts +2 -0
- package/dist/server/mqtt.js +165 -30
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/nodeName.d.ts +2 -0
- package/dist/server/nodeName.js +107 -23
- package/dist/server/nodeName.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +25 -0
- package/dist/server/serverHelpers/Headers.js +139 -1
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +9 -0
- package/dist/server/serverHelpers/contentTypes.js +32 -25
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/deployValidationState.d.ts +3 -0
- package/dist/server/serverHelpers/deployValidationState.js +9 -19
- package/dist/server/serverHelpers/deployValidationState.js.map +1 -1
- package/dist/server/serverHelpers/operationAuthorizationState.d.ts +12 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js +24 -2
- package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -1
- package/dist/server/serverHelpers/registeredOperations.d.ts +5 -4
- package/dist/server/serverHelpers/registeredOperations.js +74 -21
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
- package/dist/server/serverHelpers/requestSanitization.d.ts +11 -0
- package/dist/server/serverHelpers/requestSanitization.js +20 -0
- package/dist/server/serverHelpers/requestSanitization.js.map +1 -0
- package/dist/server/serverHelpers/serverHandlers.js +6 -3
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +18 -0
- package/dist/server/serverHelpers/serverUtilities.js +90 -20
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/sharedMessageEncoding.d.ts +67 -0
- package/dist/server/serverHelpers/sharedMessageEncoding.js +280 -0
- package/dist/server/serverHelpers/sharedMessageEncoding.js.map +1 -0
- package/dist/server/serverHelpers/uwsServer.js +19 -1
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/static.js +24 -28
- package/dist/server/static.js.map +1 -1
- package/dist/server/storageReclamation.d.ts +5 -0
- package/dist/server/storageReclamation.js +16 -0
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/itc.d.ts +7 -2
- package/dist/server/threads/itc.js +5 -1
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +26 -4
- package/dist/server/threads/manageThreads.js +517 -72
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.js +89 -26
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadHeapMemory.d.ts +2 -0
- package/dist/server/threads/threadHeapMemory.js +31 -0
- package/dist/server/threads/threadHeapMemory.js.map +1 -0
- package/dist/server/threads/threadServer.js +46 -15
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/sqlEngine/config.d.ts +1 -3
- package/dist/sqlEngine/config.js +19 -16
- package/dist/sqlEngine/config.js.map +1 -1
- package/dist/sqlTranslator/index.d.ts +1 -1
- package/dist/sqlTranslator/index.js +30 -7
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.d.ts +7 -0
- package/dist/upgrade/directives/5-3-0.js +148 -0
- package/dist/upgrade/directives/5-3-0.js.map +1 -0
- package/dist/upgrade/directives/directivesController.js +2 -1
- package/dist/upgrade/directives/directivesController.js.map +1 -1
- package/dist/utility/OperationFunctionCaller.js +2 -1
- package/dist/utility/OperationFunctionCaller.js.map +1 -1
- package/dist/utility/common_utils.d.ts +16 -0
- package/dist/utility/common_utils.js +32 -6
- package/dist/utility/common_utils.js.map +1 -1
- package/dist/utility/componentNames.d.ts +8 -0
- package/dist/utility/componentNames.js +12 -1
- package/dist/utility/componentNames.js.map +1 -1
- package/dist/utility/environment/environmentManager.js +3 -6
- package/dist/utility/environment/environmentManager.js.map +1 -1
- package/dist/utility/environment/systemInformation.d.ts +1 -0
- package/dist/utility/environment/systemInformation.js +1 -0
- package/dist/utility/environment/systemInformation.js.map +1 -1
- package/dist/utility/errors/commonErrors.d.ts +2 -0
- package/dist/utility/errors/commonErrors.js +2 -0
- package/dist/utility/errors/commonErrors.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +17 -0
- package/dist/utility/errors/hdbError.js +30 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +16 -0
- package/dist/utility/hdbTerms.js +18 -2
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +2 -0
- package/dist/utility/logging/harper_logger.js +91 -14
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logRotator.js +76 -49
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/nodeIdentity.d.ts +9 -0
- package/dist/utility/nodeIdentity.js +58 -0
- package/dist/utility/nodeIdentity.js.map +1 -0
- package/dist/utility/npmUtilities.js +9 -7
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/operationPermissions.d.ts +3 -1
- package/dist/utility/operationPermissions.js +16 -1
- package/dist/utility/operationPermissions.js.map +1 -1
- package/dist/utility/operation_authorization.d.ts +10 -7
- package/dist/utility/operation_authorization.js +212 -41
- package/dist/utility/operation_authorization.js.map +1 -1
- package/dist/utility/watchPath.d.ts +29 -0
- package/dist/utility/watchPath.js +68 -0
- package/dist/utility/watchPath.js.map +1 -0
- package/dist/utility/watcherFallback.d.ts +86 -0
- package/dist/utility/watcherFallback.js +278 -1
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.d.ts +12 -0
- package/dist/validation/configValidator.js +199 -72
- package/dist/validation/configValidator.js.map +1 -1
- package/dist/validation/installValidator.js +12 -0
- package/dist/validation/installValidator.js.map +1 -1
- package/dist/validation/validationWrapper.d.ts +11 -0
- package/dist/validation/validationWrapper.js +16 -3
- package/dist/validation/validationWrapper.js.map +1 -1
- package/index.ts +2 -0
- package/json/systemSchema.json +52 -0
- package/npm-shrinkwrap.json +235 -233
- package/package.json +8 -7
- package/resources/DESIGN.md +114 -52
- package/resources/DatabaseTransaction.ts +522 -130
- package/resources/LMDBTransaction.ts +26 -4
- package/resources/PrimaryRocksDatabase.ts +29 -3
- package/resources/RecordEncoder.ts +103 -8
- package/resources/RequestTarget.ts +2 -0
- package/resources/Resource.ts +17 -9
- package/resources/ResourceInterface.ts +23 -0
- package/resources/RocksIndexStore.ts +30 -10
- package/resources/RocksTransactionLogStore.ts +78 -21
- package/resources/Table.ts +1107 -164
- package/resources/auditStore.ts +187 -21
- package/resources/blob.ts +1029 -110
- package/resources/branchDatabase.ts +941 -0
- package/resources/crdt.ts +76 -12
- package/resources/databases.ts +1354 -522
- package/resources/defineTable.ts +18 -2
- package/resources/graphql.ts +70 -16
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +6 -2
- package/resources/longLivedTransactions.ts +360 -0
- package/resources/models/backendRegistry.ts +84 -2
- package/resources/models/bootstrap.ts +473 -28
- package/resources/recordLock.ts +253 -0
- package/resources/replayLogs.ts +147 -26
- package/resources/replayLogsGuards.ts +171 -8
- package/resources/search.ts +154 -32
- package/resources/transactionBroadcast.ts +3 -3
- package/security/auth.ts +68 -29
- package/security/authn/oidc/claims.ts +72 -0
- package/security/authn/oidc/identityToken.ts +129 -0
- package/security/authn/oidc/jwks.ts +260 -0
- package/security/authn/oidc/providers/generic.ts +40 -0
- package/security/authn/oidc/providers/githubActions.ts +137 -0
- package/security/authn/oidc/providers/index.ts +52 -0
- package/security/authn/oidc/tokenExchange.ts +300 -0
- package/security/authn/oidc/trustPolicyOperations.ts +343 -0
- package/security/authn/oidc/types.ts +41 -0
- package/security/certificateVerification/index.ts +54 -13
- package/security/certificateVerification/trustedIssuers.ts +76 -0
- package/security/certificateVerification/types.ts +1 -0
- package/security/credentialProvenance.ts +47 -0
- package/security/credentialRejection.ts +22 -0
- package/security/deferredAuthentication.ts +71 -0
- package/security/impersonation.ts +117 -12
- package/security/jsLoader.ts +81 -18
- package/security/keys.ts +298 -72
- package/security/operationScope.ts +33 -0
- package/security/permissionsTranslator.js +23 -0
- package/security/tokenAuthentication.ts +233 -12
- package/security/user.ts +4 -3
- package/server/DESIGN.md +183 -16
- package/server/DurableSubscriptionsSession.ts +71 -11
- package/server/REST.ts +79 -1
- package/server/graphqlQuerying.ts +4 -0
- package/server/http.ts +99 -18
- package/server/itc/serverHandlers.js +8 -1
- package/server/jobs/jobProcess.ts +8 -2
- package/server/jobs/jobs.ts +4 -1
- package/server/liveSubscriptionAuth.ts +129 -46
- package/server/loadRootComponents.js +50 -8
- package/server/mqtt.ts +179 -38
- package/server/nodeName.ts +103 -21
- package/server/serverHelpers/Headers.ts +131 -0
- package/server/serverHelpers/contentTypes.ts +29 -21
- package/server/serverHelpers/deployValidationState.ts +24 -13
- package/server/serverHelpers/operationAuthorizationState.ts +34 -3
- package/server/serverHelpers/registeredOperations.ts +79 -22
- package/server/serverHelpers/requestSanitization.ts +15 -0
- package/server/serverHelpers/serverHandlers.js +6 -3
- package/server/serverHelpers/serverUtilities.ts +104 -31
- package/server/serverHelpers/sharedMessageEncoding.ts +307 -0
- package/server/serverHelpers/uwsServer.ts +17 -2
- package/server/static.ts +23 -29
- package/server/storageReclamation.ts +13 -0
- package/server/threads/itc.js +11 -1
- package/server/threads/manageThreads.js +526 -63
- package/server/threads/socketRouter.ts +81 -26
- package/server/threads/threadHeapMemory.ts +26 -0
- package/server/threads/threadServer.js +43 -15
- package/sqlTranslator/index.ts +31 -8
- package/studio/web/assets/{Chat-Br06zdMA.js → Chat-BnCBegQz.js} +1 -1
- package/studio/web/assets/{FloatingChat-BWImX5fA.js → FloatingChat-CoDW1ySS.js} +3 -3
- package/studio/web/assets/{apiToken-DN0nmDsq.js → apiToken-Bwk5BLXW.js} +1 -1
- package/studio/web/assets/{applications-kSxVoyeU.js → applications-DHxGi7JH.js} +1 -1
- package/studio/web/assets/{cssMode-C1JeufH5.js → cssMode-s0cWI-_M.js} +1 -1
- package/studio/web/assets/{editor-qoo9CrGO.js → editor-DNcRHK54.js} +1 -1
- package/studio/web/assets/{html-Dt4IIy04.js → html-Bdssedlg.js} +1 -1
- package/studio/web/assets/{htmlMode-DXgKKr4C.js → htmlMode-CoDlJ3fw.js} +1 -1
- package/studio/web/assets/{index-6onkYFOG.js → index-D6sxmFLR.js} +5 -5
- package/studio/web/assets/{index.lazy-BrCFnpNJ.js → index.lazy-tmU5BS8s.js} +1 -1
- package/studio/web/assets/{javascript-DNCQGUBc.js → javascript-B8meVSTH.js} +1 -1
- package/studio/web/assets/{jsonMode-CR6HWruP.js → jsonMode-DpIPd35T.js} +1 -1
- package/studio/web/assets/{languageServices-BM4fI4rS.js → languageServices-C_5FMJzQ.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-DSa1ttcD.js → lspLanguageFeatures-BIzNBzPK.js} +1 -1
- package/studio/web/assets/{notifications-BHXLnh6x.js → notifications-CQf18QKb.js} +1 -1
- package/studio/web/assets/{notifications-CUmtIA6z.js → notifications-CvZivSbh.js} +1 -1
- package/studio/web/assets/{profile-8BeFSF3j.js → profile-DdOwtntb.js} +1 -1
- package/studio/web/assets/{regions-C8qR0HhD.js → regions-n69fwagr.js} +1 -1
- package/studio/web/assets/{register-B4n5i0SD.js → register-PfWTCXWB.js} +2 -2
- package/studio/web/assets/{setComponentFile-g0_B0lgX.js → setComponentFile-Bg6O7X0S.js} +1 -1
- package/studio/web/assets/{setup-B0CTj_Q5.js → setup-CUx_aUDl.js} +2 -2
- package/studio/web/assets/{status-C6Yib7-K.js → status-D7BVKqX9.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-Dgta7bVi.js → toggleHighContrast-DBSyXzMr.js} +1 -1
- package/studio/web/assets/{tsMode-CH_jHvU-.js → tsMode-BByKCjBS.js} +1 -1
- package/studio/web/assets/{typescript-Co9LCXd5.js → typescript-DDLnLpw9.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-wKC8NsC_.js → useEntityRestURL-31CHGaHk.js} +1 -1
- package/studio/web/assets/{workers-CWeLxCXA.js → workers-pR3jRY9D.js} +1 -1
- package/studio/web/assets/{xml-BadC-0Rk.js → xml-2iRnMhQO.js} +1 -1
- package/studio/web/assets/{yaml-BiUfxPbC.js → yaml-Bf92gJpd.js} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-3-0.ts +132 -0
- package/upgrade/directives/directivesController.ts +2 -1
- package/utility/OperationFunctionCaller.ts +2 -1
- package/utility/common_utils.ts +30 -5
- package/utility/componentNames.ts +12 -0
- package/utility/environment/environmentManager.ts +3 -7
- package/utility/environment/systemInformation.ts +7 -0
- package/utility/errors/commonErrors.ts +4 -0
- package/utility/errors/hdbError.ts +29 -0
- package/utility/hdbTerms.ts +17 -0
- package/utility/logging/harper_logger.ts +87 -14
- package/utility/logging/logRotator.ts +72 -45
- package/utility/nodeIdentity.ts +45 -0
- package/utility/npmUtilities.ts +10 -8
- package/utility/operationPermissions.ts +18 -1
- package/utility/operation_authorization.ts +231 -42
- package/utility/watchPath.ts +63 -0
- package/utility/watcherFallback.ts +270 -0
- package/validation/configValidator.ts +209 -72
- package/validation/installValidator.ts +15 -0
- package/validation/validationWrapper.ts +18 -4
package/dist/resources/blob.js
CHANGED
|
@@ -12,23 +12,31 @@
|
|
|
12
12
|
* - Note that for compressed data, the size is the uncompressed size, and the compressed size in the file
|
|
13
13
|
*/
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.databasePaths = exports.BLOB_UNAVAILABLE_STATUS = exports.blobsWereEncoded = exports.Blob = void 0;
|
|
15
|
+
exports.databasePaths = exports.BLOB_UNAVAILABLE_STATUS = exports.blobsWereEncoded = exports.Blob = exports.DEFLATE_TYPE = void 0;
|
|
16
16
|
exports.registerBlobReceiveInFlight = registerBlobReceiveInFlight;
|
|
17
17
|
exports.unregisterBlobReceiveInFlight = unregisterBlobReceiveInFlight;
|
|
18
18
|
exports.isBlobReceiveInFlight = isBlobReceiveInFlight;
|
|
19
19
|
exports.createPendingMarkerBarrier = createPendingMarkerBarrier;
|
|
20
|
+
exports.watchInProgressFile = watchInProgressFile;
|
|
20
21
|
exports.getBlobHoldStateForTesting = getBlobHoldStateForTesting;
|
|
21
22
|
exports.holdBlobFile = holdBlobFile;
|
|
22
23
|
exports.deleteBlob = deleteBlob;
|
|
23
24
|
exports.setDeletionDelay = setDeletionDelay;
|
|
25
|
+
exports.resolveBlobCompression = resolveBlobCompression;
|
|
24
26
|
exports.createBlob = createBlob;
|
|
27
|
+
exports.createBlobFromStoredBody = createBlobFromStoredBody;
|
|
25
28
|
exports.saveBlob = saveBlob;
|
|
26
29
|
exports.shouldDestroyIdleBlobSource = shouldDestroyIdleBlobSource;
|
|
30
|
+
exports.createStoredDeflateVerifier = createStoredDeflateVerifier;
|
|
27
31
|
exports.getFileId = getFileId;
|
|
28
32
|
exports.isSaving = isSaving;
|
|
29
33
|
exports.getFilePathForBlob = getFilePathForBlob;
|
|
30
34
|
exports.repairBlobFile = repairBlobFile;
|
|
35
|
+
exports.isSystemicIoError = isSystemicIoError;
|
|
36
|
+
exports.classifyBlobFileForCapture = classifyBlobFileForCapture;
|
|
37
|
+
exports.createCaptureMarker = createCaptureMarker;
|
|
31
38
|
exports.blobHeaderIndicatesIncomplete = blobHeaderIndicatesIncomplete;
|
|
39
|
+
exports.blobFileMissingOrIncompleteAsync = blobFileMissingOrIncompleteAsync;
|
|
32
40
|
exports.blobFileMissingOrIncomplete = blobFileMissingOrIncomplete;
|
|
33
41
|
exports.getRootBlobPathsForDB = getRootBlobPathsForDB;
|
|
34
42
|
exports.getBlobPathsForDatabaseName = getBlobPathsForDatabaseName;
|
|
@@ -47,7 +55,9 @@ exports.startPreCommitBlobsForRecord = startPreCommitBlobsForRecord;
|
|
|
47
55
|
exports.cleanupUnusedBlobs = cleanupUnusedBlobs;
|
|
48
56
|
exports.collectRetainedFileIds = collectRetainedFileIds;
|
|
49
57
|
exports.cleanupOrphans = cleanupOrphans;
|
|
58
|
+
exports.inflatesToExactly = inflatesToExactly;
|
|
50
59
|
exports.isBlobComplete = isBlobComplete;
|
|
60
|
+
exports.openStoredBlobBody = openStoredBlobBody;
|
|
51
61
|
exports.findIncompleteBlobRefs = findIncompleteBlobRefs;
|
|
52
62
|
const msgpackr_1 = require("msgpackr");
|
|
53
63
|
const promises_1 = require("node:fs/promises");
|
|
@@ -59,25 +69,31 @@ const environmentManager_ts_1 = require("../utility/environment/environmentManag
|
|
|
59
69
|
const hdbTerms_ts_1 = require("../utility/hdbTerms.js");
|
|
60
70
|
const path_1 = require("path");
|
|
61
71
|
const logger_ts_1 = require("../utility/logging/logger.js");
|
|
72
|
+
const watchPath_ts_1 = require("../utility/watchPath.js");
|
|
62
73
|
const contentTypes_ts_1 = require("../server/serverHelpers/contentTypes.js");
|
|
63
74
|
const node_v8_1 = require("node:v8");
|
|
64
75
|
const promises_2 = require("node:timers/promises");
|
|
65
76
|
const globals_js_1 = require("../globals.js");
|
|
77
|
+
function discardStorage(storageInfo) {
|
|
78
|
+
(storageInfo.fileState ??= {}).discarded = true;
|
|
79
|
+
}
|
|
66
80
|
const FILE_STORAGE_THRESHOLD = 8192; // if the file is below this size, we will store it in memory, or within the record itself, otherwise we will store it in a file
|
|
67
81
|
// We want to keep the file path private (but accessible to the extension)
|
|
68
82
|
const HEADER_SIZE = 8;
|
|
69
83
|
const UNCOMPRESSED_TYPE = 0;
|
|
70
|
-
|
|
84
|
+
exports.DEFLATE_TYPE = 1;
|
|
71
85
|
const ERROR_TYPE = 0xff;
|
|
72
86
|
// A write that aborted on a re-streamable external source (replication receive / origin fetch) stamps the
|
|
73
87
|
// file with this type so a downstream read returns 503 (retry) rather than 500 (confidently incomplete).
|
|
74
|
-
// The
|
|
75
|
-
//
|
|
76
|
-
//
|
|
88
|
+
// The re-stream builds a fresh blob and takes a NEW file id (harper-pro's `createBlob` → `saveBlob`), so
|
|
89
|
+
// this stub is orphaned rather than overwritten; a terminal give-up unlinks it (→ 404). No path rewrites
|
|
90
|
+
// a published blob file in place: `repairBlobFile` is the only same-id writer and it publishes via a
|
|
91
|
+
// `.repair` temp + rename. Distinct from ERROR_TYPE (a permanent corrupt/error stub, replicated as-is).
|
|
92
|
+
// See harper-pro#481.
|
|
77
93
|
const PENDING_TYPE = 0xfe;
|
|
78
94
|
const BLOB_REPAIR_SUFFIX = '.repair';
|
|
79
95
|
const DEFAULT_HEADER = new Uint8Array([0, UNCOMPRESSED_TYPE, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
|
|
80
|
-
const COMPRESS_HEADER = new Uint8Array([0, DEFLATE_TYPE, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
|
|
96
|
+
const COMPRESS_HEADER = new Uint8Array([0, exports.DEFLATE_TYPE, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
|
|
81
97
|
const UNKNOWN_SIZE = 0xffffffffffff;
|
|
82
98
|
const storageInfoForBlob = new WeakMap();
|
|
83
99
|
// Cross-thread receive-in-flight signal. The lock lives on the shared store (same primitive
|
|
@@ -215,6 +231,50 @@ const BLOB_GONE_STATUS = 404;
|
|
|
215
231
|
// retryable class from the permanent ones without duplicating the status code.
|
|
216
232
|
exports.BLOB_UNAVAILABLE_STATUS = 503;
|
|
217
233
|
const BLOB_CORRUPT_STATUS = 500;
|
|
234
|
+
/**
|
|
235
|
+
* Arm the one-shot watch that wakes an in-progress blob read of `filePath`, or return `undefined`
|
|
236
|
+
* for a read that has to poll instead. `isLive` decides whether the read still owns a callback's
|
|
237
|
+
* watcher: a callback from one it has already replaced must not act, or it would close the live
|
|
238
|
+
* watcher and start a second read at the same position. `onFailure` runs only for a live watcher
|
|
239
|
+
* that fails after registration — a registration that throws latches `mustPoll` and leaves the
|
|
240
|
+
* caller in its own no-watcher branch.
|
|
241
|
+
*/
|
|
242
|
+
function watchInProgressFile(filePath, watchTarget, handlers, watchFile = node_fs_1.watch) {
|
|
243
|
+
if (watchTarget.mustPoll)
|
|
244
|
+
return undefined;
|
|
245
|
+
let watcher;
|
|
246
|
+
try {
|
|
247
|
+
watcher = watchFile(watchTarget.path, { persistent: false }, () => {
|
|
248
|
+
if (handlers.isLive(watcher))
|
|
249
|
+
handlers.onChange();
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
// fs.watch throws synchronously when the OS watcher pool is exhausted (EMFILE/ENOSPC). Latch
|
|
254
|
+
// on the stream-scoped target, or the caller's re-poll re-enters here and re-attempts the same
|
|
255
|
+
// failing registration for the rest of the read.
|
|
256
|
+
logger_ts_1.logger.debug?.(`Could not watch ${filePath} for in-progress writes, polling instead:`, error);
|
|
257
|
+
watchTarget.mustPoll = true;
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
// An FSWatcher that fails after registration emits 'error'; with no listener Node rethrows it out
|
|
261
|
+
// of the watcher callback.
|
|
262
|
+
watcher.on('error', (error) => {
|
|
263
|
+
if (!handlers.isLive(watcher))
|
|
264
|
+
return;
|
|
265
|
+
logger_ts_1.logger.debug?.(`Watch of ${filePath} failed, polling instead:`, error);
|
|
266
|
+
watchTarget.mustPoll = true;
|
|
267
|
+
try {
|
|
268
|
+
watcher.close();
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// A close() that throws here must not skip onFailure — the poll fallback is what recovers
|
|
272
|
+
// the read, and the exception would otherwise escape this listener uncaught.
|
|
273
|
+
}
|
|
274
|
+
handlers.onFailure();
|
|
275
|
+
});
|
|
276
|
+
return watcher;
|
|
277
|
+
}
|
|
218
278
|
class BlobReadError extends Error {
|
|
219
279
|
statusCode;
|
|
220
280
|
code;
|
|
@@ -231,6 +291,41 @@ class BlobReadError extends Error {
|
|
|
231
291
|
this.code = 'ENOENT';
|
|
232
292
|
}
|
|
233
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Wait until no writer holds the blob's write lock, bounded by the blob read timeout. This is the
|
|
296
|
+
* completeness authority for a compressed body: its header stores the uncompressed size, so unlike
|
|
297
|
+
* an uncompressed blob its length on disk proves nothing, and inflating a partial deflate stream
|
|
298
|
+
* errors. Resolving means the writer (if any) has finished — not that this caller holds the lock.
|
|
299
|
+
*/
|
|
300
|
+
function waitForBlobWriteCompletion(storageInfo) {
|
|
301
|
+
const store = storageInfo.store;
|
|
302
|
+
const lockKey = storageInfo.fileId + ':blob';
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
// Probe first: the writer lock is free on the overwhelming majority of reads, so arm the
|
|
305
|
+
// timeout only when a writer actually holds it — an uncontended compressed read pays a
|
|
306
|
+
// tryLock, not a setTimeout.
|
|
307
|
+
let settled = false;
|
|
308
|
+
let timer;
|
|
309
|
+
function onReleased() {
|
|
310
|
+
if (settled)
|
|
311
|
+
return;
|
|
312
|
+
settled = true;
|
|
313
|
+
clearTimeout(timer);
|
|
314
|
+
resolve();
|
|
315
|
+
}
|
|
316
|
+
if (store.tryLock(lockKey, onReleased)) {
|
|
317
|
+
store.unlock(lockKey);
|
|
318
|
+
return resolve();
|
|
319
|
+
}
|
|
320
|
+
timer = setTimeout(() => {
|
|
321
|
+
if (settled)
|
|
322
|
+
return;
|
|
323
|
+
settled = true;
|
|
324
|
+
reject(new BlobReadError(`Blob ${storageInfo.fileId} is unavailable; the in-progress write did not complete in time`, exports.BLOB_UNAVAILABLE_STATUS));
|
|
325
|
+
}, getBlobReadTimeout());
|
|
326
|
+
timer.unref();
|
|
327
|
+
});
|
|
328
|
+
}
|
|
234
329
|
// We want FileBackedBlob instances to be an instanceof Blob, but we don't want to actually extend the class and call Blob's constructor, which is quite expensive because it has to set it up as a transferrable.
|
|
235
330
|
function InstanceOfBlobWithNoConstructor() { }
|
|
236
331
|
InstanceOfBlobWithNoConstructor.prototype = exports.Blob.prototype;
|
|
@@ -329,8 +424,6 @@ class FileBackedBlob extends exports.Blob {
|
|
|
329
424
|
throw new BlobReadError('Blob pending replication for ' + filePath, exports.BLOB_UNAVAILABLE_STATUS);
|
|
330
425
|
}
|
|
331
426
|
size = Number(headerValue & 0xffffffffffffn);
|
|
332
|
-
if (size < end)
|
|
333
|
-
size = end;
|
|
334
427
|
if (size < UNKNOWN_SIZE) {
|
|
335
428
|
if (isFullRead && descriptorSize != null && descriptorSize < UNKNOWN_SIZE && size !== descriptorSize) {
|
|
336
429
|
// the header claims a different (uncompressed) size than the record descriptor: the file
|
|
@@ -449,7 +542,7 @@ class FileBackedBlob extends exports.Blob {
|
|
|
449
542
|
// Only sniff the storage-type byte and take the decompress branch when a full header is present.
|
|
450
543
|
// A file shorter than the header (truncated/corrupted, #1424) would otherwise be mis-read as a
|
|
451
544
|
// deflate body and yield garbage; instead it falls through to checkCompletion as incomplete.
|
|
452
|
-
if (rawBytes.length >= HEADER_SIZE && rawBytes[1] === DEFLATE_TYPE) {
|
|
545
|
+
if (rawBytes.length >= HEADER_SIZE && rawBytes[1] === exports.DEFLATE_TYPE) {
|
|
453
546
|
// An in-flight compressed file still carrying the UNKNOWN_SIZE placeholder is definitely
|
|
454
547
|
// incomplete; otherwise the size is finalized but the compressed body may still be streaming,
|
|
455
548
|
// so verify via the write lock before inflating (inflating a partial deflate stream errors).
|
|
@@ -457,10 +550,22 @@ class FileBackedBlob extends exports.Blob {
|
|
|
457
550
|
if (pending)
|
|
458
551
|
return pending;
|
|
459
552
|
return new Promise((resolve, reject) => {
|
|
460
|
-
// start/end index into the UNCOMPRESSED content, so inflate first, then slice
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
553
|
+
// start/end index into the UNCOMPRESSED content, so inflate first, then slice — with
|
|
554
|
+
// output capped at the header's declared size, so a body that inflates past its own
|
|
555
|
+
// header (a lying or corrupted header) is refused instead of allocating without bound,
|
|
556
|
+
// matching the streaming read path's ceiling. zlib rejects a cap of 0, so an empty
|
|
557
|
+
// blob's cap is 1 and the exact-length check below covers it.
|
|
558
|
+
(0, node_zlib_1.inflate)(rawBytes.subarray(HEADER_SIZE), { maxOutputLength: Math.max(size, 1) }, (error, result) => {
|
|
559
|
+
if (error) {
|
|
560
|
+
const code = error.code ?? '';
|
|
561
|
+
reject(code === 'ERR_BUFFER_TOO_LARGE'
|
|
562
|
+
? new BlobReadError(`Blob ${filePath} inflates past its declared size of ${size}`, BLOB_CORRUPT_STATUS)
|
|
563
|
+
: code.startsWith('Z_')
|
|
564
|
+
? new BlobReadError(`Blob ${filePath} is not a valid deflate stream: ${error.message}`, BLOB_CORRUPT_STATUS)
|
|
565
|
+
: error);
|
|
566
|
+
}
|
|
567
|
+
else if (result.length !== size)
|
|
568
|
+
reject(new BlobReadError(`Blob ${filePath} inflates to ${result.length} bytes, not its declared size of ${size}`, BLOB_CORRUPT_STATUS));
|
|
464
569
|
else
|
|
465
570
|
resolve(sliceContent(result));
|
|
466
571
|
});
|
|
@@ -507,6 +612,20 @@ class FileBackedBlob extends exports.Blob {
|
|
|
507
612
|
let position = 0;
|
|
508
613
|
let totalContentRead = 0;
|
|
509
614
|
let watcher;
|
|
615
|
+
// Drop the live watcher before closing it, so a callback racing the close cannot pass its
|
|
616
|
+
// `isLive` check — and so a close() on an already-failed handle cannot abandon the teardown
|
|
617
|
+
// it was part of. Every site that retires this watcher goes through here.
|
|
618
|
+
const closeWatcher = () => {
|
|
619
|
+
const opened = watcher;
|
|
620
|
+
watcher = null;
|
|
621
|
+
try {
|
|
622
|
+
opened?.close();
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
logger_ts_1.logger.debug?.(`Could not close the in-progress watch of ${filePath}:`, error);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
let watchTarget;
|
|
510
629
|
let timer;
|
|
511
630
|
// The start() open-retry timer lives in a different scope/phase than pull()'s `timer`; track it
|
|
512
631
|
// separately so a cancel() during the file-creation wait clears it instead of leaking an fd (#1457).
|
|
@@ -535,6 +654,174 @@ class FileBackedBlob extends exports.Blob {
|
|
|
535
654
|
// full reads can be compared against the header.
|
|
536
655
|
const descriptorSize = this.size;
|
|
537
656
|
const isFullRead = start === undefined && end === undefined;
|
|
657
|
+
let compressedRead;
|
|
658
|
+
let teardownCompressedRead;
|
|
659
|
+
// `bodyPrefix` is what the first raw read already pulled past the header; `bodyPosition` is where
|
|
660
|
+
// the file read continues. Both stay valid across the writer wait because a body is only ever
|
|
661
|
+
// appended to, and the stamps that do overwrite it (PENDING/ERROR) change the header re-read below.
|
|
662
|
+
async function* readCompressedBlob(bodyPrefix, bodyPosition) {
|
|
663
|
+
// The writer's lock is the only completeness authority for a compressed body (the header
|
|
664
|
+
// stores the uncompressed size, so body length proves nothing; a partial deflate stream
|
|
665
|
+
// errors on inflate). Wait for it — bounded, like every other blob read wait (#1423).
|
|
666
|
+
await waitForBlobWriteCompletion(storageInfo);
|
|
667
|
+
if (cancelled || fd == null)
|
|
668
|
+
return;
|
|
669
|
+
// An in-place repair renames a fresh (uncompressed) file over the path while holding the
|
|
670
|
+
// writer lock, so the descriptor we opened before the wait can be left on the orphaned inode.
|
|
671
|
+
// The repair may finish either during our wait or in the gap between the first read and the
|
|
672
|
+
// lock probe, so detect it by file identity rather than by whether we waited: if the path now
|
|
673
|
+
// resolves to a different inode than our descriptor, the file was swapped. Reopen so the header
|
|
674
|
+
// re-read and the stream below see the current file — a repaired (uncompressed) header is then
|
|
675
|
+
// reported retryable 503 (mirroring openStoredBlobBody), and the prefix read from the old inode
|
|
676
|
+
// is discarded. If the inode is unchanged the descriptor is still valid and the same-fd re-read
|
|
677
|
+
// still catches an in-place PENDING/ERROR stamp (harper-pro#481).
|
|
678
|
+
let fileSwapped = false;
|
|
679
|
+
try {
|
|
680
|
+
const currentFd = fd;
|
|
681
|
+
const [openStat, pathStat] = await Promise.all([
|
|
682
|
+
new Promise((res, rej) => (0, node_fs_1.fstat)(currentFd, (error, s) => (error ? rej(error) : res(s)))),
|
|
683
|
+
new Promise((res, rej) => (0, node_fs_1.stat)(filePath, (error, s) => (error ? rej(error) : res(s)))),
|
|
684
|
+
]);
|
|
685
|
+
fileSwapped = openStat.ino !== pathStat.ino;
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
// the path is momentarily unstatable, or the descriptor was closed by a concurrent cancel;
|
|
689
|
+
// fall through — the cancellation recheck below returns, and otherwise the header re-read
|
|
690
|
+
// reports the outcome. The blob hold makes a vanished path unlikely here.
|
|
691
|
+
}
|
|
692
|
+
if (cancelled || fd == null)
|
|
693
|
+
return; // the identity stat awaited; a cancel may have landed
|
|
694
|
+
if (fileSwapped) {
|
|
695
|
+
// Null `fd` before closing it: a closed descriptor number left in `fd` across the await
|
|
696
|
+
// below would be closed a second time by a concurrent cancel()/closeFd() — after the OS may
|
|
697
|
+
// have reassigned it to an unrelated file or socket (#1457).
|
|
698
|
+
const staleFd = fd;
|
|
699
|
+
fd = null;
|
|
700
|
+
(0, node_fs_1.close)(staleFd);
|
|
701
|
+
fd = await new Promise((resolveOpen, rejectOpen) => (0, node_fs_1.open)(filePath, 'r', (error, openedFd) => (error ? rejectOpen(error) : resolveOpen(openedFd))));
|
|
702
|
+
if (cancelled) {
|
|
703
|
+
closeFd();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
bodyPrefix = Buffer.alloc(0);
|
|
707
|
+
bodyPosition = HEADER_SIZE;
|
|
708
|
+
}
|
|
709
|
+
// Re-read the header from the (current) fd: the writer we just waited for finalizes the size
|
|
710
|
+
// in place, and an aborted receive stamps PENDING/ERROR over the same inode (harper-pro#481).
|
|
711
|
+
const header = Buffer.allocUnsafe(HEADER_SIZE);
|
|
712
|
+
const headerBytes = await new Promise((resolveRead, rejectRead) => (0, node_fs_1.read)(fd, header, 0, HEADER_SIZE, 0, (error, bytesRead) => (error ? rejectRead(error) : resolveRead(bytesRead))));
|
|
713
|
+
if (cancelled || fd == null)
|
|
714
|
+
return;
|
|
715
|
+
if (headerBytes < HEADER_SIZE)
|
|
716
|
+
throw new BlobReadError(`Incomplete blob for ${filePath}`, BLOB_CORRUPT_STATUS);
|
|
717
|
+
const headerValue = new DataView(header.buffer, header.byteOffset, HEADER_SIZE).getBigUint64(0);
|
|
718
|
+
const type = Number(headerValue >> 48n);
|
|
719
|
+
if (type === ERROR_TYPE || type === PENDING_TYPE) {
|
|
720
|
+
const message = Buffer.allocUnsafe(256);
|
|
721
|
+
const messageBytes = await new Promise((resolveRead) => (0, node_fs_1.read)(fd, message, 0, message.length, HEADER_SIZE, (error, bytesRead) => resolveRead(error ? 0 : bytesRead)));
|
|
722
|
+
throw type === ERROR_TYPE
|
|
723
|
+
? new BlobReadError('Error in blob: ' + message.subarray(0, messageBytes), BLOB_CORRUPT_STATUS)
|
|
724
|
+
: new BlobReadError('Blob pending replication for ' + filePath, exports.BLOB_UNAVAILABLE_STATUS);
|
|
725
|
+
}
|
|
726
|
+
const declaredSize = Number(headerValue & 0xffffffffffffn);
|
|
727
|
+
if (type === UNCOMPRESSED_TYPE) {
|
|
728
|
+
// We sniffed DEFLATE, waited, and the file is now uncompressed: an in-place repair
|
|
729
|
+
// replaced it with a healthy uncompressed body. Report retryable so the caller re-reads
|
|
730
|
+
// the current file through the ordinary path rather than misclassifying it as corrupt.
|
|
731
|
+
throw new BlobReadError(`Blob ${filePath} was replaced while reading; retry`, exports.BLOB_UNAVAILABLE_STATUS);
|
|
732
|
+
}
|
|
733
|
+
if (type !== exports.DEFLATE_TYPE || declaredSize === UNKNOWN_SIZE) {
|
|
734
|
+
// the writer is confirmed done, so a placeholder size (or a header no longer readable as
|
|
735
|
+
// deflate) is a write that never finished — confidently incomplete, not merely in progress
|
|
736
|
+
throw new BlobReadError(`Incomplete blob for ${filePath}`, BLOB_CORRUPT_STATUS);
|
|
737
|
+
}
|
|
738
|
+
if (isFullRead && descriptorSize != null && descriptorSize < UNKNOWN_SIZE && declaredSize !== descriptorSize) {
|
|
739
|
+
throw new BlobReadError(`Blob size mismatch for ${filePath}: record descriptor expects ${descriptorSize} bytes, on-disk header reports ${declaredSize}`, BLOB_CORRUPT_STATUS);
|
|
740
|
+
}
|
|
741
|
+
if (blob.size !== declaredSize) {
|
|
742
|
+
blob.size = declaredSize;
|
|
743
|
+
if (blob.#onSize) {
|
|
744
|
+
for (const callback of blob.#onSize)
|
|
745
|
+
callback(declaredSize);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// The read stream takes sole ownership of the descriptor (nulling `fd` leaves closeFd() only
|
|
749
|
+
// the hold to release): an fs stream's worker-thread close(2) races any other user of the fd.
|
|
750
|
+
const source = (0, node_fs_1.createReadStream)(filePath, { fd, start: bodyPosition, autoClose: true });
|
|
751
|
+
fd = null;
|
|
752
|
+
const inflater = (0, node_zlib_1.createInflate)();
|
|
753
|
+
teardownCompressedRead = () => {
|
|
754
|
+
source.destroy();
|
|
755
|
+
inflater.destroy();
|
|
756
|
+
};
|
|
757
|
+
// pipe() does not forward source errors to its destination; without this a disk fault
|
|
758
|
+
// mid-read would be an unhandled 'error' on the source stream.
|
|
759
|
+
source.on('error', (error) => inflater.destroy(error));
|
|
760
|
+
if (bodyPrefix.length > 0)
|
|
761
|
+
inflater.write(bodyPrefix);
|
|
762
|
+
source.pipe(inflater);
|
|
763
|
+
const sliceStart = start ?? 0;
|
|
764
|
+
let inflatedTotal = 0; // content offset consumed so far (emitted or discarded)
|
|
765
|
+
try {
|
|
766
|
+
for await (const chunk of inflater) {
|
|
767
|
+
const chunkStart = inflatedTotal;
|
|
768
|
+
inflatedTotal += chunk.length;
|
|
769
|
+
if (inflatedTotal > declaredSize) {
|
|
770
|
+
// refused before emitting, so a body that lies about its length cannot stream unbounded output
|
|
771
|
+
throw new BlobReadError(`Blob ${filePath} inflates past its declared size of ${declaredSize}`, BLOB_CORRUPT_STATUS);
|
|
772
|
+
}
|
|
773
|
+
const emitFrom = Math.max(sliceStart - chunkStart, 0);
|
|
774
|
+
const emitTo = end === undefined ? chunk.length : Math.min(end - chunkStart, chunk.length);
|
|
775
|
+
if (emitTo > emitFrom)
|
|
776
|
+
yield emitFrom === 0 && emitTo === chunk.length ? chunk : chunk.subarray(emitFrom, emitTo);
|
|
777
|
+
if (end !== undefined && inflatedTotal >= end)
|
|
778
|
+
return; // slice satisfied; stop inflating
|
|
779
|
+
}
|
|
780
|
+
if (inflatedTotal !== declaredSize) {
|
|
781
|
+
// clean deflate end short of the declared size: torn despite the writer being done
|
|
782
|
+
throw new BlobReadError(`Incomplete blob for ${filePath}: inflated to ${inflatedTotal} bytes of a declared ${declaredSize}`, BLOB_CORRUPT_STATUS);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
if (cancelled)
|
|
787
|
+
return;
|
|
788
|
+
if (!(error instanceof BlobReadError) && error.code?.startsWith?.('Z_')) {
|
|
789
|
+
// the writer is confirmed done, so a zlib failure is real corruption, not an in-progress body
|
|
790
|
+
throw new BlobReadError(`Error reading compressed blob for ${filePath}: ${error.message}`, BLOB_CORRUPT_STATUS);
|
|
791
|
+
}
|
|
792
|
+
throw error;
|
|
793
|
+
}
|
|
794
|
+
finally {
|
|
795
|
+
teardownCompressedRead();
|
|
796
|
+
teardownCompressedRead = undefined;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const pumpCompressedRead = (controller) => compressedRead.next().then((step) => {
|
|
800
|
+
if (cancelled)
|
|
801
|
+
return;
|
|
802
|
+
if (step.done) {
|
|
803
|
+
closeFd();
|
|
804
|
+
try {
|
|
805
|
+
controller.close();
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
// controller may already be closed
|
|
809
|
+
}
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
try {
|
|
813
|
+
controller.enqueue(step.value);
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
logger_ts_1.logger.debug?.('Error enqueuing chunk', error);
|
|
817
|
+
}
|
|
818
|
+
}, (error) => {
|
|
819
|
+
closeFd();
|
|
820
|
+
if (cancelled)
|
|
821
|
+
return;
|
|
822
|
+
blob.#onError?.forEach((callback) => callback(error));
|
|
823
|
+
throw error;
|
|
824
|
+
});
|
|
538
825
|
return new ReadableStream({
|
|
539
826
|
start() {
|
|
540
827
|
// Bound the wait for the file to appear so a write that was abandoned (e.g. the file was
|
|
@@ -586,6 +873,8 @@ class FileBackedBlob extends exports.Blob {
|
|
|
586
873
|
return new Promise(openFile);
|
|
587
874
|
},
|
|
588
875
|
pull: (controller) => {
|
|
876
|
+
if (compressedRead)
|
|
877
|
+
return pumpCompressedRead(controller);
|
|
589
878
|
let size = 0;
|
|
590
879
|
let retries = 100;
|
|
591
880
|
// No-progress deadline for the incomplete-content wait below, mirroring the open-retry loop's
|
|
@@ -608,8 +897,7 @@ class FileBackedBlob extends exports.Blob {
|
|
|
608
897
|
settled = true;
|
|
609
898
|
closeFd();
|
|
610
899
|
clearTimeout(timer);
|
|
611
|
-
|
|
612
|
-
watcher.close();
|
|
900
|
+
closeWatcher();
|
|
613
901
|
reject(error);
|
|
614
902
|
blob.#onError?.forEach((callback) => callback(error));
|
|
615
903
|
}
|
|
@@ -652,35 +940,12 @@ class FileBackedBlob extends exports.Blob {
|
|
|
652
940
|
// half-replicated blob whose source stream aborted; the bytes are still expected — retry (harper-pro#481)
|
|
653
941
|
return onError(new BlobReadError('Blob pending replication for ' + filePath, exports.BLOB_UNAVAILABLE_STATUS));
|
|
654
942
|
}
|
|
655
|
-
if (buffer[1] === DEFLATE_TYPE) {
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
// Settle exactly once and route the close through closeFd() so we never touch a
|
|
662
|
-
// reassigned/nulled descriptor (#1457).
|
|
663
|
-
if (settled || cancelled)
|
|
664
|
-
return resolve();
|
|
665
|
-
settled = true;
|
|
666
|
-
closeFd();
|
|
667
|
-
if (bytes.length > 0) {
|
|
668
|
-
try {
|
|
669
|
-
controller.enqueue(bytes);
|
|
670
|
-
}
|
|
671
|
-
catch (error) {
|
|
672
|
-
logger_ts_1.logger.debug?.('Error enqueuing chunk', error);
|
|
673
|
-
return resolve();
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
try {
|
|
677
|
-
controller.close();
|
|
678
|
-
}
|
|
679
|
-
catch {
|
|
680
|
-
// controller may already be closed
|
|
681
|
-
}
|
|
682
|
-
resolve();
|
|
683
|
-
}, (error) => onError(error));
|
|
943
|
+
if (buffer[1] === exports.DEFLATE_TYPE) {
|
|
944
|
+
// a deflate body cannot be length-checked or offset-seeked: this and every later
|
|
945
|
+
// pull go through the streaming inflate iterator instead of the raw read loop
|
|
946
|
+
settled = true;
|
|
947
|
+
compressedRead = readCompressedBlob(buffer.subarray(HEADER_SIZE, bytesRead), bytesRead);
|
|
948
|
+
return pumpCompressedRead(controller).then(resolve, reject);
|
|
684
949
|
}
|
|
685
950
|
size = Number(headerValue & 0xffffffffffffn);
|
|
686
951
|
if (size < UNKNOWN_SIZE) {
|
|
@@ -728,10 +993,7 @@ class FileBackedBlob extends exports.Blob {
|
|
|
728
993
|
if (updatedSize === UNKNOWN_SIZE)
|
|
729
994
|
return false;
|
|
730
995
|
size = updatedSize;
|
|
731
|
-
|
|
732
|
-
watcher.close();
|
|
733
|
-
watcher = null;
|
|
734
|
-
}
|
|
996
|
+
closeWatcher();
|
|
735
997
|
// The header reports a known final size but the bytes at `position` have not arrived.
|
|
736
998
|
// Re-entering readMore() synchronously here busy-spins the worker at ~100% CPU on a
|
|
737
999
|
// present-but-truncated blob (header rewritten to a self-consistent smaller size, lock
|
|
@@ -749,26 +1011,39 @@ class FileBackedBlob extends exports.Blob {
|
|
|
749
1011
|
return true;
|
|
750
1012
|
};
|
|
751
1013
|
// the file is not finished being written, watch the file for changes to resume reading
|
|
752
|
-
|
|
753
|
-
watcher = (
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1014
|
+
watchTarget ??= (0, watchPath_ts_1.resolveWatchTarget)(filePath);
|
|
1015
|
+
watcher = watchInProgressFile(filePath, watchTarget, {
|
|
1016
|
+
isLive: (candidate) => watcher === candidate,
|
|
1017
|
+
onChange: () => {
|
|
1018
|
+
closeWatcher();
|
|
757
1019
|
clearTimeout(timer); // clear it
|
|
758
1020
|
readMore(resolve, reject);
|
|
759
|
-
}
|
|
1021
|
+
},
|
|
1022
|
+
onFailure: () => {
|
|
1023
|
+
watcher = null;
|
|
1024
|
+
clearTimeout(timer);
|
|
1025
|
+
timer = setTimeout(() => readMore(resolve, reject), 20).unref();
|
|
1026
|
+
},
|
|
760
1027
|
});
|
|
761
1028
|
// immediately try to read again in case there was a change before we started watching,
|
|
762
1029
|
// readSync should be fine here, the data should be in memory
|
|
763
1030
|
if ((0, node_fs_1.readSync)(fd, buffer, 0, buffer.length, position) > 0) {
|
|
764
1031
|
// never mind with the watcher, let's read more data
|
|
765
|
-
|
|
766
|
-
watcher.close();
|
|
767
|
-
watcher = null;
|
|
768
|
-
}
|
|
1032
|
+
closeWatcher();
|
|
769
1033
|
readMore(resolve, reject);
|
|
770
1034
|
}
|
|
771
1035
|
else if (!resumeIfWriterFinished()) {
|
|
1036
|
+
if (!watcher) {
|
|
1037
|
+
// Nothing will wake this read, so poll on the deadline resumeIfWriterFinished uses
|
|
1038
|
+
// rather than sitting out the full read timeout and 503-ing a healthy write.
|
|
1039
|
+
if (Date.now() >= incompleteDeadline) {
|
|
1040
|
+
onError(new BlobReadError(`File read timed out reading from ${filePath}, read ${totalContentRead} bytes, but size is supposed to be ${size} bytes`, exports.BLOB_UNAVAILABLE_STATUS));
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
timer = setTimeout(() => readMore(resolve, reject), 20).unref();
|
|
1044
|
+
}
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
772
1047
|
// set a timer for the watcher too. A write that stalls past the bound returns a
|
|
773
1048
|
// prompt 503 (retryable) instead of holding the connection for the full 60s (#1423).
|
|
774
1049
|
timer = setTimeout(() => {
|
|
@@ -851,11 +1126,11 @@ class FileBackedBlob extends exports.Blob {
|
|
|
851
1126
|
},
|
|
852
1127
|
cancel() {
|
|
853
1128
|
cancelled = true;
|
|
1129
|
+
teardownCompressedRead?.();
|
|
854
1130
|
closeFd(); // releases the hold, including when cancelled before any open succeeded
|
|
855
1131
|
clearTimeout(timer);
|
|
856
1132
|
clearTimeout(openTimer);
|
|
857
|
-
|
|
858
|
-
watcher.close();
|
|
1133
|
+
closeWatcher();
|
|
859
1134
|
},
|
|
860
1135
|
});
|
|
861
1136
|
function checkIfIsBeingWritten() {
|
|
@@ -877,6 +1152,7 @@ class FileBackedBlob extends exports.Blob {
|
|
|
877
1152
|
if (sourceStorageInfo?.fileId) {
|
|
878
1153
|
const slicedStorageInfo = {
|
|
879
1154
|
...sourceStorageInfo,
|
|
1155
|
+
fileState: (sourceStorageInfo.fileState ??= {}),
|
|
880
1156
|
start,
|
|
881
1157
|
end,
|
|
882
1158
|
};
|
|
@@ -1153,8 +1429,24 @@ function deleteBlob(blob) {
|
|
|
1153
1429
|
Atomics.store(state.table, state.slot + REREFERENCED, 0);
|
|
1154
1430
|
}
|
|
1155
1431
|
// Reusing the queued entry when two writes supersede the same file keeps the age cap measuring
|
|
1156
|
-
// from the first supersession.
|
|
1157
|
-
const pending = pendingReclamation.get(filePath) ?? {
|
|
1432
|
+
// from the first supersession and tracks every live blob instance that carries the condemned fileId.
|
|
1433
|
+
const pending = pendingReclamation.get(filePath) ?? {
|
|
1434
|
+
blobs: [],
|
|
1435
|
+
seenBlobs: new WeakSet(),
|
|
1436
|
+
fileInfo: { store: storageInfo?.store, fileId: storageInfo?.fileId },
|
|
1437
|
+
deadline: 0,
|
|
1438
|
+
enqueuedAt: now,
|
|
1439
|
+
supersededAt: now,
|
|
1440
|
+
};
|
|
1441
|
+
if (!pending.seenBlobs.has(blob)) {
|
|
1442
|
+
pending.seenBlobs.add(blob);
|
|
1443
|
+
pending.blobs.push(new WeakRef(blob));
|
|
1444
|
+
}
|
|
1445
|
+
if (pending.unlinking) {
|
|
1446
|
+
if (storageInfo)
|
|
1447
|
+
discardStorage(storageInfo);
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1158
1450
|
scheduleReclamation(enqueue(filePath, pending, Math.max(pending.deadline, now + getReclamationDelay())));
|
|
1159
1451
|
}
|
|
1160
1452
|
/**
|
|
@@ -1201,7 +1493,7 @@ function runReclamation() {
|
|
|
1201
1493
|
earliest = pending.deadline;
|
|
1202
1494
|
break; // insertion order is deadline order; nothing behind this entry is due
|
|
1203
1495
|
}
|
|
1204
|
-
const storageInfo =
|
|
1496
|
+
const storageInfo = pending.fileInfo;
|
|
1205
1497
|
const expired = now - pending.enqueuedAt >= ageCap;
|
|
1206
1498
|
let held;
|
|
1207
1499
|
try {
|
|
@@ -1255,6 +1547,16 @@ function runReclamation() {
|
|
|
1255
1547
|
// Keep the entry until the unlink lands so a concurrent re-reference can tell that the file is
|
|
1256
1548
|
// already going away instead of silently adopting a doomed path.
|
|
1257
1549
|
pending.unlinking = true;
|
|
1550
|
+
// Once reclamation has claimed this file, a later write must not preserve its soon-to-be-deleted
|
|
1551
|
+
// fileId. The retention window above still permits legitimate re-references before this point.
|
|
1552
|
+
for (const blobRef of pending.blobs) {
|
|
1553
|
+
const blob = blobRef.deref();
|
|
1554
|
+
if (!blob)
|
|
1555
|
+
continue;
|
|
1556
|
+
const instanceStorageInfo = storageInfoForBlob.get(blob);
|
|
1557
|
+
if (instanceStorageInfo)
|
|
1558
|
+
discardStorage(instanceStorageInfo);
|
|
1559
|
+
}
|
|
1258
1560
|
(0, node_fs_1.unlink)(filePath, (error) => {
|
|
1259
1561
|
pendingReclamation.delete(filePath);
|
|
1260
1562
|
if (pendingReclamation.size === 0)
|
|
@@ -1276,6 +1578,87 @@ function runReclamation() {
|
|
|
1276
1578
|
function setDeletionDelay(delay) {
|
|
1277
1579
|
reclamationDelayOverride = delay;
|
|
1278
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* `StorageInfo.contentBuffer` must always hold a `Buffer`: `Buffer.prototype.toString()` decodes
|
|
1583
|
+
* UTF-8, while the inherited `Uint8Array.prototype.toString()` joins the byte values with commas.
|
|
1584
|
+
*/
|
|
1585
|
+
function asBuffer(bytes) {
|
|
1586
|
+
return Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1587
|
+
}
|
|
1588
|
+
const DEFAULT_COMPRESSION_THRESHOLD = FILE_STORAGE_THRESHOLD;
|
|
1589
|
+
/**
|
|
1590
|
+
* Already-compressed containers (the deployment recorder writes `application/gzip`). An operator entry
|
|
1591
|
+
* for the same key overrides these; a broader operator wildcard does not — specificity beats origin.
|
|
1592
|
+
*/
|
|
1593
|
+
const BUILT_IN_COMPRESSION_ENTRIES = {
|
|
1594
|
+
'application/gzip': false,
|
|
1595
|
+
'application/zip': false,
|
|
1596
|
+
'application/zstd': false,
|
|
1597
|
+
'image/*': false,
|
|
1598
|
+
'video/*': false,
|
|
1599
|
+
'audio/*': false,
|
|
1600
|
+
};
|
|
1601
|
+
/** The matcher works on the canonical lowercase base media type, with any `;` parameters stripped. */
|
|
1602
|
+
function normalizeContentType(type) {
|
|
1603
|
+
if (!type)
|
|
1604
|
+
return '';
|
|
1605
|
+
const parametersStart = type.indexOf(';');
|
|
1606
|
+
return (parametersStart === -1 ? type : type.slice(0, parametersStart)).trim().toLowerCase();
|
|
1607
|
+
}
|
|
1608
|
+
// The normalized policy map is rebuilt only when the configured value object changes identity, so
|
|
1609
|
+
// the per-save cost is one config lookup and a reference compare.
|
|
1610
|
+
let compressionPolicyForConfig;
|
|
1611
|
+
let compressionPolicy = null;
|
|
1612
|
+
let invalidCompressionConfigLogged = false;
|
|
1613
|
+
function getBlobCompressionPolicy() {
|
|
1614
|
+
const configured = (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_BLOBS_COMPRESSION);
|
|
1615
|
+
if (configured === compressionPolicyForConfig)
|
|
1616
|
+
return compressionPolicy;
|
|
1617
|
+
compressionPolicyForConfig = configured;
|
|
1618
|
+
compressionPolicy = null;
|
|
1619
|
+
if (configured == null)
|
|
1620
|
+
return null;
|
|
1621
|
+
if (typeof configured !== 'object' || Array.isArray(configured)) {
|
|
1622
|
+
if (!invalidCompressionConfigLogged) {
|
|
1623
|
+
invalidCompressionConfigLogged = true;
|
|
1624
|
+
logger_ts_1.logger.warn?.('Ignoring invalid storage.blobs.compression value; expected a map of content types');
|
|
1625
|
+
}
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
const policy = new Map(Object.entries(BUILT_IN_COMPRESSION_ENTRIES));
|
|
1629
|
+
for (const [key, entry] of Object.entries(configured)) {
|
|
1630
|
+
policy.set(normalizeContentType(key) || 'default', entry);
|
|
1631
|
+
}
|
|
1632
|
+
compressionPolicy = policy;
|
|
1633
|
+
return policy;
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Whether a new blob of `type` and (uncompressed) `size` should be deflate-compressed on disk,
|
|
1637
|
+
* per the opt-in `storage.blobs.compression` map. Absent config means never. Matching precedence
|
|
1638
|
+
* is exact type, then `type/*`, then `default`. An unknown size never compresses: the threshold
|
|
1639
|
+
* cannot be evaluated, and compressing a small streamed write under a large configured threshold
|
|
1640
|
+
* would violate the operator's limit.
|
|
1641
|
+
*/
|
|
1642
|
+
function resolveBlobCompression(type, size) {
|
|
1643
|
+
const policy = getBlobCompressionPolicy();
|
|
1644
|
+
if (!policy)
|
|
1645
|
+
return false;
|
|
1646
|
+
const normalized = normalizeContentType(type);
|
|
1647
|
+
let entry = policy.get(normalized);
|
|
1648
|
+
if (entry === undefined && normalized) {
|
|
1649
|
+
const slash = normalized.indexOf('/');
|
|
1650
|
+
if (slash > 0)
|
|
1651
|
+
entry = policy.get(normalized.slice(0, slash + 1) + '*');
|
|
1652
|
+
}
|
|
1653
|
+
entry ??= policy.get('default');
|
|
1654
|
+
if (!entry)
|
|
1655
|
+
return false;
|
|
1656
|
+
if (entry.codec !== undefined && entry.codec !== 'deflate')
|
|
1657
|
+
return false; // enforced by config validation; fail safe
|
|
1658
|
+
if (size === undefined)
|
|
1659
|
+
return false;
|
|
1660
|
+
return size >= (entry.threshold ?? DEFAULT_COMPRESSION_THRESHOLD);
|
|
1661
|
+
}
|
|
1279
1662
|
/**
|
|
1280
1663
|
* Create a blob from a readable stream or a buffer by creating a file in the blob storage path with a new unique internal id, that
|
|
1281
1664
|
* can be saved/stored.
|
|
@@ -1292,7 +1675,7 @@ function createBlob(source, options) {
|
|
|
1292
1675
|
storageInfoForBlob.set(blob, storageInfo);
|
|
1293
1676
|
if (source instanceof Uint8Array) {
|
|
1294
1677
|
blob.size = source.length;
|
|
1295
|
-
storageInfo.contentBuffer = source;
|
|
1678
|
+
storageInfo.contentBuffer = asBuffer(source);
|
|
1296
1679
|
}
|
|
1297
1680
|
else if (source instanceof node_stream_1.Readable) {
|
|
1298
1681
|
storageInfo.source = source;
|
|
@@ -1306,6 +1689,22 @@ function createBlob(source, options) {
|
|
|
1306
1689
|
return blob;
|
|
1307
1690
|
}
|
|
1308
1691
|
(0, globals_js_1._assignPackageExport)('createBlob', createBlob);
|
|
1692
|
+
/**
|
|
1693
|
+
* Create a blob whose source stream carries the STORED representation — an already-deflated body —
|
|
1694
|
+
* rather than content bytes, so a replication receiver can land a peer's compressed blob without
|
|
1695
|
+
* recompressing it. `size` is the uncompressed content length (it goes in the header; a raw
|
|
1696
|
+
* compressed source cannot derive it) and the body is verified on write: it must be a well-formed
|
|
1697
|
+
* deflate stream inflating to exactly `size` bytes, or the save rejects (see
|
|
1698
|
+
* createStoredDeflateVerifier). Internal to replication on purpose — not part of BlobCreationOptions
|
|
1699
|
+
* and not exported to components.
|
|
1700
|
+
*/
|
|
1701
|
+
function createBlobFromStoredBody(source, options) {
|
|
1702
|
+
if (options.codec !== 'deflate')
|
|
1703
|
+
throw new Error(`Unsupported stored blob codec: ${options.codec}`);
|
|
1704
|
+
const blob = createBlob(source, { type: options.type, size: options.size });
|
|
1705
|
+
storageInfoForBlob.get(blob).storedCodec = options.codec;
|
|
1706
|
+
return blob;
|
|
1707
|
+
}
|
|
1309
1708
|
// When set (during a migration via encodeBlobsWithFilePath), saveBlob pushes the in-flight
|
|
1310
1709
|
// writeBlob save promise here so the migration can await every blob's durable write before
|
|
1311
1710
|
// declaring the database done. Without this, the migration's `await targetDbi.put(...)`
|
|
@@ -1319,6 +1718,13 @@ function saveBlob(blob, deleteOnFailure = false) {
|
|
|
1319
1718
|
storageInfoForBlob.set(blob, storageInfo);
|
|
1320
1719
|
}
|
|
1321
1720
|
else {
|
|
1721
|
+
if (storageInfo.fileState?.discarded) {
|
|
1722
|
+
// The file this blob was saved to has been deleted (an aborted/skipped write's cleanup, or an
|
|
1723
|
+
// explicit delete). Re-storing it would commit a reference to a file that no longer exists —
|
|
1724
|
+
// a permanently unreadable record, and for a replicated one a blob the peer can never fetch.
|
|
1725
|
+
// Fail here, where the cause is still known, instead of at the eventual read (issue #2062).
|
|
1726
|
+
throw new Error('Blob was discarded (its file was deleted by an aborted or superseded write) and can no longer be stored; the data must be re-supplied');
|
|
1727
|
+
}
|
|
1322
1728
|
if (storageInfo.fileId)
|
|
1323
1729
|
return storageInfo; // if there is any file id, we are already saving and can return the info
|
|
1324
1730
|
storageInfo.store = currentStore;
|
|
@@ -1331,6 +1737,14 @@ function saveBlob(blob, deleteOnFailure = false) {
|
|
|
1331
1737
|
}
|
|
1332
1738
|
return storageInfo; // nothing more to do if it supposed to be saved in the record
|
|
1333
1739
|
}
|
|
1740
|
+
// The policy resolves here, not in createBlob, because the HTTP-upload path never sees creation
|
|
1741
|
+
// options — every local write funnels through saveBlob. An explicit `compress` option (either
|
|
1742
|
+
// value) and a pre-compressed source both take precedence.
|
|
1743
|
+
if (storageInfo.compress === undefined && !storageInfo.storedCodec) {
|
|
1744
|
+
const size = blob.size ?? storageInfo.contentBuffer?.length;
|
|
1745
|
+
if (resolveBlobCompression(blob.type, size))
|
|
1746
|
+
storageInfo.compress = true;
|
|
1747
|
+
}
|
|
1334
1748
|
generateFilePath(storageInfo);
|
|
1335
1749
|
if (storageInfo.source)
|
|
1336
1750
|
writeBlobWithStream(blob, storageInfo.source, storageInfo);
|
|
@@ -1406,11 +1820,92 @@ function getBlobStreamIdleTimeoutMs(stream) {
|
|
|
1406
1820
|
function shouldDestroyIdleBlobSource(paused, bytesWritten, lastProgressBytes) {
|
|
1407
1821
|
return !(paused && bytesWritten > lastProgressBytes);
|
|
1408
1822
|
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Pass-through for a pre-compressed (stored deflate) blob body that concurrently inflates what it
|
|
1825
|
+
* forwards and fails the pipeline unless the body is a well-formed deflate stream inflating to
|
|
1826
|
+
* exactly `expectedSize` bytes. This is what lets a raw peer-preserved body land under a finalized
|
|
1827
|
+
* DEFLATE header without trusting the peer: a truncated, corrupt, oversized (bomb), or trailing-
|
|
1828
|
+
* garbage body rejects the save instead of being published.
|
|
1829
|
+
*/
|
|
1830
|
+
function createStoredDeflateVerifier(expectedSize, fileId) {
|
|
1831
|
+
const inflater = (0, node_zlib_1.createInflate)();
|
|
1832
|
+
let inflatedLength = 0;
|
|
1833
|
+
let failure;
|
|
1834
|
+
// The in-flight transform() callback, kept so a failure can settle it directly. A mid-write
|
|
1835
|
+
// zlib data error (a corrupt, not merely truncated, body) fires 'error'/'close' but NEVER invokes
|
|
1836
|
+
// the pending `inflater.write` callback, so a transform that waited only on that callback would
|
|
1837
|
+
// stall forever — hanging the save pipeline and leaking the blob write lock. settlePending() is
|
|
1838
|
+
// the escape: whichever of the write callback or the failure runs first clears it; the other is a
|
|
1839
|
+
// no-op, so callback() is still called exactly once per chunk.
|
|
1840
|
+
let pending;
|
|
1841
|
+
const settlePending = () => {
|
|
1842
|
+
if (!pending)
|
|
1843
|
+
return;
|
|
1844
|
+
const { callback, chunk } = pending;
|
|
1845
|
+
pending = undefined;
|
|
1846
|
+
callback(failure, failure ? undefined : chunk);
|
|
1847
|
+
};
|
|
1848
|
+
const fail = (error) => {
|
|
1849
|
+
failure ??= error;
|
|
1850
|
+
inflater.destroy();
|
|
1851
|
+
settlePending();
|
|
1852
|
+
};
|
|
1853
|
+
inflater.on('data', (inflated) => {
|
|
1854
|
+
inflatedLength += inflated.length;
|
|
1855
|
+
if (inflatedLength > expectedSize)
|
|
1856
|
+
fail(new Error(`Pre-compressed blob body for ${fileId} inflates past its declared size of ${expectedSize}`));
|
|
1857
|
+
});
|
|
1858
|
+
inflater.on('error', (error) => fail(new Error(`Pre-compressed blob body for ${fileId} is not a valid deflate stream`, { cause: error })));
|
|
1859
|
+
// Trailing data past the final deflate block is detected best-effort (zlib may acknowledge a write
|
|
1860
|
+
// before emitting the early 'end' it triggers); readers ignore trailing bytes, so the residue is
|
|
1861
|
+
// only wasted disk, while truncation, wrong length, invalid stream, and oversize are deterministic.
|
|
1862
|
+
const trailingData = () => new Error(`Pre-compressed blob body for ${fileId} continues past the end of its deflate stream`);
|
|
1863
|
+
let sawDeflateEnd = false;
|
|
1864
|
+
inflater.on('end', () => {
|
|
1865
|
+
if (!inflater.writableEnded)
|
|
1866
|
+
fail(trailingData());
|
|
1867
|
+
sawDeflateEnd = true;
|
|
1868
|
+
});
|
|
1869
|
+
return new node_stream_1.Transform({
|
|
1870
|
+
transform(chunk, _encoding, callback) {
|
|
1871
|
+
if (!failure && sawDeflateEnd)
|
|
1872
|
+
fail(trailingData());
|
|
1873
|
+
if (failure)
|
|
1874
|
+
return callback(failure);
|
|
1875
|
+
// Forward the chunk only once zlib has consumed it: this is the backpressure coupling and
|
|
1876
|
+
// what keeps the end/overflow/error detections ordered ahead of the next chunk. If zlib
|
|
1877
|
+
// data-errors while consuming this chunk it drops the write callback, so fail()/settlePending()
|
|
1878
|
+
// settles this transform instead (see the `pending` note above).
|
|
1879
|
+
pending = { callback, chunk };
|
|
1880
|
+
inflater.write(chunk, (writeError) => {
|
|
1881
|
+
if (!pending)
|
|
1882
|
+
return; // already settled by a failure; do not call callback twice
|
|
1883
|
+
pending = undefined;
|
|
1884
|
+
const error = failure ?? writeError;
|
|
1885
|
+
callback(error, error ? undefined : chunk);
|
|
1886
|
+
});
|
|
1887
|
+
},
|
|
1888
|
+
flush(callback) {
|
|
1889
|
+
if (failure)
|
|
1890
|
+
return callback(failure);
|
|
1891
|
+
inflater.on('close', () => {
|
|
1892
|
+
if (!failure && inflatedLength !== expectedSize)
|
|
1893
|
+
failure = new Error(`Pre-compressed blob body for ${fileId} inflated to ${inflatedLength} bytes; expected ${expectedSize}`);
|
|
1894
|
+
callback(failure);
|
|
1895
|
+
});
|
|
1896
|
+
inflater.end();
|
|
1897
|
+
},
|
|
1898
|
+
destroy(error, callback) {
|
|
1899
|
+
inflater.destroy();
|
|
1900
|
+
callback(error);
|
|
1901
|
+
},
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1409
1904
|
function writeBlobWithStream(blob, stream, storageInfo, options) {
|
|
1410
1905
|
const repairTargetPath = options?.repairTargetPath;
|
|
1411
1906
|
const repairing = repairTargetPath !== undefined;
|
|
1412
1907
|
const repairTempLockKey = options?.repairTempLockKey;
|
|
1413
|
-
const { filePath, fileId, store, compress, flush } = storageInfo;
|
|
1908
|
+
const { filePath, fileId, store, compress, storedCodec, flush } = storageInfo;
|
|
1414
1909
|
storageInfo.saving = new Promise((resolve, reject) => {
|
|
1415
1910
|
const lockKey = fileId + ':blob';
|
|
1416
1911
|
let lockAcquired = repairing;
|
|
@@ -1458,7 +1953,7 @@ function writeBlobWithStream(blob, stream, storageInfo, options) {
|
|
|
1458
1953
|
let headerValue = BigInt(size);
|
|
1459
1954
|
const header = new Uint8Array(HEADER_SIZE);
|
|
1460
1955
|
const headerView = new DataView(header.buffer);
|
|
1461
|
-
headerValue |= BigInt(compress ? DEFLATE_TYPE : UNCOMPRESSED_TYPE) << 48n;
|
|
1956
|
+
headerValue |= BigInt(compress || storedCodec ? exports.DEFLATE_TYPE : UNCOMPRESSED_TYPE) << 48n;
|
|
1462
1957
|
headerView.setBigInt64(0, headerValue);
|
|
1463
1958
|
return header;
|
|
1464
1959
|
}
|
|
@@ -1499,8 +1994,8 @@ function writeBlobWithStream(blob, stream, storageInfo, options) {
|
|
|
1499
1994
|
// half-replicated blob returns 503 (retry) instead of 500 (confidently incomplete → the peer
|
|
1500
1995
|
// advances its resume cursor past it = silent loss, harper-pro#481). Hold the write lock until the
|
|
1501
1996
|
// marker is durable so no concurrent read/send observes the bare partial file (lock-free + short =
|
|
1502
|
-
// classified 500 = the very loss this prevents). The re-stream
|
|
1503
|
-
//
|
|
1997
|
+
// classified 500 = the very loss this prevents). The re-stream builds a fresh blob and so takes a
|
|
1998
|
+
// new file id, leaving this stub for orphan GC; a terminal give-up unlinks it (→ 404). Build
|
|
1504
1999
|
// the header directly rather than via createHeader so its compress-type OR can't collide with the
|
|
1505
2000
|
// PENDING type bits.
|
|
1506
2001
|
// Bounded so a `writeFile` that never calls back cannot leave `saving` un-settled for the
|
|
@@ -1606,6 +2101,20 @@ function writeBlobWithStream(blob, stream, storageInfo, options) {
|
|
|
1606
2101
|
void replaceTarget();
|
|
1607
2102
|
return;
|
|
1608
2103
|
}
|
|
2104
|
+
if (compressedStream && blob.size !== compressedStream.bytesWritten) {
|
|
2105
|
+
// A known-size compressed write stamped the declared size into the header up front, but
|
|
2106
|
+
// the source ended with a different content length. Unlike the uncompressed path — whose
|
|
2107
|
+
// short body reads as retryable-incomplete (503) — this file would inflate to the wrong
|
|
2108
|
+
// size and every reader would reject it as permanently corrupt (500). Fail the save so the
|
|
2109
|
+
// record never commits a reference to an unreadable file (matches the storedCodec verifier
|
|
2110
|
+
// and the repair size check); the unreferenced file is left for orphan GC. The unknown-size
|
|
2111
|
+
// path syncs blob.size to bytesWritten before reaching here, so it never trips this.
|
|
2112
|
+
store.unlock(lockKey);
|
|
2113
|
+
reject(new Error(`Blob ${fileId} deflated ${compressedStream.bytesWritten} bytes but its header declares ${blob.size}`));
|
|
2114
|
+
(0, node_fs_1.close)(fd);
|
|
2115
|
+
writeStream.fd = null;
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
1609
2118
|
store.unlock(lockKey);
|
|
1610
2119
|
if (flush) {
|
|
1611
2120
|
// we just use fdatasync because we really aren't that concerned with flushing file metadata
|
|
@@ -1625,6 +2134,12 @@ function writeBlobWithStream(blob, stream, storageInfo, options) {
|
|
|
1625
2134
|
}
|
|
1626
2135
|
}
|
|
1627
2136
|
try {
|
|
2137
|
+
if (storedCodec && blob.size === undefined) {
|
|
2138
|
+
// The header must carry the uncompressed length, and it cannot be derived from a raw
|
|
2139
|
+
// compressed source (bytesWritten would be the compressed count). Refuse before any file
|
|
2140
|
+
// exists at the path so the failure reads as a cleanly absent blob, not a torn one.
|
|
2141
|
+
throw new Error(`Cannot store a pre-compressed blob without a known uncompressed size (fileId=${fileId})`);
|
|
2142
|
+
}
|
|
1628
2143
|
if (!lockAcquired) {
|
|
1629
2144
|
if (!store.tryLock(lockKey))
|
|
1630
2145
|
throw new Error(`Unable to get lock for blob file ${fileId}`);
|
|
@@ -1654,7 +2169,14 @@ function writeBlobWithStream(blob, stream, storageInfo, options) {
|
|
|
1654
2169
|
stream.on('resume', armIdleTimer);
|
|
1655
2170
|
armIdleTimer();
|
|
1656
2171
|
}
|
|
1657
|
-
if (
|
|
2172
|
+
if (storedCodec) {
|
|
2173
|
+
// The source bytes are already the stored deflate representation (a peer preserving its
|
|
2174
|
+
// codec). Verify — not just copy — so a truncated or forged body can never land under a
|
|
2175
|
+
// finalized DEFLATE header: the verifier inflates the pass-through bytes concurrently and
|
|
2176
|
+
// fails the pipeline unless they inflate to exactly the declared uncompressed size.
|
|
2177
|
+
(0, node_stream_1.pipeline)(stream, createStoredDeflateVerifier(blob.size, fileId), writeStream, finished);
|
|
2178
|
+
}
|
|
2179
|
+
else if (compress) {
|
|
1658
2180
|
if (!wroteSize)
|
|
1659
2181
|
writeStream.write(COMPRESS_HEADER);
|
|
1660
2182
|
compressedStream = (0, node_zlib_1.createDeflate)();
|
|
@@ -1701,9 +2223,11 @@ function getFilePathForBlob(blob) {
|
|
|
1701
2223
|
* Repairs a damaged file-backed blob under its existing fileId. The damage check and atomic file
|
|
1702
2224
|
* replacement share the blob lock, so a failed repair never modifies the referenced file. Callers
|
|
1703
2225
|
* must establish an exact record identity tie (same version and source node) and positional blob
|
|
1704
|
-
* pairing before calling
|
|
1705
|
-
*
|
|
1706
|
-
*
|
|
2226
|
+
* pairing before calling, and classify the file with `blobFileMissingOrIncompleteAsync` first — a
|
|
2227
|
+
* compressed body's damage is only provable by inflating it, which the locked recheck here will
|
|
2228
|
+
* not do. The received byte count must match the reported source size and, when present, the
|
|
2229
|
+
* stored descriptor. On a synchronous `undefined`, no repair started and `source` remains owned
|
|
2230
|
+
* by the caller.
|
|
1707
2231
|
*/
|
|
1708
2232
|
function repairBlobFile(blob, source, sourceSize = blob.size) {
|
|
1709
2233
|
let blobLockKey;
|
|
@@ -1765,7 +2289,9 @@ function repairBlobFile(blob, source, sourceSize = blob.size) {
|
|
|
1765
2289
|
filePath: repairFilePath,
|
|
1766
2290
|
saving: undefined,
|
|
1767
2291
|
flush: true,
|
|
2292
|
+
// the repair source is inflated content; its size verification counts uncompressed bytes
|
|
1768
2293
|
compress: false,
|
|
2294
|
+
storedCodec: undefined,
|
|
1769
2295
|
};
|
|
1770
2296
|
writeBlobWithStream(blob, source, repairStorageInfo, {
|
|
1771
2297
|
expectedSize,
|
|
@@ -1792,11 +2318,84 @@ function repairBlobFile(blob, source, sourceSize = blob.size) {
|
|
|
1792
2318
|
return undefined;
|
|
1793
2319
|
}
|
|
1794
2320
|
}
|
|
2321
|
+
/** Fail-closed storage and descriptor errors that must abort the capture instead of producing an unverified backup. */
|
|
2322
|
+
const SYSTEMIC_IO_ERRORS = new Set(['EMFILE', 'ENFILE', 'ENOSPC', 'EIO', 'EROFS']);
|
|
2323
|
+
function isSystemicIoError(error) {
|
|
2324
|
+
return SYSTEMIC_IO_ERRORS.has(error?.code ?? '');
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* How a consumer that captures a blob root (backup snapshot, backup archive) should treat one file.
|
|
2328
|
+
* `skip` is not a blob to capture at all; `pending` and `gone` require markers; `capture` is taken as-is.
|
|
2329
|
+
* `capture` means settled as far as the path can show, which is short of a guarantee: a known-size write
|
|
2330
|
+
* that has landed every byte is indistinguishable here from a finished one, and if it then aborts, the
|
|
2331
|
+
* PENDING stamp rewrites that inode in place, truncating any same-filesystem hard link taken from it.
|
|
2332
|
+
* Telling the two apart needs the blob write lock, which is keyed by file id and unreachable from a walk.
|
|
2333
|
+
*/
|
|
2334
|
+
async function classifyBlobFileForCapture(filePath) {
|
|
2335
|
+
if (filePath.endsWith(BLOB_REPAIR_SUFFIX))
|
|
2336
|
+
return 'skip';
|
|
2337
|
+
let header;
|
|
2338
|
+
let fileSize;
|
|
2339
|
+
let handle;
|
|
2340
|
+
try {
|
|
2341
|
+
handle = await (0, promises_1.open)(filePath, 'r');
|
|
2342
|
+
}
|
|
2343
|
+
catch (error) {
|
|
2344
|
+
// Reclamation can unlink a superseded blob after the engine checkpoint but before this walk. The
|
|
2345
|
+
// checkpointed record may still reference it, so reserve the id. Treat local absence as terminal
|
|
2346
|
+
// rather than making every read wait for repair; this gives up peer repair of that checkpointed version.
|
|
2347
|
+
if (error.code === 'ENOENT')
|
|
2348
|
+
return 'gone';
|
|
2349
|
+
throw error;
|
|
2350
|
+
}
|
|
2351
|
+
try {
|
|
2352
|
+
fileSize = (await handle.stat()).size;
|
|
2353
|
+
header = Buffer.alloc(HEADER_SIZE);
|
|
2354
|
+
const { bytesRead } = await handle.read(header, 0, HEADER_SIZE, 0);
|
|
2355
|
+
header = header.subarray(0, bytesRead);
|
|
2356
|
+
}
|
|
2357
|
+
finally {
|
|
2358
|
+
await handle.close();
|
|
2359
|
+
}
|
|
2360
|
+
if (blobHeaderIsAbortMarker(header))
|
|
2361
|
+
return 'capture';
|
|
2362
|
+
if (blobHeaderIndicatesIncomplete(header, fileSize))
|
|
2363
|
+
return 'pending';
|
|
2364
|
+
// A deflate header records the *uncompressed* length, so the check above compares lengths only for
|
|
2365
|
+
// UNCOMPRESSED_TYPE and a short compressed body reaches here looking whole. Both producers of one
|
|
2366
|
+
// need this: saveBlob stamps a known size before the first compressed byte, so a live write is
|
|
2367
|
+
// invisible above and caught only here, and an unclean shutdown can leave a torn body the
|
|
2368
|
+
// asynchronous repair sweep has not reached yet. Compression is opt-in via storage.blobs.compression
|
|
2369
|
+
// (off by default): on a corpus with it disabled this branch is never taken, but where an operator
|
|
2370
|
+
// enables it each completeness check fully inflates the compressed body it reaches.
|
|
2371
|
+
if (header[1] !== exports.DEFLATE_TYPE)
|
|
2372
|
+
return 'capture';
|
|
2373
|
+
const uncompressedSize = Number(new DataView(header.buffer, header.byteOffset, HEADER_SIZE).getBigUint64(0) & 0xffffffffffffn);
|
|
2374
|
+
return (await inflatesToExactly(filePath, uncompressedSize)) ? 'capture' : 'pending';
|
|
2375
|
+
}
|
|
2376
|
+
/**
|
|
2377
|
+
* Whether the header is a deliberate abort marker rather than content. Written once and never
|
|
2378
|
+
* rewritten, so a consumer sharing blob inodes can keep them: dropping a PENDING marker downgrades a
|
|
2379
|
+
* retryable 503 to a 404 the replication layer reads as "cleanly gone" (harper-pro#481).
|
|
2380
|
+
*/
|
|
2381
|
+
function blobHeaderIsAbortMarker(header) {
|
|
2382
|
+
if (header.length < HEADER_SIZE)
|
|
2383
|
+
return false;
|
|
2384
|
+
const type = header.readUInt16BE(0);
|
|
2385
|
+
return type === PENDING_TYPE || type === ERROR_TYPE;
|
|
2386
|
+
}
|
|
2387
|
+
/** The stand-in bytes for a blob that could not be captured whole. */
|
|
2388
|
+
function createCaptureMarker(disposition, message) {
|
|
2389
|
+
const messageBuffer = Buffer.from(message);
|
|
2390
|
+
const header = Buffer.alloc(HEADER_SIZE);
|
|
2391
|
+
new DataView(header.buffer, header.byteOffset, HEADER_SIZE).setBigInt64(0, BigInt(messageBuffer.length) | (BigInt(disposition === 'gone' ? ERROR_TYPE : PENDING_TYPE) << 48n));
|
|
2392
|
+
return Buffer.concat([header, messageBuffer]);
|
|
2393
|
+
}
|
|
1795
2394
|
function blobHeaderIndicatesIncomplete(header, fileSize) {
|
|
1796
2395
|
if (fileSize < HEADER_SIZE || header.length < HEADER_SIZE)
|
|
1797
2396
|
return true;
|
|
1798
2397
|
const type = header.readUInt16BE(0);
|
|
1799
|
-
if (type !== UNCOMPRESSED_TYPE && type !== DEFLATE_TYPE)
|
|
2398
|
+
if (type !== UNCOMPRESSED_TYPE && type !== exports.DEFLATE_TYPE)
|
|
1800
2399
|
return true;
|
|
1801
2400
|
const storedSize = header.readUIntBE(2, 6);
|
|
1802
2401
|
if (storedSize === UNKNOWN_SIZE)
|
|
@@ -1804,10 +2403,83 @@ function blobHeaderIndicatesIncomplete(header, fileSize) {
|
|
|
1804
2403
|
return type === UNCOMPRESSED_TYPE && fileSize !== HEADER_SIZE + storedSize;
|
|
1805
2404
|
}
|
|
1806
2405
|
/**
|
|
1807
|
-
*
|
|
1808
|
-
*
|
|
1809
|
-
*
|
|
1810
|
-
*
|
|
2406
|
+
* What a blob file's header and length alone say about its completeness: `true` when the file is
|
|
2407
|
+
* missing, short, unfinished, an error/pending stub, or disagrees with the record descriptor; `false`
|
|
2408
|
+
* when an uncompressed body is whole; `null` for a deflate body, whose header records the uncompressed
|
|
2409
|
+
* length and so proves nothing about the body — only inflating it can. `undefined` when the question
|
|
2410
|
+
* does not apply or the file could not be inspected.
|
|
2411
|
+
*/
|
|
2412
|
+
function classifyBlobFileByHeader(blob, fileSize, header, headerBytes) {
|
|
2413
|
+
if (fileSize < HEADER_SIZE || headerBytes < HEADER_SIZE)
|
|
2414
|
+
return true;
|
|
2415
|
+
if (blobHeaderIndicatesIncomplete(header, fileSize))
|
|
2416
|
+
return true;
|
|
2417
|
+
const descriptorSize = blob.size;
|
|
2418
|
+
if (descriptorSize !== undefined && header.readUIntBE(2, 6) !== descriptorSize)
|
|
2419
|
+
return true;
|
|
2420
|
+
return header.readUInt16BE(0) === exports.DEFLATE_TYPE ? null : false;
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Whether a file-backed blob's backing file is missing or incomplete on disk — the unlocked,
|
|
2424
|
+
* non-blocking classification a copy-delivery repair candidate gets first (harper-pro#699). A deflate
|
|
2425
|
+
* body is inflated (streamed, off the event loop) to decide; the file it judged damaged is remembered
|
|
2426
|
+
* so the locked recheck in `repairBlobFile` can recognize it without inflating again. Returns
|
|
2427
|
+
* undefined for blobs the question does not apply to, or that could not be inspected.
|
|
2428
|
+
*/
|
|
2429
|
+
async function blobFileMissingOrIncompleteAsync(blob) {
|
|
2430
|
+
if (!(blob instanceof FileBackedBlob))
|
|
2431
|
+
return undefined;
|
|
2432
|
+
const storageInfo = storageInfoForBlob.get(blob);
|
|
2433
|
+
if (!storageInfo?.fileId)
|
|
2434
|
+
return undefined;
|
|
2435
|
+
storageInfo.probedDamage = undefined; // every probe starts over; no early exit may leave a stale verdict behind
|
|
2436
|
+
const filePath = getFilePath(storageInfo);
|
|
2437
|
+
let file;
|
|
2438
|
+
try {
|
|
2439
|
+
file = await (0, promises_1.open)(filePath, 'r');
|
|
2440
|
+
}
|
|
2441
|
+
catch (error) {
|
|
2442
|
+
if (error?.code === 'ENOENT')
|
|
2443
|
+
return true;
|
|
2444
|
+
logger_ts_1.logger.debug?.('Unable to open blob file for in-place repair inspection', error);
|
|
2445
|
+
return undefined;
|
|
2446
|
+
}
|
|
2447
|
+
let fileSize;
|
|
2448
|
+
const header = Buffer.allocUnsafe(HEADER_SIZE);
|
|
2449
|
+
let verdict;
|
|
2450
|
+
try {
|
|
2451
|
+
fileSize = (await file.stat()).size;
|
|
2452
|
+
const { bytesRead } = await file.read(header, 0, HEADER_SIZE, 0);
|
|
2453
|
+
verdict = classifyBlobFileByHeader(blob, fileSize, header, bytesRead);
|
|
2454
|
+
}
|
|
2455
|
+
catch (error) {
|
|
2456
|
+
logger_ts_1.logger.debug?.('Unable to inspect blob file for in-place repair', error);
|
|
2457
|
+
return undefined;
|
|
2458
|
+
}
|
|
2459
|
+
finally {
|
|
2460
|
+
await file.close().catch(() => { });
|
|
2461
|
+
}
|
|
2462
|
+
if (verdict !== null)
|
|
2463
|
+
return verdict;
|
|
2464
|
+
try {
|
|
2465
|
+
verdict = !(await inflatesToExactly(filePath, header.readUIntBE(2, 6)));
|
|
2466
|
+
}
|
|
2467
|
+
catch (error) {
|
|
2468
|
+
logger_ts_1.logger.debug?.('Unable to inspect blob file for in-place repair', error);
|
|
2469
|
+
return undefined;
|
|
2470
|
+
}
|
|
2471
|
+
if (verdict)
|
|
2472
|
+
storageInfo.probedDamage = { fileSize, header };
|
|
2473
|
+
return verdict;
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* The locked, synchronous recheck of `blobFileMissingOrIncompleteAsync`'s verdict, run by
|
|
2477
|
+
* `repairBlobFile` under the blob's write lock so a repair can only replace a file no writer is
|
|
2478
|
+
* still producing. It repeats the header classification, and for a deflate body — which it cannot
|
|
2479
|
+
* afford to inflate on the event loop — answers `true` only while the file is still the one the
|
|
2480
|
+
* probe found damaged: a body can only grow (writers append; a repair replaces the file) so an
|
|
2481
|
+
* unchanged length and header mean unchanged bytes, and anything else is `undefined` (not provably
|
|
2482
|
+
* damaged — decline, and let the next probe or the repair sweep classify it).
|
|
1811
2483
|
*/
|
|
1812
2484
|
function blobFileMissingOrIncomplete(blob) {
|
|
1813
2485
|
try {
|
|
@@ -1828,16 +2500,13 @@ function blobFileMissingOrIncomplete(blob) {
|
|
|
1828
2500
|
return undefined;
|
|
1829
2501
|
}
|
|
1830
2502
|
try {
|
|
1831
|
-
const
|
|
1832
|
-
if (size < HEADER_SIZE)
|
|
1833
|
-
return true;
|
|
2503
|
+
const fileSize = (0, node_fs_1.fstatSync)(fd).size;
|
|
1834
2504
|
const header = Buffer.allocUnsafe(HEADER_SIZE);
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
return descriptorSize !== undefined && header.readUIntBE(2, 6) !== descriptorSize;
|
|
2505
|
+
const verdict = classifyBlobFileByHeader(blob, fileSize, header, (0, node_fs_1.readSync)(fd, header, 0, HEADER_SIZE, 0));
|
|
2506
|
+
if (verdict !== null)
|
|
2507
|
+
return verdict;
|
|
2508
|
+
const probed = storageInfo.probedDamage;
|
|
2509
|
+
return probed?.fileSize === fileSize && probed.header.equals(header) ? true : undefined;
|
|
1841
2510
|
}
|
|
1842
2511
|
finally {
|
|
1843
2512
|
(0, node_fs_1.closeSync)(fd);
|
|
@@ -2349,6 +3018,10 @@ function cleanupUnusedBlobs(blobs, retainedFileIds) {
|
|
|
2349
3018
|
continue; // no file written, nothing to clean up
|
|
2350
3019
|
if (retainedFileIds?.has(storageInfo.fileId))
|
|
2351
3020
|
continue; // the committed record still references this blob
|
|
3021
|
+
// Tombstone the instance as soon as the deletion is DECIDED, not when the unlink is issued: the
|
|
3022
|
+
// unlink waits for an in-flight save to settle, and a re-store in that window would otherwise
|
|
3023
|
+
// mint a reference to a file that is already condemned (issue #2062).
|
|
3024
|
+
discardStorage(storageInfo);
|
|
2352
3025
|
const settle = storageInfo.saving ?? Promise.resolve();
|
|
2353
3026
|
settle.then(() => deleteBlob(blob), () => deleteBlob(blob) // even on save failure, attempt cleanup in case a partial file remains
|
|
2354
3027
|
);
|
|
@@ -2402,7 +3075,7 @@ const copyingUnpacker = new msgpackr_1.Packr({ copyBuffers: true, mapsAsObjects:
|
|
|
2402
3075
|
storageInfoForBlob.set(blob, {
|
|
2403
3076
|
storageIndex: 0,
|
|
2404
3077
|
fileId: null,
|
|
2405
|
-
contentBuffer: blobInfo[1],
|
|
3078
|
+
contentBuffer: asBuffer(blobInfo[1]),
|
|
2406
3079
|
});
|
|
2407
3080
|
blob.size = blobInfo[1]?.length;
|
|
2408
3081
|
}
|
|
@@ -2436,6 +3109,12 @@ const copyingUnpacker = new msgpackr_1.Packr({ copyBuffers: true, mapsAsObjects:
|
|
|
2436
3109
|
throw new Error('Unable to save blob without file id');
|
|
2437
3110
|
}
|
|
2438
3111
|
storageInfo.recordId = encodeForStorageForRecordId;
|
|
3112
|
+
// Per-node hint only — a replication sender uses it to skip the header sniff for the
|
|
3113
|
+
// uncompressed majority, but always confirms against the local file header before sending
|
|
3114
|
+
// raw: a relayed record (or an in-place repair) can outlive the storage form recorded here.
|
|
3115
|
+
// Namespaced away from `codec`, which a media blob plausibly carries as its own property.
|
|
3116
|
+
if (storageInfo.compress || storageInfo.storedCodec)
|
|
3117
|
+
options.storedCodec = 'deflate';
|
|
2439
3118
|
// A record version being written now references this file, so any reclamation queued by an
|
|
2440
3119
|
// earlier supersession is void — the retain-on-update check in RecordEncoder only covers the
|
|
2441
3120
|
// write that supersedes, not a file already awaiting reclamation from a previous one.
|
|
@@ -2548,7 +3227,7 @@ function polyfillBlob() {
|
|
|
2548
3227
|
* @param database
|
|
2549
3228
|
*/
|
|
2550
3229
|
async function cleanupOrphans(database, databaseName) {
|
|
2551
|
-
const { HAS_BLOBS } = await import("./auditStore.js");
|
|
3230
|
+
const { HAS_BLOBS, isAuditEntryWrite } = await import("./auditStore.js");
|
|
2552
3231
|
let store;
|
|
2553
3232
|
let auditStore;
|
|
2554
3233
|
let orphansDeleted = 0;
|
|
@@ -2632,7 +3311,10 @@ async function cleanupOrphans(database, databaseName) {
|
|
|
2632
3311
|
if (!primaryStore)
|
|
2633
3312
|
continue;
|
|
2634
3313
|
const entry = primaryStore?.getEntry(auditRecord.recordId);
|
|
2635
|
-
|
|
3314
|
+
// Only the write this audit record describes had its blobs scanned by the table loop above.
|
|
3315
|
+
// Identity is the log key, not the version: a version match can name a different write and
|
|
3316
|
+
// would skip an audit value whose blobs are still referenced.
|
|
3317
|
+
if (!entry?.value || !isAuditEntryWrite(entry, auditRecord)) {
|
|
2636
3318
|
checkObjectForReferences(auditRecord.getValue(primaryStore));
|
|
2637
3319
|
}
|
|
2638
3320
|
// slow this down a bit to reduce excessive load, this runs approximately at 10k per second
|
|
@@ -2697,10 +3379,11 @@ async function cleanupOrphans(database, databaseName) {
|
|
|
2697
3379
|
}
|
|
2698
3380
|
}
|
|
2699
3381
|
async function isBlobFileComplete(storageInfo) {
|
|
2700
|
-
|
|
3382
|
+
return isBlobFileCompleteAtPath(getFilePath(storageInfo));
|
|
3383
|
+
}
|
|
3384
|
+
async function isBlobFileCompleteAtPath(filePath) {
|
|
2701
3385
|
let fileSize;
|
|
2702
3386
|
try {
|
|
2703
|
-
filePath = getFilePath(storageInfo);
|
|
2704
3387
|
fileSize = (0, node_fs_1.statSync)(filePath).size;
|
|
2705
3388
|
}
|
|
2706
3389
|
catch (e) {
|
|
@@ -2726,32 +3409,56 @@ async function isBlobFileComplete(storageInfo) {
|
|
|
2726
3409
|
// uncompressed byte count). For an uncompressed blob that equals the on-disk body length;
|
|
2727
3410
|
// for a compressed blob it does not, so the body length can't be compared to it directly.
|
|
2728
3411
|
const size = Number(headerValue & 0xffffffffffffn);
|
|
2729
|
-
if (header[1] === DEFLATE_TYPE) {
|
|
2730
|
-
//
|
|
2731
|
-
//
|
|
2732
|
-
//
|
|
2733
|
-
|
|
2734
|
-
// the whole buffer) keeps memory bounded during the repair sweep, which may touch many large
|
|
2735
|
-
// blobs.
|
|
2736
|
-
return new Promise((resolve) => {
|
|
2737
|
-
let inflatedLength = 0;
|
|
2738
|
-
const source = (0, node_fs_1.createReadStream)(filePath, { start: HEADER_SIZE });
|
|
2739
|
-
const inflate = (0, node_zlib_1.createInflate)();
|
|
2740
|
-
const fail = () => {
|
|
2741
|
-
source.destroy();
|
|
2742
|
-
resolve(false);
|
|
2743
|
-
};
|
|
2744
|
-
source.on('error', fail);
|
|
2745
|
-
inflate.on('error', fail);
|
|
2746
|
-
inflate.on('data', (chunk) => {
|
|
2747
|
-
inflatedLength += chunk.length;
|
|
2748
|
-
});
|
|
2749
|
-
inflate.on('end', () => resolve(inflatedLength === size));
|
|
2750
|
-
source.pipe(inflate);
|
|
2751
|
-
});
|
|
3412
|
+
if (header[1] === exports.DEFLATE_TYPE) {
|
|
3413
|
+
// This function's contract is to resolve true/false; harper-pro's repair sweep awaits it without a
|
|
3414
|
+
// catch (replication/blobRepair.ts), so an I/O fault must not become a rejection here. The capture
|
|
3415
|
+
// classifier calls inflatesToExactly directly, where the distinction does matter.
|
|
3416
|
+
return inflatesToExactly(filePath, size).catch(() => false);
|
|
2752
3417
|
}
|
|
2753
3418
|
return true;
|
|
2754
3419
|
}
|
|
3420
|
+
/**
|
|
3421
|
+
* Whether the deflate body after the header inflates to exactly `size` bytes. A compressed blob's
|
|
3422
|
+
* header records the uncompressed length, so it cannot be compared to the on-disk body; a truncated
|
|
3423
|
+
* stream errors (Z_BUF_ERROR) or yields fewer bytes. Streamed rather than inflateSync so memory stays
|
|
3424
|
+
* bounded over a sweep that may touch many large blobs, and abandoned as soon as the output passes
|
|
3425
|
+
* `size` so a body that lies about its length costs no more than the length it claimed.
|
|
3426
|
+
*/
|
|
3427
|
+
function inflatesToExactly(filePath, size) {
|
|
3428
|
+
return new Promise((resolve, reject) => {
|
|
3429
|
+
let inflatedLength = 0;
|
|
3430
|
+
const source = (0, node_fs_1.createReadStream)(filePath, { start: HEADER_SIZE });
|
|
3431
|
+
const inflate = (0, node_zlib_1.createInflate)();
|
|
3432
|
+
// Both: pipe() does not tear down the destination when the source errors, so the inflate's
|
|
3433
|
+
// native zlib handle would sit allocated until GC — once per blob, on every backup walk.
|
|
3434
|
+
const stop = () => {
|
|
3435
|
+
source.destroy();
|
|
3436
|
+
inflate.destroy();
|
|
3437
|
+
};
|
|
3438
|
+
// A zlib error is the answer (body truncated or corrupt); an I/O error is a failure to answer and
|
|
3439
|
+
// must propagate, or a systemic fault would classify a corpus of complete blobs as incomplete.
|
|
3440
|
+
// The reject arm is unverified: it needs a read fault raised mid-inflate on a file that opened
|
|
3441
|
+
// cleanly, which no test here can produce.
|
|
3442
|
+
const fail = (error) => {
|
|
3443
|
+
stop();
|
|
3444
|
+
if (error?.code?.startsWith('Z_'))
|
|
3445
|
+
resolve(false);
|
|
3446
|
+
else
|
|
3447
|
+
reject(error);
|
|
3448
|
+
};
|
|
3449
|
+
source.on('error', fail);
|
|
3450
|
+
inflate.on('error', fail);
|
|
3451
|
+
inflate.on('data', (chunk) => {
|
|
3452
|
+
inflatedLength += chunk.length;
|
|
3453
|
+
if (inflatedLength > size) {
|
|
3454
|
+
stop();
|
|
3455
|
+
resolve(false);
|
|
3456
|
+
}
|
|
3457
|
+
});
|
|
3458
|
+
inflate.on('end', () => resolve(inflatedLength === size));
|
|
3459
|
+
source.pipe(inflate);
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
2755
3462
|
/**
|
|
2756
3463
|
* Resolves true if the given blob's backing file is present and complete: no error stub, not an
|
|
2757
3464
|
* in-flight placeholder, and the content matches the header (uncompressed blobs by body length,
|
|
@@ -2766,6 +3473,177 @@ async function isBlobComplete(blob) {
|
|
|
2766
3473
|
return false;
|
|
2767
3474
|
return isBlobFileComplete(storageInfo);
|
|
2768
3475
|
}
|
|
3476
|
+
/**
|
|
3477
|
+
* Open a blob's STORED body for transfer, without inflating it — the sender half of replication's
|
|
3478
|
+
* codec preservation. Returns undefined whenever the stored form is not a settled deflate file
|
|
3479
|
+
* (uncompressed, sliced, still being written, missing, or already condemned); the caller then falls
|
|
3480
|
+
* back to the ordinary content stream, which knows how to wait and how to report errors.
|
|
3481
|
+
*
|
|
3482
|
+
* Deliberately synchronous through the decision: the caller decides raw-vs-inflated inside the same
|
|
3483
|
+
* synchronous block that puts the owning record frame on the wire. The stream opens its own
|
|
3484
|
+
* descriptor later and re-reads the header first: a settled file's header never changes except by
|
|
3485
|
+
* a repair publishing a fresh (uncompressed) file over the path, so a header that differs from the
|
|
3486
|
+
* sniffed one means the announced form is gone — reported as transient (503), since a retry
|
|
3487
|
+
* re-decides against the new file. The stream verifies as it sends — a concurrent inflate must
|
|
3488
|
+
* land on exactly `size` bytes — so a torn body that survived the header checks becomes a loud,
|
|
3489
|
+
* permanently-classified error instead of replicated garbage.
|
|
3490
|
+
*/
|
|
3491
|
+
function openStoredBlobBody(blob) {
|
|
3492
|
+
if (!(blob instanceof FileBackedBlob))
|
|
3493
|
+
return undefined;
|
|
3494
|
+
const storageInfo = storageInfoForBlob.get(blob);
|
|
3495
|
+
if (!storageInfo?.fileId || !storageInfo.store)
|
|
3496
|
+
return undefined;
|
|
3497
|
+
if (storageInfo.start !== undefined || storageInfo.end !== undefined)
|
|
3498
|
+
return undefined;
|
|
3499
|
+
if (storageInfo.fileState?.discarded)
|
|
3500
|
+
return undefined;
|
|
3501
|
+
const releaseHold = holdBlobFile(blob);
|
|
3502
|
+
if (!releaseHold)
|
|
3503
|
+
return undefined; // already being reclaimed
|
|
3504
|
+
let fd;
|
|
3505
|
+
let filePath;
|
|
3506
|
+
const decline = () => {
|
|
3507
|
+
if (fd !== undefined) {
|
|
3508
|
+
try {
|
|
3509
|
+
(0, node_fs_1.closeSync)(fd);
|
|
3510
|
+
}
|
|
3511
|
+
catch (error) {
|
|
3512
|
+
logger_ts_1.logger.debug?.('Error closing stored blob body descriptor', error);
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
releaseHold();
|
|
3516
|
+
return undefined;
|
|
3517
|
+
};
|
|
3518
|
+
let size;
|
|
3519
|
+
const header = Buffer.allocUnsafe(HEADER_SIZE);
|
|
3520
|
+
try {
|
|
3521
|
+
const lockKey = storageInfo.fileId + ':blob';
|
|
3522
|
+
// A held write lock means the body may still be streaming to disk; the normal read path knows
|
|
3523
|
+
// how to wait for it, so decline rather than duplicate that machinery here.
|
|
3524
|
+
if (!storageInfo.store.tryLock(lockKey))
|
|
3525
|
+
return decline();
|
|
3526
|
+
storageInfo.store.unlock(lockKey);
|
|
3527
|
+
filePath = getFilePath(storageInfo);
|
|
3528
|
+
fd = (0, node_fs_1.openSync)(filePath, 'r');
|
|
3529
|
+
if ((0, node_fs_1.readSync)(fd, header, 0, HEADER_SIZE, 0) < HEADER_SIZE)
|
|
3530
|
+
return decline();
|
|
3531
|
+
const headerValue = new DataView(header.buffer, header.byteOffset, HEADER_SIZE).getBigUint64(0);
|
|
3532
|
+
if (Number(headerValue >> 48n) !== exports.DEFLATE_TYPE)
|
|
3533
|
+
return decline();
|
|
3534
|
+
size = Number(headerValue & 0xffffffffffffn);
|
|
3535
|
+
if (size === UNKNOWN_SIZE)
|
|
3536
|
+
return decline();
|
|
3537
|
+
const descriptorSize = blob.size;
|
|
3538
|
+
if (descriptorSize !== undefined && descriptorSize < UNKNOWN_SIZE && descriptorSize !== size)
|
|
3539
|
+
return decline();
|
|
3540
|
+
(0, node_fs_1.closeSync)(fd);
|
|
3541
|
+
fd = undefined;
|
|
3542
|
+
}
|
|
3543
|
+
catch (error) {
|
|
3544
|
+
logger_ts_1.logger.debug?.('Unable to open stored blob body', storageInfo.fileId, error);
|
|
3545
|
+
return decline();
|
|
3546
|
+
}
|
|
3547
|
+
let holdReleased = false;
|
|
3548
|
+
const releaseHoldOnce = () => {
|
|
3549
|
+
if (holdReleased)
|
|
3550
|
+
return;
|
|
3551
|
+
holdReleased = true;
|
|
3552
|
+
releaseHold();
|
|
3553
|
+
};
|
|
3554
|
+
let streamStarted = false;
|
|
3555
|
+
const failVerification = (detail) => new BlobReadError(`Stored blob body for ${filePath} ${detail}`, BLOB_CORRUPT_STATUS);
|
|
3556
|
+
return {
|
|
3557
|
+
codec: 'deflate',
|
|
3558
|
+
size,
|
|
3559
|
+
close: () => {
|
|
3560
|
+
streamStarted = true;
|
|
3561
|
+
releaseHoldOnce();
|
|
3562
|
+
},
|
|
3563
|
+
stream: async function* () {
|
|
3564
|
+
if (streamStarted)
|
|
3565
|
+
throw new Error(`Stored blob body for ${filePath} was already consumed or closed`);
|
|
3566
|
+
streamStarted = true;
|
|
3567
|
+
let streamFd;
|
|
3568
|
+
let source;
|
|
3569
|
+
try {
|
|
3570
|
+
streamFd = (0, node_fs_1.openSync)(filePath, 'r');
|
|
3571
|
+
const currentHeader = Buffer.allocUnsafe(HEADER_SIZE);
|
|
3572
|
+
const headerBytes = (0, node_fs_1.readSync)(streamFd, currentHeader, 0, HEADER_SIZE, 0);
|
|
3573
|
+
if (headerBytes < HEADER_SIZE || !currentHeader.equals(header)) {
|
|
3574
|
+
throw new BlobReadError(`Stored blob body for ${filePath} was replaced before it was streamed`, exports.BLOB_UNAVAILABLE_STATUS);
|
|
3575
|
+
}
|
|
3576
|
+
// the stream owns the descriptor from here (an fs stream's worker-thread close(2) races any other user)
|
|
3577
|
+
source = (0, node_fs_1.createReadStream)(filePath, { fd: streamFd, start: HEADER_SIZE, autoClose: true });
|
|
3578
|
+
}
|
|
3579
|
+
catch (error) {
|
|
3580
|
+
if (streamFd !== undefined)
|
|
3581
|
+
(0, node_fs_1.closeSync)(streamFd);
|
|
3582
|
+
releaseHoldOnce();
|
|
3583
|
+
throw error;
|
|
3584
|
+
}
|
|
3585
|
+
const verifier = (0, node_zlib_1.createInflate)();
|
|
3586
|
+
let inflated = 0;
|
|
3587
|
+
let verifyFailure;
|
|
3588
|
+
// A mid-write zlib data error (a corrupt, not merely truncated, body) fires 'error'/'close'
|
|
3589
|
+
// but drops the pending `verifier.write` callback, so the per-chunk await below would hang
|
|
3590
|
+
// forever — suspending this generator inside the try and leaking the descriptor and the file
|
|
3591
|
+
// hold its finally releases. settleWrite() unblocks it from error/close too; whichever runs
|
|
3592
|
+
// first resolves the promise and clears the ref, so the write callback is a harmless no-op.
|
|
3593
|
+
let resolvePendingWrite;
|
|
3594
|
+
const settleWrite = () => {
|
|
3595
|
+
const resolve = resolvePendingWrite;
|
|
3596
|
+
resolvePendingWrite = undefined;
|
|
3597
|
+
resolve?.();
|
|
3598
|
+
};
|
|
3599
|
+
verifier.on('data', (chunk) => {
|
|
3600
|
+
inflated += chunk.length;
|
|
3601
|
+
if (inflated > size && !verifyFailure) {
|
|
3602
|
+
verifyFailure = failVerification(`inflates past its declared size of ${size}`);
|
|
3603
|
+
verifier.destroy();
|
|
3604
|
+
}
|
|
3605
|
+
});
|
|
3606
|
+
verifier.on('error', (error) => {
|
|
3607
|
+
verifyFailure ??= failVerification(`is not a valid deflate stream: ${error.message}`);
|
|
3608
|
+
settleWrite();
|
|
3609
|
+
});
|
|
3610
|
+
verifier.on('close', settleWrite); // a destroy() with no 'error' (oversize) also drops the write callback
|
|
3611
|
+
try {
|
|
3612
|
+
for await (const chunk of source) {
|
|
3613
|
+
if (verifyFailure)
|
|
3614
|
+
throw verifyFailure;
|
|
3615
|
+
if (!verifier.destroyed) {
|
|
3616
|
+
// Yield only once the verifier consumed the chunk, so a detection surfaces before the
|
|
3617
|
+
// bytes it condemns are handed to the caller's wire frame (and never after the caller
|
|
3618
|
+
// has already seen a clean end).
|
|
3619
|
+
await new Promise((resolveWrite) => {
|
|
3620
|
+
resolvePendingWrite = resolveWrite;
|
|
3621
|
+
verifier.write(chunk, settleWrite);
|
|
3622
|
+
});
|
|
3623
|
+
if (verifyFailure)
|
|
3624
|
+
throw verifyFailure;
|
|
3625
|
+
}
|
|
3626
|
+
yield chunk;
|
|
3627
|
+
}
|
|
3628
|
+
if (!verifier.destroyed) {
|
|
3629
|
+
await new Promise((resolveEnd) => {
|
|
3630
|
+
verifier.on('close', resolveEnd);
|
|
3631
|
+
verifier.end();
|
|
3632
|
+
});
|
|
3633
|
+
}
|
|
3634
|
+
if (verifyFailure)
|
|
3635
|
+
throw verifyFailure;
|
|
3636
|
+
if (inflated !== size)
|
|
3637
|
+
throw failVerification(`inflated to ${inflated} bytes of a declared ${size}`);
|
|
3638
|
+
}
|
|
3639
|
+
finally {
|
|
3640
|
+
source.destroy();
|
|
3641
|
+
verifier.destroy();
|
|
3642
|
+
releaseHoldOnce();
|
|
3643
|
+
}
|
|
3644
|
+
},
|
|
3645
|
+
};
|
|
3646
|
+
}
|
|
2769
3647
|
/**
|
|
2770
3648
|
* Async generator that yields records whose referenced blob files are missing, truncated, or in an
|
|
2771
3649
|
* error state. Used by the blob repair sweep to find candidates for peer-fetched recovery.
|