@harperfast/harper 5.3.0-beta.3 → 5.3.0-beta.4
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 +25 -3
- package/bin/run.ts +18 -0
- package/components/Application.ts +52 -78
- package/components/DESIGN.md +76 -3
- package/components/Scope.ts +10 -4
- package/components/anthropic/index.ts +10 -1
- package/components/bedrock/index.ts +10 -1
- package/components/componentLoader.ts +23 -14
- package/components/mcp/tools/operations.ts +3 -1
- package/components/ollama/index.ts +10 -1
- package/components/openai/index.ts +265 -18
- package/components/operations.js +40 -1
- package/config/configUtils.ts +3 -19
- package/dataLayer/DESIGN.md +29 -5
- package/dataLayer/harperBridge/ResourceBridge.ts +39 -3
- package/dataLayer/restoreMarker.ts +250 -14
- package/dataLayer/schema.ts +2 -7
- package/dataLayer/schemaDescribe.ts +10 -0
- package/dist/bin/cliOperations.js +28 -3
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/run.js +19 -0
- package/dist/bin/run.js.map +1 -1
- package/dist/components/Application.d.ts +8 -2
- package/dist/components/Application.js +43 -69
- package/dist/components/Application.js.map +1 -1
- package/dist/components/Scope.d.ts +1 -1
- package/dist/components/Scope.js +9 -4
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/anthropic/index.d.ts +0 -10
- package/dist/components/anthropic/index.js +9 -1
- package/dist/components/anthropic/index.js.map +1 -1
- package/dist/components/bedrock/index.d.ts +0 -12
- package/dist/components/bedrock/index.js +9 -1
- package/dist/components/bedrock/index.js.map +1 -1
- package/dist/components/componentLoader.js +23 -14
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +1 -1
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/ollama/index.d.ts +0 -13
- package/dist/components/ollama/index.js +9 -1
- package/dist/components/ollama/index.js.map +1 -1
- package/dist/components/openai/index.d.ts +11 -15
- package/dist/components/openai/index.js +224 -12
- package/dist/components/openai/index.js.map +1 -1
- package/dist/components/operations.d.ts +9 -0
- package/dist/components/operations.js +32 -2
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.d.ts +1 -2
- package/dist/config/configUtils.js +7 -22
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +29 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +25 -3
- package/dist/dataLayer/restoreMarker.js +213 -13
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/dataLayer/schema.js +1 -4
- package/dist/dataLayer/schema.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +10 -0
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +28 -0
- package/dist/resources/DatabaseTransaction.d.ts +21 -1
- package/dist/resources/DatabaseTransaction.js +147 -8
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +1 -1
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/Table.d.ts +31 -3
- package/dist/resources/Table.js +397 -79
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/auditStore.js +16 -10
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +1 -0
- package/dist/resources/blob.js +14 -5
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/branchDatabase.js +3 -3
- package/dist/resources/branchDatabase.js.map +1 -1
- package/dist/resources/databaseDropPreparation.d.ts +20 -0
- package/dist/resources/databaseDropPreparation.js +106 -0
- package/dist/resources/databaseDropPreparation.js.map +1 -0
- package/dist/resources/databases.d.ts +26 -21
- package/dist/resources/databases.js +1040 -204
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +23 -0
- package/dist/resources/derivedIndexRegistry.js +92 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -1
- package/dist/resources/derivedIndexRuntime.d.ts +10 -0
- package/dist/resources/derivedIndexRuntime.js +136 -25
- package/dist/resources/derivedIndexRuntime.js.map +1 -1
- package/dist/resources/derivedIndexes.d.ts +39 -0
- package/dist/resources/derivedIndexes.js +733 -0
- package/dist/resources/derivedIndexes.js.map +1 -0
- package/dist/resources/fullTextSchema.d.ts +8 -0
- package/dist/resources/fullTextSchema.js +42 -0
- package/dist/resources/fullTextSchema.js.map +1 -1
- package/dist/resources/fullTextSchemaLifecycle.d.ts +0 -5
- package/dist/resources/fullTextSchemaLifecycle.js +0 -9
- package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
- package/dist/resources/graphql.js +101 -9
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +0 -10
- package/dist/resources/indexes/hnswDerivedIndex.js +0 -230
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -1
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +3 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +57 -8
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
- package/dist/resources/models/Models.d.ts +26 -3
- package/dist/resources/models/Models.js +256 -13
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/TestBackend.d.ts +3 -1
- package/dist/resources/models/TestBackend.js +35 -1
- package/dist/resources/models/TestBackend.js.map +1 -1
- package/dist/resources/models/analyticsTable.d.ts +2 -2
- package/dist/resources/models/analyticsTable.js +4 -2
- package/dist/resources/models/analyticsTable.js.map +1 -1
- package/dist/resources/models/backendHelpers.d.ts +24 -0
- package/dist/resources/models/backendHelpers.js +18 -1
- package/dist/resources/models/backendHelpers.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +10 -21
- package/dist/resources/models/backendRegistry.js +74 -18
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +7 -0
- package/dist/resources/models/bootstrap.js +36 -4
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/models/decideHook.d.ts +33 -0
- package/dist/resources/models/decideHook.js +105 -0
- package/dist/resources/models/decideHook.js.map +1 -0
- package/dist/resources/models/decision.d.ts +64 -0
- package/dist/resources/models/decision.js +507 -0
- package/dist/resources/models/decision.js.map +1 -0
- package/dist/resources/models/decisionStore.d.ts +134 -0
- package/dist/resources/models/decisionStore.js +350 -0
- package/dist/resources/models/decisionStore.js.map +1 -0
- package/dist/resources/models/embedHook.d.ts +66 -8
- package/dist/resources/models/embedHook.js +228 -61
- package/dist/resources/models/embedHook.js.map +1 -1
- package/dist/resources/models/generativeDecision.d.ts +58 -0
- package/dist/resources/models/generativeDecision.js +315 -0
- package/dist/resources/models/generativeDecision.js.map +1 -0
- package/dist/resources/models/types.d.ts +232 -4
- package/dist/security/authn/oidc/tokenExchange.js +3 -33
- package/dist/security/authn/oidc/tokenExchange.js.map +1 -1
- package/dist/security/authn/oidc/tokenUseTable.d.ts +9 -0
- package/dist/security/authn/oidc/tokenUseTable.js +28 -0
- package/dist/security/authn/oidc/tokenUseTable.js.map +1 -0
- package/dist/security/certificateVerification/certificateVerificationSource.d.ts +0 -1
- package/dist/security/certificateVerification/certificateVerificationSource.js +3 -3
- package/dist/security/certificateVerification/certificateVerificationSource.js.map +1 -1
- package/dist/security/certificateVerification/crlVerification.js +77 -165
- package/dist/security/certificateVerification/crlVerification.js.map +1 -1
- package/dist/security/certificateVerification/types.d.ts +0 -3
- package/dist/security/certificateVerification/verificationTables.d.ts +19 -0
- package/dist/security/certificateVerification/verificationTables.js +125 -0
- package/dist/security/certificateVerification/verificationTables.js.map +1 -0
- package/dist/security/certificateVerification/verificationUtils.d.ts +1 -1
- package/dist/security/certificateVerification/verificationUtils.js +6 -31
- package/dist/security/certificateVerification/verificationUtils.js.map +1 -1
- package/dist/security/jsLoader.d.ts +5 -0
- package/dist/security/jsLoader.js +8 -0
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/Server.d.ts +1 -0
- package/dist/server/Server.js.map +1 -1
- package/dist/server/http.js +2 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +21 -2
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobOwnership.d.ts +25 -0
- package/dist/server/jobs/jobOwnership.js +127 -0
- package/dist/server/jobs/jobOwnership.js.map +1 -0
- package/dist/server/jobs/jobProcess.js +9 -2
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobs.js +18 -5
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/loadRootComponents.js +10 -0
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +11 -1
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +12 -2
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/threads/itc.d.ts +4 -1
- package/dist/server/threads/itc.js +29 -6
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +12 -4
- package/dist/server/threads/manageThreads.js +241 -119
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/threadServer.d.ts +1 -0
- package/dist/server/threads/threadServer.js +15 -1
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/server/threads/windowsProcessTree.d.ts +91 -0
- package/dist/server/threads/windowsProcessTree.js +361 -0
- package/dist/server/threads/windowsProcessTree.js.map +1 -0
- package/dist/server/throttle.d.ts +1 -1
- package/dist/server/throttle.js +8 -1
- package/dist/server/throttle.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.js +53 -46
- package/dist/upgrade/directives/5-3-0.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +10 -1
- package/dist/utility/errors/hdbError.js +55 -11
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/fsync.d.ts +2 -0
- package/dist/utility/fsync.js +20 -0
- package/dist/utility/fsync.js.map +1 -0
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +3 -0
- package/dist/utility/hdbTerms.js +3 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/signalling.d.ts +7 -1
- package/dist/utility/signalling.js +27 -3
- package/dist/utility/signalling.js.map +1 -1
- package/dist/utility/when.d.ts +1 -0
- package/dist/utility/when.js +21 -0
- package/dist/utility/when.js.map +1 -1
- package/dist/validation/configValidator.js +30 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +13 -0
- package/json/systemSchema.json +28 -0
- package/npm-shrinkwrap.json +151 -55
- package/package.json +14 -13
- package/resources/DESIGN.md +80 -100
- package/resources/DatabaseTransaction.ts +157 -10
- package/resources/PrimaryRocksDatabase.ts +2 -2
- package/resources/Table.ts +494 -101
- package/resources/auditStore.ts +18 -10
- package/resources/blob.ts +13 -6
- package/resources/branchDatabase.ts +3 -3
- package/resources/databaseDropPreparation.ts +120 -0
- package/resources/databases.ts +1155 -205
- package/resources/derivedIndexRegistry.ts +122 -0
- package/resources/derivedIndexRuntime.ts +157 -30
- package/resources/derivedIndexes.ts +856 -0
- package/resources/fullTextSchema.ts +55 -0
- package/resources/fullTextSchemaLifecycle.ts +0 -15
- package/resources/graphql.ts +107 -12
- package/resources/indexes/DESIGN.md +7 -0
- package/resources/indexes/hnswDerivedIndex.ts +0 -260
- package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +72 -9
- package/resources/models/DESIGN.md +39 -0
- package/resources/models/Models.ts +361 -18
- package/resources/models/TestBackend.ts +56 -1
- package/resources/models/analyticsTable.ts +6 -4
- package/resources/models/backendHelpers.ts +17 -0
- package/resources/models/backendRegistry.ts +76 -21
- package/resources/models/bootstrap.ts +57 -7
- package/resources/models/decideHook.ts +135 -0
- package/resources/models/decision.ts +505 -0
- package/resources/models/decisionStore.ts +419 -0
- package/resources/models/embedHook.ts +272 -55
- package/resources/models/generativeDecision.ts +459 -0
- package/resources/models/types.ts +231 -4
- package/security/DESIGN.md +22 -2
- package/security/authn/oidc/tokenExchange.ts +6 -34
- package/security/authn/oidc/tokenUseTable.ts +27 -0
- package/security/certificateVerification/certificateVerificationSource.ts +2 -3
- package/security/certificateVerification/crlVerification.ts +96 -174
- package/security/certificateVerification/types.ts +0 -3
- package/security/certificateVerification/verificationTables.ts +124 -0
- package/security/certificateVerification/verificationUtils.ts +8 -32
- package/security/jsLoader.ts +9 -1
- package/server/DESIGN.md +27 -1
- package/server/Server.ts +2 -1
- package/server/http.ts +4 -2
- package/server/itc/serverHandlers.js +29 -1
- package/server/jobs/jobOwnership.ts +89 -0
- package/server/jobs/jobProcess.ts +8 -2
- package/server/jobs/jobs.ts +15 -4
- package/server/loadRootComponents.js +11 -0
- package/server/serverHelpers/contentTypes.ts +13 -1
- package/server/serverHelpers/serverHandlers.js +11 -2
- package/server/threads/itc.js +32 -6
- package/server/threads/manageThreads.js +268 -122
- package/server/threads/threadServer.js +13 -1
- package/server/threads/windowsProcessTree.ts +409 -0
- package/server/throttle.ts +11 -1
- package/studio/web/assets/{Chat-CveGbxe6.js → Chat-Bq60AO-g.js} +1 -1
- package/studio/web/assets/{FloatingChat-IpCFgYWL.js → FloatingChat-D4560T0p.js} +3 -3
- package/studio/web/assets/{apiToken-CS8fFTgL.js → apiToken-BEvbFPMc.js} +1 -1
- package/studio/web/assets/{applications-D7F4Jopl.js → applications-CSUVTH4E.js} +1 -1
- package/studio/web/assets/{cssMode-Daw_oLPq.js → cssMode-DJyc9mHj.js} +1 -1
- package/studio/web/assets/{editor-B3Bs1zWX.js → editor-DDIsQfj6.js} +1 -1
- package/studio/web/assets/{html-CDaQwWiE.js → html-g1BJHxLy.js} +1 -1
- package/studio/web/assets/{htmlMode-Cb6emO4U.js → htmlMode-Vh5IglCQ.js} +1 -1
- package/studio/web/assets/{index-B4g2Fu_2.js → index-DGRAzvO4.js} +5 -5
- package/studio/web/assets/{index.lazy-NwhvF1q1.js → index.lazy-vgSic7et.js} +1 -1
- package/studio/web/assets/{javascript-2_86gB9Q.js → javascript-5VIolqf7.js} +1 -1
- package/studio/web/assets/{jsonMode-DX-i86uQ.js → jsonMode-DG4zQraI.js} +1 -1
- package/studio/web/assets/{languageServices-B85WN-Ej.js → languageServices-t5FBpX-i.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-JCWHIsKg.js → lspLanguageFeatures-DgVnNPh-.js} +1 -1
- package/studio/web/assets/{notifications-D9ejzt52.js → notifications-BeWcX04m.js} +1 -1
- package/studio/web/assets/{notifications-B7hjKhai.js → notifications-DUGWrWoO.js} +1 -1
- package/studio/web/assets/{profile-D8B6No3F.js → profile-BvUEDBzW.js} +1 -1
- package/studio/web/assets/{regions-DFazIfTK.js → regions-CB-_2m7M.js} +1 -1
- package/studio/web/assets/{register-CwFLviLF.js → register-1w1Zr3b2.js} +2 -2
- package/studio/web/assets/{setComponentFile-VnJ80SSJ.js → setComponentFile-z4-qJCbA.js} +1 -1
- package/studio/web/assets/{setup-BYQxFy-C.js → setup-CS50uJ4x.js} +2 -2
- package/studio/web/assets/{status-Cn5XjmsL.js → status-CI0wOdOE.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-g9xIh6TX.js → toggleHighContrast-BsAE4jPe.js} +1 -1
- package/studio/web/assets/{tsMode-EC-n0Wqc.js → tsMode-D83Gg3cq.js} +1 -1
- package/studio/web/assets/{typescript-DHmM9JCO.js → typescript-Dfp-GrNq.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-BUdnVSVv.js → useEntityRestURL-BKetlINf.js} +1 -1
- package/studio/web/assets/{workers-BVl9bBB-.js → workers-CCvmDLH3.js} +1 -1
- package/studio/web/assets/{xml-C4_sqOkX.js → xml-06pZp00U.js} +1 -1
- package/studio/web/assets/{yaml-CVkbDqtW.js → yaml-iSnWMVlV.js} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-3-0.ts +61 -54
- package/utility/DESIGN.md +5 -1
- package/utility/errors/hdbError.ts +56 -8
- package/utility/fsync.ts +16 -0
- package/utility/hdbTerms.ts +3 -0
- package/utility/signalling.ts +33 -4
- package/utility/when.ts +22 -0
- package/validation/configValidator.ts +36 -7
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
import { ClientError } from '../utility/errors/hdbError.ts';
|
|
2
|
+
import { loggerWithTag } from '../utility/logging/logger.ts';
|
|
3
|
+
import { RocksDatabase } from '@harperfast/rocksdb-js';
|
|
4
|
+
import type { RocksTransactionLogStore } from './RocksTransactionLogStore.ts';
|
|
5
|
+
import {
|
|
6
|
+
acquireFullTextRetirementFence,
|
|
7
|
+
registerDerivedIndexTables,
|
|
8
|
+
waitForFullTextRetirement,
|
|
9
|
+
waitForFullTextRetirementLease,
|
|
10
|
+
} from './derivedIndexRegistry.ts';
|
|
11
|
+
import {
|
|
12
|
+
DerivedIndexRuntime,
|
|
13
|
+
publishDerivedIndexReadiness,
|
|
14
|
+
publishDerivedIndexUnavailableIfUnknown,
|
|
15
|
+
readDerivedIndexCoverage,
|
|
16
|
+
readDerivedIndexReadiness,
|
|
17
|
+
retryDerivedIndexUnavailable,
|
|
18
|
+
} from './derivedIndexRuntime.ts';
|
|
19
|
+
import {
|
|
20
|
+
DERIVED_INDEX_CURSOR_KEY,
|
|
21
|
+
HnswDerivedIndexBackend,
|
|
22
|
+
type DerivedNativeIndex,
|
|
23
|
+
} from './indexes/hnswDerivedIndex.ts';
|
|
24
|
+
import {
|
|
25
|
+
createNativeFullTextDerivedIndexBackend,
|
|
26
|
+
retireNativeFullTextDerivedIndexStorage,
|
|
27
|
+
} from './indexes/nativeFullTextDerivedIndexLifecycle.ts';
|
|
28
|
+
import type { NativeFullTextModule } from './indexes/fullTextNativeBinding.ts';
|
|
29
|
+
import { fullTextStorageDefinition, type FullTextDefinition } from './fullTextSchema.ts';
|
|
30
|
+
|
|
31
|
+
const logger = loggerWithTag('HNSW');
|
|
32
|
+
const fullTextLogger = loggerWithTag('fulltext-derived-index');
|
|
33
|
+
const derivedIndexLogger = loggerWithTag('derived-index');
|
|
34
|
+
const DEFAULT_MAX_LAG_MILLISECONDS = 30_000;
|
|
35
|
+
const DEFAULT_FULL_TEXT_RETIREMENT_RETRY_MILLISECONDS = 70_000;
|
|
36
|
+
const FULL_TEXT_LIMITS = Object.freeze({
|
|
37
|
+
indexingThreads: 1,
|
|
38
|
+
searchThreads: 1,
|
|
39
|
+
writerMemoryBytes: 32 * 1024 * 1024,
|
|
40
|
+
maxQueuedCommands: 16,
|
|
41
|
+
maxQueuedBytes: 64 * 1024 * 1024,
|
|
42
|
+
maxBatchBytes: 8 * 1024 * 1024,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
type RegisteredTable = { current: { Table: any }; owners: Set<{ Table: any }> };
|
|
46
|
+
type RegisteredBackend = {
|
|
47
|
+
settle: () => Promise<void>;
|
|
48
|
+
};
|
|
49
|
+
type BackendHandoff = {
|
|
50
|
+
settlePredecessor: () => Promise<void>;
|
|
51
|
+
predecessorSettled: Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
type Registered = {
|
|
54
|
+
runtime: DerivedIndexRuntime;
|
|
55
|
+
tables: Map<number, RegisteredTable>;
|
|
56
|
+
backends: Map<string, RegisteredBackend>;
|
|
57
|
+
tableBackends: Map<number, Set<RegisteredBackend>>;
|
|
58
|
+
droppingTables: Set<number>;
|
|
59
|
+
retryUnavailable: Set<string>;
|
|
60
|
+
};
|
|
61
|
+
type DerivedIndexAttachment = {
|
|
62
|
+
close(dropping?: boolean): Promise<void>;
|
|
63
|
+
fullTextDefinitions(): readonly FullTextDefinition[];
|
|
64
|
+
matchesCurrent(): boolean;
|
|
65
|
+
retryUnavailableFullText(): void;
|
|
66
|
+
restoreAfterFailedDrop(): DerivedIndexAttachment | undefined;
|
|
67
|
+
retireAfterConfirmedDrop(definitions?: readonly Pick<FullTextDefinition, 'name'>[]): Promise<boolean>;
|
|
68
|
+
completeDrop(dropped?: boolean): void;
|
|
69
|
+
};
|
|
70
|
+
type FullTextTestConfiguration = {
|
|
71
|
+
binding: NativeFullTextModule;
|
|
72
|
+
closeTimeoutMilliseconds: number;
|
|
73
|
+
shutdownTimeoutMilliseconds: number;
|
|
74
|
+
runnerOptions: {
|
|
75
|
+
rebuildBackoffMilliseconds: number;
|
|
76
|
+
maxRebuildBackoffMilliseconds: number;
|
|
77
|
+
maxRebuildAttempts: number;
|
|
78
|
+
lockRetryMilliseconds: number;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
const runtimes = new WeakMap<object, Registered>();
|
|
82
|
+
const suspendedActivation = new WeakMap<object, number>();
|
|
83
|
+
const PERMANENTLY_SUSPENDED = -1;
|
|
84
|
+
const retryUnavailableByStore = new WeakMap<object, Set<string>>();
|
|
85
|
+
let fullTextTestConfiguration: FullTextTestConfiguration | undefined;
|
|
86
|
+
|
|
87
|
+
/** Prevent new derived-index attachments while a root store is being torn down. */
|
|
88
|
+
export function suspendDerivedIndexActivation(rootStore: object): () => void {
|
|
89
|
+
const current = suspendedActivation.get(rootStore) ?? 0;
|
|
90
|
+
if (current !== PERMANENTLY_SUSPENDED) suspendedActivation.set(rootStore, current + 1);
|
|
91
|
+
let released = false;
|
|
92
|
+
return () => {
|
|
93
|
+
if (released) return;
|
|
94
|
+
released = true;
|
|
95
|
+
const current = suspendedActivation.get(rootStore);
|
|
96
|
+
if (current === PERMANENTLY_SUSPENDED) return;
|
|
97
|
+
const remaining = (current ?? 1) - 1;
|
|
98
|
+
if (remaining > 0) suspendedActivation.set(rootStore, remaining);
|
|
99
|
+
else suspendedActivation.delete(rootStore);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Prevent abandoned root wrappers from reattaching derived indexes after a failed native close. */
|
|
104
|
+
export function permanentlySuspendDerivedIndexActivation(rootStore: object): void {
|
|
105
|
+
suspendedActivation.set(rootStore, PERMANENTLY_SUSPENDED);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function activationSuspended(Table: any): boolean {
|
|
109
|
+
const rootStore = Table.primaryStore?.rootStore;
|
|
110
|
+
// Closed roots no longer retain an active suspension count, but stale table classes must not
|
|
111
|
+
// resurrect their derived-index runtime after successful teardown.
|
|
112
|
+
return rootStore != null && (rootStore.status === 'closed' || suspendedActivation.has(rootStore));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function markUnavailableRetry(registered: Registered, auditStore: RocksTransactionLogStore, backendId: string): void {
|
|
116
|
+
registered.retryUnavailable.add(backendId);
|
|
117
|
+
const rootStore = auditStore.rootStore;
|
|
118
|
+
let retries = retryUnavailableByStore.get(rootStore);
|
|
119
|
+
if (!retries) retryUnavailableByStore.set(rootStore, (retries = new Set()));
|
|
120
|
+
retries.add(backendId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function consumeUnavailableRetry(
|
|
124
|
+
registered: Registered,
|
|
125
|
+
auditStore: RocksTransactionLogStore,
|
|
126
|
+
backendId: string
|
|
127
|
+
): boolean {
|
|
128
|
+
const local = registered.retryUnavailable.delete(backendId);
|
|
129
|
+
const rootStore = auditStore.rootStore;
|
|
130
|
+
const retries = retryUnavailableByStore.get(rootStore);
|
|
131
|
+
const storeScoped = retries?.delete(backendId) ?? false;
|
|
132
|
+
if (retries?.size === 0) retryUnavailableByStore.delete(rootStore);
|
|
133
|
+
return local || storeScoped;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function runtimeFor(auditStore: RocksTransactionLogStore): Registered {
|
|
137
|
+
let registered = runtimes.get(auditStore);
|
|
138
|
+
if (registered) return registered;
|
|
139
|
+
const tables = new Map<number, RegisteredTable>();
|
|
140
|
+
const runtime = new DerivedIndexRuntime(
|
|
141
|
+
auditStore,
|
|
142
|
+
(tableId, recordId) => {
|
|
143
|
+
const entry = tables.get(tableId)?.current.Table.primaryStore.getEntry(recordId);
|
|
144
|
+
return entry?.value == null ? undefined : { version: entry.version, value: entry.value, size: entry.size };
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
scanRecords: (tableId) => {
|
|
148
|
+
const Table = tables.get(tableId)?.current.Table;
|
|
149
|
+
if (!Table) throw new Error(`Derived-index table ${tableId} is no longer registered`);
|
|
150
|
+
return Table.primaryStore
|
|
151
|
+
.getRange({ versions: true, snapshot: false })
|
|
152
|
+
.map(({ key, value, version, size }) => ({ recordId: key, version, value, size }));
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
registered = {
|
|
157
|
+
runtime,
|
|
158
|
+
tables,
|
|
159
|
+
backends: new Map(),
|
|
160
|
+
tableBackends: new Map(),
|
|
161
|
+
droppingTables: new Set(),
|
|
162
|
+
retryUnavailable: new Set(),
|
|
163
|
+
};
|
|
164
|
+
runtimes.set(auditStore, registered);
|
|
165
|
+
return registered;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function beginBackendHandoff(registered: Registered, id: string): BackendHandoff {
|
|
169
|
+
let predecessor = registered.backends.get(id);
|
|
170
|
+
const inherited = predecessor;
|
|
171
|
+
const settlePredecessor = () => {
|
|
172
|
+
const current = predecessor;
|
|
173
|
+
if (!current) return Promise.resolve();
|
|
174
|
+
const settled = current.settle();
|
|
175
|
+
settled.then(
|
|
176
|
+
() => {
|
|
177
|
+
if (predecessor === current) predecessor = undefined;
|
|
178
|
+
},
|
|
179
|
+
() => {}
|
|
180
|
+
);
|
|
181
|
+
return settled;
|
|
182
|
+
};
|
|
183
|
+
const predecessorSettled = settlePredecessor();
|
|
184
|
+
predecessorSettled.catch(() => {});
|
|
185
|
+
if (registered.backends.get(id) === inherited) registered.backends.delete(id);
|
|
186
|
+
return { settlePredecessor, predecessorSettled };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function registerBackend(
|
|
190
|
+
registered: Registered,
|
|
191
|
+
tableId: number,
|
|
192
|
+
id: string,
|
|
193
|
+
label: string,
|
|
194
|
+
settlePredecessor: () => Promise<void>,
|
|
195
|
+
settleCurrent: () => Promise<unknown>
|
|
196
|
+
): RegisteredBackend {
|
|
197
|
+
let settling: Promise<void> | undefined;
|
|
198
|
+
const registeredBackend: RegisteredBackend = {
|
|
199
|
+
settle: () => {
|
|
200
|
+
if (settling) return settling;
|
|
201
|
+
const attempt = Promise.allSettled([settlePredecessor(), settleCurrent()]).then((results) => {
|
|
202
|
+
if (results.every((result) => result.status === 'fulfilled')) {
|
|
203
|
+
if (registered.backends.get(id) === registeredBackend) registered.backends.delete(id);
|
|
204
|
+
const tableBackends = registered.tableBackends.get(tableId);
|
|
205
|
+
tableBackends?.delete(registeredBackend);
|
|
206
|
+
if (tableBackends?.size === 0) registered.tableBackends.delete(tableId);
|
|
207
|
+
}
|
|
208
|
+
const failures = results
|
|
209
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
210
|
+
.map((result) => result.reason);
|
|
211
|
+
if (failures.length === 1) throw failures[0];
|
|
212
|
+
if (failures.length) throw new AggregateError(failures, `Could not settle ${label} backend '${id}'`);
|
|
213
|
+
});
|
|
214
|
+
const retryable = attempt.catch((error) => {
|
|
215
|
+
if (settling === retryable) settling = undefined;
|
|
216
|
+
throw error;
|
|
217
|
+
});
|
|
218
|
+
settling = retryable;
|
|
219
|
+
return retryable;
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
let tableBackends = registered.tableBackends.get(tableId);
|
|
223
|
+
if (!tableBackends) registered.tableBackends.set(tableId, (tableBackends = new Set()));
|
|
224
|
+
tableBackends.add(registeredBackend);
|
|
225
|
+
registered.backends.set(id, registeredBackend);
|
|
226
|
+
return registeredBackend;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const warnedAuditIndexes = new Set<string>();
|
|
230
|
+
|
|
231
|
+
export function attachDerivedIndexes(
|
|
232
|
+
Table: any,
|
|
233
|
+
options: { retryUnavailableReadiness?: boolean } = {}
|
|
234
|
+
): DerivedIndexAttachment | undefined {
|
|
235
|
+
if (activationSuspended(Table)) return;
|
|
236
|
+
const hnswAttributes = Table.attributes.filter(
|
|
237
|
+
(attribute: any) => attribute.indexed?.type === 'HNSW' && Table.indices[attribute.name]?.customIndex
|
|
238
|
+
);
|
|
239
|
+
const attributes = hnswAttributes.filter((attribute: any) => Table.indices[attribute.name]?.customIndex?.postCommit);
|
|
240
|
+
const fullTextDefinitions = (Table.fullTextIndexes ?? []) as FullTextDefinition[];
|
|
241
|
+
const fullTextRetirementNames =
|
|
242
|
+
Table.primaryStore?.rootStore instanceof RocksDatabase
|
|
243
|
+
? [...new Set((Table.fullTextIndexRetirements ?? []) as string[])].sort()
|
|
244
|
+
: [];
|
|
245
|
+
if (Table.audit !== true && (attributes.length > 0 || fullTextDefinitions.length > 0)) {
|
|
246
|
+
throw new ClientError(
|
|
247
|
+
`Table '${Table.databaseName}.${Table.tableName}' must enable audit logging before using a post-commit derived index`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
assertFullTextActivationSupported(
|
|
251
|
+
Table.primaryStore?.rootStore,
|
|
252
|
+
Table.databaseName,
|
|
253
|
+
Table.tableName,
|
|
254
|
+
fullTextDefinitions
|
|
255
|
+
);
|
|
256
|
+
if (!Table.auditStore) {
|
|
257
|
+
if (fullTextRetirementNames.length > 0) return attachFullTextRetirementRecovery(Table, fullTextRetirementNames);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const auditStore = Table.auditStore as RocksTransactionLogStore;
|
|
261
|
+
const registered = runtimeFor(auditStore);
|
|
262
|
+
const fullTextTest = fullTextTestConfiguration;
|
|
263
|
+
if (registered.droppingTables.has(Table.tableId)) return;
|
|
264
|
+
for (const attribute of hnswAttributes) {
|
|
265
|
+
const indexStore = Table.indices[attribute.name];
|
|
266
|
+
const id = `hnsw:${indexStore.name}`;
|
|
267
|
+
// A transient JS-backed declaration can precede the first native registration while a
|
|
268
|
+
// schema is being installed. Only an explicit transition away from an already-registered
|
|
269
|
+
// native backend should make a later native registration eligible to rearm an exhausted
|
|
270
|
+
// rebuild budget.
|
|
271
|
+
if (attribute.indexed.nativePlane === false && !indexStore.customIndex.postCommit && registered.backends.has(id))
|
|
272
|
+
markUnavailableRetry(registered, auditStore, id);
|
|
273
|
+
}
|
|
274
|
+
if (attributes.length === 0 && fullTextDefinitions.length === 0 && fullTextRetirementNames.length === 0) return;
|
|
275
|
+
if (
|
|
276
|
+
attributes.length === 0 &&
|
|
277
|
+
fullTextDefinitions.length === 0 &&
|
|
278
|
+
(registered.tableBackends.get(Table.tableId)?.size ?? 0) > 0
|
|
279
|
+
)
|
|
280
|
+
return;
|
|
281
|
+
const installed = { Table };
|
|
282
|
+
let registeredTable = registered.tables.get(Table.tableId);
|
|
283
|
+
if (registeredTable) {
|
|
284
|
+
registeredTable.owners.add(installed);
|
|
285
|
+
registeredTable.current = installed;
|
|
286
|
+
} else {
|
|
287
|
+
registeredTable = { current: installed, owners: new Set([installed]) };
|
|
288
|
+
registered.tables.set(Table.tableId, registeredTable);
|
|
289
|
+
}
|
|
290
|
+
const releases: Array<() => Promise<void>> = [];
|
|
291
|
+
const hnswRetryBackendIds: string[] = [];
|
|
292
|
+
const registeredBackends = new Map<string, RegisteredBackend>();
|
|
293
|
+
const fullTextSetups = new Map<string, Promise<void>>();
|
|
294
|
+
let closing = false;
|
|
295
|
+
let registrationReleased = false;
|
|
296
|
+
let closeOperation: Promise<void> | undefined;
|
|
297
|
+
const fullTextSnapshot = fullTextActivationSnapshot(Table, fullTextDefinitions);
|
|
298
|
+
let fullTextRetirementSnapshot = JSON.stringify(fullTextRetirementNames);
|
|
299
|
+
let fullTextRetirementRecoveryFailed = false;
|
|
300
|
+
const persistedRetirementOperation = resumePersistedFullTextRetirements(
|
|
301
|
+
Table,
|
|
302
|
+
fullTextRetirementNames,
|
|
303
|
+
() => !closing && registered.tables.get(Table.tableId)?.current === installed
|
|
304
|
+
);
|
|
305
|
+
const persistedRetirementSettlement = persistedRetirementOperation.then(
|
|
306
|
+
(completed) => {
|
|
307
|
+
if (completed) fullTextRetirementSnapshot = JSON.stringify(Table.fullTextIndexRetirements ?? []);
|
|
308
|
+
else fullTextRetirementRecoveryFailed = true;
|
|
309
|
+
},
|
|
310
|
+
(error) => {
|
|
311
|
+
fullTextRetirementRecoveryFailed = true;
|
|
312
|
+
fullTextLogger.warn?.(
|
|
313
|
+
`Could not resume full-text retirement for ${Table.databaseName}.${Table.tableName}`,
|
|
314
|
+
error
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
const hnswSnapshot = attributes.map((attribute: any) => ({
|
|
319
|
+
name: attribute.name,
|
|
320
|
+
index: Table.indices[attribute.name]?.customIndex,
|
|
321
|
+
computedFrom: attribute.computed?.from,
|
|
322
|
+
computedFromExpression: attribute.computedFromExpression,
|
|
323
|
+
userResolver:
|
|
324
|
+
attribute.computed && !attribute.computedFromExpression ? Table.userResolvers?.[attribute.name] : undefined,
|
|
325
|
+
}));
|
|
326
|
+
const releaseFullTextRegistration =
|
|
327
|
+
fullTextDefinitions.length > 0 ? registerDerivedIndexTables(auditStore, [Table.tableId]) : undefined;
|
|
328
|
+
const releaseRegistration = () => {
|
|
329
|
+
if (registrationReleased) return;
|
|
330
|
+
registrationReleased = true;
|
|
331
|
+
releaseFullTextRegistration?.();
|
|
332
|
+
};
|
|
333
|
+
for (const attribute of attributes) {
|
|
334
|
+
const indexStore = Table.indices[attribute.name];
|
|
335
|
+
const index = indexStore.customIndex as DerivedNativeIndex & { postCommit: true };
|
|
336
|
+
const id = `hnsw:${indexStore.name}`;
|
|
337
|
+
const warningKey = `${Table.databaseName}.${Table.tableName}.${indexStore.name}`;
|
|
338
|
+
if (!warnedAuditIndexes.has(warningKey)) {
|
|
339
|
+
warnedAuditIndexes.add(warningKey);
|
|
340
|
+
logger.warn?.(
|
|
341
|
+
`Derived index ${indexStore.name} requires auditing; the audit API retains full record history for the configured retention window`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
const resolver = Table.propertyResolvers?.[attribute.name];
|
|
345
|
+
const label = `Vector for attribute "${attribute.name}"`;
|
|
346
|
+
index.attachDerivedHost({
|
|
347
|
+
readiness: () => registered.runtime.getReadiness(id),
|
|
348
|
+
coverage: (maxLagMilliseconds) =>
|
|
349
|
+
readDerivedIndexCoverage(
|
|
350
|
+
auditStore,
|
|
351
|
+
id,
|
|
352
|
+
() => indexStore.getSync(DERIVED_INDEX_CURSOR_KEY),
|
|
353
|
+
maxLagMilliseconds
|
|
354
|
+
),
|
|
355
|
+
requestRebuild: () => registered.runtime.requestRebuild(id),
|
|
356
|
+
waitForCoverage: (since, timeout, signal) => registered.runtime.waitForCoverage(id, since, timeout, signal),
|
|
357
|
+
});
|
|
358
|
+
const { settlePredecessor } = beginBackendHandoff(registered, id);
|
|
359
|
+
const release = registered.runtime.register({
|
|
360
|
+
backend: new HnswDerivedIndexBackend(id, index),
|
|
361
|
+
projections: new Map([
|
|
362
|
+
[
|
|
363
|
+
Table.tableId,
|
|
364
|
+
(record: any) => {
|
|
365
|
+
const vector = resolver ? resolver(record) : record[attribute.name];
|
|
366
|
+
if (vector == null) return undefined;
|
|
367
|
+
index.assertDerivedValue(vector, label);
|
|
368
|
+
return vector;
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
]),
|
|
372
|
+
options: { maxLagMilliseconds: attribute.indexed?.maxLagMilliseconds ?? DEFAULT_MAX_LAG_MILLISECONDS },
|
|
373
|
+
});
|
|
374
|
+
const registeredBackend = registerBackend(registered, Table.tableId, id, 'HNSW', settlePredecessor, release);
|
|
375
|
+
registeredBackends.set(id, registeredBackend);
|
|
376
|
+
const retryUnavailable = consumeUnavailableRetry(registered, auditStore, id);
|
|
377
|
+
if (retryUnavailable && registered.runtime.getReadiness(id).state === 'unavailable')
|
|
378
|
+
registered.runtime.requestRebuild(id);
|
|
379
|
+
hnswRetryBackendIds.push(id);
|
|
380
|
+
releases.push(() => registeredBackend.settle());
|
|
381
|
+
}
|
|
382
|
+
for (const definition of fullTextDefinitions) startFullTextBackend(definition, options.retryUnavailableReadiness);
|
|
383
|
+
|
|
384
|
+
function startFullTextBackend(definition: FullTextDefinition, retryUnavailableReadiness = false): void {
|
|
385
|
+
if (fullTextSetups.has(definition.name)) return;
|
|
386
|
+
const generation = Table.fullTextIndexGenerations?.[definition.name];
|
|
387
|
+
const id = fullTextDerivedIndexId(Table, definition.name);
|
|
388
|
+
const readinessId = fullTextDerivedIndexReadinessId(Table, definition.name, generation);
|
|
389
|
+
if (typeof generation !== 'string' || generation.length === 0) {
|
|
390
|
+
publishDerivedIndexReadiness(auditStore, readinessId, 'unavailable', 'backend-failed');
|
|
391
|
+
fullTextLogger.error?.(
|
|
392
|
+
`Full-text index ${Table.databaseName}.${Table.tableName}.${definition.name} has no durable generation; reapply its schema to activate it`
|
|
393
|
+
);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const { settlePredecessor, predecessorSettled } = beginBackendHandoff(registered, id);
|
|
398
|
+
const quiescePredecessor = async () => {
|
|
399
|
+
try {
|
|
400
|
+
await predecessorSettled;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (!registered.runtime.requestRebuild(id)) throw error;
|
|
403
|
+
await settlePredecessor();
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
let cancelled = false;
|
|
407
|
+
let release: (() => Promise<void>) | undefined;
|
|
408
|
+
let setup: Promise<void>;
|
|
409
|
+
const registeredBackend = registerBackend(registered, Table.tableId, id, 'full-text', settlePredecessor, () => {
|
|
410
|
+
cancelled = true;
|
|
411
|
+
return setup.then(() => release?.());
|
|
412
|
+
});
|
|
413
|
+
registeredBackends.set(id, registeredBackend);
|
|
414
|
+
releases.push(() => registeredBackend.settle());
|
|
415
|
+
|
|
416
|
+
const isCurrent = () =>
|
|
417
|
+
!closing &&
|
|
418
|
+
!cancelled &&
|
|
419
|
+
registered.tables.get(Table.tableId)?.current === installed &&
|
|
420
|
+
Table.fullTextIndexGenerations?.[definition.name] === generation;
|
|
421
|
+
setup = (async () => {
|
|
422
|
+
await quiescePredecessor();
|
|
423
|
+
if (!isCurrent()) return;
|
|
424
|
+
if (
|
|
425
|
+
!(await waitForFullTextRetirement(Table.primaryStore.rootStore, Table.tableName, {
|
|
426
|
+
shouldContinue: isCurrent,
|
|
427
|
+
}))
|
|
428
|
+
)
|
|
429
|
+
return;
|
|
430
|
+
if (!isCurrent()) return;
|
|
431
|
+
if (retryUnavailableReadiness) retryDerivedIndexUnavailable(auditStore, readinessId);
|
|
432
|
+
const storage = fullTextStorageDefinition(definition);
|
|
433
|
+
const storeName = `${Table.tableName}/${definition.name}`;
|
|
434
|
+
const warningKey = `${Table.primaryStore.rootStore.path}:${storeName}`;
|
|
435
|
+
if (!warnedAuditIndexes.has(warningKey)) {
|
|
436
|
+
warnedAuditIndexes.add(warningKey);
|
|
437
|
+
fullTextLogger.warn?.(
|
|
438
|
+
`Derived index ${storeName} requires auditing; the audit API retains full record history for the configured retention window`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const backend = await createNativeFullTextDerivedIndexBackend({
|
|
442
|
+
id,
|
|
443
|
+
storePath: Table.primaryStore.rootStore.path,
|
|
444
|
+
storeName,
|
|
445
|
+
sourceGeneration: `${Table.tableId}:${generation}`,
|
|
446
|
+
fields: storage.fields,
|
|
447
|
+
analyzer: storage.analyzer,
|
|
448
|
+
stopWords: storage.stopWords,
|
|
449
|
+
positions: storage.positions,
|
|
450
|
+
surfaceTerms: storage.surfaceTerms,
|
|
451
|
+
limits: { ...FULL_TEXT_LIMITS },
|
|
452
|
+
...(fullTextTest
|
|
453
|
+
? {
|
|
454
|
+
binding: fullTextTest.binding,
|
|
455
|
+
closeTimeoutMilliseconds: fullTextTest.closeTimeoutMilliseconds,
|
|
456
|
+
shutdownTimeoutMilliseconds: fullTextTest.shutdownTimeoutMilliseconds,
|
|
457
|
+
}
|
|
458
|
+
: {}),
|
|
459
|
+
});
|
|
460
|
+
if (!isCurrent()) return;
|
|
461
|
+
release = registered.runtime.register({
|
|
462
|
+
backend,
|
|
463
|
+
readinessId,
|
|
464
|
+
isCurrent,
|
|
465
|
+
projections: new Map([[Table.tableId, fullTextProjection(definition)]]),
|
|
466
|
+
options: {
|
|
467
|
+
maxLagMilliseconds: DEFAULT_MAX_LAG_MILLISECONDS,
|
|
468
|
+
...fullTextTest?.runnerOptions,
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
})().catch((error) => {
|
|
472
|
+
if (!isCurrent()) return;
|
|
473
|
+
// Setup runs on every worker, but readiness is shared. A local setup failure must not
|
|
474
|
+
// replace a state already published by the elected runner on another worker.
|
|
475
|
+
publishDerivedIndexUnavailableIfUnknown(auditStore, readinessId);
|
|
476
|
+
fullTextLogger.error?.(
|
|
477
|
+
`Could not activate full-text index ${Table.databaseName}.${Table.tableName}.${definition.name}`,
|
|
478
|
+
error
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
fullTextSetups.set(definition.name, setup);
|
|
482
|
+
void setup.finally(() => {
|
|
483
|
+
if (fullTextSetups.get(definition.name) === setup) fullTextSetups.delete(definition.name);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
fullTextDefinitions() {
|
|
488
|
+
return fullTextDefinitions;
|
|
489
|
+
},
|
|
490
|
+
async close(dropping = false) {
|
|
491
|
+
if (dropping) registered.droppingTables.add(Table.tableId);
|
|
492
|
+
if (closeOperation) return closeOperation;
|
|
493
|
+
closing = true;
|
|
494
|
+
const settlements = dropping
|
|
495
|
+
? [...(registered.tableBackends.get(Table.tableId) ?? [])].map((backend) => backend.settle())
|
|
496
|
+
: releases.map((release) => release());
|
|
497
|
+
const retained = new Set(
|
|
498
|
+
((Table.fullTextIndexes ?? []) as FullTextDefinition[]).map((definition) => definition.name)
|
|
499
|
+
);
|
|
500
|
+
const removedDefinitions = dropping
|
|
501
|
+
? []
|
|
502
|
+
: fullTextDefinitions.filter((definition) => !retained.has(definition.name));
|
|
503
|
+
let releaseRetirementFence: (() => void) | undefined;
|
|
504
|
+
let waitForPeerRetirement: Promise<boolean> | undefined;
|
|
505
|
+
if (removedDefinitions.length > 0) {
|
|
506
|
+
releaseRetirementFence = acquireFullTextRetirementFence(Table.primaryStore.rootStore, Table.tableName);
|
|
507
|
+
if (!releaseRetirementFence)
|
|
508
|
+
waitForPeerRetirement = waitForFullTextRetirement(Table.primaryStore.rootStore, Table.tableName, {
|
|
509
|
+
shouldContinue: () => Table.primaryStore.rootStore.status === 'open',
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
const tableRegistration = registered.tables.get(Table.tableId);
|
|
513
|
+
if (tableRegistration) {
|
|
514
|
+
tableRegistration.owners.delete(installed);
|
|
515
|
+
if (tableRegistration.owners.size === 0) {
|
|
516
|
+
registered.tables.delete(Table.tableId);
|
|
517
|
+
} else if (tableRegistration.current === installed) {
|
|
518
|
+
tableRegistration.current = tableRegistration.owners.values().next().value;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const operation = (async () => {
|
|
522
|
+
try {
|
|
523
|
+
if (dropping) {
|
|
524
|
+
await Promise.all(settlements);
|
|
525
|
+
await persistedRetirementSettlement;
|
|
526
|
+
} else {
|
|
527
|
+
const results = await Promise.allSettled(settlements);
|
|
528
|
+
const failures: unknown[] = [];
|
|
529
|
+
for (const result of results) {
|
|
530
|
+
if (result.status === 'rejected') {
|
|
531
|
+
failures.push(result.reason);
|
|
532
|
+
derivedIndexLogger.error(
|
|
533
|
+
`Could not settle a superseded derived-index backend for table ${Table.tableId}`,
|
|
534
|
+
result.reason
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (failures.length === 1) throw failures[0];
|
|
539
|
+
if (failures.length)
|
|
540
|
+
throw new AggregateError(failures, `Could not settle derived indexes for table ${Table.tableId}`);
|
|
541
|
+
if (releaseRetirementFence) {
|
|
542
|
+
const retired = await retireFullTextIndexes(Table, removedDefinitions);
|
|
543
|
+
if (retired) await Table.completeFullTextIndexRetirements?.(removedDefinitions.map(({ name }) => name));
|
|
544
|
+
} else if (waitForPeerRetirement) await waitForPeerRetirement;
|
|
545
|
+
await persistedRetirementSettlement;
|
|
546
|
+
releaseRegistration();
|
|
547
|
+
}
|
|
548
|
+
} finally {
|
|
549
|
+
releaseRetirementFence?.();
|
|
550
|
+
}
|
|
551
|
+
})();
|
|
552
|
+
const retryable = operation.catch((error) => {
|
|
553
|
+
if (closeOperation === retryable) closeOperation = undefined;
|
|
554
|
+
throw error;
|
|
555
|
+
});
|
|
556
|
+
closeOperation = retryable;
|
|
557
|
+
return closeOperation;
|
|
558
|
+
},
|
|
559
|
+
matchesCurrent() {
|
|
560
|
+
if (closing || fullTextRetirementRecoveryFailed || !matchesCurrentHnsw()) return false;
|
|
561
|
+
const currentDefinitions = (Table.fullTextIndexes ?? []) as FullTextDefinition[];
|
|
562
|
+
return (
|
|
563
|
+
fullTextActivationSnapshot(Table, currentDefinitions) === fullTextSnapshot &&
|
|
564
|
+
JSON.stringify(Table.fullTextIndexRetirements ?? []) === fullTextRetirementSnapshot
|
|
565
|
+
);
|
|
566
|
+
},
|
|
567
|
+
retryUnavailableFullText() {
|
|
568
|
+
if (closing) return;
|
|
569
|
+
for (const definition of fullTextDefinitions) {
|
|
570
|
+
const generation = Table.fullTextIndexGenerations?.[definition.name];
|
|
571
|
+
if (
|
|
572
|
+
readDerivedIndexReadiness(auditStore, fullTextDerivedIndexReadinessId(Table, definition.name, generation))
|
|
573
|
+
.state === 'unavailable'
|
|
574
|
+
)
|
|
575
|
+
startFullTextBackend(definition, true);
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
restoreAfterFailedDrop() {
|
|
579
|
+
registered.droppingTables.delete(Table.tableId);
|
|
580
|
+
const currentTable = registered.tables.get(Table.tableId)?.current.Table;
|
|
581
|
+
if (currentTable && currentTable !== Table) {
|
|
582
|
+
releaseRegistration();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
for (const registration of registered.tables.values()) {
|
|
586
|
+
const current = registration.current.Table;
|
|
587
|
+
if (current !== Table && current.databasePath === Table.databasePath && current.tableName === Table.tableName) {
|
|
588
|
+
releaseRegistration();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
for (const [id, registeredBackend] of registeredBackends) {
|
|
593
|
+
const current = registered.backends.get(id);
|
|
594
|
+
if (current && current !== registeredBackend) {
|
|
595
|
+
releaseRegistration();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const restored = attachDerivedIndexes(Table, { retryUnavailableReadiness: true });
|
|
600
|
+
if (restored) releaseRegistration();
|
|
601
|
+
return restored;
|
|
602
|
+
},
|
|
603
|
+
retireAfterConfirmedDrop(definitions = fullTextDefinitions) {
|
|
604
|
+
return retireFullTextIndexes(Table, definitions);
|
|
605
|
+
},
|
|
606
|
+
completeDrop(dropped = true) {
|
|
607
|
+
releaseRegistration();
|
|
608
|
+
registered.droppingTables.delete(Table.tableId);
|
|
609
|
+
if (dropped) for (const id of hnswRetryBackendIds) markUnavailableRetry(registered, auditStore, id);
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
function matchesCurrentHnsw(): boolean {
|
|
614
|
+
const current = Table.attributes.filter(
|
|
615
|
+
(attribute: any) => attribute.indexed?.type === 'HNSW' && Table.indices[attribute.name]?.customIndex?.postCommit
|
|
616
|
+
);
|
|
617
|
+
if (current.length !== hnswSnapshot.length) return false;
|
|
618
|
+
return hnswSnapshot.every(({ name, index, computedFrom, computedFromExpression, userResolver }) => {
|
|
619
|
+
const indexStore = Table.indices[name];
|
|
620
|
+
const attribute = current.find((candidate: any) => candidate.name === name);
|
|
621
|
+
return (
|
|
622
|
+
indexStore?.customIndex === index &&
|
|
623
|
+
Object.is(attribute?.computed?.from, computedFrom) &&
|
|
624
|
+
attribute?.computedFromExpression === computedFromExpression &&
|
|
625
|
+
(attribute?.computed && !attribute.computedFromExpression ? Table.userResolvers?.[name] : undefined) ===
|
|
626
|
+
userResolver
|
|
627
|
+
);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Retire durable native indexes even when their in-memory attachment could not be restored. */
|
|
633
|
+
export async function retireFullTextIndexes(
|
|
634
|
+
Table: any,
|
|
635
|
+
definitions: readonly Pick<FullTextDefinition, 'name'>[],
|
|
636
|
+
shouldContinue: () => boolean = () => true
|
|
637
|
+
): Promise<boolean> {
|
|
638
|
+
const fullTextTest = fullTextTestConfiguration;
|
|
639
|
+
const retryMilliseconds =
|
|
640
|
+
fullTextTest?.shutdownTimeoutMilliseconds ?? DEFAULT_FULL_TEXT_RETIREMENT_RETRY_MILLISECONDS;
|
|
641
|
+
const deadline = Date.now() + retryMilliseconds;
|
|
642
|
+
let retired = true;
|
|
643
|
+
for (const definition of definitions) {
|
|
644
|
+
if (!shouldContinue()) return false;
|
|
645
|
+
let retryDelayMilliseconds = 10;
|
|
646
|
+
let retrying = false;
|
|
647
|
+
for (;;) {
|
|
648
|
+
try {
|
|
649
|
+
await retireNativeFullTextDerivedIndexStorage({
|
|
650
|
+
storePath: Table.primaryStore.rootStore.path,
|
|
651
|
+
storeName: `${Table.tableName}/${definition.name}`,
|
|
652
|
+
indexId: fullTextDerivedIndexId(Table, definition.name),
|
|
653
|
+
...(fullTextTest ? { binding: fullTextTest.binding } : {}),
|
|
654
|
+
});
|
|
655
|
+
break;
|
|
656
|
+
} catch (error) {
|
|
657
|
+
if (!shouldContinue()) return false;
|
|
658
|
+
const code =
|
|
659
|
+
error && typeof error === 'object' && 'code' in error && typeof error.code === 'string'
|
|
660
|
+
? error.code
|
|
661
|
+
: undefined;
|
|
662
|
+
const remaining = deadline - Date.now();
|
|
663
|
+
if (code === 'E_LOCK_BUSY' && remaining > 0) {
|
|
664
|
+
if (!retrying) {
|
|
665
|
+
retrying = true;
|
|
666
|
+
fullTextLogger.warn?.(
|
|
667
|
+
`Waiting up to ${retryMilliseconds}ms to retire full-text index ${Table.databaseName}.${Table.tableName}.${definition.name}`
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(retryDelayMilliseconds, remaining)));
|
|
671
|
+
if (!shouldContinue()) return false;
|
|
672
|
+
retryDelayMilliseconds = Math.min(retryDelayMilliseconds * 2, 250);
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
fullTextLogger.warn?.(
|
|
676
|
+
`Could not retire full-text index ${Table.databaseName}.${Table.tableName}.${definition.name}`,
|
|
677
|
+
error
|
|
678
|
+
);
|
|
679
|
+
retired = false;
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return retired;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function resumePersistedFullTextRetirements(
|
|
688
|
+
Table: any,
|
|
689
|
+
names: readonly string[],
|
|
690
|
+
shouldContinue: () => boolean
|
|
691
|
+
): Promise<boolean> {
|
|
692
|
+
if (names.length === 0) return true;
|
|
693
|
+
const rootStore = Table.primaryStore.rootStore;
|
|
694
|
+
const releaseRetirementFence = await waitForFullTextRetirementLease(rootStore, Table.tableName, {
|
|
695
|
+
shouldContinue,
|
|
696
|
+
timeoutMilliseconds: DEFAULT_FULL_TEXT_RETIREMENT_RETRY_MILLISECONDS,
|
|
697
|
+
});
|
|
698
|
+
if (!releaseRetirementFence) return false;
|
|
699
|
+
try {
|
|
700
|
+
if (!shouldContinue()) return false;
|
|
701
|
+
if (!(await Table.hasCurrentFullTextIndexRetirements(names))) return true;
|
|
702
|
+
const retired = await retireFullTextIndexes(
|
|
703
|
+
Table,
|
|
704
|
+
names.map((name) => ({ name })),
|
|
705
|
+
shouldContinue
|
|
706
|
+
);
|
|
707
|
+
if (!retired || !shouldContinue()) return false;
|
|
708
|
+
await Table.completeFullTextIndexRetirements?.(names);
|
|
709
|
+
return true;
|
|
710
|
+
} finally {
|
|
711
|
+
releaseRetirementFence();
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function attachFullTextRetirementRecovery(Table: any, names: readonly string[]): DerivedIndexAttachment {
|
|
716
|
+
let closing = false;
|
|
717
|
+
let settled = false;
|
|
718
|
+
const operation = resumePersistedFullTextRetirements(Table, names, () => !closing);
|
|
719
|
+
const settlement = operation.then(
|
|
720
|
+
() => {
|
|
721
|
+
settled = true;
|
|
722
|
+
},
|
|
723
|
+
(error) => {
|
|
724
|
+
settled = true;
|
|
725
|
+
fullTextLogger.warn?.(
|
|
726
|
+
`Could not resume full-text retirement for ${Table.databaseName}.${Table.tableName}`,
|
|
727
|
+
error
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
return {
|
|
732
|
+
async close() {
|
|
733
|
+
closing = true;
|
|
734
|
+
await settlement;
|
|
735
|
+
},
|
|
736
|
+
fullTextDefinitions() {
|
|
737
|
+
return [];
|
|
738
|
+
},
|
|
739
|
+
matchesCurrent() {
|
|
740
|
+
return !closing && !settled && JSON.stringify(Table.fullTextIndexRetirements ?? []) === JSON.stringify(names);
|
|
741
|
+
},
|
|
742
|
+
retryUnavailableFullText() {},
|
|
743
|
+
restoreAfterFailedDrop() {
|
|
744
|
+
return undefined;
|
|
745
|
+
},
|
|
746
|
+
retireAfterConfirmedDrop(definitions = names.map((name) => ({ name }))) {
|
|
747
|
+
return retireFullTextIndexes(Table, definitions);
|
|
748
|
+
},
|
|
749
|
+
completeDrop() {},
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** Keep a healthy, generation-identical full-text attachment across routine catalog reloads. */
|
|
754
|
+
export function refreshDerivedIndexes(Table: any): void {
|
|
755
|
+
const current = Table.derivedIndexRuntime;
|
|
756
|
+
if (activationSuspended(Table)) {
|
|
757
|
+
const closing = current?.close();
|
|
758
|
+
if (closing)
|
|
759
|
+
void closing.then(
|
|
760
|
+
() => {
|
|
761
|
+
if (Table.derivedIndexRuntime === current) Table.derivedIndexRuntime = undefined;
|
|
762
|
+
},
|
|
763
|
+
(error) => {
|
|
764
|
+
derivedIndexLogger.error(`Could not settle suspended derived indexes for table ${Table.tableId}`, error);
|
|
765
|
+
}
|
|
766
|
+
);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (current?.matchesCurrent?.()) {
|
|
770
|
+
current.retryUnavailableFullText?.();
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
let next: DerivedIndexAttachment | undefined;
|
|
774
|
+
const closing = current?.close();
|
|
775
|
+
if (closing)
|
|
776
|
+
void closing.then(
|
|
777
|
+
() => {
|
|
778
|
+
if (!next && Table.derivedIndexRuntime === current) Table.derivedIndexRuntime = undefined;
|
|
779
|
+
},
|
|
780
|
+
(error) => {
|
|
781
|
+
derivedIndexLogger.error(`Could not settle replaced derived indexes for table ${Table.tableId}`, error);
|
|
782
|
+
}
|
|
783
|
+
);
|
|
784
|
+
next = attachDerivedIndexes(Table, {
|
|
785
|
+
retryUnavailableReadiness: Boolean(current),
|
|
786
|
+
});
|
|
787
|
+
Table.derivedIndexRuntime = next ?? current;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export function assertFullTextActivationSupported(
|
|
791
|
+
rootStore: unknown,
|
|
792
|
+
databaseName: string,
|
|
793
|
+
tableName: string,
|
|
794
|
+
definitions: readonly FullTextDefinition[]
|
|
795
|
+
): void {
|
|
796
|
+
if (definitions.length > 0 && !(rootStore instanceof RocksDatabase))
|
|
797
|
+
throw new ClientError(
|
|
798
|
+
`Table '${databaseName}.${tableName}' cannot activate @fullText with the LMDB storage engine`,
|
|
799
|
+
400
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function fullTextProjection(definition: FullTextDefinition) {
|
|
804
|
+
return (record: Record<string, unknown>) => {
|
|
805
|
+
const projection: Record<string, string | string[]> = Object.create(null);
|
|
806
|
+
for (const { name } of definition.fields) {
|
|
807
|
+
const value = record[name];
|
|
808
|
+
if (typeof value === 'string') {
|
|
809
|
+
projection[name] = value;
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (Array.isArray(value)) {
|
|
813
|
+
const copied: string[] = [];
|
|
814
|
+
for (const entry of value) {
|
|
815
|
+
if (typeof entry === 'string') copied.push(entry);
|
|
816
|
+
else if (entry != null)
|
|
817
|
+
throw new ClientError(`Full-text source '${name}' contains a non-text array value`, 400);
|
|
818
|
+
}
|
|
819
|
+
projection[name] = copied;
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
if (value != null) throw new ClientError(`Full-text source '${name}' contains a non-text value`, 400);
|
|
823
|
+
}
|
|
824
|
+
return projection;
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function fullTextActivationSnapshot(Table: any, definitions: readonly FullTextDefinition[]): string {
|
|
829
|
+
return JSON.stringify(
|
|
830
|
+
definitions.map((definition) => ({
|
|
831
|
+
storage: fullTextStorageDefinition(definition),
|
|
832
|
+
generation: Table.fullTextIndexGenerations?.[definition.name],
|
|
833
|
+
}))
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
export function fullTextDerivedIndexId(Table: any, indexName: string): string {
|
|
838
|
+
return `fulltext:${Table.tableName}/${indexName}`;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
export function fullTextDerivedIndexReadinessId(Table: any, indexName: string, generation?: string): string {
|
|
842
|
+
return `${fullTextDerivedIndexId(Table, indexName)}:${Table.tableId}:${generation ?? 'missing'}`;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
export function fullTextDerivedIndexReadiness(Table: any, indexName: string) {
|
|
846
|
+
if (!Table.fullTextIndexes?.some((definition: FullTextDefinition) => definition.name === indexName))
|
|
847
|
+
throw new ClientError(`'${indexName}' is not a full-text index`, 400);
|
|
848
|
+
return readDerivedIndexReadiness(
|
|
849
|
+
Table.auditStore,
|
|
850
|
+
fullTextDerivedIndexReadinessId(Table, indexName, Table.fullTextIndexGenerations?.[indexName])
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
export function setFullTextNativeBindingForTests(configuration: FullTextTestConfiguration | undefined): void {
|
|
855
|
+
fullTextTestConfiguration = configuration;
|
|
856
|
+
}
|