harper 5.1.20 → 5.2.0-alpha.5
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/operations.ts +13 -16
- package/components/Application.ts +188 -7
- package/components/ApplicationScope.ts +3 -0
- package/components/Scope.ts +12 -0
- package/components/anthropic/index.ts +9 -2
- package/components/bedrock/index.ts +25 -2
- package/components/componentLoader.ts +56 -7
- package/components/componentSecrets.ts +451 -0
- package/components/deploymentRecorder.ts +7 -1
- package/components/mcp/adapters/fastify.ts +3 -0
- package/components/mcp/adapters/harperHttp.ts +3 -0
- package/components/mcp/audit.ts +21 -6
- package/components/mcp/customResourceRegistry.ts +17 -38
- package/components/mcp/listChanged.ts +37 -20
- package/components/mcp/quota.ts +145 -0
- package/components/mcp/rateLimit.ts +152 -14
- package/components/mcp/resources.ts +2 -4
- package/components/mcp/toolRegistry.ts +73 -4
- package/components/mcp/tools/application.ts +77 -8
- package/components/mcp/tools/operations.ts +106 -36
- package/components/mcp/tools/schemas/operationDescriptions.ts +4 -4
- package/components/mcp/tools/schemas/operations.ts +5 -0
- package/components/mcp/transport.ts +55 -4
- package/components/ollama/index.ts +6 -2
- package/components/openai/index.ts +9 -2
- package/components/operations.js +204 -10
- package/components/operationsValidation.js +173 -0
- package/components/secretOperations.ts +498 -0
- package/config/configUtils.ts +25 -1
- package/config-app.schema.json +81 -0
- package/config-root.schema.json +8 -1
- package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
- package/dataLayer/readAuditLog.ts +14 -0
- package/dataLayer/schemaDescribe.ts +1 -0
- package/dist/agent/operations.d.ts +7 -4
- package/dist/agent/operations.js +13 -15
- package/dist/agent/operations.js.map +1 -1
- package/dist/components/Application.d.ts +25 -2
- package/dist/components/Application.js +166 -7
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +2 -0
- package/dist/components/ApplicationScope.js +3 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/Scope.d.ts +8 -0
- package/dist/components/Scope.js +11 -0
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/anthropic/index.d.ts +4 -1
- package/dist/components/anthropic/index.js +4 -2
- package/dist/components/anthropic/index.js.map +1 -1
- package/dist/components/bedrock/index.d.ts +4 -1
- package/dist/components/bedrock/index.js +22 -2
- package/dist/components/bedrock/index.js.map +1 -1
- package/dist/components/componentLoader.d.ts +2 -0
- package/dist/components/componentLoader.js +52 -10
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/componentSecrets.d.ts +63 -0
- package/dist/components/componentSecrets.js +405 -0
- package/dist/components/componentSecrets.js.map +1 -0
- package/dist/components/deploymentRecorder.d.ts +3 -0
- package/dist/components/deploymentRecorder.js +4 -3
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/mcp/adapters/fastify.d.ts +2 -0
- package/dist/components/mcp/adapters/fastify.js +1 -0
- package/dist/components/mcp/adapters/fastify.js.map +1 -1
- package/dist/components/mcp/adapters/harperHttp.d.ts +2 -0
- package/dist/components/mcp/adapters/harperHttp.js +1 -0
- package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
- package/dist/components/mcp/audit.d.ts +2 -2
- package/dist/components/mcp/audit.js +20 -5
- package/dist/components/mcp/audit.js.map +1 -1
- package/dist/components/mcp/customResourceRegistry.d.ts +5 -12
- package/dist/components/mcp/customResourceRegistry.js +15 -30
- package/dist/components/mcp/customResourceRegistry.js.map +1 -1
- package/dist/components/mcp/listChanged.d.ts +7 -2
- package/dist/components/mcp/listChanged.js +28 -0
- package/dist/components/mcp/listChanged.js.map +1 -1
- package/dist/components/mcp/quota.d.ts +31 -0
- package/dist/components/mcp/quota.js +155 -0
- package/dist/components/mcp/quota.js.map +1 -0
- package/dist/components/mcp/rateLimit.d.ts +19 -3
- package/dist/components/mcp/rateLimit.js +129 -13
- package/dist/components/mcp/rateLimit.js.map +1 -1
- package/dist/components/mcp/resources.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +39 -1
- package/dist/components/mcp/toolRegistry.js +60 -5
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/application.d.ts +8 -1
- package/dist/components/mcp/tools/application.js +65 -5
- package/dist/components/mcp/tools/application.js.map +1 -1
- package/dist/components/mcp/tools/operations.d.ts +15 -3
- package/dist/components/mcp/tools/operations.js +99 -38
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +4 -4
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operations.js +4 -0
- package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
- package/dist/components/mcp/transport.d.ts +6 -0
- package/dist/components/mcp/transport.js +47 -3
- package/dist/components/mcp/transport.js.map +1 -1
- package/dist/components/ollama/index.d.ts +4 -1
- package/dist/components/ollama/index.js +4 -2
- package/dist/components/ollama/index.js.map +1 -1
- package/dist/components/openai/index.d.ts +4 -1
- package/dist/components/openai/index.js +4 -2
- package/dist/components/openai/index.js.map +1 -1
- package/dist/components/operations.d.ts +39 -0
- package/dist/components/operations.js +194 -2
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.d.ts +38 -0
- package/dist/components/operationsValidation.js +155 -0
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/secretOperations.d.ts +113 -0
- package/dist/components/secretOperations.js +475 -0
- package/dist/components/secretOperations.js.map +1 -0
- package/dist/config/configUtils.d.ts +4 -1
- package/dist/config/configUtils.js +18 -1
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
- package/dist/dataLayer/readAuditLog.js +11 -0
- package/dist/dataLayer/readAuditLog.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +2 -0
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/globals.d.ts +1 -1
- package/dist/globals.js +1 -0
- package/dist/globals.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +38 -0
- package/dist/resources/DatabaseTransaction.d.ts +31 -3
- package/dist/resources/DatabaseTransaction.js +190 -29
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +1 -1
- package/dist/resources/LMDBTransaction.js +43 -5
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.d.ts +51 -0
- package/dist/resources/PrimaryRocksDatabase.js +194 -0
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -0
- package/dist/resources/RecordEncoder.d.ts +1 -0
- package/dist/resources/RecordEncoder.js +81 -75
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/RequestTarget.d.ts +19 -0
- package/dist/resources/RequestTarget.js.map +1 -1
- package/dist/resources/Resource.d.ts +0 -1
- package/dist/resources/Resource.js +103 -16
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +6 -2
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/Resources.js.map +1 -1
- package/dist/resources/Table.d.ts +6 -4
- package/dist/resources/Table.js +247 -24
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/read.js +25 -28
- package/dist/resources/analytics/read.js.map +1 -1
- package/dist/resources/blob.js +165 -22
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/crdt.d.ts +2 -0
- package/dist/resources/crdt.js +45 -36
- package/dist/resources/crdt.js.map +1 -1
- package/dist/resources/databases.d.ts +1 -0
- package/dist/resources/databases.js +134 -7
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/graphql.js +8 -0
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +35 -5
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +239 -14
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/jsResource.js +6 -0
- package/dist/resources/jsResource.js.map +1 -1
- package/dist/resources/loadEnv.js +30 -3
- package/dist/resources/loadEnv.js.map +1 -1
- package/dist/resources/login.js +2 -2
- package/dist/resources/login.js.map +1 -1
- package/dist/resources/models/Models.js +14 -3
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/embedHook.js +10 -2
- package/dist/resources/models/embedHook.js.map +1 -1
- package/dist/resources/models/openaiStream.d.ts +56 -0
- package/dist/resources/models/openaiStream.js +94 -0
- package/dist/resources/models/openaiStream.js.map +1 -0
- package/dist/resources/replayLogs.js +4 -3
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/search.d.ts +1 -1
- package/dist/resources/search.js +104 -11
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/secretDecryptor.d.ts +54 -0
- package/dist/resources/secretDecryptor.js +131 -0
- package/dist/resources/secretDecryptor.js.map +1 -0
- package/dist/resources/tracked.js +15 -5
- package/dist/resources/tracked.js.map +1 -1
- package/dist/security/auth.js +71 -8
- package/dist/security/auth.js.map +1 -1
- package/dist/security/jsLoader.js +11 -0
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/tokenAuthentication.d.ts +9 -1
- package/dist/security/tokenAuthentication.js +31 -1
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/server/DurableSubscriptionsSession.d.ts +1 -1
- package/dist/server/DurableSubscriptionsSession.js +8 -4
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/REST.js +28 -9
- package/dist/server/REST.js.map +1 -1
- package/dist/server/fastifyRoutes.js +11 -1
- package/dist/server/fastifyRoutes.js.map +1 -1
- package/dist/server/graphqlQuerying.js +4 -3
- package/dist/server/graphqlQuerying.js.map +1 -1
- package/dist/server/http.d.ts +40 -2
- package/dist/server/http.js +560 -77
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +64 -0
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/liveSubscriptionAuth.d.ts +12 -0
- package/dist/server/liveSubscriptionAuth.js +140 -0
- package/dist/server/liveSubscriptionAuth.js.map +1 -0
- package/dist/server/loadRootComponents.js +2 -1
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/middlewareChain.d.ts +44 -1
- package/dist/server/middlewareChain.js +86 -10
- package/dist/server/middlewareChain.js.map +1 -1
- package/dist/server/mqtt.js +2 -1
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/operationsServer.js +1 -1
- package/dist/server/operationsServer.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +9 -0
- package/dist/server/serverHelpers/Headers.js +22 -0
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/JSONStream.js +3 -3
- package/dist/server/serverHelpers/JSONStream.js.map +1 -1
- package/dist/server/serverHelpers/Request.d.ts +52 -1
- package/dist/server/serverHelpers/Request.js +122 -1
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +3 -8
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/progressEmitter.d.ts +19 -0
- package/dist/server/serverHelpers/progressEmitter.js +52 -4
- package/dist/server/serverHelpers/progressEmitter.js.map +1 -1
- package/dist/server/serverHelpers/registeredOperations.d.ts +40 -0
- package/dist/server/serverHelpers/registeredOperations.js +286 -0
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -0
- package/dist/server/serverHelpers/serverHandlers.js +11 -5
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +1 -0
- package/dist/server/serverHelpers/serverUtilities.js +67 -5
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.d.ts +80 -0
- package/dist/server/serverHelpers/uwsServer.js +433 -0
- package/dist/server/serverHelpers/uwsServer.js.map +1 -0
- package/dist/server/static.d.ts +0 -15
- package/dist/server/static.js +161 -2
- package/dist/server/static.js.map +1 -1
- package/dist/server/status/index.d.ts +4 -1
- package/dist/server/status/index.js +64 -4
- package/dist/server/status/index.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +10 -0
- package/dist/server/threads/manageThreads.js +107 -4
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.js +5 -1
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +92 -14
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/upgrade/directives/5-2-0.d.ts +7 -0
- package/dist/upgrade/directives/5-2-0.js +108 -0
- package/dist/upgrade/directives/5-2-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/common_utils.js +5 -0
- package/dist/utility/common_utils.js.map +1 -1
- package/dist/utility/envFile.d.ts +41 -0
- package/dist/utility/envFile.js +221 -0
- package/dist/utility/envFile.js.map +1 -0
- package/dist/utility/globalSchema.d.ts +9 -0
- package/dist/utility/hdbTerms.d.ts +25 -0
- package/dist/utility/hdbTerms.js +31 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +21 -0
- package/dist/utility/logging/harper_logger.js +160 -8
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logRotator.js +29 -17
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/logging/readLog.d.ts +1 -1
- package/dist/utility/logging/readLog.js +305 -2
- package/dist/utility/logging/readLog.js.map +1 -1
- package/dist/utility/operationPermissions.d.ts +18 -0
- package/dist/utility/operationPermissions.js +35 -1
- package/dist/utility/operationPermissions.js.map +1 -1
- package/dist/utility/operation_authorization.d.ts +17 -0
- package/dist/utility/operation_authorization.js +38 -0
- package/dist/utility/operation_authorization.js.map +1 -1
- package/dist/utility/secretEnvelope.d.ts +30 -0
- package/dist/utility/secretEnvelope.js +94 -0
- package/dist/utility/secretEnvelope.js.map +1 -0
- package/dist/utility/signalling.d.ts +7 -0
- package/dist/utility/signalling.js +16 -0
- package/dist/utility/signalling.js.map +1 -1
- package/dist/validation/configValidator.js +70 -24
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +4 -0
- package/json/systemSchema.json +38 -0
- package/npm-shrinkwrap.json +15066 -9347
- package/package.json +13 -8
- package/resources/DESIGN.md +15 -12
- package/resources/DatabaseTransaction.ts +206 -37
- package/resources/LMDBTransaction.ts +43 -9
- package/resources/PrimaryRocksDatabase.ts +201 -0
- package/resources/RecordEncoder.ts +81 -69
- package/resources/RequestTarget.ts +21 -0
- package/resources/Resource.ts +109 -25
- package/resources/ResourceInterface.ts +6 -2
- package/resources/Resources.ts +1 -3
- package/resources/Table.ts +238 -29
- package/resources/analytics/read.ts +25 -30
- package/resources/blob.ts +159 -23
- package/resources/crdt.ts +37 -31
- package/resources/databases.ts +143 -6
- package/resources/graphql.ts +8 -0
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +251 -17
- package/resources/jsResource.ts +6 -0
- package/resources/loadEnv.ts +34 -3
- package/resources/login.ts +2 -2
- package/resources/models/Models.ts +19 -3
- package/resources/models/embedHook.ts +13 -2
- package/resources/models/openaiStream.ts +133 -0
- package/resources/replayLogs.ts +4 -3
- package/resources/search.ts +113 -11
- package/resources/secretDecryptor.ts +159 -0
- package/resources/tracked.ts +24 -17
- package/security/auth.ts +70 -10
- package/security/jsLoader.ts +12 -0
- package/security/tokenAuthentication.ts +42 -1
- package/server/DESIGN.md +10 -0
- package/server/DurableSubscriptionsSession.ts +8 -4
- package/server/REST.ts +26 -9
- package/server/fastifyRoutes.ts +10 -1
- package/server/graphqlQuerying.ts +4 -3
- package/server/http.ts +563 -77
- package/server/itc/serverHandlers.js +70 -0
- package/server/liveSubscriptionAuth.ts +152 -0
- package/server/loadRootComponents.js +2 -1
- package/server/middlewareChain.ts +107 -17
- package/server/mqtt.ts +2 -1
- package/server/operationsServer.ts +2 -2
- package/server/serverHelpers/Headers.ts +24 -0
- package/server/serverHelpers/JSONStream.ts +3 -3
- package/server/serverHelpers/Request.ts +127 -0
- package/server/serverHelpers/contentTypes.ts +3 -8
- package/server/serverHelpers/progressEmitter.ts +55 -5
- package/server/serverHelpers/registeredOperations.ts +259 -0
- package/server/serverHelpers/serverHandlers.js +11 -5
- package/server/serverHelpers/serverUtilities.ts +88 -5
- package/server/serverHelpers/uwsServer.ts +488 -0
- package/server/static.ts +180 -2
- package/server/status/index.ts +72 -4
- package/server/threads/manageThreads.js +102 -4
- package/server/threads/socketRouter.ts +5 -1
- package/server/threads/threadServer.js +93 -10
- package/studio/web/assets/{Chat-SdD1EZca.js → Chat-Cv_2paZE.js} +2 -2
- package/studio/web/assets/{Chat-SdD1EZca.js.map → Chat-Cv_2paZE.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-GMq6tHK9.js → FloatingChat-CPyPIcVR.js} +4 -4
- package/studio/web/assets/{FloatingChat-GMq6tHK9.js.map → FloatingChat-CPyPIcVR.js.map} +1 -1
- package/studio/web/assets/{applications-B5hhv7uA.js → applications-C0jT2vdZ.js} +2 -2
- package/studio/web/assets/{applications-B5hhv7uA.js.map → applications-C0jT2vdZ.js.map} +1 -1
- package/studio/web/assets/{index-VFcpNWgq.js → index-D_GKOkhn.js} +6 -6
- package/studio/web/assets/index-D_GKOkhn.js.map +1 -0
- package/studio/web/assets/{index.lazy-DgcJojIA.js → index.lazy-CAmCIA65.js} +4 -4
- package/studio/web/assets/{index.lazy-DgcJojIA.js.map → index.lazy-CAmCIA65.js.map} +1 -1
- package/studio/web/assets/{profile-BqN-8vzm.js → profile-CaF-4aZe.js} +2 -2
- package/studio/web/assets/{profile-BqN-8vzm.js.map → profile-CaF-4aZe.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-CHFvFq9u.js → setComponentFile-DLW1DHCt.js} +2 -2
- package/studio/web/assets/{setComponentFile-CHFvFq9u.js.map → setComponentFile-DLW1DHCt.js.map} +1 -1
- package/studio/web/assets/{setup-CQ3vWJSr.js → setup-TGCErIhq.js} +2 -2
- package/studio/web/assets/{setup-CQ3vWJSr.js.map → setup-TGCErIhq.js.map} +1 -1
- package/studio/web/assets/{status-BsW5fjuh.js → status-DG0Maoao.js} +2 -2
- package/studio/web/assets/{status-BsW5fjuh.js.map → status-DG0Maoao.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-DITdeB9-.js → swagger-ui-react-8T4SgQ1m.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-DITdeB9-.js.map → swagger-ui-react-8T4SgQ1m.js.map} +1 -1
- package/studio/web/assets/{tsMode-Dyph-OUs.js → tsMode-DJ6Sl24Q.js} +2 -2
- package/studio/web/assets/{tsMode-Dyph-OUs.js.map → tsMode-DJ6Sl24Q.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-D0QTUqex.js → useEntityRestURL-JO2mfWTQ.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-D0QTUqex.js.map → useEntityRestURL-JO2mfWTQ.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-2-0.ts +82 -0
- package/upgrade/directives/directivesController.ts +2 -1
- package/utility/common_utils.ts +5 -0
- package/utility/envFile.ts +218 -0
- package/utility/hdbTerms.ts +31 -0
- package/utility/logging/harper_logger.ts +156 -8
- package/utility/logging/logRotator.ts +24 -14
- package/utility/logging/readLog.ts +323 -2
- package/utility/operationPermissions.ts +35 -1
- package/utility/operation_authorization.ts +74 -1
- package/utility/secretEnvelope.ts +113 -0
- package/utility/signalling.ts +15 -0
- package/validation/configValidator.ts +78 -25
- package/studio/web/assets/index-VFcpNWgq.js.map +0 -1
package/server/static.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { realpathSync, existsSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { Scope } from '../components/Scope';
|
|
4
4
|
import { resolveBaseURLPath } from '../components/resolveBaseURLPath.ts';
|
|
5
|
+
import { convertToMS } from '../utility/common_utils.ts';
|
|
6
|
+
import { isMatch } from 'micromatch';
|
|
5
7
|
import send from 'send';
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -12,13 +14,134 @@ import send from 'send';
|
|
|
12
14
|
* - `index`: If enabled, it will serve `index.html` files from directories.
|
|
13
15
|
* - `extensions`: An array of file extensions to try when serving files. If a file is not found, it will try appending each extension in order. For example, if set to `['html'], and the request is `/page`, it will try `/page.html` if `/page` is not found.
|
|
14
16
|
* - `fallthrough`: If true, it will fall through to the next handler if the file is not found. If false, it will return a 404 error.
|
|
17
|
+
* - `maxAge`: Freshness lifetime for served files — a number of seconds or a duration string
|
|
18
|
+
* (`'5m'`, `'1d'`). Emitted as `Cache-Control: public, max-age=<seconds>`. Defaults to 0
|
|
19
|
+
* (revalidate every request via the ETag/Last-Modified that are always emitted).
|
|
20
|
+
* - `immutable`: If true, adds the `immutable` directive to `Cache-Control` — for content-hashed
|
|
21
|
+
* assets that never change under the same URL. Requires `maxAge` to be meaningful.
|
|
22
|
+
* - `cacheControl`: Full `Cache-Control` override string (takes precedence over `maxAge`/`immutable`),
|
|
23
|
+
* or `false` to suppress the header entirely. Static files are served before authentication, so
|
|
24
|
+
* they are public by construction — do not put per-user content behind this handler.
|
|
25
|
+
* - `cacheOverrides`: A map of glob pattern → partial cache options (`maxAge` / `immutable` /
|
|
26
|
+
* `cacheControl`), letting specific files opt out of the top-level defaults. The typical case is
|
|
27
|
+
* long-lived `immutable` defaults for content-hashed assets while `index.html` gets a short window
|
|
28
|
+
* or `stale-while-revalidate`. Patterns are matched (via `micromatch`, same engine as `files`)
|
|
29
|
+
* against the mount-relative URL path **and** the served file's basename — so `index.html` also
|
|
30
|
+
* targets the directory-index (`/`) serve. Entries are tested in config order and the first match
|
|
31
|
+
* wins; each is a partial (keys present replace the default, absent keys inherit), with the same
|
|
32
|
+
* `cacheControl`-beats-`maxAge`/`immutable` precedence as the top level. Example:
|
|
33
|
+
* ```yaml
|
|
34
|
+
* static:
|
|
35
|
+
* files: 'web/**'
|
|
36
|
+
* maxAge: 1y
|
|
37
|
+
* immutable: true
|
|
38
|
+
* cacheOverrides:
|
|
39
|
+
* 'index.html': { cacheControl: 'public, max-age=0, stale-while-revalidate=60' }
|
|
40
|
+
* '*.html': { maxAge: 5m, immutable: false }
|
|
41
|
+
* ```
|
|
15
42
|
* - `notFound`: Can be specified as a string to serve a custom 404 page, or an object with `file` and `statusCode` properties to serve a custom file with a specific status code. This is useful for hosting SPAs that use client-side routing. Make sure to set `fallthrough` to `false`!
|
|
43
|
+
* - `before` / `after`: Position this handler in the HTTP middleware chain relative to another named
|
|
44
|
+
* handler. By default the handler runs `before: 'authentication'` — and therefore before the REST
|
|
45
|
+
* handler — so plain file requests skip credential parsing. That default means a `fallthrough: false`
|
|
46
|
+
* catch-all answers GETs for exported REST resources too; an SPA with history-mode routing should set
|
|
47
|
+
* `after: 'rest'` so the API is matched first and only unmatched URLs receive the `notFound` fallback.
|
|
48
|
+
* `before: false` clears the default without adding a new constraint (registration order applies).
|
|
49
|
+
* Handler names are the component config keys as registered (e.g. `rest`, not the legacy `REST` alias);
|
|
50
|
+
* a name that matches no registered handler is ignored, with a warning logged by the middleware chain.
|
|
51
|
+
* Ordering is applied when the component loads; changing `before`/`after` triggers a component restart.
|
|
16
52
|
*
|
|
17
53
|
* This plugin dynamically updates its behavior based on the current configuration file. Users can make updates and immediately see the changes reflect in the next request.
|
|
18
54
|
*
|
|
19
55
|
* Updates to the `files` option will clear the in-memory maps and allow them to regenerate based on the new configuration (since the default EntryHandler will regenerate anyways).
|
|
20
56
|
* Updates to `urlPath` request a restart: the HTTP route mount is registered once at load and cannot be re-registered on a live server (#1583).
|
|
21
57
|
*/
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the effective cache-header inputs for a given served file: the live top-level
|
|
60
|
+
* `maxAge`/`immutable`/`cacheControl` options, with the first matching `cacheOverrides` entry layered
|
|
61
|
+
* on top (a partial — keys present replace the default, absent keys inherit). Patterns are matched
|
|
62
|
+
* (via `micromatch`, same engine as `files`) against the mount-relative URL path and the file's
|
|
63
|
+
* basename, so `index.html` also targets the directory-index (`/`) serve.
|
|
64
|
+
*/
|
|
65
|
+
function resolveCacheOptions(scope: Scope, urlKey: string, basename: string) {
|
|
66
|
+
let maxAge = scope.options.get(['maxAge']);
|
|
67
|
+
let immutable = scope.options.get(['immutable']);
|
|
68
|
+
let cacheControl = scope.options.get(['cacheControl']);
|
|
69
|
+
|
|
70
|
+
const overrides = scope.options.get(['cacheOverrides']);
|
|
71
|
+
if (overrides !== undefined) {
|
|
72
|
+
if (typeof overrides !== 'object' || overrides === null || Array.isArray(overrides)) {
|
|
73
|
+
throw new Error(`Invalid cacheOverrides option: ${overrides}. Must be a map of glob pattern to cache options.`);
|
|
74
|
+
}
|
|
75
|
+
for (const pattern of Object.keys(overrides)) {
|
|
76
|
+
if (isMatch(urlKey, pattern) || isMatch(basename, pattern)) {
|
|
77
|
+
const override = overrides[pattern];
|
|
78
|
+
if (typeof override !== 'object' || override === null || Array.isArray(override)) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Invalid cacheOverrides['${pattern}'] value: ${override}. Must be an object with maxAge/immutable/cacheControl.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
// partial merge: only keys present in the override replace the top-level default
|
|
84
|
+
if ('maxAge' in override) maxAge = override.maxAge;
|
|
85
|
+
if ('immutable' in override) immutable = override.immutable;
|
|
86
|
+
if ('cacheControl' in override) cacheControl = override.cacheControl;
|
|
87
|
+
break; // first match wins
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
immutable = immutable ?? false;
|
|
93
|
+
if (maxAge !== undefined && typeof maxAge !== 'number' && typeof maxAge !== 'string') {
|
|
94
|
+
throw new Error(`Invalid maxAge option: ${maxAge}. Must be a number of seconds or a duration string like '5m'.`);
|
|
95
|
+
}
|
|
96
|
+
const maxAgeMs = maxAge === undefined ? 0 : convertToMS(maxAge);
|
|
97
|
+
if (Number.isNaN(maxAgeMs)) {
|
|
98
|
+
throw new Error(`Invalid maxAge option: ${maxAge}. Must be a number of seconds or a duration string like '5m'.`);
|
|
99
|
+
}
|
|
100
|
+
if (typeof immutable !== 'boolean') {
|
|
101
|
+
throw new Error(`Invalid immutable option: ${immutable}. Must be a boolean.`);
|
|
102
|
+
}
|
|
103
|
+
if (cacheControl !== undefined && typeof cacheControl !== 'string' && cacheControl !== false) {
|
|
104
|
+
throw new Error(`Invalid cacheControl option: ${cacheControl}. Must be a string or false.`);
|
|
105
|
+
}
|
|
106
|
+
const customCacheControl = typeof cacheControl === 'string' ? cacheControl : undefined;
|
|
107
|
+
return { cacheControlDisabled: cacheControl === false, maxAgeMs, immutable, customCacheControl };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Serve a file through `send`, applying the plugin's cache-header options (read live from the
|
|
112
|
+
* component config, like `fallthrough`, and layered with any matching `cacheOverrides` entry).
|
|
113
|
+
* `cacheControl` as a string overrides `maxAge`/`immutable` via send's `headers` event;
|
|
114
|
+
* `cacheControl: false` suppresses the header entirely. Applied only to the main file serve — the
|
|
115
|
+
* `notFound` fallback keeps send's default `max-age=0`, which is the right policy for SPA index
|
|
116
|
+
* fallbacks.
|
|
117
|
+
*/
|
|
118
|
+
function serveFile(req, path: string, scope: Scope) {
|
|
119
|
+
// The staticFiles map keys are the mount-relative URL path (leading slash stripped for matching);
|
|
120
|
+
// for a directory-index serve req.pathname is the directory, so also match the served basename.
|
|
121
|
+
const urlKey = typeof req.pathname === 'string' ? req.pathname.replace(/^\//, '') : '';
|
|
122
|
+
const basename = path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1);
|
|
123
|
+
const { cacheControlDisabled, maxAgeMs, immutable, customCacheControl } = resolveCacheOptions(
|
|
124
|
+
scope,
|
|
125
|
+
urlKey,
|
|
126
|
+
basename
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const stream = send(req, path, {
|
|
130
|
+
// suppress send's own header when we set a full override below (or when disabled)
|
|
131
|
+
cacheControl: cacheControlDisabled || customCacheControl !== undefined ? false : true,
|
|
132
|
+
maxAge: maxAgeMs,
|
|
133
|
+
immutable,
|
|
134
|
+
});
|
|
135
|
+
// an empty-string override intentionally behaves like `false` (send's header is suppressed
|
|
136
|
+
// above and no override is written)
|
|
137
|
+
if (customCacheControl) {
|
|
138
|
+
stream.on('headers', (response) => {
|
|
139
|
+
response.setHeader('Cache-Control', customCacheControl);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return stream;
|
|
143
|
+
}
|
|
144
|
+
|
|
22
145
|
export function handleApplication(scope: Scope) {
|
|
23
146
|
// in-memory map of static files
|
|
24
147
|
// keys are the URL paths relative to the mount base, values are the absolute paths to the files
|
|
@@ -30,6 +153,34 @@ export function handleApplication(scope: Scope) {
|
|
|
30
153
|
// with the registered route (#1583).
|
|
31
154
|
const baseURLPath = resolveBaseURLPath(scope.pluginName, (scope.options.getAll() as any)?.urlPath);
|
|
32
155
|
|
|
156
|
+
// A bare `before:` / `after:` key in YAML parses as null — treat it as unset, like before this
|
|
157
|
+
// option was validated.
|
|
158
|
+
const before = scope.options.get(['before']) ?? undefined;
|
|
159
|
+
const after = scope.options.get(['after']) ?? undefined;
|
|
160
|
+
validateOrderingOption('before', before, true);
|
|
161
|
+
validateOrderingOption('after', after, false);
|
|
162
|
+
// Default to the pre-authentication hoist only when no ordering was configured at all.
|
|
163
|
+
const noOrderingConfigured = before === undefined && after === undefined;
|
|
164
|
+
|
|
165
|
+
// With the default ordering this handler answers unmatched GETs ahead of the REST handler, so a
|
|
166
|
+
// `fallthrough: false` catch-all makes any exported REST resources unreachable over GET.
|
|
167
|
+
// Reads the live option values (not the registration-time ones above): a config save can change
|
|
168
|
+
// the ordering options together with `fallthrough`, and this re-runs from the change listener.
|
|
169
|
+
const warnIfBlockingRest = () => {
|
|
170
|
+
const liveBefore = scope.options.get(['before']) ?? undefined;
|
|
171
|
+
const liveAfter = scope.options.get(['after']) ?? undefined;
|
|
172
|
+
if (
|
|
173
|
+
liveAfter === undefined &&
|
|
174
|
+
(liveBefore === undefined || liveBefore === 'authentication') &&
|
|
175
|
+
scope.options.get(['fallthrough']) === false
|
|
176
|
+
) {
|
|
177
|
+
scope.logger.warn(
|
|
178
|
+
`The static handler runs before authentication and REST by default, so \`fallthrough: false\` answers every unmatched GET itself — including GETs for any exported REST resources. If this application serves an API, add \`after: 'rest'\` to the static options so API requests are matched first, or remove \`fallthrough: false\`.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
warnIfBlockingRest();
|
|
183
|
+
|
|
33
184
|
scope.options.on('change', (key) => {
|
|
34
185
|
if (key[0] === 'files') {
|
|
35
186
|
// If the files option changes, clear the maps and let the entry handler regenerate them
|
|
@@ -43,6 +194,16 @@ export function handleApplication(scope: Scope) {
|
|
|
43
194
|
scope.requestRestart();
|
|
44
195
|
return;
|
|
45
196
|
}
|
|
197
|
+
if (key[0] === 'fallthrough') {
|
|
198
|
+
warnIfBlockingRest();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// `before`/`after` are consumed once at registration; the middleware chain can only pick
|
|
202
|
+
// up a new ordering by reloading the component. (Scope's own auto-restart on option
|
|
203
|
+
// changes is bypassed once a plugin registers its own 'change' listener, as we do above.)
|
|
204
|
+
if (key[0] === 'before' || key[0] === 'after') {
|
|
205
|
+
scope.requestRestart();
|
|
206
|
+
}
|
|
46
207
|
});
|
|
47
208
|
|
|
48
209
|
// Handle entry events for the default entry handler based on the `files` and `urlPath` options
|
|
@@ -171,7 +332,7 @@ export function handleApplication(scope: Scope) {
|
|
|
171
332
|
// The benefit to using `send` is that it handles a lot of edge cases and headers for us.
|
|
172
333
|
return {
|
|
173
334
|
handlesHeaders: true,
|
|
174
|
-
body:
|
|
335
|
+
body: serveFile(req, realpathSync(staticFile), scope),
|
|
175
336
|
};
|
|
176
337
|
}
|
|
177
338
|
|
|
@@ -206,7 +367,24 @@ export function handleApplication(scope: Scope) {
|
|
|
206
367
|
body: send(req, realpathSync(notFoundPath)),
|
|
207
368
|
};
|
|
208
369
|
},
|
|
209
|
-
{
|
|
370
|
+
{
|
|
371
|
+
// `after` (e.g. `after: 'rest'`) must suppress the default pre-authentication hoist —
|
|
372
|
+
// combining the two constraints would be a cycle, which falls back to registration order.
|
|
373
|
+
before: typeof before === 'string' ? before : noOrderingConfigured ? 'authentication' : undefined,
|
|
374
|
+
after: after as string | undefined,
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function validateOrderingOption(
|
|
380
|
+
name: string,
|
|
381
|
+
value: any,
|
|
382
|
+
allowFalse: boolean
|
|
383
|
+
): asserts value is undefined | string | false {
|
|
384
|
+
if (value === undefined || (typeof value === 'string' && value.length > 0)) return;
|
|
385
|
+
if (allowFalse && value === false) return;
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Invalid \`${name}\` option: ${value}. Must be the name of another handler${allowFalse ? ', or false to clear the default ordering' : ''}.`
|
|
210
388
|
);
|
|
211
389
|
}
|
|
212
390
|
|
package/server/status/index.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { validateStatus } from '../../validation/statusValidator.ts';
|
|
|
5
5
|
import { type StatusId, type StatusValueMap, type StatusRecord, DEFAULT_STATUS_ID } from './definitions.ts';
|
|
6
6
|
import { internal as statusInternal, type AggregatedComponentStatus } from '../../components/status/index.ts';
|
|
7
7
|
import { restartNeeded } from '../../components/requestRestart.ts';
|
|
8
|
+
import { sendItcEvent } from '../threads/itc.js';
|
|
9
|
+
import { onMessageByType, workers } from '../threads/manageThreads.js';
|
|
10
|
+
import { ITC_EVENT_TYPES, THREAD_TYPES } from '../../utility/hdbTerms.ts';
|
|
8
11
|
|
|
9
12
|
export { clearStatus as clear, getStatus as get, setStatus as set };
|
|
10
13
|
|
|
@@ -18,6 +21,9 @@ const { HTTP_STATUS_CODES } = hdbErrors;
|
|
|
18
21
|
// For direct function calls, we don't need the operation fields
|
|
19
22
|
type StatusRequestBody = {
|
|
20
23
|
id: StatusId;
|
|
24
|
+
// Opt in to the resolved HTTP/upgrade/WebSocket middleware chains in the aggregated (no-id)
|
|
25
|
+
// response (#1573). Off by default so routine polling avoids the cross-thread lookup.
|
|
26
|
+
middleware?: boolean;
|
|
21
27
|
};
|
|
22
28
|
|
|
23
29
|
type StatusWriteRequestBody<T extends StatusId = StatusId> = {
|
|
@@ -76,9 +82,69 @@ interface AllStatusSummary {
|
|
|
76
82
|
systemStatus: Promise<AsyncIterable<StatusRecord>>;
|
|
77
83
|
componentStatus: AggregatedComponentStatusWithName[];
|
|
78
84
|
restartRequired: boolean;
|
|
85
|
+
// Only present when the request opts in with `middleware: true`.
|
|
86
|
+
middlewareChains?: MiddlewareChainsSummary | null;
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
|
|
89
|
+
type MiddlewareChainsSummary = ReturnType<typeof import('../http.ts').describeMiddlewareChains>;
|
|
90
|
+
|
|
91
|
+
let nextChainsRequestId = 1;
|
|
92
|
+
let chainsResponseListenerAttached = false;
|
|
93
|
+
const pendingChainsRequests = new Map<number, (chains: MiddlewareChainsSummary) => void>();
|
|
94
|
+
|
|
95
|
+
function attachChainsResponseListener(): void {
|
|
96
|
+
if (chainsResponseListenerAttached) return;
|
|
97
|
+
onMessageByType(ITC_EVENT_TYPES.MIDDLEWARE_CHAINS_RESPONSE, ({ message }: any) => {
|
|
98
|
+
const resolve = pendingChainsRequests.get(message.requestId);
|
|
99
|
+
if (resolve) {
|
|
100
|
+
pendingChainsRequests.delete(message.requestId);
|
|
101
|
+
resolve(message.chains);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
chainsResponseListenerAttached = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// App HTTP middleware is only registered on worker threads, so when get_status runs on the main
|
|
108
|
+
// thread we ask an HTTP worker for its resolved chains (all workers register identically). Returns
|
|
109
|
+
// null if no worker answers within the timeout — mirrors queryWorkerForOpenApi in operationsServer.
|
|
110
|
+
function queryWorkerForMiddlewareChains(): Promise<MiddlewareChainsSummary | null> {
|
|
111
|
+
attachChainsResponseListener();
|
|
112
|
+
const requestId = nextChainsRequestId++;
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const timeoutHandle = setTimeout(() => {
|
|
115
|
+
pendingChainsRequests.delete(requestId);
|
|
116
|
+
resolve(null);
|
|
117
|
+
}, 5000);
|
|
118
|
+
pendingChainsRequests.set(requestId, (chains) => {
|
|
119
|
+
clearTimeout(timeoutHandle);
|
|
120
|
+
resolve(chains);
|
|
121
|
+
});
|
|
122
|
+
sendItcEvent({ type: ITC_EVENT_TYPES.MIDDLEWARE_CHAINS_REQUEST, message: { requestId } }).catch(() => {
|
|
123
|
+
clearTimeout(timeoutHandle);
|
|
124
|
+
pendingChainsRequests.delete(requestId);
|
|
125
|
+
resolve(null);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Introspect the resolved HTTP/upgrade/WebSocket middleware order (#1573). In a multi-worker
|
|
131
|
+
// deployment the app middleware is registered on the HTTP worker threads while the main thread
|
|
132
|
+
// carries only the operations-API middleware, so when an HTTP worker exists we fetch the chains from
|
|
133
|
+
// one over ITC. With no HTTP worker this thread is the app server itself (single-thread mode, or a
|
|
134
|
+
// worker serving the request) and reports locally. Job workers don't serve HTTP, so they're ignored.
|
|
135
|
+
async function getMiddlewareChains(): Promise<MiddlewareChainsSummary | null> {
|
|
136
|
+
try {
|
|
137
|
+
if (workers.some((worker: { name?: string }) => worker.name === THREAD_TYPES.HTTP))
|
|
138
|
+
return await queryWorkerForMiddlewareChains();
|
|
139
|
+
const { describeMiddlewareChains } = await import('../http.ts');
|
|
140
|
+
return describeMiddlewareChains();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
statusLogger.debug?.('getMiddlewareChains failed', error);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function getAllStatus(includeMiddleware = false): Promise<AllStatusSummary> {
|
|
82
148
|
statusLogger.debug?.('getAllStatus');
|
|
83
149
|
const statusRecords = getStatusTable().search([]);
|
|
84
150
|
|
|
@@ -94,17 +160,19 @@ async function getAllStatus(): Promise<AllStatusSummary> {
|
|
|
94
160
|
// Get restart flag status
|
|
95
161
|
const restartRequired = restartNeeded();
|
|
96
162
|
|
|
97
|
-
|
|
163
|
+
const summary: AllStatusSummary = {
|
|
98
164
|
systemStatus: statusRecords as Promise<AsyncIterable<StatusRecord>>,
|
|
99
165
|
componentStatus: componentStatusArray,
|
|
100
166
|
restartRequired,
|
|
101
167
|
};
|
|
168
|
+
if (includeMiddleware) summary.middlewareChains = await getMiddlewareChains();
|
|
169
|
+
return summary;
|
|
102
170
|
}
|
|
103
171
|
|
|
104
|
-
function getStatus({ id }: Partial<StatusRequestBody>): Promise<StatusRecord | AllStatusSummary> {
|
|
172
|
+
function getStatus({ id, middleware }: Partial<StatusRequestBody>): Promise<StatusRecord | AllStatusSummary> {
|
|
105
173
|
if (!id) {
|
|
106
174
|
statusLogger.debug?.('getStatus', 'all');
|
|
107
|
-
return getAllStatus();
|
|
175
|
+
return getAllStatus(middleware === true);
|
|
108
176
|
}
|
|
109
177
|
|
|
110
178
|
statusLogger.debug?.('getStatus', id);
|
|
@@ -136,6 +136,8 @@ module.exports = {
|
|
|
136
136
|
setTerminateTimeout,
|
|
137
137
|
extendShutdownDeadline,
|
|
138
138
|
restoreShutdownDeadline,
|
|
139
|
+
registerWorkerDataProvider,
|
|
140
|
+
onThreadExit,
|
|
139
141
|
restartNumber: workerData?.restartNumber || 1,
|
|
140
142
|
};
|
|
141
143
|
|
|
@@ -178,6 +180,59 @@ function setMainIsWorker(isWorker) {
|
|
|
178
180
|
module.exports.threadsHaveStarted();
|
|
179
181
|
}
|
|
180
182
|
let workerCount = 1; // should be assigned when workers are created
|
|
183
|
+
|
|
184
|
+
// Every workerData key core itself produces or consumes — providers may not collide with these.
|
|
185
|
+
// Covers the keys startWorker spreads below plus keys read elsewhere: `noServerStart` is set by
|
|
186
|
+
// the embedding entry point (index.ts) and read by threadServer.js to skip startServers(); a
|
|
187
|
+
// provider shadowing it would wedge HTTP worker startup.
|
|
188
|
+
const RESERVED_WORKER_DATA_KEYS = [
|
|
189
|
+
'addPorts',
|
|
190
|
+
'addThreadIds',
|
|
191
|
+
'workerIndex',
|
|
192
|
+
'workerCount',
|
|
193
|
+
'name',
|
|
194
|
+
'restartNumber',
|
|
195
|
+
'ticketKeys',
|
|
196
|
+
'noServerStart',
|
|
197
|
+
'__proto__', // never a legitimate payload name; spread would define it as an own property
|
|
198
|
+
];
|
|
199
|
+
const workerDataProviders = new Map();
|
|
200
|
+
/**
|
|
201
|
+
* Register a provider that contributes an extra `workerData` property to every worker spawned
|
|
202
|
+
* from this thread. `provider(options)` receives the startWorker options (`options.name` is the
|
|
203
|
+
* thread type, e.g. 'http' or 'job') and returns the value to place at `workerData[name]`, or
|
|
204
|
+
* undefined to skip that worker. Values must be structured-cloneable; a provider that throws or
|
|
205
|
+
* returns a non-cloneable value is logged and skipped so it can never break a spawn.
|
|
206
|
+
* Returns a function that unregisters the provider.
|
|
207
|
+
*/
|
|
208
|
+
function registerWorkerDataProvider(name, provider) {
|
|
209
|
+
if (RESERVED_WORKER_DATA_KEYS.includes(name) || workerDataProviders.has(name)) {
|
|
210
|
+
throw new Error(`workerData provider name '${name}' is already in use`);
|
|
211
|
+
}
|
|
212
|
+
if (typeof provider !== 'function') throw new Error('workerData provider must be a function');
|
|
213
|
+
workerDataProviders.set(name, provider);
|
|
214
|
+
return () => {
|
|
215
|
+
if (workerDataProviders.get(name) === provider) workerDataProviders.delete(name);
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function collectProvidedWorkerData(options) {
|
|
219
|
+
if (workerDataProviders.size === 0) return undefined;
|
|
220
|
+
let provided;
|
|
221
|
+
for (const [name, provider] of workerDataProviders) {
|
|
222
|
+
try {
|
|
223
|
+
const value = provider(options);
|
|
224
|
+
if (value === undefined) continue;
|
|
225
|
+
// Use the clone, not the original: this both pre-flights cloneability (so a bad value
|
|
226
|
+
// can't break the Worker spawn) and detaches the payload from accessor-backed objects
|
|
227
|
+
// or later mutation that could still throw inside new Worker(). Null prototype so a
|
|
228
|
+
// provider name can never collide with Object.prototype members.
|
|
229
|
+
(provided ??= Object.create(null))[name] = structuredClone(value);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
harperLogger.error(`workerData provider '${name}' failed and will be skipped for this worker:`, error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return provided;
|
|
235
|
+
}
|
|
181
236
|
let ticketKeys;
|
|
182
237
|
function getTicketKeys() {
|
|
183
238
|
if (ticketKeys) return ticketKeys;
|
|
@@ -212,6 +267,11 @@ listenersByType.set(hdbTerms.ITC_EVENT_TYPES.USER, null);
|
|
|
212
267
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.COMPONENT_STATUS_REQUEST, null);
|
|
213
268
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.RESOURCE_OPENAPI_REQUEST, null);
|
|
214
269
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.RESOURCE_OPENAPI_RESPONSE, null);
|
|
270
|
+
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.MIDDLEWARE_CHAINS_REQUEST, null);
|
|
271
|
+
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.MIDDLEWARE_CHAINS_RESPONSE, null);
|
|
272
|
+
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.OPERATION_REGISTERED, null);
|
|
273
|
+
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.OPERATION_EXECUTE_REQUEST, null);
|
|
274
|
+
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.OPERATION_EXECUTE_RESPONSE, null);
|
|
215
275
|
|
|
216
276
|
function startWorker(path, options = {}) {
|
|
217
277
|
// Take a percentage of total memory to determine the max memory for each thread. The percentage is based
|
|
@@ -281,6 +341,7 @@ function startWorker(path, options = {}) {
|
|
|
281
341
|
argv: process.argv.slice(2),
|
|
282
342
|
// pass these in synchronously to the worker so it has them on startup:
|
|
283
343
|
workerData: {
|
|
344
|
+
...collectProvidedWorkerData(options),
|
|
284
345
|
addPorts: portsToSend,
|
|
285
346
|
addThreadIds: channelsToConnect.map((channel) => channel.existingPort.threadId),
|
|
286
347
|
workerIndex: options.workerIndex,
|
|
@@ -380,10 +441,16 @@ async function restartWorkers(
|
|
|
380
441
|
// make a copy of the workers before iterating them, as the workers array mutates a lot during this
|
|
381
442
|
let waitingToFinish = []; // promises for workers we have shut down and are waiting to exit
|
|
382
443
|
// We can only start the replacement *before* the old worker releases its port when the OS lets
|
|
383
|
-
// both listen on the same port at once (SO_REUSEPORT). Without that — Windows (no SO_REUSEPORT)
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
|
|
444
|
+
// both listen on the same port at once (SO_REUSEPORT). Without that — Windows (no SO_REUSEPORT),
|
|
445
|
+
// macOS (unreliable SO_REUSEPORT, so workers bind exclusively), and Bun — the replacement can't
|
|
446
|
+
// bind a port the old worker still holds: its EADDRINUSE would be swallowed and the port left
|
|
447
|
+
// unbound once the old worker exits (this silently killed worker-owned listeners like MQTT on
|
|
448
|
+
// macOS after every component-reload restart). So there we keep the original ordering: shut the
|
|
449
|
+
// old worker down first (server.close() releases its ports immediately), then start the
|
|
450
|
+
// replacement — an unavoidable brief gap, but only for worker-owned listeners, since the main
|
|
451
|
+
// thread keeps serving the HTTP ports throughout. This ordering is also what lets
|
|
452
|
+
// listenOnPorts() treat a dedicated listener's EADDRINUSE as an external conflict.
|
|
453
|
+
const canPreStartReplacement = process.platform !== 'win32' && process.platform !== 'darwin' && !isBun;
|
|
387
454
|
for (let worker of workers.slice(0)) {
|
|
388
455
|
if ((name && worker.name !== name) || worker.wasShutdown) continue; // filter by type, if specified
|
|
389
456
|
const overlapping = OVERLAPPING_RESTART_TYPES.indexOf(worker.name) > -1;
|
|
@@ -766,10 +833,36 @@ if (parentPort && workerData?.addPorts) {
|
|
|
766
833
|
}
|
|
767
834
|
module.exports.getThreadInfo = getThreadInfo;
|
|
768
835
|
|
|
836
|
+
// Listeners notified when a connected thread's port closes (worker exit/restart), so
|
|
837
|
+
// modules holding per-thread state (e.g. registeredOperations' registry and in-flight
|
|
838
|
+
// forwards) can clean up.
|
|
839
|
+
const threadExitListeners = [];
|
|
840
|
+
function onThreadExit(listener) {
|
|
841
|
+
threadExitListeners.push(listener);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Thread ids already reported to threadExitListeners, so a dead worker is only reported once
|
|
845
|
+
// regardless of which of the two removal paths below observes it first. Node worker_threads
|
|
846
|
+
// ids are monotonically increasing and never reused within a process, so this never needs
|
|
847
|
+
// pruning (unbounded growth is one entry per worker restart over the process lifetime).
|
|
848
|
+
const notifiedDeadThreadIds = new Set();
|
|
849
|
+
function notifyThreadExit(deadThreadId) {
|
|
850
|
+
if (deadThreadId == null || notifiedDeadThreadIds.has(deadThreadId)) return;
|
|
851
|
+
notifiedDeadThreadIds.add(deadThreadId);
|
|
852
|
+
for (const listener of threadExitListeners) {
|
|
853
|
+
try {
|
|
854
|
+
listener(deadThreadId);
|
|
855
|
+
} catch (error) {
|
|
856
|
+
harperLogger.error(error);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
769
861
|
function removePort(port, deadThreadId) {
|
|
770
862
|
const idx = connectedPorts.indexOf(port);
|
|
771
863
|
if (idx === -1) return;
|
|
772
864
|
connectedPorts.splice(idx, 1);
|
|
865
|
+
if (deadThreadId != null) notifyThreadExit(deadThreadId);
|
|
773
866
|
// Notify remaining peers to remove this dead sibling port. In Bun, sibling
|
|
774
867
|
// MessagePorts don't emit 'close' when a peer worker exits, so we broadcast
|
|
775
868
|
// a REMOVE_PORT message from here (which fires reliably on Worker 'exit')
|
|
@@ -804,6 +897,11 @@ function addPort(port, keepRef, isJobWorker) {
|
|
|
804
897
|
} else if (message.type === REMOVE_PORT) {
|
|
805
898
|
const idx = connectedPorts.findIndex((p) => p.threadId === message.threadId);
|
|
806
899
|
if (idx !== -1) connectedPorts.splice(idx, 1);
|
|
900
|
+
// A sibling's port-to-the-dead-worker can close (and broadcast this) before this
|
|
901
|
+
// thread's OWN port to that worker fires its 'close'/'exit' — at which point
|
|
902
|
+
// removePort() would no-op (already spliced) and threadExitListeners would never
|
|
903
|
+
// fire. Notify here too; notifyThreadExit dedupes so it isn't reported twice.
|
|
904
|
+
notifyThreadExit(message.threadId);
|
|
807
905
|
} else {
|
|
808
906
|
notifyMessageListeners(message, port);
|
|
809
907
|
}
|
|
@@ -18,7 +18,7 @@ if (isMainThread) {
|
|
|
18
18
|
harperLogger.disableStdio();
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
|
-
console.error('uncaughtException', error);
|
|
21
|
+
console.error('uncaughtException', harperLogger.errorForLog(error));
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -30,6 +30,10 @@ export async function startHTTPThreads(threadCount = 2, dynamicThreads?: boolean
|
|
|
30
30
|
startTransactionLogCooling();
|
|
31
31
|
try {
|
|
32
32
|
if (dynamicThreads) {
|
|
33
|
+
// No caller currently passes dynamicThreads. If one ever does, note that the main thread
|
|
34
|
+
// does not bind ports in this mode, so on platforms without SO_REUSEPORT (macOS/Windows)
|
|
35
|
+
// worker 0's exclusive HTTP bind would silently swallow an external EADDRINUSE — the
|
|
36
|
+
// external-conflict detection in listenOnPorts() assumes the main thread binds first.
|
|
33
37
|
startHTTPWorker(0, 1);
|
|
34
38
|
} else {
|
|
35
39
|
const { loadRootComponents } = require('../loadRootComponents.js');
|