@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
|
@@ -45,6 +45,61 @@ export type FullTextDefinition = {
|
|
|
45
45
|
highlighting?: FullTextHighlighting;
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
export type FullTextStorageDefinition = Pick<
|
|
49
|
+
FullTextDefinition,
|
|
50
|
+
'analyzer' | 'stopWords' | 'positions' | 'surfaceTerms'
|
|
51
|
+
> & {
|
|
52
|
+
fields: Array<Pick<FullTextSource, 'name' | 'weight'>>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type FullTextIndexGenerations = Record<string, string>;
|
|
56
|
+
|
|
57
|
+
/** Names are enough to locate native storage even when source attributes are missing or stale. */
|
|
58
|
+
export function persistedFullTextIndexNames(values: unknown): string[] {
|
|
59
|
+
if (!Array.isArray(values)) return [];
|
|
60
|
+
const names = new Set<string>();
|
|
61
|
+
for (const value of values) {
|
|
62
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
|
|
63
|
+
const name = (value as { name?: unknown }).name;
|
|
64
|
+
if (typeof name === 'string' && name.length > 0) names.add(name);
|
|
65
|
+
}
|
|
66
|
+
return [...names].sort();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function fullTextStorageDefinition(definition: FullTextDefinition): FullTextStorageDefinition {
|
|
70
|
+
return {
|
|
71
|
+
fields: definition.fields.map(({ name, weight }) => ({ name, weight })),
|
|
72
|
+
analyzer: definition.analyzer,
|
|
73
|
+
stopWords: definition.stopWords,
|
|
74
|
+
positions: definition.positions,
|
|
75
|
+
surfaceTerms: definition.surfaceTerms,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function reconcileFullTextIndexGenerations(
|
|
80
|
+
previousDefinitions: readonly FullTextDefinition[],
|
|
81
|
+
previousGenerations: unknown,
|
|
82
|
+
nextDefinitions: readonly FullTextDefinition[],
|
|
83
|
+
createGeneration: () => string
|
|
84
|
+
): FullTextIndexGenerations {
|
|
85
|
+
const previousByName = new Map(previousDefinitions.map((definition) => [definition.name, definition]));
|
|
86
|
+
const durable =
|
|
87
|
+
previousGenerations && typeof previousGenerations === 'object' && !Array.isArray(previousGenerations)
|
|
88
|
+
? (previousGenerations as Record<string, unknown>)
|
|
89
|
+
: Object.create(null);
|
|
90
|
+
const generations: FullTextIndexGenerations = Object.create(null);
|
|
91
|
+
for (const definition of nextDefinitions) {
|
|
92
|
+
const previous = previousByName.get(definition.name);
|
|
93
|
+
const generation = durable[definition.name];
|
|
94
|
+
const storageUnchanged =
|
|
95
|
+
previous &&
|
|
96
|
+
JSON.stringify(fullTextStorageDefinition(previous)) === JSON.stringify(fullTextStorageDefinition(definition));
|
|
97
|
+
generations[definition.name] =
|
|
98
|
+
storageUnchanged && typeof generation === 'string' && generation.length > 0 ? generation : createGeneration();
|
|
99
|
+
}
|
|
100
|
+
return generations;
|
|
101
|
+
}
|
|
102
|
+
|
|
48
103
|
export type FullTextSchemaAttribute = {
|
|
49
104
|
name: string;
|
|
50
105
|
type?: string;
|
|
@@ -34,21 +34,6 @@ export function readPersistedFullTextDefinitions(
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* Produces the catalog state that is safe while source descriptors transition. The caller publishes
|
|
39
|
-
* the requested definitions only after their source descriptors are durable.
|
|
40
|
-
*/
|
|
41
|
-
export function safeInterimFullTextDefinitions(
|
|
42
|
-
values: unknown,
|
|
43
|
-
attributes: readonly FullTextSchemaAttribute[],
|
|
44
|
-
requested: readonly FullTextDefinition[]
|
|
45
|
-
): FullTextDefinition[] {
|
|
46
|
-
const requestedByName = new Map(requested.map((definition) => [definition.name, definition]));
|
|
47
|
-
return compileValidFullTextDefinitions(values ?? [], attributes, () => {}).filter((definition) =>
|
|
48
|
-
definitionsEqual(definition, requestedByName.get(definition.name))
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
37
|
export function retainFullTextDefinitions(
|
|
53
38
|
values: unknown,
|
|
54
39
|
durableAttributes: readonly FullTextSchemaAttribute[],
|
package/resources/graphql.ts
CHANGED
|
@@ -4,11 +4,15 @@ import { scopedTableFactory, table } from './databases.ts';
|
|
|
4
4
|
import { getWorkerIndex } from '../server/threads/manageThreads.js';
|
|
5
5
|
import { thisThreadOwnsApplication } from '../server/threads/isolatedApplications.ts';
|
|
6
6
|
import { Resources } from './Resources.ts';
|
|
7
|
-
import type { NamedTypeNode, StringValueNode, ValueNode } from 'graphql';
|
|
7
|
+
import type { DirectiveNode, NamedTypeNode, StringValueNode, ValueNode } from 'graphql';
|
|
8
8
|
import { ClientError } from '../utility/errors/hdbError.ts';
|
|
9
9
|
import { attributeToFragment, type JsonSchemaFragment } from './jsonSchemaTypes.ts';
|
|
10
10
|
import harperLogger from '../utility/logging/harper_logger.ts';
|
|
11
11
|
import { compileFullTextDefinitions } from './fullTextSchema.ts';
|
|
12
|
+
import { validateDecisionSchema } from './models/decision.ts';
|
|
13
|
+
import { assertDerivedFieldOwnership } from './models/embedHook.ts';
|
|
14
|
+
import type { DecideConfig } from './models/decideHook.ts';
|
|
15
|
+
import type { DecisionLeaf } from './models/types.ts';
|
|
12
16
|
|
|
13
17
|
const PRIMITIVE_TYPES = ['ID', 'Int', 'Float', 'Long', 'String', 'Boolean', 'Date', 'Bytes', 'Any', 'BigInt', 'Blob'];
|
|
14
18
|
|
|
@@ -41,6 +45,101 @@ function coerceDirectiveValue(node: ValueNode): any {
|
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
const DECIDE_STRING_ARGS = new Set(['source', 'model', 'confidence', 'decision', 'instructions']);
|
|
49
|
+
const DECIDE_INT_ARGS = new Set(['minimum', 'maximum']);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `@decide(source, values | minimum/maximum, model?, confidence?, decision?, instructions?)`: the attribute
|
|
53
|
+
* type selects the decision leaf (String → the `values` enum, Boolean, Int → a bounded range),
|
|
54
|
+
* and the leaf is validated here so a bad closed set fails the schema load, not the first write.
|
|
55
|
+
* Field references (source, confidence, decision) are checked once every field of the type is known.
|
|
56
|
+
*/
|
|
57
|
+
function parseDecideDirective(directive: DirectiveNode, property: any): DecideConfig {
|
|
58
|
+
const target = `@decide on "${property.name}"`;
|
|
59
|
+
const args: {
|
|
60
|
+
source?: string;
|
|
61
|
+
model?: string;
|
|
62
|
+
confidence?: string;
|
|
63
|
+
decision?: string;
|
|
64
|
+
instructions?: string;
|
|
65
|
+
values?: string[];
|
|
66
|
+
minimum?: number;
|
|
67
|
+
maximum?: number;
|
|
68
|
+
} = {};
|
|
69
|
+
for (const arg of directive.arguments || []) {
|
|
70
|
+
const name = arg.name.value;
|
|
71
|
+
if (Object.hasOwn(args, name)) throw new ClientError(`${target} declares "${name}" more than once`, 400);
|
|
72
|
+
const value = arg.value;
|
|
73
|
+
if (DECIDE_STRING_ARGS.has(name)) {
|
|
74
|
+
if (value.kind !== 'StringValue')
|
|
75
|
+
throw new ClientError(`@decide(${name}: ...) on "${property.name}" expects a string literal`, 400);
|
|
76
|
+
args[name] = value.value;
|
|
77
|
+
} else if (DECIDE_INT_ARGS.has(name)) {
|
|
78
|
+
if (value.kind !== 'IntValue')
|
|
79
|
+
throw new ClientError(`@decide(${name}: ...) on "${property.name}" expects an integer literal`, 400);
|
|
80
|
+
args[name] = Number(value.value);
|
|
81
|
+
} else if (name === 'values') {
|
|
82
|
+
if (value.kind !== 'ListValue' || value.values.some((item) => item.kind !== 'StringValue'))
|
|
83
|
+
throw new ClientError(`@decide(values: ...) on "${property.name}" expects a list of string literals`, 400);
|
|
84
|
+
args.values = value.values.map((item) => (item as StringValueNode).value);
|
|
85
|
+
} else throw new ClientError(`${target} has an unknown argument "${name}"`, 400);
|
|
86
|
+
}
|
|
87
|
+
if (!args.source) throw new ClientError(`${target} requires a "source" argument`, 400);
|
|
88
|
+
for (const name of ['confidence', 'decision'] as const)
|
|
89
|
+
if (args[name] === '') throw new ClientError(`${target}: "${name}" must name a field`, 400);
|
|
90
|
+
const hasRange = args.minimum !== undefined || args.maximum !== undefined;
|
|
91
|
+
let schema: DecisionLeaf;
|
|
92
|
+
switch (property.type) {
|
|
93
|
+
case 'String':
|
|
94
|
+
if (!args.values)
|
|
95
|
+
throw new ClientError(`${target} requires "values" (the allowed strings) on a String attribute`, 400);
|
|
96
|
+
if (hasRange) throw new ClientError(`${target} takes "minimum" and "maximum" only on an Int attribute`, 400);
|
|
97
|
+
schema = { enum: args.values };
|
|
98
|
+
break;
|
|
99
|
+
case 'Boolean':
|
|
100
|
+
if (args.values || hasRange)
|
|
101
|
+
throw new ClientError(`${target} takes no "values", "minimum" or "maximum" on a Boolean attribute`, 400);
|
|
102
|
+
schema = { type: 'boolean' };
|
|
103
|
+
break;
|
|
104
|
+
case 'Int':
|
|
105
|
+
if (args.values)
|
|
106
|
+
throw new ClientError(`${target} takes "minimum" and "maximum", not "values", on an Int attribute`, 400);
|
|
107
|
+
if (args.minimum === undefined || args.maximum === undefined)
|
|
108
|
+
throw new ClientError(`${target} requires both "minimum" and "maximum" on an Int attribute`, 400);
|
|
109
|
+
// Table validation holds an Int to 32 bits; a wider bound would load and then fail every write.
|
|
110
|
+
for (const [name, bound] of Object.entries({ minimum: args.minimum, maximum: args.maximum }))
|
|
111
|
+
if (bound >> 0 !== bound)
|
|
112
|
+
throw new ClientError(`${target}: "${name}" must be an integer from -2147483648 to 2147483647`, 400);
|
|
113
|
+
schema = { type: 'integer', minimum: args.minimum, maximum: args.maximum };
|
|
114
|
+
break;
|
|
115
|
+
default:
|
|
116
|
+
throw new ClientError(
|
|
117
|
+
`${target} requires a String, Boolean or Int attribute type; got "${property.type === 'array' ? '[...]' : property.type}"`,
|
|
118
|
+
400
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
validateDecisionSchema(schema);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
throw new ClientError(`${target}: ${(err as Error).message}`, 400);
|
|
125
|
+
}
|
|
126
|
+
if (property.nullable === false)
|
|
127
|
+
throw new ClientError(`${target} cannot be declared non-null: a null source clears the attribute`, 400);
|
|
128
|
+
if (property.isPrimaryKey || property.computed)
|
|
129
|
+
throw new ClientError(`${target} cannot combine with @primaryKey or @computed`, 400);
|
|
130
|
+
const fields = new Set([property.name, args.source, args.confidence, args.decision].filter(Boolean));
|
|
131
|
+
if (fields.size !== 2 + (args.confidence ? 1 : 0) + (args.decision ? 1 : 0))
|
|
132
|
+
throw new ClientError(
|
|
133
|
+
`${target}: the attribute, "source", "confidence" and "decision" must be different fields`,
|
|
134
|
+
400
|
|
135
|
+
);
|
|
136
|
+
const config: DecideConfig = { source: args.source, model: args.model ?? 'default', schema };
|
|
137
|
+
if (args.confidence) config.confidence = args.confidence;
|
|
138
|
+
if (args.decision) config.decision = args.decision;
|
|
139
|
+
if (args.instructions) config.instructions = args.instructions;
|
|
140
|
+
return config;
|
|
141
|
+
}
|
|
142
|
+
|
|
44
143
|
if (!server.knownGraphQLDirectives) {
|
|
45
144
|
server.knownGraphQLDirectives = [];
|
|
46
145
|
}
|
|
@@ -52,6 +151,7 @@ server.knownGraphQLDirectives.push(
|
|
|
52
151
|
'indexed',
|
|
53
152
|
'computed',
|
|
54
153
|
'embed',
|
|
154
|
+
'decide',
|
|
55
155
|
'fullText',
|
|
56
156
|
'relationship',
|
|
57
157
|
'createdTime',
|
|
@@ -288,6 +388,10 @@ async function processGraphQLSchema(
|
|
|
288
388
|
property.version = `embed:${embedDefinition.model}`;
|
|
289
389
|
}
|
|
290
390
|
}
|
|
391
|
+
} else if (directiveName === 'decide') {
|
|
392
|
+
// Resolved after the field's other directives so @primaryKey / @computed in any
|
|
393
|
+
// order are seen; the ownership checks below need every field of the type.
|
|
394
|
+
property.decide = directive;
|
|
291
395
|
} else if (directiveName === 'fullText') {
|
|
292
396
|
throw new ClientError('@fullText must be declared on a @table type, not on a field', 400);
|
|
293
397
|
} else if (directiveName === 'relationship') {
|
|
@@ -337,18 +441,9 @@ async function processGraphQLSchema(
|
|
|
337
441
|
400
|
|
338
442
|
);
|
|
339
443
|
}
|
|
444
|
+
if (property.decide) property.decide = parseDecideDirective(property.decide, property);
|
|
340
445
|
}
|
|
341
|
-
|
|
342
|
-
// the vector column unpopulated (the source key never appears in write payloads).
|
|
343
|
-
for (const prop of attributes as any[]) {
|
|
344
|
-
// Object.hasOwn (not `in`): `attributesObject` is a plain object, so `in` would
|
|
345
|
-
// match inherited prototype keys (toString, constructor) and pass a bad source.
|
|
346
|
-
if (prop.embed && !Object.hasOwn(attributesObject, prop.embed.source))
|
|
347
|
-
throw new ClientError(
|
|
348
|
-
`@embed on "${prop.name}" references unknown source field "${prop.embed.source}"`,
|
|
349
|
-
400
|
|
350
|
-
);
|
|
351
|
-
}
|
|
446
|
+
assertDerivedFieldOwnership(attributes as any[]);
|
|
352
447
|
if (typeDef.fullTextIndexes.length > 0 && !typeDef.table)
|
|
353
448
|
throw new ClientError('@fullText is only supported on a @table type', 400);
|
|
354
449
|
typeDef.fullTextIndexes = compileFullTextDefinitions(typeDef.fullTextIndexes, attributes);
|
|
@@ -387,6 +387,13 @@ replay after its payload; replicas independently derive it from their own applie
|
|
|
387
387
|
backup and restore need only authoritative records and schema. A new or unusable directory serves no
|
|
388
388
|
full-text queries until rebuild and catch-up publish `ready`.
|
|
389
389
|
|
|
390
|
+
A table drop keeps the tombstoned catalog descriptor, including its native index names, until
|
|
391
|
+
retirement completes. The drop broadcast first unloads peer attachments and awaits their writer
|
|
392
|
+
shutdown; only then may the wrapper retire native storage, followed by RocksDB column families and
|
|
393
|
+
catalog rows. If native retirement cannot prove success, the table remains logically dropped and
|
|
394
|
+
unloaded but its tombstone stays durable so restart or same-name creation retries cleanup. Removing
|
|
395
|
+
the catalog first would turn a crash in that interval into an untraceable native-directory leak.
|
|
396
|
+
|
|
390
397
|
Tantivy files are not an opaque encrypted cache. They contain the document-id term dictionary,
|
|
391
398
|
analyzed term dictionaries, postings, frequencies and optionally positions; `surfaceTerms: true`
|
|
392
399
|
also stores the original projected strings needed for surface-term features. Operators must protect
|
|
@@ -5,9 +5,7 @@ import type { RocksTransactionLogStore } from '../RocksTransactionLogStore.ts';
|
|
|
5
5
|
import {
|
|
6
6
|
DERIVED_INDEX_ACCEPTED,
|
|
7
7
|
DERIVED_INDEX_DEFERRED,
|
|
8
|
-
DerivedIndexRuntime,
|
|
9
8
|
readDerivedIndexReadiness,
|
|
10
|
-
readDerivedIndexCoverage,
|
|
11
9
|
sameDerivedIndexPositions,
|
|
12
10
|
type DerivedIndexBackend,
|
|
13
11
|
type DerivedIndexBackendHost,
|
|
@@ -42,9 +40,6 @@ const QUEUE_CAPACITY_BYTES = 64 * 1024 * 1024;
|
|
|
42
40
|
const APPLY_SLICE_MILLIS = 5;
|
|
43
41
|
const BARRIER_IDLE_MULTIPLE = 3;
|
|
44
42
|
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
43
|
/**
|
|
49
44
|
* The native index methods the backend drives. `applyDerivedValue` inserts, replaces or removes
|
|
50
45
|
* one primary key's vector and stages its mapping as pending; `flushDerived` is the durability
|
|
@@ -277,261 +272,6 @@ export class HnswDerivedIndexBackend implements DerivedIndexBackend {
|
|
|
277
272
|
}
|
|
278
273
|
}
|
|
279
274
|
|
|
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
|
-
const currentTable = registered.tables.get(Table.tableId)?.current.Table;
|
|
516
|
-
if (currentTable && currentTable !== Table) return;
|
|
517
|
-
for (const registration of registered.tables.values()) {
|
|
518
|
-
const current = registration.current.Table;
|
|
519
|
-
if (current !== Table && current.databasePath === Table.databasePath && current.tableName === Table.tableName)
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
for (const [id, registeredBackend] of registeredBackends) {
|
|
523
|
-
const current = registered.backends.get(id);
|
|
524
|
-
if (current && current !== registeredBackend) return;
|
|
525
|
-
}
|
|
526
|
-
return attachDerivedIndexes(Table);
|
|
527
|
-
},
|
|
528
|
-
completeDrop(dropped = true) {
|
|
529
|
-
registered.droppingTables.delete(Table.tableId);
|
|
530
|
-
if (dropped) for (const id of backendIds) markUnavailableRetry(registered, auditStore, id);
|
|
531
|
-
},
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
|
|
535
275
|
/** Shared readiness of an index on any worker, registered or not. */
|
|
536
276
|
export function derivedIndexReadiness(auditStore: RocksTransactionLogStore, indexStoreName: string) {
|
|
537
277
|
return readDerivedIndexReadiness(auditStore, `hnsw:${indexStoreName}`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
4
|
import { loggerWithTag } from '../../utility/logging/logger.ts';
|
|
4
5
|
import {
|
|
5
6
|
FullTextDerivedIndexBackend,
|
|
@@ -29,6 +30,11 @@ export type NativeFullTextDerivedIndexLifecycleOptions = NativeFullTextIndexConf
|
|
|
29
30
|
export type NativeFullTextDerivedIndexBackendOptions = Omit<FullTextDerivedIndexBackendOptions, 'lifecycle'> &
|
|
30
31
|
Omit<NativeFullTextDerivedIndexLifecycleOptions, 'indexId'>;
|
|
31
32
|
|
|
33
|
+
export type RetireNativeFullTextDerivedIndexStorageOptions = Pick<
|
|
34
|
+
NativeFullTextDerivedIndexLifecycleOptions,
|
|
35
|
+
'storePath' | 'storeName' | 'indexId' | 'binding'
|
|
36
|
+
>;
|
|
37
|
+
|
|
32
38
|
export class NativeFullTextDerivedIndexLifecycle {
|
|
33
39
|
readonly #options: NativeFullTextDerivedIndexLifecycleOptions;
|
|
34
40
|
readonly #path: string;
|
|
@@ -48,7 +54,7 @@ export class NativeFullTextDerivedIndexLifecycle {
|
|
|
48
54
|
limits: { ...options.limits },
|
|
49
55
|
};
|
|
50
56
|
// Hashing bounds filenames for arbitrary store names; the index id remains in native metadata and logs.
|
|
51
|
-
this.#path =
|
|
57
|
+
this.#path = nativeFullTextIndexPath(options.storePath, options.storeName);
|
|
52
58
|
this.#generation = digest(options.sourceGeneration);
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -135,10 +141,7 @@ export class NativeFullTextDerivedIndexLifecycle {
|
|
|
135
141
|
|
|
136
142
|
async #getBinding(): Promise<NativeFullTextModule> {
|
|
137
143
|
if (this.#binding) return this.#binding;
|
|
138
|
-
|
|
139
|
-
this.#binding = configured
|
|
140
|
-
? await validateFullTextNativeBinding(typeof configured === 'function' ? await configured() : configured)
|
|
141
|
-
: await loadFullTextNativeBinding();
|
|
144
|
+
this.#binding = await resolveNativeFullTextBinding(this.#options.binding);
|
|
142
145
|
return this.#binding;
|
|
143
146
|
}
|
|
144
147
|
|
|
@@ -148,16 +151,64 @@ export class NativeFullTextDerivedIndexLifecycle {
|
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
153
|
|
|
154
|
+
/** Retire one quiesced native directory after its Harper declaration or table is removed. */
|
|
155
|
+
export async function retireNativeFullTextDerivedIndexStorage(
|
|
156
|
+
options: RetireNativeFullTextDerivedIndexStorageOptions
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
if (!isAbsolute(options.storePath)) throw new TypeError('Full-text storePath must be absolute');
|
|
159
|
+
if (!options.storeName) throw new TypeError('Full-text storeName is required');
|
|
160
|
+
if (!options.indexId) throw new TypeError('Full-text indexId is required');
|
|
161
|
+
const path = nativeFullTextIndexPath(options.storePath, options.storeName);
|
|
162
|
+
if (!existsSync(path)) {
|
|
163
|
+
if (existsSync(join(dirname(path), '.fulltext-retired'))) {
|
|
164
|
+
try {
|
|
165
|
+
const binding = await resolveNativeFullTextBinding(options.binding);
|
|
166
|
+
const reclaimed = await binding.reclaimRetiredNativeFullTextIndexes({ path });
|
|
167
|
+
if (reclaimed.failed > 0)
|
|
168
|
+
logWarning(`Could not remove ${reclaimed.failed} retired paths for full-text index '${options.indexId}'`);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
logWarning(`Could not reclaim retired storage for full-text index '${options.indexId}'`, error);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const binding = await resolveNativeFullTextBinding(options.binding);
|
|
176
|
+
// The wrapper owns the physical lifecycle fence: reset refuses an active/unproven writer and
|
|
177
|
+
// takes Tantivy's writer lock before renaming. Harper treats that refusal as best-effort cleanup,
|
|
178
|
+
// never as permission to remove a live directory itself.
|
|
179
|
+
const result = await binding.resetNativeFullTextIndex({ path, indexId: options.indexId });
|
|
180
|
+
void binding
|
|
181
|
+
.reclaimRetiredNativeFullTextIndexes({
|
|
182
|
+
path,
|
|
183
|
+
retiredPath: result.state === 'reset' ? result.retiredPath : undefined,
|
|
184
|
+
})
|
|
185
|
+
.then((reclaimed) => {
|
|
186
|
+
if (reclaimed.failed > 0)
|
|
187
|
+
logWarning(`Could not remove ${reclaimed.failed} retired paths for full-text index '${options.indexId}'`);
|
|
188
|
+
})
|
|
189
|
+
.catch((error) => logWarning(`Could not reclaim retired storage for full-text index '${options.indexId}'`, error));
|
|
190
|
+
}
|
|
191
|
+
|
|
151
192
|
export async function createNativeFullTextDerivedIndexBackend(
|
|
152
193
|
options: NativeFullTextDerivedIndexBackendOptions
|
|
153
194
|
): Promise<FullTextDerivedIndexBackend> {
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
195
|
+
const configuredMaxCursorPayloadBytes = options.maxCursorPayloadBytes;
|
|
196
|
+
if (
|
|
197
|
+
configuredMaxCursorPayloadBytes !== undefined &&
|
|
198
|
+
configuredMaxCursorPayloadBytes > HARPER_FULLTEXT_MAX_CURSOR_PAYLOAD_BYTES
|
|
199
|
+
)
|
|
156
200
|
throw new RangeError(`Full-text maxCursorPayloadBytes must not exceed ${HARPER_FULLTEXT_MAX_CURSOR_PAYLOAD_BYTES}`);
|
|
157
201
|
const lifecycle = new NativeFullTextDerivedIndexLifecycle({ ...options, indexId: options.id });
|
|
158
202
|
await lifecycle.initialize({ reclaimRetired: false });
|
|
159
|
-
if (
|
|
203
|
+
if (
|
|
204
|
+
configuredMaxCursorPayloadBytes !== undefined &&
|
|
205
|
+
configuredMaxCursorPayloadBytes > lifecycle.maxCommitPayloadBytes
|
|
206
|
+
)
|
|
160
207
|
throw new RangeError('Full-text maxCursorPayloadBytes exceeds the native commit payload capacity');
|
|
208
|
+
const maxCursorPayloadBytes = Math.min(
|
|
209
|
+
configuredMaxCursorPayloadBytes ?? HARPER_FULLTEXT_MAX_CURSOR_PAYLOAD_BYTES,
|
|
210
|
+
lifecycle.maxCommitPayloadBytes
|
|
211
|
+
);
|
|
161
212
|
const backend = new FullTextDerivedIndexBackend({
|
|
162
213
|
...options,
|
|
163
214
|
id: options.id,
|
|
@@ -168,6 +219,18 @@ export async function createNativeFullTextDerivedIndexBackend(
|
|
|
168
219
|
return backend;
|
|
169
220
|
}
|
|
170
221
|
|
|
222
|
+
function nativeFullTextIndexPath(storePath: string, storeName: string): string {
|
|
223
|
+
return join(resolve(storePath), `${digest(storeName)}.fulltext`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function resolveNativeFullTextBinding(
|
|
227
|
+
configured?: NativeFullTextModule | (() => Promise<NativeFullTextModule>)
|
|
228
|
+
): Promise<NativeFullTextModule> {
|
|
229
|
+
return configured
|
|
230
|
+
? validateFullTextNativeBinding(typeof configured === 'function' ? await configured() : configured)
|
|
231
|
+
: loadFullTextNativeBinding();
|
|
232
|
+
}
|
|
233
|
+
|
|
171
234
|
function digest(value: string): string {
|
|
172
235
|
return createHash('sha256').update(value).digest('hex');
|
|
173
236
|
}
|