@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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { FullTextDerivedIndexEngine } from './fullTextDerivedIndex.ts';
|
|
2
|
+
import { loggerWithTag } from '../../utility/logging/logger.ts';
|
|
3
|
+
|
|
4
|
+
const FULLTEXT_LIFECYCLE_API_VERSION = 1;
|
|
5
|
+
const FULLTEXT_MUTATION_BATCH_API_VERSION = 3;
|
|
6
|
+
const logger = loggerWithTag('fulltext-derived-index');
|
|
7
|
+
|
|
8
|
+
export interface NativeFullTextIndexConfiguration {
|
|
9
|
+
fields: Array<{ name: string; weight?: number }>;
|
|
10
|
+
analyzer: 'english@1';
|
|
11
|
+
stopWords?: boolean;
|
|
12
|
+
positions?: boolean;
|
|
13
|
+
surfaceTerms?: boolean;
|
|
14
|
+
limits: {
|
|
15
|
+
indexingThreads: number;
|
|
16
|
+
searchThreads: number;
|
|
17
|
+
writerMemoryBytes: number;
|
|
18
|
+
maxQueuedCommands: number;
|
|
19
|
+
maxQueuedBytes: number;
|
|
20
|
+
maxBatchBytes: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type NativeFullTextRuntimeInfo = {
|
|
25
|
+
packageVersion: string;
|
|
26
|
+
tantivyVersion: string;
|
|
27
|
+
nativeAbiVersion: number;
|
|
28
|
+
lifecycleApiVersion: number;
|
|
29
|
+
mutationBatchApiVersion: number;
|
|
30
|
+
storageBackends: ReadonlyArray<'native'>;
|
|
31
|
+
limits: { maxCommitPayloadBytes: number };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface NativeFullTextModule {
|
|
35
|
+
NativeFullTextIndex: {
|
|
36
|
+
prototype: Pick<FullTextDerivedIndexEngine, 'applyMutationBatch' | 'publish' | 'close'>;
|
|
37
|
+
};
|
|
38
|
+
runtimeInfo(): Promise<NativeFullTextRuntimeInfo>;
|
|
39
|
+
validateNativeFullTextIndexOptions(
|
|
40
|
+
options: NativeFullTextIndexConfiguration & {
|
|
41
|
+
path: string;
|
|
42
|
+
indexId: string;
|
|
43
|
+
generation: string;
|
|
44
|
+
}
|
|
45
|
+
): void;
|
|
46
|
+
openNativeFullTextIndex(
|
|
47
|
+
options: NativeFullTextIndexConfiguration & {
|
|
48
|
+
path: string;
|
|
49
|
+
indexId: string;
|
|
50
|
+
generation: string;
|
|
51
|
+
}
|
|
52
|
+
): Promise<FullTextDerivedIndexEngine>;
|
|
53
|
+
inspectNativeFullTextIndex(
|
|
54
|
+
options: Omit<NativeFullTextIndexConfiguration, 'limits'> & {
|
|
55
|
+
path: string;
|
|
56
|
+
indexId: string;
|
|
57
|
+
generation: string;
|
|
58
|
+
}
|
|
59
|
+
):
|
|
60
|
+
| { state: 'missing' | 'cursorless' }
|
|
61
|
+
| { state: 'checkpointed'; committedPayload: string }
|
|
62
|
+
| { state: 'incompatible'; code: string };
|
|
63
|
+
resetNativeFullTextIndex(options: {
|
|
64
|
+
path: string;
|
|
65
|
+
indexId: string;
|
|
66
|
+
}): Promise<{ state: 'missing' } | { state: 'reset'; retiredPath: string }>;
|
|
67
|
+
reclaimRetiredNativeFullTextIndexes(options: {
|
|
68
|
+
path: string;
|
|
69
|
+
retiredPath?: string;
|
|
70
|
+
}): Promise<{ removed: number; failed: number }>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let bindingPromise: Promise<NativeFullTextModule> | undefined;
|
|
74
|
+
let bindingWarningLogged = false;
|
|
75
|
+
const validatedRuntimeInfo = new WeakMap<NativeFullTextModule, NativeFullTextRuntimeInfo>();
|
|
76
|
+
|
|
77
|
+
export async function loadFullTextNativeBinding(): Promise<NativeFullTextModule> {
|
|
78
|
+
if (!bindingPromise) {
|
|
79
|
+
const moduleName = '@harperfast/fulltext/native';
|
|
80
|
+
bindingPromise = import(moduleName).then(validateFullTextNativeBinding);
|
|
81
|
+
bindingPromise.catch((error) => {
|
|
82
|
+
bindingPromise = undefined;
|
|
83
|
+
if (!bindingWarningLogged) {
|
|
84
|
+
bindingWarningLogged = true;
|
|
85
|
+
const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
|
|
86
|
+
try {
|
|
87
|
+
logger.warn?.(
|
|
88
|
+
`The @harperfast/fulltext native module is unavailable${typeof code === 'string' ? ` (${code})` : ''}`
|
|
89
|
+
);
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return bindingPromise;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function validateFullTextNativeBinding(module: unknown): Promise<NativeFullTextModule> {
|
|
98
|
+
const prototype = (module as { NativeFullTextIndex?: { prototype?: Partial<FullTextDerivedIndexEngine> } })
|
|
99
|
+
?.NativeFullTextIndex?.prototype;
|
|
100
|
+
if (
|
|
101
|
+
!module ||
|
|
102
|
+
typeof module !== 'object' ||
|
|
103
|
+
!('runtimeInfo' in module) ||
|
|
104
|
+
typeof module.runtimeInfo !== 'function' ||
|
|
105
|
+
!('NativeFullTextIndex' in module) ||
|
|
106
|
+
typeof module.NativeFullTextIndex !== 'function' ||
|
|
107
|
+
typeof prototype?.applyMutationBatch !== 'function' ||
|
|
108
|
+
typeof prototype.publish !== 'function' ||
|
|
109
|
+
typeof prototype.close !== 'function' ||
|
|
110
|
+
!('openNativeFullTextIndex' in module) ||
|
|
111
|
+
typeof module.openNativeFullTextIndex !== 'function' ||
|
|
112
|
+
!('validateNativeFullTextIndexOptions' in module) ||
|
|
113
|
+
typeof module.validateNativeFullTextIndexOptions !== 'function' ||
|
|
114
|
+
!('inspectNativeFullTextIndex' in module) ||
|
|
115
|
+
typeof module.inspectNativeFullTextIndex !== 'function' ||
|
|
116
|
+
!('resetNativeFullTextIndex' in module) ||
|
|
117
|
+
typeof module.resetNativeFullTextIndex !== 'function' ||
|
|
118
|
+
!('reclaimRetiredNativeFullTextIndexes' in module) ||
|
|
119
|
+
typeof module.reclaimRetiredNativeFullTextIndexes !== 'function'
|
|
120
|
+
)
|
|
121
|
+
throw new TypeError('@harperfast/fulltext/native does not implement the required Harper binding contract');
|
|
122
|
+
const binding = module as NativeFullTextModule;
|
|
123
|
+
const info = await binding.runtimeInfo();
|
|
124
|
+
if (
|
|
125
|
+
!info ||
|
|
126
|
+
typeof info.packageVersion !== 'string' ||
|
|
127
|
+
typeof info.tantivyVersion !== 'string' ||
|
|
128
|
+
!Number.isSafeInteger(info.nativeAbiVersion) ||
|
|
129
|
+
info.lifecycleApiVersion !== FULLTEXT_LIFECYCLE_API_VERSION ||
|
|
130
|
+
info.mutationBatchApiVersion !== FULLTEXT_MUTATION_BATCH_API_VERSION ||
|
|
131
|
+
!Array.isArray(info.storageBackends) ||
|
|
132
|
+
!info.storageBackends.includes('native') ||
|
|
133
|
+
!info.limits ||
|
|
134
|
+
!Number.isSafeInteger(info.limits.maxCommitPayloadBytes) ||
|
|
135
|
+
info.limits.maxCommitPayloadBytes <= 0
|
|
136
|
+
)
|
|
137
|
+
throw new TypeError('@harperfast/fulltext/native reported incompatible runtime capabilities');
|
|
138
|
+
validatedRuntimeInfo.set(binding, info);
|
|
139
|
+
return binding;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getValidatedFullTextRuntimeInfo(binding: NativeFullTextModule): NativeFullTextRuntimeInfo {
|
|
143
|
+
const info = validatedRuntimeInfo.get(binding);
|
|
144
|
+
if (!info) throw new Error('Full-text native binding has not been validated');
|
|
145
|
+
return info;
|
|
146
|
+
}
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { ClientError } from '../../utility/errors/hdbError.ts';
|
|
2
|
+
import { loggerWithTag } from '../../utility/logging/logger.ts';
|
|
3
|
+
import type { Id } from '../ResourceInterface.ts';
|
|
4
|
+
import type { RocksTransactionLogStore } from '../RocksTransactionLogStore.ts';
|
|
5
|
+
import {
|
|
6
|
+
DERIVED_INDEX_ACCEPTED,
|
|
7
|
+
DERIVED_INDEX_DEFERRED,
|
|
8
|
+
DerivedIndexRuntime,
|
|
9
|
+
readDerivedIndexReadiness,
|
|
10
|
+
readDerivedIndexCoverage,
|
|
11
|
+
sameDerivedIndexPositions,
|
|
12
|
+
type DerivedIndexBackend,
|
|
13
|
+
type DerivedIndexBackendHost,
|
|
14
|
+
type DerivedIndexBackendStateChange,
|
|
15
|
+
type DerivedIndexBatch,
|
|
16
|
+
type DerivedIndexCursor,
|
|
17
|
+
type DerivedIndexCoverage,
|
|
18
|
+
type DerivedIndexPositions,
|
|
19
|
+
type DerivedIndexDeliveryResult,
|
|
20
|
+
type DerivedIndexFlushReason,
|
|
21
|
+
type DerivedIndexMutation,
|
|
22
|
+
type DerivedIndexReadiness,
|
|
23
|
+
} from '../derivedIndexRuntime.ts';
|
|
24
|
+
|
|
25
|
+
const logger = loggerWithTag('HNSW');
|
|
26
|
+
|
|
27
|
+
/** The one durable cursor vector of a file-primary HNSW index, stored beside its node mappings. */
|
|
28
|
+
export const DERIVED_INDEX_CURSOR_KEY = Symbol.for('derived-index-cursor');
|
|
29
|
+
export const DEFAULT_MAX_INDEX_LAG_MILLISECONDS = 3000;
|
|
30
|
+
export const MAX_WAIT_FOR_INDEX_MILLISECONDS = 30_000;
|
|
31
|
+
|
|
32
|
+
export type DerivedNativeIndexHost = {
|
|
33
|
+
readiness: () => DerivedIndexReadiness;
|
|
34
|
+
coverage: (maxLagMilliseconds: number) => DerivedIndexCoverage;
|
|
35
|
+
waitForCoverage: (since: bigint, timeout: number, signal?: AbortSignal) => Promise<void>;
|
|
36
|
+
requestRebuild: () => boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Bounds on the queue between the runtime's delivery and the native applier. The runtime already
|
|
40
|
+
// chunks a delivery by records and estimated bytes; this caps how many chunks may wait.
|
|
41
|
+
const QUEUE_CAPACITY_BYTES = 64 * 1024 * 1024;
|
|
42
|
+
const APPLY_SLICE_MILLIS = 5;
|
|
43
|
+
const BARRIER_IDLE_MULTIPLE = 3;
|
|
44
|
+
const BARRIER_IDLE_CEILING_MILLISECONDS = 7_500;
|
|
45
|
+
// Writes to an index this far behind fail with a retryable 503 (see the runtime's lag policy).
|
|
46
|
+
const DEFAULT_MAX_LAG_MILLISECONDS = 30_000;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The native index methods the backend drives. `applyDerivedValue` inserts, replaces or removes
|
|
50
|
+
* one primary key's vector and stages its mapping as pending; `flushDerived` is the durability
|
|
51
|
+
* barrier that publishes the pending mappings; `resetDerivedStorage` destroys the native file.
|
|
52
|
+
*/
|
|
53
|
+
export interface DerivedNativeIndex {
|
|
54
|
+
readonly indexStore: any;
|
|
55
|
+
applyDerivedValue(primaryKey: Id, vector: number[] | undefined, version?: number): void;
|
|
56
|
+
flushDerived(): Promise<void>;
|
|
57
|
+
resetDerivedStorage(): void;
|
|
58
|
+
assertDerivedValue(vector: unknown, label: string): void;
|
|
59
|
+
attachDerivedHost(host: DerivedNativeIndexHost): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `DerivedIndexBackend` over a file-primary HNSW index. `deliver()` only enqueues; an applier drains
|
|
64
|
+
* the queue in bounded time slices so a 4096-record chunk at ~0.35 ms per native insert does not
|
|
65
|
+
* hold the event loop for a second and a half. The barrier is the plane's `msync`, after which the
|
|
66
|
+
* pending mappings and then the cursor vector are published — in that order, so a crash between
|
|
67
|
+
* any two steps leaves a cursor that replays (idempotently) over a mapping that was never published,
|
|
68
|
+
* never a published cursor over native state the barrier did not cover.
|
|
69
|
+
*/
|
|
70
|
+
export class HnswDerivedIndexBackend implements DerivedIndexBackend {
|
|
71
|
+
readonly id: string;
|
|
72
|
+
#index: DerivedNativeIndex;
|
|
73
|
+
#host?: DerivedIndexBackendHost;
|
|
74
|
+
#wake?: (change?: DerivedIndexBackendStateChange) => void;
|
|
75
|
+
#queue: DerivedIndexBatch[] = [];
|
|
76
|
+
#queuedBytes = 0;
|
|
77
|
+
#position = 0;
|
|
78
|
+
#scheduled = false;
|
|
79
|
+
#appliedCursor?: DerivedIndexCursor;
|
|
80
|
+
#appliedEpoch?: bigint;
|
|
81
|
+
#flushRequested = false;
|
|
82
|
+
#flushing?: Promise<void>;
|
|
83
|
+
#interruptBarrierAfter = 0;
|
|
84
|
+
#resetting?: Promise<void>;
|
|
85
|
+
#unindexable = 0;
|
|
86
|
+
|
|
87
|
+
constructor(id: string, index: DerivedNativeIndex) {
|
|
88
|
+
this.id = id;
|
|
89
|
+
this.#index = index;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
attach(host: DerivedIndexBackendHost): void {
|
|
93
|
+
this.#host = host;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
onStateChange(wake: (change?: DerivedIndexBackendStateChange) => void): () => void {
|
|
97
|
+
this.#wake = wake;
|
|
98
|
+
return () => {
|
|
99
|
+
if (this.#wake === wake) this.#wake = undefined;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getDurableCursor(): DerivedIndexCursor | undefined {
|
|
104
|
+
const cursor = this.#index.indexStore.getSync(DERIVED_INDEX_CURSOR_KEY);
|
|
105
|
+
return cursor === undefined ? undefined : (cursor as DerivedIndexCursor);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
publishCoverage(positions: DerivedIndexPositions, ownerEpoch: bigint): void {
|
|
109
|
+
if (!this.#host?.isOwnerEpoch(ownerEpoch)) return;
|
|
110
|
+
const cursor = this.getDurableCursor();
|
|
111
|
+
if (!cursor) throw new Error('Cannot publish native index coverage without a durable cursor');
|
|
112
|
+
if (cursor.coverage && sameDerivedIndexPositions(cursor.coverage, positions)) return;
|
|
113
|
+
this.#index.indexStore.putSync(DERIVED_INDEX_CURSOR_KEY, { ...cursor, coverage: positions });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
deliver(batch: DerivedIndexBatch): DerivedIndexDeliveryResult {
|
|
117
|
+
if (this.#queuedBytes >= QUEUE_CAPACITY_BYTES) return DERIVED_INDEX_DEFERRED;
|
|
118
|
+
this.#queue.push(batch);
|
|
119
|
+
this.#queuedBytes += batch.bytes;
|
|
120
|
+
this.#schedule();
|
|
121
|
+
return DERIVED_INDEX_ACCEPTED;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
flush(_reason: DerivedIndexFlushReason): void {
|
|
125
|
+
this.#flushRequested = true;
|
|
126
|
+
if (this.#queue.length === 0 && !this.#scheduled) this.#runFlush();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Drop what is queued and resolve once no barrier is in flight. The runtime quiesces an epoch
|
|
131
|
+
* before it mints the next, so nothing of a newer epoch can be queued here yet.
|
|
132
|
+
*/
|
|
133
|
+
async shutdown(_epoch: bigint): Promise<void> {
|
|
134
|
+
this.#dropQueue();
|
|
135
|
+
await Promise.allSettled([this.#flushing, this.#resetting]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The cursor goes first: a crash anywhere after this line reopens cursorless and rebuilds, never
|
|
140
|
+
* on a format-valid cursor over a destroyed graph.
|
|
141
|
+
*/
|
|
142
|
+
reset(_epoch: bigint): Promise<void> {
|
|
143
|
+
this.#dropQueue();
|
|
144
|
+
this.#appliedCursor = undefined;
|
|
145
|
+
this.#resetting = (async () => {
|
|
146
|
+
await this.#flushing;
|
|
147
|
+
this.#index.indexStore.removeSync(DERIVED_INDEX_CURSOR_KEY);
|
|
148
|
+
this.#index.resetDerivedStorage();
|
|
149
|
+
await this.#index.indexStore.clear();
|
|
150
|
+
})();
|
|
151
|
+
return this.#resetting.finally(() => (this.#resetting = undefined));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#dropQueue() {
|
|
155
|
+
this.#queue.length = 0;
|
|
156
|
+
this.#queuedBytes = 0;
|
|
157
|
+
this.#position = 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#schedule() {
|
|
161
|
+
if (this.#scheduled) return;
|
|
162
|
+
this.#scheduled = true;
|
|
163
|
+
setImmediate(() => {
|
|
164
|
+
this.#scheduled = false;
|
|
165
|
+
this.#applySlice();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#applySlice() {
|
|
170
|
+
// Application pauses while a barrier is in flight so the barrier publishes exactly the
|
|
171
|
+
// mappings it covers; the flush reschedules application when it settles.
|
|
172
|
+
if (this.#flushing) return;
|
|
173
|
+
const host = this.#host!;
|
|
174
|
+
const until = performance.now() + APPLY_SLICE_MILLIS;
|
|
175
|
+
const wasFull = this.#queuedBytes >= QUEUE_CAPACITY_BYTES;
|
|
176
|
+
let advancedCursor = false;
|
|
177
|
+
while (this.#queue.length > 0 && performance.now() < until) {
|
|
178
|
+
const batch = this.#queue[0];
|
|
179
|
+
if (!host.isOwnerEpoch(batch.ownerEpoch)) {
|
|
180
|
+
this.#queue.shift();
|
|
181
|
+
this.#queuedBytes -= batch.bytes;
|
|
182
|
+
this.#position = 0;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const records = batch.records;
|
|
186
|
+
while (this.#position < records.length && performance.now() < until) {
|
|
187
|
+
try {
|
|
188
|
+
this.#apply(records[this.#position++]);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.#queue.length = 0;
|
|
191
|
+
this.#queuedBytes = 0;
|
|
192
|
+
this.#position = 0;
|
|
193
|
+
logger.error(`Derived index '${this.id}' could not apply a record; requesting a rebuild`, error);
|
|
194
|
+
this.#wake?.('failed');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (this.#position < records.length) break;
|
|
199
|
+
this.#queue.shift();
|
|
200
|
+
this.#queuedBytes -= batch.bytes;
|
|
201
|
+
this.#position = 0;
|
|
202
|
+
this.#appliedEpoch = batch.ownerEpoch;
|
|
203
|
+
if (batch.through) {
|
|
204
|
+
this.#appliedCursor = batch.through;
|
|
205
|
+
advancedCursor = true;
|
|
206
|
+
if (this.#mayInterruptForBarrier()) break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (this.#queue.length > 0) {
|
|
210
|
+
if (wasFull && this.#queuedBytes < QUEUE_CAPACITY_BYTES) this.#wake?.('changed');
|
|
211
|
+
if (this.#position === 0 && advancedCursor && this.#mayInterruptForBarrier()) this.#runFlush(true);
|
|
212
|
+
else this.#schedule();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (wasFull) this.#wake?.('changed');
|
|
216
|
+
if (this.#flushRequested) this.#runFlush();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#apply(mutation: DerivedIndexMutation) {
|
|
220
|
+
const { recordId, logVersion, state } = mutation;
|
|
221
|
+
if (state.kind !== 'record' || state.projection == null) {
|
|
222
|
+
this.#index.applyDerivedValue(recordId, undefined, logVersion);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
this.#index.applyDerivedValue(recordId, state.projection as number[], state.version);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
// The projection validated the vector, but the plane's dimensionality is only known to the
|
|
229
|
+
// worker holding it: a mismatch is this record's fault, not the index's. Same rule as the
|
|
230
|
+
// runtime's own 4xx handling — remove any entry and count it, never rebuild.
|
|
231
|
+
if (!(error instanceof ClientError)) throw error;
|
|
232
|
+
if (this.#unindexable++ === 0)
|
|
233
|
+
logger.warn?.(`Derived index '${this.id}' skipped a record its plane cannot hold: ${error.message}`);
|
|
234
|
+
this.#index.applyDerivedValue(recordId, undefined, state.version);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#mayInterruptForBarrier(): boolean {
|
|
239
|
+
return this.#flushRequested && performance.now() >= this.#interruptBarrierAfter;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#runFlush(interrupting = false) {
|
|
243
|
+
if (this.#flushing) return;
|
|
244
|
+
this.#flushRequested = false;
|
|
245
|
+
const started = performance.now();
|
|
246
|
+
const cursor = this.#appliedCursor;
|
|
247
|
+
const epoch = this.#appliedEpoch;
|
|
248
|
+
const host = this.#host!;
|
|
249
|
+
this.#flushing = (async () => {
|
|
250
|
+
await this.#index.flushDerived();
|
|
251
|
+
if (epoch !== undefined && !host.isOwnerEpoch(epoch)) return;
|
|
252
|
+
if (cursor) {
|
|
253
|
+
const coverage = this.getDurableCursor()?.coverage;
|
|
254
|
+
this.#index.indexStore.putSync(DERIVED_INDEX_CURSOR_KEY, coverage ? { ...cursor, coverage } : cursor);
|
|
255
|
+
}
|
|
256
|
+
})();
|
|
257
|
+
this.#flushing.then(
|
|
258
|
+
() => {
|
|
259
|
+
this.#flushing = undefined;
|
|
260
|
+
if (interrupting) {
|
|
261
|
+
const idle = Math.min(
|
|
262
|
+
BARRIER_IDLE_MULTIPLE * (performance.now() - started),
|
|
263
|
+
BARRIER_IDLE_CEILING_MILLISECONDS
|
|
264
|
+
);
|
|
265
|
+
this.#interruptBarrierAfter = performance.now() + idle;
|
|
266
|
+
}
|
|
267
|
+
this.#wake?.('changed');
|
|
268
|
+
if (this.#queue.length > 0) this.#schedule();
|
|
269
|
+
else if (this.#flushRequested) this.#runFlush();
|
|
270
|
+
},
|
|
271
|
+
(error) => {
|
|
272
|
+
this.#flushing = undefined;
|
|
273
|
+
logger.error(`Derived index '${this.id}' durability barrier failed`, error);
|
|
274
|
+
this.#wake?.('failed');
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// One generation owns each physical backend; destructive cleanup also awaits superseded generations.
|
|
281
|
+
type RegisteredTable = { current: { Table: any }; owners: Set<{ Table: any }> };
|
|
282
|
+
type RegisteredBackend = {
|
|
283
|
+
tableId: number;
|
|
284
|
+
settle: () => Promise<void>;
|
|
285
|
+
};
|
|
286
|
+
type Registered = {
|
|
287
|
+
runtime: DerivedIndexRuntime;
|
|
288
|
+
tables: Map<number, RegisteredTable>;
|
|
289
|
+
backends: Map<string, RegisteredBackend>;
|
|
290
|
+
tableBackends: Map<number, Set<RegisteredBackend>>;
|
|
291
|
+
droppingTables: Set<number>;
|
|
292
|
+
retryUnavailable: Set<string>;
|
|
293
|
+
};
|
|
294
|
+
const runtimes = new WeakMap<object, Registered>();
|
|
295
|
+
const retryUnavailableByStore = new Set<string>();
|
|
296
|
+
|
|
297
|
+
function retryUnavailableKey(auditStore: RocksTransactionLogStore, backendId: string): string {
|
|
298
|
+
return `${auditStore.rootStore.path}\0${backendId}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function markUnavailableRetry(registered: Registered, auditStore: RocksTransactionLogStore, backendId: string): void {
|
|
302
|
+
registered.retryUnavailable.add(backendId);
|
|
303
|
+
retryUnavailableByStore.add(retryUnavailableKey(auditStore, backendId));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function consumeUnavailableRetry(
|
|
307
|
+
registered: Registered,
|
|
308
|
+
auditStore: RocksTransactionLogStore,
|
|
309
|
+
backendId: string
|
|
310
|
+
): boolean {
|
|
311
|
+
const local = registered.retryUnavailable.delete(backendId);
|
|
312
|
+
const storeScoped = retryUnavailableByStore.delete(retryUnavailableKey(auditStore, backendId));
|
|
313
|
+
return local || storeScoped;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function runtimeFor(auditStore: RocksTransactionLogStore): Registered {
|
|
317
|
+
let registered = runtimes.get(auditStore);
|
|
318
|
+
if (registered) return registered;
|
|
319
|
+
const tables = new Map<number, RegisteredTable>();
|
|
320
|
+
const runtime = new DerivedIndexRuntime(
|
|
321
|
+
auditStore,
|
|
322
|
+
(tableId, recordId) => {
|
|
323
|
+
const entry = tables.get(tableId)?.current.Table.primaryStore.getEntry(recordId);
|
|
324
|
+
return entry?.value == null ? undefined : { version: entry.version, value: entry.value };
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
scanRecords: (tableId) =>
|
|
328
|
+
tables
|
|
329
|
+
.get(tableId)!
|
|
330
|
+
.current.Table.primaryStore.getRange({ versions: true, snapshot: false })
|
|
331
|
+
.map(({ key, value, version }) => ({ recordId: key, version, value })),
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
registered = {
|
|
335
|
+
runtime,
|
|
336
|
+
tables,
|
|
337
|
+
backends: new Map(),
|
|
338
|
+
tableBackends: new Map(),
|
|
339
|
+
droppingTables: new Set(),
|
|
340
|
+
retryUnavailable: new Set(),
|
|
341
|
+
};
|
|
342
|
+
runtimes.set(auditStore, registered);
|
|
343
|
+
return registered;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const warnedAuditIndexes = new Set<string>();
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Register every post-commit custom index of a table with the shared derived-index runtime of its
|
|
350
|
+
* database. Returns the release for the table's registrations, or undefined when it has none. Runs
|
|
351
|
+
* on every worker; the runtime elects one owner per index.
|
|
352
|
+
*/
|
|
353
|
+
export function attachDerivedIndexes(Table: any):
|
|
354
|
+
| {
|
|
355
|
+
close(dropping?: boolean): Promise<void>;
|
|
356
|
+
restoreAfterFailedDrop(): ReturnType<typeof attachDerivedIndexes>;
|
|
357
|
+
completeDrop(dropped?: boolean): void;
|
|
358
|
+
}
|
|
359
|
+
| undefined {
|
|
360
|
+
const hnswAttributes = Table.attributes.filter(
|
|
361
|
+
(attribute: any) => attribute.indexed?.type === 'HNSW' && Table.indices[attribute.name]?.customIndex
|
|
362
|
+
);
|
|
363
|
+
const attributes = hnswAttributes.filter((attribute: any) => Table.indices[attribute.name]?.customIndex?.postCommit);
|
|
364
|
+
if (Table.audit !== true && attributes.length > 0) {
|
|
365
|
+
throw new ClientError(
|
|
366
|
+
`Table '${Table.databaseName}.${Table.tableName}' must enable audit logging before using a post-commit derived index`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (!Table.auditStore) return;
|
|
370
|
+
const auditStore = Table.auditStore as RocksTransactionLogStore;
|
|
371
|
+
const registered = runtimeFor(auditStore);
|
|
372
|
+
if (registered.droppingTables.has(Table.tableId)) return;
|
|
373
|
+
for (const attribute of hnswAttributes) {
|
|
374
|
+
const indexStore = Table.indices[attribute.name];
|
|
375
|
+
const id = `hnsw:${indexStore.name}`;
|
|
376
|
+
// A transient JS-backed declaration can precede the first native registration while a
|
|
377
|
+
// schema is being installed. Only an explicit transition away from an already-registered
|
|
378
|
+
// native backend should make a later native registration eligible to rearm an exhausted
|
|
379
|
+
// rebuild budget.
|
|
380
|
+
if (attribute.indexed.nativePlane === false && !indexStore.customIndex.postCommit && registered.backends.has(id))
|
|
381
|
+
markUnavailableRetry(registered, auditStore, id);
|
|
382
|
+
}
|
|
383
|
+
if (attributes.length === 0) return;
|
|
384
|
+
const installed = { Table };
|
|
385
|
+
let registeredTable = registered.tables.get(Table.tableId);
|
|
386
|
+
if (registeredTable) {
|
|
387
|
+
registeredTable.owners.add(installed);
|
|
388
|
+
registeredTable.current = installed;
|
|
389
|
+
} else {
|
|
390
|
+
registeredTable = { current: installed, owners: new Set([installed]) };
|
|
391
|
+
registered.tables.set(Table.tableId, registeredTable);
|
|
392
|
+
}
|
|
393
|
+
const releases: Array<() => Promise<void>> = [];
|
|
394
|
+
const backendIds: string[] = [];
|
|
395
|
+
const registeredBackends = new Map<string, RegisteredBackend>();
|
|
396
|
+
for (const attribute of attributes) {
|
|
397
|
+
const indexStore = Table.indices[attribute.name];
|
|
398
|
+
const index = indexStore.customIndex as DerivedNativeIndex & { postCommit: true };
|
|
399
|
+
const id = `hnsw:${indexStore.name}`;
|
|
400
|
+
const warningKey = `${Table.databaseName}.${Table.tableName}.${indexStore.name}`;
|
|
401
|
+
if (!warnedAuditIndexes.has(warningKey)) {
|
|
402
|
+
warnedAuditIndexes.add(warningKey);
|
|
403
|
+
logger.warn?.(
|
|
404
|
+
`Derived index ${indexStore.name} requires auditing; the audit API retains full record history for the configured retention window`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
const resolver = Table.propertyResolvers?.[attribute.name];
|
|
408
|
+
const label = `Vector for attribute "${attribute.name}"`;
|
|
409
|
+
index.attachDerivedHost({
|
|
410
|
+
readiness: () => registered.runtime.getReadiness(id),
|
|
411
|
+
coverage: (maxLagMilliseconds) =>
|
|
412
|
+
readDerivedIndexCoverage(
|
|
413
|
+
auditStore,
|
|
414
|
+
id,
|
|
415
|
+
() => indexStore.getSync(DERIVED_INDEX_CURSOR_KEY),
|
|
416
|
+
maxLagMilliseconds
|
|
417
|
+
),
|
|
418
|
+
requestRebuild: () => registered.runtime.requestRebuild(id),
|
|
419
|
+
waitForCoverage: (since, timeout, signal) => registered.runtime.waitForCoverage(id, since, timeout, signal),
|
|
420
|
+
});
|
|
421
|
+
let predecessor = registered.backends.get(id);
|
|
422
|
+
const inherited = predecessor;
|
|
423
|
+
const settlePredecessor = () => {
|
|
424
|
+
const current = predecessor;
|
|
425
|
+
if (!current) return Promise.resolve();
|
|
426
|
+
const settled = current.settle();
|
|
427
|
+
settled.then(
|
|
428
|
+
() => {
|
|
429
|
+
if (predecessor === current) predecessor = undefined;
|
|
430
|
+
},
|
|
431
|
+
() => {}
|
|
432
|
+
);
|
|
433
|
+
return settled;
|
|
434
|
+
};
|
|
435
|
+
const predecessorSettled = settlePredecessor();
|
|
436
|
+
predecessorSettled.catch(() => {});
|
|
437
|
+
if (registered.backends.get(id) === inherited) registered.backends.delete(id);
|
|
438
|
+
const release = registered.runtime.register({
|
|
439
|
+
backend: new HnswDerivedIndexBackend(id, index),
|
|
440
|
+
projections: new Map([
|
|
441
|
+
[
|
|
442
|
+
Table.tableId,
|
|
443
|
+
(record: any) => {
|
|
444
|
+
const vector = resolver ? resolver(record) : record[attribute.name];
|
|
445
|
+
if (vector == null) return undefined;
|
|
446
|
+
index.assertDerivedValue(vector, label);
|
|
447
|
+
return vector;
|
|
448
|
+
},
|
|
449
|
+
],
|
|
450
|
+
]),
|
|
451
|
+
options: { maxLagMilliseconds: attribute.indexed?.maxLagMilliseconds ?? DEFAULT_MAX_LAG_MILLISECONDS },
|
|
452
|
+
});
|
|
453
|
+
let settling: Promise<void> | undefined;
|
|
454
|
+
const registeredBackend: RegisteredBackend = {
|
|
455
|
+
tableId: Table.tableId,
|
|
456
|
+
settle: () => {
|
|
457
|
+
if (settling) return settling;
|
|
458
|
+
const attempt = Promise.allSettled([settlePredecessor(), release()]).then((results) => {
|
|
459
|
+
if (results[1].status === 'fulfilled') {
|
|
460
|
+
if (registered.backends.get(id) === registeredBackend) registered.backends.delete(id);
|
|
461
|
+
const tableBackends = registered.tableBackends.get(Table.tableId);
|
|
462
|
+
tableBackends?.delete(registeredBackend);
|
|
463
|
+
if (tableBackends?.size === 0) registered.tableBackends.delete(Table.tableId);
|
|
464
|
+
}
|
|
465
|
+
const failures = results
|
|
466
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
467
|
+
.map((result) => result.reason);
|
|
468
|
+
if (failures.length === 1) throw failures[0];
|
|
469
|
+
if (failures.length) throw new AggregateError(failures, `Could not settle HNSW backend '${id}'`);
|
|
470
|
+
});
|
|
471
|
+
settling = attempt;
|
|
472
|
+
attempt.catch(() => {
|
|
473
|
+
if (settling === attempt) settling = undefined;
|
|
474
|
+
});
|
|
475
|
+
return attempt;
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
let tableBackends = registered.tableBackends.get(Table.tableId);
|
|
479
|
+
if (!tableBackends) registered.tableBackends.set(Table.tableId, (tableBackends = new Set()));
|
|
480
|
+
tableBackends.add(registeredBackend);
|
|
481
|
+
registered.backends.set(id, registeredBackend);
|
|
482
|
+
registeredBackends.set(id, registeredBackend);
|
|
483
|
+
const retryUnavailable = consumeUnavailableRetry(registered, auditStore, id);
|
|
484
|
+
if (retryUnavailable && registered.runtime.getReadiness(id).state === 'unavailable')
|
|
485
|
+
registered.runtime.requestRebuild(id);
|
|
486
|
+
backendIds.push(id);
|
|
487
|
+
releases.push(() => registeredBackend.settle());
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
async close(dropping = false) {
|
|
491
|
+
if (dropping) registered.droppingTables.add(Table.tableId);
|
|
492
|
+
const settlements = dropping
|
|
493
|
+
? [...(registered.tableBackends.get(Table.tableId) ?? [])].map((backend) => backend.settle())
|
|
494
|
+
: releases.map((release) => release());
|
|
495
|
+
const tableRegistration = registered.tables.get(Table.tableId);
|
|
496
|
+
if (tableRegistration) {
|
|
497
|
+
tableRegistration.owners.delete(installed);
|
|
498
|
+
if (tableRegistration.owners.size === 0) {
|
|
499
|
+
registered.tables.delete(Table.tableId);
|
|
500
|
+
} else if (tableRegistration.current === installed) {
|
|
501
|
+
tableRegistration.current = tableRegistration.owners.values().next().value;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (dropping) {
|
|
505
|
+
await Promise.all(settlements);
|
|
506
|
+
} else {
|
|
507
|
+
for (const result of await Promise.allSettled(settlements)) {
|
|
508
|
+
if (result.status === 'rejected')
|
|
509
|
+
logger.error(`Could not settle a superseded HNSW backend for table ${Table.tableId}`, result.reason);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
restoreAfterFailedDrop() {
|
|
514
|
+
registered.droppingTables.delete(Table.tableId);
|
|
515
|
+
for (const [id, registeredBackend] of registeredBackends) {
|
|
516
|
+
const current = registered.backends.get(id);
|
|
517
|
+
if (current && current !== registeredBackend) return;
|
|
518
|
+
}
|
|
519
|
+
return attachDerivedIndexes(Table);
|
|
520
|
+
},
|
|
521
|
+
completeDrop(dropped = true) {
|
|
522
|
+
registered.droppingTables.delete(Table.tableId);
|
|
523
|
+
if (dropped) for (const id of backendIds) markUnavailableRetry(registered, auditStore, id);
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Shared readiness of an index on any worker, registered or not. */
|
|
529
|
+
export function derivedIndexReadiness(auditStore: RocksTransactionLogStore, indexStoreName: string) {
|
|
530
|
+
return readDerivedIndexReadiness(auditStore, `hnsw:${indexStoreName}`);
|
|
531
|
+
}
|