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/resources/Table.ts
CHANGED
|
@@ -140,6 +140,18 @@ function isFrozenRecordObject(value: any): boolean {
|
|
|
140
140
|
Object.isFrozen(value)
|
|
141
141
|
);
|
|
142
142
|
}
|
|
143
|
+
// Freeze a decoded record value for cache integrity, guarding the bare-TypedArray-root case:
|
|
144
|
+
// V8 throws "Cannot freeze array buffer views with elements" on Object.freeze of a non-empty
|
|
145
|
+
// TypedArray/DataView (#1298). _writeUpdate now rejects such roots on write, but this read-side
|
|
146
|
+
// guard still backstops records that bypass validation (source/cache population, replication of
|
|
147
|
+
// legacy data) and lets an already-poisoned table be read again after upgrade. The freeze is
|
|
148
|
+
// shallow anyway, so a typed-array root needs none. Only freeze plain objects: skip ArrayBuffer
|
|
149
|
+
// views/ArrayBuffers, and short-circuit primitives/null/undefined to avoid a needless native
|
|
150
|
+
// Object.freeze call on the hot read/scan path.
|
|
151
|
+
export function freezeRecord(value: any): void {
|
|
152
|
+
if (value !== null && typeof value === 'object' && !ArrayBuffer.isView(value) && !(value instanceof ArrayBuffer))
|
|
153
|
+
Object.freeze(value);
|
|
154
|
+
}
|
|
143
155
|
export const INVALIDATED = 1;
|
|
144
156
|
export const EVICTED = 8; // note that 2 is reserved for timestamps
|
|
145
157
|
const TEST_WRITE_KEY_BUFFER = Buffer.allocUnsafeSlow(8192);
|
|
@@ -203,6 +215,7 @@ export function makeTable(options) {
|
|
|
203
215
|
replicate,
|
|
204
216
|
description,
|
|
205
217
|
hidden,
|
|
218
|
+
cacheControl,
|
|
206
219
|
} = options;
|
|
207
220
|
let { expirationMS: expirationMs, evictionMS: evictionMs, audit, trackDeletes } = options;
|
|
208
221
|
evictionMs ??= 0;
|
|
@@ -306,6 +319,8 @@ export function makeTable(options) {
|
|
|
306
319
|
static description = description;
|
|
307
320
|
static properties = properties;
|
|
308
321
|
static hidden = hidden;
|
|
322
|
+
// default `Cache-Control` for anonymous REST reads (from `@table(cacheControl: "...")`), see REST.ts
|
|
323
|
+
static cacheControl = cacheControl;
|
|
309
324
|
static outputSchemas: { [verb: string]: JsonSchemaFragment } | undefined;
|
|
310
325
|
static mcp: { annotations?: { [verb: string]: any } } | undefined;
|
|
311
326
|
static replicate = replicate;
|
|
@@ -730,7 +745,7 @@ export function makeTable(options) {
|
|
|
730
745
|
// return 504 (rather than 404) if there is no content and the cache-control header
|
|
731
746
|
// dictates not to go to source
|
|
732
747
|
if (!this.doesExist()) throw new ServerError('Entry is not cached', 504);
|
|
733
|
-
if (hasSourceGet
|
|
748
|
+
if (hasSourceGet) setLoadedFromSource(target, false); // mark it as cached
|
|
734
749
|
} else if (resourceOptions?.ensureLoaded) {
|
|
735
750
|
const loadingFromSource = ensureLoadedFromSource(
|
|
736
751
|
(this.constructor as any).source,
|
|
@@ -746,7 +761,7 @@ export function makeTable(options) {
|
|
|
746
761
|
TableResource._updateResource(this, entry);
|
|
747
762
|
return this;
|
|
748
763
|
});
|
|
749
|
-
} else if (hasSourceGet) target
|
|
764
|
+
} else if (hasSourceGet) setLoadedFromSource(target, false); // mark it as cached
|
|
750
765
|
}
|
|
751
766
|
return this;
|
|
752
767
|
}
|
|
@@ -1212,7 +1227,12 @@ export function makeTable(options) {
|
|
|
1212
1227
|
let allowed = true;
|
|
1213
1228
|
if ((target as any)?.checkPermission) {
|
|
1214
1229
|
// requesting authorization verification
|
|
1215
|
-
|
|
1230
|
+
try {
|
|
1231
|
+
allowed = this.allowRead(context.user, target, context);
|
|
1232
|
+
} catch {
|
|
1233
|
+
// allow* threw — fail closed rather than letting the request proceed
|
|
1234
|
+
throw new AccessViolation(context.user);
|
|
1235
|
+
}
|
|
1216
1236
|
}
|
|
1217
1237
|
return promiseNormalize(
|
|
1218
1238
|
when(
|
|
@@ -1227,6 +1247,7 @@ export function makeTable(options) {
|
|
|
1227
1247
|
// return 504 (rather than 404) if there is no content and the cache-control header
|
|
1228
1248
|
// dictates not to go to source
|
|
1229
1249
|
if (!entry?.value) throw new ServerError('Entry is not cached', 504);
|
|
1250
|
+
if (hasSourceGet) setLoadedFromSource(target, false); // mark it as cached
|
|
1230
1251
|
} else if (ensureLoaded) {
|
|
1231
1252
|
const loadingFromSource = ensureLoadedFromSource(
|
|
1232
1253
|
constructor.source,
|
|
@@ -1239,7 +1260,7 @@ export function makeTable(options) {
|
|
|
1239
1260
|
if (loadingFromSource) {
|
|
1240
1261
|
txn?.disregardReadTxn(); // this could take some time, so don't keep the transaction open if possible
|
|
1241
1262
|
return loadingFromSource.then((entry) => entry?.value);
|
|
1242
|
-
}
|
|
1263
|
+
} else if (hasSourceGet) setLoadedFromSource(target, false); // mark it as cached
|
|
1243
1264
|
}
|
|
1244
1265
|
return entry?.value;
|
|
1245
1266
|
});
|
|
@@ -1303,7 +1324,10 @@ export function makeTable(options) {
|
|
|
1303
1324
|
if (!property.name) property = { name: property };
|
|
1304
1325
|
if (!property.checkPermission && (target as any).checkPermission)
|
|
1305
1326
|
property.checkPermission = (target as any).checkPermission;
|
|
1306
|
-
|
|
1327
|
+
// Invoke the related table's allowRead on a properly-bound instance rather than
|
|
1328
|
+
// `.call(null, ...)` so `this` is a valid resource of the related type.
|
|
1329
|
+
const relatedResource = new relatedTable(undefined, context);
|
|
1330
|
+
if (!relatedResource.allowRead(user, property, context)) return false;
|
|
1307
1331
|
if (!property.select) return property.name; // no select was applied, just return the name
|
|
1308
1332
|
}
|
|
1309
1333
|
return property;
|
|
@@ -1895,6 +1919,33 @@ export function makeTable(options) {
|
|
|
1895
1919
|
if (fullUpdate || (recordUpdate && hasChanges(this.#changes === recordUpdate ? this : recordUpdate))) {
|
|
1896
1920
|
if (!(context as any)?.source) {
|
|
1897
1921
|
transaction.checkOverloaded();
|
|
1922
|
+
// A record must be a plain object. Reject primitive, string/number, bare-binary,
|
|
1923
|
+
// and bare-array roots — e.g. a raw Buffer from an application/octet-stream PUT, a
|
|
1924
|
+
// JSON string/number body, or a top-level JSON array. Such roots carry no primary
|
|
1925
|
+
// key or attributes, are meaningless to SQL/get-attributes, and a bare TypedArray
|
|
1926
|
+
// root additionally throws on Object.freeze during scans (#1298). Binary data belongs
|
|
1927
|
+
// in a Bytes/Blob attribute. (Messages go through _writePublish, not here, so raw
|
|
1928
|
+
// publish payloads are unaffected.)
|
|
1929
|
+
const isBinaryRoot = ArrayBuffer.isView(recordUpdate) || recordUpdate instanceof ArrayBuffer;
|
|
1930
|
+
if (
|
|
1931
|
+
recordUpdate === null ||
|
|
1932
|
+
typeof recordUpdate !== 'object' ||
|
|
1933
|
+
isBinaryRoot ||
|
|
1934
|
+
Array.isArray(recordUpdate)
|
|
1935
|
+
) {
|
|
1936
|
+
// Avoid dumping every byte of a large binary body or huge payload into the error.
|
|
1937
|
+
let received: string;
|
|
1938
|
+
if (isBinaryRoot) {
|
|
1939
|
+
received = `${recordUpdate.constructor?.name ?? 'binary'} of ${recordUpdate.byteLength} bytes`;
|
|
1940
|
+
} else {
|
|
1941
|
+
const full = stringify(recordUpdate) ?? typeof recordUpdate;
|
|
1942
|
+
received = full.length > 200 ? full.slice(0, 200) + '...' : full;
|
|
1943
|
+
}
|
|
1944
|
+
throw new ClientError(
|
|
1945
|
+
`A record must be an object, but received ${received}. To store binary data, put it ` +
|
|
1946
|
+
`in a Bytes or Blob attribute (e.g. a record like { "${primaryKey ?? 'id'}": …, "data": <bytes> }).`
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1898
1949
|
// Records are intentionally immutable: decoded records are frozen (and 5.2 record
|
|
1899
1950
|
// caching relies on it), so mutating in place would corrupt cached/shared state.
|
|
1900
1951
|
// validate() coerces values and we stamp created/updated times + the primary key
|
|
@@ -1946,6 +1997,16 @@ export function makeTable(options) {
|
|
|
1946
1997
|
},
|
|
1947
1998
|
before: writeToSource(),
|
|
1948
1999
|
commit: (txnTime: number, existingEntry: Entry, retry: boolean, transaction: any) => {
|
|
2000
|
+
// Whether a prior attempt of THIS write appended its own audit entry (sticky, set in
|
|
2001
|
+
// save(); log entries are not part of the aborted rocks transaction, so they survive).
|
|
2002
|
+
// Only such a write can find its own orphaned entry in the dedup lookups below and must
|
|
2003
|
+
// not treat it as "already applied". Per-write on purpose: the transaction-wide retry
|
|
2004
|
+
// flag would also suppress dedup for a genuine re-delivered duplicate co-batched with
|
|
2005
|
+
// the conflicting write (double-applying it) and for fresh writes staged through a
|
|
2006
|
+
// reused transaction whose retries counter is stale. Sticky on purpose: a proxy read
|
|
2007
|
+
// from the last attempt's skipped state launders when a recommit round self-skips
|
|
2008
|
+
// (walk identity tie against its own staged record) before a fresh-transaction replay.
|
|
2009
|
+
const stagedOwnAuditEntry = retry && write.appendedAuditEntry === true;
|
|
1949
2010
|
write.skipped = false; // reset on each retry; cleanup happens after commit if still true
|
|
1950
2011
|
if (retry) {
|
|
1951
2012
|
if (context && existingEntry?.version > (context.lastModified || 0))
|
|
@@ -2066,8 +2127,12 @@ export function makeTable(options) {
|
|
|
2066
2127
|
// local audit time, not version, so this version-keyed lookup doesn't apply there (LMDB keeps the
|
|
2067
2128
|
// exact unbounded walk). A miss (the keyed lookup can lag a back-to-back re-delivery — #1137)
|
|
2068
2129
|
// simply falls through to the walk, so this never changes correctness; the additionalAuditRefs
|
|
2069
|
-
// check above remains the read-your-writes guard.
|
|
2070
|
-
|
|
2130
|
+
// check above remains the read-your-writes guard. Never when this write staged in a prior
|
|
2131
|
+
// failed attempt: that attempt already appended this write's own audit entry, so the lookup
|
|
2132
|
+
// would find it and skip the write as "already applied" when the record was never committed.
|
|
2133
|
+
// A recommit of the same transaction survived that skip only because the old write batch
|
|
2134
|
+
// still carried the put; a fresh-transaction replay (ERR_TRY_AGAIN) would drop the write.
|
|
2135
|
+
if (isRocksDB && !stagedOwnAuditEntry && dedupVersionCouldBeRetained(txnTime)) {
|
|
2071
2136
|
const priorAudit = auditStore.get(txnTime, tableId, id, options?.nodeId);
|
|
2072
2137
|
if (
|
|
2073
2138
|
priorAudit &&
|
|
@@ -2126,7 +2191,11 @@ export function makeTable(options) {
|
|
|
2126
2191
|
// A re-delivered write whose exact (version, nodeId) is already in the audit log was already
|
|
2127
2192
|
// applied; drop it rather than re-applying it (double-applying commutative ops) or writing a
|
|
2128
2193
|
// duplicate audit-only record. Used by the early-out and the depth-cap block below.
|
|
2194
|
+
// Never a duplicate for a write that staged in a prior failed attempt: that attempt already
|
|
2195
|
+
// appended this write's own audit entry, so the lookup would match it while the record was
|
|
2196
|
+
// never committed (see the up-front keyed dedup above).
|
|
2129
2197
|
const isReDeliveredDuplicate = () => {
|
|
2198
|
+
if (stagedOwnAuditEntry) return false;
|
|
2130
2199
|
if (!dedupVersionCouldBeRetained(txnTime)) return false; // pre-retention version — skip the end-of-log scan (best-effort; see above)
|
|
2131
2200
|
const duplicate = auditStore.get(txnTime, tableId, id, options?.nodeId);
|
|
2132
2201
|
return (
|
|
@@ -2573,7 +2642,13 @@ export function makeTable(options) {
|
|
|
2573
2642
|
if (target.parseError) throw target.parseError; // if there was a parse error, we can throw it now
|
|
2574
2643
|
if (target.checkPermission) {
|
|
2575
2644
|
// requesting authorization verification
|
|
2576
|
-
|
|
2645
|
+
let allowed;
|
|
2646
|
+
try {
|
|
2647
|
+
allowed = this.allowRead((context as any).user, target, context);
|
|
2648
|
+
} catch {
|
|
2649
|
+
// allow* threw — fail closed rather than letting the request proceed
|
|
2650
|
+
throw new AccessViolation((context as any).user);
|
|
2651
|
+
}
|
|
2577
2652
|
if (!allowed) {
|
|
2578
2653
|
throw new AccessViolation((context as any).user);
|
|
2579
2654
|
}
|
|
@@ -2775,7 +2850,21 @@ export function makeTable(options) {
|
|
|
2775
2850
|
// transaction, and we really don't care if the
|
|
2776
2851
|
// counts are done in the same read transaction because they are just estimates) until the search
|
|
2777
2852
|
// results have been iterated and finished.
|
|
2778
|
-
|
|
2853
|
+
// When the query opts out of a snapshot (`snapshot: false`, e.g. long-running analytics
|
|
2854
|
+
// scans), the read transaction reads against the latest committed data without pinning a
|
|
2855
|
+
// consistent snapshot, so the scan doesn't hold a snapshot that blocks compaction.
|
|
2856
|
+
const readTxn = txn.useReadTxn(target.snapshot === false);
|
|
2857
|
+
// Record-level read guard (#1241): a resource may define a static allowReadRecord(user, record)
|
|
2858
|
+
// that participates in query execution (pushed into HNSW traversal for vector sorts, applied as a
|
|
2859
|
+
// post-filter otherwise). Resolve off the actual (possibly subclassed) constructor so overrides win.
|
|
2860
|
+
// SCOPE: this is a QUERY-result filter, not a general read-authorization boundary — direct
|
|
2861
|
+
// single-record get(id) does not consult it (use allowRead/instance hooks for that). It must be
|
|
2862
|
+
// synchronous, side-effect free, and fast; it can run once per candidate record during traversal.
|
|
2863
|
+
const allowReadRecord = (this.constructor as any).allowReadRecord;
|
|
2864
|
+
const recordAccess =
|
|
2865
|
+
typeof allowReadRecord === 'function' || typeof target.vectorFilter === 'function'
|
|
2866
|
+
? { allowReadRecord, user: (context as any)?.user, vectorFilter: target.vectorFilter }
|
|
2867
|
+
: undefined;
|
|
2779
2868
|
const entries = executeConditions(
|
|
2780
2869
|
conditions,
|
|
2781
2870
|
operator,
|
|
@@ -2784,16 +2873,28 @@ export function makeTable(options) {
|
|
|
2784
2873
|
target,
|
|
2785
2874
|
context,
|
|
2786
2875
|
(results: any[], filters: Function[]) => transformToEntries(results, select, context, readTxn, filters),
|
|
2787
|
-
filtered
|
|
2876
|
+
filtered,
|
|
2877
|
+
recordAccess
|
|
2788
2878
|
);
|
|
2789
2879
|
const ensure_loaded = (target as any).ensureLoaded !== false;
|
|
2880
|
+
// Authoritative RBAC enforcement (#1241): the guards inside executeConditions evaluate the LOCAL
|
|
2881
|
+
// record, but on a caching table transformEntryForSelect may then revalidate an expired/invalidated
|
|
2882
|
+
// row from source and return a DIFFERENT record. An authorization check must hold on the record
|
|
2883
|
+
// actually returned, so allowReadRecord is re-checked there, after materialization (the earlier
|
|
2884
|
+
// evaluation stays as a prune that also bounds HNSW traversal). vectorFilter and condition filters
|
|
2885
|
+
// intentionally keep the local-record semantics all query filters have on caching tables.
|
|
2886
|
+
const recordGuard =
|
|
2887
|
+
typeof allowReadRecord === 'function'
|
|
2888
|
+
? (record: any) => allowReadRecord((context as any)?.user, record)
|
|
2889
|
+
: undefined;
|
|
2790
2890
|
const transformToRecord = TableResource.transformEntryForSelect(
|
|
2791
2891
|
select,
|
|
2792
2892
|
context,
|
|
2793
2893
|
readTxn,
|
|
2794
2894
|
filtered,
|
|
2795
2895
|
ensure_loaded,
|
|
2796
|
-
true
|
|
2896
|
+
true,
|
|
2897
|
+
recordGuard
|
|
2797
2898
|
);
|
|
2798
2899
|
let results = TableResource.transformToOrderedSelect(
|
|
2799
2900
|
entries,
|
|
@@ -3004,9 +3105,12 @@ export function makeTable(options) {
|
|
|
3004
3105
|
* @param filtered
|
|
3005
3106
|
* @param ensure_loaded
|
|
3006
3107
|
* @param canSkip
|
|
3108
|
+
* @param recordGuard record-level read check (#1241) applied to the record actually being
|
|
3109
|
+
* returned — i.e. AFTER any caching-source revalidation replaces a stale local copy — so an
|
|
3110
|
+
* authorization verdict can't be made on bytes that differ from what the caller receives.
|
|
3007
3111
|
* @returns
|
|
3008
3112
|
*/
|
|
3009
|
-
static transformEntryForSelect(select, context, readTxn, filtered, ensure_loaded?, canSkip?) {
|
|
3113
|
+
static transformEntryForSelect(select, context, readTxn, filtered, ensure_loaded?, canSkip?, recordGuard?) {
|
|
3010
3114
|
let checkLoaded;
|
|
3011
3115
|
if (
|
|
3012
3116
|
ensure_loaded &&
|
|
@@ -3025,6 +3129,7 @@ export function makeTable(options) {
|
|
|
3025
3129
|
}
|
|
3026
3130
|
let transformCache;
|
|
3027
3131
|
const source = this.source;
|
|
3132
|
+
const resourceClass = this;
|
|
3028
3133
|
// Transform an entry to a record. Note that *this* instance is intended to be the iterator.
|
|
3029
3134
|
const transform = function (entry: Entry) {
|
|
3030
3135
|
let record;
|
|
@@ -3062,13 +3167,33 @@ export function makeTable(options) {
|
|
|
3062
3167
|
message: 'This entry has expired',
|
|
3063
3168
|
};
|
|
3064
3169
|
}
|
|
3065
|
-
|
|
3170
|
+
// Stale-while-revalidate is an instance method, but a query has no per-row resource
|
|
3171
|
+
// instance to consult (the single-record `get` path passes `this`). Construct one for this
|
|
3172
|
+
// row — via the same `new constructor(id, context)` the framework's getResource uses — so
|
|
3173
|
+
// the hook sees the current row's identity (this.getId()) and record state, matching the
|
|
3174
|
+
// single-record path. It must be a real instance, not the bare class prototype: every
|
|
3175
|
+
// resource prototype chain ends in a tracked-property Proxy, so reading an absent property
|
|
3176
|
+
// (probing for an undefined `allowStaleWhileRevalidate`, or a hook touching `this.x`) on a
|
|
3177
|
+
// non-instance invokes getChanges() with no backing state and throws (harper#1578). We also
|
|
3178
|
+
// load the stale entry into it (as the single-record path does via _updateResource) so a
|
|
3179
|
+
// hook consulting this.getRecord()/this.<field> sees the stale row, not undefined. `entry`
|
|
3180
|
+
// here may be lazy (its `.value` is a GC-able deref, undefined once collected), so we set the
|
|
3181
|
+
// already-dereferenced `record` explicitly rather than relying on `entry.value`. This runs
|
|
3182
|
+
// only for the expired/invalidated rows already headed to source, so the cost is negligible.
|
|
3183
|
+
const swrResource = new resourceClass(entry.key ?? entry, context);
|
|
3184
|
+
resourceClass._updateResource(swrResource, entry);
|
|
3185
|
+
swrResource.setRecord(record);
|
|
3186
|
+
const loadingFromSource = ensureLoadedFromSource(source, entry.key ?? entry, entry, context, swrResource);
|
|
3066
3187
|
if (loadingFromSource?.then) {
|
|
3067
3188
|
return loadingFromSource.then(transform);
|
|
3068
3189
|
}
|
|
3069
3190
|
}
|
|
3070
3191
|
}
|
|
3071
3192
|
if (record == null) return canSkip ? SKIP : record;
|
|
3193
|
+
// Record-level RBAC (#1241): enforced here because `record` is now the final, materialized
|
|
3194
|
+
// record — a caching table's source revalidation (above) may have replaced the local copy the
|
|
3195
|
+
// query filters evaluated. Fail closed on the record actually being returned.
|
|
3196
|
+
if (recordGuard && !recordGuard(record)) return canSkip ? SKIP : undefined;
|
|
3072
3197
|
if (select && !(select[0] === '*' && select.length === 1)) {
|
|
3073
3198
|
let promises: Promise<any>[];
|
|
3074
3199
|
const selectAttribute = (attribute, callback) => {
|
|
@@ -3233,6 +3358,9 @@ export function makeTable(options) {
|
|
|
3233
3358
|
// snapshot:false, which catches any commits that land during yield points; dropping in the
|
|
3234
3359
|
// listener avoids duplicate delivery.
|
|
3235
3360
|
let dropDuringReplay = false;
|
|
3361
|
+
// Coalescing guards for the reload re-snapshot (harper-pro#495), driven from the listener below.
|
|
3362
|
+
let reloadResnapshotRunning = false;
|
|
3363
|
+
let reloadResnapshotPending = false;
|
|
3236
3364
|
const thisId = requestTargetToId(request) ?? null; // treat undefined and null as the root
|
|
3237
3365
|
const subscription = addSubscription(
|
|
3238
3366
|
TableResource,
|
|
@@ -3246,9 +3374,15 @@ export function makeTable(options) {
|
|
|
3246
3374
|
// we only send the full message, this are individual messages that can be sent out of order
|
|
3247
3375
|
// TODO: Do we want to have a limit to how far out-of-order we are willing to send?
|
|
3248
3376
|
value = auditRecord.getValue?.(primaryStore, getFullRecord);
|
|
3249
|
-
} else if (type
|
|
3250
|
-
//
|
|
3251
|
-
//
|
|
3377
|
+
} else if (type === 'reload') {
|
|
3378
|
+
// Whole-table marker with a null id and no record (harper-pro#489): a copyApply base copy
|
|
3379
|
+
// back-filled rows with no per-row audit events. For a user table, re-deliver the current
|
|
3380
|
+
// scope as 'put's so MQTT/SSE/WS subscribers recover the snapshotted records, and do NOT
|
|
3381
|
+
// forward the bare marker (harper-pro#495). System-DB subscribers (knownNodes etc.) instead
|
|
3382
|
+
// get the raw marker and run their own bespoke whole-table rescan.
|
|
3383
|
+
if (databaseName !== 'system') return scheduleReloadResnapshot();
|
|
3384
|
+
// system DB: fall through to forward the bare 'reload' event verbatim.
|
|
3385
|
+
} else if (type !== 'end_txn') {
|
|
3252
3386
|
// these are events that indicate that the primary record has changed. I believe we always want to simply
|
|
3253
3387
|
// send the latest value. Note that it is fine to synchronously access these records, they should have just
|
|
3254
3388
|
// been written, so are fresh in memory.
|
|
@@ -3369,7 +3503,7 @@ export function makeTable(options) {
|
|
|
3369
3503
|
logger.error?.('Error getting history entry', auditRecord.localTime, error);
|
|
3370
3504
|
}
|
|
3371
3505
|
}
|
|
3372
|
-
for (let i = history.length; i > 0;
|
|
3506
|
+
for (let i = history.length; i > 0;) {
|
|
3373
3507
|
send(history[--i]);
|
|
3374
3508
|
}
|
|
3375
3509
|
// Use the latest record cursor saw (history[0] = most recent due to reverse
|
|
@@ -3478,7 +3612,7 @@ export function makeTable(options) {
|
|
|
3478
3612
|
} else break;
|
|
3479
3613
|
if (count) count--;
|
|
3480
3614
|
} while (nextTime > startTime && count !== 0);
|
|
3481
|
-
for (let i = history.length; i > 0;
|
|
3615
|
+
for (let i = history.length; i > 0;) {
|
|
3482
3616
|
send(history[--i]);
|
|
3483
3617
|
}
|
|
3484
3618
|
}
|
|
@@ -3509,6 +3643,78 @@ export function makeTable(options) {
|
|
|
3509
3643
|
}
|
|
3510
3644
|
subscription.send(event);
|
|
3511
3645
|
}
|
|
3646
|
+
// #region reload re-snapshot (harper-pro#495)
|
|
3647
|
+
// A copyApply base copy back-fills rows as snapshots with NO per-row audit entries, so the live
|
|
3648
|
+
// listener above never fires for them and an already-connected subscriber would miss them until
|
|
3649
|
+
// the next direct write. After the copy, a whole-table 'reload' marker is delivered here (id=null).
|
|
3650
|
+
// For a user table we react by re-delivering the subscription's current scope as ordinary 'put'
|
|
3651
|
+
// events — so EVERY consumer that funnels through subscribe() (MQTT, SSE, WS) recovers the records
|
|
3652
|
+
// uniformly, with no per-protocol handling. System-DB reloads are NOT re-snapshotted: their
|
|
3653
|
+
// subscribers (knownNodes peer-discovery, hdb_certificate CA install) run a bespoke whole-table
|
|
3654
|
+
// rescan off the raw marker, which a per-row re-emit cannot express (it can't drop stale rows).
|
|
3655
|
+
// Yields the latest committed value (snapshot:false) and skips tombstones, mirroring the
|
|
3656
|
+
// omitCurrent initial-snapshot scan.
|
|
3657
|
+
async function* currentScopeRecords() {
|
|
3658
|
+
const isCollection = request.isCollection ?? thisId == null;
|
|
3659
|
+
if (isCollection) {
|
|
3660
|
+
let sinceYield = 0;
|
|
3661
|
+
for (const { key: id, value, version, localTime, size } of primaryStore.getRange({
|
|
3662
|
+
start: thisId ?? false,
|
|
3663
|
+
end: thisId == null ? undefined : [thisId, MAXIMUM_KEY],
|
|
3664
|
+
versions: true,
|
|
3665
|
+
snapshot: false, // no need for a snapshot, just want the latest data
|
|
3666
|
+
})) {
|
|
3667
|
+
if (++sinceYield >= REPLAY_YIELD_INTERVAL) {
|
|
3668
|
+
sinceYield = 0;
|
|
3669
|
+
await rest();
|
|
3670
|
+
}
|
|
3671
|
+
if (!value) continue; // skip tombstones
|
|
3672
|
+
yield { id, localTime, value, version, type: 'put', size };
|
|
3673
|
+
}
|
|
3674
|
+
} else {
|
|
3675
|
+
const entry = primaryStore.getEntry(thisId);
|
|
3676
|
+
if (entry?.value) yield { id: thisId, ...entry, type: 'put' };
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
// Drain the current scope into the subscription with the same back-pressure as the live path.
|
|
3680
|
+
// Coalesced: a marker that arrives while a re-snapshot is running just re-arms it once more (so
|
|
3681
|
+
// markers for several tables, or a marker landing mid-scan, are not lost), and we never run two
|
|
3682
|
+
// scans concurrently. The first thing it does is `await rest()` (a setImmediate macrotask), so the
|
|
3683
|
+
// scan never executes inside the synchronous broadcast listener — which on the same-thread
|
|
3684
|
+
// aftercommit path holds an inter-thread lock that must not span event-loop turns.
|
|
3685
|
+
async function runReloadResnapshot() {
|
|
3686
|
+
reloadResnapshotRunning = true;
|
|
3687
|
+
try {
|
|
3688
|
+
await rest(); // defer off the broadcast listener's stack before scanning
|
|
3689
|
+
while (reloadResnapshotPending) {
|
|
3690
|
+
reloadResnapshotPending = false;
|
|
3691
|
+
// Subscription.end() nulls `subscriptions`; bail the moment it closes — before, between,
|
|
3692
|
+
// or mid-scan — so we never scan + send into a dead queue. pending is already cleared, so
|
|
3693
|
+
// the finally re-arm won't re-loop.
|
|
3694
|
+
if (!subscription.subscriptions) return;
|
|
3695
|
+
for await (const record of currentScopeRecords()) {
|
|
3696
|
+
if (!subscription.subscriptions) return;
|
|
3697
|
+
send(record);
|
|
3698
|
+
if (subscription.queue?.length > EVENT_HIGH_WATER_MARK) {
|
|
3699
|
+
if ((await subscription.waitForDrain()) === false) return;
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
} catch (error) {
|
|
3704
|
+
harperLogger.error?.('Error in reload re-snapshot:', error);
|
|
3705
|
+
} finally {
|
|
3706
|
+
reloadResnapshotRunning = false;
|
|
3707
|
+
// A marker that landed after the last pending-check but before we cleared the flag would
|
|
3708
|
+
// otherwise be dropped — re-arm if so (unless the subscription has since closed).
|
|
3709
|
+
if (subscription.subscriptions && reloadResnapshotPending) scheduleReloadResnapshot();
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
function scheduleReloadResnapshot() {
|
|
3713
|
+
if (!subscription.subscriptions) return;
|
|
3714
|
+
reloadResnapshotPending = true;
|
|
3715
|
+
if (!reloadResnapshotRunning) runReloadResnapshot();
|
|
3716
|
+
}
|
|
3717
|
+
// #endregion
|
|
3512
3718
|
return subscription;
|
|
3513
3719
|
}
|
|
3514
3720
|
|
|
@@ -3704,12 +3910,10 @@ export function makeTable(options) {
|
|
|
3704
3910
|
);
|
|
3705
3911
|
break;
|
|
3706
3912
|
case 'ID':
|
|
3707
|
-
if (
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
)
|
|
3712
|
-
)
|
|
3913
|
+
if (!(
|
|
3914
|
+
typeof value === 'string' ||
|
|
3915
|
+
(value?.length > 0 && value.every?.((value) => typeof value === 'string'))
|
|
3916
|
+
))
|
|
3713
3917
|
(validationErrors || (validationErrors = [])).push(
|
|
3714
3918
|
`Value ${stringify(value)} in property ${name} must be a string, or an array of strings`
|
|
3715
3919
|
);
|
|
@@ -4054,7 +4258,7 @@ export function makeTable(options) {
|
|
|
4054
4258
|
});
|
|
4055
4259
|
if (value?.then) hasPromises = true;
|
|
4056
4260
|
// for now, we shouldn't be getting promises until rocksdb
|
|
4057
|
-
if (TableResource.loadAsInstance === false)
|
|
4261
|
+
if (TableResource.loadAsInstance === false) freezeRecord(returnEntry ? value?.value : value);
|
|
4058
4262
|
return value;
|
|
4059
4263
|
});
|
|
4060
4264
|
return relationship.filterMissing
|
|
@@ -4069,7 +4273,7 @@ export function makeTable(options) {
|
|
|
4069
4273
|
transaction: txnForContext(context).getReadTxn(),
|
|
4070
4274
|
});
|
|
4071
4275
|
// for now, we shouldn't be getting promises until rocksdb
|
|
4072
|
-
if (TableResource.loadAsInstance === false)
|
|
4276
|
+
if (TableResource.loadAsInstance === false) freezeRecord(returnEntry ? value?.value : value);
|
|
4073
4277
|
return value;
|
|
4074
4278
|
};
|
|
4075
4279
|
attribute.set = (object, related) => {
|
|
@@ -4440,7 +4644,7 @@ export function makeTable(options) {
|
|
|
4440
4644
|
// we need to freeze entry records to ensure the integrity of the cache;
|
|
4441
4645
|
// but we only do this when users have opted into loadAsInstance/freezeRecords to avoid back-compat
|
|
4442
4646
|
// issues
|
|
4443
|
-
|
|
4647
|
+
freezeRecord(entry?.value);
|
|
4444
4648
|
if (
|
|
4445
4649
|
entry?.residencyId &&
|
|
4446
4650
|
entry.metadataFlags & INVALIDATED &&
|
|
@@ -4557,6 +4761,11 @@ export function makeTable(options) {
|
|
|
4557
4761
|
}
|
|
4558
4762
|
}
|
|
4559
4763
|
|
|
4764
|
+
function setLoadedFromSource(target: RequestTarget | undefined, loadedFromSource: boolean) {
|
|
4765
|
+
// cache disposition is a per-get result, recorded on the RequestTarget of the get (#1576)
|
|
4766
|
+
// target may be a primitive id on instance-API calls, which can't hold the flag
|
|
4767
|
+
if (target && typeof target === 'object') target.loadedFromSource = loadedFromSource;
|
|
4768
|
+
}
|
|
4560
4769
|
function ensureLoadedFromSource(source: typeof TableResource, id, entry, context, resource?, target?) {
|
|
4561
4770
|
if (context?.onlyIfCached) {
|
|
4562
4771
|
if (!entry?.value) throw new ServerError('Entry is not cached', 504);
|
|
@@ -4789,7 +4998,7 @@ export function makeTable(options) {
|
|
|
4789
4998
|
whenResolved(getFromSource(source, id, primaryStore.getEntry(id), context, target));
|
|
4790
4999
|
else {
|
|
4791
5000
|
// served from cache after waiting for another request to resolve
|
|
4792
|
-
|
|
5001
|
+
setLoadedFromSource(target, false);
|
|
4793
5002
|
whenResolved(entry);
|
|
4794
5003
|
}
|
|
4795
5004
|
};
|
|
@@ -4804,7 +5013,7 @@ export function makeTable(options) {
|
|
|
4804
5013
|
});
|
|
4805
5014
|
}
|
|
4806
5015
|
// lock acquired — this request will actually load from source
|
|
4807
|
-
|
|
5016
|
+
setLoadedFromSource(target, true);
|
|
4808
5017
|
|
|
4809
5018
|
const existingRecord = existingEntry?.value;
|
|
4810
5019
|
// it is important to remember that this is _NOT_ part of the current transaction; nothing is changing
|
|
@@ -192,52 +192,47 @@ interface GetAnalyticsOpts {
|
|
|
192
192
|
|
|
193
193
|
export async function get(metric: string, opts?: GetAnalyticsOpts): Promise<Metric[]> {
|
|
194
194
|
const { getAttributes, startTime, endTime, additionalConditions, log: logName } = opts ?? {};
|
|
195
|
-
|
|
196
|
-
// A two-sided time window bounds the primary-key (`id` = [time, nodeId]) range on both ends. When we
|
|
197
|
-
// have one, lead with it and pin execution order (below) so it drives the scan. `Table.search`
|
|
198
|
-
// otherwise reorders conditions by estimated cost, but the planner estimates a primary-key `between`
|
|
199
|
-
// as a flat 10% of the table (search.ts BETWEEN_ESTIMATE) rather than from the actual range width —
|
|
200
|
-
// so on a large `hdb_analytics` table a narrow window looks costlier than `metric equals`, iteration
|
|
201
|
-
// is driven off the metric index, and the metric's entire history is decoded instead of the window
|
|
202
|
-
// (#1796). The proper long-term fix is a range-width-aware cost estimate in search.ts.
|
|
203
|
-
const boundedWindow = startTime != null && endTime != null;
|
|
204
|
-
const conditions: Conditions = [];
|
|
205
|
-
if (boundedWindow) {
|
|
206
|
-
conditions.push({ attribute: 'id', comparator: 'between', value: [startTime, endTime] });
|
|
207
|
-
}
|
|
208
|
-
conditions.push({ attribute: 'metric', comparator: 'equals', value: metric });
|
|
195
|
+
const conditions: Conditions = [{ attribute: 'metric', comparator: 'equals', value: metric }];
|
|
209
196
|
if (logName !== undefined) {
|
|
210
197
|
conditions.push({ attribute: 'log', comparator: 'equals', value: logName });
|
|
211
198
|
}
|
|
212
199
|
if (additionalConditions) {
|
|
213
200
|
conditions.push(...additionalConditions.map(conformCondition));
|
|
214
201
|
}
|
|
215
|
-
// An open-ended range (only start or only end) can span the whole table, so we append it and leave
|
|
216
|
-
// ordering to the planner rather than forcing an unbounded range to drive — which could scan the
|
|
217
|
-
// full table for a selective metric.
|
|
218
|
-
if (!boundedWindow) {
|
|
219
|
-
if (startTime != null) {
|
|
220
|
-
conditions.push({ attribute: 'id', comparator: 'greater_than_equal', value: startTime });
|
|
221
|
-
}
|
|
222
|
-
if (endTime != null) {
|
|
223
|
-
conditions.push({ attribute: 'id', comparator: 'less_than', value: endTime });
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
202
|
const select = getAttributes ?? [];
|
|
228
203
|
|
|
229
|
-
// ensure we're always selecting id
|
|
204
|
+
// ensure we're always selecting id
|
|
230
205
|
if (!isSelected(select, 'id')) {
|
|
231
206
|
select.push('id');
|
|
232
207
|
}
|
|
233
208
|
|
|
209
|
+
if (startTime && endTime) {
|
|
210
|
+
conditions.push({
|
|
211
|
+
attribute: 'id',
|
|
212
|
+
comparator: 'between',
|
|
213
|
+
value: [startTime, endTime],
|
|
214
|
+
});
|
|
215
|
+
} else {
|
|
216
|
+
if (startTime) {
|
|
217
|
+
conditions.push({
|
|
218
|
+
attribute: 'id',
|
|
219
|
+
comparator: 'greater_than_equal',
|
|
220
|
+
value: startTime,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (endTime) {
|
|
224
|
+
conditions.push({
|
|
225
|
+
attribute: 'id',
|
|
226
|
+
comparator: 'less_than',
|
|
227
|
+
value: endTime,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
234
232
|
// `snapshot: false` lets these (potentially long-running) analytics scans read against the
|
|
235
233
|
// latest committed data without holding a consistent read snapshot open, so they stay easier
|
|
236
234
|
// on the rest of the system (a pinned snapshot would block compaction for the scan's duration).
|
|
237
235
|
const request: any = { conditions, allowConditionsOnDynamicAttributes: true, snapshot: false };
|
|
238
|
-
if (boundedWindow) {
|
|
239
|
-
request.enforceExecutionOrder = true;
|
|
240
|
-
}
|
|
241
236
|
if (select.length > 0) {
|
|
242
237
|
request['select'] = select;
|
|
243
238
|
}
|