@harperfast/harper 5.3.0-alpha.1 → 5.3.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cliOperations.ts +12 -0
- package/bin/restart.ts +66 -6
- package/components/Application.ts +1134 -112
- package/components/DESIGN.md +421 -0
- package/components/OptionsWatcher.ts +368 -102
- package/components/Scope.ts +20 -1
- package/components/componentLoader.ts +43 -4
- package/components/deploymentOperations.ts +4 -1
- package/components/deploymentRecorder.ts +9 -2
- package/components/mcp/DESIGN.md +109 -0
- package/components/mcp/audit.ts +21 -17
- package/components/operations.js +284 -52
- package/components/operationsValidation.js +49 -2
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -0
- package/config/DESIGN.md +306 -0
- package/config/RootConfigWatcher.ts +191 -37
- package/config/configReadRetry.ts +62 -0
- package/config/configUtils.ts +78 -26
- package/config/parseConfigFile.ts +34 -0
- package/config/readConfigFileSync.ts +44 -0
- package/config/watcherArming.ts +59 -0
- package/config-root.schema.json +4 -0
- package/dataLayer/DESIGN.md +179 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
- package/dataLayer/restoreMarker.ts +92 -25
- package/dist/bin/cliOperations.js +13 -0
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/restart.js +42 -6
- package/dist/bin/restart.js.map +1 -1
- package/dist/components/Application.d.ts +104 -9
- package/dist/components/Application.js +954 -102
- package/dist/components/Application.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +4 -1
- package/dist/components/OptionsWatcher.js +378 -104
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/Scope.js +15 -1
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +35 -3
- package/dist/components/componentLoader.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/audit.d.ts +2 -1
- package/dist/components/mcp/audit.js +21 -17
- package/dist/components/mcp/audit.js.map +1 -1
- package/dist/components/operations.d.ts +28 -0
- package/dist/components/operations.js +263 -54
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +48 -2
- 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/config/RootConfigWatcher.d.ts +2 -0
- package/dist/config/RootConfigWatcher.js +189 -35
- 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 +10 -9
- package/dist/config/configUtils.js +63 -27
- package/dist/config/configUtils.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/harperBridge/ResourceBridge.js +21 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +21 -8
- package/dist/dataLayer/restoreMarker.js +94 -27
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +3 -0
- package/dist/resources/DatabaseTransaction.d.ts +25 -0
- package/dist/resources/DatabaseTransaction.js +224 -18
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +2 -1
- package/dist/resources/LMDBTransaction.js +22 -3
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +22 -6
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +1 -1
- package/dist/resources/RecordEncoder.js +46 -10
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/Resource.js +97 -13
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -0
- package/dist/resources/RocksIndexStore.js +2 -1
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
- package/dist/resources/RocksTransactionLogStore.js +104 -33
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +58 -7
- package/dist/resources/Table.js +1143 -348
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.d.ts +3 -0
- package/dist/resources/analytics/write.js +49 -16
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +170 -0
- package/dist/resources/auditStore.js +457 -11
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/crdt.d.ts +10 -0
- package/dist/resources/crdt.js +22 -0
- 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 +16 -13
- package/dist/resources/databases.js +625 -177
- package/dist/resources/databases.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.js +3 -2
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +867 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/fullTextDerivedIndex.d.ts +81 -0
- package/dist/resources/indexes/fullTextDerivedIndex.js +1004 -0
- package/dist/resources/indexes/fullTextDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/fullTextNativeBinding.d.ts +78 -0
- package/dist/resources/indexes/fullTextNativeBinding.js +85 -0
- package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -0
- 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/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +24 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +149 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -0
- 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 +47 -4
- package/dist/resources/recordLock.js +138 -7
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/resources/recordLockCoordinator.d.ts +557 -0
- package/dist/resources/recordLockCoordinator.js +2597 -0
- package/dist/resources/recordLockCoordinator.js.map +1 -0
- package/dist/resources/replayLogs.js +5 -0
- package/dist/resources/replayLogs.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 +160 -40
- 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.js +4 -6
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/auth.js +59 -23
- package/dist/security/auth.js.map +1 -1
- package/dist/security/deferredAuthentication.d.ts +11 -0
- package/dist/security/deferredAuthentication.js +25 -3
- package/dist/security/deferredAuthentication.js.map +1 -1
- package/dist/security/jsLoader.js +9 -4
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/REST.js +39 -5
- package/dist/server/REST.js.map +1 -1
- package/dist/server/http.d.ts +5 -1
- package/dist/server/http.js +34 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/mqtt.js +5 -1
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +2 -0
- package/dist/server/serverHelpers/Headers.js +6 -0
- 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 +2 -0
- package/dist/server/serverHelpers/contentTypes.js +189 -15
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +3 -3
- package/dist/server/serverHelpers/serverUtilities.js +110 -20
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.js +4 -1
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/serverHelpers/webSocketCloseReason.d.ts +2 -0
- package/dist/server/serverHelpers/webSocketCloseReason.js +29 -0
- package/dist/server/serverHelpers/webSocketCloseReason.js.map +1 -0
- package/dist/server/storageReclamation.js +1 -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/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 +64 -6
- package/dist/server/threads/manageThreads.js +261 -12
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.d.ts +1 -0
- package/dist/server/threads/socketRouter.js +196 -13
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +30 -7
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +24 -0
- package/dist/utility/errors/hdbError.js +58 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +2 -0
- package/dist/utility/hdbTerms.js +2 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.js +218 -38
- 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 +172 -92
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/npmUtilities.js +6 -4
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/watcherFallback.d.ts +0 -45
- package/dist/utility/watcherFallback.js +1 -125
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.js +6 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +9 -0
- package/json/systemSchema.json +3 -0
- package/npm-shrinkwrap.json +177 -87
- package/package.json +13 -5
- package/resources/DESIGN.md +689 -19
- package/resources/DatabaseTransaction.ts +230 -17
- package/resources/LMDBTransaction.ts +21 -3
- package/resources/PrimaryRocksDatabase.ts +20 -7
- package/resources/RecordEncoder.ts +52 -10
- package/resources/Resource.ts +97 -13
- package/resources/ResourceInterface.ts +8 -0
- package/resources/RocksIndexStore.ts +2 -1
- package/resources/RocksTransactionLogStore.ts +111 -31
- package/resources/Table.ts +1273 -396
- package/resources/analytics/DESIGN.md +38 -0
- package/resources/analytics/write.ts +50 -17
- package/resources/auditStore.ts +460 -11
- package/resources/crdt.ts +22 -0
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +611 -147
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +3 -2
- package/resources/indexes/DESIGN.md +833 -0
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +924 -46
- package/resources/indexes/fullTextDerivedIndex.ts +1165 -0
- package/resources/indexes/fullTextNativeBinding.ts +146 -0
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +181 -0
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/record-locks.md +1407 -0
- package/resources/recordLock.ts +173 -7
- package/resources/recordLockCoordinator.ts +3082 -0
- package/resources/replayLogs.ts +5 -0
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/DESIGN.md +40 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +169 -49
- package/resources/tracked.ts +73 -22
- package/resources/transactionBroadcast.ts +4 -4
- package/security/DESIGN.md +175 -0
- package/security/auth.ts +53 -24
- package/security/deferredAuthentication.ts +24 -2
- package/security/jsLoader.ts +9 -4
- package/server/DESIGN.md +275 -0
- package/server/REST.ts +42 -6
- package/server/http.ts +34 -2
- package/server/mqtt.ts +6 -4
- package/server/serverHelpers/Headers.ts +5 -1
- package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
- package/server/serverHelpers/Request.ts +33 -131
- package/server/serverHelpers/contentTypes.ts +188 -15
- package/server/serverHelpers/serverUtilities.ts +157 -27
- package/server/serverHelpers/uwsServer.ts +4 -1
- package/server/serverHelpers/webSocketCloseReason.ts +25 -0
- package/server/storageReclamation.ts +2 -2
- package/server/threads/isolatedApplications.ts +157 -0
- package/server/threads/logRotationTransport.ts +40 -0
- package/server/threads/manageThreads.js +254 -12
- package/server/threads/socketRouter.ts +217 -11
- package/server/threads/threadServer.js +30 -7
- package/studio/web/assets/{Chat-BnCBegQz.js → Chat-DADFFGe_.js} +1 -1
- package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-D_mI-rZ7.js} +3 -3
- package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-c2NiSDHa.js} +1 -1
- package/studio/web/assets/{applications-DHxGi7JH.js → applications-DktUqh7G.js} +1 -1
- package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-Cs_75Xhw.js} +1 -1
- package/studio/web/assets/{editor-DNcRHK54.js → editor-19b-Y1IN.js} +1 -1
- package/studio/web/assets/{html-Bdssedlg.js → html-DiYEQMpB.js} +1 -1
- package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CmR0y7P_.js} +1 -1
- package/studio/web/assets/{index-D6sxmFLR.js → index-Dm0rfkJ7.js} +5 -5
- package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-7vqt2CC3.js} +1 -1
- package/studio/web/assets/{javascript-B8meVSTH.js → javascript-BWtCFuOt.js} +1 -1
- package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-Buzzbv9y.js} +1 -1
- package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-SqsFWfTM.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-EMV5cmjo.js} +1 -1
- package/studio/web/assets/{notifications-CvZivSbh.js → notifications-CAB-LZWT.js} +1 -1
- package/studio/web/assets/{notifications-CQf18QKb.js → notifications-DRzmSRxM.js} +1 -1
- package/studio/web/assets/{profile-DdOwtntb.js → profile-BNKAl79n.js} +1 -1
- package/studio/web/assets/{regions-n69fwagr.js → regions-CUow_Zw2.js} +1 -1
- package/studio/web/assets/{register-PfWTCXWB.js → register-Dkt3WUMp.js} +2 -2
- package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BZRfMD0N.js} +1 -1
- package/studio/web/assets/{setup-CUx_aUDl.js → setup-D_yiEPO2.js} +2 -2
- package/studio/web/assets/{status-D7BVKqX9.js → status-DhHh1Ge-.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-D7L1PDtV.js} +1 -1
- package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-CCwLk1YS.js} +1 -1
- package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-BP1j1mjn.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-D7bnYxLw.js} +1 -1
- package/studio/web/assets/{workers-pR3jRY9D.js → workers-tOuCNT17.js} +1 -1
- package/studio/web/assets/{xml-2iRnMhQO.js → xml-BSG_3mQT.js} +1 -1
- package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-DxiLprBB.js} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/DESIGN.md +55 -0
- package/utility/errors/hdbError.ts +54 -0
- package/utility/hdbTerms.ts +2 -0
- package/utility/logging/harper_logger.ts +210 -30
- package/utility/logging/logGenerationCoordinator.ts +196 -0
- package/utility/logging/logRotation.ts +367 -0
- package/utility/logging/logRotator.ts +196 -91
- package/utility/npmUtilities.ts +6 -4
- package/utility/watcherFallback.ts +0 -122
- package/validation/configValidator.ts +6 -3
package/dist/resources/Table.js
CHANGED
|
@@ -63,12 +63,14 @@ const Resource_ts_1 = require("./Resource.js");
|
|
|
63
63
|
const when_ts_1 = require("../utility/when.js");
|
|
64
64
|
const DatabaseTransaction_ts_1 = require("./DatabaseTransaction.js");
|
|
65
65
|
const recordLock_ts_1 = require("./recordLock.js");
|
|
66
|
+
const nodeName_ts_1 = require("../server/nodeName.js");
|
|
66
67
|
const envMngr = __importStar(require("../utility/environment/environmentManager.js"));
|
|
67
68
|
const transactionBroadcast_ts_1 = require("./transactionBroadcast.js");
|
|
68
69
|
const hdbError_ts_1 = require("../utility/errors/hdbError.js");
|
|
69
70
|
const signalling = __importStar(require("../utility/signalling.js"));
|
|
70
71
|
const itc_js_1 = require("../server/threads/itc.js");
|
|
71
72
|
const databases_ts_1 = require("./databases.js");
|
|
73
|
+
const replicatedApplyFailure_ts_1 = require("./replicatedApplyFailure.js");
|
|
72
74
|
const search_ts_1 = require("./search.js");
|
|
73
75
|
const logger_ts_1 = require("../utility/logging/logger.js");
|
|
74
76
|
const staticResourceDispatch_ts_1 = require("./staticResourceDispatch.js");
|
|
@@ -77,6 +79,8 @@ const transaction_ts_1 = require("./transaction.js");
|
|
|
77
79
|
const ordered_binary_1 = require("ordered-binary");
|
|
78
80
|
const manageThreads_js_1 = require("../server/threads/manageThreads.js");
|
|
79
81
|
const auditStore_ts_1 = require("./auditStore.js");
|
|
82
|
+
const derivedIndexRegistry_ts_1 = require("./derivedIndexRegistry.js");
|
|
83
|
+
const recordLockCoordinator_ts_1 = require("./recordLockCoordinator.js");
|
|
80
84
|
const embedHook_ts_1 = require("./models/embedHook.js");
|
|
81
85
|
const common_utils_ts_1 = require("../utility/common_utils.js");
|
|
82
86
|
const RecordEncoder_ts_1 = require("./RecordEncoder.js");
|
|
@@ -97,6 +101,9 @@ const { sortBy } = lodash_1.default;
|
|
|
97
101
|
const { validateAttribute } = lmdbProcessRows_js_1.default;
|
|
98
102
|
const NULL_WITH_TIMESTAMP = new Uint8Array(9);
|
|
99
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');
|
|
100
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
|
|
101
108
|
const MAX_DATE_TIMESTAMP = 8.64e15;
|
|
102
109
|
const RECORD_PRUNING_INTERVAL = 60000; // one minute
|
|
@@ -124,6 +131,20 @@ const MAX_COUNT_PAGE = 10_000;
|
|
|
124
131
|
// How often the exact-count drain yields to the macrotask queue (must be a power of two for the bit-mask
|
|
125
132
|
// check). Keeps a large scan from monopolizing the event loop without adding a yield per row.
|
|
126
133
|
const COUNT_YIELD_INTERVAL = 2_048;
|
|
134
|
+
// Smallest forward sample `getRecordCount` will extrapolate a record rate from; below it the scan runs
|
|
135
|
+
// to completion and reports an exact count.
|
|
136
|
+
const MIN_ESTIMATOR_SAMPLE = 1_000;
|
|
137
|
+
// Budget intervals the forward scan may spend before it must estimate rather than keep scanning.
|
|
138
|
+
const MAX_ESTIMATE_CHECKPOINTS = 20;
|
|
139
|
+
// A store estimate's `count`, or 0 when the store answered with a shape that cannot be trusted --
|
|
140
|
+
// DESIGN.md's invariant for this API family is that such an answer degrades rather than poisons.
|
|
141
|
+
function usableCount(estimate) {
|
|
142
|
+
const { count, confidence } = estimate ?? {};
|
|
143
|
+
// `confidence` needs its own finiteness check, not just the range: `null >= 0 && null <= 1` is true
|
|
144
|
+
if (!Number.isFinite(count) || count < 0 || !Number.isFinite(confidence) || confidence < 0 || confidence > 1)
|
|
145
|
+
return 0;
|
|
146
|
+
return count;
|
|
147
|
+
}
|
|
127
148
|
envMngr.initSync();
|
|
128
149
|
const LMDB_PREFETCH_WRITES = envMngr.get(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_PREFETCHWRITES);
|
|
129
150
|
const LOCK_TIMEOUT = 10000;
|
|
@@ -416,6 +437,17 @@ function contextArgument(context) {
|
|
|
416
437
|
const resolved = context.getContext?.() || context;
|
|
417
438
|
return resolved instanceof DatabaseTransaction_ts_1.DatabaseTransaction ? { transaction: resolved } : resolved;
|
|
418
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
|
+
}
|
|
419
451
|
/** Distinguishes bare lock options from a record target (id, URL, {id:...}). */
|
|
420
452
|
function isPlainOptions(value) {
|
|
421
453
|
return (typeof value === 'object' &&
|
|
@@ -424,10 +456,23 @@ function isPlainOptions(value) {
|
|
|
424
456
|
!(value instanceof URLSearchParams) &&
|
|
425
457
|
value.id === undefined);
|
|
426
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
|
+
});
|
|
427
467
|
function makeTable(options) {
|
|
428
468
|
var _a;
|
|
429
469
|
const { primaryKey, indices, tableId, tableName, primaryStore, databasePath, databaseName, auditStore, schemaDefined, dbisDB: dbisDb, sealed, splitSegments, replicate, description, hidden, cacheControl, isBranch, } = options;
|
|
430
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
|
|
431
476
|
evictionMs ??= 0;
|
|
432
477
|
// Eviction without explicit expiration means expiration:0. Apply at construction so
|
|
433
478
|
// describe_all sees it on every worker, not just ones that ran setTTLExpiration.
|
|
@@ -439,6 +484,9 @@ function makeTable(options) {
|
|
|
439
484
|
if (!properties)
|
|
440
485
|
properties = (0, jsonSchemaTypes_ts_1.projectAttributesToProperties)(attributes);
|
|
441
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;
|
|
442
490
|
let warnedNullSourcePut = false; // latched: one warn per table per worker (see _writeUpdate)
|
|
443
491
|
let warnedFutureSourceVersion = false; // likewise (see getFromSource)
|
|
444
492
|
let sourceLoad; // if a source has a load function (replicator), record it here
|
|
@@ -665,6 +713,28 @@ function makeTable(options) {
|
|
|
665
713
|
}
|
|
666
714
|
return { txnLogKey: version, nodeId };
|
|
667
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
|
+
}
|
|
668
738
|
class TableResource extends Resource_ts_1.Resource {
|
|
669
739
|
#record; // the stored/frozen record from the database and stored in the cache (should not be modified directly)
|
|
670
740
|
#changes; // the changes to the record that have been made (should not be modified directly)
|
|
@@ -673,13 +743,27 @@ function makeTable(options) {
|
|
|
673
743
|
#savingOperation; // operation for the record is currently being saved
|
|
674
744
|
#lockHandle; // the record lock acquired by lock() — scoped or hold
|
|
675
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
|
+
}
|
|
676
758
|
/**
|
|
677
759
|
* Shared guard: if this instance is lock-writable but the handle is gone (expired or
|
|
678
760
|
* released), throw 409 before staging any write. Covers update/invalidate/relocate/delete
|
|
679
761
|
* in addition to the save() path. Every lock-writable instance carries its own handle in
|
|
680
762
|
* #lockHandle (scoped and hold alike), so we never need to search the registry here.
|
|
681
763
|
*/
|
|
682
|
-
#assertLiveHandle(id) {
|
|
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]();
|
|
683
767
|
if (!this.#lockWritable)
|
|
684
768
|
return;
|
|
685
769
|
const handle = this.#lockHandle;
|
|
@@ -687,7 +771,7 @@ function makeTable(options) {
|
|
|
687
771
|
// exact key the lock was acquired for.
|
|
688
772
|
if (handle.keyId !== (0, DatabaseTransaction_ts_1.writeKeyId)(id))
|
|
689
773
|
return;
|
|
690
|
-
if (handle.
|
|
774
|
+
if (handle.isExpired()) {
|
|
691
775
|
throw (0, recordLock_ts_1.lockNotHeldError)(handle);
|
|
692
776
|
}
|
|
693
777
|
}
|
|
@@ -699,6 +783,7 @@ function makeTable(options) {
|
|
|
699
783
|
static tableName = tableName;
|
|
700
784
|
static tableId = tableId;
|
|
701
785
|
static indices = indices;
|
|
786
|
+
static derivedIndexRuntime;
|
|
702
787
|
static audit = audit;
|
|
703
788
|
static databasePath = databasePath;
|
|
704
789
|
static databaseName = databaseName;
|
|
@@ -784,8 +869,60 @@ function makeTable(options) {
|
|
|
784
869
|
(async () => {
|
|
785
870
|
let userRoleUpdate = false;
|
|
786
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
|
+
};
|
|
787
922
|
// perform the write of an individual write event
|
|
788
923
|
const writeUpdate = async (event, context) => {
|
|
924
|
+
if ((0, auditStore_ts_1.isLockControlType)(event.type))
|
|
925
|
+
return applyLockControlEvent(event, context);
|
|
789
926
|
const value = event.value;
|
|
790
927
|
const Table = event.table ? databases_ts_1.databases[databaseName][event.table] : _a;
|
|
791
928
|
if (databaseName === hdbTerms_ts_1.SYSTEM_SCHEMA_NAME &&
|
|
@@ -816,6 +953,15 @@ function makeTable(options) {
|
|
|
816
953
|
async: true,
|
|
817
954
|
};
|
|
818
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'));
|
|
819
965
|
const resource = await Table.getResource(id, context, options);
|
|
820
966
|
if (event.finished)
|
|
821
967
|
await event.finished;
|
|
@@ -837,12 +983,15 @@ function makeTable(options) {
|
|
|
837
983
|
return resource._writeInvalidate(id, value, options);
|
|
838
984
|
case 'relocate':
|
|
839
985
|
return resource._writeRelocate(id, options);
|
|
840
|
-
default:
|
|
841
|
-
logger_ts_1.logger.error?.('Unknown operation', event.type, event.id);
|
|
842
986
|
}
|
|
843
987
|
};
|
|
844
988
|
/** Keeps the writes to any one key in arrival order; see DESIGN.md (harper#2211). */
|
|
845
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);
|
|
846
995
|
let chainKey;
|
|
847
996
|
try {
|
|
848
997
|
const Table = event.table ? databases_ts_1.databases[databaseName][event.table] : _a;
|
|
@@ -886,14 +1035,18 @@ function makeTable(options) {
|
|
|
886
1035
|
omitCurrent: true,
|
|
887
1036
|
};
|
|
888
1037
|
const subscribeOnThisThread = source.subscribeOnThisThread
|
|
889
|
-
? source.subscribeOnThisThread((0, manageThreads_js_1.
|
|
890
|
-
: (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
|
|
891
1040
|
const subscription = hasSubscribe && subscribeOnThisThread && (await source.subscribe?.(subscriptionOptions));
|
|
892
1041
|
if (subscription) {
|
|
893
1042
|
let txnInProgress;
|
|
894
1043
|
// we listen for events by iterating through the async iterator provided by the subscription
|
|
895
1044
|
for await (const event of subscription) {
|
|
1045
|
+
let failureEvent = event;
|
|
1046
|
+
let failurePosition;
|
|
1047
|
+
let applied = false;
|
|
896
1048
|
try {
|
|
1049
|
+
failurePosition = event?.timestamp;
|
|
897
1050
|
if (!event || typeof event !== 'object') {
|
|
898
1051
|
logger_ts_1.logger.error?.('Bad subscription event', event);
|
|
899
1052
|
continue;
|
|
@@ -901,6 +1054,7 @@ function makeTable(options) {
|
|
|
901
1054
|
const firstWrite = event.type === 'transaction' ? event.writes[0] : event;
|
|
902
1055
|
if (!firstWrite) {
|
|
903
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);
|
|
904
1058
|
continue;
|
|
905
1059
|
}
|
|
906
1060
|
event.source = source;
|
|
@@ -909,11 +1063,16 @@ function makeTable(options) {
|
|
|
909
1063
|
// there is no re-subscribe / sequence-id-resume path to recover it. Mark the context so the
|
|
910
1064
|
// commit retries such conflicts without a cap (see DatabaseTransaction commit).
|
|
911
1065
|
event.sourceApply = true;
|
|
1066
|
+
event[SOURCE_APPLY_POSITION] = failurePosition;
|
|
912
1067
|
if (event.type === 'end_txn') {
|
|
913
1068
|
// Capture the in-progress transaction in a stable local: the loop variable is reset
|
|
914
1069
|
// once this transaction completes (below), but the seq-id closure and the commit await
|
|
915
1070
|
// still need to reference it afterward.
|
|
916
1071
|
const committingTxn = txnInProgress;
|
|
1072
|
+
if (committingTxn) {
|
|
1073
|
+
failureEvent = committingTxn;
|
|
1074
|
+
failurePosition = committingTxn[SOURCE_APPLY_POSITION];
|
|
1075
|
+
}
|
|
917
1076
|
committingTxn?.resolve();
|
|
918
1077
|
let updateRecordedSequenceId;
|
|
919
1078
|
if (event.localTime && lastSequenceId !== event.localTime) {
|
|
@@ -996,6 +1155,7 @@ function makeTable(options) {
|
|
|
996
1155
|
let committed;
|
|
997
1156
|
try {
|
|
998
1157
|
committed = committingTxn ? await committingTxn.committed : undefined;
|
|
1158
|
+
applied = true;
|
|
999
1159
|
if (event.onCommit) {
|
|
1000
1160
|
// the onCommit callback can be async and carry associated work (e.g. blob
|
|
1001
1161
|
// transfer); wait for it too before recording the sequence id. Pass the commit
|
|
@@ -1032,6 +1192,7 @@ function makeTable(options) {
|
|
|
1032
1192
|
// than rethrow) so the current beginTxn still starts a fresh transaction with
|
|
1033
1193
|
// correct boundaries instead of having its writes applied as standalone ones.
|
|
1034
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);
|
|
1035
1196
|
}
|
|
1036
1197
|
finally {
|
|
1037
1198
|
// Clear it regardless of outcome so a rejected commit isn't re-awaited on the
|
|
@@ -1121,6 +1282,7 @@ function makeTable(options) {
|
|
|
1121
1282
|
// standalone write: backpressure on the commit before pulling the next event,
|
|
1122
1283
|
// and pass the commit resolution through to the callback.
|
|
1123
1284
|
const committed = commitResolution ? await commitResolution : undefined;
|
|
1285
|
+
applied = true;
|
|
1124
1286
|
await event.onCommit(committed);
|
|
1125
1287
|
}
|
|
1126
1288
|
}
|
|
@@ -1131,6 +1293,15 @@ function makeTable(options) {
|
|
|
1131
1293
|
}
|
|
1132
1294
|
catch (error) {
|
|
1133
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
|
+
}
|
|
1134
1305
|
}
|
|
1135
1306
|
}
|
|
1136
1307
|
}
|
|
@@ -1394,15 +1565,24 @@ function makeTable(options) {
|
|
|
1394
1565
|
* This also informs the scheduling for record eviction.
|
|
1395
1566
|
* @param opts Time in seconds until records expire, or an options object with `expiration`, `eviction`,
|
|
1396
1567
|
* and `scanInterval` (all in seconds, all optional). Number form preserves any previously configured
|
|
1397
|
-
* 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.
|
|
1398
1570
|
*/
|
|
1399
1571
|
static setTTLExpiration(opts) {
|
|
1400
1572
|
if (opts == null || (typeof opts !== 'number' && typeof opts !== 'object'))
|
|
1401
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
|
+
}
|
|
1402
1582
|
if (typeof opts === 'number') {
|
|
1403
1583
|
expirationMs = opts * 1000;
|
|
1404
1584
|
}
|
|
1405
|
-
else {
|
|
1585
|
+
else if (!preserveLoadedConfiguration) {
|
|
1406
1586
|
// `??` so an explicit 0 is treated as the user's chosen value, not as "missing"
|
|
1407
1587
|
expirationMs = (opts.expiration ?? 0) * 1000;
|
|
1408
1588
|
evictionMs = (opts.eviction ?? 0) * 1000;
|
|
@@ -1410,10 +1590,19 @@ function makeTable(options) {
|
|
|
1410
1590
|
}
|
|
1411
1591
|
if (expirationMs < 0)
|
|
1412
1592
|
throw new Error('Expiration can not be negative');
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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();
|
|
1417
1606
|
}
|
|
1418
1607
|
static getResidencyRecord(id) {
|
|
1419
1608
|
// getSync (not get): callers consume the result synchronously (e.g. residency.includes(...) in a
|
|
@@ -1521,6 +1710,39 @@ function makeTable(options) {
|
|
|
1521
1710
|
static async dropTable() {
|
|
1522
1711
|
_a.assertSchemaMutable('drop a table');
|
|
1523
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 + '/';
|
|
1524
1746
|
if (databaseName === databasePath) {
|
|
1525
1747
|
// Persist a drop tombstone on the primary catalog entry BEFORE any
|
|
1526
1748
|
// destructive work. If the process dies or a column family drop fails
|
|
@@ -1528,11 +1750,21 @@ function makeTable(options) {
|
|
|
1528
1750
|
// the next startup (or a same-name create) completes the drop via
|
|
1529
1751
|
// completeInterruptedDrop in databases.ts instead of resurrecting
|
|
1530
1752
|
// the table.
|
|
1531
|
-
|
|
1753
|
+
let tombstoneWrite;
|
|
1532
1754
|
const writeTombstone = () => {
|
|
1533
|
-
|
|
1534
|
-
if (!primaryMeta
|
|
1535
|
-
|
|
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;
|
|
1536
1768
|
primaryMeta.dropping = true;
|
|
1537
1769
|
// Stamps this drop's identity so the interrupted-drop retry budget in
|
|
1538
1770
|
// databases.ts can be scoped to THIS drop rather than the table name: a
|
|
@@ -1542,26 +1774,33 @@ function makeTable(options) {
|
|
|
1542
1774
|
// the budget by generation instead makes the new drop's tombstone carry
|
|
1543
1775
|
// its own fresh key regardless of what any worker last observed.
|
|
1544
1776
|
primaryMeta.dropGeneration = (0, node_crypto_1.randomUUID)();
|
|
1545
|
-
|
|
1777
|
+
tombstoneWrite = dbisDb.put(primaryCatalogKey, primaryMeta);
|
|
1778
|
+
return true;
|
|
1546
1779
|
};
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
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
|
+
}
|
|
1555
1793
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
tombstoneWrite = writeTombstone();
|
|
1560
|
-
});
|
|
1561
|
-
if (typeof tombstoneWrite?.then === 'function')
|
|
1562
|
-
await tombstoneWrite;
|
|
1794
|
+
catch (error) {
|
|
1795
|
+
restoreDerivedIndexesAfterFailedDrop();
|
|
1796
|
+
throw error;
|
|
1563
1797
|
}
|
|
1564
1798
|
}
|
|
1799
|
+
if (!dropIdentityConfirmed) {
|
|
1800
|
+
abortStaleDrop();
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
_a.derivedIndexRuntime = undefined;
|
|
1565
1804
|
// A get() against a sourcedFrom table resolves to its caller before the resolved
|
|
1566
1805
|
// record's cache write has committed (see getFromSource) - the write lands "in the
|
|
1567
1806
|
// background" for latency reasons. Flip this BEFORE removing the table from the
|
|
@@ -1574,7 +1813,8 @@ function makeTable(options) {
|
|
|
1574
1813
|
// family drops below. If a drop fails past this point the table stays
|
|
1575
1814
|
// invisible, and the tombstone guarantees the drop completes on the
|
|
1576
1815
|
// next startup (or on a same-name create).
|
|
1577
|
-
|
|
1816
|
+
if (databases_ts_1.databases[databaseName]?.[tableName] === _a)
|
|
1817
|
+
delete databases_ts_1.databases[databaseName][tableName];
|
|
1578
1818
|
// The above stops new source-fill writes from starting, but a write from a get()
|
|
1579
1819
|
// that already returned to its caller may still be in flight. Dropping the column
|
|
1580
1820
|
// families out from under that write is a genuine invariant violation, not just a
|
|
@@ -1605,14 +1845,21 @@ function makeTable(options) {
|
|
|
1605
1845
|
]);
|
|
1606
1846
|
clearTimeout(timer);
|
|
1607
1847
|
if (result === timedOut) {
|
|
1848
|
+
derivedIndexRuntime?.completeDrop?.();
|
|
1608
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.`);
|
|
1609
1850
|
}
|
|
1610
1851
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
(
|
|
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
|
+
}
|
|
1614
1857
|
}
|
|
1615
1858
|
}
|
|
1859
|
+
catch (error) {
|
|
1860
|
+
derivedIndexRuntime?.completeDrop?.();
|
|
1861
|
+
throw error;
|
|
1862
|
+
}
|
|
1616
1863
|
if (databaseName === databasePath) {
|
|
1617
1864
|
// part of a database.
|
|
1618
1865
|
// Drop the column families, then remove the catalog metadata - never
|
|
@@ -1629,13 +1876,13 @@ function makeTable(options) {
|
|
|
1629
1876
|
// same-name create completes the interrupted drop and writes fresh
|
|
1630
1877
|
// catalog rows, and clobbering those would orphan the new table.
|
|
1631
1878
|
const removeTombstonedCatalog = () => {
|
|
1632
|
-
const currentPrimary = dbisDb.getSync(
|
|
1633
|
-
if (!currentPrimary?.dropping)
|
|
1879
|
+
const currentPrimary = dbisDb.getSync(primaryCatalogKey);
|
|
1880
|
+
if (!currentPrimary?.dropping || (currentPrimary.tableId != null && currentPrimary.tableId !== tableId))
|
|
1634
1881
|
return false;
|
|
1635
1882
|
for (const attribute of attributes) {
|
|
1636
1883
|
dbisDb.remove(_a.tableName + '/' + attribute.name);
|
|
1637
1884
|
}
|
|
1638
|
-
dbisDb.remove(
|
|
1885
|
+
dbisDb.remove(primaryCatalogKey);
|
|
1639
1886
|
return true;
|
|
1640
1887
|
};
|
|
1641
1888
|
if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
|
|
@@ -1647,42 +1894,76 @@ function makeTable(options) {
|
|
|
1647
1894
|
// completeInterruptedDrop does), never an awaited drop(), or a
|
|
1648
1895
|
// concurrent create's wait would be stuck on a drop that the blocked
|
|
1649
1896
|
// event loop can never resolve, burning its full deadline before failing.
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1897
|
+
let removed;
|
|
1898
|
+
try {
|
|
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;
|
|
1924
|
+
}
|
|
1925
|
+
catch (error) {
|
|
1926
|
+
derivedIndexRuntime?.completeDrop?.();
|
|
1927
|
+
throw error;
|
|
1928
|
+
}
|
|
1929
|
+
if (!removed) {
|
|
1930
|
+
abortStaleDrop();
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1671
1933
|
}
|
|
1672
1934
|
else {
|
|
1673
1935
|
// LMDB: no shared column-family double-drop, and its engine lock is
|
|
1674
1936
|
// transactional rather than this spin lock, so keep the awaited drop
|
|
1675
1937
|
// plus the same tombstone-guarded catalog removal.
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
const
|
|
1679
|
-
if (
|
|
1680
|
-
|
|
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`);
|
|
1681
1966
|
}
|
|
1682
|
-
drops.push(primaryStore.drop().catch(ignoreAlreadyDropped));
|
|
1683
|
-
await Promise.all(drops);
|
|
1684
|
-
if (removeTombstonedCatalog())
|
|
1685
|
-
await dbisDb.committed;
|
|
1686
1967
|
}
|
|
1687
1968
|
}
|
|
1688
1969
|
else {
|
|
@@ -1690,11 +1971,18 @@ function makeTable(options) {
|
|
|
1690
1971
|
// assigns `primaryStore.auditStore` — openAuditStore() assigns `rootStore.auditStore`, and
|
|
1691
1972
|
// this is the reference makeTable() was handed. Awaited so a pass suspended mid-removal has
|
|
1692
1973
|
// released the primary DBI before it is closed and unlinked.
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
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
|
+
}
|
|
1697
1984
|
}
|
|
1985
|
+
derivedIndexRuntime?.completeDrop?.();
|
|
1698
1986
|
signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, hdbTerms_ts_1.OPERATIONS_ENUM.DROP_TABLE, databaseName, tableName));
|
|
1699
1987
|
}
|
|
1700
1988
|
// #section: read-path
|
|
@@ -1716,7 +2004,7 @@ function makeTable(options) {
|
|
|
1716
2004
|
records: './', // an href to the records themselves
|
|
1717
2005
|
name: tableName,
|
|
1718
2006
|
database: databaseName,
|
|
1719
|
-
auditSize: auditStore
|
|
2007
|
+
auditSize: auditStore?.getStats().entryCount,
|
|
1720
2008
|
attributes,
|
|
1721
2009
|
recordCount: undefined,
|
|
1722
2010
|
estimatedRecordRange: undefined,
|
|
@@ -1947,12 +2235,18 @@ function makeTable(options) {
|
|
|
1947
2235
|
else {
|
|
1948
2236
|
id = requestTargetToId(target);
|
|
1949
2237
|
}
|
|
2238
|
+
if (this.#writeGeneration?.closed) {
|
|
2239
|
+
this.#changes = undefined;
|
|
2240
|
+
this.#writeGeneration = undefined;
|
|
2241
|
+
}
|
|
2242
|
+
this.#assertLiveHandle(id, true);
|
|
1950
2243
|
const context = this.getContext();
|
|
1951
2244
|
const envTxn = txnForContext(context);
|
|
1952
2245
|
if (!envTxn)
|
|
1953
2246
|
throw new Error('Can not update a table resource outside of a transaction');
|
|
1954
2247
|
// record in the list of updating records so it can be written to the database when we commit
|
|
1955
|
-
|
|
2248
|
+
// `false` is the patch-cancel sentinel, not a record root — but only incrementally.
|
|
2249
|
+
if (updates === false && !fullUpdate) {
|
|
1956
2250
|
// TODO: Remove from transaction
|
|
1957
2251
|
return this;
|
|
1958
2252
|
}
|
|
@@ -2000,14 +2294,22 @@ function makeTable(options) {
|
|
|
2000
2294
|
});
|
|
2001
2295
|
}
|
|
2002
2296
|
}
|
|
2003
|
-
|
|
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);
|
|
2004
2302
|
}
|
|
2005
2303
|
/**
|
|
2006
2304
|
* Save any changes into this instance to the current transaction
|
|
2007
2305
|
*/
|
|
2008
2306
|
save() {
|
|
2009
|
-
this.#assertLiveHandle(this.getId()); // a write through a released or expired lock never lands
|
|
2010
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
|
|
2011
2313
|
if ((!operation || operation.dropped) && this.#lockWritable && this.#lockHandle?.hold) {
|
|
2012
2314
|
// A held lock's record stages its update here rather than at lock() time: it is often
|
|
2013
2315
|
// written after the acquiring transaction has already completed, which would have
|
|
@@ -2019,7 +2321,7 @@ function makeTable(options) {
|
|
|
2019
2321
|
// released between lock acquisition and this save(), throw 409 rather than silently
|
|
2020
2322
|
// committing stale data. Every lock-writable instance carries its own handle.
|
|
2021
2323
|
const saveHandle = this.#lockHandle;
|
|
2022
|
-
if (saveHandle.
|
|
2324
|
+
if (saveHandle.isExpired()) {
|
|
2023
2325
|
throw (0, recordLock_ts_1.lockNotHeldError)(saveHandle);
|
|
2024
2326
|
}
|
|
2025
2327
|
const changes = this.#changes;
|
|
@@ -2067,12 +2369,23 @@ function makeTable(options) {
|
|
|
2067
2369
|
// resolve before that native commit actually settles. Chain on innerCommit (as the
|
|
2068
2370
|
// lock-writable hold branch above already does) so callers awaiting save() see the
|
|
2069
2371
|
// write durably land, not just the outer (possibly premature) resolution.
|
|
2070
|
-
|
|
2372
|
+
let result;
|
|
2373
|
+
try {
|
|
2374
|
+
result = this.#saveOperation(operation);
|
|
2375
|
+
}
|
|
2376
|
+
catch (error) {
|
|
2377
|
+
if (!operation.saved)
|
|
2378
|
+
this.#savingOperation = operation;
|
|
2379
|
+
throw error;
|
|
2380
|
+
}
|
|
2071
2381
|
const innerCommit = operation.innerCommit;
|
|
2072
2382
|
return innerCommit ? (0, when_ts_1.when)(innerCommit, () => result) : result;
|
|
2073
2383
|
}
|
|
2074
2384
|
}
|
|
2075
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?.();
|
|
2076
2389
|
const transaction = txnForContext(this.getContext());
|
|
2077
2390
|
const holder = operation.stagedIn;
|
|
2078
2391
|
// never-drop-on-conflict lives on the transaction and would not travel with the write, so an
|
|
@@ -2090,13 +2403,26 @@ function makeTable(options) {
|
|
|
2090
2403
|
// merge and index diff would be relative to a record that may never land.
|
|
2091
2404
|
operation.priorWrite = undefined;
|
|
2092
2405
|
operation.deferSave = false;
|
|
2093
|
-
|
|
2406
|
+
const result = (0, when_ts_1.when)(transaction.addWrite(operation), () => operation.promise ?? operation.result);
|
|
2407
|
+
this.#closeWriteChain(operation);
|
|
2408
|
+
return result;
|
|
2094
2409
|
}
|
|
2095
2410
|
const owner = holder ?? transaction;
|
|
2096
|
-
if (owner.save)
|
|
2097
|
-
|
|
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
|
+
}
|
|
2098
2423
|
}
|
|
2099
2424
|
addTo(property, value) {
|
|
2425
|
+
this[tracked_ts_1.ASSERT_TRACKED_WRITABLE]();
|
|
2100
2426
|
if (typeof value === 'number' || typeof value === 'bigint') {
|
|
2101
2427
|
if (this.#savingOperation?.fullUpdate)
|
|
2102
2428
|
this.set(property, (+this.getProperty(property) || 0) + value);
|
|
@@ -2152,6 +2478,7 @@ function makeTable(options) {
|
|
|
2152
2478
|
const context = this.getContext();
|
|
2153
2479
|
checkValidId(id);
|
|
2154
2480
|
const transaction = txnForContext(this.getContext());
|
|
2481
|
+
assertDerivedIndexAdmission(options, transaction);
|
|
2155
2482
|
const write = {
|
|
2156
2483
|
key: id,
|
|
2157
2484
|
store: primaryStore,
|
|
@@ -2159,6 +2486,7 @@ function makeTable(options) {
|
|
|
2159
2486
|
entry: this.#entry,
|
|
2160
2487
|
recordVersion: options?.version,
|
|
2161
2488
|
lockHandle: this.#lockHandle && this.#lockHandle.keyId === (0, DatabaseTransaction_ts_1.writeKeyId)(id) ? this.#lockHandle : undefined,
|
|
2489
|
+
reloadCommitBase: true,
|
|
2162
2490
|
commit: (txnTime, existingEntry, _retry, transaction) => {
|
|
2163
2491
|
const txnLogKey = isRocksDB && options?.version != null ? (transaction?.getTimestamp?.() ?? txnTime) : txnTime;
|
|
2164
2492
|
write.skipped = false; // reset on each retry; cleanup happens after commit if still true
|
|
@@ -2201,6 +2529,7 @@ function makeTable(options) {
|
|
|
2201
2529
|
const context = this.getContext();
|
|
2202
2530
|
checkValidId(id);
|
|
2203
2531
|
const transaction = txnForContext(this.getContext());
|
|
2532
|
+
assertDerivedIndexAdmission(options, transaction);
|
|
2204
2533
|
const write = {
|
|
2205
2534
|
key: id,
|
|
2206
2535
|
store: primaryStore,
|
|
@@ -2208,6 +2537,7 @@ function makeTable(options) {
|
|
|
2208
2537
|
entry: this.#entry,
|
|
2209
2538
|
recordVersion: options?.version,
|
|
2210
2539
|
lockHandle: this.#lockHandle && this.#lockHandle.keyId === (0, DatabaseTransaction_ts_1.writeKeyId)(id) ? this.#lockHandle : undefined,
|
|
2540
|
+
reloadCommitBase: true,
|
|
2211
2541
|
before: this.constructor.source?.relocate && !context?.source
|
|
2212
2542
|
? this.constructor.source.relocate.bind(this.constructor.source, id, undefined, context)
|
|
2213
2543
|
: undefined,
|
|
@@ -2301,9 +2631,8 @@ function makeTable(options) {
|
|
|
2301
2631
|
if (primaryStore.hasLock(id, entry.version))
|
|
2302
2632
|
return;
|
|
2303
2633
|
}
|
|
2304
|
-
//
|
|
2305
|
-
//
|
|
2306
|
-
// 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.
|
|
2307
2636
|
let lmdbCompletion;
|
|
2308
2637
|
if (primaryStore.ifVersion) {
|
|
2309
2638
|
// lmdb: the index cleanup and the record removal are both version-guarded optimistic writes.
|
|
@@ -2317,6 +2646,7 @@ function makeTable(options) {
|
|
|
2317
2646
|
}
|
|
2318
2647
|
else {
|
|
2319
2648
|
updateIndices(id, existingRecord, null, options);
|
|
2649
|
+
stageDerivedIndexEviction(transaction, id, existingVersion);
|
|
2320
2650
|
(0, RecordEncoder_ts_1.removeEntry)(primaryStore, entry ?? primaryStore.getEntry(id), options);
|
|
2321
2651
|
}
|
|
2322
2652
|
committed = true;
|
|
@@ -2412,13 +2742,24 @@ function makeTable(options) {
|
|
|
2412
2742
|
}
|
|
2413
2743
|
const id = target != null ? requestTargetToId(target) : this.getId();
|
|
2414
2744
|
checkValidId(id);
|
|
2745
|
+
this.#assertLiveHandle(id);
|
|
2415
2746
|
const resolved = (0, recordLock_ts_1.resolveLockOptions)(options);
|
|
2416
2747
|
const context = this.getContext();
|
|
2417
2748
|
const link = txnForContext(context);
|
|
2418
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`));
|
|
2419
2757
|
const held = this.#lockHandle;
|
|
2420
|
-
if (held && !held.
|
|
2758
|
+
if (held && !held.isExpired() && held.keyId === keyId) {
|
|
2421
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);
|
|
2422
2763
|
if (resolved.hold && !held.hold) {
|
|
2423
2764
|
held.upgradeToHold(resolved.lease);
|
|
2424
2765
|
// The scoped phase eagerly staged a TransactionWrite (see #reloadLocked); hold
|
|
@@ -2432,7 +2773,10 @@ function makeTable(options) {
|
|
|
2432
2773
|
return Promise.resolve(this.#reloadLocked(id, undefined, true));
|
|
2433
2774
|
}
|
|
2434
2775
|
const scoped = link.recordLockFor(primaryStore, keyId);
|
|
2435
|
-
if (scoped && !scoped.
|
|
2776
|
+
if (scoped && !scoped.isExpired()) {
|
|
2777
|
+
const violation = scopeViolation(scoped, resolved, databaseName);
|
|
2778
|
+
if (violation)
|
|
2779
|
+
return Promise.reject(violation);
|
|
2436
2780
|
if (resolved.hold && !scoped.hold) {
|
|
2437
2781
|
// Upgrade scoped → hold: flip the existing handle object to hold mode so every
|
|
2438
2782
|
// instance that already references this handle stays valid. Retiring and creating a
|
|
@@ -2445,6 +2789,18 @@ function makeTable(options) {
|
|
|
2445
2789
|
// Already held with the same type: re-entrant return. Preserve any staged changes.
|
|
2446
2790
|
return Promise.resolve(this.#reloadLocked(id, scoped, true));
|
|
2447
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
|
+
}
|
|
2448
2804
|
const key = (0, recordLock_ts_1.lockAttemptKey)(tableId, id);
|
|
2449
2805
|
// Coalesce concurrent lock() calls for the same key inside one link so they don't
|
|
2450
2806
|
// self-block: Promise.all([T.lock(id), T.lock(id)]) would otherwise have both calls
|
|
@@ -2457,6 +2813,10 @@ function makeTable(options) {
|
|
|
2457
2813
|
// The follower waits on the leader's acquisition, but only for its own timeout.
|
|
2458
2814
|
let followerTimer;
|
|
2459
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;
|
|
2460
2820
|
const followerStart = Date.now();
|
|
2461
2821
|
const followerDeadline = new Promise((_, reject) => {
|
|
2462
2822
|
followerTimer = setTimeout(() => reject(followerTimedOut), resolved.timeout).unref();
|
|
@@ -2471,13 +2831,24 @@ function makeTable(options) {
|
|
|
2471
2831
|
throw new hdbError_ts_1.ServerError('Transaction was closed while waiting for a record lock', 500);
|
|
2472
2832
|
const remaining = resolved.timeout - (Date.now() - followerStart);
|
|
2473
2833
|
if (remaining <= 0)
|
|
2474
|
-
throw new hdbError_ts_1.ClientError(`Record is locked and was not released in time`, 423);
|
|
2475
|
-
|
|
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
|
+
});
|
|
2476
2844
|
};
|
|
2477
2845
|
return Promise.race([pending, followerDeadline]).then(() => {
|
|
2478
2846
|
clearTimeout(followerTimer);
|
|
2479
2847
|
const acquired = link.recordLockFor(primaryStore, keyId);
|
|
2480
|
-
if (acquired && !acquired.
|
|
2848
|
+
if (acquired && !acquired.isExpired()) {
|
|
2849
|
+
const violation = scopeViolation(acquired, resolved, databaseName);
|
|
2850
|
+
if (violation)
|
|
2851
|
+
throw violation;
|
|
2481
2852
|
if (resolved.hold && !acquired.hold) {
|
|
2482
2853
|
detachScopedUpgradeWrite(link, keyId, acquired);
|
|
2483
2854
|
acquired.upgradeToHold(resolved.lease);
|
|
@@ -2487,20 +2858,89 @@ function makeTable(options) {
|
|
|
2487
2858
|
return retryOnRemainingBudget();
|
|
2488
2859
|
}, (error) => {
|
|
2489
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.
|
|
2490
2865
|
if (error === followerTimedOut)
|
|
2491
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;
|
|
2492
2869
|
return retryOnRemainingBudget();
|
|
2493
2870
|
});
|
|
2494
2871
|
}
|
|
2495
2872
|
const pendingPromise = (0, recordLock_ts_1.acquireRecordKey)(link, primaryStore, key, keyId, resolved.timeout, resolved.lease, resolved.hold);
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
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()) {
|
|
2500
2881
|
// The transaction was aborted while this call waited; nothing would ever release the handle.
|
|
2501
2882
|
handle.release();
|
|
2502
2883
|
throw new hdbError_ts_1.ServerError('Transaction was closed while waiting for a record lock', 500);
|
|
2503
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
|
+
}
|
|
2504
2944
|
link.registerRecordLock(handle);
|
|
2505
2945
|
if (link.saveCommits && context?.timestamp)
|
|
2506
2946
|
handle.noteCandidateFloor(context.timestamp);
|
|
@@ -2530,10 +2970,15 @@ function makeTable(options) {
|
|
|
2530
2970
|
}
|
|
2531
2971
|
// ImmediateTransaction: no clock pinning in lock(); save() stamps each write
|
|
2532
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);
|
|
2533
2978
|
return this.#reloadLocked(id, handle);
|
|
2534
|
-
}, (
|
|
2979
|
+
}, (error) => {
|
|
2535
2980
|
link.unregisterPendingLock(primaryStore, keyId);
|
|
2536
|
-
throw
|
|
2981
|
+
throw error;
|
|
2537
2982
|
});
|
|
2538
2983
|
}
|
|
2539
2984
|
#reloadLocked(id, holdHandle, preserveChanges = false) {
|
|
@@ -2733,6 +3178,7 @@ function makeTable(options) {
|
|
|
2733
3178
|
const context = this.getContext();
|
|
2734
3179
|
const transaction = txnForContext(context);
|
|
2735
3180
|
const replaying = transaction.isReplay === true;
|
|
3181
|
+
assertDerivedIndexAdmission(options, transaction);
|
|
2736
3182
|
checkValidId(id);
|
|
2737
3183
|
if (fullUpdate && recordUpdate == null && options?.isNotification) {
|
|
2738
3184
|
// A source/replication-applied put must carry the record; these applies skip record
|
|
@@ -2747,6 +3193,16 @@ function makeTable(options) {
|
|
|
2747
3193
|
}
|
|
2748
3194
|
return;
|
|
2749
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
|
+
}
|
|
2750
3206
|
const entry = this.#entry ?? primaryStore.getEntry(id, { transaction: transaction.getReadTxn() });
|
|
2751
3207
|
const writeToSource = () => {
|
|
2752
3208
|
if (!this.constructor.source || context?.source)
|
|
@@ -2768,12 +3224,19 @@ function makeTable(options) {
|
|
|
2768
3224
|
}
|
|
2769
3225
|
}
|
|
2770
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));
|
|
2771
3231
|
const write = {
|
|
2772
3232
|
key: id,
|
|
2773
3233
|
store: primaryStore,
|
|
2774
3234
|
entry,
|
|
2775
3235
|
nodeName: context?.nodeName,
|
|
2776
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,
|
|
2777
3240
|
deferSave: true,
|
|
2778
3241
|
// the origin's record version on an applied write; absent for a locally-originated one
|
|
2779
3242
|
recordVersion: options?.version,
|
|
@@ -2782,9 +3245,10 @@ function makeTable(options) {
|
|
|
2782
3245
|
// Only attach the hold handle when it covers exactly this key; off-key writes
|
|
2783
3246
|
// are ordinary and must not carry an unrelated hold's handle.
|
|
2784
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,
|
|
2785
3250
|
validate: (txnTime, committedBy = transaction) => {
|
|
2786
|
-
|
|
2787
|
-
recordUpdate = this.#changes;
|
|
3251
|
+
write.captureChanges?.();
|
|
2788
3252
|
if (fullUpdate || (recordUpdate && (0, tracked_ts_1.hasChanges)(this.#changes === recordUpdate ? this : recordUpdate))) {
|
|
2789
3253
|
if (!context?.source) {
|
|
2790
3254
|
committedBy.checkOverloaded();
|
|
@@ -2834,10 +3298,13 @@ function makeTable(options) {
|
|
|
2834
3298
|
: txnTime;
|
|
2835
3299
|
}
|
|
2836
3300
|
if (createdTimeProperty) {
|
|
2837
|
-
|
|
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) {
|
|
2838
3305
|
if (fullUpdate || recordUpdate[createdTimeProperty.name]) {
|
|
2839
3306
|
// make sure to retain original created time
|
|
2840
|
-
recordUpdate[createdTimeProperty.name] =
|
|
3307
|
+
recordUpdate[createdTimeProperty.name] = base.value[createdTimeProperty.name];
|
|
2841
3308
|
}
|
|
2842
3309
|
}
|
|
2843
3310
|
else {
|
|
@@ -2954,6 +3421,8 @@ function makeTable(options) {
|
|
|
2954
3421
|
// existing timestamp, which means that we received updates out of order, and must resequence the application
|
|
2955
3422
|
// of the updates to the record to ensure consistency across the cluster
|
|
2956
3423
|
// TODO: can the previous version be older, but even more previous version be newer?
|
|
3424
|
+
let belowAuditFloor = false;
|
|
3425
|
+
let dedupVersionCouldBeRetained;
|
|
2957
3426
|
if (audit) {
|
|
2958
3427
|
// A re-delivered out-of-order write (full-copy audit-replay re-delivers writes) must not have
|
|
2959
3428
|
// its commutative ops re-folded. additionalAuditRefs is the record's own list of folded
|
|
@@ -2990,7 +3459,7 @@ function makeTable(options) {
|
|
|
2990
3459
|
// Resolve the oldest retained entry once, for the same log the dedup reads.
|
|
2991
3460
|
let oldestRetainedAuditTime;
|
|
2992
3461
|
let oldestRetainedAuditTimeResolved = false;
|
|
2993
|
-
|
|
3462
|
+
dedupVersionCouldBeRetained = (version) => {
|
|
2994
3463
|
if (!isRocksDB)
|
|
2995
3464
|
return true; // LMDB keeps its exact, unbounded lookup (keyed by local audit time)
|
|
2996
3465
|
if (!oldestRetainedAuditTimeResolved) {
|
|
@@ -3032,6 +3501,19 @@ function makeTable(options) {
|
|
|
3032
3501
|
return; // duplicate already applied; avoid the resequencing walk
|
|
3033
3502
|
}
|
|
3034
3503
|
}
|
|
3504
|
+
// The walk terminates at this write only by reaching an audit entry whose log key is at or below
|
|
3505
|
+
// txnTime (the loop condition below), so below the floor it cannot: it runs the whole retained
|
|
3506
|
+
// chain to an outcome the floor already determines (harper#2642). txnTime is the coordinate
|
|
3507
|
+
// because it is the one that loop compares; txnLogKey addresses this write's own entry, a
|
|
3508
|
+
// different question that dedupVersionCouldBeRetained already answers. An unknown floor is
|
|
3509
|
+
// Infinity, which fails closed for a cursor check but has to fail OPEN here — walk rather than
|
|
3510
|
+
// discard. RocksDB only: LMDB keeps its exact, unbounded reconciliation.
|
|
3511
|
+
if (isRocksDB && precedesExisting < 0) {
|
|
3512
|
+
const auditFloor = (0, auditStore_ts_1.getAuditFloor)(auditStore);
|
|
3513
|
+
belowAuditFloor = Number.isFinite(auditFloor) && txnTime < auditFloor;
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
if (audit && !belowAuditFloor) {
|
|
3035
3517
|
// incremental CRDT updates are only available with audit logging on
|
|
3036
3518
|
const initialAuditHead = isRocksDB
|
|
3037
3519
|
? resolveAuditHead(id, existingEntry.version, existingEntry.nodeId, existingEntry.additionalAuditRefs)
|
|
@@ -3039,7 +3521,12 @@ function makeTable(options) {
|
|
|
3039
3521
|
let localTime = initialAuditHead.txnLogKey;
|
|
3040
3522
|
let auditedVersion = existingEntry.version;
|
|
3041
3523
|
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));
|
|
3042
|
-
|
|
3524
|
+
// Normalized here and not at the lookup, because the two sources of an undefined nodeId do not
|
|
3525
|
+
// mean the same thing: a record's own nodeId is resolved by the same expression as the id its
|
|
3526
|
+
// audit entry is logged under, which applies `?? 0` (RecordEncoder), so absent means log 0;
|
|
3527
|
+
// `previousNodeId` is never encoded, so absent there means the log is unknown and only the
|
|
3528
|
+
// aggregate lookup can resolve a cross-origin predecessor.
|
|
3529
|
+
let nodeId = initialAuditHead.nodeId ?? 0;
|
|
3043
3530
|
const succeedingUpdates = []; // record the "future" updates, as we need to apply the updates in reverse order
|
|
3044
3531
|
const auditRefsToVisit = existingEntry.additionalAuditRefs
|
|
3045
3532
|
? existingEntry.additionalAuditRefs.map((ref) => ({ localTime: ref.version, nodeId: ref.nodeId }))
|
|
@@ -3276,6 +3763,32 @@ function makeTable(options) {
|
|
|
3276
3763
|
return;
|
|
3277
3764
|
}
|
|
3278
3765
|
}
|
|
3766
|
+
else if (belowAuditFloor) {
|
|
3767
|
+
// The walk cannot reach this write, so it may contribute only what is order-independent: its
|
|
3768
|
+
// commutative ops. Whether a plain field survives depends on what newer writes did to that key,
|
|
3769
|
+
// which is what the purged history no longer answers, so applying one would resurrect state a
|
|
3770
|
+
// newer put may have erased. Everything else loses to the strictly newer head: a full update, a
|
|
3771
|
+
// head carrying no record (a delete or a residency-omitted record, which an op must not
|
|
3772
|
+
// resurrect), and an op-less patch. Bare return, no writeCommit, so no audit record references
|
|
3773
|
+
// the losing update's pre-saved blobs.
|
|
3774
|
+
incrementalUpdateToApply = fullUpdate || existingRecord == null ? null : (0, crdt_ts_1.commutativeOpsOf)(recordUpdate);
|
|
3775
|
+
if (!incrementalUpdateToApply) {
|
|
3776
|
+
write.skipped = true;
|
|
3777
|
+
return;
|
|
3778
|
+
}
|
|
3779
|
+
// The surviving head's addressable log-key pointer lives in these, wherever the record and log
|
|
3780
|
+
// clocks differ.
|
|
3781
|
+
if (existingEntry.additionalAuditRefs) {
|
|
3782
|
+
for (const ref of existingEntry.additionalAuditRefs) {
|
|
3783
|
+
additionalAuditRefs.push(ref);
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
// Once the walk no longer runs, this ref is what the read-your-writes check above matches a
|
|
3787
|
+
// re-delivery of these ops on. Best-effort, like every other guard here: the encoder bounds
|
|
3788
|
+
// the persisted list, so an identity can age out of it (harper#1148's full-copy convergence
|
|
3789
|
+
// is the backstop).
|
|
3790
|
+
additionalAuditRefs.push({ version: txnLogKey, nodeId: options?.nodeId });
|
|
3791
|
+
}
|
|
3279
3792
|
else if (fullUpdate) {
|
|
3280
3793
|
// if no audit, we can't accurately do incremental updates, so we just assume the last update
|
|
3281
3794
|
// was the same type. Assuming a full update this record update loses and there are no changes —
|
|
@@ -3305,8 +3818,8 @@ function makeTable(options) {
|
|
|
3305
3818
|
if (recordToStore && recordToStore.getRecord)
|
|
3306
3819
|
throw new Error('Can not assign a record to a record, check for circular references');
|
|
3307
3820
|
if (residencyId == undefined) {
|
|
3308
|
-
if (
|
|
3309
|
-
context.previousResidency = _a.getResidencyRecord(
|
|
3821
|
+
if (existingEntry?.residencyId)
|
|
3822
|
+
context.previousResidency = _a.getResidencyRecord(existingEntry.residencyId);
|
|
3310
3823
|
const residency = residencyFromFunction(_a.getResidency(recordToStore, context));
|
|
3311
3824
|
if (residency) {
|
|
3312
3825
|
if (!residency.includes(server.hostname)) {
|
|
@@ -3518,6 +4031,7 @@ function makeTable(options) {
|
|
|
3518
4031
|
this.#assertLiveHandle(id);
|
|
3519
4032
|
const context = this.getContext();
|
|
3520
4033
|
const transaction = txnForContext(context);
|
|
4034
|
+
assertDerivedIndexAdmission(options, transaction);
|
|
3521
4035
|
checkValidId(id);
|
|
3522
4036
|
const entry = this.#entry ?? primaryStore.getEntry(id, { transaction: transaction.getReadTxn() });
|
|
3523
4037
|
const write = {
|
|
@@ -3525,6 +4039,7 @@ function makeTable(options) {
|
|
|
3525
4039
|
store: primaryStore,
|
|
3526
4040
|
entry,
|
|
3527
4041
|
chainsStagedState: true,
|
|
4042
|
+
reloadCommitBase: true,
|
|
3528
4043
|
nodeName: context?.nodeName,
|
|
3529
4044
|
recordVersion: options?.version,
|
|
3530
4045
|
lockHandle: this.#lockHandle && this.#lockHandle.keyId === (0, DatabaseTransaction_ts_1.writeKeyId)(id) ? this.#lockHandle : undefined,
|
|
@@ -3736,6 +4251,7 @@ function makeTable(options) {
|
|
|
3736
4251
|
// objects. Entries are small and shallow; the clone is cheap next to the query.
|
|
3737
4252
|
conditions = cloneConditions(conditions);
|
|
3738
4253
|
let orderAlignedCondition;
|
|
4254
|
+
let syntheticOrderCondition;
|
|
3739
4255
|
const filtered = {};
|
|
3740
4256
|
function prepareConditions(conditions, operator) {
|
|
3741
4257
|
// some validation:
|
|
@@ -3875,14 +4391,19 @@ function makeTable(options) {
|
|
|
3875
4391
|
// if it is indexed, we add a pseudo-condition to align with the natural sort order of the index.
|
|
3876
4392
|
// the primary key has no secondary index, but the primary store is itself keyed in
|
|
3877
4393
|
// primary-key order, so scanning it is already aligned with the sort
|
|
3878
|
-
orderAlignedCondition = { ...sort, comparator: 'sort' };
|
|
4394
|
+
orderAlignedCondition = syntheticOrderCondition = { ...sort, comparator: 'sort' };
|
|
3879
4395
|
conditions.push(orderAlignedCondition);
|
|
3880
4396
|
}
|
|
3881
4397
|
else if (conditions.length === 0 && !target.allowFullScan)
|
|
3882
4398
|
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);
|
|
3883
4399
|
}
|
|
3884
|
-
if (orderAlignedCondition)
|
|
4400
|
+
if (orderAlignedCondition) {
|
|
3885
4401
|
orderAlignedCondition.descending = Boolean(sort.descending);
|
|
4402
|
+
if (orderAlignedCondition.maxIndexLagMilliseconds === undefined)
|
|
4403
|
+
orderAlignedCondition.maxIndexLagMilliseconds = sort.maxIndexLagMilliseconds;
|
|
4404
|
+
if (orderAlignedCondition.waitForIndexMilliseconds === undefined)
|
|
4405
|
+
orderAlignedCondition.waitForIndexMilliseconds = sort.waitForIndexMilliseconds;
|
|
4406
|
+
}
|
|
3886
4407
|
}
|
|
3887
4408
|
}
|
|
3888
4409
|
conditions = orderConditions(conditions, operator);
|
|
@@ -3902,9 +4423,11 @@ function makeTable(options) {
|
|
|
3902
4423
|
}
|
|
3903
4424
|
}
|
|
3904
4425
|
else {
|
|
3905
|
-
// if we had to add an aligned condition that isn't first, we remove it and do ordering later
|
|
3906
|
-
|
|
3907
|
-
|
|
4426
|
+
// if we had to add an aligned condition that isn't first, we remove it and do ordering later —
|
|
4427
|
+
// only the one we added; a caller's own condition on the sort attribute is still a filter
|
|
4428
|
+
const syntheticIndex = syntheticOrderCondition ? conditions.indexOf(syntheticOrderCondition) : -1;
|
|
4429
|
+
if (syntheticIndex >= 0)
|
|
4430
|
+
conditions.splice(syntheticIndex, 1);
|
|
3908
4431
|
postOrdering = sort;
|
|
3909
4432
|
}
|
|
3910
4433
|
}
|
|
@@ -3949,150 +4472,156 @@ function makeTable(options) {
|
|
|
3949
4472
|
const recordAccess = boundRowFilter || typeof target.vectorFilter === 'function'
|
|
3950
4473
|
? { rowFilter: boundRowFilter, vectorFilter: target.vectorFilter }
|
|
3951
4474
|
: undefined;
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4475
|
+
try {
|
|
4476
|
+
const entries = (0, search_ts_1.executeConditions)(conditions, operator, _a, readTxn, target, context, (results, filters) => transformToEntries(results, select, context, readTxn, filters), filtered, recordAccess);
|
|
4477
|
+
const ensure_loaded = target.ensureLoaded !== false;
|
|
4478
|
+
// The guards inside executeConditions evaluate the
|
|
4479
|
+
// LOCAL record, but on a caching table transformEntryForSelect may then revalidate an
|
|
4480
|
+
// expired/invalidated row from source and return a DIFFERENT record. The explicit row filter
|
|
4481
|
+
// must hold on the record actually returned, so it is re-checked
|
|
4482
|
+
// there, after materialization (the earlier evaluation stays as a prune that also bounds HNSW
|
|
4483
|
+
// traversal). vectorFilter and condition filters intentionally keep the local-record
|
|
4484
|
+
// semantics all query filters have on caching tables.
|
|
4485
|
+
//
|
|
4486
|
+
// A row that is past its TTL but not yet swept by the background eviction
|
|
4487
|
+
// scan is still physically present. A write that is about to overwrite it
|
|
4488
|
+
// anyway (e.g. the SQL engine locating UPDATE/DELETE targets) needs to see
|
|
4489
|
+
// it as a match — the same leniency a direct by-id put/patch already gets,
|
|
4490
|
+
// since those never run the ensureLoaded-gated freshness check this transform
|
|
4491
|
+
// otherwise applies unconditionally to every read.
|
|
4492
|
+
const includeExpired = target.includeExpired === true;
|
|
4493
|
+
const transformToRecord = _a.transformEntryForSelect(select, context, readTxn, filtered, ensure_loaded, true, boundRowFilter, includeExpired, postOrdering);
|
|
4494
|
+
let results = _a.transformToOrderedSelect(entries, select, postOrdering, context, readTxn, transformToRecord);
|
|
4495
|
+
const offset = target.offset || 0;
|
|
4496
|
+
const end = target.limit !== undefined ? offset + target.limit : undefined;
|
|
4497
|
+
// `Prefer: count=` (REST pagination): materialize the requested page and attach a total record
|
|
4498
|
+
// count so the HTTP layer can emit a Content-Range. `exact` drains the full matched set once,
|
|
4499
|
+
// windowing the page in the same pass; `estimated` returns just the page plus a cheap planner/
|
|
4500
|
+
// table estimate. Opt-in only — the default streaming path below is untouched.
|
|
4501
|
+
//
|
|
4502
|
+
// Requires a bounded page AND window. Counting is a pagination feature; both the limit and the
|
|
4503
|
+
// offset must be finite, non-negative integers, the limit no larger than MAX_COUNT_PAGE, and the
|
|
4504
|
+
// window (offset + limit) no larger than MAX_EXACT_COUNT_SCAN. Anything else — a missing/
|
|
4505
|
+
// oversized/non-finite/negative limit or offset (a bare collection GET, limit(Infinity),
|
|
4506
|
+
// limit(foo), limit(-5,10)) or a deep-page window past the scan budget — falls through to the
|
|
4507
|
+
// normal streaming path with no count. This bounds the offset too: without it a huge offset would
|
|
4508
|
+
// postpone the exact guardrail (which only engages past the page) until that offset was scanned.
|
|
4509
|
+
const pageLimit = target.limit;
|
|
4510
|
+
if (target.count &&
|
|
4511
|
+
Number.isInteger(pageLimit) &&
|
|
4512
|
+
pageLimit >= 0 &&
|
|
4513
|
+
pageLimit <= MAX_COUNT_PAGE &&
|
|
4514
|
+
Number.isInteger(offset) &&
|
|
4515
|
+
offset >= 0 &&
|
|
4516
|
+
offset + pageLimit <= MAX_EXACT_COUNT_SCAN) {
|
|
4517
|
+
const wantExact = target.count === 'exact';
|
|
4518
|
+
const pageEnd = offset + pageLimit;
|
|
4519
|
+
const countStart = node_perf_hooks_1.performance.now();
|
|
4520
|
+
// A custom-index (vector/HNSW) traversal returns a bounded, approximate candidate set whose size is
|
|
4521
|
+
// chosen from `minResults` (offset + limit), so `scanned` over it tracks the requested page size, not
|
|
4522
|
+
// the true match count — the same query at limit(5) vs limit(200) would otherwise advertise two
|
|
4523
|
+
// different `count=exact` totals. Any query whose execution touches a custom index is affected: a
|
|
4524
|
+
// custom-index sort (its aligned pseudo-condition lands in `conditions`), a custom-index threshold
|
|
4525
|
+
// filter (an HNSW `lt`/`le` is the same minResults-widened traversal as a sort), or an opaque vector
|
|
4526
|
+
// filter. Report the total as unavailable for those rather than advertising it as count=exact
|
|
4527
|
+
// (mirroring how the estimated branch below bails to null for an opaque row/vector filter). A vector
|
|
4528
|
+
// sort applied as in-memory post-ordering leaves no custom-index condition here and stays exact.
|
|
4529
|
+
const touchesCustomIndex = (conds) => conds.some((c) => {
|
|
4530
|
+
if (!c)
|
|
4531
|
+
return false;
|
|
4532
|
+
if (c.conditions)
|
|
4533
|
+
return touchesCustomIndex(c.conditions);
|
|
4534
|
+
const attr = Array.isArray(c.attribute) ? c.attribute[0] : (c.attribute ?? c[0]);
|
|
4535
|
+
return typeof attr === 'string' && Boolean(indices[attr]?.customIndex);
|
|
4536
|
+
});
|
|
4537
|
+
const approximateResultSet = typeof target.vectorFilter === 'function' || touchesCustomIndex(conditions);
|
|
4538
|
+
return (async () => {
|
|
4539
|
+
const page = [];
|
|
4540
|
+
let scanned = 0;
|
|
4541
|
+
let exact = true;
|
|
4542
|
+
try {
|
|
4543
|
+
for await (const record of results) {
|
|
4544
|
+
if (scanned >= offset && scanned < pageEnd)
|
|
4545
|
+
page.push(record);
|
|
4546
|
+
scanned++;
|
|
4547
|
+
// A store whose async iterator settles synchronously (the common indexed-scan case) would
|
|
4548
|
+
// otherwise let this drain spin as one uninterrupted microtask run, blocking the event loop
|
|
4549
|
+
// for the whole count. Yield to the macrotask queue periodically so concurrent requests and
|
|
4550
|
+
// I/O still make progress during a large exact scan.
|
|
4551
|
+
if ((scanned & (COUNT_YIELD_INTERVAL - 1)) === 0)
|
|
4552
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
4553
|
+
// The page window [offset, pageEnd) is always collected in full first — the guardrail
|
|
4554
|
+
// only ever abandons the running TOTAL, never truncates the page body.
|
|
4555
|
+
if (scanned >= pageEnd) {
|
|
4556
|
+
// `estimated` needs nothing past the page; an approximate (vector) exact total is going to
|
|
4557
|
+
// be reported unavailable anyway, so don't drain its tail for a number we won't publish.
|
|
4558
|
+
if (!wantExact || approximateResultSet)
|
|
4559
|
+
break;
|
|
4560
|
+
// `exact` keeps counting the tail, bounded by a row cap AND a time budget so a
|
|
4561
|
+
// large match set can't turn a bounded page fetch into an unbounded scan.
|
|
4562
|
+
if (scanned > MAX_EXACT_COUNT_SCAN || node_perf_hooks_1.performance.now() - countStart > MAX_EXACT_COUNT_MS) {
|
|
4563
|
+
exact = false;
|
|
4564
|
+
break;
|
|
4565
|
+
}
|
|
4041
4566
|
}
|
|
4042
4567
|
}
|
|
4043
4568
|
}
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
}
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
results =
|
|
4089
|
-
|
|
4090
|
-
|
|
4569
|
+
finally {
|
|
4570
|
+
// We own the iteration here (no results.onDone consumer), so release the read
|
|
4571
|
+
// transaction unconditionally — including when the drain throws — or the snapshot leaks.
|
|
4572
|
+
txn.doneReadTxn();
|
|
4573
|
+
}
|
|
4574
|
+
let total;
|
|
4575
|
+
if (wantExact) {
|
|
4576
|
+
// `scanned` is only an authoritative total when the iteration was exhaustive and deterministic;
|
|
4577
|
+
// an approximate (vector/HNSW) result set is neither, so report the total as unavailable.
|
|
4578
|
+
total = exact && !approximateResultSet ? scanned : null;
|
|
4579
|
+
}
|
|
4580
|
+
else if (boundRowFilter || typeof target.vectorFilter === 'function') {
|
|
4581
|
+
// An opaque row/vector filter shapes the result but isn't reflected in the index/condition
|
|
4582
|
+
// estimate; guessing would both mislead and disclose cardinality the filter hides.
|
|
4583
|
+
total = null;
|
|
4584
|
+
}
|
|
4585
|
+
else if (!hasUserConditions) {
|
|
4586
|
+
total = (0, search_ts_1.estimatedEntryCount)(primaryStore);
|
|
4587
|
+
}
|
|
4588
|
+
else {
|
|
4589
|
+
// Estimate from the real conditions only — drop the planner's synthetic `sort`
|
|
4590
|
+
// pseudo-condition, which otherwise contributes a bogus (entryCount/2) cardinality.
|
|
4591
|
+
const est = (0, search_ts_1.estimateCondition)(_a)({
|
|
4592
|
+
conditions: conditions.filter((c) => c.comparator !== 'sort'),
|
|
4593
|
+
operator: operator ? String(operator).toLowerCase() : 'and',
|
|
4594
|
+
});
|
|
4595
|
+
total = isFinite(est) ? Math.round(est) : null;
|
|
4596
|
+
}
|
|
4597
|
+
// For an estimate, never report a total below the last row actually returned — keeps the
|
|
4598
|
+
// Content-Range valid (start-end/total) when an estimate undershoots a non-empty page.
|
|
4599
|
+
// Exact totals are authoritative (and an empty page past the end must not be clamped up).
|
|
4600
|
+
if (!wantExact && total != null && page.length > 0 && total < offset + page.length) {
|
|
4601
|
+
total = offset + page.length;
|
|
4602
|
+
}
|
|
4603
|
+
page.recordCount = total;
|
|
4604
|
+
page.recordCountExact = wantExact && exact && !approximateResultSet;
|
|
4605
|
+
page.selectApplied = true;
|
|
4606
|
+
page.getColumns = getColumns;
|
|
4607
|
+
return page;
|
|
4608
|
+
})();
|
|
4609
|
+
}
|
|
4610
|
+
// apply any offset/limit after all the sorting and filtering
|
|
4611
|
+
if (target.offset || target.limit !== undefined)
|
|
4612
|
+
results = results.slice(offset, end);
|
|
4613
|
+
results.onDone = () => {
|
|
4614
|
+
results.onDone = null; // ensure that it isn't called twice
|
|
4615
|
+
txn.doneReadTxn();
|
|
4616
|
+
};
|
|
4617
|
+
results.selectApplied = true;
|
|
4618
|
+
results.getColumns = getColumns;
|
|
4619
|
+
return results;
|
|
4620
|
+
}
|
|
4621
|
+
catch (error) {
|
|
4091
4622
|
txn.doneReadTxn();
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
results.getColumns = getColumns;
|
|
4095
|
-
return results;
|
|
4623
|
+
throw error;
|
|
4624
|
+
}
|
|
4096
4625
|
}
|
|
4097
4626
|
/**
|
|
4098
4627
|
* This is responsible for ordering and select()ing the attributes/properties from returned entries
|
|
@@ -4108,10 +4637,18 @@ function makeTable(options) {
|
|
|
4108
4637
|
if (sort) {
|
|
4109
4638
|
// there might be some situations where we don't need to transform to entries for sorting, not sure
|
|
4110
4639
|
entries = transformToEntries(entries, select, context, readTxn, null);
|
|
4111
|
-
|
|
4640
|
+
// Sort keys are resolved as entries are collected, so comparison never dereferences a record: a
|
|
4641
|
+
// cached entry holds its record only weakly, and a re-read per comparison is what this avoids.
|
|
4642
|
+
const clauses = [];
|
|
4643
|
+
for (let order = sort; order; order = order.next)
|
|
4644
|
+
clauses.push(order);
|
|
4645
|
+
const clauseCount = clauses.length;
|
|
4112
4646
|
// if we are doing post-ordering, we need to get records first, then sort them
|
|
4113
4647
|
results.iterate = function (options) {
|
|
4114
|
-
let
|
|
4648
|
+
let ordered;
|
|
4649
|
+
let orderedKeys;
|
|
4650
|
+
let sortedPositions;
|
|
4651
|
+
let sortedIndex;
|
|
4115
4652
|
const dbIterator = options?.async && entries[Symbol.asyncIterator]
|
|
4116
4653
|
? entries[Symbol.asyncIterator]()
|
|
4117
4654
|
: entries[Symbol.iterator]();
|
|
@@ -4120,26 +4657,34 @@ function makeTable(options) {
|
|
|
4120
4657
|
let enqueuedEntryForNextGroup;
|
|
4121
4658
|
let lastGroupingValue;
|
|
4122
4659
|
let firstEntry = true;
|
|
4123
|
-
function
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4660
|
+
function collect(entry) {
|
|
4661
|
+
ordered.push(entry);
|
|
4662
|
+
for (let i = 0; i < clauseCount; i++) {
|
|
4663
|
+
const clause = clauses[i];
|
|
4664
|
+
orderedKeys[i].push(convertToComparableKeys(getAttributeValue(entry, clause.attribute, context, clause)));
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
function comparePositions(positionA, positionB) {
|
|
4668
|
+
for (let i = 0; i < clauseCount; i++) {
|
|
4669
|
+
const keys = orderedKeys[i];
|
|
4670
|
+
const diff = clauses[i].descending
|
|
4671
|
+
? (0, ordered_binary_1.compareKeys)(keys[positionB], keys[positionA])
|
|
4672
|
+
: (0, ordered_binary_1.compareKeys)(keys[positionA], keys[positionB]);
|
|
4673
|
+
if (diff !== 0)
|
|
4674
|
+
return diff;
|
|
4675
|
+
}
|
|
4676
|
+
return 0;
|
|
4677
|
+
}
|
|
4678
|
+
function nextSorted() {
|
|
4679
|
+
if (sortedIndex < sortedPositions.length)
|
|
4680
|
+
return { done: false, value: ordered[sortedPositions[sortedIndex++]] };
|
|
4681
|
+
return { done: true, value: undefined };
|
|
4136
4682
|
}
|
|
4137
|
-
const comparator = createComparator(sort);
|
|
4138
4683
|
return {
|
|
4139
4684
|
async next() {
|
|
4140
4685
|
let iteration;
|
|
4141
|
-
if (
|
|
4142
|
-
iteration =
|
|
4686
|
+
if (sortedPositions) {
|
|
4687
|
+
iteration = nextSorted();
|
|
4143
4688
|
if (iteration.done) {
|
|
4144
4689
|
if (dbDone) {
|
|
4145
4690
|
if (results.onDone)
|
|
@@ -4153,8 +4698,11 @@ function makeTable(options) {
|
|
|
4153
4698
|
};
|
|
4154
4699
|
}
|
|
4155
4700
|
ordered = [];
|
|
4701
|
+
orderedKeys = [];
|
|
4702
|
+
for (let i = 0; i < clauseCount; i++)
|
|
4703
|
+
orderedKeys.push([]);
|
|
4156
4704
|
if (enqueuedEntryForNextGroup)
|
|
4157
|
-
|
|
4705
|
+
collect(enqueuedEntryForNextGroup);
|
|
4158
4706
|
// need to load all the entries into ordered
|
|
4159
4707
|
do {
|
|
4160
4708
|
iteration = await dbIterator.next();
|
|
@@ -4186,17 +4734,18 @@ function makeTable(options) {
|
|
|
4186
4734
|
break;
|
|
4187
4735
|
}
|
|
4188
4736
|
}
|
|
4189
|
-
|
|
4190
|
-
// before the sorting is completed
|
|
4191
|
-
ordered.push(entry);
|
|
4737
|
+
collect(entry);
|
|
4192
4738
|
}
|
|
4193
4739
|
} while (true);
|
|
4194
4740
|
if (sort.isGrouped) {
|
|
4195
4741
|
// TODO: Return grouped results
|
|
4196
4742
|
}
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4743
|
+
sortedPositions = [];
|
|
4744
|
+
for (let i = 0; i < ordered.length; i++)
|
|
4745
|
+
sortedPositions.push(i);
|
|
4746
|
+
sortedPositions.sort(comparePositions);
|
|
4747
|
+
sortedIndex = 0;
|
|
4748
|
+
iteration = nextSorted();
|
|
4200
4749
|
if (!iteration.done)
|
|
4201
4750
|
return {
|
|
4202
4751
|
value: await transformToRecord.call(this, iteration.value),
|
|
@@ -4601,6 +5150,9 @@ function makeTable(options) {
|
|
|
4601
5150
|
return;
|
|
4602
5151
|
try {
|
|
4603
5152
|
let type = auditRecord.type;
|
|
5153
|
+
// Ahead of the rawEvents branch, which forwards every type verbatim.
|
|
5154
|
+
if ((0, auditStore_ts_1.isLockControlType)(type))
|
|
5155
|
+
return;
|
|
4604
5156
|
let value;
|
|
4605
5157
|
if (type === 'message' || request.rawEvents) {
|
|
4606
5158
|
// we only send the full message, this are individual messages that can be sent out of order
|
|
@@ -4699,7 +5251,9 @@ function makeTable(options) {
|
|
|
4699
5251
|
if (!isActive())
|
|
4700
5252
|
return;
|
|
4701
5253
|
}
|
|
4702
|
-
if (auditRecord.tableId !== tableId)
|
|
5254
|
+
if (auditRecord.tableId !== tableId || auditRecord.type === 'evict')
|
|
5255
|
+
continue;
|
|
5256
|
+
if ((0, auditStore_ts_1.isLockControlType)(auditRecord.type))
|
|
4703
5257
|
continue;
|
|
4704
5258
|
const id = auditRecord.recordId;
|
|
4705
5259
|
if (thisId == null || isDescendantId(thisId, id)) {
|
|
@@ -4739,7 +5293,9 @@ function makeTable(options) {
|
|
|
4739
5293
|
return;
|
|
4740
5294
|
}
|
|
4741
5295
|
try {
|
|
4742
|
-
if (auditRecord.tableId !== tableId)
|
|
5296
|
+
if (auditRecord.tableId !== tableId || auditRecord.type === 'evict')
|
|
5297
|
+
continue;
|
|
5298
|
+
if ((0, auditStore_ts_1.isLockControlType)(auditRecord.type))
|
|
4743
5299
|
continue;
|
|
4744
5300
|
const id = auditRecord.recordId;
|
|
4745
5301
|
if (thisId == null || isDescendantId(thisId, id)) {
|
|
@@ -5171,6 +5727,108 @@ function makeTable(options) {
|
|
|
5171
5727
|
});
|
|
5172
5728
|
});
|
|
5173
5729
|
}
|
|
5730
|
+
/**
|
|
5731
|
+
* Write one cluster record-lock control entry (harper#483 Phase 1). Not local-only: replicating
|
|
5732
|
+
* it IS the send.
|
|
5733
|
+
*
|
|
5734
|
+
* `recordId` must stay null. An entry carrying the locked key would share
|
|
5735
|
+
* `(version, tableId, recordId, nodeId)` with the holder's own first write, which is stamped at
|
|
5736
|
+
* exactly `ts_R`, and `RocksTransactionLogStore.getSync` answers with the FIRST entry at a
|
|
5737
|
+
* timestamp and key — so `_writeUpdate`'s keyed dedup would find this one and drop that write.
|
|
5738
|
+
* The payload goes in as bytes rather than through `recordUpdater`, which would run it through
|
|
5739
|
+
* schema projection and the table's shared structure dictionary.
|
|
5740
|
+
*/
|
|
5741
|
+
static writeLockControlEntry(entry) {
|
|
5742
|
+
const encodedRecord = (0, recordLockCoordinator_ts_1.encodeLockControlPayload)(entry);
|
|
5743
|
+
const nodeId = (0, nodeIdMapping_ts_1.getThisNodeId)(auditStore) ?? 0;
|
|
5744
|
+
let position;
|
|
5745
|
+
// No entry pins its clock, the request included. `ts_R` is minted before the write, so pinning
|
|
5746
|
+
// to it can land the entry behind a peer's replication cursor if any write to this table
|
|
5747
|
+
// commits in between — the same hazard that rules it out for grants and releases, which are
|
|
5748
|
+
// written later still. The protocol reads `ts_R` from the payload, so the entry's own log key
|
|
5749
|
+
// never has to equal it.
|
|
5750
|
+
const context = {};
|
|
5751
|
+
return Promise.resolve((0, transaction_ts_1.transaction)(context, (txn) => {
|
|
5752
|
+
const tableTxn = txnForContext({ transaction: txn });
|
|
5753
|
+
tableTxn.addWrite({
|
|
5754
|
+
key: null,
|
|
5755
|
+
store: primaryStore,
|
|
5756
|
+
skipReplicationConfirmation: true,
|
|
5757
|
+
commit: (txnTime, _existingEntry, _retry, nativeTransaction) => {
|
|
5758
|
+
position = txnTime;
|
|
5759
|
+
return auditStore[isRocksDB ? 'putSync' : 'put'](null, {
|
|
5760
|
+
version: txnTime,
|
|
5761
|
+
tableId,
|
|
5762
|
+
recordId: null,
|
|
5763
|
+
nodeId,
|
|
5764
|
+
type: entry.type,
|
|
5765
|
+
encodedRecord,
|
|
5766
|
+
extendedType: 0,
|
|
5767
|
+
// Zero, not the table's count: these bytes were packed by the private control `Packr`
|
|
5768
|
+
// and carry none of the table's structures. `RocksTransactionLogStore` raises the
|
|
5769
|
+
// per-(log, table) structure watermark from this field and flags the entry that does
|
|
5770
|
+
// it, so claiming the table's version would let a release take `HAS_STRUCTURE_UPDATE`
|
|
5771
|
+
// and leave the next real write at that version unflagged — a receiver that learns
|
|
5772
|
+
// structures only from flagged entries then decodes later records against a stale set
|
|
5773
|
+
// (harper#1348's class). A payload with no table structures cannot advance them.
|
|
5774
|
+
structureVersion: 0,
|
|
5775
|
+
}, { instructedWrite: true, transaction: nativeTransaction, nodeId, viaNodeId: nodeId });
|
|
5776
|
+
},
|
|
5777
|
+
});
|
|
5778
|
+
})).then(() => position);
|
|
5779
|
+
}
|
|
5780
|
+
/**
|
|
5781
|
+
* The coordinator that holds this node's admissions, transport or not. Releasing and registering
|
|
5782
|
+
* go here rather than through `lockCoordinator`, which answers undefined while a transport is
|
|
5783
|
+
* momentarily unregistered — and a release dropped on that answer leaves the key's home holding
|
|
5784
|
+
* its grant until the delegation's own deadline.
|
|
5785
|
+
*/
|
|
5786
|
+
static get admittingCoordinator() {
|
|
5787
|
+
return lockCoordinator;
|
|
5788
|
+
}
|
|
5789
|
+
/**
|
|
5790
|
+
* This table's cluster lock coordinator, created on first use and only while a transport is
|
|
5791
|
+
* registered for the database. Nothing is allocated on the Phase 0 path.
|
|
5792
|
+
*/
|
|
5793
|
+
static get lockCoordinator() {
|
|
5794
|
+
const transport = (0, recordLockCoordinator_ts_1.getClusterLockTransport)(databaseName);
|
|
5795
|
+
if (!transport) {
|
|
5796
|
+
// Deliberately NOT closed. harper-pro unregisters without a standalone claim during a
|
|
5797
|
+
// reconnect, and closing here would drop this node's record of the delegations it has
|
|
5798
|
+
// issued as a home — so the next registration would start empty and could grant a key
|
|
5799
|
+
// whose delegate is still admitting. The coordinator keeps ticking, its grants expire on
|
|
5800
|
+
// their own deadlines, and `isClusterLockRequired` is what fails an acquire closed in the
|
|
5801
|
+
// meantime. A genuine standalone claim clears the requirement and the coordinator with it.
|
|
5802
|
+
if (!(0, recordLockCoordinator_ts_1.isClusterLockRequired)(databaseName)) {
|
|
5803
|
+
lockCoordinator?.close();
|
|
5804
|
+
lockCoordinator = undefined;
|
|
5805
|
+
}
|
|
5806
|
+
return undefined;
|
|
5807
|
+
}
|
|
5808
|
+
if (lockCoordinator?.transport !== transport) {
|
|
5809
|
+
// The transport object changed, but this node's delegations and the handles they admitted
|
|
5810
|
+
// did not. The successor adopts that live authority in its constructor; the predecessor
|
|
5811
|
+
// is closed afterwards so nothing is dropped in between. See LockCoordinatorOptions.adopt.
|
|
5812
|
+
const predecessor = lockCoordinator;
|
|
5813
|
+
lockCoordinator = new recordLockCoordinator_ts_1.LockCoordinator({
|
|
5814
|
+
database: databaseName,
|
|
5815
|
+
table: tableName,
|
|
5816
|
+
nodeId: (0, nodeName_ts_1.getThisNodeName)(),
|
|
5817
|
+
transport,
|
|
5818
|
+
adopt: predecessor,
|
|
5819
|
+
// Writing to the local transaction log IS the send, so a transport that only computes
|
|
5820
|
+
// the participant set gets core's writer.
|
|
5821
|
+
writeControl: transport.writeControl
|
|
5822
|
+
? (entry) => transport.writeControl(tableName, entry)
|
|
5823
|
+
: (entry) => _a.writeLockControlEntry(entry),
|
|
5824
|
+
keyIdOf: DatabaseTransaction_ts_1.writeKeyId,
|
|
5825
|
+
nextTimestamp: () => primaryStore.getMonotonicTimestamp(),
|
|
5826
|
+
grantableAfterMono: transport.grantableAfterMono,
|
|
5827
|
+
});
|
|
5828
|
+
predecessor?.close();
|
|
5829
|
+
}
|
|
5830
|
+
return lockCoordinator;
|
|
5831
|
+
}
|
|
5174
5832
|
// #section: validation
|
|
5175
5833
|
validate(record, patch) {
|
|
5176
5834
|
// Accumulate structured per-field issues so the 400 carries `{ path, code,
|
|
@@ -5383,85 +6041,182 @@ function makeTable(options) {
|
|
|
5383
6041
|
const exactCount = options?.exactCount;
|
|
5384
6042
|
const TIME_LIMIT = options?.timeLimit ?? 1000 / 2; // one second time limit, enforced by seeing if we are halfway through at 500ms
|
|
5385
6043
|
const start = node_perf_hooks_1.performance.now();
|
|
5386
|
-
// `entryCount` (the exact key count) is only needed once the scan blows the time budget --
|
|
5387
|
-
// to decide whether to estimate and as the extrapolation base. On RocksDB it is a full
|
|
5388
|
-
// key-only scan, so we defer it: tables that finish within budget (the common case) and
|
|
5389
|
-
// `exact_count` requests never pay for it. `halfway`/`entryCount` stay 0 until first computed.
|
|
5390
6044
|
let entryCount = 0;
|
|
5391
|
-
let
|
|
5392
|
-
let
|
|
5393
|
-
|
|
6045
|
+
let remainderPhysical = 0;
|
|
6046
|
+
let estimator;
|
|
6047
|
+
// feature-detected per DESIGN.md's invariant for this API family; LMDB stores do not implement it
|
|
6048
|
+
const canEstimate = typeof primaryStore.createCountEstimator === 'function' && typeof primaryStore.estimateCount === 'function';
|
|
6049
|
+
let estimatorFailed = false;
|
|
6050
|
+
let warnedNoBase = false;
|
|
6051
|
+
let checkpoints = 0;
|
|
6052
|
+
let checkpointedEntries = 0;
|
|
5394
6053
|
let recordCount = 0;
|
|
5395
6054
|
let entriesScanned = 0;
|
|
6055
|
+
let lastKey;
|
|
5396
6056
|
let limit;
|
|
5397
|
-
|
|
6057
|
+
let nextCheckAt = start + TIME_LIMIT;
|
|
6058
|
+
for (const { key, value } of primaryStore.getRange({ start: true, lazy: true, snapshot: false })) {
|
|
5398
6059
|
if (value != null)
|
|
5399
6060
|
recordCount++;
|
|
5400
6061
|
entriesScanned++;
|
|
6062
|
+
lastKey = key;
|
|
5401
6063
|
await rest();
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
6064
|
+
// a table too small to reach the floor is small enough to finish exactly
|
|
6065
|
+
if (exactCount || entriesScanned < MIN_ESTIMATOR_SAMPLE)
|
|
6066
|
+
continue;
|
|
6067
|
+
const now = node_perf_hooks_1.performance.now();
|
|
6068
|
+
if (now <= nextCheckAt)
|
|
6069
|
+
continue;
|
|
6070
|
+
nextCheckAt = now + TIME_LIMIT;
|
|
6071
|
+
checkpoints++;
|
|
6072
|
+
if (canEstimate && !estimatorFailed) {
|
|
6073
|
+
try {
|
|
6074
|
+
estimator ??= primaryStore.createCountEstimator({ start: true });
|
|
6075
|
+
estimator.advance(lastKey, entriesScanned - checkpointedEntries);
|
|
6076
|
+
checkpointedEntries = entriesScanned;
|
|
6077
|
+
entryCount = usableCount(estimator.estimate());
|
|
6078
|
+
}
|
|
6079
|
+
catch (error) {
|
|
6080
|
+
// a store closing concurrently -- drop_table can, while this scan is parked in a yield
|
|
6081
|
+
logger_ts_1.logger.debug?.('Count estimator unavailable, falling back to an exact scan', error);
|
|
6082
|
+
estimatorFailed = true;
|
|
6083
|
+
estimator = undefined;
|
|
6084
|
+
entryCount = 0;
|
|
5414
6085
|
}
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
6086
|
+
}
|
|
6087
|
+
else if (!canEstimate) {
|
|
6088
|
+
// `canEstimate` false is not the same as "LMDB": a RocksDB store whose native module predates
|
|
6089
|
+
// the estimator API lands here too, and `RocksDatabase.getStats()` carries no `entryCount`.
|
|
6090
|
+
try {
|
|
6091
|
+
const stats = primaryStore.getStats?.();
|
|
6092
|
+
entryCount = Number.isFinite(stats?.entryCount) && stats.entryCount > 0 ? stats.entryCount : 0;
|
|
6093
|
+
}
|
|
6094
|
+
catch {
|
|
6095
|
+
entryCount = 0;
|
|
6096
|
+
}
|
|
6097
|
+
}
|
|
6098
|
+
if (!entryCount && canEstimate && !estimatorFailed) {
|
|
6099
|
+
// Range estimates are block-granular and can report 0 for a store whose entries are still
|
|
6100
|
+
// in the memtable. Without a base the escape cannot fire at all, so fall back to the
|
|
6101
|
+
// whole-store property rather than silently walking the table.
|
|
6102
|
+
try {
|
|
6103
|
+
const wholeStore = primaryStore.getEstimatedKeyCount();
|
|
6104
|
+
entryCount = Number.isFinite(wholeStore) && wholeStore > 0 ? wholeStore : 0;
|
|
6105
|
+
}
|
|
6106
|
+
catch {
|
|
6107
|
+
entryCount = 0;
|
|
6108
|
+
}
|
|
6109
|
+
if (!entryCount && !warnedNoBase) {
|
|
6110
|
+
warnedNoBase = true;
|
|
6111
|
+
logger_ts_1.logger.debug?.(`No usable key-count estimate for ${tableName}; counting records by full scan`);
|
|
6112
|
+
}
|
|
6113
|
+
}
|
|
6114
|
+
// Zero is "no usable base": degrade to the exact scan. The checkpoint ceiling is what stops a
|
|
6115
|
+
// base that keeps undershooting from holding the halfway test false forever and walking the
|
|
6116
|
+
// whole table; the reverse sample is bounded by `limit` in turn.
|
|
6117
|
+
if (entryCount > 0 &&
|
|
6118
|
+
(checkpoints >= MAX_ESTIMATE_CHECKPOINTS || entriesScanned < Math.floor(entryCount / 2))) {
|
|
6119
|
+
if (canEstimate) {
|
|
6120
|
+
try {
|
|
6121
|
+
const remaining = primaryStore.estimateCount({ start: lastKey, exclusiveStart: true });
|
|
6122
|
+
// widened by its own reported untrustworthiness: block-granular, so it can land below
|
|
6123
|
+
// the live count it is meant to bound
|
|
6124
|
+
const remainingCount = usableCount(remaining);
|
|
6125
|
+
remainderPhysical = remainingCount > 0 ? remainingCount * (2 - remaining.confidence) : 0;
|
|
6126
|
+
}
|
|
6127
|
+
catch {
|
|
6128
|
+
remainderPhysical = 0;
|
|
6129
|
+
}
|
|
6130
|
+
// A zero or unusable remainder is valid -- entries still in the memtable read as none
|
|
6131
|
+
// through range statistics -- but it would leave `baseMax` resting on the sampled ends
|
|
6132
|
+
// alone. The whole-store property is a separate, non-range source, so fall back to it.
|
|
6133
|
+
if (!remainderPhysical) {
|
|
6134
|
+
try {
|
|
6135
|
+
const wholeStore = primaryStore.getEstimatedKeyCount();
|
|
6136
|
+
if (Number.isFinite(wholeStore))
|
|
6137
|
+
remainderPhysical = Math.max(wholeStore - entriesScanned, 0);
|
|
6138
|
+
}
|
|
6139
|
+
catch {
|
|
6140
|
+
remainderPhysical = 0;
|
|
6141
|
+
}
|
|
6142
|
+
}
|
|
6143
|
+
}
|
|
6144
|
+
limit = entriesScanned;
|
|
6145
|
+
break;
|
|
5418
6146
|
}
|
|
5419
6147
|
}
|
|
5420
6148
|
if (limit) {
|
|
5421
6149
|
// in this case we are going to make an estimate of the table count using the first thousand
|
|
5422
6150
|
// entries and last thousand entries
|
|
5423
6151
|
const firstRecordCount = recordCount;
|
|
6152
|
+
const firstKey = lastKey;
|
|
5424
6153
|
recordCount = 0;
|
|
5425
6154
|
// Bound the reverse scan explicitly. The getRange `limit` option is honored by lmdb-js but
|
|
5426
6155
|
// ignored by rocksdb-js; without this break the scan reads the whole table, so `recordRate`
|
|
5427
6156
|
// blows up to ~entryCount/(2*limit) and the estimate scales with entryCount^2 -- the source
|
|
5428
6157
|
// of the wildly inflated `record_count` (e.g. 20,000,000 for ~105k rows) on large RocksDB
|
|
5429
|
-
// tables.
|
|
6158
|
+
// tables.
|
|
5430
6159
|
let reverseScanned = 0;
|
|
5431
|
-
|
|
6160
|
+
// Sized independently of the forward scan. `entriesScanned` is whatever the forward pass
|
|
6161
|
+
// covered before it escaped, and the checkpoint ceiling lets that run twenty budget
|
|
6162
|
+
// intervals when the base keeps undershooting; matching it here would read that same count
|
|
6163
|
+
// again and double the wall clock of the call this path exists to bound.
|
|
6164
|
+
const reverseLimit = Math.min(limit, MIN_ESTIMATOR_SAMPLE);
|
|
6165
|
+
// Disjointness is enforced against the forward scan's own last key rather than inferred from
|
|
6166
|
+
// the base, which is an estimate that can overshoot by more than 2x.
|
|
6167
|
+
let sampledWholeTable = false;
|
|
6168
|
+
for (const { key, value } of primaryStore.getRange({
|
|
5432
6169
|
start: '\uffff',
|
|
5433
6170
|
reverse: true,
|
|
5434
6171
|
lazy: true,
|
|
5435
|
-
limit,
|
|
6172
|
+
limit: reverseLimit,
|
|
5436
6173
|
snapshot: false,
|
|
5437
6174
|
})) {
|
|
6175
|
+
if ((0, ordered_binary_1.compareKeys)(key, firstKey) <= 0) {
|
|
6176
|
+
sampledWholeTable = true;
|
|
6177
|
+
break;
|
|
6178
|
+
}
|
|
5438
6179
|
if (value != null)
|
|
5439
6180
|
recordCount++;
|
|
5440
6181
|
reverseScanned++;
|
|
5441
6182
|
await rest();
|
|
5442
|
-
if (reverseScanned >=
|
|
6183
|
+
if (reverseScanned >= reverseLimit)
|
|
5443
6184
|
break;
|
|
5444
6185
|
}
|
|
6186
|
+
// the samples met, so between them they covered every entry
|
|
6187
|
+
if (sampledWholeTable)
|
|
6188
|
+
return { recordCount: recordCount + firstRecordCount };
|
|
5445
6189
|
// Use the actual entries sampled, not limit*2: the reverse scan can yield fewer than `limit`
|
|
5446
6190
|
// (concurrent deletions under snapshot:false, or an overestimated entryCount), and counting
|
|
5447
6191
|
// those un-scanned slots would inflate the denominator and underestimate the rate.
|
|
5448
|
-
const
|
|
5449
|
-
const recordRate = (
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
//
|
|
5455
|
-
//
|
|
5456
|
-
const
|
|
5457
|
-
const
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
6192
|
+
const sampledRecords = recordCount + firstRecordCount;
|
|
6193
|
+
const recordRate = sampledRecords / (limit + reverseScanned);
|
|
6194
|
+
// Endpoints for the extrapolation base, spanning both ways an estimated base can be wrong:
|
|
6195
|
+
// every remaining entry superseded (only what was sampled is live) through every remaining
|
|
6196
|
+
// entry live (the uncalibrated physical count). Churn concentrated outside the sampled ends
|
|
6197
|
+
// calibrates to nothing, so an interval derived from the estimator's confidence would sit
|
|
6198
|
+
// narrowly around the wrong number. Both endpoints are themselves estimates on RocksDB, so
|
|
6199
|
+
// this is a widened heuristic interval, not a guaranteed bound on the live count.
|
|
6200
|
+
const baseMin = entriesScanned + reverseScanned;
|
|
6201
|
+
const baseMax = Math.max(entriesScanned + remainderPhysical, entryCount, baseMin);
|
|
6202
|
+
const estimatedRecordCount = Math.round(recordRate * Math.max(entryCount, baseMin));
|
|
6203
|
+
// The samples counted these directly, and the entries between them can only add; everything
|
|
6204
|
+
// outside the samples could be live. A statistical interval inside those endpoints would be
|
|
6205
|
+
// narrowest exactly where the ends are least representative of the middle -- sampled ends
|
|
6206
|
+
// that are all deletion entries give a rate of 0, collapsing an upper end to ~0 with live
|
|
6207
|
+
// rows in between -- so the endpoints are the evidence itself.
|
|
6208
|
+
const lower = sampledRecords;
|
|
6209
|
+
const upper = Math.round(baseMax);
|
|
6210
|
+
// Report only the precision the interval supports, but never so coarse a unit that the
|
|
6211
|
+
// estimate rounds away: `baseMax` is physical and can exceed a calibrated estimate by
|
|
6212
|
+
// orders of magnitude, which a single division cannot walk back.
|
|
6213
|
+
let significantUnit = Math.pow(10, Math.round(Math.log10(Math.max((upper - lower) / 2, 1))));
|
|
6214
|
+
while (significantUnit > estimatedRecordCount && significantUnit > 1)
|
|
6215
|
+
significantUnit /= 10;
|
|
6216
|
+
recordCount = Math.min(Math.max(Math.round(estimatedRecordCount / significantUnit) * significantUnit, lower), upper);
|
|
5462
6217
|
return {
|
|
5463
6218
|
recordCount,
|
|
5464
|
-
estimatedRange: [
|
|
6219
|
+
estimatedRange: [lower, upper],
|
|
5465
6220
|
};
|
|
5466
6221
|
}
|
|
5467
6222
|
return {
|
|
@@ -5475,6 +6230,7 @@ function makeTable(options) {
|
|
|
5475
6230
|
// Refresh on every call: schema reload mutates `attributes` in place, so the
|
|
5476
6231
|
// class-construction snapshot would otherwise go stale.
|
|
5477
6232
|
this.embedAttributes = this.attributes.filter((a) => a?.embed);
|
|
6233
|
+
expiresAtProperty = this.attributes.find((attribute) => attribute.expiresAt);
|
|
5478
6234
|
// Drop registry entries for attributes that are no longer `@embed`, so a dropped
|
|
5479
6235
|
// directive doesn't leave a stale embedder or block a default refresh on re-add.
|
|
5480
6236
|
const embedNames = new Set(this.embedAttributes.map((a) => a.name));
|
|
@@ -5541,7 +6297,7 @@ function makeTable(options) {
|
|
|
5541
6297
|
const id = object[relationship.from ? relationship.from : primaryKey];
|
|
5542
6298
|
const relatedTable = attribute.elements.definition.tableClass;
|
|
5543
6299
|
if (returnEntry) {
|
|
5544
|
-
return (0, search_ts_1.searchByIndex)({ attribute: relationship.to, value: id }, txnForContext(context).getReadTxn(), false, relatedTable, false).map((entry) => {
|
|
6300
|
+
return (0, search_ts_1.searchByIndex)({ attribute: relationship.to, value: id }, txnForContext(context).getReadTxn(), false, relatedTable, { allowFullScan: false }).map((entry) => {
|
|
5545
6301
|
if (entry && entry.key !== undefined)
|
|
5546
6302
|
return entry;
|
|
5547
6303
|
return relatedTable.primaryStore.getEntry(entry, {
|
|
@@ -5788,10 +6544,24 @@ function makeTable(options) {
|
|
|
5788
6544
|
}
|
|
5789
6545
|
const drainRemovals = () => Promise.all(inFlightRemovals);
|
|
5790
6546
|
let entriesDeleted = 0;
|
|
6547
|
+
// LMDB only: RocksTransactionLogStore.remove() is a no-op, so a RocksDB deleteHistory removes
|
|
6548
|
+
// nothing and must not claim it did.
|
|
6549
|
+
// A bound above everything reachable must not be recorded as the floor: the floor only rises
|
|
6550
|
+
// and a store with a record is never re-stamped, so it would never come down, for every table in
|
|
6551
|
+
// this database. `boundedAuditPruneEnd` clamps the cutoff to just above the newest key in the
|
|
6552
|
+
// log, and the scan below uses that same value as its range end, so the prune cannot remove an
|
|
6553
|
+
// entry the floor does not cover.
|
|
6554
|
+
let pruneEnd = endTime;
|
|
6555
|
+
if (!isRocksDB) {
|
|
6556
|
+
pruneEnd = (0, auditStore_ts_1.boundedAuditPruneEnd)(auditStore, endTime);
|
|
6557
|
+
(0, auditStore_ts_1.raiseAuditFloor)(auditStore, pruneEnd);
|
|
6558
|
+
}
|
|
5791
6559
|
try {
|
|
5792
6560
|
for (const auditRecord of auditStore.getRange({
|
|
5793
|
-
|
|
5794
|
-
|
|
6561
|
+
// must not be zero: 0 encodes to all zero bytes and so overlaps the symbol keys, as in
|
|
6562
|
+
// getHistory below
|
|
6563
|
+
start: 1,
|
|
6564
|
+
end: pruneEnd,
|
|
5795
6565
|
})) {
|
|
5796
6566
|
await rest(); // yield to other async operations
|
|
5797
6567
|
if (auditRecord.tableId !== tableId)
|
|
@@ -5816,7 +6586,7 @@ function makeTable(options) {
|
|
|
5816
6586
|
const auditTime = isRocksDB && version != null
|
|
5817
6587
|
? resolveAuditHead(key, version, entry.nodeId, entry.additionalAuditRefs).txnLogKey
|
|
5818
6588
|
: localTime;
|
|
5819
|
-
if (value === null && version != null && auditTime <
|
|
6589
|
+
if (value === null && version != null && auditTime < pruneEnd) {
|
|
5820
6590
|
const backpressure = queueRemoval(() => primaryStore.remove(key, version), 'Error removing deleted record during deleteHistory');
|
|
5821
6591
|
if (backpressure)
|
|
5822
6592
|
await backpressure;
|
|
@@ -5840,7 +6610,7 @@ function makeTable(options) {
|
|
|
5840
6610
|
end: endTime,
|
|
5841
6611
|
})) {
|
|
5842
6612
|
await rest(); // yield to other async operations
|
|
5843
|
-
if (auditRecord.tableId !== tableId)
|
|
6613
|
+
if (auditRecord.tableId !== tableId || auditRecord.type === 'evict' || (0, auditStore_ts_1.isLockControlType)(auditRecord.type))
|
|
5844
6614
|
continue;
|
|
5845
6615
|
yield {
|
|
5846
6616
|
id: auditRecord.recordId,
|
|
@@ -5874,7 +6644,10 @@ function makeTable(options) {
|
|
|
5874
6644
|
let highestPreviousVersion = 0;
|
|
5875
6645
|
const start = nextVersion - auditWindow;
|
|
5876
6646
|
for (const auditRecord of auditStore.getRange({ start, end: nextVersion + 0.001 })) {
|
|
5877
|
-
if (auditRecord.tableId === tableId &&
|
|
6647
|
+
if (auditRecord.tableId === tableId &&
|
|
6648
|
+
auditRecord.type !== 'evict' &&
|
|
6649
|
+
!(0, auditStore_ts_1.isLockControlType)(auditRecord.type) &&
|
|
6650
|
+
(0, ordered_binary_1.compareKeys)(auditRecord.recordId, id) === 0) {
|
|
5878
6651
|
history.splice(insertionPoint, 0, {
|
|
5879
6652
|
id: auditRecord.recordId,
|
|
5880
6653
|
localTime: isRocksDB ? auditRecord.txnLogKey : auditRecord.version,
|
|
@@ -5905,6 +6678,7 @@ function makeTable(options) {
|
|
|
5905
6678
|
const promises = [primaryStore.clear()];
|
|
5906
6679
|
for (const key in indices) {
|
|
5907
6680
|
const index = indices[key];
|
|
6681
|
+
index.customIndex?.resetDerivedStorage?.();
|
|
5908
6682
|
promises.push(index.clearAsync ? index.clearAsync() : index.clear());
|
|
5909
6683
|
}
|
|
5910
6684
|
return Promise.all(promises);
|
|
@@ -5912,6 +6686,7 @@ function makeTable(options) {
|
|
|
5912
6686
|
/** Release everything makeTable() registered process-wide; the class must not be used afterwards. */
|
|
5913
6687
|
static cleanup() {
|
|
5914
6688
|
disposed = true;
|
|
6689
|
+
void _a.derivedIndexRuntime?.close();
|
|
5915
6690
|
clearTimeout(cleanupTimer);
|
|
5916
6691
|
settlePendingCleanup();
|
|
5917
6692
|
clearInterval(recordExpirationInterval);
|
|
@@ -5938,9 +6713,16 @@ function makeTable(options) {
|
|
|
5938
6713
|
});
|
|
5939
6714
|
try {
|
|
5940
6715
|
TableResource.updatedAttributes(); // on creation, update accessors as well
|
|
5941
|
-
if (expirationMs)
|
|
5942
|
-
|
|
5943
|
-
|
|
6716
|
+
if (expirationMs) {
|
|
6717
|
+
ttlFromLoad = true;
|
|
6718
|
+
try {
|
|
6719
|
+
TableResource.setTTLExpiration(expirationMs / 1000);
|
|
6720
|
+
}
|
|
6721
|
+
finally {
|
|
6722
|
+
ttlFromLoad = false;
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
if (expiresAtProperty && !recordExpirationInterval)
|
|
5944
6726
|
runRecordExpirationEviction();
|
|
5945
6727
|
}
|
|
5946
6728
|
catch (error) {
|
|
@@ -6729,25 +7511,35 @@ function makeTable(options) {
|
|
|
6729
7511
|
resolve(resolvedEntry);
|
|
6730
7512
|
}
|
|
6731
7513
|
catch (error) {
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
7514
|
+
// A source may reject with anything at all, so deciding how to settle is itself
|
|
7515
|
+
// fallible: `message` is not assignable on every error (a DOMException from
|
|
7516
|
+
// AbortSignal.timeout), and a nullish rejection makes the reads below throw.
|
|
7517
|
+
// Leaving this promise unsettled hangs the caller forever, so every path here
|
|
7518
|
+
// has to end in resolve() or reject().
|
|
7519
|
+
try {
|
|
7520
|
+
(0, hdbError_ts_1.appendErrorContext)(error, ` while resolving record ${id} for ${tableName}`);
|
|
7521
|
+
if (existingRecord &&
|
|
7522
|
+
(((error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED' || error.code === 'EAI_AGAIN') &&
|
|
7523
|
+
!context?.mustRevalidate) ||
|
|
7524
|
+
(context?.staleIfError &&
|
|
7525
|
+
(error.statusCode === 500 ||
|
|
7526
|
+
error.statusCode === 502 ||
|
|
7527
|
+
error.statusCode === 503 ||
|
|
7528
|
+
error.statusCode === 504)))) {
|
|
7529
|
+
// these are conditions under which we can use stale data after an error
|
|
7530
|
+
resolve({
|
|
7531
|
+
key: id,
|
|
7532
|
+
version: existingVersion,
|
|
7533
|
+
value: existingRecord,
|
|
7534
|
+
});
|
|
7535
|
+
logger_ts_1.logger.trace?.(error?.message, '(returned stale record)');
|
|
7536
|
+
}
|
|
7537
|
+
else
|
|
7538
|
+
reject(error);
|
|
7539
|
+
}
|
|
7540
|
+
catch (settlingError) {
|
|
7541
|
+
reject(error ?? settlingError);
|
|
6748
7542
|
}
|
|
6749
|
-
else
|
|
6750
|
-
reject(error);
|
|
6751
7543
|
const resolveDuration = node_perf_hooks_1.performance.now() - start;
|
|
6752
7544
|
(0, write_ts_1.recordAction)(resolveDuration, 'cache-resolution', tableName, null, 'fail');
|
|
6753
7545
|
if (responseHeaders)
|
|
@@ -6957,6 +7749,7 @@ function makeTable(options) {
|
|
|
6957
7749
|
if (hasSourceGet && primaryStore.hasLock(item.key, entry.version))
|
|
6958
7750
|
continue; // resolution in progress
|
|
6959
7751
|
updateIndices(item.key, entry.value, null, options);
|
|
7752
|
+
stageDerivedIndexEviction(transaction, item.key, entry.version);
|
|
6960
7753
|
}
|
|
6961
7754
|
(0, RecordEncoder_ts_1.removeEntry)(primaryStore, entry, options);
|
|
6962
7755
|
staged++;
|
|
@@ -7054,7 +7847,7 @@ function makeTable(options) {
|
|
|
7054
7847
|
if (cleanupInterval === lastCleanupInterval && !runImmediately)
|
|
7055
7848
|
return;
|
|
7056
7849
|
lastCleanupInterval = cleanupInterval;
|
|
7057
|
-
if ((0, manageThreads_js_1.
|
|
7850
|
+
if ((0, manageThreads_js_1.ownsStoreMaintenance)(primaryStore.path) || (ttlConfiguredByApplication && (0, manageThreads_js_1.isDedicatedWorker)())) {
|
|
7058
7851
|
// run on the last thread so we aren't overloading lower-numbered threads
|
|
7059
7852
|
if (cleanupTimer)
|
|
7060
7853
|
clearTimeout(cleanupTimer);
|
|
@@ -7196,12 +7989,14 @@ function makeTable(options) {
|
|
|
7196
7989
|
}
|
|
7197
7990
|
function runRecordExpirationEviction() {
|
|
7198
7991
|
// Periodically evict expired records, searching for records who expiresAt timestamp is before now
|
|
7199
|
-
if ((0, manageThreads_js_1.
|
|
7992
|
+
if ((0, manageThreads_js_1.ownsStoreExpiration)(primaryStore.path) || (ttlConfiguredByApplication && (0, manageThreads_js_1.isDedicatedWorker)())) {
|
|
7200
7993
|
// we want to run the pruning of expired records on only one thread so we don't have conflicts in evicting
|
|
7201
7994
|
recordExpirationInterval = setInterval(async () => {
|
|
7202
7995
|
// go through each database and table and then search for expired entries
|
|
7203
7996
|
// find any entries that are set to expire before now
|
|
7204
|
-
|
|
7997
|
+
// updatedAttributes() clears expiresAtProperty when a live redeclaration drops the directive,
|
|
7998
|
+
// and there is nothing left for this interval to scan by
|
|
7999
|
+
if (disposed || runningRecordExpiration || !expiresAtProperty)
|
|
7205
8000
|
return;
|
|
7206
8001
|
runningRecordExpiration = true;
|
|
7207
8002
|
try {
|