@harperfast/harper 5.3.0-alpha.1 → 5.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cliOperations.ts +12 -0
- package/bin/restart.ts +66 -6
- package/components/Application.ts +1134 -112
- 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/operations.js +284 -52
- package/components/operationsValidation.js +49 -2
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -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/harperBridge/ResourceBridge.ts +28 -2
- 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/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/index.d.ts +1 -0
- package/dist/index.js +4 -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 +21 -5
- 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 +1095 -346
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +10 -3
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +170 -0
- package/dist/resources/auditStore.js +457 -11
- package/dist/resources/auditStore.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 +624 -176
- 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 +848 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
- package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
- package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
- package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
- package/dist/resources/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 +2565 -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/security/jsLoader.js +6 -4
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/REST.js +33 -2
- 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/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.js +96 -17
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- 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 +217 -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 +6 -0
- package/json/systemSchema.json +3 -0
- package/npm-shrinkwrap.json +131 -41
- package/package.json +10 -3
- package/resources/DESIGN.md +124 -19
- package/resources/DatabaseTransaction.ts +230 -17
- package/resources/LMDBTransaction.ts +21 -3
- package/resources/PrimaryRocksDatabase.ts +20 -7
- package/resources/RecordEncoder.ts +25 -5
- 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 +1224 -393
- package/resources/analytics/write.ts +10 -3
- package/resources/auditStore.ts +460 -11
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +610 -146
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +3 -2
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +905 -46
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/recordLock.ts +173 -7
- package/resources/recordLockCoordinator.ts +3043 -0
- package/resources/replayLogs.ts +5 -0
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +169 -49
- package/resources/tracked.ts +73 -22
- package/security/jsLoader.ts +6 -4
- package/server/DESIGN.md +11 -0
- package/server/REST.ts +36 -3
- package/server/http.ts +34 -2
- 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 +143 -24
- 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-D3j-1yY1.js} +1 -1
- package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-BxJGYcfB.js} +3 -3
- package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-CT55oWOe.js} +1 -1
- package/studio/web/assets/{applications-DHxGi7JH.js → applications-D9Ct9_vm.js} +1 -1
- package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-DV8H7VwA.js} +1 -1
- package/studio/web/assets/{editor-DNcRHK54.js → editor-uatc0unt.js} +1 -1
- package/studio/web/assets/{html-Bdssedlg.js → html-Bm6D6paN.js} +1 -1
- package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CEn7tpLG.js} +1 -1
- package/studio/web/assets/{index-D6sxmFLR.js → index-BIXW6Pu4.js} +5 -5
- package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-UI7L-Vrk.js} +1 -1
- package/studio/web/assets/{javascript-B8meVSTH.js → javascript-CJ0G3AFZ.js} +1 -1
- package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-DQADAYEa.js} +1 -1
- package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-CAQJXWcI.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
- package/studio/web/assets/{notifications-CQf18QKb.js → notifications-BbxTU6Aw.js} +1 -1
- package/studio/web/assets/{notifications-CvZivSbh.js → notifications-Cvb3P1lB.js} +1 -1
- package/studio/web/assets/{profile-DdOwtntb.js → profile-Yyb7gsvL.js} +1 -1
- package/studio/web/assets/{regions-n69fwagr.js → regions-OgjGHlU5.js} +1 -1
- package/studio/web/assets/{register-PfWTCXWB.js → register-6qwNEOY3.js} +2 -2
- package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BilDMtgB.js} +1 -1
- package/studio/web/assets/{setup-CUx_aUDl.js → setup-J6qJ7OIU.js} +2 -2
- package/studio/web/assets/{status-D7BVKqX9.js → status-0RWGcfyD.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-BIn-vErT.js} +1 -1
- package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-DgUXku4d.js} +1 -1
- package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-C9orXcsM.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-BEoXXbUB.js} +1 -1
- package/studio/web/assets/{workers-pR3jRY9D.js → workers-JVzSDmgx.js} +1 -1
- package/studio/web/assets/{xml-2iRnMhQO.js → xml-Cq-S8S4X.js} +1 -1
- package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-sfoRdh1M.js} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/errors/hdbError.ts +54 -0
- package/utility/hdbTerms.ts +2 -0
- package/utility/logging/harper_logger.ts +209 -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,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
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { closeSync, fsyncSync, openSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { loggerWithTag } from '../../utility/logging/logger.ts';
|
|
4
|
+
|
|
5
|
+
const logger = loggerWithTag('HNSW');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parallel arrays, ascending by distance: hit i is (ids[i], distances[i]) with its host key at
|
|
9
|
+
* keys.subarray(keyEnds[i - 1] ?? 0, keyEnds[i]); an empty key means the node vanished mid-query.
|
|
10
|
+
*/
|
|
11
|
+
export interface PlaneSearchHits {
|
|
12
|
+
ids: Uint32Array;
|
|
13
|
+
distances: Float32Array;
|
|
14
|
+
keys: Buffer;
|
|
15
|
+
keyEnds: Uint32Array;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A predicate batch: candidate ids and their host keys, laid out as in PlaneSearchHits. */
|
|
19
|
+
export type PlanePredicate = (ids: number[], keys: Buffer, keyEnds: Uint32Array) => Uint8Array;
|
|
20
|
+
|
|
21
|
+
/** NAPI surface of the native file-primary HNSW index (`@harperfast/hnsw`). */
|
|
22
|
+
export interface HnswPlane {
|
|
23
|
+
readonly dims: number;
|
|
24
|
+
readonly layer0Cap: number;
|
|
25
|
+
readonly keyCap: number;
|
|
26
|
+
insert(vector: Float32Array, key?: Buffer): number;
|
|
27
|
+
remove(id: number): void;
|
|
28
|
+
writeNodeRaw(
|
|
29
|
+
id: number,
|
|
30
|
+
level: number,
|
|
31
|
+
vector: Buffer,
|
|
32
|
+
scale: number,
|
|
33
|
+
invMag: number,
|
|
34
|
+
neighbors: Uint32Array,
|
|
35
|
+
upper: Uint32Array[] | null
|
|
36
|
+
): void;
|
|
37
|
+
clearNode(id: number): void;
|
|
38
|
+
setEntryPoint(id: number, level: number): void;
|
|
39
|
+
getEntryPoint(): number[];
|
|
40
|
+
search(
|
|
41
|
+
vector: Float32Array,
|
|
42
|
+
k: number,
|
|
43
|
+
ef: number,
|
|
44
|
+
filter?: Uint8Array | null,
|
|
45
|
+
filterExpansion?: number | null
|
|
46
|
+
): Promise<PlaneSearchHits>;
|
|
47
|
+
searchWithPredicate(
|
|
48
|
+
vector: Float32Array,
|
|
49
|
+
k: number,
|
|
50
|
+
ef: number,
|
|
51
|
+
predicate: PlanePredicate,
|
|
52
|
+
filterExpansion?: number | null,
|
|
53
|
+
visitBudget?: number | null
|
|
54
|
+
): Promise<PlaneSearchHits>;
|
|
55
|
+
searchSync(vector: Float32Array, k: number, ef: number): PlaneSearchHits;
|
|
56
|
+
writeNodeRawIfAbsent(
|
|
57
|
+
id: number,
|
|
58
|
+
level: number,
|
|
59
|
+
vector: Buffer,
|
|
60
|
+
scale: number,
|
|
61
|
+
invMag: number,
|
|
62
|
+
neighbors: Uint32Array,
|
|
63
|
+
upper: Uint32Array[] | null
|
|
64
|
+
): boolean;
|
|
65
|
+
openedClean(): boolean;
|
|
66
|
+
idHighWater(): number;
|
|
67
|
+
getWatermark(): number;
|
|
68
|
+
setWatermark(txn: number): void;
|
|
69
|
+
flush(watermark?: number): void;
|
|
70
|
+
flushAsync(watermark?: number): Promise<void>;
|
|
71
|
+
invalidateFile(): PlaneInvalidationOutcome;
|
|
72
|
+
invalidated(): boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface HnswPlaneConstructor {
|
|
76
|
+
readonly prototype: HnswPlane;
|
|
77
|
+
create(path: string, dims: number, layer0Cap: number, maxNodes: number, keyCap?: number): HnswPlane;
|
|
78
|
+
open(path: string): HnswPlane;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface PlaneInvalidationOutcome {
|
|
82
|
+
inBand: boolean;
|
|
83
|
+
sidecar: boolean;
|
|
84
|
+
inBandError?: string;
|
|
85
|
+
sidecarError?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface HnswPlanePackage {
|
|
89
|
+
Plane: HnswPlaneConstructor;
|
|
90
|
+
invalidatePlane(path: string): PlaneInvalidationOutcome;
|
|
91
|
+
stalePathFor(path: string): string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Entry-point id meaning "none" (u32::MAX in the plane header). */
|
|
95
|
+
export const PLANE_NO_ID = 0xffffffff;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Where an index's plane file lives: next to its store, named by the dbiKey
|
|
99
|
+
* (`table/attribute`, flattened to a single file name). Exposed separately from the index
|
|
100
|
+
* instance so crash-recovery drop paths can remove the file without opening the index.
|
|
101
|
+
*/
|
|
102
|
+
export function planeFilePathFor(storePath: string, storeName: string): string {
|
|
103
|
+
// encodeURIComponent is injective and never emits a path separator: table `a` attribute
|
|
104
|
+
// `b.c` and table `a.b` attribute `c` must not share a plane file (dot-flattening let two
|
|
105
|
+
// indexes serve each other's node ids as their own primary keys)
|
|
106
|
+
return join(storePath, `${encodeURIComponent(storeName)}.hnsw`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Tombstone marking a plane file that could not be deleted (e.g. Windows EBUSY while still
|
|
111
|
+
* mapped). Its presence means the plane file is STALE: never open it — delete both when
|
|
112
|
+
* possible and rebuild.
|
|
113
|
+
*/
|
|
114
|
+
export function planeStalePathFor(planePath: string): string {
|
|
115
|
+
// the package owns the convention; the literal covers the calls that precede its load
|
|
116
|
+
return binding?.stalePathFor(planePath) ?? `${planePath}.stale`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let binding: HnswPlanePackage | null | undefined;
|
|
120
|
+
let bindingLoadError: Error | undefined;
|
|
121
|
+
let bindingWarningLogged = false;
|
|
122
|
+
|
|
123
|
+
function getHnswPackage(warn = true): HnswPlanePackage | null {
|
|
124
|
+
if (binding === undefined) {
|
|
125
|
+
try {
|
|
126
|
+
binding = require('@harperfast/hnsw') as HnswPlanePackage;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
binding = null;
|
|
129
|
+
bindingLoadError = error as Error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (binding === null && warn && !bindingWarningLogged) {
|
|
133
|
+
bindingWarningLogged = true;
|
|
134
|
+
logger.warn?.(
|
|
135
|
+
`The @harperfast/hnsw native module is not available (${bindingLoadError?.message}); ` +
|
|
136
|
+
'indexes with nativePlane enabled will remain unavailable until the module can load'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return binding;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The native plane constructor, or null when the compiled artifact is unavailable (warns once by default). */
|
|
143
|
+
export function getPlaneBinding(warn = true): HnswPlaneConstructor | null {
|
|
144
|
+
return getHnswPackage(warn)?.Plane ?? null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Make a derived plane unadoptable before it is replaced or removed. */
|
|
148
|
+
export function invalidatePlaneFile(filePath: string, attached?: HnswPlane | null): PlaneInvalidationOutcome {
|
|
149
|
+
if (attached) return attached.invalidateFile();
|
|
150
|
+
const hnswPackage = getHnswPackage();
|
|
151
|
+
if (hnswPackage) return hnswPackage.invalidatePlane(filePath);
|
|
152
|
+
// A plane can outlive the package that made it (uninstall, or a prebuild that stopped
|
|
153
|
+
// loading), and a reinstall would then adopt it. Only the sidecar is reachable without the
|
|
154
|
+
// package, and it has to survive a power loss to be worth writing, so fsync it and the
|
|
155
|
+
// directory entry that names it — the same durability the package's own sidecar has.
|
|
156
|
+
const fd = openSync(planeStalePathFor(filePath), 'w');
|
|
157
|
+
try {
|
|
158
|
+
fsyncSync(fd);
|
|
159
|
+
} finally {
|
|
160
|
+
closeSync(fd);
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const dirFd = openSync(dirname(filePath), 'r');
|
|
164
|
+
try {
|
|
165
|
+
fsyncSync(dirFd);
|
|
166
|
+
} finally {
|
|
167
|
+
closeSync(dirFd);
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Windows cannot open a directory as a file; it also does not need this — the metadata
|
|
171
|
+
// journal already orders the create ahead of anything that could read the sidecar
|
|
172
|
+
}
|
|
173
|
+
return { inBand: false, sidecar: true };
|
|
174
|
+
}
|