@harperfast/harper 5.2.13 → 5.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/mcpTools.ts +1 -1
- package/agent/session.ts +25 -14
- package/bin/cliOperations.ts +58 -9
- package/bin/copyDb.ts +282 -59
- package/bin/deploySetup.ts +16 -5
- package/bin/harper.ts +1 -1
- package/bin/help.ts +4 -1
- package/bin/lite.ts +4 -1
- package/bin/restart.ts +120 -4
- package/bin/run.ts +6 -11
- package/bin/upgrade.js +7 -3
- package/bin/workloadIdentity.ts +119 -0
- package/components/Application.ts +3436 -242
- package/components/ApplicationScope.ts +8 -0
- package/components/EntryHandler.ts +59 -39
- package/components/OptionsWatcher.ts +440 -98
- package/components/RuntimeModuleTracker.ts +38 -7
- package/components/Scope.ts +56 -15
- package/components/awaitRestart.ts +84 -0
- package/components/componentLoader.ts +381 -32
- package/components/componentPreparationLock.ts +16 -5
- package/components/deploymentOperations.ts +4 -1
- package/components/deploymentRecorder.ts +9 -2
- package/components/mcp/adapters/harperHttp.ts +4 -0
- package/components/mcp/listChanged.ts +4 -0
- package/components/mcp/toolRegistry.ts +2 -0
- package/components/mcp/tools/operations.ts +9 -0
- package/components/mcp/tools/schemas/operationDescriptions.ts +2 -2
- package/components/operations.js +537 -113
- package/components/operationsValidation.js +98 -3
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -0
- package/components/status/ComponentStatusRegistry.ts +59 -0
- package/config/RootConfigWatcher.ts +240 -40
- package/config/configReadRetry.ts +62 -0
- package/config/configUtils.ts +357 -48
- package/config/harperConfigEnvVars.ts +170 -27
- package/config/parseConfigFile.ts +34 -0
- package/config/readConfigFileSync.ts +44 -0
- package/config/watcherArming.ts +59 -0
- package/config-root.schema.json +33 -0
- package/dataLayer/blobBackup.ts +160 -50
- package/dataLayer/delete.ts +6 -1
- package/dataLayer/harperBridge/ResourceBridge.ts +80 -10
- package/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
- package/dataLayer/hdbInfoController.ts +34 -1
- package/dataLayer/insert.ts +44 -1
- package/dataLayer/rocksdbBackup.ts +53 -10
- package/dataLayer/schema.ts +11 -1
- package/dataLayer/schemaDescribe.ts +8 -1
- package/dist/agent/mcpTools.js +1 -1
- package/dist/agent/mcpTools.js.map +1 -1
- package/dist/agent/session.d.ts +22 -0
- package/dist/agent/session.js +26 -15
- package/dist/agent/session.js.map +1 -1
- package/dist/bin/cliOperations.js +61 -9
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/copyDb.d.ts +12 -1
- package/dist/bin/copyDb.js +248 -60
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/deploySetup.d.ts +2 -0
- package/dist/bin/deploySetup.js +11 -3
- package/dist/bin/deploySetup.js.map +1 -1
- package/dist/bin/harper.js +1 -1
- package/dist/bin/harper.js.map +1 -1
- package/dist/bin/help.js +4 -1
- package/dist/bin/help.js.map +1 -1
- package/dist/bin/lite.js +4 -1
- package/dist/bin/lite.js.map +1 -1
- package/dist/bin/restart.js +91 -6
- package/dist/bin/restart.js.map +1 -1
- package/dist/bin/run.js +4 -10
- package/dist/bin/run.js.map +1 -1
- package/dist/bin/upgrade.js +4 -3
- package/dist/bin/upgrade.js.map +1 -1
- package/dist/bin/workloadIdentity.d.ts +18 -0
- package/dist/bin/workloadIdentity.js +100 -0
- package/dist/bin/workloadIdentity.js.map +1 -0
- package/dist/components/Application.d.ts +235 -17
- package/dist/components/Application.js +3038 -238
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +8 -0
- package/dist/components/ApplicationScope.js +7 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/EntryHandler.js +26 -10
- package/dist/components/EntryHandler.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +4 -0
- package/dist/components/OptionsWatcher.js +440 -99
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/RuntimeModuleTracker.js +40 -6
- package/dist/components/RuntimeModuleTracker.js.map +1 -1
- package/dist/components/Scope.js +52 -13
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/awaitRestart.d.ts +33 -0
- package/dist/components/awaitRestart.js +61 -0
- package/dist/components/awaitRestart.js.map +1 -0
- package/dist/components/componentLoader.d.ts +38 -1
- package/dist/components/componentLoader.js +313 -24
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/componentPreparationLock.d.ts +5 -0
- package/dist/components/componentPreparationLock.js +14 -6
- package/dist/components/componentPreparationLock.js.map +1 -1
- package/dist/components/deploymentOperations.js +4 -1
- package/dist/components/deploymentOperations.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +4 -2
- package/dist/components/deploymentRecorder.js +1 -0
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/mcp/adapters/harperHttp.js +4 -0
- package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
- package/dist/components/mcp/listChanged.js +5 -0
- package/dist/components/mcp/listChanged.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +1 -0
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/operations.d.ts +5 -0
- package/dist/components/mcp/tools/operations.js +9 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -2
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/operations.d.ts +28 -0
- package/dist/components/operations.js +476 -113
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +97 -3
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/packageComponent.js +24 -0
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/components/requestRestart.d.ts +1 -0
- package/dist/components/requestRestart.js +7 -0
- package/dist/components/requestRestart.js.map +1 -1
- package/dist/components/status/ComponentStatusRegistry.d.ts +0 -4
- package/dist/components/status/ComponentStatusRegistry.js +63 -0
- package/dist/components/status/ComponentStatusRegistry.js.map +1 -1
- package/dist/config/RootConfigWatcher.d.ts +9 -1
- package/dist/config/RootConfigWatcher.js +228 -37
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configReadRetry.d.ts +8 -0
- package/dist/config/configReadRetry.js +62 -0
- package/dist/config/configReadRetry.js.map +1 -0
- package/dist/config/configUtils.d.ts +11 -2
- package/dist/config/configUtils.js +304 -47
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/harperConfigEnvVars.d.ts +16 -0
- package/dist/config/harperConfigEnvVars.js +162 -25
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/config/parseConfigFile.d.ts +4 -0
- package/dist/config/parseConfigFile.js +35 -0
- package/dist/config/parseConfigFile.js.map +1 -0
- package/dist/config/readConfigFileSync.d.ts +1 -0
- package/dist/config/readConfigFileSync.js +47 -0
- package/dist/config/readConfigFileSync.js.map +1 -0
- package/dist/config/watcherArming.d.ts +15 -0
- package/dist/config/watcherArming.js +59 -0
- package/dist/config/watcherArming.js.map +1 -0
- package/dist/dataLayer/blobBackup.d.ts +49 -20
- package/dist/dataLayer/blobBackup.js +139 -50
- package/dist/dataLayer/blobBackup.js.map +1 -1
- package/dist/dataLayer/delete.js +1 -1
- package/dist/dataLayer/delete.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +14 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +72 -12
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.d.ts +3 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.d.ts +10 -0
- package/dist/dataLayer/hdbInfoController.js +30 -1
- package/dist/dataLayer/hdbInfoController.js.map +1 -1
- package/dist/dataLayer/insert.d.ts +9 -1
- package/dist/dataLayer/insert.js +30 -0
- package/dist/dataLayer/insert.js.map +1 -1
- package/dist/dataLayer/rocksdbBackup.d.ts +2 -2
- package/dist/dataLayer/rocksdbBackup.js +45 -8
- package/dist/dataLayer/rocksdbBackup.js.map +1 -1
- package/dist/dataLayer/schema.js +8 -0
- package/dist/dataLayer/schema.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +8 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +55 -0
- package/dist/resources/DatabaseTransaction.d.ts +77 -0
- package/dist/resources/DatabaseTransaction.js +586 -54
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +2 -1
- package/dist/resources/LMDBTransaction.js +43 -6
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.d.ts +1 -0
- package/dist/resources/PrimaryRocksDatabase.js +49 -5
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +20 -0
- package/dist/resources/RecordEncoder.js +110 -9
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/RequestTarget.d.ts +2 -0
- package/dist/resources/RequestTarget.js.map +1 -1
- package/dist/resources/Resource.js +117 -24
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +28 -1
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/RocksIndexStore.d.ts +6 -1
- package/dist/resources/RocksIndexStore.js +24 -9
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +24 -1
- package/dist/resources/RocksTransactionLogStore.js +160 -49
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +165 -10
- package/dist/resources/Table.js +1805 -303
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +10 -3
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +181 -2
- package/dist/resources/auditStore.js +640 -29
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +129 -9
- package/dist/resources/blob.js +992 -114
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/branchDatabase.d.ts +48 -0
- package/dist/resources/branchDatabase.js +892 -0
- package/dist/resources/branchDatabase.js.map +1 -0
- package/dist/resources/crdt.js +50 -12
- package/dist/resources/crdt.js.map +1 -1
- package/dist/resources/dataLoader.js +3 -4
- package/dist/resources/dataLoader.js.map +1 -1
- package/dist/resources/databases.d.ts +157 -14
- package/dist/resources/databases.js +1462 -325
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/defineTable.d.ts +10 -2
- package/dist/resources/defineTable.js +9 -1
- package/dist/resources/defineTable.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +5 -0
- package/dist/resources/derivedIndexRegistry.js +68 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -0
- package/dist/resources/derivedIndexRuntime.d.ts +215 -0
- package/dist/resources/derivedIndexRuntime.js +2027 -0
- package/dist/resources/derivedIndexRuntime.js.map +1 -0
- package/dist/resources/graphql.d.ts +1 -1
- package/dist/resources/graphql.js +52 -16
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +103 -9
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +854 -41
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
- package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
- package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
- package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
- package/dist/resources/longLivedTransactions.d.ts +71 -0
- package/dist/resources/longLivedTransactions.js +358 -0
- package/dist/resources/longLivedTransactions.js.map +1 -0
- package/dist/resources/models/backendRegistry.d.ts +26 -0
- package/dist/resources/models/backendRegistry.js +60 -2
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +33 -1
- package/dist/resources/models/bootstrap.js +416 -31
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/nodeIdMapping.d.ts +5 -0
- package/dist/resources/nodeIdMapping.js +49 -0
- package/dist/resources/nodeIdMapping.js.map +1 -1
- package/dist/resources/recordLock.d.ts +123 -0
- package/dist/resources/recordLock.js +315 -0
- package/dist/resources/recordLock.js.map +1 -0
- package/dist/resources/recordLockCoordinator.d.ts +557 -0
- package/dist/resources/recordLockCoordinator.js +2565 -0
- package/dist/resources/recordLockCoordinator.js.map +1 -0
- package/dist/resources/replayLogs.d.ts +14 -1
- package/dist/resources/replayLogs.js +157 -24
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/replayLogsGuards.d.ts +94 -7
- package/dist/resources/replayLogsGuards.js +111 -7
- package/dist/resources/replayLogsGuards.js.map +1 -1
- package/dist/resources/replicatedApplyFailure.d.ts +16 -0
- package/dist/resources/replicatedApplyFailure.js +63 -0
- package/dist/resources/replicatedApplyFailure.js.map +1 -0
- package/dist/resources/scheduler/scheduler.js +3 -3
- package/dist/resources/scheduler/scheduler.js.map +1 -1
- package/dist/resources/search.d.ts +10 -4
- package/dist/resources/search.js +283 -37
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/tracked.d.ts +5 -1
- package/dist/resources/tracked.js +74 -23
- package/dist/resources/tracked.js.map +1 -1
- package/dist/resources/transactionBroadcast.d.ts +1 -1
- package/dist/resources/transactionBroadcast.js +2 -2
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/auth.js +61 -30
- package/dist/security/auth.js.map +1 -1
- package/dist/security/authn/oidc/claims.d.ts +22 -0
- package/dist/security/authn/oidc/claims.js +71 -0
- package/dist/security/authn/oidc/claims.js.map +1 -0
- package/dist/security/authn/oidc/identityToken.d.ts +27 -0
- package/dist/security/authn/oidc/identityToken.js +111 -0
- package/dist/security/authn/oidc/identityToken.js.map +1 -0
- package/dist/security/authn/oidc/jwks.d.ts +25 -0
- package/dist/security/authn/oidc/jwks.js +261 -0
- package/dist/security/authn/oidc/jwks.js.map +1 -0
- package/dist/security/authn/oidc/providers/generic.d.ts +13 -0
- package/dist/security/authn/oidc/providers/generic.js +34 -0
- package/dist/security/authn/oidc/providers/generic.js.map +1 -0
- package/dist/security/authn/oidc/providers/githubActions.d.ts +11 -0
- package/dist/security/authn/oidc/providers/githubActions.js +129 -0
- package/dist/security/authn/oidc/providers/githubActions.js.map +1 -0
- package/dist/security/authn/oidc/providers/index.d.ts +37 -0
- package/dist/security/authn/oidc/providers/index.js +24 -0
- package/dist/security/authn/oidc/providers/index.js.map +1 -0
- package/dist/security/authn/oidc/tokenExchange.d.ts +12 -0
- package/dist/security/authn/oidc/tokenExchange.js +306 -0
- package/dist/security/authn/oidc/tokenExchange.js.map +1 -0
- package/dist/security/authn/oidc/trustPolicyOperations.d.ts +49 -0
- package/dist/security/authn/oidc/trustPolicyOperations.js +358 -0
- package/dist/security/authn/oidc/trustPolicyOperations.js.map +1 -0
- package/dist/security/authn/oidc/types.d.ts +38 -0
- package/dist/security/authn/oidc/types.js +6 -0
- package/dist/security/authn/oidc/types.js.map +1 -0
- package/dist/security/certificateVerification/index.js +40 -11
- package/dist/security/certificateVerification/index.js.map +1 -1
- package/dist/security/certificateVerification/trustedIssuers.d.ts +24 -0
- package/dist/security/certificateVerification/trustedIssuers.js +79 -0
- package/dist/security/certificateVerification/trustedIssuers.js.map +1 -0
- package/dist/security/certificateVerification/types.d.ts +1 -0
- package/dist/security/credentialProvenance.d.ts +35 -0
- package/dist/security/credentialProvenance.js +51 -0
- package/dist/security/credentialProvenance.js.map +1 -0
- package/dist/security/credentialRejection.d.ts +4 -0
- package/dist/security/credentialRejection.js +24 -0
- package/dist/security/credentialRejection.js.map +1 -0
- package/dist/security/deferredAuthentication.d.ts +36 -0
- package/dist/security/deferredAuthentication.js +70 -0
- package/dist/security/deferredAuthentication.js.map +1 -0
- package/dist/security/impersonation.d.ts +21 -0
- package/dist/security/impersonation.js +108 -9
- package/dist/security/impersonation.js.map +1 -1
- package/dist/security/jsLoader.d.ts +6 -0
- package/dist/security/jsLoader.js +77 -15
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/keys.js +301 -71
- package/dist/security/keys.js.map +1 -1
- package/dist/security/operationScope.d.ts +21 -0
- package/dist/security/operationScope.js +36 -0
- package/dist/security/operationScope.js.map +1 -0
- package/dist/security/permissionsTranslator.js +21 -0
- package/dist/security/permissionsTranslator.js.map +1 -1
- package/dist/security/tokenAuthentication.d.ts +19 -1
- package/dist/security/tokenAuthentication.js +191 -10
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/security/user.js +4 -3
- package/dist/security/user.js.map +1 -1
- package/dist/server/DurableSubscriptionsSession.d.ts +2 -2
- package/dist/server/DurableSubscriptionsSession.js +67 -10
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/REST.js +106 -2
- package/dist/server/REST.js.map +1 -1
- package/dist/server/graphqlQuerying.js +4 -0
- package/dist/server/graphqlQuerying.js.map +1 -1
- package/dist/server/http.d.ts +16 -1
- package/dist/server/http.js +122 -17
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +8 -1
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobProcess.js +6 -2
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobs.js +4 -1
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/liveSubscriptionAuth.d.ts +26 -4
- package/dist/server/liveSubscriptionAuth.js +105 -39
- package/dist/server/liveSubscriptionAuth.js.map +1 -1
- package/dist/server/loadRootComponents.js +49 -10
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/mqtt.d.ts +2 -0
- package/dist/server/mqtt.js +165 -30
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/nodeName.d.ts +2 -0
- package/dist/server/nodeName.js +107 -23
- package/dist/server/nodeName.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +27 -0
- package/dist/server/serverHelpers/Headers.js +145 -1
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
- package/dist/server/serverHelpers/Request.d.ts +5 -10
- package/dist/server/serverHelpers/Request.js +38 -136
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +11 -0
- package/dist/server/serverHelpers/contentTypes.js +221 -40
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/deployValidationState.d.ts +3 -0
- package/dist/server/serverHelpers/deployValidationState.js +9 -19
- package/dist/server/serverHelpers/deployValidationState.js.map +1 -1
- package/dist/server/serverHelpers/operationAuthorizationState.d.ts +12 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js +24 -2
- package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -1
- package/dist/server/serverHelpers/registeredOperations.d.ts +5 -4
- package/dist/server/serverHelpers/registeredOperations.js +74 -21
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
- package/dist/server/serverHelpers/requestSanitization.d.ts +11 -0
- package/dist/server/serverHelpers/requestSanitization.js +20 -0
- package/dist/server/serverHelpers/requestSanitization.js.map +1 -0
- package/dist/server/serverHelpers/serverHandlers.js +6 -3
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +18 -0
- package/dist/server/serverHelpers/serverUtilities.js +178 -29
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/sharedMessageEncoding.d.ts +67 -0
- package/dist/server/serverHelpers/sharedMessageEncoding.js +280 -0
- package/dist/server/serverHelpers/sharedMessageEncoding.js.map +1 -0
- package/dist/server/serverHelpers/uwsServer.js +19 -1
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/static.js +24 -28
- package/dist/server/static.js.map +1 -1
- package/dist/server/storageReclamation.d.ts +5 -0
- package/dist/server/storageReclamation.js +17 -1
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/isolatedApplications.d.ts +47 -0
- package/dist/server/threads/isolatedApplications.js +171 -0
- package/dist/server/threads/isolatedApplications.js.map +1 -0
- package/dist/server/threads/itc.d.ts +7 -2
- package/dist/server/threads/itc.js +5 -1
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/logRotationTransport.d.ts +1 -0
- package/dist/server/threads/logRotationTransport.js +33 -0
- package/dist/server/threads/logRotationTransport.js.map +1 -0
- package/dist/server/threads/manageThreads.d.ts +83 -3
- package/dist/server/threads/manageThreads.js +769 -75
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.d.ts +1 -0
- package/dist/server/threads/socketRouter.js +277 -31
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadHeapMemory.d.ts +2 -0
- package/dist/server/threads/threadHeapMemory.js +31 -0
- package/dist/server/threads/threadHeapMemory.js.map +1 -0
- package/dist/server/threads/threadServer.js +76 -22
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/sqlEngine/config.d.ts +1 -3
- package/dist/sqlEngine/config.js +19 -16
- package/dist/sqlEngine/config.js.map +1 -1
- package/dist/sqlTranslator/index.d.ts +1 -1
- package/dist/sqlTranslator/index.js +30 -7
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.d.ts +7 -0
- package/dist/upgrade/directives/5-3-0.js +148 -0
- package/dist/upgrade/directives/5-3-0.js.map +1 -0
- package/dist/upgrade/directives/directivesController.js +2 -1
- package/dist/upgrade/directives/directivesController.js.map +1 -1
- package/dist/utility/OperationFunctionCaller.js +2 -1
- package/dist/utility/OperationFunctionCaller.js.map +1 -1
- package/dist/utility/common_utils.d.ts +16 -0
- package/dist/utility/common_utils.js +32 -6
- package/dist/utility/common_utils.js.map +1 -1
- package/dist/utility/componentNames.d.ts +8 -0
- package/dist/utility/componentNames.js +12 -1
- package/dist/utility/componentNames.js.map +1 -1
- package/dist/utility/environment/environmentManager.js +3 -6
- package/dist/utility/environment/environmentManager.js.map +1 -1
- package/dist/utility/environment/systemInformation.d.ts +1 -0
- package/dist/utility/environment/systemInformation.js +1 -0
- package/dist/utility/environment/systemInformation.js.map +1 -1
- package/dist/utility/errors/commonErrors.d.ts +2 -0
- package/dist/utility/errors/commonErrors.js +2 -0
- package/dist/utility/errors/commonErrors.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +33 -0
- package/dist/utility/errors/hdbError.js +58 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +18 -0
- package/dist/utility/hdbTerms.js +20 -2
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +2 -0
- package/dist/utility/logging/harper_logger.js +286 -30
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
- package/dist/utility/logging/logGenerationCoordinator.js +184 -0
- package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
- package/dist/utility/logging/logRotation.d.ts +46 -0
- package/dist/utility/logging/logRotation.js +365 -0
- package/dist/utility/logging/logRotation.js.map +1 -0
- package/dist/utility/logging/logRotator.d.ts +1 -1
- package/dist/utility/logging/logRotator.js +192 -85
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/nodeIdentity.d.ts +9 -0
- package/dist/utility/nodeIdentity.js +58 -0
- package/dist/utility/nodeIdentity.js.map +1 -0
- package/dist/utility/npmUtilities.js +11 -7
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/operationPermissions.d.ts +3 -1
- package/dist/utility/operationPermissions.js +16 -1
- package/dist/utility/operationPermissions.js.map +1 -1
- package/dist/utility/operation_authorization.d.ts +10 -7
- package/dist/utility/operation_authorization.js +212 -41
- package/dist/utility/operation_authorization.js.map +1 -1
- package/dist/utility/watchPath.d.ts +29 -0
- package/dist/utility/watchPath.js +68 -0
- package/dist/utility/watchPath.js.map +1 -0
- package/dist/utility/watcherFallback.d.ts +41 -0
- package/dist/utility/watcherFallback.js +153 -0
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.d.ts +12 -0
- package/dist/validation/configValidator.js +205 -75
- package/dist/validation/configValidator.js.map +1 -1
- package/dist/validation/installValidator.js +12 -0
- package/dist/validation/installValidator.js.map +1 -1
- package/dist/validation/validationWrapper.d.ts +11 -0
- package/dist/validation/validationWrapper.js +16 -3
- package/dist/validation/validationWrapper.js.map +1 -1
- package/index.ts +8 -0
- package/json/systemSchema.json +55 -0
- package/npm-shrinkwrap.json +286 -194
- package/package.json +15 -7
- package/resources/DESIGN.md +219 -52
- package/resources/DatabaseTransaction.ts +657 -52
- package/resources/LMDBTransaction.ts +46 -6
- package/resources/PrimaryRocksDatabase.ts +46 -7
- package/resources/RecordEncoder.ts +124 -9
- package/resources/RequestTarget.ts +2 -0
- package/resources/Resource.ts +114 -22
- package/resources/ResourceInterface.ts +31 -0
- package/resources/RocksIndexStore.ts +30 -9
- package/resources/RocksTransactionLogStore.ts +188 -51
- package/resources/Table.ts +1960 -301
- package/resources/analytics/write.ts +10 -3
- package/resources/auditStore.ts +647 -32
- package/resources/blob.ts +1029 -110
- package/resources/branchDatabase.ts +941 -0
- package/resources/crdt.ts +76 -12
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +1584 -320
- package/resources/defineTable.ts +18 -2
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +72 -17
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +911 -48
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/longLivedTransactions.ts +360 -0
- package/resources/models/backendRegistry.ts +84 -2
- package/resources/models/bootstrap.ts +473 -28
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/recordLock.ts +419 -0
- package/resources/recordLockCoordinator.ts +3043 -0
- package/resources/replayLogs.ts +152 -26
- package/resources/replayLogsGuards.ts +171 -8
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +288 -46
- package/resources/tracked.ts +73 -22
- package/resources/transactionBroadcast.ts +3 -3
- package/security/auth.ts +68 -29
- package/security/authn/oidc/claims.ts +72 -0
- package/security/authn/oidc/identityToken.ts +129 -0
- package/security/authn/oidc/jwks.ts +260 -0
- package/security/authn/oidc/providers/generic.ts +40 -0
- package/security/authn/oidc/providers/githubActions.ts +137 -0
- package/security/authn/oidc/providers/index.ts +52 -0
- package/security/authn/oidc/tokenExchange.ts +300 -0
- package/security/authn/oidc/trustPolicyOperations.ts +343 -0
- package/security/authn/oidc/types.ts +41 -0
- package/security/certificateVerification/index.ts +54 -13
- package/security/certificateVerification/trustedIssuers.ts +76 -0
- package/security/certificateVerification/types.ts +1 -0
- package/security/credentialProvenance.ts +47 -0
- package/security/credentialRejection.ts +22 -0
- package/security/deferredAuthentication.ts +71 -0
- package/security/impersonation.ts +117 -12
- package/security/jsLoader.ts +83 -18
- package/security/keys.ts +298 -72
- package/security/operationScope.ts +33 -0
- package/security/permissionsTranslator.js +23 -0
- package/security/tokenAuthentication.ts +233 -12
- package/security/user.ts +4 -3
- package/server/DESIGN.md +194 -16
- package/server/DurableSubscriptionsSession.ts +71 -11
- package/server/REST.ts +115 -4
- package/server/graphqlQuerying.ts +4 -0
- package/server/http.ts +133 -20
- package/server/itc/serverHandlers.js +8 -1
- package/server/jobs/jobProcess.ts +8 -2
- package/server/jobs/jobs.ts +4 -1
- package/server/liveSubscriptionAuth.ts +129 -46
- package/server/loadRootComponents.js +50 -8
- package/server/mqtt.ts +179 -38
- package/server/nodeName.ts +103 -21
- package/server/serverHelpers/Headers.ts +135 -0
- package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
- package/server/serverHelpers/Request.ts +33 -131
- package/server/serverHelpers/contentTypes.ts +217 -36
- package/server/serverHelpers/deployValidationState.ts +24 -13
- package/server/serverHelpers/operationAuthorizationState.ts +34 -3
- package/server/serverHelpers/registeredOperations.ts +79 -22
- package/server/serverHelpers/requestSanitization.ts +15 -0
- package/server/serverHelpers/serverHandlers.js +6 -3
- package/server/serverHelpers/serverUtilities.ts +232 -40
- package/server/serverHelpers/sharedMessageEncoding.ts +307 -0
- package/server/serverHelpers/uwsServer.ts +17 -2
- package/server/static.ts +23 -29
- package/server/storageReclamation.ts +15 -2
- package/server/threads/isolatedApplications.ts +157 -0
- package/server/threads/itc.js +11 -1
- package/server/threads/logRotationTransport.ts +40 -0
- package/server/threads/manageThreads.js +771 -66
- package/server/threads/socketRouter.ts +291 -30
- package/server/threads/threadHeapMemory.ts +26 -0
- package/server/threads/threadServer.js +73 -22
- package/sqlTranslator/index.ts +31 -8
- package/studio/web/assets/{Chat-4RrB5134.js → Chat-D3j-1yY1.js} +1 -1
- package/studio/web/assets/{FloatingChat-omlNMDcJ.js → FloatingChat-BxJGYcfB.js} +3 -3
- package/studio/web/assets/{apiToken-Bke3wvfZ.js → apiToken-CT55oWOe.js} +1 -1
- package/studio/web/assets/{applications-DvFDYJgK.js → applications-D9Ct9_vm.js} +1 -1
- package/studio/web/assets/{cssMode-C1vRa7zh.js → cssMode-DV8H7VwA.js} +1 -1
- package/studio/web/assets/{editor-3XRWEDWX.js → editor-uatc0unt.js} +1 -1
- package/studio/web/assets/{html-CTY5tdMr.js → html-Bm6D6paN.js} +1 -1
- package/studio/web/assets/{htmlMode-CG1vSD9t.js → htmlMode-CEn7tpLG.js} +1 -1
- package/studio/web/assets/{index-Dfpeofdu.js → index-BIXW6Pu4.js} +5 -5
- package/studio/web/assets/{index.lazy-B1VOIv-t.js → index.lazy-UI7L-Vrk.js} +1 -1
- package/studio/web/assets/{javascript-CJeJzGnI.js → javascript-CJ0G3AFZ.js} +1 -1
- package/studio/web/assets/{jsonMode-CYPBwM82.js → jsonMode-DQADAYEa.js} +1 -1
- package/studio/web/assets/{languageServices-IKH4GUHl.js → languageServices-CAQJXWcI.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BC8_gxKG.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
- package/studio/web/assets/{notifications-BfKBpYcq.js → notifications-BbxTU6Aw.js} +1 -1
- package/studio/web/assets/{notifications-DFbArTfC.js → notifications-Cvb3P1lB.js} +1 -1
- package/studio/web/assets/{profile-Q4-T6c-S.js → profile-Yyb7gsvL.js} +1 -1
- package/studio/web/assets/{regions-CtkV0xje.js → regions-OgjGHlU5.js} +1 -1
- package/studio/web/assets/{register-1ZZuMsiA.js → register-6qwNEOY3.js} +2 -2
- package/studio/web/assets/{setComponentFile-ZWLcWv5X.js → setComponentFile-BilDMtgB.js} +1 -1
- package/studio/web/assets/{setup-DWprJyJy.js → setup-J6qJ7OIU.js} +2 -2
- package/studio/web/assets/{status--aNm8isn.js → status-0RWGcfyD.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-Cq_lt3XD.js → toggleHighContrast-BIn-vErT.js} +1 -1
- package/studio/web/assets/{tsMode-pjgytARx.js → tsMode-DgUXku4d.js} +1 -1
- package/studio/web/assets/{typescript-Cdg0mqUh.js → typescript-C9orXcsM.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-RZhaY8Rn.js → useEntityRestURL-BEoXXbUB.js} +1 -1
- package/studio/web/assets/{workers-BgoXIqQe.js → workers-JVzSDmgx.js} +1 -1
- package/studio/web/assets/{xml-BwSeDMiP.js → xml-Cq-S8S4X.js} +1 -1
- package/studio/web/assets/{yaml-DotCUG5l.js → yaml-sfoRdh1M.js} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-3-0.ts +132 -0
- package/upgrade/directives/directivesController.ts +2 -1
- package/utility/OperationFunctionCaller.ts +2 -1
- package/utility/common_utils.ts +30 -5
- package/utility/componentNames.ts +12 -0
- package/utility/environment/environmentManager.ts +3 -7
- package/utility/environment/systemInformation.ts +7 -0
- package/utility/errors/commonErrors.ts +4 -0
- package/utility/errors/hdbError.ts +57 -0
- package/utility/hdbTerms.ts +19 -0
- package/utility/logging/harper_logger.ts +278 -26
- package/utility/logging/logGenerationCoordinator.ts +196 -0
- package/utility/logging/logRotation.ts +367 -0
- package/utility/logging/logRotator.ts +213 -81
- package/utility/nodeIdentity.ts +45 -0
- package/utility/npmUtilities.ts +12 -8
- package/utility/operationPermissions.ts +18 -1
- package/utility/operation_authorization.ts +231 -42
- package/utility/watchPath.ts +63 -0
- package/utility/watcherFallback.ts +148 -0
- package/validation/configValidator.ts +215 -75
- package/validation/installValidator.ts +15 -0
- package/validation/validationWrapper.ts +18 -4
package/dist/resources/Table.js
CHANGED
|
@@ -41,7 +41,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
41
41
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
42
42
|
};
|
|
43
43
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
-
exports.EVICTED = exports.INVALIDATED = void 0;
|
|
44
|
+
exports.EVICTED = exports.INVALIDATED = exports.UPDATE_ATTRIBUTES_LOCK_SLOW_WAIT = exports.UPDATE_ATTRIBUTES_LOCK_TIMEOUT = void 0;
|
|
45
|
+
exports.acquireUpdateAttributesLock = acquireUpdateAttributesLock;
|
|
46
|
+
exports.releaseUpdateAttributesLock = releaseUpdateAttributesLock;
|
|
47
|
+
exports.withUpdateAttributesLock = withUpdateAttributesLock;
|
|
45
48
|
exports.ignoreAlreadyDropped = ignoreAlreadyDropped;
|
|
46
49
|
exports.freezeRecord = freezeRecord;
|
|
47
50
|
exports.frozenRecordView = frozenRecordView;
|
|
@@ -50,6 +53,7 @@ exports.coerceType = coerceType;
|
|
|
50
53
|
const hdbTerms_ts_1 = require("../utility/hdbTerms.js");
|
|
51
54
|
const node_vm_1 = require("node:vm");
|
|
52
55
|
const node_crypto_1 = require("node:crypto");
|
|
56
|
+
const node_perf_hooks_1 = require("node:perf_hooks");
|
|
53
57
|
const commonUtility_ts_1 = require("../utility/lmdb/commonUtility.js");
|
|
54
58
|
const nodeIdMapping_ts_1 = require("./nodeIdMapping.js");
|
|
55
59
|
const lodash_1 = __importDefault(require("lodash"));
|
|
@@ -58,12 +62,15 @@ const lmdbProcessRows_js_1 = __importDefault(require("../dataLayer/harperBridge/
|
|
|
58
62
|
const Resource_ts_1 = require("./Resource.js");
|
|
59
63
|
const when_ts_1 = require("../utility/when.js");
|
|
60
64
|
const DatabaseTransaction_ts_1 = require("./DatabaseTransaction.js");
|
|
65
|
+
const recordLock_ts_1 = require("./recordLock.js");
|
|
66
|
+
const nodeName_ts_1 = require("../server/nodeName.js");
|
|
61
67
|
const envMngr = __importStar(require("../utility/environment/environmentManager.js"));
|
|
62
68
|
const transactionBroadcast_ts_1 = require("./transactionBroadcast.js");
|
|
63
69
|
const hdbError_ts_1 = require("../utility/errors/hdbError.js");
|
|
64
70
|
const signalling = __importStar(require("../utility/signalling.js"));
|
|
65
71
|
const itc_js_1 = require("../server/threads/itc.js");
|
|
66
72
|
const databases_ts_1 = require("./databases.js");
|
|
73
|
+
const replicatedApplyFailure_ts_1 = require("./replicatedApplyFailure.js");
|
|
67
74
|
const search_ts_1 = require("./search.js");
|
|
68
75
|
const logger_ts_1 = require("../utility/logging/logger.js");
|
|
69
76
|
const staticResourceDispatch_ts_1 = require("./staticResourceDispatch.js");
|
|
@@ -72,6 +79,8 @@ const transaction_ts_1 = require("./transaction.js");
|
|
|
72
79
|
const ordered_binary_1 = require("ordered-binary");
|
|
73
80
|
const manageThreads_js_1 = require("../server/threads/manageThreads.js");
|
|
74
81
|
const auditStore_ts_1 = require("./auditStore.js");
|
|
82
|
+
const derivedIndexRegistry_ts_1 = require("./derivedIndexRegistry.js");
|
|
83
|
+
const recordLockCoordinator_ts_1 = require("./recordLockCoordinator.js");
|
|
75
84
|
const embedHook_ts_1 = require("./models/embedHook.js");
|
|
76
85
|
const common_utils_ts_1 = require("../utility/common_utils.js");
|
|
77
86
|
const RecordEncoder_ts_1 = require("./RecordEncoder.js");
|
|
@@ -92,8 +101,14 @@ const { sortBy } = lodash_1.default;
|
|
|
92
101
|
const { validateAttribute } = lmdbProcessRows_js_1.default;
|
|
93
102
|
const NULL_WITH_TIMESTAMP = new Uint8Array(9);
|
|
94
103
|
NULL_WITH_TIMESTAMP[8] = 0xc0; // null
|
|
104
|
+
const sourceWriteTypes = new Set(['put', 'patch', 'delete', 'publish', 'message', 'invalidate', 'relocate']);
|
|
105
|
+
const isSourceWriteType = (type) => sourceWriteTypes.has(type);
|
|
106
|
+
const SOURCE_APPLY_POSITION = Symbol('sourceApplyPosition');
|
|
95
107
|
const UNCACHEABLE_TIMESTAMP = Infinity; // we use this when dynamic content is accessed that we can't safely cache, and this prevents earlier timestamps from change the "last" modification
|
|
108
|
+
const MAX_DATE_TIMESTAMP = 8.64e15;
|
|
96
109
|
const RECORD_PRUNING_INTERVAL = 60000; // one minute
|
|
110
|
+
const MAX_CONCURRENT_HISTORY_REMOVALS = 10;
|
|
111
|
+
const MAX_CONCURRENT_LMDB_HISTORY_REMOVALS = 1000;
|
|
97
112
|
// RocksDB-only: number of eviction/tombstone removals coalesced into a single transaction commit.
|
|
98
113
|
// Each evict otherwise pays a full transaction commit, so batching amortizes that cost. LMDB already
|
|
99
114
|
// coalesces async writes per event turn (eventTurnBatching), so it keeps the per-record path.
|
|
@@ -102,6 +117,20 @@ const EVICTION_BATCH_SIZE = 100;
|
|
|
102
117
|
// letting an unbounded number of open transactions (and their snapshots) accumulate.
|
|
103
118
|
const MAX_INFLIGHT_EVICTION_BATCHES = 4;
|
|
104
119
|
const CACHEABLE_STATUS_CODES = new Set([200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501]);
|
|
120
|
+
// Guardrails for `Prefer: count=exact`: once the requested page has been collected, counting the rest
|
|
121
|
+
// of the match set is bounded by BOTH a row cap and a wall-clock budget, so a paginated read can't turn
|
|
122
|
+
// into an unbounded scan. Exceeding either reports an unknown total (Content-Range `.../*`) rather than
|
|
123
|
+
// truncating the page. These bound the count tail, not the page itself; a genuinely expensive query
|
|
124
|
+
// (large filtered full-scan, in-memory sort) should still be gated by config before broad exposure.
|
|
125
|
+
const MAX_EXACT_COUNT_SCAN = 1_000_000;
|
|
126
|
+
const MAX_EXACT_COUNT_MS = 1_000;
|
|
127
|
+
// Largest page a `Prefer: count=` request will materialize. A request whose limit exceeds this (or is
|
|
128
|
+
// not a finite, non-negative integer, e.g. `limit(Infinity)`/`limit(foo)`) falls through to the normal
|
|
129
|
+
// streaming path with no count, so a count request can't be coerced into buffering an unbounded page.
|
|
130
|
+
const MAX_COUNT_PAGE = 10_000;
|
|
131
|
+
// How often the exact-count drain yields to the macrotask queue (must be a power of two for the bit-mask
|
|
132
|
+
// check). Keeps a large scan from monopolizing the event loop without adding a yield per row.
|
|
133
|
+
const COUNT_YIELD_INTERVAL = 2_048;
|
|
105
134
|
// Smallest forward sample `getRecordCount` will extrapolate a record rate from; below it the scan runs
|
|
106
135
|
// to completion and reports an exact count.
|
|
107
136
|
const MIN_ESTIMATOR_SAMPLE = 1_000;
|
|
@@ -119,6 +148,58 @@ function usableCount(estimate) {
|
|
|
119
148
|
envMngr.initSync();
|
|
120
149
|
const LMDB_PREFETCH_WRITES = envMngr.get(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_PREFETCHWRITES);
|
|
121
150
|
const LOCK_TIMEOUT = 10000;
|
|
151
|
+
// This bounds schema-lock acquisition; LOCK_TIMEOUT bounds in-flight record writes during a drop.
|
|
152
|
+
exports.UPDATE_ATTRIBUTES_LOCK_TIMEOUT = 10000;
|
|
153
|
+
const UPDATE_ATTRIBUTES_LOCK = 'update-attributes';
|
|
154
|
+
// Contention is otherwise only visible once it becomes a timeout (harper#2251).
|
|
155
|
+
exports.UPDATE_ATTRIBUTES_LOCK_SLOW_WAIT = 1000;
|
|
156
|
+
// raw ASCII bytes are ordered-binary's encoding of the string, so this addresses the same native
|
|
157
|
+
// lock as string-keyed tryLock/unlock calls
|
|
158
|
+
const updateAttributesLockKey = Buffer.from(UPDATE_ATTRIBUTES_LOCK);
|
|
159
|
+
const lockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
160
|
+
/** The wait blocks the event loop, so the locked section must stay synchronous. */
|
|
161
|
+
function acquireUpdateAttributesLock(rootStore, scopeDescription, timeout = exports.UPDATE_ATTRIBUTES_LOCK_TIMEOUT) {
|
|
162
|
+
if (rootStore.tryLock(updateAttributesLockKey))
|
|
163
|
+
return;
|
|
164
|
+
const startTime = node_perf_hooks_1.performance.now();
|
|
165
|
+
let waitTime = 1;
|
|
166
|
+
while (!rootStore.tryLock(updateAttributesLockKey)) {
|
|
167
|
+
const elapsed = node_perf_hooks_1.performance.now() - startTime;
|
|
168
|
+
if (elapsed >= timeout) {
|
|
169
|
+
throw new hdbError_ts_1.UpdateAttributesLockTimeoutError(`Timed out after ${Math.round(elapsed)}ms waiting for the exclusive '${UPDATE_ATTRIBUTES_LOCK}' lock on ${scopeDescription}; the lock holder did not release it before the deadline, so this schema/attribute update cannot proceed`);
|
|
170
|
+
}
|
|
171
|
+
if (elapsed >= 2) {
|
|
172
|
+
Atomics.wait(lockWait, 0, 0, Math.min(waitTime, timeout - elapsed));
|
|
173
|
+
if (waitTime < 16)
|
|
174
|
+
waitTime *= 2;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const waited = node_perf_hooks_1.performance.now() - startTime;
|
|
178
|
+
// The caller cannot register its release until we return, so a throw here would leak the lock
|
|
179
|
+
// with no `finally` able to reach it.
|
|
180
|
+
if (waited >= exports.UPDATE_ATTRIBUTES_LOCK_SLOW_WAIT)
|
|
181
|
+
try {
|
|
182
|
+
logger_ts_1.logger.warn?.(`Acquired the exclusive '${UPDATE_ATTRIBUTES_LOCK}' lock on ${scopeDescription} after waiting ${Math.round(waited)}ms; this worker's event loop was blocked for that wait, and a holder that runs past ${exports.UPDATE_ATTRIBUTES_LOCK_TIMEOUT}ms fails the update outright`);
|
|
183
|
+
}
|
|
184
|
+
catch { }
|
|
185
|
+
}
|
|
186
|
+
function releaseUpdateAttributesLock(rootStore) {
|
|
187
|
+
rootStore.unlock(updateAttributesLockKey);
|
|
188
|
+
}
|
|
189
|
+
function withUpdateAttributesLock(rootStore, scopeDescription, callback) {
|
|
190
|
+
acquireUpdateAttributesLock(rootStore, scopeDescription);
|
|
191
|
+
try {
|
|
192
|
+
const result = callback();
|
|
193
|
+
if (typeof result?.then === 'function') {
|
|
194
|
+
Promise.resolve(result).catch((error) => logger_ts_1.logger.error?.(`Async update-attributes callback rejected after its lock was released (${scopeDescription})`, error));
|
|
195
|
+
throw new TypeError(`withUpdateAttributesLock callback must be synchronous (${scopeDescription}); asynchronous work may continue after the lock is released`);
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
releaseUpdateAttributesLock(rootStore);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
122
203
|
// Tolerate a redundant column family drop. Drops are broadcast to every worker
|
|
123
204
|
// thread and each holds its own handle to the same underlying family, so a
|
|
124
205
|
// concurrent worker may already have dropped it; the storage engine reports
|
|
@@ -349,9 +430,49 @@ function detectCyclicEnumerable(start) {
|
|
|
349
430
|
function chainKeyForId(id) {
|
|
350
431
|
return typeof id === 'string' ? 's' + id : 'k' + (0, DatabaseTransaction_ts_1.writeKeyId)(id);
|
|
351
432
|
}
|
|
433
|
+
/** Normalizes a passed `context` argument as `transactional()` does; undefined means fall back to ambient. */
|
|
434
|
+
function contextArgument(context) {
|
|
435
|
+
if (!context || (0, DatabaseTransaction_ts_1.isReleasedTransaction)(context))
|
|
436
|
+
return undefined;
|
|
437
|
+
const resolved = context.getContext?.() || context;
|
|
438
|
+
return resolved instanceof DatabaseTransaction_ts_1.DatabaseTransaction ? { transaction: resolved } : resolved;
|
|
439
|
+
}
|
|
440
|
+
/** The cluster round never ran for a node-scoped handle, so no peer ever deferred to it. */
|
|
441
|
+
function scopeViolation(handle, resolved, databaseName) {
|
|
442
|
+
if (resolved.scope !== 'cluster' || handle.clusterTsR !== undefined)
|
|
443
|
+
return undefined;
|
|
444
|
+
// The same predicate lock() fails closed on, not the transport alone: a coalesced caller re-checks
|
|
445
|
+
// this after its wait, and a transport unregistered during that wait leaves the database still
|
|
446
|
+
// clustered while the lookup answers undefined. Only the implicit Phase 0 case falls through.
|
|
447
|
+
if (!resolved.scopeRequested && !(0, recordLockCoordinator_ts_1.isClusterLockRequired)(databaseName) && !(0, recordLockCoordinator_ts_1.getClusterLockTransport)(databaseName))
|
|
448
|
+
return undefined;
|
|
449
|
+
return new hdbError_ts_1.ClientError('This transaction already holds a node-scoped lock on this record, so a cluster-scoped lock cannot be taken on top of it', 409);
|
|
450
|
+
}
|
|
451
|
+
/** Distinguishes bare lock options from a record target (id, URL, {id:...}). */
|
|
452
|
+
function isPlainOptions(value) {
|
|
453
|
+
return (typeof value === 'object' &&
|
|
454
|
+
value !== null &&
|
|
455
|
+
!Array.isArray(value) &&
|
|
456
|
+
!(value instanceof URLSearchParams) &&
|
|
457
|
+
value.id === undefined);
|
|
458
|
+
}
|
|
459
|
+
// Lets a transport push a received control entry straight to the right coordinator without
|
|
460
|
+
// importing Table (which would be a cycle through databases.ts).
|
|
461
|
+
(0, recordLockCoordinator_ts_1.setLockCoordinatorResolver)((database, tableName) => databases_ts_1.databases[database]?.[tableName]?.lockCoordinator, (database, tableName) => databases_ts_1.databases[database]?.[tableName]?.admittingCoordinator, (database, tableName) => {
|
|
462
|
+
const Table = databases_ts_1.databases[database]?.[tableName];
|
|
463
|
+
if (typeof Table?.writeLockControlEntry !== 'function')
|
|
464
|
+
return undefined;
|
|
465
|
+
return (entry) => Table.writeLockControlEntry(entry);
|
|
466
|
+
});
|
|
352
467
|
function makeTable(options) {
|
|
353
|
-
|
|
468
|
+
var _a;
|
|
469
|
+
const { primaryKey, indices, tableId, tableName, primaryStore, databasePath, databaseName, auditStore, schemaDefined, dbisDB: dbisDb, sealed, splitSegments, replicate, description, hidden, cacheControl, isBranch, } = options;
|
|
354
470
|
let { expirationMS: expirationMs, evictionMS: evictionMs, audit, trackDeletes } = options;
|
|
471
|
+
// Set when the TTL exists only on this thread: either application code configured it at runtime, or
|
|
472
|
+
// an isolated application's schema was declared here. Hydrating persisted metadata does not set it:
|
|
473
|
+
// dedicated workers open unrelated shared tables too, whose scan remains owned by the pool.
|
|
474
|
+
let ttlConfiguredByApplication = false;
|
|
475
|
+
let ttlFromLoad = false; // true only around the creation-time call below
|
|
355
476
|
evictionMs ??= 0;
|
|
356
477
|
// Eviction without explicit expiration means expiration:0. Apply at construction so
|
|
357
478
|
// describe_all sees it on every worker, not just ones that ran setTTLExpiration.
|
|
@@ -363,7 +484,11 @@ function makeTable(options) {
|
|
|
363
484
|
if (!properties)
|
|
364
485
|
properties = (0, jsonSchemaTypes_ts_1.projectAttributesToProperties)(attributes);
|
|
365
486
|
const updateRecord = (0, RecordEncoder_ts_1.recordUpdater)(primaryStore, tableId, auditStore);
|
|
487
|
+
// Created on first cluster-scoped lock() or first arriving control entry, and only while a
|
|
488
|
+
// transport is registered for this database.
|
|
489
|
+
let lockCoordinator;
|
|
366
490
|
let warnedNullSourcePut = false; // latched: one warn per table per worker (see _writeUpdate)
|
|
491
|
+
let warnedFutureSourceVersion = false; // likewise (see getFromSource)
|
|
367
492
|
let sourceLoad; // if a source has a load function (replicator), record it here
|
|
368
493
|
let hasSourceGet;
|
|
369
494
|
let primaryKeyAttribute;
|
|
@@ -397,6 +522,10 @@ function makeTable(options) {
|
|
|
397
522
|
let cleanupPriority = 0;
|
|
398
523
|
let lastCleanupInterval;
|
|
399
524
|
let cleanupTimer;
|
|
525
|
+
let recordExpirationInterval;
|
|
526
|
+
// a reclamation pass awaits a scheduled cleanup, which only settles from its timer
|
|
527
|
+
const pendingCleanupResolvers = new Set();
|
|
528
|
+
let disposed = false;
|
|
400
529
|
// true once a table-level expiration/eviction/scanInterval has armed the periodic cleanup scan at setup
|
|
401
530
|
let expirationScanScheduled = false;
|
|
402
531
|
// set on the first expiring write so the unscheduled-expiration warning is evaluated at most once per table
|
|
@@ -445,10 +574,11 @@ function makeTable(options) {
|
|
|
445
574
|
const MAX_PREFETCH_BUNDLE = 6;
|
|
446
575
|
if (audit)
|
|
447
576
|
addDeleteRemoval();
|
|
448
|
-
|
|
577
|
+
const reclamationHandler = (priority) => {
|
|
449
578
|
if (hasSourceGet)
|
|
450
579
|
return scheduleCleanup(priority);
|
|
451
|
-
}
|
|
580
|
+
};
|
|
581
|
+
(0, storageReclamation_ts_1.onStorageReclamation)(primaryStore.path, reclamationHandler);
|
|
452
582
|
class Updatable extends tracked_ts_1.GenericTrackedObject {
|
|
453
583
|
getUpdatedTime() {
|
|
454
584
|
return RecordEncoder_ts_1.entryMap.get(this.getRecord())?.version;
|
|
@@ -547,12 +677,104 @@ function makeTable(options) {
|
|
|
547
677
|
},
|
|
548
678
|
});
|
|
549
679
|
}
|
|
680
|
+
function resolveAuditHead(id, version, nodeId, refs) {
|
|
681
|
+
if (!refs?.length)
|
|
682
|
+
return { txnLogKey: version, nodeId };
|
|
683
|
+
const visited = new Set();
|
|
684
|
+
function findHead(candidateRefs) {
|
|
685
|
+
if (!candidateRefs)
|
|
686
|
+
return;
|
|
687
|
+
const pending = candidateRefs.slice().reverse();
|
|
688
|
+
while (pending.length > 0) {
|
|
689
|
+
const ref = pending.pop();
|
|
690
|
+
const identity = `${ref.nodeId ?? 0}:${ref.version}`;
|
|
691
|
+
if (visited.has(identity))
|
|
692
|
+
continue;
|
|
693
|
+
visited.add(identity);
|
|
694
|
+
const entry = auditStore.getSync(ref.version, tableId, id, ref.nodeId);
|
|
695
|
+
if (!entry)
|
|
696
|
+
continue;
|
|
697
|
+
if (entry.version === version && (nodeId == null || (entry.nodeId ?? 0) === nodeId))
|
|
698
|
+
return { txnLogKey: ref.version, nodeId: ref.nodeId };
|
|
699
|
+
const previousRefs = entry.previousAdditionalAuditRefs;
|
|
700
|
+
if (previousRefs) {
|
|
701
|
+
for (let index = previousRefs.length - 1; index >= 0; index--)
|
|
702
|
+
pending.push(previousRefs[index]);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const referencedHead = findHead(refs);
|
|
707
|
+
if (referencedHead)
|
|
708
|
+
return referencedHead;
|
|
709
|
+
if (version != null) {
|
|
710
|
+
const directHead = auditStore.getSync(version, tableId, id, nodeId);
|
|
711
|
+
if (directHead?.version === version && (nodeId == null || (directHead.nodeId ?? 0) === nodeId))
|
|
712
|
+
return { txnLogKey: version, nodeId };
|
|
713
|
+
}
|
|
714
|
+
return { txnLogKey: version, nodeId };
|
|
715
|
+
}
|
|
716
|
+
// Canonical-source applies (sourceApply), replay and replication notifications are never shed;
|
|
717
|
+
// dropping one would advance the source cursor past a write that never landed.
|
|
718
|
+
function assertDerivedIndexAdmission(options, transaction) {
|
|
719
|
+
if (options?.isNotification || transaction?.sourceApply || transaction?.isReplay)
|
|
720
|
+
return;
|
|
721
|
+
const reason = (0, derivedIndexRegistry_ts_1.derivedIndexWriteRejection)(auditStore, tableId);
|
|
722
|
+
if (reason)
|
|
723
|
+
throw new hdbError_ts_1.DerivedIndexLagError(reason);
|
|
724
|
+
}
|
|
725
|
+
function stageDerivedIndexEviction(transaction, id, version) {
|
|
726
|
+
if (!(0, derivedIndexRegistry_ts_1.hasDerivedIndexRegistration)(auditStore, tableId))
|
|
727
|
+
return;
|
|
728
|
+
const nodeId = (0, nodeIdMapping_ts_1.getThisNodeId)(auditStore) ?? 0;
|
|
729
|
+
auditStore.put(null, {
|
|
730
|
+
type: 'evict',
|
|
731
|
+
tableId,
|
|
732
|
+
recordId: id,
|
|
733
|
+
version,
|
|
734
|
+
nodeId,
|
|
735
|
+
extendedType: auditStore_ts_1.LOCAL_ONLY,
|
|
736
|
+
}, { transaction, nodeId });
|
|
737
|
+
}
|
|
550
738
|
class TableResource extends Resource_ts_1.Resource {
|
|
551
739
|
#record; // the stored/frozen record from the database and stored in the cache (should not be modified directly)
|
|
552
740
|
#changes; // the changes to the record that have been made (should not be modified directly)
|
|
553
741
|
#version; // version of the record
|
|
554
742
|
#entry; // the entry from the database
|
|
555
743
|
#savingOperation; // operation for the record is currently being saved
|
|
744
|
+
#lockHandle; // the record lock acquired by lock() — scoped or hold
|
|
745
|
+
#lockWritable; // set by #reloadLocked to let save() stage lock-writable updates
|
|
746
|
+
#writeGeneration;
|
|
747
|
+
[tracked_ts_1.ASSERT_TRACKED_WRITABLE](generation = this.#writeGeneration) {
|
|
748
|
+
if (!generation)
|
|
749
|
+
return;
|
|
750
|
+
if (generation.internalWrites > 0)
|
|
751
|
+
return;
|
|
752
|
+
if (generation !== this.#writeGeneration || generation.closed)
|
|
753
|
+
throw new hdbError_ts_1.ClientError('Can not modify an update instance after it has been saved; call update() again', 409);
|
|
754
|
+
}
|
|
755
|
+
[tracked_ts_1.GET_TRACKED_WRITE_GENERATION]() {
|
|
756
|
+
return (this.#writeGeneration ??= { closed: false, internalWrites: 0 });
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Shared guard: if this instance is lock-writable but the handle is gone (expired or
|
|
760
|
+
* released), throw 409 before staging any write. Covers update/invalidate/relocate/delete
|
|
761
|
+
* in addition to the save() path. Every lock-writable instance carries its own handle in
|
|
762
|
+
* #lockHandle (scoped and hold alike), so we never need to search the registry here.
|
|
763
|
+
*/
|
|
764
|
+
#assertLiveHandle(id, allowClosed = false) {
|
|
765
|
+
if (!allowClosed && this.#writeGeneration?.closed && (0, DatabaseTransaction_ts_1.writeKeyId)(id) === (0, DatabaseTransaction_ts_1.writeKeyId)(this.getId()))
|
|
766
|
+
this[tracked_ts_1.ASSERT_TRACKED_WRITABLE]();
|
|
767
|
+
if (!this.#lockWritable)
|
|
768
|
+
return;
|
|
769
|
+
const handle = this.#lockHandle;
|
|
770
|
+
// Off-key writes through the same resource instance are ordinary; only guard the
|
|
771
|
+
// exact key the lock was acquired for.
|
|
772
|
+
if (handle.keyId !== (0, DatabaseTransaction_ts_1.writeKeyId)(id))
|
|
773
|
+
return;
|
|
774
|
+
if (handle.isExpired()) {
|
|
775
|
+
throw (0, recordLock_ts_1.lockNotHeldError)(handle);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
556
778
|
// #section: static-config
|
|
557
779
|
static name = tableName; // for display/debugging purposes
|
|
558
780
|
static primaryStore = primaryStore;
|
|
@@ -561,6 +783,7 @@ function makeTable(options) {
|
|
|
561
783
|
static tableName = tableName;
|
|
562
784
|
static tableId = tableId;
|
|
563
785
|
static indices = indices;
|
|
786
|
+
static derivedIndexRuntime;
|
|
564
787
|
static audit = audit;
|
|
565
788
|
static databasePath = databasePath;
|
|
566
789
|
static databaseName = databaseName;
|
|
@@ -646,10 +869,62 @@ function makeTable(options) {
|
|
|
646
869
|
(async () => {
|
|
647
870
|
let userRoleUpdate = false;
|
|
648
871
|
let lastSequenceId;
|
|
872
|
+
let pendingApplyFailures;
|
|
873
|
+
const reportDroppedWrite = (event, context, error) => {
|
|
874
|
+
const position = event === context ? context[SOURCE_APPLY_POSITION] : (event.timestamp ?? context[SOURCE_APPLY_POSITION]);
|
|
875
|
+
const notification = (0, replicatedApplyFailure_ts_1.notifyReplicatedApplyFailure)(databaseName, {
|
|
876
|
+
nodeId: event.nodeId ?? context.nodeId,
|
|
877
|
+
table: event.table ?? context.table,
|
|
878
|
+
localTime: event.localTime ?? context.localTime,
|
|
879
|
+
}, position, error, tableName);
|
|
880
|
+
pendingApplyFailures = pendingApplyFailures
|
|
881
|
+
? Promise.all([pendingApplyFailures, notification]).then(noop)
|
|
882
|
+
: notification;
|
|
883
|
+
return notification;
|
|
884
|
+
};
|
|
885
|
+
/** Cluster lock coordination entries (harper#483 Phase 1) describe no record. */
|
|
886
|
+
const applyLockControlEvent = (event, context) => {
|
|
887
|
+
const entry = (0, recordLockCoordinator_ts_1.decodeLockControlPayload)(event.type, event.value);
|
|
888
|
+
if (!entry) {
|
|
889
|
+
logger_ts_1.logger.warn?.('discarding a malformed record lock control entry from', event.nodeId, event.type);
|
|
890
|
+
return reportDroppedWrite(event, context, new Error('Malformed record lock control entry'));
|
|
891
|
+
}
|
|
892
|
+
const target = event.table ? databases_ts_1.databases[databaseName]?.[event.table] : _a;
|
|
893
|
+
try {
|
|
894
|
+
// The audit header's nodeId is the origin, translated on receive and preserved across
|
|
895
|
+
// relays. The payload's own names are peer-supplied and prove nothing. Rebuild the id
|
|
896
|
+
// map on a miss rather than waiting out the negative-cache window: a dropped release
|
|
897
|
+
// leaves the key's home holding its grant until the delegation's own deadline, and
|
|
898
|
+
// control entries are far too rare to drive the store.
|
|
899
|
+
//
|
|
900
|
+
// Inside the guard, not before it: that rebuild reads the audit store, and a throw
|
|
901
|
+
// there would escape this sink and stall the apply loop for every later entry — the §8
|
|
902
|
+
// rule that a receive boundary settles its callers and keeps admission closed.
|
|
903
|
+
const author = (0, nodeIdMapping_ts_1.getNodeNameForId)(auditStore, event.nodeId, true);
|
|
904
|
+
if (!author) {
|
|
905
|
+
logger_ts_1.logger.warn?.('discarding a record lock control entry whose origin node could not be resolved');
|
|
906
|
+
return reportDroppedWrite(event, context, new Error('Record lock control origin could not be resolved'));
|
|
907
|
+
}
|
|
908
|
+
// The coordinator getter fails closed on an unusable node identity. That is right for
|
|
909
|
+
// an acquire and wrong here: rejecting out of this sink stalls the apply loop for
|
|
910
|
+
// every later entry rather than dropping one.
|
|
911
|
+
// `admittingCoordinator`, because `lockCoordinator` answers undefined while a transport
|
|
912
|
+
// is momentarily unregistered — and this sink runs off the replication stream, not off
|
|
913
|
+
// that transport. Dropping a peer's clean-handoff release there leaves the home holding
|
|
914
|
+
// its grant for the delegation's whole deadline.
|
|
915
|
+
target?.admittingCoordinator?.applyEntry(entry, author, event.timestamp);
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
logger_ts_1.logger.warn?.('dropping a record lock control entry: the coordinator is unavailable', error);
|
|
919
|
+
return reportDroppedWrite(event, context, error);
|
|
920
|
+
}
|
|
921
|
+
};
|
|
649
922
|
// perform the write of an individual write event
|
|
650
923
|
const writeUpdate = async (event, context) => {
|
|
924
|
+
if ((0, auditStore_ts_1.isLockControlType)(event.type))
|
|
925
|
+
return applyLockControlEvent(event, context);
|
|
651
926
|
const value = event.value;
|
|
652
|
-
const Table = event.table ? databases_ts_1.databases[databaseName][event.table] :
|
|
927
|
+
const Table = event.table ? databases_ts_1.databases[databaseName][event.table] : _a;
|
|
653
928
|
if (databaseName === hdbTerms_ts_1.SYSTEM_SCHEMA_NAME &&
|
|
654
929
|
(event.table === hdbTerms_ts_1.SYSTEM_TABLE_NAMES.ROLE_TABLE_NAME || event.table === hdbTerms_ts_1.SYSTEM_TABLE_NAMES.USER_TABLE_NAME)) {
|
|
655
930
|
userRoleUpdate = true;
|
|
@@ -666,6 +941,9 @@ function makeTable(options) {
|
|
|
666
941
|
ensureLoaded: false,
|
|
667
942
|
nodeId: event.nodeId,
|
|
668
943
|
viaNodeId: event.viaNodeId,
|
|
944
|
+
// the origin's record version, stored as-is so every replica holds the version the
|
|
945
|
+
// origin holds; the transaction's own timestamp stays the origin's log key
|
|
946
|
+
version: event.version,
|
|
669
947
|
// use per-event expiresAt: batched txn context only holds the first event's expiration
|
|
670
948
|
expiresAt: event.expiresAt,
|
|
671
949
|
// bulk base-copy snapshot frame: apply current-state directly, without an audit/transaction-log
|
|
@@ -675,6 +953,15 @@ function makeTable(options) {
|
|
|
675
953
|
async: true,
|
|
676
954
|
};
|
|
677
955
|
const id = event.id;
|
|
956
|
+
if (!isSourceWriteType(event.type)) {
|
|
957
|
+
logger_ts_1.logger.error?.('Unknown operation', event.type, event.id);
|
|
958
|
+
const notification = reportDroppedWrite(event, context, new Error('Unknown source operation'));
|
|
959
|
+
if (event.finished)
|
|
960
|
+
await event.finished;
|
|
961
|
+
return notification;
|
|
962
|
+
}
|
|
963
|
+
if (Table && event.type === 'put' && value == null && !shouldRevalidateEvents)
|
|
964
|
+
await reportDroppedWrite(event, context, new Error('Source-applied put has no record content'));
|
|
678
965
|
const resource = await Table.getResource(id, context, options);
|
|
679
966
|
if (event.finished)
|
|
680
967
|
await event.finished;
|
|
@@ -696,15 +983,18 @@ function makeTable(options) {
|
|
|
696
983
|
return resource._writeInvalidate(id, value, options);
|
|
697
984
|
case 'relocate':
|
|
698
985
|
return resource._writeRelocate(id, options);
|
|
699
|
-
default:
|
|
700
|
-
logger_ts_1.logger.error?.('Unknown operation', event.type, event.id);
|
|
701
986
|
}
|
|
702
987
|
};
|
|
703
988
|
/** Keeps the writes to any one key in arrival order; see DESIGN.md (harper#2211). */
|
|
704
989
|
const stageWrite = (event, context) => {
|
|
990
|
+
// A grant must not queue behind whatever the key it names is doing.
|
|
991
|
+
if ((0, auditStore_ts_1.isLockControlType)(event.type) ||
|
|
992
|
+
!isSourceWriteType(event.type) ||
|
|
993
|
+
(event.type === 'put' && event.value == null && !shouldRevalidateEvents))
|
|
994
|
+
return writeUpdate(event, context);
|
|
705
995
|
let chainKey;
|
|
706
996
|
try {
|
|
707
|
-
const Table = event.table ? databases_ts_1.databases[databaseName][event.table] :
|
|
997
|
+
const Table = event.table ? databases_ts_1.databases[databaseName][event.table] : _a;
|
|
708
998
|
const id = event.id ?? (event.value ? event.value[Table?.primaryKey] : undefined);
|
|
709
999
|
if (id != null && typeof id !== 'symbol')
|
|
710
1000
|
chainKey = `${event.table ?? tableName} ${chainKeyForId(id)}`;
|
|
@@ -745,14 +1035,18 @@ function makeTable(options) {
|
|
|
745
1035
|
omitCurrent: true,
|
|
746
1036
|
};
|
|
747
1037
|
const subscribeOnThisThread = source.subscribeOnThisThread
|
|
748
|
-
? source.subscribeOnThisThread((0, manageThreads_js_1.
|
|
749
|
-
: (0, manageThreads_js_1.
|
|
1038
|
+
? source.subscribeOnThisThread((0, manageThreads_js_1.applicationWorkerIndex)(), subscriptionOptions)
|
|
1039
|
+
: (0, manageThreads_js_1.runsApplicationCodeSingletons)(); // set up by the defining application's code, so it runs where that code does
|
|
750
1040
|
const subscription = hasSubscribe && subscribeOnThisThread && (await source.subscribe?.(subscriptionOptions));
|
|
751
1041
|
if (subscription) {
|
|
752
1042
|
let txnInProgress;
|
|
753
1043
|
// we listen for events by iterating through the async iterator provided by the subscription
|
|
754
1044
|
for await (const event of subscription) {
|
|
1045
|
+
let failureEvent = event;
|
|
1046
|
+
let failurePosition;
|
|
1047
|
+
let applied = false;
|
|
755
1048
|
try {
|
|
1049
|
+
failurePosition = event?.timestamp;
|
|
756
1050
|
if (!event || typeof event !== 'object') {
|
|
757
1051
|
logger_ts_1.logger.error?.('Bad subscription event', event);
|
|
758
1052
|
continue;
|
|
@@ -760,6 +1054,7 @@ function makeTable(options) {
|
|
|
760
1054
|
const firstWrite = event.type === 'transaction' ? event.writes[0] : event;
|
|
761
1055
|
if (!firstWrite) {
|
|
762
1056
|
logger_ts_1.logger.error?.('Bad subscription event', event);
|
|
1057
|
+
await (0, replicatedApplyFailure_ts_1.notifyReplicatedApplyFailure)(databaseName, event, failurePosition, new Error('Subscription transaction has no writes'), tableName);
|
|
763
1058
|
continue;
|
|
764
1059
|
}
|
|
765
1060
|
event.source = source;
|
|
@@ -768,11 +1063,16 @@ function makeTable(options) {
|
|
|
768
1063
|
// there is no re-subscribe / sequence-id-resume path to recover it. Mark the context so the
|
|
769
1064
|
// commit retries such conflicts without a cap (see DatabaseTransaction commit).
|
|
770
1065
|
event.sourceApply = true;
|
|
1066
|
+
event[SOURCE_APPLY_POSITION] = failurePosition;
|
|
771
1067
|
if (event.type === 'end_txn') {
|
|
772
1068
|
// Capture the in-progress transaction in a stable local: the loop variable is reset
|
|
773
1069
|
// once this transaction completes (below), but the seq-id closure and the commit await
|
|
774
1070
|
// still need to reference it afterward.
|
|
775
1071
|
const committingTxn = txnInProgress;
|
|
1072
|
+
if (committingTxn) {
|
|
1073
|
+
failureEvent = committingTxn;
|
|
1074
|
+
failurePosition = committingTxn[SOURCE_APPLY_POSITION];
|
|
1075
|
+
}
|
|
776
1076
|
committingTxn?.resolve();
|
|
777
1077
|
let updateRecordedSequenceId;
|
|
778
1078
|
if (event.localTime && lastSequenceId !== event.localTime) {
|
|
@@ -855,6 +1155,7 @@ function makeTable(options) {
|
|
|
855
1155
|
let committed;
|
|
856
1156
|
try {
|
|
857
1157
|
committed = committingTxn ? await committingTxn.committed : undefined;
|
|
1158
|
+
applied = true;
|
|
858
1159
|
if (event.onCommit) {
|
|
859
1160
|
// the onCommit callback can be async and carry associated work (e.g. blob
|
|
860
1161
|
// transfer); wait for it too before recording the sequence id. Pass the commit
|
|
@@ -891,6 +1192,7 @@ function makeTable(options) {
|
|
|
891
1192
|
// than rethrow) so the current beginTxn still starts a fresh transaction with
|
|
892
1193
|
// correct boundaries instead of having its writes applied as standalone ones.
|
|
893
1194
|
logger_ts_1.logger.error?.('source-applied transaction commit failed during apply', error);
|
|
1195
|
+
await (0, replicatedApplyFailure_ts_1.notifyReplicatedApplyFailure)(databaseName, txnInProgress, txnInProgress[SOURCE_APPLY_POSITION], error, tableName);
|
|
894
1196
|
}
|
|
895
1197
|
finally {
|
|
896
1198
|
// Clear it regardless of outcome so a rejected commit isn't re-awaited on the
|
|
@@ -904,7 +1206,9 @@ function makeTable(options) {
|
|
|
904
1206
|
continue;
|
|
905
1207
|
}
|
|
906
1208
|
}
|
|
907
|
-
//
|
|
1209
|
+
// A source that reports no log position of its own (no `timestamp`) has only one clock,
|
|
1210
|
+
// so its record version doubles as the apply transaction's timestamp. A replication
|
|
1211
|
+
// receiver always sets `timestamp` from the origin's log key and never reaches this.
|
|
908
1212
|
if (!event.timestamp && event.version)
|
|
909
1213
|
event.timestamp = event.version;
|
|
910
1214
|
const commitResolution = (0, transaction_ts_1.transaction)(event, () => {
|
|
@@ -978,6 +1282,7 @@ function makeTable(options) {
|
|
|
978
1282
|
// standalone write: backpressure on the commit before pulling the next event,
|
|
979
1283
|
// and pass the commit resolution through to the callback.
|
|
980
1284
|
const committed = commitResolution ? await commitResolution : undefined;
|
|
1285
|
+
applied = true;
|
|
981
1286
|
await event.onCommit(committed);
|
|
982
1287
|
}
|
|
983
1288
|
}
|
|
@@ -988,6 +1293,15 @@ function makeTable(options) {
|
|
|
988
1293
|
}
|
|
989
1294
|
catch (error) {
|
|
990
1295
|
logger_ts_1.logger.error?.('error in subscription handler', error);
|
|
1296
|
+
if (!applied)
|
|
1297
|
+
await (0, replicatedApplyFailure_ts_1.notifyReplicatedApplyFailure)(databaseName, failureEvent, failurePosition, error, tableName);
|
|
1298
|
+
}
|
|
1299
|
+
finally {
|
|
1300
|
+
while (pendingApplyFailures) {
|
|
1301
|
+
const notification = pendingApplyFailures;
|
|
1302
|
+
pendingApplyFailures = undefined;
|
|
1303
|
+
await notification;
|
|
1304
|
+
}
|
|
991
1305
|
}
|
|
992
1306
|
}
|
|
993
1307
|
}
|
|
@@ -1005,7 +1319,7 @@ function makeTable(options) {
|
|
|
1005
1319
|
/** Indicates if the events should be revalidated when they are received. By default we do this if the get
|
|
1006
1320
|
* method is overriden */
|
|
1007
1321
|
static get shouldRevalidateEvents() {
|
|
1008
|
-
return this.prototype.get !==
|
|
1322
|
+
return this.prototype.get !== _a.prototype.get;
|
|
1009
1323
|
}
|
|
1010
1324
|
/**
|
|
1011
1325
|
* Gets a resource instance, as defined by the Resource class, adding the table-specific handling
|
|
@@ -1041,7 +1355,7 @@ function makeTable(options) {
|
|
|
1041
1355
|
}
|
|
1042
1356
|
return loadLocalRecord(id, request, { transaction: readTxn, ensureLoaded: resourceOptions?.ensureLoaded }, sync, (entry) => {
|
|
1043
1357
|
if (entry) {
|
|
1044
|
-
|
|
1358
|
+
_a._updateResource(this, entry);
|
|
1045
1359
|
}
|
|
1046
1360
|
else
|
|
1047
1361
|
this.#record = null;
|
|
@@ -1059,7 +1373,7 @@ function makeTable(options) {
|
|
|
1059
1373
|
if (loadingFromSource) {
|
|
1060
1374
|
txn?.disregardReadTxn(); // this could take some time, so don't keep the transaction open if possible
|
|
1061
1375
|
return (0, when_ts_1.when)(loadingFromSource, (entry) => {
|
|
1062
|
-
|
|
1376
|
+
_a._updateResource(this, entry);
|
|
1063
1377
|
return this;
|
|
1064
1378
|
});
|
|
1065
1379
|
}
|
|
@@ -1251,15 +1565,24 @@ function makeTable(options) {
|
|
|
1251
1565
|
* This also informs the scheduling for record eviction.
|
|
1252
1566
|
* @param opts Time in seconds until records expire, or an options object with `expiration`, `eviction`,
|
|
1253
1567
|
* and `scanInterval` (all in seconds, all optional). Number form preserves any previously configured
|
|
1254
|
-
* eviction/scanInterval; object form replaces all three.
|
|
1568
|
+
* eviction/scanInterval; object form replaces all three. An internal schema ownership-only call with
|
|
1569
|
+
* none of those values preserves the settings already loaded from the catalog.
|
|
1255
1570
|
*/
|
|
1256
1571
|
static setTTLExpiration(opts) {
|
|
1257
1572
|
if (opts == null || (typeof opts !== 'number' && typeof opts !== 'object'))
|
|
1258
1573
|
throw new Error('Invalid expiration value type');
|
|
1574
|
+
const declaredHere = typeof opts === 'object' && opts.fromSchema;
|
|
1575
|
+
const isolatedApplicationOwner = declaredHere && opts.isolatedApplicationOwner;
|
|
1576
|
+
const preserveLoadedConfiguration = declaredHere && opts.expiration === undefined && opts.eviction === undefined && opts.scanInterval === undefined;
|
|
1577
|
+
if (((!ttlFromLoad && !declaredHere) || isolatedApplicationOwner) && !ttlConfiguredByApplication) {
|
|
1578
|
+
ttlConfiguredByApplication = true;
|
|
1579
|
+
// the scan owner may have changed with this: re-evaluate even if the interval did not
|
|
1580
|
+
lastCleanupInterval = undefined;
|
|
1581
|
+
}
|
|
1259
1582
|
if (typeof opts === 'number') {
|
|
1260
1583
|
expirationMs = opts * 1000;
|
|
1261
1584
|
}
|
|
1262
|
-
else {
|
|
1585
|
+
else if (!preserveLoadedConfiguration) {
|
|
1263
1586
|
// `??` so an explicit 0 is treated as the user's chosen value, not as "missing"
|
|
1264
1587
|
expirationMs = (opts.expiration ?? 0) * 1000;
|
|
1265
1588
|
evictionMs = (opts.eviction ?? 0) * 1000;
|
|
@@ -1267,10 +1590,19 @@ function makeTable(options) {
|
|
|
1267
1590
|
}
|
|
1268
1591
|
if (expirationMs < 0)
|
|
1269
1592
|
throw new Error('Expiration can not be negative');
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1593
|
+
if (!preserveLoadedConfiguration) {
|
|
1594
|
+
// default to one quarter of the total expiration+eviction window
|
|
1595
|
+
cleanupInterval = cleanupInterval || (expirationMs + evictionMs) / 4;
|
|
1596
|
+
expirationScanScheduled = true;
|
|
1597
|
+
}
|
|
1598
|
+
// Re-evaluate an existing table-level scan after an ownership-only declaration, but do not
|
|
1599
|
+
// create the default daily cleanup timer for a table that has only an @expiresAt field.
|
|
1600
|
+
if (!preserveLoadedConfiguration || expirationScanScheduled || evictionMs)
|
|
1601
|
+
scheduleCleanup();
|
|
1602
|
+
// @expiresAt has its own interval rather than the cleanup timer above. Arm it whenever a live
|
|
1603
|
+
// declaration introduces the attribute, including after this application already claimed TTL.
|
|
1604
|
+
if (expiresAtProperty && !recordExpirationInterval)
|
|
1605
|
+
runRecordExpirationEviction();
|
|
1274
1606
|
}
|
|
1275
1607
|
static getResidencyRecord(id) {
|
|
1276
1608
|
// getSync (not get): callers consume the result synchronously (e.g. residency.includes(...) in a
|
|
@@ -1279,7 +1611,7 @@ function makeTable(options) {
|
|
|
1279
1611
|
return dbisDb.getSync([Symbol.for('residency_by_id'), id]);
|
|
1280
1612
|
}
|
|
1281
1613
|
static setResidency(getResidency) {
|
|
1282
|
-
|
|
1614
|
+
_a.getResidency =
|
|
1283
1615
|
getResidency &&
|
|
1284
1616
|
((record, context) => {
|
|
1285
1617
|
try {
|
|
@@ -1292,7 +1624,7 @@ function makeTable(options) {
|
|
|
1292
1624
|
});
|
|
1293
1625
|
}
|
|
1294
1626
|
static setResidencyById(getResidencyById) {
|
|
1295
|
-
|
|
1627
|
+
_a.getResidencyById =
|
|
1296
1628
|
getResidencyById &&
|
|
1297
1629
|
((id) => {
|
|
1298
1630
|
try {
|
|
@@ -1305,8 +1637,8 @@ function makeTable(options) {
|
|
|
1305
1637
|
});
|
|
1306
1638
|
}
|
|
1307
1639
|
static getResidency(record, context) {
|
|
1308
|
-
if (
|
|
1309
|
-
return
|
|
1640
|
+
if (_a.getResidencyById) {
|
|
1641
|
+
return _a.getResidencyById(record[primaryKey]);
|
|
1310
1642
|
}
|
|
1311
1643
|
let count = replicateToCount;
|
|
1312
1644
|
if (context.replicateTo != undefined) {
|
|
@@ -1348,7 +1680,7 @@ function makeTable(options) {
|
|
|
1348
1680
|
return; // already enabled
|
|
1349
1681
|
audit = true;
|
|
1350
1682
|
addDeleteRemoval();
|
|
1351
|
-
|
|
1683
|
+
_a.audit = true;
|
|
1352
1684
|
}
|
|
1353
1685
|
/**
|
|
1354
1686
|
* Coerce the id as a string to the correct type for the primary key
|
|
@@ -1360,7 +1692,57 @@ function makeTable(options) {
|
|
|
1360
1692
|
return null;
|
|
1361
1693
|
return coerceType(id, primaryKeyAttribute);
|
|
1362
1694
|
}
|
|
1695
|
+
/**
|
|
1696
|
+
* A branch's Table classes deliberately carry the BASE's logical database name so an
|
|
1697
|
+
* application's schema and code resolve unchanged (harper#643). That makes every schema
|
|
1698
|
+
* mutation resolve against the global catalog — a `dropTable()` through a branch would delete
|
|
1699
|
+
* the live base table. Reads and writes are per-branch and unaffected; DDL is refused until a
|
|
1700
|
+
* branch owns a schema identity of its own.
|
|
1701
|
+
*/
|
|
1702
|
+
static assertSchemaMutable(operation) {
|
|
1703
|
+
if (!isBranch)
|
|
1704
|
+
return;
|
|
1705
|
+
const error = new Error(`Cannot ${operation} through a branched database: '${tableName}' resolves to the schema of base ` +
|
|
1706
|
+
`database '${databaseName}', so the change would apply to the base rather than the branch`);
|
|
1707
|
+
error.statusCode = 400;
|
|
1708
|
+
throw error;
|
|
1709
|
+
}
|
|
1363
1710
|
static async dropTable() {
|
|
1711
|
+
_a.assertSchemaMutable('drop a table');
|
|
1712
|
+
const rootStore = primaryStore.rootStore;
|
|
1713
|
+
if (databaseName === databasePath &&
|
|
1714
|
+
rootStore instanceof rocksdb_js_1.RocksDatabase &&
|
|
1715
|
+
dbisDb.put !== dbisDb.putSync)
|
|
1716
|
+
throw new Error(`Cannot drop ${databaseName}.${_a.tableName}: the catalog store's put is asynchronous, so the drop tombstone cannot be made durable before the column families are dropped`);
|
|
1717
|
+
// Release post-commit derived-index delivery before any destructive work: the runner's
|
|
1718
|
+
// backend must have quiesced before its stores and native file are destroyed, and a
|
|
1719
|
+
// same-name recreate must not race an owner still applying to the old generation.
|
|
1720
|
+
const derivedIndexRuntime = _a.derivedIndexRuntime;
|
|
1721
|
+
const restoreDerivedIndexesAfterFailedDrop = () => {
|
|
1722
|
+
try {
|
|
1723
|
+
_a.derivedIndexRuntime = derivedIndexRuntime?.restoreAfterFailedDrop?.();
|
|
1724
|
+
}
|
|
1725
|
+
catch (restoreError) {
|
|
1726
|
+
_a.derivedIndexRuntime = undefined;
|
|
1727
|
+
logger_ts_1.logger.error?.(`Could not restore derived indexes after failed drop of ${databaseName}.${_a.tableName}`, restoreError);
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
try {
|
|
1731
|
+
await derivedIndexRuntime?.close(true);
|
|
1732
|
+
}
|
|
1733
|
+
catch (error) {
|
|
1734
|
+
restoreDerivedIndexesAfterFailedDrop();
|
|
1735
|
+
throw error;
|
|
1736
|
+
}
|
|
1737
|
+
const abortStaleDrop = () => {
|
|
1738
|
+
derivedIndexRuntime?.completeDrop?.(false);
|
|
1739
|
+
_a.derivedIndexRuntime = undefined;
|
|
1740
|
+
_a.cleanup();
|
|
1741
|
+
if (databases_ts_1.databases[databaseName]?.[tableName] === _a)
|
|
1742
|
+
delete databases_ts_1.databases[databaseName][tableName];
|
|
1743
|
+
};
|
|
1744
|
+
let dropIdentityConfirmed = databaseName !== databasePath;
|
|
1745
|
+
let primaryCatalogKey = _a.tableName + '/';
|
|
1364
1746
|
if (databaseName === databasePath) {
|
|
1365
1747
|
// Persist a drop tombstone on the primary catalog entry BEFORE any
|
|
1366
1748
|
// destructive work. If the process dies or a column family drop fails
|
|
@@ -1368,9 +1750,21 @@ function makeTable(options) {
|
|
|
1368
1750
|
// the next startup (or a same-name create) completes the drop via
|
|
1369
1751
|
// completeInterruptedDrop in databases.ts instead of resurrecting
|
|
1370
1752
|
// the table.
|
|
1371
|
-
|
|
1372
|
-
const
|
|
1373
|
-
|
|
1753
|
+
let tombstoneWrite;
|
|
1754
|
+
const writeTombstone = () => {
|
|
1755
|
+
let primaryMeta = dbisDb.getSync(primaryCatalogKey);
|
|
1756
|
+
if (!primaryMeta && primaryKey) {
|
|
1757
|
+
const legacyPrimaryKey = `${_a.tableName}/${primaryKey}`;
|
|
1758
|
+
const legacyPrimaryMeta = dbisDb.getSync(legacyPrimaryKey);
|
|
1759
|
+
if (legacyPrimaryMeta?.isPrimaryKey) {
|
|
1760
|
+
primaryCatalogKey = legacyPrimaryKey;
|
|
1761
|
+
primaryMeta = legacyPrimaryMeta;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
if (!primaryMeta || (primaryMeta.tableId != null && primaryMeta.tableId !== tableId))
|
|
1765
|
+
return false;
|
|
1766
|
+
if (primaryMeta.dropping)
|
|
1767
|
+
return true;
|
|
1374
1768
|
primaryMeta.dropping = true;
|
|
1375
1769
|
// Stamps this drop's identity so the interrupted-drop retry budget in
|
|
1376
1770
|
// databases.ts can be scoped to THIS drop rather than the table name: a
|
|
@@ -1380,14 +1774,33 @@ function makeTable(options) {
|
|
|
1380
1774
|
// the budget by generation instead makes the new drop's tombstone carry
|
|
1381
1775
|
// its own fresh key regardless of what any worker last observed.
|
|
1382
1776
|
primaryMeta.dropGeneration = (0, node_crypto_1.randomUUID)();
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
if (
|
|
1388
|
-
await
|
|
1777
|
+
tombstoneWrite = dbisDb.put(primaryCatalogKey, primaryMeta);
|
|
1778
|
+
return true;
|
|
1779
|
+
};
|
|
1780
|
+
try {
|
|
1781
|
+
if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
|
|
1782
|
+
// withUpdateAttributesLock's locked section cannot be held across an await, so a durable
|
|
1783
|
+
// tombstone depends on put being rebound to putSync for RocksDB primary stores.
|
|
1784
|
+
dropIdentityConfirmed = withUpdateAttributesLock(rootStore, `drop table '${databaseName}.${_a.tableName}'`, writeTombstone);
|
|
1785
|
+
}
|
|
1786
|
+
else {
|
|
1787
|
+
rootStore.transactionSync(() => {
|
|
1788
|
+
dropIdentityConfirmed = writeTombstone();
|
|
1789
|
+
});
|
|
1790
|
+
if (typeof tombstoneWrite?.then === 'function')
|
|
1791
|
+
await tombstoneWrite;
|
|
1792
|
+
}
|
|
1389
1793
|
}
|
|
1794
|
+
catch (error) {
|
|
1795
|
+
restoreDerivedIndexesAfterFailedDrop();
|
|
1796
|
+
throw error;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
if (!dropIdentityConfirmed) {
|
|
1800
|
+
abortStaleDrop();
|
|
1801
|
+
return;
|
|
1390
1802
|
}
|
|
1803
|
+
_a.derivedIndexRuntime = undefined;
|
|
1391
1804
|
// A get() against a sourcedFrom table resolves to its caller before the resolved
|
|
1392
1805
|
// record's cache write has committed (see getFromSource) - the write lands "in the
|
|
1393
1806
|
// background" for latency reasons. Flip this BEFORE removing the table from the
|
|
@@ -1400,7 +1813,8 @@ function makeTable(options) {
|
|
|
1400
1813
|
// family drops below. If a drop fails past this point the table stays
|
|
1401
1814
|
// invisible, and the tombstone guarantees the drop completes on the
|
|
1402
1815
|
// next startup (or on a same-name create).
|
|
1403
|
-
|
|
1816
|
+
if (databases_ts_1.databases[databaseName]?.[tableName] === _a)
|
|
1817
|
+
delete databases_ts_1.databases[databaseName][tableName];
|
|
1404
1818
|
// The above stops new source-fill writes from starting, but a write from a get()
|
|
1405
1819
|
// that already returned to its caller may still be in flight. Dropping the column
|
|
1406
1820
|
// families out from under that write is a genuine invariant violation, not just a
|
|
@@ -1431,14 +1845,21 @@ function makeTable(options) {
|
|
|
1431
1845
|
]);
|
|
1432
1846
|
clearTimeout(timer);
|
|
1433
1847
|
if (result === timedOut) {
|
|
1848
|
+
derivedIndexRuntime?.completeDrop?.();
|
|
1434
1849
|
throw new Error(`dropTable() timed out after ${LOCK_TIMEOUT}ms waiting for ${pending.length} in-flight source-populated cache write(s) on ${tableName} to settle; refusing to drop the column families out from under a write that may still be staged. The drop tombstone is durable, so this will be retried on the next load.`);
|
|
1435
1850
|
}
|
|
1436
1851
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
(
|
|
1852
|
+
try {
|
|
1853
|
+
for (const entry of primaryStore.getRange({ versions: true, snapshot: false, lazy: true })) {
|
|
1854
|
+
if (entry.metadataFlags & auditStore_ts_1.HAS_BLOBS && entry.value) {
|
|
1855
|
+
(0, blob_ts_1.deleteBlobsInObject)(entry.value);
|
|
1856
|
+
}
|
|
1440
1857
|
}
|
|
1441
1858
|
}
|
|
1859
|
+
catch (error) {
|
|
1860
|
+
derivedIndexRuntime?.completeDrop?.();
|
|
1861
|
+
throw error;
|
|
1862
|
+
}
|
|
1442
1863
|
if (databaseName === databasePath) {
|
|
1443
1864
|
// part of a database.
|
|
1444
1865
|
// Drop the column families, then remove the catalog metadata - never
|
|
@@ -1455,66 +1876,94 @@ function makeTable(options) {
|
|
|
1455
1876
|
// same-name create completes the interrupted drop and writes fresh
|
|
1456
1877
|
// catalog rows, and clobbering those would orphan the new table.
|
|
1457
1878
|
const removeTombstonedCatalog = () => {
|
|
1458
|
-
const currentPrimary = dbisDb.getSync(
|
|
1459
|
-
if (!currentPrimary?.dropping)
|
|
1879
|
+
const currentPrimary = dbisDb.getSync(primaryCatalogKey);
|
|
1880
|
+
if (!currentPrimary?.dropping || (currentPrimary.tableId != null && currentPrimary.tableId !== tableId))
|
|
1460
1881
|
return false;
|
|
1461
1882
|
for (const attribute of attributes) {
|
|
1462
|
-
dbisDb.remove(
|
|
1883
|
+
dbisDb.remove(_a.tableName + '/' + attribute.name);
|
|
1463
1884
|
}
|
|
1464
|
-
dbisDb.remove(
|
|
1885
|
+
dbisDb.remove(primaryCatalogKey);
|
|
1465
1886
|
return true;
|
|
1466
1887
|
};
|
|
1467
|
-
const rootStore = primaryStore.rootStore;
|
|
1468
1888
|
if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
|
|
1469
1889
|
// Serialize the drops + catalog removal against a concurrent
|
|
1470
1890
|
// same-name create (and completeInterruptedDrop) under the database's
|
|
1471
1891
|
// 'update-attributes' exclusive lock - the same lock the create path
|
|
1472
|
-
// holds. It is a synchronous
|
|
1892
|
+
// holds. It is a synchronous lock wait that blocks the event loop, so
|
|
1473
1893
|
// the locked section MUST stay synchronous: drop with dropSync (as
|
|
1474
1894
|
// completeInterruptedDrop does), never an awaited drop(), or a
|
|
1475
|
-
// concurrent create's
|
|
1476
|
-
//
|
|
1477
|
-
|
|
1478
|
-
let removed = false;
|
|
1895
|
+
// concurrent create's wait would be stuck on a drop that the blocked
|
|
1896
|
+
// event loop can never resolve, burning its full deadline before failing.
|
|
1897
|
+
let removed;
|
|
1479
1898
|
try {
|
|
1480
|
-
|
|
1481
|
-
const
|
|
1482
|
-
if (
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1899
|
+
removed = withUpdateAttributesLock(rootStore, `table '${databaseName}.${tableName}'`, () => {
|
|
1900
|
+
const currentPrimary = dbisDb.getSync(primaryCatalogKey);
|
|
1901
|
+
if (!currentPrimary?.dropping || (currentPrimary.tableId != null && currentPrimary.tableId !== tableId))
|
|
1902
|
+
return false;
|
|
1903
|
+
for (const attribute of attributes) {
|
|
1904
|
+
const index = indices[attribute.name];
|
|
1905
|
+
if (index)
|
|
1906
|
+
try {
|
|
1907
|
+
index.customIndex?.resetDerivedStorage?.();
|
|
1908
|
+
index.dropSync();
|
|
1909
|
+
}
|
|
1910
|
+
catch (error) {
|
|
1911
|
+
ignoreAlreadyDropped(error);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
try {
|
|
1915
|
+
primaryStore.dropSync();
|
|
1916
|
+
}
|
|
1917
|
+
catch (error) {
|
|
1918
|
+
ignoreAlreadyDropped(error);
|
|
1919
|
+
}
|
|
1920
|
+
return removeTombstonedCatalog();
|
|
1921
|
+
});
|
|
1922
|
+
if (removed)
|
|
1923
|
+
await dbisDb.committed;
|
|
1497
1924
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1925
|
+
catch (error) {
|
|
1926
|
+
derivedIndexRuntime?.completeDrop?.();
|
|
1927
|
+
throw error;
|
|
1928
|
+
}
|
|
1929
|
+
if (!removed) {
|
|
1930
|
+
abortStaleDrop();
|
|
1931
|
+
return;
|
|
1500
1932
|
}
|
|
1501
|
-
if (removed)
|
|
1502
|
-
await dbisDb.committed;
|
|
1503
1933
|
}
|
|
1504
1934
|
else {
|
|
1505
1935
|
// LMDB: no shared column-family double-drop, and its engine lock is
|
|
1506
1936
|
// transactional rather than this spin lock, so keep the awaited drop
|
|
1507
1937
|
// plus the same tombstone-guarded catalog removal.
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const
|
|
1511
|
-
if (
|
|
1512
|
-
|
|
1938
|
+
let removed;
|
|
1939
|
+
try {
|
|
1940
|
+
const currentPrimary = dbisDb.getSync(primaryCatalogKey);
|
|
1941
|
+
if (!currentPrimary?.dropping || (currentPrimary.tableId != null && currentPrimary.tableId !== tableId)) {
|
|
1942
|
+
abortStaleDrop();
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
const drops = [];
|
|
1946
|
+
for (const attribute of attributes) {
|
|
1947
|
+
const index = indices[attribute.name];
|
|
1948
|
+
if (index) {
|
|
1949
|
+
index.customIndex?.resetDerivedStorage?.();
|
|
1950
|
+
drops.push(index.drop().catch(ignoreAlreadyDropped));
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
drops.push(primaryStore.drop().catch(ignoreAlreadyDropped));
|
|
1954
|
+
await Promise.all(drops);
|
|
1955
|
+
removed = removeTombstonedCatalog();
|
|
1956
|
+
if (removed)
|
|
1957
|
+
await dbisDb.committed;
|
|
1958
|
+
}
|
|
1959
|
+
catch (error) {
|
|
1960
|
+
derivedIndexRuntime?.completeDrop?.();
|
|
1961
|
+
throw error;
|
|
1962
|
+
}
|
|
1963
|
+
if (!removed) {
|
|
1964
|
+
abortStaleDrop();
|
|
1965
|
+
throw new Error(`Could not complete drop of ${databaseName}.${tableName}: a replacement table became current while the LMDB stores were being dropped`);
|
|
1513
1966
|
}
|
|
1514
|
-
drops.push(primaryStore.drop().catch(ignoreAlreadyDropped));
|
|
1515
|
-
await Promise.all(drops);
|
|
1516
|
-
if (removeTombstonedCatalog())
|
|
1517
|
-
await dbisDb.committed;
|
|
1518
1967
|
}
|
|
1519
1968
|
}
|
|
1520
1969
|
else {
|
|
@@ -1522,11 +1971,18 @@ function makeTable(options) {
|
|
|
1522
1971
|
// assigns `primaryStore.auditStore` — openAuditStore() assigns `rootStore.auditStore`, and
|
|
1523
1972
|
// this is the reference makeTable() was handed. Awaited so a pass suspended mid-removal has
|
|
1524
1973
|
// released the primary DBI before it is closed and unlinked.
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1974
|
+
try {
|
|
1975
|
+
await auditStore?.stopAuditCleanup?.();
|
|
1976
|
+
(0, storageReclamation_ts_1.removeStorageReclamation)(primaryStore.path);
|
|
1977
|
+
await primaryStore.close();
|
|
1978
|
+
node_fs_1.default.unlinkSync(primaryStore.path);
|
|
1979
|
+
}
|
|
1980
|
+
catch (error) {
|
|
1981
|
+
derivedIndexRuntime?.completeDrop?.();
|
|
1982
|
+
throw error;
|
|
1983
|
+
}
|
|
1529
1984
|
}
|
|
1985
|
+
derivedIndexRuntime?.completeDrop?.();
|
|
1530
1986
|
signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, hdbTerms_ts_1.OPERATIONS_ENUM.DROP_TABLE, databaseName, tableName));
|
|
1531
1987
|
}
|
|
1532
1988
|
// #section: read-path
|
|
@@ -1554,7 +2010,7 @@ function makeTable(options) {
|
|
|
1554
2010
|
estimatedRecordRange: undefined,
|
|
1555
2011
|
};
|
|
1556
2012
|
if (this.getContext()?.includeExpensiveRecordCountEstimates) {
|
|
1557
|
-
return
|
|
2013
|
+
return _a.getRecordCount().then((recordCount) => {
|
|
1558
2014
|
description.recordCount = recordCount.recordCount;
|
|
1559
2015
|
description.estimatedRecordRange = recordCount.estimatedRange;
|
|
1560
2016
|
return description;
|
|
@@ -1779,12 +2235,18 @@ function makeTable(options) {
|
|
|
1779
2235
|
else {
|
|
1780
2236
|
id = requestTargetToId(target);
|
|
1781
2237
|
}
|
|
2238
|
+
if (this.#writeGeneration?.closed) {
|
|
2239
|
+
this.#changes = undefined;
|
|
2240
|
+
this.#writeGeneration = undefined;
|
|
2241
|
+
}
|
|
2242
|
+
this.#assertLiveHandle(id, true);
|
|
1782
2243
|
const context = this.getContext();
|
|
1783
2244
|
const envTxn = txnForContext(context);
|
|
1784
2245
|
if (!envTxn)
|
|
1785
2246
|
throw new Error('Can not update a table resource outside of a transaction');
|
|
1786
2247
|
// record in the list of updating records so it can be written to the database when we commit
|
|
1787
|
-
|
|
2248
|
+
// `false` is the patch-cancel sentinel, not a record root — but only incrementally.
|
|
2249
|
+
if (updates === false && !fullUpdate) {
|
|
1788
2250
|
// TODO: Remove from transaction
|
|
1789
2251
|
return this;
|
|
1790
2252
|
}
|
|
@@ -1832,22 +2294,98 @@ function makeTable(options) {
|
|
|
1832
2294
|
});
|
|
1833
2295
|
}
|
|
1834
2296
|
}
|
|
1835
|
-
|
|
2297
|
+
// Keep absent changes distinguishable from an explicit empty patch: framework-created
|
|
2298
|
+
// post/publish updates do not necessarily mutate or save the instance.
|
|
2299
|
+
// A supplied root must reach validation as itself, not as the staged changes (harper#1298).
|
|
2300
|
+
const recordRoot = updates === undefined ? this.#changes : updates;
|
|
2301
|
+
return (0, when_ts_1.when)(this._writeUpdate(id, recordRoot, fullUpdate), () => this);
|
|
1836
2302
|
}
|
|
1837
2303
|
/**
|
|
1838
2304
|
* Save any changes into this instance to the current transaction
|
|
1839
2305
|
*/
|
|
1840
2306
|
save() {
|
|
2307
|
+
const operation = this.#savingOperation;
|
|
2308
|
+
if (!this.#lockWritable &&
|
|
2309
|
+
this.#writeGeneration?.closed &&
|
|
2310
|
+
(!operation || operation.writeGeneration === this.#writeGeneration))
|
|
2311
|
+
return;
|
|
2312
|
+
this.#assertLiveHandle(operation?.key ?? this.getId()); // a write through a released or expired lock never lands
|
|
2313
|
+
if ((!operation || operation.dropped) && this.#lockWritable && this.#lockHandle?.hold) {
|
|
2314
|
+
// A held lock's record stages its update here rather than at lock() time: it is often
|
|
2315
|
+
// written after the acquiring transaction has already completed, which would have
|
|
2316
|
+
// dropped an update staged then. Nothing set means nothing to stage — a held-but-untouched
|
|
2317
|
+
// id stays untouched. Scoped locks do not take this branch: #reloadLocked stages their
|
|
2318
|
+
// TransactionWrite at lock() time (exactly like update()), so #savingOperation is always
|
|
2319
|
+
// set for a live scoped lock and the ordinary path below applies.
|
|
2320
|
+
// Verify the hold is still alive: if #lockWritable is set but the handle expired or was
|
|
2321
|
+
// released between lock acquisition and this save(), throw 409 rather than silently
|
|
2322
|
+
// committing stale data. Every lock-writable instance carries its own handle.
|
|
2323
|
+
const saveHandle = this.#lockHandle;
|
|
2324
|
+
if (saveHandle.isExpired()) {
|
|
2325
|
+
throw (0, recordLock_ts_1.lockNotHeldError)(saveHandle);
|
|
2326
|
+
}
|
|
2327
|
+
const changes = this.#changes;
|
|
2328
|
+
if (changes && Object.keys(changes).length > 0) {
|
|
2329
|
+
this.#savingOperation = null;
|
|
2330
|
+
return (0, when_ts_1.when)(this._writeUpdate(this.getId(), changes, false), () => {
|
|
2331
|
+
const op = this.#savingOperation;
|
|
2332
|
+
if (op?.dropped) {
|
|
2333
|
+
this.#changes = undefined;
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
// Clear #savingOperation so the next sequential save() enters the lock-writable
|
|
2337
|
+
// path and creates a fresh write (otherwise a non-null #savingOperation makes
|
|
2338
|
+
// save() take the #saveOperation branch with an already-committed write, which
|
|
2339
|
+
// is a no-op, silently dropping the new change).
|
|
2340
|
+
// op.innerCommit is the real native-transaction commit Promise set on the
|
|
2341
|
+
// immediateCommit path in DatabaseTransaction.save(); await it to ensure
|
|
2342
|
+
// durability before resolving to the caller.
|
|
2343
|
+
if (op?.saved) {
|
|
2344
|
+
this.#savingOperation = null;
|
|
2345
|
+
return op?.innerCommit;
|
|
2346
|
+
}
|
|
2347
|
+
// op.saved = false means addWrite deferred the save; #saveOperation commits it
|
|
2348
|
+
// synchronously but ImmediateTransaction.save() returns undefined while the
|
|
2349
|
+
// inner rocksdb commit is still pending — return innerCommit so the caller
|
|
2350
|
+
// actually waits for durability.
|
|
2351
|
+
return (0, when_ts_1.when)(this.save(), () => op?.innerCommit);
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
// No changes: nothing to stage. A dropped operation (detached at a scoped→hold
|
|
2355
|
+
// upgrade — see detachScopedUpgradeWrite) must not fall through to the ordinary
|
|
2356
|
+
// #saveOperation path below with its now-detached reference.
|
|
2357
|
+
if (!operation || operation.dropped) {
|
|
2358
|
+
this.#savingOperation = null;
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
1841
2362
|
if (this.#savingOperation) {
|
|
2363
|
+
const operation = this.#savingOperation;
|
|
2364
|
+
this.#savingOperation = null;
|
|
2365
|
+
// A write that lands via a nested immediateCommit (e.g. a second sequential save() on
|
|
2366
|
+
// the same ImmediateTransaction context, once the first has already closed it) sets
|
|
2367
|
+
// operation.innerCommit to the real native-commit promise, but the commit() sweep loop
|
|
2368
|
+
// that triggers it discards its own return value — #saveOperation()'s result can
|
|
2369
|
+
// resolve before that native commit actually settles. Chain on innerCommit (as the
|
|
2370
|
+
// lock-writable hold branch above already does) so callers awaiting save() see the
|
|
2371
|
+
// write durably land, not just the outer (possibly premature) resolution.
|
|
2372
|
+
let result;
|
|
1842
2373
|
try {
|
|
1843
|
-
|
|
2374
|
+
result = this.#saveOperation(operation);
|
|
1844
2375
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
2376
|
+
catch (error) {
|
|
2377
|
+
if (!operation.saved)
|
|
2378
|
+
this.#savingOperation = operation;
|
|
2379
|
+
throw error;
|
|
1847
2380
|
}
|
|
2381
|
+
const innerCommit = operation.innerCommit;
|
|
2382
|
+
return innerCommit ? (0, when_ts_1.when)(innerCommit, () => result) : result;
|
|
1848
2383
|
}
|
|
1849
2384
|
}
|
|
1850
2385
|
#saveOperation(operation) {
|
|
2386
|
+
// LMDB validates staged writes at transaction commit, so bind a lazy update to the
|
|
2387
|
+
// generation selected by save() before another update can replace its changes.
|
|
2388
|
+
operation.captureChanges?.();
|
|
1851
2389
|
const transaction = txnForContext(this.getContext());
|
|
1852
2390
|
const holder = operation.stagedIn;
|
|
1853
2391
|
// never-drop-on-conflict lives on the transaction and would not travel with the write, so an
|
|
@@ -1865,13 +2403,26 @@ function makeTable(options) {
|
|
|
1865
2403
|
// merge and index diff would be relative to a record that may never land.
|
|
1866
2404
|
operation.priorWrite = undefined;
|
|
1867
2405
|
operation.deferSave = false;
|
|
1868
|
-
|
|
2406
|
+
const result = (0, when_ts_1.when)(transaction.addWrite(operation), () => operation.promise ?? operation.result);
|
|
2407
|
+
this.#closeWriteChain(operation);
|
|
2408
|
+
return result;
|
|
1869
2409
|
}
|
|
1870
2410
|
const owner = holder ?? transaction;
|
|
1871
|
-
if (owner.save)
|
|
1872
|
-
|
|
2411
|
+
if (owner.save) {
|
|
2412
|
+
const result = owner.save(operation) || operation.promise || operation.result;
|
|
2413
|
+
this.#closeWriteChain(operation);
|
|
2414
|
+
return result;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
#closeWriteChain(operation) {
|
|
2418
|
+
const owner = operation.stagedIn;
|
|
2419
|
+
for (let write = operation; write && !write.instanceClosed; write = write.priorWrite) {
|
|
2420
|
+
if (write === operation || owner?.ownedWrites?.has(write))
|
|
2421
|
+
(0, DatabaseTransaction_ts_1.closeWriteInstance)(write);
|
|
2422
|
+
}
|
|
1873
2423
|
}
|
|
1874
2424
|
addTo(property, value) {
|
|
2425
|
+
this[tracked_ts_1.ASSERT_TRACKED_WRITABLE]();
|
|
1875
2426
|
if (typeof value === 'number' || typeof value === 'bigint') {
|
|
1876
2427
|
if (this.#savingOperation?.fullUpdate)
|
|
1877
2428
|
this.set(property, (+this.getProperty(property) || 0) + value);
|
|
@@ -1923,17 +2474,23 @@ function makeTable(options) {
|
|
|
1923
2474
|
});
|
|
1924
2475
|
}
|
|
1925
2476
|
_writeInvalidate(id, partialRecord, options) {
|
|
2477
|
+
this.#assertLiveHandle(id);
|
|
1926
2478
|
const context = this.getContext();
|
|
1927
2479
|
checkValidId(id);
|
|
1928
2480
|
const transaction = txnForContext(this.getContext());
|
|
2481
|
+
assertDerivedIndexAdmission(options, transaction);
|
|
1929
2482
|
const write = {
|
|
1930
2483
|
key: id,
|
|
1931
2484
|
store: primaryStore,
|
|
1932
2485
|
invalidated: true,
|
|
1933
2486
|
entry: this.#entry,
|
|
2487
|
+
recordVersion: options?.version,
|
|
2488
|
+
lockHandle: this.#lockHandle && this.#lockHandle.keyId === (0, DatabaseTransaction_ts_1.writeKeyId)(id) ? this.#lockHandle : undefined,
|
|
2489
|
+
reloadCommitBase: true,
|
|
1934
2490
|
commit: (txnTime, existingEntry, _retry, transaction) => {
|
|
2491
|
+
const txnLogKey = isRocksDB && options?.version != null ? (transaction?.getTimestamp?.() ?? txnTime) : txnTime;
|
|
1935
2492
|
write.skipped = false; // reset on each retry; cleanup happens after commit if still true
|
|
1936
|
-
if (precedesExistingVersion(txnTime, existingEntry, options?.nodeId)
|
|
2493
|
+
if (precedesExistingVersion(txnTime, existingEntry, options?.nodeId) < 0) {
|
|
1937
2494
|
write.skipped = true;
|
|
1938
2495
|
return;
|
|
1939
2496
|
}
|
|
@@ -1954,7 +2511,13 @@ function makeTable(options) {
|
|
|
1954
2511
|
viaNodeId: options?.viaNodeId,
|
|
1955
2512
|
transaction,
|
|
1956
2513
|
tableToTrack: tableName,
|
|
2514
|
+
recordVersion: txnTime,
|
|
2515
|
+
additionalAuditRefs: isRocksDB && audit && txnLogKey !== txnTime
|
|
2516
|
+
? [{ version: txnLogKey, nodeId: options?.nodeId }]
|
|
2517
|
+
: undefined,
|
|
1957
2518
|
}, 'invalidate');
|
|
2519
|
+
if (write.trackRecordVersion)
|
|
2520
|
+
write.recordVersionApplied = true;
|
|
1958
2521
|
// TODO: recordDeletion?
|
|
1959
2522
|
},
|
|
1960
2523
|
};
|
|
@@ -1962,21 +2525,27 @@ function makeTable(options) {
|
|
|
1962
2525
|
transaction.addWrite(write);
|
|
1963
2526
|
}
|
|
1964
2527
|
_writeRelocate(id, options) {
|
|
2528
|
+
this.#assertLiveHandle(id);
|
|
1965
2529
|
const context = this.getContext();
|
|
1966
2530
|
checkValidId(id);
|
|
1967
2531
|
const transaction = txnForContext(this.getContext());
|
|
1968
|
-
transaction
|
|
2532
|
+
assertDerivedIndexAdmission(options, transaction);
|
|
2533
|
+
const write = {
|
|
1969
2534
|
key: id,
|
|
1970
2535
|
store: primaryStore,
|
|
1971
2536
|
invalidated: true,
|
|
1972
2537
|
entry: this.#entry,
|
|
2538
|
+
recordVersion: options?.version,
|
|
2539
|
+
lockHandle: this.#lockHandle && this.#lockHandle.keyId === (0, DatabaseTransaction_ts_1.writeKeyId)(id) ? this.#lockHandle : undefined,
|
|
2540
|
+
reloadCommitBase: true,
|
|
1973
2541
|
before: this.constructor.source?.relocate && !context?.source
|
|
1974
2542
|
? this.constructor.source.relocate.bind(this.constructor.source, id, undefined, context)
|
|
1975
2543
|
: undefined,
|
|
1976
2544
|
commit: (txnTime, existingEntry, _retry, transaction) => {
|
|
1977
|
-
|
|
2545
|
+
const txnLogKey = isRocksDB && options?.version != null ? (transaction?.getTimestamp?.() ?? txnTime) : txnTime;
|
|
2546
|
+
if (precedesExistingVersion(txnTime, existingEntry, options?.nodeId) < 0)
|
|
1978
2547
|
return;
|
|
1979
|
-
const residency =
|
|
2548
|
+
const residency = _a.getResidencyRecord(options.residencyId);
|
|
1980
2549
|
let metadata = 0;
|
|
1981
2550
|
let newRecord = null;
|
|
1982
2551
|
const existingRecord = existingEntry?.value;
|
|
@@ -2000,9 +2569,16 @@ function makeTable(options) {
|
|
|
2000
2569
|
viaNodeId: options?.viaNodeId,
|
|
2001
2570
|
expiresAt: options.expiresAt,
|
|
2002
2571
|
transaction,
|
|
2572
|
+
recordVersion: txnTime,
|
|
2573
|
+
additionalAuditRefs: isRocksDB && audit && txnLogKey !== txnTime
|
|
2574
|
+
? [{ version: txnLogKey, nodeId: options?.nodeId }]
|
|
2575
|
+
: undefined,
|
|
2003
2576
|
}, 'relocate', false, null);
|
|
2577
|
+
if (write.trackRecordVersion)
|
|
2578
|
+
write.recordVersionApplied = true;
|
|
2004
2579
|
},
|
|
2005
|
-
}
|
|
2580
|
+
};
|
|
2581
|
+
transaction.addWrite(write);
|
|
2006
2582
|
}
|
|
2007
2583
|
/**
|
|
2008
2584
|
* Record the relocation of an entry (when a record is moved to a different node), return true if it is now located locally
|
|
@@ -2055,9 +2631,8 @@ function makeTable(options) {
|
|
|
2055
2631
|
if (primaryStore.hasLock(id, entry.version))
|
|
2056
2632
|
return;
|
|
2057
2633
|
}
|
|
2058
|
-
//
|
|
2059
|
-
//
|
|
2060
|
-
// removed the entry entirely, but first cleanup indices
|
|
2634
|
+
// Eviction is not a canonical delete. Indexed caching tables add a local-only control entry so
|
|
2635
|
+
// their derived indexes can remove the resident projection without exposing a delete event.
|
|
2061
2636
|
let lmdbCompletion;
|
|
2062
2637
|
if (primaryStore.ifVersion) {
|
|
2063
2638
|
// lmdb: the index cleanup and the record removal are both version-guarded optimistic writes.
|
|
@@ -2071,6 +2646,7 @@ function makeTable(options) {
|
|
|
2071
2646
|
}
|
|
2072
2647
|
else {
|
|
2073
2648
|
updateIndices(id, existingRecord, null, options);
|
|
2649
|
+
stageDerivedIndexEviction(transaction, id, existingVersion);
|
|
2074
2650
|
(0, RecordEncoder_ts_1.removeEntry)(primaryStore, entry ?? primaryStore.getEntry(id), options);
|
|
2075
2651
|
}
|
|
2076
2652
|
committed = true;
|
|
@@ -2121,10 +2697,368 @@ function makeTable(options) {
|
|
|
2121
2697
|
}
|
|
2122
2698
|
}
|
|
2123
2699
|
/**
|
|
2124
|
-
*
|
|
2700
|
+
* Static entry point: `Table.lock(id, options?, context?)` — creates an instance in the given,
|
|
2701
|
+
* ambient, or a fresh context and delegates to the instance lock(). This shadows Resource.static
|
|
2702
|
+
* lock so that both callers share the same transaction link (required for cross-instance upgrade
|
|
2703
|
+
* detection). lock() is an in-process API with no authorization hook of its own; it is not
|
|
2704
|
+
* protocol-dispatched, so no allowUpdate/allowCreate check runs on acquisition.
|
|
2705
|
+
*
|
|
2706
|
+
* Dropping the trailing `context` leaks the key: the bare `{}` fallback is an
|
|
2707
|
+
* ImmediateTransaction, which releases no record locks.
|
|
2708
|
+
*/
|
|
2709
|
+
static async lock(target, options, context) {
|
|
2710
|
+
if (!isRocksDB)
|
|
2711
|
+
throw new hdbError_ts_1.ClientError('Record locks are not supported on LMDB', 501);
|
|
2712
|
+
if (options === undefined && isPlainOptions(target)) {
|
|
2713
|
+
options = target;
|
|
2714
|
+
target = undefined;
|
|
2715
|
+
}
|
|
2716
|
+
const id = target != null ? requestTargetToId(target) : null;
|
|
2717
|
+
const resolvedContext = contextArgument(context) ?? transaction_ts_1.contextStorage.getStore() ?? {};
|
|
2718
|
+
const resource = new _a(id, resolvedContext);
|
|
2719
|
+
return resource.lock(target, options);
|
|
2720
|
+
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Acquire an exclusive lock on this record (or on `target`'s) and return it ready for updates
|
|
2723
|
+
* (harper#483, Phase 0: exclusive across every worker thread of this node). The lock is held
|
|
2724
|
+
* in process memory only — no durable writes. Phase 0 contract: lock() is mutually exclusive
|
|
2725
|
+
* with other lock() calls on the same key; plain writes (put/patch/delete/create) are never
|
|
2726
|
+
* gated or blocked. The generation expires after `lease` if it is never released.
|
|
2727
|
+
*
|
|
2728
|
+
* Transaction-scoped (default): write through the returned record (or the table's static verbs
|
|
2729
|
+
* in the same transaction), and the commit or abort releases it. `{ hold: true }`: the lock
|
|
2730
|
+
* outlives the transaction; write through the returned record and release with `unlock()`, or
|
|
2731
|
+
* let the lease expire.
|
|
2732
|
+
*/
|
|
2733
|
+
// async so option/id validation rejects rather than throwing past a caller's `.catch()`; the
|
|
2734
|
+
// body still runs to completion synchronously, which is what keeps concurrent lock() calls
|
|
2735
|
+
// on one key coalescing instead of racing to tryLock.
|
|
2736
|
+
async lock(target, options) {
|
|
2737
|
+
if (!isRocksDB)
|
|
2738
|
+
throw new hdbError_ts_1.ClientError('Record locks are not supported on LMDB', 501);
|
|
2739
|
+
if (options === undefined && isPlainOptions(target)) {
|
|
2740
|
+
options = target;
|
|
2741
|
+
target = undefined;
|
|
2742
|
+
}
|
|
2743
|
+
const id = target != null ? requestTargetToId(target) : this.getId();
|
|
2744
|
+
checkValidId(id);
|
|
2745
|
+
this.#assertLiveHandle(id);
|
|
2746
|
+
const resolved = (0, recordLock_ts_1.resolveLockOptions)(options);
|
|
2747
|
+
const context = this.getContext();
|
|
2748
|
+
const link = txnForContext(context);
|
|
2749
|
+
const keyId = (0, DatabaseTransaction_ts_1.writeKeyId)(id);
|
|
2750
|
+
// Before the re-entrant paths, not after: a transaction that already holds this key
|
|
2751
|
+
// node-scoped would otherwise be handed that handle back for an explicit cluster request,
|
|
2752
|
+
// while the same request on a fresh key fails closed.
|
|
2753
|
+
if (resolved.scope === 'cluster' &&
|
|
2754
|
+
(resolved.scopeRequested || (0, recordLockCoordinator_ts_1.isClusterLockRequired)(databaseName)) &&
|
|
2755
|
+
!(0, recordLockCoordinator_ts_1.getClusterLockTransport)(databaseName))
|
|
2756
|
+
return Promise.reject(new hdbError_ts_1.LockUnavailableError(`Cluster-scoped record locks are not available on ${databaseName}: no record lock transport is registered`));
|
|
2757
|
+
const held = this.#lockHandle;
|
|
2758
|
+
if (held && !held.isExpired() && held.keyId === keyId) {
|
|
2759
|
+
// Re-entrant: upgrade to hold if requested, then preserve staged changes.
|
|
2760
|
+
const violation = scopeViolation(held, resolved, databaseName);
|
|
2761
|
+
if (violation)
|
|
2762
|
+
return Promise.reject(violation);
|
|
2763
|
+
if (resolved.hold && !held.hold) {
|
|
2764
|
+
held.upgradeToHold(resolved.lease);
|
|
2765
|
+
// The scoped phase eagerly staged a TransactionWrite (see #reloadLocked); hold
|
|
2766
|
+
// staging is deferred and explicit-save-only, so an unsaved scoped write left
|
|
2767
|
+
// dangling here would otherwise auto-commit at the transaction sweep and clobber
|
|
2768
|
+
// whatever the hold write lands. detachScopedUpgradeWrite marks it .dropped so a
|
|
2769
|
+
// later save() on this instance falls through to the hold branch instead of the
|
|
2770
|
+
// dead #savingOperation reference.
|
|
2771
|
+
detachScopedUpgradeWrite(link, keyId, held);
|
|
2772
|
+
}
|
|
2773
|
+
return Promise.resolve(this.#reloadLocked(id, undefined, true));
|
|
2774
|
+
}
|
|
2775
|
+
const scoped = link.recordLockFor(primaryStore, keyId);
|
|
2776
|
+
if (scoped && !scoped.isExpired()) {
|
|
2777
|
+
const violation = scopeViolation(scoped, resolved, databaseName);
|
|
2778
|
+
if (violation)
|
|
2779
|
+
return Promise.reject(violation);
|
|
2780
|
+
if (resolved.hold && !scoped.hold) {
|
|
2781
|
+
// Upgrade scoped → hold: flip the existing handle object to hold mode so every
|
|
2782
|
+
// instance that already references this handle stays valid. Retiring and creating a
|
|
2783
|
+
// new handle would invalidate those other references (their save() would then throw
|
|
2784
|
+
// 409 against a released handle). The native key stays locked throughout.
|
|
2785
|
+
scoped.upgradeToHold(resolved.lease);
|
|
2786
|
+
detachScopedUpgradeWrite(link, keyId, scoped);
|
|
2787
|
+
return Promise.resolve(this.#reloadLocked(id, scoped, true));
|
|
2788
|
+
}
|
|
2789
|
+
// Already held with the same type: re-entrant return. Preserve any staged changes.
|
|
2790
|
+
return Promise.resolve(this.#reloadLocked(id, scoped, true));
|
|
2791
|
+
}
|
|
2792
|
+
// Cluster scope needs a registered transport. An EXPLICIT { scope: 'cluster' } without one is
|
|
2793
|
+
// a caller asking for a guarantee this node cannot make, so it fails closed rather than
|
|
2794
|
+
// silently returning the node-local lock; the default keeps Phase 0 behavior, which is what
|
|
2795
|
+
// a build with no replication has anyway.
|
|
2796
|
+
// The getter fails closed on an unusable node identity, and lock() answers with a promise.
|
|
2797
|
+
let coordinator;
|
|
2798
|
+
try {
|
|
2799
|
+
coordinator = resolved.scope === 'node' ? undefined : _a.lockCoordinator;
|
|
2800
|
+
}
|
|
2801
|
+
catch (error) {
|
|
2802
|
+
return Promise.reject(error);
|
|
2803
|
+
}
|
|
2804
|
+
const key = (0, recordLock_ts_1.lockAttemptKey)(tableId, id);
|
|
2805
|
+
// Coalesce concurrent lock() calls for the same key inside one link so they don't
|
|
2806
|
+
// self-block: Promise.all([T.lock(id), T.lock(id)]) would otherwise have both calls
|
|
2807
|
+
// reach tryLock before either registers, making the second park against the first.
|
|
2808
|
+
const pending = link.pendingLockFor(primaryStore, keyId);
|
|
2809
|
+
if (pending) {
|
|
2810
|
+
// Wait for the in-flight acquisition, then take the re-entrant path as if
|
|
2811
|
+
// recordLockFor had found it. If the first attempt timed out, re-enter so
|
|
2812
|
+
// the second caller gets its own timeout.
|
|
2813
|
+
// The follower waits on the leader's acquisition, but only for its own timeout.
|
|
2814
|
+
let followerTimer;
|
|
2815
|
+
const followerTimedOut = Symbol('follower timeout');
|
|
2816
|
+
// Why the leader failed, so the follower can report that instead of inventing contention
|
|
2817
|
+
// when its own budget runs out. A leader 503 means the guarantee could not be established
|
|
2818
|
+
// at all; retrying is still right (the condition may clear) but 423 at the end is not.
|
|
2819
|
+
let leaderFailure;
|
|
2820
|
+
const followerStart = Date.now();
|
|
2821
|
+
const followerDeadline = new Promise((_, reject) => {
|
|
2822
|
+
followerTimer = setTimeout(() => reject(followerTimedOut), resolved.timeout).unref();
|
|
2823
|
+
});
|
|
2824
|
+
// Try again on this caller's own terms with the budget it has left.
|
|
2825
|
+
const retryOnRemainingBudget = () => {
|
|
2826
|
+
// The enclosing transaction ended while we were parked. A retry re-resolves the
|
|
2827
|
+
// context, which no longer points at this link, so the handle it acquired would be
|
|
2828
|
+
// registered on a fresh transaction that no commit or abort ever releases — the
|
|
2829
|
+
// same abandonment the leader's own post-acquisition guard below rejects.
|
|
2830
|
+
if (link.open === DatabaseTransaction_ts_1.TRANSACTION_STATE.CLOSED && !link.saveCommits)
|
|
2831
|
+
throw new hdbError_ts_1.ServerError('Transaction was closed while waiting for a record lock', 500);
|
|
2832
|
+
const remaining = resolved.timeout - (Date.now() - followerStart);
|
|
2833
|
+
if (remaining <= 0)
|
|
2834
|
+
throw leaderFailure ?? new hdbError_ts_1.ClientError(`Record is locked and was not released in time`, 423);
|
|
2835
|
+
// Carry the scope only if the caller named it: spreading the resolved options would turn
|
|
2836
|
+
// a defaulted 'cluster' into an explicit one, which is fail-closed when no transport is
|
|
2837
|
+
// registered.
|
|
2838
|
+
return this.lock(target, {
|
|
2839
|
+
lease: resolved.lease,
|
|
2840
|
+
timeout: remaining,
|
|
2841
|
+
hold: resolved.hold,
|
|
2842
|
+
scope: resolved.scopeRequested ? resolved.scope : undefined,
|
|
2843
|
+
});
|
|
2844
|
+
};
|
|
2845
|
+
return Promise.race([pending, followerDeadline]).then(() => {
|
|
2846
|
+
clearTimeout(followerTimer);
|
|
2847
|
+
const acquired = link.recordLockFor(primaryStore, keyId);
|
|
2848
|
+
if (acquired && !acquired.isExpired()) {
|
|
2849
|
+
const violation = scopeViolation(acquired, resolved, databaseName);
|
|
2850
|
+
if (violation)
|
|
2851
|
+
throw violation;
|
|
2852
|
+
if (resolved.hold && !acquired.hold) {
|
|
2853
|
+
detachScopedUpgradeWrite(link, keyId, acquired);
|
|
2854
|
+
acquired.upgradeToHold(resolved.lease);
|
|
2855
|
+
}
|
|
2856
|
+
return this.#reloadLocked(id, acquired, true);
|
|
2857
|
+
}
|
|
2858
|
+
return retryOnRemainingBudget();
|
|
2859
|
+
}, (error) => {
|
|
2860
|
+
clearTimeout(followerTimer);
|
|
2861
|
+
// A follower that simply ran out of its own wait was waiting on another caller in this
|
|
2862
|
+
// process, which is the contention 423 describes. But if the LEADER failed for a reason
|
|
2863
|
+
// that is not contention, that reason is the true one — keep it and report it if the
|
|
2864
|
+
// retries below also run out, rather than ending on a 423 for a key nobody held.
|
|
2865
|
+
if (error === followerTimedOut)
|
|
2866
|
+
throw new hdbError_ts_1.ClientError(`Record is locked and was not released in time`, 423);
|
|
2867
|
+
if (error instanceof hdbError_ts_1.LockUnavailableError)
|
|
2868
|
+
leaderFailure = error;
|
|
2869
|
+
return retryOnRemainingBudget();
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
const pendingPromise = (0, recordLock_ts_1.acquireRecordKey)(link, primaryStore, key, keyId, resolved.timeout, resolved.lease, resolved.hold);
|
|
2873
|
+
const clusterStart = node_perf_hooks_1.performance.now();
|
|
2874
|
+
// What a follower waits on must span the cluster round and registration, not just the native
|
|
2875
|
+
// acquire. Waking it at the native hand-off leaves it in a window where the key is held but no
|
|
2876
|
+
// handle is registered, so it retries and parks on the leader's own lock for its full timeout
|
|
2877
|
+
// — inside a transaction that cannot finish until it gives up.
|
|
2878
|
+
const acquisition = pendingPromise.then(async (handle) => {
|
|
2879
|
+
const closedWhileWaiting = () => link.open === DatabaseTransaction_ts_1.TRANSACTION_STATE.CLOSED && !link.saveCommits;
|
|
2880
|
+
if (closedWhileWaiting()) {
|
|
2881
|
+
// The transaction was aborted while this call waited; nothing would ever release the handle.
|
|
2882
|
+
handle.release();
|
|
2883
|
+
throw new hdbError_ts_1.ServerError('Transaction was closed while waiting for a record lock', 500);
|
|
2884
|
+
}
|
|
2885
|
+
// Anything that fails from here must give the native key back, or it becomes a lock this
|
|
2886
|
+
// caller does not know it owns.
|
|
2887
|
+
// Re-resolved, not the snapshot taken before `acquireRecordKey`: that wait can run the
|
|
2888
|
+
// caller's whole timeout, long enough for harper-pro to register the transport on this
|
|
2889
|
+
// worker. Using the snapshot would take the native key alone and hand back a node-scoped
|
|
2890
|
+
// handle while a peer that already had the transport is granted the same key.
|
|
2891
|
+
try {
|
|
2892
|
+
if (resolved.scope !== 'node')
|
|
2893
|
+
coordinator = _a.lockCoordinator ?? coordinator;
|
|
2894
|
+
}
|
|
2895
|
+
catch (error) {
|
|
2896
|
+
// The getter fails closed on an unusable node identity, and that has to reach the caller
|
|
2897
|
+
// the same way it does before the wait. Swallowing it let an implicit cluster lock fall
|
|
2898
|
+
// through to node-local authority — the one outcome failing closed exists to prevent —
|
|
2899
|
+
// because `coordinator` is still whatever it was, including undefined.
|
|
2900
|
+
handle.release();
|
|
2901
|
+
throw error;
|
|
2902
|
+
}
|
|
2903
|
+
if (coordinator) {
|
|
2904
|
+
try {
|
|
2905
|
+
// Not a 423 when the budget is gone, and not a skip either: the native wait can consume
|
|
2906
|
+
// the whole timeout, and `acquire` with no wait left still admits from a live delegation
|
|
2907
|
+
// or a local grant without sending anything. Only if it cannot does the caller learn the
|
|
2908
|
+
// guarantee was unavailable — which is not the same as the key being held.
|
|
2909
|
+
const remaining = Math.max(0, resolved.timeout - (node_perf_hooks_1.performance.now() - clusterStart));
|
|
2910
|
+
const round = await coordinator.acquire(id, resolved.lease, remaining);
|
|
2911
|
+
// Resolved through the getter rather than captured, so a transport swap between
|
|
2912
|
+
// acquisition and release reaches the coordinator that now owns the delegation.
|
|
2913
|
+
if (!handle.joinClusterRound(round.tsR, resolved.lease, round.mintedMono, () => _a.admittingCoordinator?.release(id, round.admissionId))) {
|
|
2914
|
+
// The round completed inside its lease but the lease elapsed before the handle
|
|
2915
|
+
// could take it. The coordinator still holds it, and only this call knows the
|
|
2916
|
+
// hold was never handed out.
|
|
2917
|
+
// The getter, not the captured coordinator: after a transport swap the captured one no
|
|
2918
|
+
// longer owns this admission, so releasing through it would be a silent no-op.
|
|
2919
|
+
// `.then`, not `Promise.resolve(release())`: the call can throw synchronously, and that
|
|
2920
|
+
// throw would escape the catch and replace the 423 below with an internal error.
|
|
2921
|
+
Promise.resolve()
|
|
2922
|
+
.then(() => _a.admittingCoordinator?.release(id, round.admissionId))
|
|
2923
|
+
.catch(noop);
|
|
2924
|
+
// 503, not 423: the home granted this key to US and the lease elapsed before the handle
|
|
2925
|
+
// could take it, so nobody ever held it. The coordinator classifies the same thing the
|
|
2926
|
+
// same way — see its `timeout` denial.
|
|
2927
|
+
throw new hdbError_ts_1.LockUnavailableError(`A cluster record lock on ${databaseName}.${tableName} was granted after its lease had elapsed`);
|
|
2928
|
+
}
|
|
2929
|
+
// A recall must be able to fence a write this handle staged and then unlocked, so
|
|
2930
|
+
// the coordinator needs a way to revoke it — see LockCoordinator.registerAdmission.
|
|
2931
|
+
// The getter again: a swap during the acquisition moved this admission to the
|
|
2932
|
+
// successor, and registering on the predecessor would revoke a handle that is fine.
|
|
2933
|
+
_a.admittingCoordinator?.registerAdmission(round.admissionId, () => handle.revokeLease());
|
|
2934
|
+
}
|
|
2935
|
+
catch (error) {
|
|
2936
|
+
handle.release();
|
|
2937
|
+
throw error;
|
|
2938
|
+
}
|
|
2939
|
+
if (closedWhileWaiting()) {
|
|
2940
|
+
handle.release();
|
|
2941
|
+
throw new hdbError_ts_1.ServerError('Transaction was closed while waiting for a record lock', 500);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
link.registerRecordLock(handle);
|
|
2945
|
+
if (link.saveCommits && context?.timestamp)
|
|
2946
|
+
handle.noteCandidateFloor(context.timestamp);
|
|
2947
|
+
if (link.open === DatabaseTransaction_ts_1.TRANSACTION_STATE.OPEN && !link.saveCommits) {
|
|
2948
|
+
// Explicit transaction() (not ImmediateTransaction): pin the clock to
|
|
2949
|
+
// acquiredAt when no writes have been staged yet. When writes already
|
|
2950
|
+
// exist, leave the clock alone (ordering is best-effort; write held records
|
|
2951
|
+
// in their own transaction for the guarantee). ImmediateTransaction is
|
|
2952
|
+
// excluded (saveCommits=true) — its clock is never pinned in lock();
|
|
2953
|
+
// each save() stamps from the handle's committed version floor instead.
|
|
2954
|
+
if (link.writes.length === 0 && !link.timestamp) {
|
|
2955
|
+
link.timestamp = handle.acquiredAt;
|
|
2956
|
+
}
|
|
2957
|
+
if (!resolved.hold && link.transaction) {
|
|
2958
|
+
// Scoped lock: the read snapshot may predate the lock; drop it so the
|
|
2959
|
+
// scope reads what it locked. Hold locks use acquiredAt directly and
|
|
2960
|
+
// do not update the read snapshot.
|
|
2961
|
+
// The timestamp guard matches DatabaseTransaction's own setTimestamp calls: a
|
|
2962
|
+
// deferred update() write leaves the clock at 0, which rocksdb-js rejects.
|
|
2963
|
+
if (link.writes.length === 0 && link.readTxnsUsed <= 1) {
|
|
2964
|
+
link.releaseReadTxn();
|
|
2965
|
+
link.snapshotFree = true;
|
|
2966
|
+
}
|
|
2967
|
+
else if (link.timestamp)
|
|
2968
|
+
link.transaction.setTimestamp(link.timestamp);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
// ImmediateTransaction: no clock pinning in lock(); save() stamps each write
|
|
2972
|
+
// from the committed handle floor for both scoped and hold handles.
|
|
2973
|
+
return handle;
|
|
2974
|
+
});
|
|
2975
|
+
link.registerPendingLock(primaryStore, keyId, acquisition);
|
|
2976
|
+
return acquisition.then((handle) => {
|
|
2977
|
+
link.unregisterPendingLock(primaryStore, keyId);
|
|
2978
|
+
return this.#reloadLocked(id, handle);
|
|
2979
|
+
}, (error) => {
|
|
2980
|
+
link.unregisterPendingLock(primaryStore, keyId);
|
|
2981
|
+
throw error;
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
#reloadLocked(id, holdHandle, preserveChanges = false) {
|
|
2985
|
+
// For freshness, read the committed entry (snapshot-free) so a hold lock sees concurrent
|
|
2986
|
+
// committed writes rather than a stale snapshot. A write earlier in THIS explicit
|
|
2987
|
+
// transaction has not landed in that committed entry yet (harper#1968: Harper defers an
|
|
2988
|
+
// explicit transaction's writes until the writing call actually runs them), so pull the
|
|
2989
|
+
// current value the same way a chained write picks up its basis (priorStagedWrite): the
|
|
2990
|
+
// record comes from the prior staged write, the rest of the entry (version, audit chain,
|
|
2991
|
+
// blob metadata) stays the pre-transaction one.
|
|
2992
|
+
const link = txnForContext(this.getContext());
|
|
2993
|
+
let entryForReload = primaryStore.getEntry(id);
|
|
2994
|
+
if (link.open === DatabaseTransaction_ts_1.TRANSACTION_STATE.OPEN) {
|
|
2995
|
+
const keyId = (0, DatabaseTransaction_ts_1.writeKeyId)(id);
|
|
2996
|
+
const tailWrite = link.writesByKey?.get(primaryStore)?.get(keyId);
|
|
2997
|
+
const priorStaged = tailWrite && (tailWrite.stagedEntry !== undefined ? tailWrite : (0, DatabaseTransaction_ts_1.priorStagedWrite)(tailWrite));
|
|
2998
|
+
if (priorStaged?.stagedEntry !== undefined) {
|
|
2999
|
+
entryForReload = entryForReload
|
|
3000
|
+
? { ...entryForReload, value: priorStaged.stagedEntry.value }
|
|
3001
|
+
: { value: priorStaged.stagedEntry.value };
|
|
3002
|
+
if (entryForReload.value && typeof entryForReload.value === 'object') {
|
|
3003
|
+
// Register the merged entry in entryMap so getUpdatedTime() works.
|
|
3004
|
+
RecordEncoder_ts_1.entryMap.set(entryForReload.value, entryForReload);
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
if ((0, DatabaseTransaction_ts_1.writeKeyId)(id) !== (0, DatabaseTransaction_ts_1.writeKeyId)(this.getId())) {
|
|
3009
|
+
// lock(target) where target differs from this record: return a separate instance.
|
|
3010
|
+
const fresh = new this.constructor(id, this.getContext());
|
|
3011
|
+
_a._updateResource(fresh, entryForReload);
|
|
3012
|
+
if (holdHandle != null) {
|
|
3013
|
+
fresh.#lockHandle = holdHandle;
|
|
3014
|
+
// Do not clear this.#lockHandle: the original instance keeps its own lock on its
|
|
3015
|
+
// own id; the fresh instance owns the lock on the target id independently.
|
|
3016
|
+
}
|
|
3017
|
+
fresh.#lockWritable = true;
|
|
3018
|
+
// Scoped (not hold) stages exactly like update(): create the TransactionWrite now so
|
|
3019
|
+
// save() is the ordinary #savingOperation path. Hold keeps deferred staging (the
|
|
3020
|
+
// acquiring transaction may commit before the holder ever writes).
|
|
3021
|
+
if (!fresh.#lockHandle.hold)
|
|
3022
|
+
fresh._writeUpdate(id, fresh.#changes, false);
|
|
3023
|
+
return fresh;
|
|
3024
|
+
}
|
|
3025
|
+
// Store the handle for both scoped and hold locks; undefined (re-entrant hold fast-path)
|
|
3026
|
+
// must not clear a handle already set.
|
|
3027
|
+
if (holdHandle != null)
|
|
3028
|
+
this.#lockHandle = holdHandle;
|
|
3029
|
+
_a._updateResource(this, entryForReload);
|
|
3030
|
+
// Preserve staged changes when upgrading the same instance from scoped to hold so that
|
|
3031
|
+
// set() calls made under the scoped lock survive the reload.
|
|
3032
|
+
if (!preserveChanges)
|
|
3033
|
+
this.#changes = undefined;
|
|
3034
|
+
this.#lockWritable = true;
|
|
3035
|
+
// Scoped (not hold): stage now, same as update() would. Skip if a write from an earlier
|
|
3036
|
+
// lock() cycle on this instance is still pending (re-entrant call before its save()).
|
|
3037
|
+
if (!this.#lockHandle.hold && !this.#savingOperation)
|
|
3038
|
+
this._writeUpdate(id, this.#changes, false);
|
|
3039
|
+
return this;
|
|
3040
|
+
}
|
|
3041
|
+
/**
|
|
3042
|
+
* Release the lock this instance holds. Resolves true when this call cleared the native key lock.
|
|
3043
|
+
* Works for both held (`{ hold: true }`) and transaction-scoped locks. After unlock() the
|
|
3044
|
+
* instance is no longer lock-writable; writes through it require a fresh lock.
|
|
2125
3045
|
*/
|
|
2126
|
-
|
|
2127
|
-
|
|
3046
|
+
unlock() {
|
|
3047
|
+
// Always clear the local lock-writable state so subsequent writes on this instance are
|
|
3048
|
+
// ungated, regardless of whether the handle was already released.
|
|
3049
|
+
const handle = this.#lockHandle;
|
|
3050
|
+
this.#lockHandle = undefined;
|
|
3051
|
+
this.#lockWritable = false;
|
|
3052
|
+
if (!handle || handle.released)
|
|
3053
|
+
return Promise.resolve(false);
|
|
3054
|
+
const link = txnForContext(this.getContext());
|
|
3055
|
+
// A scoped lock staged its write at lock() time; released before commit, that write must not
|
|
3056
|
+
// run into the released-handle guard at the sweep.
|
|
3057
|
+
if (this.#savingOperation && !this.#savingOperation.saved && this.#savingOperation.lockHandle === handle)
|
|
3058
|
+
this.#savingOperation = null;
|
|
3059
|
+
detachScopedUpgradeWrite(link, (0, DatabaseTransaction_ts_1.writeKeyId)(this.getId()), handle);
|
|
3060
|
+
link.unregisterRecordLock(handle);
|
|
3061
|
+
return Promise.resolve(handle.release());
|
|
2128
3062
|
}
|
|
2129
3063
|
static operation(operation, context) {
|
|
2130
3064
|
operation.table ||= tableName;
|
|
@@ -2240,8 +3174,11 @@ function makeTable(options) {
|
|
|
2240
3174
|
// a notification that a write has already occurred in the canonical data source, we need to update our
|
|
2241
3175
|
// local copy
|
|
2242
3176
|
_writeUpdate(id, recordUpdate, fullUpdate, options) {
|
|
3177
|
+
this.#assertLiveHandle(id);
|
|
2243
3178
|
const context = this.getContext();
|
|
2244
3179
|
const transaction = txnForContext(context);
|
|
3180
|
+
const replaying = transaction.isReplay === true;
|
|
3181
|
+
assertDerivedIndexAdmission(options, transaction);
|
|
2245
3182
|
checkValidId(id);
|
|
2246
3183
|
if (fullUpdate && recordUpdate == null && options?.isNotification) {
|
|
2247
3184
|
// A source/replication-applied put must carry the record; these applies skip record
|
|
@@ -2256,6 +3193,16 @@ function makeTable(options) {
|
|
|
2256
3193
|
}
|
|
2257
3194
|
return;
|
|
2258
3195
|
}
|
|
3196
|
+
let captureChanges;
|
|
3197
|
+
if (recordUpdate === undefined) {
|
|
3198
|
+
let captured = false;
|
|
3199
|
+
captureChanges = () => {
|
|
3200
|
+
if (!captured) {
|
|
3201
|
+
captured = true;
|
|
3202
|
+
recordUpdate = this.#changes;
|
|
3203
|
+
}
|
|
3204
|
+
};
|
|
3205
|
+
}
|
|
2259
3206
|
const entry = this.#entry ?? primaryStore.getEntry(id, { transaction: transaction.getReadTxn() });
|
|
2260
3207
|
const writeToSource = () => {
|
|
2261
3208
|
if (!this.constructor.source || context?.source)
|
|
@@ -2277,16 +3224,31 @@ function makeTable(options) {
|
|
|
2277
3224
|
}
|
|
2278
3225
|
}
|
|
2279
3226
|
};
|
|
3227
|
+
const receiverId = this.getId();
|
|
3228
|
+
const closesReceiver = !this.isCollection &&
|
|
3229
|
+
!isSearchTarget(receiverId) &&
|
|
3230
|
+
(id === receiverId || (0, DatabaseTransaction_ts_1.writeKeyId)(id) === (0, DatabaseTransaction_ts_1.writeKeyId)(receiverId));
|
|
2280
3231
|
const write = {
|
|
2281
3232
|
key: id,
|
|
2282
3233
|
store: primaryStore,
|
|
2283
3234
|
entry,
|
|
2284
3235
|
nodeName: context?.nodeName,
|
|
2285
3236
|
fullUpdate,
|
|
3237
|
+
chainsStagedState: true,
|
|
3238
|
+
// copy-apply rows keep their pre-read base: one read per row, healed by the post-copy replay
|
|
3239
|
+
reloadCommitBase: options?.isCopyApply !== true,
|
|
2286
3240
|
deferSave: true,
|
|
3241
|
+
// the origin's record version on an applied write; absent for a locally-originated one
|
|
3242
|
+
recordVersion: options?.version,
|
|
3243
|
+
// Include the lock handle (if any) so the expired-handle guard in
|
|
3244
|
+
// DatabaseTransaction.save() can throw 409 when the lease has lapsed.
|
|
3245
|
+
// Only attach the hold handle when it covers exactly this key; off-key writes
|
|
3246
|
+
// are ordinary and must not carry an unrelated hold's handle.
|
|
3247
|
+
lockHandle: this.#lockHandle && this.#lockHandle.keyId === (0, DatabaseTransaction_ts_1.writeKeyId)(id) ? this.#lockHandle : undefined,
|
|
3248
|
+
writeGeneration: !this.#lockWritable && closesReceiver ? this[tracked_ts_1.GET_TRACKED_WRITE_GENERATION]() : undefined,
|
|
3249
|
+
captureChanges,
|
|
2287
3250
|
validate: (txnTime, committedBy = transaction) => {
|
|
2288
|
-
|
|
2289
|
-
recordUpdate = this.#changes;
|
|
3251
|
+
write.captureChanges?.();
|
|
2290
3252
|
if (fullUpdate || (recordUpdate && (0, tracked_ts_1.hasChanges)(this.#changes === recordUpdate ? this : recordUpdate))) {
|
|
2291
3253
|
if (!context?.source) {
|
|
2292
3254
|
committedBy.checkOverloaded();
|
|
@@ -2336,10 +3298,13 @@ function makeTable(options) {
|
|
|
2336
3298
|
: txnTime;
|
|
2337
3299
|
}
|
|
2338
3300
|
if (createdTimeProperty) {
|
|
2339
|
-
|
|
3301
|
+
// the reloaded commit base, not the pre-read one: a full PUT racing a create
|
|
3302
|
+
// would otherwise stamp a fresh created time over the real one
|
|
3303
|
+
const base = write.entry;
|
|
3304
|
+
if (base?.value) {
|
|
2340
3305
|
if (fullUpdate || recordUpdate[createdTimeProperty.name]) {
|
|
2341
3306
|
// make sure to retain original created time
|
|
2342
|
-
recordUpdate[createdTimeProperty.name] =
|
|
3307
|
+
recordUpdate[createdTimeProperty.name] = base.value[createdTimeProperty.name];
|
|
2343
3308
|
}
|
|
2344
3309
|
}
|
|
2345
3310
|
else {
|
|
@@ -2408,6 +3373,7 @@ function makeTable(options) {
|
|
|
2408
3373
|
this.#savingOperation = null;
|
|
2409
3374
|
write.stagedIn = undefined; // nothing may pin this write's transaction past its commit
|
|
2410
3375
|
let omitLocalRecord = false;
|
|
3376
|
+
const txnLogKey = isRocksDB && options?.version != null ? (transaction?.getTimestamp?.() ?? txnTime) : txnTime;
|
|
2411
3377
|
// we use optimistic locking to only commit if the existing record state still holds true.
|
|
2412
3378
|
// this is superior to using an async transaction since it doesn't require JS execution
|
|
2413
3379
|
// during the write transaction.
|
|
@@ -2468,8 +3434,8 @@ function makeTable(options) {
|
|
|
2468
3434
|
// best-effort keyed lookup in the capped block below — see #1148. precedesExistingVersion(...)
|
|
2469
3435
|
// === 0 is the identity tie: same version AND same node (the local node is id 0, so an undefined
|
|
2470
3436
|
// options?.nodeId resolves to the same 0 the ref stored).
|
|
2471
|
-
if (existingEntry.additionalAuditRefs?.some((ref) => ref.version ===
|
|
2472
|
-
precedesExistingVersion(txnTime, { version: txnTime, localTime:
|
|
3437
|
+
if (existingEntry.additionalAuditRefs?.some((ref) => ref.version === txnLogKey &&
|
|
3438
|
+
precedesExistingVersion(txnTime, { version: txnTime, localTime: txnLogKey, key: id, nodeId: ref.nodeId }, options?.nodeId) === 0)) {
|
|
2473
3439
|
write.skipped = true;
|
|
2474
3440
|
return; // out-of-order write already folded into this record
|
|
2475
3441
|
}
|
|
@@ -2497,10 +3463,10 @@ function makeTable(options) {
|
|
|
2497
3463
|
if (!oldestRetainedAuditTimeResolved) {
|
|
2498
3464
|
oldestRetainedAuditTimeResolved = true;
|
|
2499
3465
|
// getRange yields ascending by audit-log key, so the first entry is the oldest retained.
|
|
2500
|
-
// Mirror replicationConnection's retention check and the cleanup key basis (
|
|
2501
|
-
//
|
|
3466
|
+
// Mirror replicationConnection's retention check and the cleanup key basis (`txnLogKey`).
|
|
3467
|
+
// Fall back to the nominal time-based purge floor when the log is empty/unavailable.
|
|
2502
3468
|
for (const entry of auditStore.getRange({ start: 1, log: options?.nodeId })) {
|
|
2503
|
-
oldestRetainedAuditTime = entry.
|
|
3469
|
+
oldestRetainedAuditTime = entry.txnLogKey;
|
|
2504
3470
|
break;
|
|
2505
3471
|
}
|
|
2506
3472
|
oldestRetainedAuditTime ??= Date.now() - auditStore_ts_1.auditRetention;
|
|
@@ -2513,43 +3479,67 @@ function makeTable(options) {
|
|
|
2513
3479
|
// depth-cap block. This is the same keyed lookup that block performs, hoisted ahead of the walk.
|
|
2514
3480
|
// It is what catches transitive/proxied re-deliveries: they arrive buried below the record head
|
|
2515
3481
|
// (so replication's head-tie fast-skip can't see them) yet are exact duplicates. Keyed by nodeId,
|
|
2516
|
-
// so it is correct across multiple source nodes.
|
|
2517
|
-
//
|
|
2518
|
-
//
|
|
3482
|
+
// so it is correct across multiple source nodes. The lookup key is this write's LOG key, not its
|
|
3483
|
+
// record version — a replication apply commits under the origin's log key while storing the
|
|
3484
|
+
// origin's version, and only the log key addresses the entry (harper#2412).
|
|
3485
|
+
// RocksDB-only: LMDB audit entries are keyed by local audit time, so this lookup doesn't apply
|
|
3486
|
+
// there (LMDB keeps the exact unbounded walk). A miss (the keyed lookup can lag a back-to-back re-delivery — #1137)
|
|
2519
3487
|
// simply falls through to the walk, so this never changes correctness; the additionalAuditRefs
|
|
2520
3488
|
// check above remains the read-your-writes guard. Never when this write staged in a prior
|
|
2521
3489
|
// failed attempt: that attempt already appended this write's own audit entry, so the lookup
|
|
2522
3490
|
// would find it and skip the write as "already applied" when the record was never committed.
|
|
2523
3491
|
// A recommit of the same transaction survived that skip only because the old write batch
|
|
2524
3492
|
// still carried the put; a fresh-transaction replay (ERR_TRY_AGAIN) would drop the write.
|
|
2525
|
-
if (isRocksDB && !stagedOwnAuditEntry && dedupVersionCouldBeRetained(
|
|
2526
|
-
const priorAudit = auditStore.get(
|
|
3493
|
+
if (isRocksDB && !replaying && !stagedOwnAuditEntry && dedupVersionCouldBeRetained(txnLogKey)) {
|
|
3494
|
+
const priorAudit = auditStore.get(txnLogKey, tableId, id, options?.nodeId);
|
|
2527
3495
|
if (priorAudit &&
|
|
2528
|
-
priorAudit.
|
|
2529
|
-
precedesExistingVersion(txnTime, { version: txnTime, localTime:
|
|
3496
|
+
priorAudit.txnLogKey === txnLogKey &&
|
|
3497
|
+
precedesExistingVersion(txnTime, { version: txnTime, localTime: txnLogKey, key: id, nodeId: priorAudit.nodeId }, options?.nodeId) === 0) {
|
|
2530
3498
|
write.skipped = true;
|
|
2531
3499
|
return; // duplicate already applied; avoid the resequencing walk
|
|
2532
3500
|
}
|
|
2533
3501
|
}
|
|
2534
3502
|
// incremental CRDT updates are only available with audit logging on
|
|
2535
|
-
|
|
3503
|
+
const initialAuditHead = isRocksDB
|
|
3504
|
+
? resolveAuditHead(id, existingEntry.version, existingEntry.nodeId, existingEntry.additionalAuditRefs)
|
|
3505
|
+
: { txnLogKey: existingEntry.localTime, nodeId: existingEntry.nodeId };
|
|
3506
|
+
let localTime = initialAuditHead.txnLogKey;
|
|
2536
3507
|
let auditedVersion = existingEntry.version;
|
|
2537
3508
|
logger_ts_1.logger.debug?.('Applying CRDT update to record with id: ', id, 'txn time', new Date(txnTime), 'applying later update from:', new Date(auditedVersion), 'local recorded time', new Date(localTime));
|
|
2538
|
-
let nodeId =
|
|
3509
|
+
let nodeId = initialAuditHead.nodeId;
|
|
2539
3510
|
const succeedingUpdates = []; // record the "future" updates, as we need to apply the updates in reverse order
|
|
2540
3511
|
const auditRefsToVisit = existingEntry.additionalAuditRefs
|
|
2541
3512
|
? existingEntry.additionalAuditRefs.map((ref) => ({ localTime: ref.version, nodeId: ref.nodeId }))
|
|
2542
3513
|
: [];
|
|
2543
|
-
//
|
|
3514
|
+
// Out-of-order merges retain every existing branch head; per-origin log keys are not globally ordered.
|
|
2544
3515
|
if (existingEntry.additionalAuditRefs) {
|
|
2545
3516
|
for (const ref of existingEntry.additionalAuditRefs) {
|
|
2546
|
-
|
|
2547
|
-
additionalAuditRefs.push(ref);
|
|
2548
|
-
}
|
|
3517
|
+
additionalAuditRefs.push(ref);
|
|
2549
3518
|
}
|
|
2550
3519
|
}
|
|
2551
3520
|
let addedAuditRef = false;
|
|
2552
3521
|
let nextRef;
|
|
3522
|
+
const visitedAuditRefs = new Set();
|
|
3523
|
+
const queuePreviousAuditRefs = (auditRecord) => {
|
|
3524
|
+
const previousRefs = auditRecord.previousAdditionalAuditRefs;
|
|
3525
|
+
if (previousRefs) {
|
|
3526
|
+
for (const ref of previousRefs) {
|
|
3527
|
+
auditRefsToVisit.push({ localTime: ref.version, nodeId: ref.nodeId });
|
|
3528
|
+
logger_ts_1.logger.debug?.('Adding audit ref from audit record to visit queue', {
|
|
3529
|
+
version: ref.version,
|
|
3530
|
+
nodeId: ref.nodeId,
|
|
3531
|
+
});
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
};
|
|
3535
|
+
const advanceToPreviousAudit = (auditRecord) => {
|
|
3536
|
+
const previousRefs = auditRecord.previousAdditionalAuditRefs;
|
|
3537
|
+
const previousHead = isRocksDB && previousRefs?.length
|
|
3538
|
+
? resolveAuditHead(id, auditRecord.previousVersion, auditRecord.previousNodeId, previousRefs)
|
|
3539
|
+
: { txnLogKey: auditRecord.previousVersion, nodeId: auditRecord.previousNodeId };
|
|
3540
|
+
localTime = previousHead.txnLogKey;
|
|
3541
|
+
nodeId = previousHead.nodeId;
|
|
3542
|
+
};
|
|
2553
3543
|
let walkSteps = 0;
|
|
2554
3544
|
let auditWalkCapped = false;
|
|
2555
3545
|
// Early-out residual: as we walk the chain newest-first, fold each succeeding patch into a
|
|
@@ -2568,17 +3558,21 @@ function makeTable(options) {
|
|
|
2568
3558
|
// appended this write's own audit entry, so the lookup would match it while the record was
|
|
2569
3559
|
// never committed (see the up-front keyed dedup above).
|
|
2570
3560
|
const isReDeliveredDuplicate = () => {
|
|
2571
|
-
if (stagedOwnAuditEntry)
|
|
3561
|
+
if (replaying || stagedOwnAuditEntry)
|
|
2572
3562
|
return false;
|
|
2573
|
-
if (!dedupVersionCouldBeRetained(
|
|
2574
|
-
return false; // pre-retention
|
|
2575
|
-
const duplicate = auditStore.get(
|
|
3563
|
+
if (!dedupVersionCouldBeRetained(txnLogKey))
|
|
3564
|
+
return false; // pre-retention log key — skip the end-of-log scan (best-effort; see above)
|
|
3565
|
+
const duplicate = auditStore.get(txnLogKey, tableId, id, options?.nodeId);
|
|
2576
3566
|
return (duplicate &&
|
|
2577
|
-
duplicate.
|
|
2578
|
-
precedesExistingVersion(txnTime, { version: txnTime, localTime:
|
|
3567
|
+
duplicate.txnLogKey === txnLogKey &&
|
|
3568
|
+
precedesExistingVersion(txnTime, { version: txnTime, localTime: txnLogKey, key: id, nodeId: duplicate.nodeId }, options?.nodeId) === 0);
|
|
2579
3569
|
};
|
|
2580
3570
|
do {
|
|
2581
3571
|
while (localTime > txnTime || (auditedVersion >= txnTime && localTime > 0)) {
|
|
3572
|
+
const auditIdentity = `${nodeId ?? 0}:${localTime}`;
|
|
3573
|
+
if (visitedAuditRefs.has(auditIdentity))
|
|
3574
|
+
break;
|
|
3575
|
+
visitedAuditRefs.add(auditIdentity);
|
|
2582
3576
|
// Bound the walk only for RocksDB, where the OOM was observed (issue #1114): each step
|
|
2583
3577
|
// is a transaction-log range scan + msgpackr decode, and the per-node logs can be huge.
|
|
2584
3578
|
// LMDB audit entries are keyed by local audit time (not version), so the duplicate
|
|
@@ -2590,19 +3584,39 @@ function makeTable(options) {
|
|
|
2590
3584
|
const auditRecord = auditStore.get(localTime, tableId, id, nodeId);
|
|
2591
3585
|
if (!auditRecord)
|
|
2592
3586
|
break;
|
|
3587
|
+
queuePreviousAuditRefs(auditRecord);
|
|
3588
|
+
if (isRocksDB &&
|
|
3589
|
+
!replaying &&
|
|
3590
|
+
!stagedOwnAuditEntry &&
|
|
3591
|
+
localTime === txnLogKey &&
|
|
3592
|
+
precedesExistingVersion(txnTime, { version: txnTime, localTime: txnLogKey, key: id, nodeId: auditRecord.nodeId }, options?.nodeId) === 0) {
|
|
3593
|
+
write.skipped = true;
|
|
3594
|
+
return;
|
|
3595
|
+
}
|
|
2593
3596
|
auditedVersion = auditRecord.version;
|
|
2594
3597
|
if (auditedVersion >= txnTime) {
|
|
2595
3598
|
if (auditedVersion === txnTime) {
|
|
2596
3599
|
precedesExisting = precedesExistingVersion(txnTime, { version: auditedVersion, localTime: localTime, key: id, nodeId: auditRecord.nodeId }, options?.nodeId);
|
|
2597
3600
|
if (precedesExisting === 0) {
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
3601
|
+
if (isRocksDB && localTime !== txnLogKey) {
|
|
3602
|
+
// Same origin and record version, but a distinct write. Its per-origin log key
|
|
3603
|
+
// orders the otherwise non-unique record clock without comparing keys across origins.
|
|
3604
|
+
precedesExisting = txnLogKey > localTime ? 1 : -1;
|
|
3605
|
+
}
|
|
3606
|
+
else if (replaying || stagedOwnAuditEntry) {
|
|
3607
|
+
// The log entry being replayed (or staged by this write's failed attempt) is
|
|
3608
|
+
// the write itself, not proof that its primary-store mutation committed.
|
|
3609
|
+
precedesExisting = 1;
|
|
3610
|
+
}
|
|
3611
|
+
else {
|
|
3612
|
+
logger_ts_1.logger.debug?.('The transaction time and log key match the existing write, treating as duplicate', id);
|
|
3613
|
+
write.skipped = true;
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
2601
3616
|
}
|
|
2602
3617
|
if (precedesExisting > 0) {
|
|
2603
3618
|
// if the existing version is older, we can skip this update
|
|
2604
|
-
|
|
2605
|
-
nodeId = auditRecord.previousNodeId;
|
|
3619
|
+
advanceToPreviousAudit(auditRecord);
|
|
2606
3620
|
continue;
|
|
2607
3621
|
}
|
|
2608
3622
|
}
|
|
@@ -2637,23 +3651,16 @@ function makeTable(options) {
|
|
|
2637
3651
|
}
|
|
2638
3652
|
if (!addedAuditRef && isRocksDB) {
|
|
2639
3653
|
addedAuditRef = true;
|
|
2640
|
-
// Add a reference to this older audit record if we had out-of-order writes
|
|
2641
|
-
|
|
3654
|
+
// Add a reference to this older audit record if we had out-of-order writes. The stored
|
|
3655
|
+
// value is a LOG key, not a record version: every consumer follows it straight into
|
|
3656
|
+
// `auditStore.get` (see the `auditRefsToVisit` mapping above and below), and on an
|
|
3657
|
+
// applied write those two clocks differ.
|
|
3658
|
+
additionalAuditRefs.push({ version: txnLogKey, nodeId: options?.nodeId });
|
|
2642
3659
|
logger_ts_1.logger.debug?.('Adding additional audit ref for out-of-order write', {
|
|
2643
|
-
|
|
3660
|
+
txnLogKey,
|
|
2644
3661
|
nodeId: options?.nodeId,
|
|
2645
3662
|
});
|
|
2646
3663
|
}
|
|
2647
|
-
// Collect any additional audit refs from this audit record to traverse other branches
|
|
2648
|
-
if (auditRecord.previousAdditionalAuditRefs) {
|
|
2649
|
-
for (const ref of auditRecord.previousAdditionalAuditRefs) {
|
|
2650
|
-
auditRefsToVisit.push({ localTime: ref.version, nodeId: ref.nodeId });
|
|
2651
|
-
logger_ts_1.logger.debug?.('Adding audit ref from audit record to visit queue', {
|
|
2652
|
-
version: ref.version,
|
|
2653
|
-
nodeId: ref.nodeId,
|
|
2654
|
-
});
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
3664
|
// Every field of this write is overwritten by newer writes, and there is no alternate
|
|
2658
3665
|
// audit branch left to scan, so it is fully superseded — the same outcome as walking to
|
|
2659
3666
|
// the end and taking the `writeCommit(false)` escape below, reached without paying the rest
|
|
@@ -2671,8 +3678,7 @@ function makeTable(options) {
|
|
|
2671
3678
|
}
|
|
2672
3679
|
return writeCommit(false);
|
|
2673
3680
|
}
|
|
2674
|
-
|
|
2675
|
-
nodeId = auditRecord.previousNodeId;
|
|
3681
|
+
advanceToPreviousAudit(auditRecord);
|
|
2676
3682
|
}
|
|
2677
3683
|
// Check if we need to scan additional audit refs from this record
|
|
2678
3684
|
if (auditWalkCapped)
|
|
@@ -2766,15 +3772,15 @@ function makeTable(options) {
|
|
|
2766
3772
|
if (recordToStore && recordToStore.getRecord)
|
|
2767
3773
|
throw new Error('Can not assign a record to a record, check for circular references');
|
|
2768
3774
|
if (residencyId == undefined) {
|
|
2769
|
-
if (
|
|
2770
|
-
context.previousResidency =
|
|
2771
|
-
const residency = residencyFromFunction(
|
|
3775
|
+
if (existingEntry?.residencyId)
|
|
3776
|
+
context.previousResidency = _a.getResidencyRecord(existingEntry.residencyId);
|
|
3777
|
+
const residency = residencyFromFunction(_a.getResidency(recordToStore, context));
|
|
2772
3778
|
if (residency) {
|
|
2773
3779
|
if (!residency.includes(server.hostname)) {
|
|
2774
3780
|
// if we aren't in the residency list, specify that our local record should be omitted or be partial
|
|
2775
3781
|
auditRecordToStore ??= recordToStore;
|
|
2776
3782
|
omitLocalRecord = true;
|
|
2777
|
-
if (
|
|
3783
|
+
if (_a.getResidencyById) {
|
|
2778
3784
|
// complete omission of the record that doesn't belong here
|
|
2779
3785
|
recordToStore = undefined;
|
|
2780
3786
|
}
|
|
@@ -2844,7 +3850,17 @@ function makeTable(options) {
|
|
|
2844
3850
|
}
|
|
2845
3851
|
})());
|
|
2846
3852
|
updateIndices(id, existingRecord, recordToStore, transaction && { transaction });
|
|
3853
|
+
// Preserve an addressable audit head when the record and log clocks diverge.
|
|
3854
|
+
if (isRocksDB && audit && !isCopyApply && txnLogKey !== txnTime) {
|
|
3855
|
+
const headIndex = additionalAuditRefs.findIndex((ref) => ref.version === txnLogKey && (ref.nodeId ?? 0) === (options?.nodeId ?? 0));
|
|
3856
|
+
if (headIndex > 0)
|
|
3857
|
+
additionalAuditRefs.unshift(additionalAuditRefs.splice(headIndex, 1)[0]);
|
|
3858
|
+
else if (headIndex < 0)
|
|
3859
|
+
additionalAuditRefs.unshift({ version: txnLogKey, nodeId: options?.nodeId });
|
|
3860
|
+
}
|
|
2847
3861
|
writeCommit(true);
|
|
3862
|
+
if (write.trackRecordVersion)
|
|
3863
|
+
write.recordVersionApplied = true;
|
|
2848
3864
|
if (expiresAt >= 0) {
|
|
2849
3865
|
scheduleCleanup(); // arm for replicated writes too, not just local-context writes
|
|
2850
3866
|
// A runtime per-record expiresAt on a table with no table-level expiration/eviction, no expiresAt
|
|
@@ -2871,6 +3887,8 @@ function makeTable(options) {
|
|
|
2871
3887
|
user: context?.user,
|
|
2872
3888
|
residencyId,
|
|
2873
3889
|
expiresAt,
|
|
3890
|
+
recordVersion: txnTime,
|
|
3891
|
+
recordNodeId: precedesExisting < 0 ? existingEntry?.nodeId : options?.nodeId,
|
|
2874
3892
|
nodeId: options?.nodeId,
|
|
2875
3893
|
viaNodeId: options?.viaNodeId,
|
|
2876
3894
|
originatingOperation: context?.originatingOperation,
|
|
@@ -2908,7 +3926,7 @@ function makeTable(options) {
|
|
|
2908
3926
|
// calls the backend, and a tracked-instance mutation (update(id,{}); row.source=…;
|
|
2909
3927
|
// save()) that sets the source via accessors after update() won't re-embed. A
|
|
2910
3928
|
// resource-layer re-embed is the proper fix; tracked as a follow-up.
|
|
2911
|
-
const embedBefore = (0, embedHook_ts_1.buildEmbedBefore)(recordUpdate, context, options,
|
|
3929
|
+
const embedBefore = (0, embedHook_ts_1.buildEmbedBefore)(recordUpdate, context, options, _a.embedAttributes, _a.userEmbedders);
|
|
2912
3930
|
const proceed = () => {
|
|
2913
3931
|
// On a source/replication apply (`isNotification`), the record's already-saved blobs were
|
|
2914
3932
|
// received out-of-band for THIS write, so track them for skip/abort cleanup (harper-pro#406).
|
|
@@ -2964,8 +3982,10 @@ function makeTable(options) {
|
|
|
2964
3982
|
return Boolean(this.#record);
|
|
2965
3983
|
}
|
|
2966
3984
|
_writeDelete(id, options) {
|
|
3985
|
+
this.#assertLiveHandle(id);
|
|
2967
3986
|
const context = this.getContext();
|
|
2968
3987
|
const transaction = txnForContext(context);
|
|
3988
|
+
assertDerivedIndexAdmission(options, transaction);
|
|
2969
3989
|
checkValidId(id);
|
|
2970
3990
|
const entry = this.#entry ?? primaryStore.getEntry(id, { transaction: transaction.getReadTxn() });
|
|
2971
3991
|
const write = {
|
|
@@ -2973,7 +3993,10 @@ function makeTable(options) {
|
|
|
2973
3993
|
store: primaryStore,
|
|
2974
3994
|
entry,
|
|
2975
3995
|
chainsStagedState: true,
|
|
3996
|
+
reloadCommitBase: true,
|
|
2976
3997
|
nodeName: context?.nodeName,
|
|
3998
|
+
recordVersion: options?.version,
|
|
3999
|
+
lockHandle: this.#lockHandle && this.#lockHandle.keyId === (0, DatabaseTransaction_ts_1.writeKeyId)(id) ? this.#lockHandle : undefined,
|
|
2977
4000
|
before: this.constructor.source?.delete && !context?.source
|
|
2978
4001
|
? this.constructor.source.delete.bind(this.constructor.source, id, undefined, context)
|
|
2979
4002
|
: undefined,
|
|
@@ -2985,10 +4008,11 @@ function makeTable(options) {
|
|
|
2985
4008
|
const priorStagedOp = (0, DatabaseTransaction_ts_1.priorStagedWrite)(write);
|
|
2986
4009
|
const priorStaged = priorStagedOp?.stagedEntry;
|
|
2987
4010
|
const existingRecord = priorStaged ? priorStaged.value : existingEntry?.value;
|
|
4011
|
+
const txnLogKey = isRocksDB && options?.version != null ? (transaction?.getTimestamp?.() ?? txnTime) : txnTime;
|
|
2988
4012
|
if (retry) {
|
|
2989
4013
|
if (context && existingEntry?.version > (context.lastModified || 0))
|
|
2990
4014
|
context.lastModified = existingEntry.version;
|
|
2991
|
-
|
|
4015
|
+
_a._updateResource(this, existingEntry);
|
|
2992
4016
|
}
|
|
2993
4017
|
// a strictly newer record exists locally, so this delete loses. An earlier write in this
|
|
2994
4018
|
// transaction can never trip this guard — it shares this transaction's timestamp and
|
|
@@ -3006,6 +4030,10 @@ function makeTable(options) {
|
|
|
3006
4030
|
viaNodeId: options?.viaNodeId,
|
|
3007
4031
|
transaction,
|
|
3008
4032
|
tableToTrack: tableName,
|
|
4033
|
+
recordVersion: txnTime,
|
|
4034
|
+
additionalAuditRefs: isRocksDB && audit && txnLogKey !== txnTime
|
|
4035
|
+
? [{ version: txnLogKey, nodeId: options?.nodeId }]
|
|
4036
|
+
: undefined,
|
|
3009
4037
|
}, 'delete');
|
|
3010
4038
|
if (!audit || isRocksDB)
|
|
3011
4039
|
scheduleCleanup();
|
|
@@ -3015,6 +4043,8 @@ function makeTable(options) {
|
|
|
3015
4043
|
(0, RecordEncoder_ts_1.removeEntry)(primaryStore, existingEntry, isRocksDB && transaction ? { transaction } : undefined);
|
|
3016
4044
|
}
|
|
3017
4045
|
write.stagedEntry = { value: undefined }; // the key holds no record for the rest of this transaction
|
|
4046
|
+
if (write.trackRecordVersion)
|
|
4047
|
+
write.recordVersionApplied = true;
|
|
3018
4048
|
// the removal supersedes the nearest record an earlier write in this transaction stored
|
|
3019
4049
|
// (older ones were already marked by their staged successors), so its saved blobs are
|
|
3020
4050
|
// cleaned up post-commit unless its audit entry references them
|
|
@@ -3274,7 +4304,7 @@ function makeTable(options) {
|
|
|
3274
4304
|
// Note, that we do allow users to disable condition re-ordering, in case they have knowledge of a preferred
|
|
3275
4305
|
// order for their query.
|
|
3276
4306
|
if (conditions.length > 1 && operator !== 'or')
|
|
3277
|
-
return sortBy(conditions, (0, search_ts_1.estimateCondition)(
|
|
4307
|
+
return sortBy(conditions, (0, search_ts_1.estimateCondition)(_a));
|
|
3278
4308
|
else
|
|
3279
4309
|
return conditions;
|
|
3280
4310
|
}
|
|
@@ -3321,8 +4351,13 @@ function makeTable(options) {
|
|
|
3321
4351
|
else if (conditions.length === 0 && !target.allowFullScan)
|
|
3322
4352
|
throw (0, hdbError_ts_1.handleHDBError)(new Error(), `${Array.isArray(attribute_name) ? attribute_name.join('.') : attribute_name} is not indexed and not combined with any other conditions`, 404);
|
|
3323
4353
|
}
|
|
3324
|
-
if (orderAlignedCondition)
|
|
4354
|
+
if (orderAlignedCondition) {
|
|
3325
4355
|
orderAlignedCondition.descending = Boolean(sort.descending);
|
|
4356
|
+
if (orderAlignedCondition.maxIndexLagMilliseconds === undefined)
|
|
4357
|
+
orderAlignedCondition.maxIndexLagMilliseconds = sort.maxIndexLagMilliseconds;
|
|
4358
|
+
if (orderAlignedCondition.waitForIndexMilliseconds === undefined)
|
|
4359
|
+
orderAlignedCondition.waitForIndexMilliseconds = sort.waitForIndexMilliseconds;
|
|
4360
|
+
}
|
|
3326
4361
|
}
|
|
3327
4362
|
}
|
|
3328
4363
|
conditions = orderConditions(conditions, operator);
|
|
@@ -3351,6 +4386,10 @@ function makeTable(options) {
|
|
|
3351
4386
|
}
|
|
3352
4387
|
}
|
|
3353
4388
|
const select = target.select;
|
|
4389
|
+
// Whether the caller supplied real filter conditions — read from the raw request, NOT the
|
|
4390
|
+
// planner-augmented `conditions` (which by now may carry a synthetic `sort` pseudo-condition and
|
|
4391
|
+
// injected full-scan condition). Used to pick the count-estimate source below.
|
|
4392
|
+
const hasUserConditions = Array.isArray(target.conditions) && target.conditions.length > 0;
|
|
3354
4393
|
if (conditions.length === 0) {
|
|
3355
4394
|
conditions = [{ attribute: primaryKey, comparator: 'greater_than', value: true }];
|
|
3356
4395
|
}
|
|
@@ -3387,35 +4426,156 @@ function makeTable(options) {
|
|
|
3387
4426
|
const recordAccess = boundRowFilter || typeof target.vectorFilter === 'function'
|
|
3388
4427
|
? { rowFilter: boundRowFilter, vectorFilter: target.vectorFilter }
|
|
3389
4428
|
: undefined;
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
4429
|
+
try {
|
|
4430
|
+
const entries = (0, search_ts_1.executeConditions)(conditions, operator, _a, readTxn, target, context, (results, filters) => transformToEntries(results, select, context, readTxn, filters), filtered, recordAccess);
|
|
4431
|
+
const ensure_loaded = target.ensureLoaded !== false;
|
|
4432
|
+
// The guards inside executeConditions evaluate the
|
|
4433
|
+
// LOCAL record, but on a caching table transformEntryForSelect may then revalidate an
|
|
4434
|
+
// expired/invalidated row from source and return a DIFFERENT record. The explicit row filter
|
|
4435
|
+
// must hold on the record actually returned, so it is re-checked
|
|
4436
|
+
// there, after materialization (the earlier evaluation stays as a prune that also bounds HNSW
|
|
4437
|
+
// traversal). vectorFilter and condition filters intentionally keep the local-record
|
|
4438
|
+
// semantics all query filters have on caching tables.
|
|
4439
|
+
//
|
|
4440
|
+
// A row that is past its TTL but not yet swept by the background eviction
|
|
4441
|
+
// scan is still physically present. A write that is about to overwrite it
|
|
4442
|
+
// anyway (e.g. the SQL engine locating UPDATE/DELETE targets) needs to see
|
|
4443
|
+
// it as a match — the same leniency a direct by-id put/patch already gets,
|
|
4444
|
+
// since those never run the ensureLoaded-gated freshness check this transform
|
|
4445
|
+
// otherwise applies unconditionally to every read.
|
|
4446
|
+
const includeExpired = target.includeExpired === true;
|
|
4447
|
+
const transformToRecord = _a.transformEntryForSelect(select, context, readTxn, filtered, ensure_loaded, true, boundRowFilter, includeExpired, postOrdering);
|
|
4448
|
+
let results = _a.transformToOrderedSelect(entries, select, postOrdering, context, readTxn, transformToRecord);
|
|
4449
|
+
const offset = target.offset || 0;
|
|
4450
|
+
const end = target.limit !== undefined ? offset + target.limit : undefined;
|
|
4451
|
+
// `Prefer: count=` (REST pagination): materialize the requested page and attach a total record
|
|
4452
|
+
// count so the HTTP layer can emit a Content-Range. `exact` drains the full matched set once,
|
|
4453
|
+
// windowing the page in the same pass; `estimated` returns just the page plus a cheap planner/
|
|
4454
|
+
// table estimate. Opt-in only — the default streaming path below is untouched.
|
|
4455
|
+
//
|
|
4456
|
+
// Requires a bounded page AND window. Counting is a pagination feature; both the limit and the
|
|
4457
|
+
// offset must be finite, non-negative integers, the limit no larger than MAX_COUNT_PAGE, and the
|
|
4458
|
+
// window (offset + limit) no larger than MAX_EXACT_COUNT_SCAN. Anything else — a missing/
|
|
4459
|
+
// oversized/non-finite/negative limit or offset (a bare collection GET, limit(Infinity),
|
|
4460
|
+
// limit(foo), limit(-5,10)) or a deep-page window past the scan budget — falls through to the
|
|
4461
|
+
// normal streaming path with no count. This bounds the offset too: without it a huge offset would
|
|
4462
|
+
// postpone the exact guardrail (which only engages past the page) until that offset was scanned.
|
|
4463
|
+
const pageLimit = target.limit;
|
|
4464
|
+
if (target.count &&
|
|
4465
|
+
Number.isInteger(pageLimit) &&
|
|
4466
|
+
pageLimit >= 0 &&
|
|
4467
|
+
pageLimit <= MAX_COUNT_PAGE &&
|
|
4468
|
+
Number.isInteger(offset) &&
|
|
4469
|
+
offset >= 0 &&
|
|
4470
|
+
offset + pageLimit <= MAX_EXACT_COUNT_SCAN) {
|
|
4471
|
+
const wantExact = target.count === 'exact';
|
|
4472
|
+
const pageEnd = offset + pageLimit;
|
|
4473
|
+
const countStart = node_perf_hooks_1.performance.now();
|
|
4474
|
+
// A custom-index (vector/HNSW) traversal returns a bounded, approximate candidate set whose size is
|
|
4475
|
+
// chosen from `minResults` (offset + limit), so `scanned` over it tracks the requested page size, not
|
|
4476
|
+
// the true match count — the same query at limit(5) vs limit(200) would otherwise advertise two
|
|
4477
|
+
// different `count=exact` totals. Any query whose execution touches a custom index is affected: a
|
|
4478
|
+
// custom-index sort (its aligned pseudo-condition lands in `conditions`), a custom-index threshold
|
|
4479
|
+
// filter (an HNSW `lt`/`le` is the same minResults-widened traversal as a sort), or an opaque vector
|
|
4480
|
+
// filter. Report the total as unavailable for those rather than advertising it as count=exact
|
|
4481
|
+
// (mirroring how the estimated branch below bails to null for an opaque row/vector filter). A vector
|
|
4482
|
+
// sort applied as in-memory post-ordering leaves no custom-index condition here and stays exact.
|
|
4483
|
+
const touchesCustomIndex = (conds) => conds.some((c) => {
|
|
4484
|
+
if (!c)
|
|
4485
|
+
return false;
|
|
4486
|
+
if (c.conditions)
|
|
4487
|
+
return touchesCustomIndex(c.conditions);
|
|
4488
|
+
const attr = Array.isArray(c.attribute) ? c.attribute[0] : (c.attribute ?? c[0]);
|
|
4489
|
+
return typeof attr === 'string' && Boolean(indices[attr]?.customIndex);
|
|
4490
|
+
});
|
|
4491
|
+
const approximateResultSet = typeof target.vectorFilter === 'function' || touchesCustomIndex(conditions);
|
|
4492
|
+
return (async () => {
|
|
4493
|
+
const page = [];
|
|
4494
|
+
let scanned = 0;
|
|
4495
|
+
let exact = true;
|
|
4496
|
+
try {
|
|
4497
|
+
for await (const record of results) {
|
|
4498
|
+
if (scanned >= offset && scanned < pageEnd)
|
|
4499
|
+
page.push(record);
|
|
4500
|
+
scanned++;
|
|
4501
|
+
// A store whose async iterator settles synchronously (the common indexed-scan case) would
|
|
4502
|
+
// otherwise let this drain spin as one uninterrupted microtask run, blocking the event loop
|
|
4503
|
+
// for the whole count. Yield to the macrotask queue periodically so concurrent requests and
|
|
4504
|
+
// I/O still make progress during a large exact scan.
|
|
4505
|
+
if ((scanned & (COUNT_YIELD_INTERVAL - 1)) === 0)
|
|
4506
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
4507
|
+
// The page window [offset, pageEnd) is always collected in full first — the guardrail
|
|
4508
|
+
// only ever abandons the running TOTAL, never truncates the page body.
|
|
4509
|
+
if (scanned >= pageEnd) {
|
|
4510
|
+
// `estimated` needs nothing past the page; an approximate (vector) exact total is going to
|
|
4511
|
+
// be reported unavailable anyway, so don't drain its tail for a number we won't publish.
|
|
4512
|
+
if (!wantExact || approximateResultSet)
|
|
4513
|
+
break;
|
|
4514
|
+
// `exact` keeps counting the tail, bounded by a row cap AND a time budget so a
|
|
4515
|
+
// large match set can't turn a bounded page fetch into an unbounded scan.
|
|
4516
|
+
if (scanned > MAX_EXACT_COUNT_SCAN || node_perf_hooks_1.performance.now() - countStart > MAX_EXACT_COUNT_MS) {
|
|
4517
|
+
exact = false;
|
|
4518
|
+
break;
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
finally {
|
|
4524
|
+
// We own the iteration here (no results.onDone consumer), so release the read
|
|
4525
|
+
// transaction unconditionally — including when the drain throws — or the snapshot leaks.
|
|
4526
|
+
txn.doneReadTxn();
|
|
4527
|
+
}
|
|
4528
|
+
let total;
|
|
4529
|
+
if (wantExact) {
|
|
4530
|
+
// `scanned` is only an authoritative total when the iteration was exhaustive and deterministic;
|
|
4531
|
+
// an approximate (vector/HNSW) result set is neither, so report the total as unavailable.
|
|
4532
|
+
total = exact && !approximateResultSet ? scanned : null;
|
|
4533
|
+
}
|
|
4534
|
+
else if (boundRowFilter || typeof target.vectorFilter === 'function') {
|
|
4535
|
+
// An opaque row/vector filter shapes the result but isn't reflected in the index/condition
|
|
4536
|
+
// estimate; guessing would both mislead and disclose cardinality the filter hides.
|
|
4537
|
+
total = null;
|
|
4538
|
+
}
|
|
4539
|
+
else if (!hasUserConditions) {
|
|
4540
|
+
total = (0, search_ts_1.estimatedEntryCount)(primaryStore);
|
|
4541
|
+
}
|
|
4542
|
+
else {
|
|
4543
|
+
// Estimate from the real conditions only — drop the planner's synthetic `sort`
|
|
4544
|
+
// pseudo-condition, which otherwise contributes a bogus (entryCount/2) cardinality.
|
|
4545
|
+
const est = (0, search_ts_1.estimateCondition)(_a)({
|
|
4546
|
+
conditions: conditions.filter((c) => c.comparator !== 'sort'),
|
|
4547
|
+
operator: operator ? String(operator).toLowerCase() : 'and',
|
|
4548
|
+
});
|
|
4549
|
+
total = isFinite(est) ? Math.round(est) : null;
|
|
4550
|
+
}
|
|
4551
|
+
// For an estimate, never report a total below the last row actually returned — keeps the
|
|
4552
|
+
// Content-Range valid (start-end/total) when an estimate undershoots a non-empty page.
|
|
4553
|
+
// Exact totals are authoritative (and an empty page past the end must not be clamped up).
|
|
4554
|
+
if (!wantExact && total != null && page.length > 0 && total < offset + page.length) {
|
|
4555
|
+
total = offset + page.length;
|
|
4556
|
+
}
|
|
4557
|
+
page.recordCount = total;
|
|
4558
|
+
page.recordCountExact = wantExact && exact && !approximateResultSet;
|
|
4559
|
+
page.selectApplied = true;
|
|
4560
|
+
page.getColumns = getColumns;
|
|
4561
|
+
return page;
|
|
4562
|
+
})();
|
|
4563
|
+
}
|
|
4564
|
+
// apply any offset/limit after all the sorting and filtering
|
|
4565
|
+
if (target.offset || target.limit !== undefined)
|
|
4566
|
+
results = results.slice(offset, end);
|
|
4567
|
+
results.onDone = () => {
|
|
4568
|
+
results.onDone = null; // ensure that it isn't called twice
|
|
4569
|
+
txn.doneReadTxn();
|
|
4570
|
+
};
|
|
4571
|
+
results.selectApplied = true;
|
|
4572
|
+
results.getColumns = getColumns;
|
|
4573
|
+
return results;
|
|
4574
|
+
}
|
|
4575
|
+
catch (error) {
|
|
3414
4576
|
txn.doneReadTxn();
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
results.getColumns = getColumns;
|
|
3418
|
-
return results;
|
|
4577
|
+
throw error;
|
|
4578
|
+
}
|
|
3419
4579
|
}
|
|
3420
4580
|
/**
|
|
3421
4581
|
* This is responsible for ordering and select()ing the attributes/properties from returned entries
|
|
@@ -3431,10 +4591,18 @@ function makeTable(options) {
|
|
|
3431
4591
|
if (sort) {
|
|
3432
4592
|
// there might be some situations where we don't need to transform to entries for sorting, not sure
|
|
3433
4593
|
entries = transformToEntries(entries, select, context, readTxn, null);
|
|
3434
|
-
|
|
4594
|
+
// Sort keys are resolved as entries are collected, so comparison never dereferences a record: a
|
|
4595
|
+
// cached entry holds its record only weakly, and a re-read per comparison is what this avoids.
|
|
4596
|
+
const clauses = [];
|
|
4597
|
+
for (let order = sort; order; order = order.next)
|
|
4598
|
+
clauses.push(order);
|
|
4599
|
+
const clauseCount = clauses.length;
|
|
3435
4600
|
// if we are doing post-ordering, we need to get records first, then sort them
|
|
3436
4601
|
results.iterate = function (options) {
|
|
3437
|
-
let
|
|
4602
|
+
let ordered;
|
|
4603
|
+
let orderedKeys;
|
|
4604
|
+
let sortedPositions;
|
|
4605
|
+
let sortedIndex;
|
|
3438
4606
|
const dbIterator = options?.async && entries[Symbol.asyncIterator]
|
|
3439
4607
|
? entries[Symbol.asyncIterator]()
|
|
3440
4608
|
: entries[Symbol.iterator]();
|
|
@@ -3443,26 +4611,34 @@ function makeTable(options) {
|
|
|
3443
4611
|
let enqueuedEntryForNextGroup;
|
|
3444
4612
|
let lastGroupingValue;
|
|
3445
4613
|
let firstEntry = true;
|
|
3446
|
-
function
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
4614
|
+
function collect(entry) {
|
|
4615
|
+
ordered.push(entry);
|
|
4616
|
+
for (let i = 0; i < clauseCount; i++) {
|
|
4617
|
+
const clause = clauses[i];
|
|
4618
|
+
orderedKeys[i].push(convertToComparableKeys(getAttributeValue(entry, clause.attribute, context, clause)));
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
function comparePositions(positionA, positionB) {
|
|
4622
|
+
for (let i = 0; i < clauseCount; i++) {
|
|
4623
|
+
const keys = orderedKeys[i];
|
|
4624
|
+
const diff = clauses[i].descending
|
|
4625
|
+
? (0, ordered_binary_1.compareKeys)(keys[positionB], keys[positionA])
|
|
4626
|
+
: (0, ordered_binary_1.compareKeys)(keys[positionA], keys[positionB]);
|
|
4627
|
+
if (diff !== 0)
|
|
4628
|
+
return diff;
|
|
4629
|
+
}
|
|
4630
|
+
return 0;
|
|
4631
|
+
}
|
|
4632
|
+
function nextSorted() {
|
|
4633
|
+
if (sortedIndex < sortedPositions.length)
|
|
4634
|
+
return { done: false, value: ordered[sortedPositions[sortedIndex++]] };
|
|
4635
|
+
return { done: true, value: undefined };
|
|
3459
4636
|
}
|
|
3460
|
-
const comparator = createComparator(sort);
|
|
3461
4637
|
return {
|
|
3462
4638
|
async next() {
|
|
3463
4639
|
let iteration;
|
|
3464
|
-
if (
|
|
3465
|
-
iteration =
|
|
4640
|
+
if (sortedPositions) {
|
|
4641
|
+
iteration = nextSorted();
|
|
3466
4642
|
if (iteration.done) {
|
|
3467
4643
|
if (dbDone) {
|
|
3468
4644
|
if (results.onDone)
|
|
@@ -3476,8 +4652,11 @@ function makeTable(options) {
|
|
|
3476
4652
|
};
|
|
3477
4653
|
}
|
|
3478
4654
|
ordered = [];
|
|
4655
|
+
orderedKeys = [];
|
|
4656
|
+
for (let i = 0; i < clauseCount; i++)
|
|
4657
|
+
orderedKeys.push([]);
|
|
3479
4658
|
if (enqueuedEntryForNextGroup)
|
|
3480
|
-
|
|
4659
|
+
collect(enqueuedEntryForNextGroup);
|
|
3481
4660
|
// need to load all the entries into ordered
|
|
3482
4661
|
do {
|
|
3483
4662
|
iteration = await dbIterator.next();
|
|
@@ -3509,17 +4688,18 @@ function makeTable(options) {
|
|
|
3509
4688
|
break;
|
|
3510
4689
|
}
|
|
3511
4690
|
}
|
|
3512
|
-
|
|
3513
|
-
// before the sorting is completed
|
|
3514
|
-
ordered.push(entry);
|
|
4691
|
+
collect(entry);
|
|
3515
4692
|
}
|
|
3516
4693
|
} while (true);
|
|
3517
4694
|
if (sort.isGrouped) {
|
|
3518
4695
|
// TODO: Return grouped results
|
|
3519
4696
|
}
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
4697
|
+
sortedPositions = [];
|
|
4698
|
+
for (let i = 0; i < ordered.length; i++)
|
|
4699
|
+
sortedPositions.push(i);
|
|
4700
|
+
sortedPositions.sort(comparePositions);
|
|
4701
|
+
sortedIndex = 0;
|
|
4702
|
+
iteration = nextSorted();
|
|
3523
4703
|
if (!iteration.done)
|
|
3524
4704
|
return {
|
|
3525
4705
|
value: await transformToRecord.call(this, iteration.value),
|
|
@@ -3723,12 +4903,12 @@ function makeTable(options) {
|
|
|
3723
4903
|
if (resolver.directReturn)
|
|
3724
4904
|
return callback(value, attribute_name);
|
|
3725
4905
|
if (value && typeof value === 'object') {
|
|
3726
|
-
const targetTable = resolver.definition?.tableClass ||
|
|
4906
|
+
const targetTable = resolver.definition?.tableClass || _a;
|
|
3727
4907
|
if (!transformCache)
|
|
3728
4908
|
transformCache = {};
|
|
3729
4909
|
// Use the target table's own read transaction; each table's readTxn is
|
|
3730
4910
|
// scoped to its RocksDB column family and cannot read another table's store.
|
|
3731
|
-
const targetReadTxn = targetTable ===
|
|
4911
|
+
const targetReadTxn = targetTable === _a ? readTxn : targetTable._readTxnForContext(context);
|
|
3732
4912
|
const transform = transformCache[attribute_name] ||
|
|
3733
4913
|
(transformCache[attribute_name] = targetTable.transformEntryForSelect(
|
|
3734
4914
|
// if it is a simple string, there is no select for the next level,
|
|
@@ -3781,7 +4961,7 @@ function makeTable(options) {
|
|
|
3781
4961
|
else {
|
|
3782
4962
|
value = record[attribute_name];
|
|
3783
4963
|
if (value && typeof value === 'object' && attribute_name !== attribute) {
|
|
3784
|
-
const subTransform =
|
|
4964
|
+
const subTransform = _a.transformEntryForSelect(attribute.select || attribute, context, readTxn, null);
|
|
3785
4965
|
// Plain JSON nested values: arrays project per-element so that
|
|
3786
4966
|
// `select: [{ name: 'addresses', select: ['city'] }]` returns
|
|
3787
4967
|
// `addresses: [{ city }, { city }]` rather than a single object.
|
|
@@ -3860,6 +5040,10 @@ function makeTable(options) {
|
|
|
3860
5040
|
if (!auditStore)
|
|
3861
5041
|
throw new Error('Can not subscribe to a table without an audit log');
|
|
3862
5042
|
if (!audit) {
|
|
5043
|
+
// Turning auditing on is a schema write, and a branch's Table classes carry the base's
|
|
5044
|
+
// logical name: without this a subscribe through a branched application would enable
|
|
5045
|
+
// auditing on the live base table for every other consumer, with no DDL call involved.
|
|
5046
|
+
_a.assertSchemaMutable('enable auditing for a subscription');
|
|
3863
5047
|
(0, databases_ts_1.table)({ table: tableName, database: databaseName, schemaDefined, attributes, audit: true });
|
|
3864
5048
|
}
|
|
3865
5049
|
const getFullRecord = !request.rawEvents;
|
|
@@ -3915,11 +5099,14 @@ function makeTable(options) {
|
|
|
3915
5099
|
return evaluateFilter(rowFilter, event.value, 'rowFilter');
|
|
3916
5100
|
}
|
|
3917
5101
|
: null;
|
|
3918
|
-
const subscription = (0, transactionBroadcast_ts_1.addSubscription)(
|
|
5102
|
+
const subscription = (0, transactionBroadcast_ts_1.addSubscription)(_a, thisId, function (id, auditRecord, txnLogKey, beginTxn) {
|
|
3919
5103
|
if (dropDuringReplay)
|
|
3920
5104
|
return;
|
|
3921
5105
|
try {
|
|
3922
5106
|
let type = auditRecord.type;
|
|
5107
|
+
// Ahead of the rawEvents branch, which forwards every type verbatim.
|
|
5108
|
+
if ((0, auditStore_ts_1.isLockControlType)(type))
|
|
5109
|
+
return;
|
|
3923
5110
|
let value;
|
|
3924
5111
|
if (type === 'message' || request.rawEvents) {
|
|
3925
5112
|
// we only send the full message, this are individual messages that can be sent out of order
|
|
@@ -3942,8 +5129,7 @@ function makeTable(options) {
|
|
|
3942
5129
|
// been written, so are fresh in memory.
|
|
3943
5130
|
const entry = primaryStore.getEntry(id);
|
|
3944
5131
|
if (entry) {
|
|
3945
|
-
|
|
3946
|
-
if (entry.version !== (auditRecord.recordVersion ?? auditRecord.version))
|
|
5132
|
+
if (entry.version !== auditRecord.version)
|
|
3947
5133
|
return; // out of order event, with old update, don't send anything
|
|
3948
5134
|
value = entry.value;
|
|
3949
5135
|
type = entry.metadataFlags & exports.INVALIDATED ? 'invalidate' : value ? 'put' : 'delete';
|
|
@@ -3954,7 +5140,7 @@ function makeTable(options) {
|
|
|
3954
5140
|
}
|
|
3955
5141
|
const event = {
|
|
3956
5142
|
id,
|
|
3957
|
-
localTime,
|
|
5143
|
+
localTime: txnLogKey,
|
|
3958
5144
|
value,
|
|
3959
5145
|
version: auditRecord.version,
|
|
3960
5146
|
type,
|
|
@@ -4019,14 +5205,16 @@ function makeTable(options) {
|
|
|
4019
5205
|
if (!isActive())
|
|
4020
5206
|
return;
|
|
4021
5207
|
}
|
|
4022
|
-
if (auditRecord.tableId !== tableId)
|
|
5208
|
+
if (auditRecord.tableId !== tableId || auditRecord.type === 'evict')
|
|
5209
|
+
continue;
|
|
5210
|
+
if ((0, auditStore_ts_1.isLockControlType)(auditRecord.type))
|
|
4023
5211
|
continue;
|
|
4024
5212
|
const id = auditRecord.recordId;
|
|
4025
5213
|
if (thisId == null || isDescendantId(thisId, id)) {
|
|
4026
|
-
const value = auditRecord.getValue(primaryStore, getFullRecord, auditRecord.
|
|
5214
|
+
const value = auditRecord.getValue(primaryStore, getFullRecord, auditRecord.txnLogKey);
|
|
4027
5215
|
if (!send({
|
|
4028
5216
|
id,
|
|
4029
|
-
localTime: auditRecord.
|
|
5217
|
+
localTime: auditRecord.txnLogKey,
|
|
4030
5218
|
value,
|
|
4031
5219
|
version: auditRecord.version,
|
|
4032
5220
|
type: auditRecord.type,
|
|
@@ -4039,7 +5227,7 @@ function makeTable(options) {
|
|
|
4039
5227
|
return;
|
|
4040
5228
|
}
|
|
4041
5229
|
}
|
|
4042
|
-
subscription.startTime = auditRecord.
|
|
5230
|
+
subscription.startTime = auditRecord.txnLogKey; // update so we don't double send
|
|
4043
5231
|
}
|
|
4044
5232
|
}
|
|
4045
5233
|
finally {
|
|
@@ -4059,7 +5247,9 @@ function makeTable(options) {
|
|
|
4059
5247
|
return;
|
|
4060
5248
|
}
|
|
4061
5249
|
try {
|
|
4062
|
-
if (auditRecord.tableId !== tableId)
|
|
5250
|
+
if (auditRecord.tableId !== tableId || auditRecord.type === 'evict')
|
|
5251
|
+
continue;
|
|
5252
|
+
if ((0, auditStore_ts_1.isLockControlType)(auditRecord.type))
|
|
4063
5253
|
continue;
|
|
4064
5254
|
const id = auditRecord.recordId;
|
|
4065
5255
|
if (thisId == null || isDescendantId(thisId, id)) {
|
|
@@ -4073,10 +5263,10 @@ function makeTable(options) {
|
|
|
4073
5263
|
logger_ts_1.logger.warn?.(`previousCount backfill on ${tableName} stopped after inspecting ${MAX_PREVIOUS_COUNT_SCAN} in-scope audit records without collecting ${request.previousCount} accepted event(s); returning ${history.length} instead`);
|
|
4074
5264
|
break;
|
|
4075
5265
|
}
|
|
4076
|
-
const value = auditRecord.getValue(primaryStore, getFullRecord, auditRecord.
|
|
5266
|
+
const value = auditRecord.getValue(primaryStore, getFullRecord, auditRecord.txnLogKey);
|
|
4077
5267
|
const historyEntry = {
|
|
4078
5268
|
id,
|
|
4079
|
-
localTime: auditRecord.
|
|
5269
|
+
localTime: auditRecord.txnLogKey,
|
|
4080
5270
|
value,
|
|
4081
5271
|
version: auditRecord.version,
|
|
4082
5272
|
type: auditRecord.type,
|
|
@@ -4093,7 +5283,7 @@ function makeTable(options) {
|
|
|
4093
5283
|
}
|
|
4094
5284
|
}
|
|
4095
5285
|
catch (error) {
|
|
4096
|
-
logger_ts_1.logger.error?.('Error getting history entry', auditRecord.
|
|
5286
|
+
logger_ts_1.logger.error?.('Error getting history entry', auditRecord.txnLogKey, error);
|
|
4097
5287
|
}
|
|
4098
5288
|
}
|
|
4099
5289
|
for (let i = history.length; i > 0;) {
|
|
@@ -4181,6 +5371,12 @@ function makeTable(options) {
|
|
|
4181
5371
|
logger_ts_1.logger.trace?.('re-retrieved record', localTime, this.#entry?.localTime);
|
|
4182
5372
|
localTime = entry?.localTime;
|
|
4183
5373
|
}
|
|
5374
|
+
let nodeId = entry?.nodeId;
|
|
5375
|
+
if (isRocksDB && entry) {
|
|
5376
|
+
const head = resolveAuditHead(thisId, entry.version, nodeId, entry.additionalAuditRefs);
|
|
5377
|
+
localTime = head.txnLogKey;
|
|
5378
|
+
nodeId = head.nodeId;
|
|
5379
|
+
}
|
|
4184
5380
|
logger_ts_1.logger.trace?.('Subscription from', startTime, 'from', thisId, localTime);
|
|
4185
5381
|
if (startTime < localTime) {
|
|
4186
5382
|
// start time specified, get the audit history for this record. Set startTime up
|
|
@@ -4191,7 +5387,6 @@ function makeTable(options) {
|
|
|
4191
5387
|
const history = [];
|
|
4192
5388
|
let inspected = 0;
|
|
4193
5389
|
let nextTime = localTime;
|
|
4194
|
-
let nodeId = entry?.nodeId;
|
|
4195
5390
|
do {
|
|
4196
5391
|
if (++recordsSinceYield >= REPLAY_YIELD_INTERVAL) {
|
|
4197
5392
|
recordsSinceYield = 0;
|
|
@@ -4222,8 +5417,11 @@ function makeTable(options) {
|
|
|
4222
5417
|
else if (!isActive())
|
|
4223
5418
|
return;
|
|
4224
5419
|
}
|
|
4225
|
-
|
|
4226
|
-
|
|
5420
|
+
const previousHead = isRocksDB
|
|
5421
|
+
? resolveAuditHead(thisId, auditRecord.previousVersion, auditRecord.previousNodeId, auditRecord.previousAdditionalAuditRefs)
|
|
5422
|
+
: { txnLogKey: auditRecord.previousVersion, nodeId: auditRecord.previousNodeId };
|
|
5423
|
+
nextTime = previousHead.txnLogKey;
|
|
5424
|
+
nodeId = previousHead.nodeId;
|
|
4227
5425
|
}
|
|
4228
5426
|
else
|
|
4229
5427
|
break;
|
|
@@ -4418,6 +5616,7 @@ function makeTable(options) {
|
|
|
4418
5616
|
store: primaryStore,
|
|
4419
5617
|
entry: this.#entry,
|
|
4420
5618
|
nodeName: context?.nodeName,
|
|
5619
|
+
recordVersion: options?.version,
|
|
4421
5620
|
validate: () => {
|
|
4422
5621
|
if (!context?.source) {
|
|
4423
5622
|
transaction.checkOverloaded();
|
|
@@ -4482,6 +5681,108 @@ function makeTable(options) {
|
|
|
4482
5681
|
});
|
|
4483
5682
|
});
|
|
4484
5683
|
}
|
|
5684
|
+
/**
|
|
5685
|
+
* Write one cluster record-lock control entry (harper#483 Phase 1). Not local-only: replicating
|
|
5686
|
+
* it IS the send.
|
|
5687
|
+
*
|
|
5688
|
+
* `recordId` must stay null. An entry carrying the locked key would share
|
|
5689
|
+
* `(version, tableId, recordId, nodeId)` with the holder's own first write, which is stamped at
|
|
5690
|
+
* exactly `ts_R`, and `RocksTransactionLogStore.getSync` answers with the FIRST entry at a
|
|
5691
|
+
* timestamp and key — so `_writeUpdate`'s keyed dedup would find this one and drop that write.
|
|
5692
|
+
* The payload goes in as bytes rather than through `recordUpdater`, which would run it through
|
|
5693
|
+
* schema projection and the table's shared structure dictionary.
|
|
5694
|
+
*/
|
|
5695
|
+
static writeLockControlEntry(entry) {
|
|
5696
|
+
const encodedRecord = (0, recordLockCoordinator_ts_1.encodeLockControlPayload)(entry);
|
|
5697
|
+
const nodeId = (0, nodeIdMapping_ts_1.getThisNodeId)(auditStore) ?? 0;
|
|
5698
|
+
let position;
|
|
5699
|
+
// No entry pins its clock, the request included. `ts_R` is minted before the write, so pinning
|
|
5700
|
+
// to it can land the entry behind a peer's replication cursor if any write to this table
|
|
5701
|
+
// commits in between — the same hazard that rules it out for grants and releases, which are
|
|
5702
|
+
// written later still. The protocol reads `ts_R` from the payload, so the entry's own log key
|
|
5703
|
+
// never has to equal it.
|
|
5704
|
+
const context = {};
|
|
5705
|
+
return Promise.resolve((0, transaction_ts_1.transaction)(context, (txn) => {
|
|
5706
|
+
const tableTxn = txnForContext({ transaction: txn });
|
|
5707
|
+
tableTxn.addWrite({
|
|
5708
|
+
key: null,
|
|
5709
|
+
store: primaryStore,
|
|
5710
|
+
skipReplicationConfirmation: true,
|
|
5711
|
+
commit: (txnTime, _existingEntry, _retry, nativeTransaction) => {
|
|
5712
|
+
position = txnTime;
|
|
5713
|
+
return auditStore[isRocksDB ? 'putSync' : 'put'](null, {
|
|
5714
|
+
version: txnTime,
|
|
5715
|
+
tableId,
|
|
5716
|
+
recordId: null,
|
|
5717
|
+
nodeId,
|
|
5718
|
+
type: entry.type,
|
|
5719
|
+
encodedRecord,
|
|
5720
|
+
extendedType: 0,
|
|
5721
|
+
// Zero, not the table's count: these bytes were packed by the private control `Packr`
|
|
5722
|
+
// and carry none of the table's structures. `RocksTransactionLogStore` raises the
|
|
5723
|
+
// per-(log, table) structure watermark from this field and flags the entry that does
|
|
5724
|
+
// it, so claiming the table's version would let a release take `HAS_STRUCTURE_UPDATE`
|
|
5725
|
+
// and leave the next real write at that version unflagged — a receiver that learns
|
|
5726
|
+
// structures only from flagged entries then decodes later records against a stale set
|
|
5727
|
+
// (harper#1348's class). A payload with no table structures cannot advance them.
|
|
5728
|
+
structureVersion: 0,
|
|
5729
|
+
}, { instructedWrite: true, transaction: nativeTransaction, nodeId, viaNodeId: nodeId });
|
|
5730
|
+
},
|
|
5731
|
+
});
|
|
5732
|
+
})).then(() => position);
|
|
5733
|
+
}
|
|
5734
|
+
/**
|
|
5735
|
+
* The coordinator that holds this node's admissions, transport or not. Releasing and registering
|
|
5736
|
+
* go here rather than through `lockCoordinator`, which answers undefined while a transport is
|
|
5737
|
+
* momentarily unregistered — and a release dropped on that answer leaves the key's home holding
|
|
5738
|
+
* its grant until the delegation's own deadline.
|
|
5739
|
+
*/
|
|
5740
|
+
static get admittingCoordinator() {
|
|
5741
|
+
return lockCoordinator;
|
|
5742
|
+
}
|
|
5743
|
+
/**
|
|
5744
|
+
* This table's cluster lock coordinator, created on first use and only while a transport is
|
|
5745
|
+
* registered for the database. Nothing is allocated on the Phase 0 path.
|
|
5746
|
+
*/
|
|
5747
|
+
static get lockCoordinator() {
|
|
5748
|
+
const transport = (0, recordLockCoordinator_ts_1.getClusterLockTransport)(databaseName);
|
|
5749
|
+
if (!transport) {
|
|
5750
|
+
// Deliberately NOT closed. harper-pro unregisters without a standalone claim during a
|
|
5751
|
+
// reconnect, and closing here would drop this node's record of the delegations it has
|
|
5752
|
+
// issued as a home — so the next registration would start empty and could grant a key
|
|
5753
|
+
// whose delegate is still admitting. The coordinator keeps ticking, its grants expire on
|
|
5754
|
+
// their own deadlines, and `isClusterLockRequired` is what fails an acquire closed in the
|
|
5755
|
+
// meantime. A genuine standalone claim clears the requirement and the coordinator with it.
|
|
5756
|
+
if (!(0, recordLockCoordinator_ts_1.isClusterLockRequired)(databaseName)) {
|
|
5757
|
+
lockCoordinator?.close();
|
|
5758
|
+
lockCoordinator = undefined;
|
|
5759
|
+
}
|
|
5760
|
+
return undefined;
|
|
5761
|
+
}
|
|
5762
|
+
if (lockCoordinator?.transport !== transport) {
|
|
5763
|
+
// The transport object changed, but this node's delegations and the handles they admitted
|
|
5764
|
+
// did not. The successor adopts that live authority in its constructor; the predecessor
|
|
5765
|
+
// is closed afterwards so nothing is dropped in between. See LockCoordinatorOptions.adopt.
|
|
5766
|
+
const predecessor = lockCoordinator;
|
|
5767
|
+
lockCoordinator = new recordLockCoordinator_ts_1.LockCoordinator({
|
|
5768
|
+
database: databaseName,
|
|
5769
|
+
table: tableName,
|
|
5770
|
+
nodeId: (0, nodeName_ts_1.getThisNodeName)(),
|
|
5771
|
+
transport,
|
|
5772
|
+
adopt: predecessor,
|
|
5773
|
+
// Writing to the local transaction log IS the send, so a transport that only computes
|
|
5774
|
+
// the participant set gets core's writer.
|
|
5775
|
+
writeControl: transport.writeControl
|
|
5776
|
+
? (entry) => transport.writeControl(tableName, entry)
|
|
5777
|
+
: (entry) => _a.writeLockControlEntry(entry),
|
|
5778
|
+
keyIdOf: DatabaseTransaction_ts_1.writeKeyId,
|
|
5779
|
+
nextTimestamp: () => primaryStore.getMonotonicTimestamp(),
|
|
5780
|
+
grantableAfterMono: transport.grantableAfterMono,
|
|
5781
|
+
});
|
|
5782
|
+
predecessor?.close();
|
|
5783
|
+
}
|
|
5784
|
+
return lockCoordinator;
|
|
5785
|
+
}
|
|
4485
5786
|
// #section: validation
|
|
4486
5787
|
validate(record, patch) {
|
|
4487
5788
|
// Accumulate structured per-field issues so the 400 carries `{ path, code,
|
|
@@ -4631,6 +5932,7 @@ function makeTable(options) {
|
|
|
4631
5932
|
return this.#version;
|
|
4632
5933
|
}
|
|
4633
5934
|
static async addAttributes(attributesToAdd) {
|
|
5935
|
+
_a.assertSchemaMutable('add attributes');
|
|
4634
5936
|
const new_attributes = attributes.slice(0);
|
|
4635
5937
|
for (const attribute of attributesToAdd) {
|
|
4636
5938
|
if (!attribute.name)
|
|
@@ -4646,9 +5948,10 @@ function makeTable(options) {
|
|
|
4646
5948
|
schemaDefined,
|
|
4647
5949
|
attributes: new_attributes,
|
|
4648
5950
|
});
|
|
4649
|
-
return
|
|
5951
|
+
return _a.indexingOperation;
|
|
4650
5952
|
}
|
|
4651
5953
|
static async removeAttributes(names) {
|
|
5954
|
+
_a.assertSchemaMutable('remove attributes');
|
|
4652
5955
|
const new_attributes = attributes.filter((attribute) => !names.includes(attribute.name));
|
|
4653
5956
|
(0, databases_ts_1.table)({
|
|
4654
5957
|
table: tableName,
|
|
@@ -4656,7 +5959,7 @@ function makeTable(options) {
|
|
|
4656
5959
|
schemaDefined,
|
|
4657
5960
|
attributes: new_attributes,
|
|
4658
5961
|
});
|
|
4659
|
-
return
|
|
5962
|
+
return _a.indexingOperation;
|
|
4660
5963
|
}
|
|
4661
5964
|
/**
|
|
4662
5965
|
* Get the size of the table in bytes (based on amount of pages stored in the database)
|
|
@@ -4669,6 +5972,10 @@ function makeTable(options) {
|
|
|
4669
5972
|
const stats = primaryStore.getStats();
|
|
4670
5973
|
return (stats.treeBranchPageCount + stats.treeLeafPageCount + stats.overflowPages) * stats.pageSize;
|
|
4671
5974
|
}
|
|
5975
|
+
/** Sizes of this table's durable record-structure dictionaries. */
|
|
5976
|
+
static getStructureCounts() {
|
|
5977
|
+
return primaryStore.encoder?.getStructureCounts?.();
|
|
5978
|
+
}
|
|
4672
5979
|
static getAuditSize() {
|
|
4673
5980
|
const stats = auditStore?.getStats();
|
|
4674
5981
|
return (stats &&
|
|
@@ -4687,7 +5994,7 @@ function makeTable(options) {
|
|
|
4687
5994
|
// iterate through the metadata entries to exclude their count and exclude the deletion counts
|
|
4688
5995
|
const exactCount = options?.exactCount;
|
|
4689
5996
|
const TIME_LIMIT = options?.timeLimit ?? 1000 / 2; // one second time limit, enforced by seeing if we are halfway through at 500ms
|
|
4690
|
-
const start = performance.now();
|
|
5997
|
+
const start = node_perf_hooks_1.performance.now();
|
|
4691
5998
|
let entryCount = 0;
|
|
4692
5999
|
let remainderPhysical = 0;
|
|
4693
6000
|
let estimator;
|
|
@@ -4711,7 +6018,7 @@ function makeTable(options) {
|
|
|
4711
6018
|
// a table too small to reach the floor is small enough to finish exactly
|
|
4712
6019
|
if (exactCount || entriesScanned < MIN_ESTIMATOR_SAMPLE)
|
|
4713
6020
|
continue;
|
|
4714
|
-
const now = performance.now();
|
|
6021
|
+
const now = node_perf_hooks_1.performance.now();
|
|
4715
6022
|
if (now <= nextCheckAt)
|
|
4716
6023
|
continue;
|
|
4717
6024
|
nextCheckAt = now + TIME_LIMIT;
|
|
@@ -4877,6 +6184,7 @@ function makeTable(options) {
|
|
|
4877
6184
|
// Refresh on every call: schema reload mutates `attributes` in place, so the
|
|
4878
6185
|
// class-construction snapshot would otherwise go stale.
|
|
4879
6186
|
this.embedAttributes = this.attributes.filter((a) => a?.embed);
|
|
6187
|
+
expiresAtProperty = this.attributes.find((attribute) => attribute.expiresAt);
|
|
4880
6188
|
// Drop registry entries for attributes that are no longer `@embed`, so a dropped
|
|
4881
6189
|
// directive doesn't leave a stale embedder or block a default refresh on re-add.
|
|
4882
6190
|
const embedNames = new Set(this.embedAttributes.map((a) => a.name));
|
|
@@ -4925,7 +6233,7 @@ function makeTable(options) {
|
|
|
4925
6233
|
const computed = attribute.computed;
|
|
4926
6234
|
// Register the default embedder unless an author override is set. Sits outside
|
|
4927
6235
|
// the resolver chain below so `@embed` fields still flow through auto-HNSW indexing.
|
|
4928
|
-
if (attribute.embed && !
|
|
6236
|
+
if (attribute.embed && !_a.userSetEmbedders.has(attribute.name)) {
|
|
4929
6237
|
this.userEmbedders[attribute.name] = (0, embedHook_ts_1.createDefaultEmbedder)(attribute.embed);
|
|
4930
6238
|
}
|
|
4931
6239
|
if (relationship) {
|
|
@@ -4943,7 +6251,7 @@ function makeTable(options) {
|
|
|
4943
6251
|
const id = object[relationship.from ? relationship.from : primaryKey];
|
|
4944
6252
|
const relatedTable = attribute.elements.definition.tableClass;
|
|
4945
6253
|
if (returnEntry) {
|
|
4946
|
-
return (0, search_ts_1.searchByIndex)({ attribute: relationship.to, value: id }, txnForContext(context).getReadTxn(), false, relatedTable, false).map((entry) => {
|
|
6254
|
+
return (0, search_ts_1.searchByIndex)({ attribute: relationship.to, value: id }, txnForContext(context).getReadTxn(), false, relatedTable, { allowFullScan: false }).map((entry) => {
|
|
4947
6255
|
if (entry && entry.key !== undefined)
|
|
4948
6256
|
return entry;
|
|
4949
6257
|
return relatedTable.primaryStore.getEntry(entry, {
|
|
@@ -4991,7 +6299,7 @@ function makeTable(options) {
|
|
|
4991
6299
|
const options = { transaction: txnForContext(context).getReadTxn() };
|
|
4992
6300
|
const results = normalizedIds.map((id) => {
|
|
4993
6301
|
const value = store[method](id, options);
|
|
4994
|
-
if (
|
|
6302
|
+
if (_a.loadAsInstance === false)
|
|
4995
6303
|
freezeRecord(returnEntry ? value?.value : value);
|
|
4996
6304
|
return value;
|
|
4997
6305
|
});
|
|
@@ -5000,7 +6308,7 @@ function makeTable(options) {
|
|
|
5000
6308
|
const value = definition.tableClass.primaryStore[returnEntry ? 'getEntry' : 'getSync'](ids, {
|
|
5001
6309
|
transaction: txnForContext(context).getReadTxn(),
|
|
5002
6310
|
});
|
|
5003
|
-
if (
|
|
6311
|
+
if (_a.loadAsInstance === false)
|
|
5004
6312
|
freezeRecord(returnEntry ? value?.value : value);
|
|
5005
6313
|
return value;
|
|
5006
6314
|
};
|
|
@@ -5155,30 +6463,99 @@ function makeTable(options) {
|
|
|
5155
6463
|
this.userSetEmbedders.add(attribute_name);
|
|
5156
6464
|
}
|
|
5157
6465
|
static async deleteHistory(endTime = 0, cleanupDeletedRecords = false) {
|
|
5158
|
-
|
|
6466
|
+
const maxConcurrentRemovals = isRocksDB ? MAX_CONCURRENT_HISTORY_REMOVALS : MAX_CONCURRENT_LMDB_HISTORY_REMOVALS;
|
|
6467
|
+
const inFlightRemovals = new Set();
|
|
6468
|
+
const removalSlotWaiters = [];
|
|
6469
|
+
let removalsAttempted = 0;
|
|
6470
|
+
let removalsSucceeded = 0;
|
|
6471
|
+
let firstRemovalError;
|
|
6472
|
+
function startRemoval(remove, errorMessage, onSuccess) {
|
|
6473
|
+
removalsAttempted++;
|
|
6474
|
+
const removal = new Promise((resolve) => resolve(remove()))
|
|
6475
|
+
.then(() => {
|
|
6476
|
+
removalsSucceeded++;
|
|
6477
|
+
onSuccess?.();
|
|
6478
|
+
}, (error) => {
|
|
6479
|
+
// capture before logging: a throwing logger must not cost us the error we may rethrow
|
|
6480
|
+
if (firstRemovalError === undefined)
|
|
6481
|
+
firstRemovalError = error;
|
|
6482
|
+
harper_logger_ts_1.default.warn(errorMessage, error);
|
|
6483
|
+
})
|
|
6484
|
+
.catch(() => undefined)
|
|
6485
|
+
.finally(() => {
|
|
6486
|
+
inFlightRemovals.delete(removal);
|
|
6487
|
+
removalSlotWaiters.shift()?.();
|
|
6488
|
+
});
|
|
6489
|
+
inFlightRemovals.add(removal);
|
|
6490
|
+
}
|
|
6491
|
+
function queueRemoval(remove, errorMessage, onSuccess) {
|
|
6492
|
+
if (inFlightRemovals.size >= maxConcurrentRemovals) {
|
|
6493
|
+
return new Promise((resolve) => {
|
|
6494
|
+
removalSlotWaiters.push(resolve);
|
|
6495
|
+
}).then(() => startRemoval(remove, errorMessage, onSuccess));
|
|
6496
|
+
}
|
|
6497
|
+
startRemoval(remove, errorMessage, onSuccess);
|
|
6498
|
+
}
|
|
6499
|
+
const drainRemovals = () => Promise.all(inFlightRemovals);
|
|
5159
6500
|
let entriesDeleted = 0;
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
6501
|
+
// LMDB only: RocksTransactionLogStore.remove() is a no-op, so a RocksDB deleteHistory removes
|
|
6502
|
+
// nothing and must not claim it did.
|
|
6503
|
+
// A bound above everything reachable must not be recorded as the floor: the floor only rises
|
|
6504
|
+
// and a store with a record is never re-stamped, so it would never come down, for every table in
|
|
6505
|
+
// this database. `boundedAuditPruneEnd` clamps the cutoff to just above the newest key in the
|
|
6506
|
+
// log, and the scan below uses that same value as its range end, so the prune cannot remove an
|
|
6507
|
+
// entry the floor does not cover.
|
|
6508
|
+
let pruneEnd = endTime;
|
|
6509
|
+
if (!isRocksDB) {
|
|
6510
|
+
pruneEnd = (0, auditStore_ts_1.boundedAuditPruneEnd)(auditStore, endTime);
|
|
6511
|
+
(0, auditStore_ts_1.raiseAuditFloor)(auditStore, pruneEnd);
|
|
6512
|
+
}
|
|
6513
|
+
try {
|
|
6514
|
+
for (const auditRecord of auditStore.getRange({
|
|
6515
|
+
// must not be zero: 0 encodes to all zero bytes and so overlaps the symbol keys, as in
|
|
6516
|
+
// getHistory below
|
|
6517
|
+
start: 1,
|
|
6518
|
+
end: pruneEnd,
|
|
6519
|
+
})) {
|
|
6520
|
+
await rest(); // yield to other async operations
|
|
6521
|
+
if (auditRecord.tableId !== tableId)
|
|
6522
|
+
continue;
|
|
6523
|
+
const backpressure = queueRemoval(() => (0, auditStore_ts_1.removeAuditEntry)(auditStore, auditRecord), 'Error removing audit entry during deleteHistory', () => {
|
|
6524
|
+
entriesDeleted++;
|
|
6525
|
+
});
|
|
6526
|
+
if (backpressure)
|
|
6527
|
+
await backpressure;
|
|
6528
|
+
}
|
|
6529
|
+
}
|
|
6530
|
+
finally {
|
|
6531
|
+
await drainRemovals();
|
|
5169
6532
|
}
|
|
5170
6533
|
if (cleanupDeletedRecords) {
|
|
5171
6534
|
// this is separate procedure we can do if the records are not being cleaned up by the audit log. This shouldn't
|
|
5172
6535
|
// ever happen, but if there are cleanup failures for some reason, we can run this to clean up the records
|
|
5173
|
-
|
|
5174
|
-
const {
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
6536
|
+
try {
|
|
6537
|
+
for (const entry of primaryStore.getRange({ start: 0, versions: true })) {
|
|
6538
|
+
const { key, value, localTime, version } = entry;
|
|
6539
|
+
await rest(); // yield to other async operations
|
|
6540
|
+
const auditTime = isRocksDB && version != null
|
|
6541
|
+
? resolveAuditHead(key, version, entry.nodeId, entry.additionalAuditRefs).txnLogKey
|
|
6542
|
+
: localTime;
|
|
6543
|
+
if (value === null && version != null && auditTime < pruneEnd) {
|
|
6544
|
+
const backpressure = queueRemoval(() => primaryStore.remove(key, version), 'Error removing deleted record during deleteHistory');
|
|
6545
|
+
if (backpressure)
|
|
6546
|
+
await backpressure;
|
|
6547
|
+
}
|
|
5178
6548
|
}
|
|
5179
6549
|
}
|
|
6550
|
+
finally {
|
|
6551
|
+
await drainRemovals();
|
|
6552
|
+
}
|
|
6553
|
+
}
|
|
6554
|
+
if (removalsAttempted > 0 && removalsSucceeded === 0) {
|
|
6555
|
+
// zero progress must not report the same success as "nothing was eligible" (see DESIGN.md);
|
|
6556
|
+
// partial failures stay best-effort, logged and excluded from the returned count
|
|
6557
|
+
throw firstRemovalError ?? new Error('Every removal attempted during deleteHistory failed');
|
|
5180
6558
|
}
|
|
5181
|
-
await completion;
|
|
5182
6559
|
return entriesDeleted;
|
|
5183
6560
|
}
|
|
5184
6561
|
static async *getHistory(startTime = 0, endTime = Infinity) {
|
|
@@ -5187,14 +6564,15 @@ function makeTable(options) {
|
|
|
5187
6564
|
end: endTime,
|
|
5188
6565
|
})) {
|
|
5189
6566
|
await rest(); // yield to other async operations
|
|
5190
|
-
if (auditRecord.tableId !== tableId)
|
|
6567
|
+
if (auditRecord.tableId !== tableId || auditRecord.type === 'evict' || (0, auditStore_ts_1.isLockControlType)(auditRecord.type))
|
|
5191
6568
|
continue;
|
|
5192
6569
|
yield {
|
|
5193
6570
|
id: auditRecord.recordId,
|
|
5194
|
-
|
|
6571
|
+
// Compatibility-facing LMDB history has always reported/grouped by record version.
|
|
6572
|
+
localTime: isRocksDB ? auditRecord.txnLogKey : auditRecord.version,
|
|
5195
6573
|
version: auditRecord.version,
|
|
5196
6574
|
type: auditRecord.type,
|
|
5197
|
-
value: auditRecord.getValue(primaryStore, true, auditRecord.
|
|
6575
|
+
value: auditRecord.getValue(primaryStore, true, auditRecord.txnLogKey),
|
|
5198
6576
|
user: auditRecord.user,
|
|
5199
6577
|
operation: auditRecord.originatingOperation,
|
|
5200
6578
|
};
|
|
@@ -5207,7 +6585,9 @@ function makeTable(options) {
|
|
|
5207
6585
|
const entry = primaryStore.getEntry(id);
|
|
5208
6586
|
if (!entry)
|
|
5209
6587
|
return history;
|
|
5210
|
-
let nextVersion =
|
|
6588
|
+
let nextVersion = isRocksDB
|
|
6589
|
+
? resolveAuditHead(id, entry.version, entry.nodeId, entry.additionalAuditRefs).txnLogKey
|
|
6590
|
+
: entry.localTime;
|
|
5211
6591
|
if (!nextVersion)
|
|
5212
6592
|
throw new Error('The entry does not have a local audit time');
|
|
5213
6593
|
const count = 0;
|
|
@@ -5218,20 +6598,26 @@ function makeTable(options) {
|
|
|
5218
6598
|
let highestPreviousVersion = 0;
|
|
5219
6599
|
const start = nextVersion - auditWindow;
|
|
5220
6600
|
for (const auditRecord of auditStore.getRange({ start, end: nextVersion + 0.001 })) {
|
|
5221
|
-
if (auditRecord.tableId === tableId &&
|
|
6601
|
+
if (auditRecord.tableId === tableId &&
|
|
6602
|
+
auditRecord.type !== 'evict' &&
|
|
6603
|
+
!(0, auditStore_ts_1.isLockControlType)(auditRecord.type) &&
|
|
6604
|
+
(0, ordered_binary_1.compareKeys)(auditRecord.recordId, id) === 0) {
|
|
5222
6605
|
history.splice(insertionPoint, 0, {
|
|
5223
6606
|
id: auditRecord.recordId,
|
|
5224
|
-
localTime: auditRecord.version,
|
|
6607
|
+
localTime: isRocksDB ? auditRecord.txnLogKey : auditRecord.version,
|
|
5225
6608
|
version: auditRecord.version,
|
|
5226
6609
|
type: auditRecord.type,
|
|
5227
|
-
// reconstruct each entry's record image as of its own
|
|
6610
|
+
// reconstruct each entry's record image as of its own log position, not the audit
|
|
5228
6611
|
// window boundary (nextVersion), matching getHistory (issue #1330)
|
|
5229
|
-
value: auditRecord.getValue(primaryStore, true, auditRecord.
|
|
6612
|
+
value: auditRecord.getValue(primaryStore, true, auditRecord.txnLogKey),
|
|
5230
6613
|
user: auditRecord.user,
|
|
5231
6614
|
operation: auditRecord.originatingOperation,
|
|
5232
6615
|
});
|
|
5233
|
-
|
|
5234
|
-
|
|
6616
|
+
const previousVersion = isRocksDB
|
|
6617
|
+
? resolveAuditHead(id, auditRecord.previousVersion, auditRecord.previousNodeId, auditRecord.previousAdditionalAuditRefs).txnLogKey
|
|
6618
|
+
: auditRecord.previousVersion;
|
|
6619
|
+
if (previousVersion > highestPreviousVersion && previousVersion < start) {
|
|
6620
|
+
highestPreviousVersion = previousVersion;
|
|
5235
6621
|
}
|
|
5236
6622
|
}
|
|
5237
6623
|
}
|
|
@@ -5246,17 +6632,26 @@ function makeTable(options) {
|
|
|
5246
6632
|
const promises = [primaryStore.clear()];
|
|
5247
6633
|
for (const key in indices) {
|
|
5248
6634
|
const index = indices[key];
|
|
6635
|
+
index.customIndex?.resetDerivedStorage?.();
|
|
5249
6636
|
promises.push(index.clearAsync ? index.clearAsync() : index.clear());
|
|
5250
6637
|
}
|
|
5251
6638
|
return Promise.all(promises);
|
|
5252
6639
|
}
|
|
6640
|
+
/** Release everything makeTable() registered process-wide; the class must not be used afterwards. */
|
|
5253
6641
|
static cleanup() {
|
|
6642
|
+
disposed = true;
|
|
6643
|
+
void _a.derivedIndexRuntime?.close();
|
|
6644
|
+
clearTimeout(cleanupTimer);
|
|
6645
|
+
settlePendingCleanup();
|
|
6646
|
+
clearInterval(recordExpirationInterval);
|
|
5254
6647
|
deleteCallbackHandle?.remove();
|
|
6648
|
+
(0, storageReclamation_ts_1.removeStorageReclamationHandler)(primaryStore.path, reclamationHandler);
|
|
5255
6649
|
}
|
|
5256
6650
|
static _readTxnForContext(context) {
|
|
5257
6651
|
return txnForContext(context).getReadTxn();
|
|
5258
6652
|
}
|
|
5259
6653
|
}
|
|
6654
|
+
_a = TableResource;
|
|
5260
6655
|
const throttledCallToSource = (0, throttle_ts_1.throttle)(async (source, id, sourceContext, existingEntry) => {
|
|
5261
6656
|
// call the data source if it exists and will fulfill our request for data
|
|
5262
6657
|
if (source && source.get && (!source.get.reliesOnPrototype || source.prototype.get)) {
|
|
@@ -5270,11 +6665,24 @@ function makeTable(options) {
|
|
|
5270
6665
|
}, () => {
|
|
5271
6666
|
throw new hdbError_ts_1.ServerError('Service unavailable, exceeded request queue limit for resolving cache record', 503);
|
|
5272
6667
|
});
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
6668
|
+
try {
|
|
6669
|
+
TableResource.updatedAttributes(); // on creation, update accessors as well
|
|
6670
|
+
if (expirationMs) {
|
|
6671
|
+
ttlFromLoad = true;
|
|
6672
|
+
try {
|
|
6673
|
+
TableResource.setTTLExpiration(expirationMs / 1000);
|
|
6674
|
+
}
|
|
6675
|
+
finally {
|
|
6676
|
+
ttlFromLoad = false;
|
|
6677
|
+
}
|
|
6678
|
+
}
|
|
6679
|
+
if (expiresAtProperty && !recordExpirationInterval)
|
|
6680
|
+
runRecordExpirationEviction();
|
|
6681
|
+
}
|
|
6682
|
+
catch (error) {
|
|
6683
|
+
TableResource.cleanup();
|
|
6684
|
+
throw error;
|
|
6685
|
+
}
|
|
5278
6686
|
return TableResource;
|
|
5279
6687
|
function updateIndices(id, existingRecord, record, options) {
|
|
5280
6688
|
let hasChanges;
|
|
@@ -5701,6 +7109,22 @@ function makeTable(options) {
|
|
|
5701
7109
|
return transaction;
|
|
5702
7110
|
}
|
|
5703
7111
|
}
|
|
7112
|
+
/**
|
|
7113
|
+
* Detach an unsaved TransactionWrite that a scoped lock() eagerly staged (see #reloadLocked)
|
|
7114
|
+
* once its handle upgrades to hold: hold staging is deferred and explicit-save-only, so a
|
|
7115
|
+
* dangling scoped write would otherwise auto-commit at the transaction sweep and clobber
|
|
7116
|
+
* whatever the hold write lands. Marking it .dropped lets a later save() on the instance that
|
|
7117
|
+
* owns it (checked via #savingOperation === this write) fall through to the hold branch
|
|
7118
|
+
* instead of resolving a detached, dead reference.
|
|
7119
|
+
*/
|
|
7120
|
+
function detachScopedUpgradeWrite(link, keyId, handle) {
|
|
7121
|
+
for (const write of link.writes) {
|
|
7122
|
+
if (write && !write.saved && write.lockHandle === handle && (0, DatabaseTransaction_ts_1.writeKeyId)(write.key) === keyId) {
|
|
7123
|
+
write.dropped = true;
|
|
7124
|
+
link.detachWrite(write);
|
|
7125
|
+
}
|
|
7126
|
+
}
|
|
7127
|
+
}
|
|
5704
7128
|
function getAttributeValue(entry, attribute_name, context, sort) {
|
|
5705
7129
|
if (!entry) {
|
|
5706
7130
|
return;
|
|
@@ -5818,6 +7242,10 @@ function makeTable(options) {
|
|
|
5818
7242
|
async function getFromSource(source, id, existingEntry, context, target) {
|
|
5819
7243
|
const metadataFlags = existingEntry?.metadataFlags;
|
|
5820
7244
|
const existingVersion = existingEntry?.version;
|
|
7245
|
+
const existingRecord = existingEntry?.value;
|
|
7246
|
+
const inheritedTimestamp = context?.timestamp || context?.transaction?.timestamp;
|
|
7247
|
+
const sourceTimestamp = inheritedTimestamp ||
|
|
7248
|
+
(isRocksDB ? primaryStore.getMonotonicTimestamp() : (0, commonUtility_ts_1.getNextMonotonicTime)());
|
|
5821
7249
|
let whenResolved, timer;
|
|
5822
7250
|
// We start by locking the record so that there is only one resolution happening at once;
|
|
5823
7251
|
// if there is already a resolution in process, we want to use the results of that resolution
|
|
@@ -5852,10 +7280,8 @@ function makeTable(options) {
|
|
|
5852
7280
|
}
|
|
5853
7281
|
// lock acquired — this request will actually load from source
|
|
5854
7282
|
setLoadedFromSource(target, true);
|
|
5855
|
-
const existingRecord = existingEntry?.value;
|
|
5856
7283
|
// it is important to remember that this is _NOT_ part of the current transaction; nothing is changing
|
|
5857
|
-
// with the canonical data, we are simply fulfilling our local copy of the canonical data
|
|
5858
|
-
// want a timestamp later than the current transaction
|
|
7284
|
+
// with the canonical data, we are simply fulfilling our local copy of the canonical data.
|
|
5859
7285
|
// we create a new context for the source, we want to determine the timestamp and don't want to
|
|
5860
7286
|
// attribute this to the current user
|
|
5861
7287
|
const sourceContext = {
|
|
@@ -5890,15 +7316,36 @@ function makeTable(options) {
|
|
|
5890
7316
|
// belt to that suspenders, at the cost of a bounded wait on a merely slow source
|
|
5891
7317
|
// before the drain's fail-closed timeout below.
|
|
5892
7318
|
const commitPromise = (0, transaction_ts_1.transaction)(sourceContext, async (_txn) => {
|
|
5893
|
-
const start = performance.now();
|
|
5894
|
-
let updatedRecord;
|
|
7319
|
+
const start = node_perf_hooks_1.performance.now();
|
|
7320
|
+
let updatedRecord, assignCreatedTime, sourceVersion;
|
|
5895
7321
|
let hasChanges, invalidated;
|
|
5896
7322
|
try {
|
|
5897
7323
|
updatedRecord = await throttledCallToSource(source, id, sourceContext, existingEntry);
|
|
5898
7324
|
invalidated = metadataFlags & exports.INVALIDATED;
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
7325
|
+
const reportedVersion = sourceContext.lastModified;
|
|
7326
|
+
const validReportedVersion = typeof reportedVersion === 'number' &&
|
|
7327
|
+
Number.isFinite(reportedVersion) &&
|
|
7328
|
+
reportedVersion > 0 &&
|
|
7329
|
+
reportedVersion <= MAX_DATE_TIMESTAMP;
|
|
7330
|
+
if (validReportedVersion) {
|
|
7331
|
+
// A record version is also this node's ordering token (precedesExistingVersion), so a
|
|
7332
|
+
// source-reported version ahead of local time would make every subsequent local write look
|
|
7333
|
+
// out-of-order and be discarded until wall-clock caught up — freezing the row. Honor what
|
|
7334
|
+
// the source reports, but never beyond now.
|
|
7335
|
+
const versionCeiling = Math.max(sourceTimestamp, Date.now());
|
|
7336
|
+
sourceVersion = Math.min(reportedVersion, versionCeiling);
|
|
7337
|
+
if (sourceVersion !== reportedVersion) {
|
|
7338
|
+
logger_ts_1.logger.trace?.(`Capping future source version for ${tableName} id ${id}: ${reportedVersion} -> ${sourceVersion}`);
|
|
7339
|
+
if (!warnedFutureSourceVersion) {
|
|
7340
|
+
warnedFutureSourceVersion = true;
|
|
7341
|
+
logger_ts_1.logger.warn?.(`The source for ${tableName} reported a lastModified ahead of local time (${new Date(reportedVersion).toISOString()}) for id ${id}; capping cached record versions at local time`);
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
7344
|
+
}
|
|
7345
|
+
else
|
|
7346
|
+
sourceVersion = sourceTimestamp;
|
|
7347
|
+
hasChanges = invalidated || (validReportedVersion && reportedVersion > existingVersion) || !existingRecord;
|
|
7348
|
+
const resolveDuration = node_perf_hooks_1.performance.now() - start;
|
|
5902
7349
|
(0, write_ts_1.recordAction)(resolveDuration, 'cache-resolution', tableName, null, 'success');
|
|
5903
7350
|
if (responseHeaders)
|
|
5904
7351
|
(0, Headers_ts_1.appendHeader)(responseHeaders, 'Server-Timing', `cache-resolve;dur=${resolveDuration.toFixed(2)}`, true);
|
|
@@ -5913,7 +7360,7 @@ function makeTable(options) {
|
|
|
5913
7360
|
if (status === 304) {
|
|
5914
7361
|
// revalidation of our current cached record
|
|
5915
7362
|
updatedRecord = existingRecord;
|
|
5916
|
-
|
|
7363
|
+
sourceVersion = existingVersion;
|
|
5917
7364
|
}
|
|
5918
7365
|
else if (!CACHEABLE_STATUS_CODES.has(status)) {
|
|
5919
7366
|
// non-cacheable status - propagate to client without caching
|
|
@@ -5991,10 +7438,14 @@ function makeTable(options) {
|
|
|
5991
7438
|
if (primaryKey && updatedRecord[primaryKey] !== id)
|
|
5992
7439
|
updatedRecord[primaryKey] = id;
|
|
5993
7440
|
}
|
|
7441
|
+
assignCreatedTime = createdTimeProperty && updatedRecord?.[createdTimeProperty.name] == null;
|
|
5994
7442
|
resolved = true;
|
|
7443
|
+
const resolvedVersion = isRocksDB && updatedRecord && existingVersion != null
|
|
7444
|
+
? Math.max(sourceVersion, existingVersion)
|
|
7445
|
+
: sourceVersion;
|
|
5995
7446
|
const resolvedEntry = {
|
|
5996
7447
|
key: id,
|
|
5997
|
-
version,
|
|
7448
|
+
version: resolvedVersion,
|
|
5998
7449
|
value: updatedRecord,
|
|
5999
7450
|
expiresAt: sourceContext.expiresAt,
|
|
6000
7451
|
metadataFlags: 0,
|
|
@@ -6043,7 +7494,7 @@ function makeTable(options) {
|
|
|
6043
7494
|
catch (settlingError) {
|
|
6044
7495
|
reject(error ?? settlingError);
|
|
6045
7496
|
}
|
|
6046
|
-
const resolveDuration = performance.now() - start;
|
|
7497
|
+
const resolveDuration = node_perf_hooks_1.performance.now() - start;
|
|
6047
7498
|
(0, write_ts_1.recordAction)(resolveDuration, 'cache-resolution', tableName, null, 'fail');
|
|
6048
7499
|
if (responseHeaders)
|
|
6049
7500
|
(0, Headers_ts_1.appendHeader)(responseHeaders, 'Server-Timing', `cache-resolve;dur=${resolveDuration.toFixed(2)}`, true);
|
|
@@ -6062,16 +7513,28 @@ function makeTable(options) {
|
|
|
6062
7513
|
const sourceWrite = {
|
|
6063
7514
|
key: id,
|
|
6064
7515
|
store: primaryStore,
|
|
6065
|
-
entry:
|
|
7516
|
+
entry: undefined,
|
|
6066
7517
|
nodeName: 'source',
|
|
6067
|
-
commit: (
|
|
7518
|
+
commit: (_txnTime, existingEntry, _retry, transaction) => {
|
|
6068
7519
|
sourceWrite.skipped = false; // reset on each retry; cleanup happens after commit if still true
|
|
6069
|
-
|
|
6070
|
-
|
|
7520
|
+
const racedVersion = existingEntry?.version;
|
|
7521
|
+
// A first fill may replace a record that raced it only when its candidate version strictly
|
|
7522
|
+
// orders after that record. The comparison has to be replica-independent, so a tie leaves the
|
|
7523
|
+
// raced record in place: precedesExistingVersion() would break the tie with *this* node's
|
|
7524
|
+
// name, and a fill from a shared source has no node identity of its own, so two replicas
|
|
7525
|
+
// resolving the same tie could keep different values at the same version.
|
|
7526
|
+
const replacesRacedRecord = racedVersion == null || sourceVersion > racedVersion;
|
|
7527
|
+
if (racedVersion !== existingVersion &&
|
|
7528
|
+
// Revalidations retain exact-CAS semantics; first fills use deterministic ordering.
|
|
7529
|
+
(existingVersion != null || !updatedRecord || !replacesRacedRecord)) {
|
|
7530
|
+
logger_ts_1.logger.trace?.(`Discarding resolved record from source with id: ${id}, source version: ${sourceVersion}, current version: ${racedVersion}`);
|
|
6071
7531
|
sourceWrite.skipped = true;
|
|
6072
7532
|
return;
|
|
6073
7533
|
}
|
|
6074
|
-
|
|
7534
|
+
const currentRecord = existingEntry?.value;
|
|
7535
|
+
const recordVersion = isRocksDB && racedVersion != null ? Math.max(sourceVersion, racedVersion) : sourceVersion;
|
|
7536
|
+
const txnLogKey = isRocksDB ? transaction?.getTimestamp?.() : recordVersion;
|
|
7537
|
+
updateIndices(id, currentRecord, updatedRecord, transaction && { transaction });
|
|
6075
7538
|
if (updatedRecord) {
|
|
6076
7539
|
if (existingEntry) {
|
|
6077
7540
|
context.previousResidency = TableResource.getResidencyRecord(existingEntry.residencyId);
|
|
@@ -6082,23 +7545,23 @@ function makeTable(options) {
|
|
|
6082
7545
|
if (updatedTimeProperty) {
|
|
6083
7546
|
updatedRecord[updatedTimeProperty.name] =
|
|
6084
7547
|
updatedTimeProperty.type === 'Date'
|
|
6085
|
-
? new Date(
|
|
7548
|
+
? new Date(recordVersion)
|
|
6086
7549
|
: updatedTimeProperty.type === 'String'
|
|
6087
|
-
? new Date(
|
|
6088
|
-
:
|
|
7550
|
+
? new Date(recordVersion).toISOString()
|
|
7551
|
+
: recordVersion;
|
|
6089
7552
|
}
|
|
6090
|
-
if (
|
|
6091
|
-
const existingCreatedTime =
|
|
7553
|
+
if (assignCreatedTime) {
|
|
7554
|
+
const existingCreatedTime = currentRecord?.[createdTimeProperty.name];
|
|
6092
7555
|
if (existingCreatedTime != null) {
|
|
6093
7556
|
updatedRecord[createdTimeProperty.name] = existingCreatedTime;
|
|
6094
7557
|
}
|
|
6095
7558
|
else {
|
|
6096
7559
|
updatedRecord[createdTimeProperty.name] =
|
|
6097
7560
|
createdTimeProperty.type === 'Date'
|
|
6098
|
-
? new Date(
|
|
7561
|
+
? new Date(recordVersion)
|
|
6099
7562
|
: createdTimeProperty.type === 'String'
|
|
6100
|
-
? new Date(
|
|
6101
|
-
:
|
|
7563
|
+
? new Date(recordVersion).toISOString()
|
|
7564
|
+
: recordVersion;
|
|
6102
7565
|
}
|
|
6103
7566
|
}
|
|
6104
7567
|
const residency = residencyFromFunction(TableResource.getResidency(updatedRecord, context));
|
|
@@ -6131,23 +7594,33 @@ function makeTable(options) {
|
|
|
6131
7594
|
}
|
|
6132
7595
|
residencyId = getResidencyId(residency);
|
|
6133
7596
|
}
|
|
6134
|
-
logger_ts_1.logger.trace?.(`Writing resolved record from source with id: ${id}, timestamp: ${new Date(
|
|
7597
|
+
logger_ts_1.logger.trace?.(`Writing resolved record from source with id: ${id}, timestamp: ${new Date(recordVersion).toISOString()}`);
|
|
6135
7598
|
// TODO: We are doing a double check for ifVersion that should probably be cleaned out
|
|
6136
|
-
|
|
7599
|
+
const writeAudit = (audit && (hasChanges || omitLocalRecord)) || null;
|
|
7600
|
+
updateRecord(id, updatedRecord, existingEntry, recordVersion, omitLocalRecord ? exports.INVALIDATED : 0, writeAudit, {
|
|
6137
7601
|
user: sourceContext?.user,
|
|
6138
7602
|
expiresAt: sourceContext.expiresAt,
|
|
6139
7603
|
residencyId,
|
|
6140
7604
|
transaction,
|
|
6141
7605
|
tableToTrack: tableName,
|
|
7606
|
+
additionalAuditRefs: writeAudit && txnLogKey !== recordVersion ? [{ version: txnLogKey, nodeId: 0 }] : undefined,
|
|
6142
7607
|
}, 'put', Boolean(invalidated), auditRecord);
|
|
6143
7608
|
// arm the eviction scanner, mirroring the .put() path
|
|
6144
7609
|
if (sourceContext.expiresAt)
|
|
6145
7610
|
scheduleCleanup();
|
|
6146
7611
|
}
|
|
6147
7612
|
else if (existingEntry) {
|
|
6148
|
-
logger_ts_1.logger.trace?.(`Deleting resolved record from source with id: ${id}, timestamp: ${new Date(
|
|
7613
|
+
logger_ts_1.logger.trace?.(`Deleting resolved record from source with id: ${id}, timestamp: ${new Date(recordVersion).toISOString()}`);
|
|
6149
7614
|
if (audit || trackDeletes) {
|
|
6150
|
-
updateRecord(id, null, existingEntry,
|
|
7615
|
+
updateRecord(id, null, existingEntry, recordVersion, 0, (audit && hasChanges) || null, {
|
|
7616
|
+
user: sourceContext?.user,
|
|
7617
|
+
transaction,
|
|
7618
|
+
tableToTrack: tableName,
|
|
7619
|
+
recordVersion,
|
|
7620
|
+
additionalAuditRefs: audit && hasChanges && txnLogKey !== recordVersion
|
|
7621
|
+
? [{ version: txnLogKey, nodeId: 0 }]
|
|
7622
|
+
: undefined,
|
|
7623
|
+
}, 'delete', Boolean(invalidated));
|
|
6151
7624
|
}
|
|
6152
7625
|
else {
|
|
6153
7626
|
(0, RecordEncoder_ts_1.removeEntry)(primaryStore, existingEntry, existingVersion);
|
|
@@ -6230,6 +7703,7 @@ function makeTable(options) {
|
|
|
6230
7703
|
if (hasSourceGet && primaryStore.hasLock(item.key, entry.version))
|
|
6231
7704
|
continue; // resolution in progress
|
|
6232
7705
|
updateIndices(item.key, entry.value, null, options);
|
|
7706
|
+
stageDerivedIndexEviction(transaction, item.key, entry.version);
|
|
6233
7707
|
}
|
|
6234
7708
|
(0, RecordEncoder_ts_1.removeEntry)(primaryStore, entry, options);
|
|
6235
7709
|
staged++;
|
|
@@ -6307,7 +7781,15 @@ function makeTable(options) {
|
|
|
6307
7781
|
},
|
|
6308
7782
|
};
|
|
6309
7783
|
}
|
|
7784
|
+
function settlePendingCleanup() {
|
|
7785
|
+
for (const resolve of pendingCleanupResolvers)
|
|
7786
|
+
resolve();
|
|
7787
|
+
pendingCleanupResolvers.clear();
|
|
7788
|
+
}
|
|
6310
7789
|
function scheduleCleanup(priority) {
|
|
7790
|
+
// a reclamation run may still hold this class's handler after cleanup(); a promise here would never settle
|
|
7791
|
+
if (disposed)
|
|
7792
|
+
return;
|
|
6311
7793
|
let runImmediately = false;
|
|
6312
7794
|
if (priority) {
|
|
6313
7795
|
// run immediately if there is a big increase in priority
|
|
@@ -6319,13 +7801,23 @@ function makeTable(options) {
|
|
|
6319
7801
|
if (cleanupInterval === lastCleanupInterval && !runImmediately)
|
|
6320
7802
|
return;
|
|
6321
7803
|
lastCleanupInterval = cleanupInterval;
|
|
6322
|
-
if ((0, manageThreads_js_1.
|
|
7804
|
+
if ((0, manageThreads_js_1.ownsStoreMaintenance)(primaryStore.path) || (ttlConfiguredByApplication && (0, manageThreads_js_1.isDedicatedWorker)())) {
|
|
6323
7805
|
// run on the last thread so we aren't overloading lower-numbered threads
|
|
6324
7806
|
if (cleanupTimer)
|
|
6325
7807
|
clearTimeout(cleanupTimer);
|
|
6326
|
-
if (!cleanupInterval)
|
|
7808
|
+
if (!cleanupInterval) {
|
|
7809
|
+
// no replacement pass is being scheduled, so nothing is left to settle a superseded one
|
|
7810
|
+
settlePendingCleanup();
|
|
6327
7811
|
return;
|
|
7812
|
+
}
|
|
7813
|
+
// This pass adopts the awaiters of the pass whose timer it just cleared: they settle when
|
|
7814
|
+
// this pass's scan completes, so a reclamation run is never told the storage was reclaimed
|
|
7815
|
+
// before any scan ran. It has to run now, though — that run blocks its whole path on the
|
|
7816
|
+
// promise, and the replacement's own slot can be a full interval out.
|
|
7817
|
+
if (pendingCleanupResolvers.size > 0)
|
|
7818
|
+
runImmediately = true;
|
|
6328
7819
|
return new Promise((resolve) => {
|
|
7820
|
+
pendingCleanupResolvers.add(resolve);
|
|
6329
7821
|
const startOfYear = new Date();
|
|
6330
7822
|
startOfYear.setMonth(0);
|
|
6331
7823
|
startOfYear.setDate(1);
|
|
@@ -6338,6 +7830,8 @@ function makeTable(options) {
|
|
|
6338
7830
|
? Date.now()
|
|
6339
7831
|
: Math.ceil((Date.now() - startOfYear.getTime()) / nextInterval) * nextInterval + startOfYear.getTime();
|
|
6340
7832
|
const startNextTimer = (nextScheduled) => {
|
|
7833
|
+
if (disposed)
|
|
7834
|
+
return;
|
|
6341
7835
|
logger_ts_1.logger.trace?.(`Scheduled next cleanup scan at ${new Date(nextScheduled)}`);
|
|
6342
7836
|
// noinspection JSVoidFunctionReturnValueUsed
|
|
6343
7837
|
cleanupTimer = setTimeout(() => (lastEvictionCompletion = lastEvictionCompletion.then(async () => {
|
|
@@ -6346,8 +7840,11 @@ function makeTable(options) {
|
|
|
6346
7840
|
const rootStore = primaryStore.rootStore;
|
|
6347
7841
|
if (rootStore.status !== 'open') {
|
|
6348
7842
|
clearTimeout(cleanupTimer);
|
|
7843
|
+
settlePendingCleanup();
|
|
6349
7844
|
return;
|
|
6350
7845
|
}
|
|
7846
|
+
// snapshot: an awaiter that arrives during this scan belongs to the pass that supersedes it
|
|
7847
|
+
const settling = [...pendingCleanupResolvers];
|
|
6351
7848
|
const MAX_CLEANUP_CONCURRENCY = 50;
|
|
6352
7849
|
const outstandingCleanupOperations = new Array(MAX_CLEANUP_CONCURRENCY);
|
|
6353
7850
|
let cleanupIndex = 0;
|
|
@@ -6427,7 +7924,10 @@ function makeTable(options) {
|
|
|
6427
7924
|
catch (error) {
|
|
6428
7925
|
logger_ts_1.logger.warn?.(`Error in cleanup scan for ${tableName}:`, error);
|
|
6429
7926
|
}
|
|
6430
|
-
|
|
7927
|
+
for (const settle of settling) {
|
|
7928
|
+
pendingCleanupResolvers.delete(settle);
|
|
7929
|
+
settle();
|
|
7930
|
+
}
|
|
6431
7931
|
cleanupPriority = 0; // reset the priority
|
|
6432
7932
|
})), Math.min(nextScheduled - Date.now(), hdbTerms_ts_1.MAX_SET_TIMEOUT_MS) // make sure it can fit in 32-bit signed number
|
|
6433
7933
|
).unref(); // don't let this prevent closing the thread
|
|
@@ -6438,17 +7938,19 @@ function makeTable(options) {
|
|
|
6438
7938
|
}
|
|
6439
7939
|
function addDeleteRemoval() {
|
|
6440
7940
|
deleteCallbackHandle = auditStore?.addDeleteRemovalCallback(tableId, primaryStore, (id, version) => {
|
|
6441
|
-
primaryStore.remove(id, version);
|
|
7941
|
+
return primaryStore.remove(id, version);
|
|
6442
7942
|
});
|
|
6443
7943
|
}
|
|
6444
7944
|
function runRecordExpirationEviction() {
|
|
6445
7945
|
// Periodically evict expired records, searching for records who expiresAt timestamp is before now
|
|
6446
|
-
if ((0, manageThreads_js_1.
|
|
7946
|
+
if ((0, manageThreads_js_1.ownsStoreExpiration)(primaryStore.path) || (ttlConfiguredByApplication && (0, manageThreads_js_1.isDedicatedWorker)())) {
|
|
6447
7947
|
// we want to run the pruning of expired records on only one thread so we don't have conflicts in evicting
|
|
6448
|
-
setInterval(async () => {
|
|
7948
|
+
recordExpirationInterval = setInterval(async () => {
|
|
6449
7949
|
// go through each database and table and then search for expired entries
|
|
6450
7950
|
// find any entries that are set to expire before now
|
|
6451
|
-
|
|
7951
|
+
// updatedAttributes() clears expiresAtProperty when a live redeclaration drops the directive,
|
|
7952
|
+
// and there is nothing left for this interval to scan by
|
|
7953
|
+
if (disposed || runningRecordExpiration || !expiresAtProperty)
|
|
6452
7954
|
return;
|
|
6453
7955
|
runningRecordExpiration = true;
|
|
6454
7956
|
try {
|