@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
package/resources/Table.ts
CHANGED
|
@@ -26,7 +26,7 @@ import type {
|
|
|
26
26
|
import type { User } from '../security/user.ts';
|
|
27
27
|
import lmdbProcessRows from '../dataLayer/harperBridge/lmdbBridge/lmdbUtility/lmdbProcessRows.js';
|
|
28
28
|
import { Resource, SEARCH_AUTHORIZATION, transformForSelect } from './Resource.ts';
|
|
29
|
-
import { when, promiseNormalize } from '../utility/when.ts';
|
|
29
|
+
import { settleBeforeDeadline, when, promiseNormalize } from '../utility/when.ts';
|
|
30
30
|
import {
|
|
31
31
|
DatabaseTransaction,
|
|
32
32
|
ImmediateTransaction,
|
|
@@ -36,6 +36,9 @@ import {
|
|
|
36
36
|
TRANSACTION_STATE,
|
|
37
37
|
writeKeyId,
|
|
38
38
|
closeWriteInstance,
|
|
39
|
+
databaseCommitsSuspended,
|
|
40
|
+
commitTrackedRocksTransaction,
|
|
41
|
+
getDatabaseCommitDrainTimeoutMilliseconds,
|
|
39
42
|
type WriteGeneration,
|
|
40
43
|
} from './DatabaseTransaction.ts';
|
|
41
44
|
import {
|
|
@@ -50,8 +53,11 @@ import {
|
|
|
50
53
|
import { getThisNodeName } from '../server/nodeName.ts';
|
|
51
54
|
import * as envMngr from '../utility/environment/environmentManager.ts';
|
|
52
55
|
import { addSubscription } from './transactionBroadcast.ts';
|
|
56
|
+
import { databaseDropPrepared } from './databaseDropPreparation.ts';
|
|
53
57
|
import {
|
|
54
58
|
DerivedIndexLagError,
|
|
59
|
+
DatabaseClosingError,
|
|
60
|
+
DatabaseDrainTimeoutError,
|
|
55
61
|
handleHDBError,
|
|
56
62
|
ClientError,
|
|
57
63
|
ServerError,
|
|
@@ -73,6 +79,7 @@ import {
|
|
|
73
79
|
sweepDroppedTableBlobs,
|
|
74
80
|
storeNameFor,
|
|
75
81
|
storeNamesFor,
|
|
82
|
+
isReadOnlyMode,
|
|
76
83
|
} from './databases.ts';
|
|
77
84
|
import { notifyReplicatedApplyFailure } from './replicatedApplyFailure.ts';
|
|
78
85
|
import {
|
|
@@ -117,7 +124,13 @@ import {
|
|
|
117
124
|
isLockControlType,
|
|
118
125
|
isAuditEntryWrite,
|
|
119
126
|
} from './auditStore.ts';
|
|
120
|
-
import {
|
|
127
|
+
import {
|
|
128
|
+
acquireFullTextClearFence,
|
|
129
|
+
acquireFullTextRetirementFence,
|
|
130
|
+
derivedIndexWriteRejection,
|
|
131
|
+
hasDerivedIndexRegistration,
|
|
132
|
+
waitForFullTextClear,
|
|
133
|
+
} from './derivedIndexRegistry.ts';
|
|
121
134
|
import {
|
|
122
135
|
decodeLockControlPayload,
|
|
123
136
|
encodeLockControlPayload,
|
|
@@ -127,7 +140,22 @@ import {
|
|
|
127
140
|
LockCoordinator,
|
|
128
141
|
type LockControlEntry,
|
|
129
142
|
} from './recordLockCoordinator.ts';
|
|
130
|
-
import {
|
|
143
|
+
import {
|
|
144
|
+
assertDerivedFieldOwnership,
|
|
145
|
+
buildEmbedBefore,
|
|
146
|
+
combineWriteHooks,
|
|
147
|
+
type WriteHook,
|
|
148
|
+
createDefaultEmbedder,
|
|
149
|
+
type EmbedAttribute,
|
|
150
|
+
type Embedder,
|
|
151
|
+
} from './models/embedHook.ts';
|
|
152
|
+
import {
|
|
153
|
+
buildDecideBefore,
|
|
154
|
+
createDefaultDecider,
|
|
155
|
+
type DecideAttribute,
|
|
156
|
+
type DecideConfig,
|
|
157
|
+
type Decider,
|
|
158
|
+
} from './models/decideHook.ts';
|
|
131
159
|
import { autoCast, autoCastBooleanStrict } from '../utility/common_utils.ts';
|
|
132
160
|
import {
|
|
133
161
|
recordUpdater,
|
|
@@ -157,7 +185,11 @@ import { RocksDatabase, Transaction as RocksTransaction } from '@harperfast/rock
|
|
|
157
185
|
import { LMDBTransaction, ImmediateTransaction as ImmediateLMDBTransaction } from './LMDBTransaction';
|
|
158
186
|
import { contentTypes } from '../server/serverHelpers/contentTypes';
|
|
159
187
|
import { type JsonSchemaFragment, projectAttributesToProperties } from './jsonSchemaTypes.ts';
|
|
160
|
-
import {
|
|
188
|
+
import {
|
|
189
|
+
persistedFullTextIndexNames,
|
|
190
|
+
type FullTextDefinition,
|
|
191
|
+
type FullTextIndexGenerations,
|
|
192
|
+
} from './fullTextSchema.ts';
|
|
161
193
|
|
|
162
194
|
const { sortBy } = lodash;
|
|
163
195
|
const { validateAttribute } = lmdbProcessRows;
|
|
@@ -178,6 +210,7 @@ export type Attribute = {
|
|
|
178
210
|
resolve?: any;
|
|
179
211
|
computedFromExpression?: any;
|
|
180
212
|
embed?: { source: string; model: string };
|
|
213
|
+
decide?: DecideConfig;
|
|
181
214
|
version?: any;
|
|
182
215
|
properties?: Array<Attribute>;
|
|
183
216
|
elements?: Attribute;
|
|
@@ -294,12 +327,38 @@ export function releaseUpdateAttributesLock(rootStore: RocksDatabase) {
|
|
|
294
327
|
rootStore.unlock(updateAttributesLockKey);
|
|
295
328
|
}
|
|
296
329
|
|
|
297
|
-
|
|
330
|
+
async function acquireUpdateAttributesLockAsync(
|
|
331
|
+
rootStore: RocksDatabase,
|
|
332
|
+
scopeDescription: string,
|
|
333
|
+
timeout = UPDATE_ATTRIBUTES_LOCK_TIMEOUT
|
|
334
|
+
): Promise<void> {
|
|
335
|
+
if (rootStore.tryLock(updateAttributesLockKey)) return;
|
|
336
|
+
const startTime = performance.now();
|
|
337
|
+
let waitTime = 1;
|
|
338
|
+
while (!rootStore.tryLock(updateAttributesLockKey)) {
|
|
339
|
+
const elapsed = performance.now() - startTime;
|
|
340
|
+
if (elapsed >= timeout) {
|
|
341
|
+
throw new UpdateAttributesLockTimeoutError(
|
|
342
|
+
`Timed out after ${Math.round(elapsed)}ms waiting for the exclusive '${UPDATE_ATTRIBUTES_LOCK}' lock on ${scopeDescription}; the lock holder did not release it before the deadline, so this schema/attribute update cannot proceed`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(waitTime, timeout - elapsed)));
|
|
346
|
+
if (waitTime < 16) waitTime *= 2;
|
|
347
|
+
}
|
|
348
|
+
const waited = performance.now() - startTime;
|
|
349
|
+
if (waited >= UPDATE_ATTRIBUTES_LOCK_SLOW_WAIT)
|
|
350
|
+
try {
|
|
351
|
+
logger.warn?.(
|
|
352
|
+
`Acquired the exclusive '${UPDATE_ATTRIBUTES_LOCK}' lock on ${scopeDescription} after waiting ${Math.round(waited)}ms`
|
|
353
|
+
);
|
|
354
|
+
} catch {}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function runWithUpdateAttributesLock<Callback extends () => unknown>(
|
|
298
358
|
rootStore: RocksDatabase,
|
|
299
359
|
scopeDescription: string,
|
|
300
360
|
callback: Callback & (ReturnType<Callback> extends PromiseLike<unknown> ? never : unknown)
|
|
301
361
|
): ReturnType<Callback> {
|
|
302
|
-
acquireUpdateAttributesLock(rootStore, scopeDescription);
|
|
303
362
|
try {
|
|
304
363
|
const result = callback();
|
|
305
364
|
if (typeof (result as any)?.then === 'function') {
|
|
@@ -318,6 +377,27 @@ export function withUpdateAttributesLock<Callback extends () => unknown>(
|
|
|
318
377
|
releaseUpdateAttributesLock(rootStore);
|
|
319
378
|
}
|
|
320
379
|
}
|
|
380
|
+
|
|
381
|
+
export function withUpdateAttributesLock<Callback extends () => unknown>(
|
|
382
|
+
rootStore: RocksDatabase,
|
|
383
|
+
scopeDescription: string,
|
|
384
|
+
callback: Callback & (ReturnType<Callback> extends PromiseLike<unknown> ? never : unknown)
|
|
385
|
+
): ReturnType<Callback> {
|
|
386
|
+
acquireUpdateAttributesLock(rootStore, scopeDescription);
|
|
387
|
+
return runWithUpdateAttributesLock(rootStore, scopeDescription, callback);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function withUpdateAttributesLockNonBlocking<Callback extends () => unknown>(
|
|
391
|
+
rootStore: RocksDatabase,
|
|
392
|
+
scopeDescription: string,
|
|
393
|
+
callback: Callback & (ReturnType<Callback> extends PromiseLike<unknown> ? never : unknown)
|
|
394
|
+
): ReturnType<Callback> | Promise<ReturnType<Callback>> {
|
|
395
|
+
if (rootStore.tryLock(updateAttributesLockKey))
|
|
396
|
+
return runWithUpdateAttributesLock(rootStore, scopeDescription, callback);
|
|
397
|
+
return acquireUpdateAttributesLockAsync(rootStore, scopeDescription).then(() =>
|
|
398
|
+
runWithUpdateAttributesLock(rootStore, scopeDescription, callback)
|
|
399
|
+
);
|
|
400
|
+
}
|
|
321
401
|
// Tolerate a redundant column family drop. Drops are broadcast to every worker
|
|
322
402
|
// thread and each holds its own handle to the same underlying family, so a
|
|
323
403
|
// concurrent worker may already have dropped it; the storage engine reports
|
|
@@ -677,7 +757,11 @@ export function makeTable(options) {
|
|
|
677
757
|
cacheControl,
|
|
678
758
|
isBranch,
|
|
679
759
|
fullTextIndexes = [],
|
|
760
|
+
fullTextIndexGenerations = Object.create(null),
|
|
761
|
+
fullTextIndexRetirements = [],
|
|
680
762
|
} = options;
|
|
763
|
+
const tableRootStore = primaryStore.rootStore;
|
|
764
|
+
const tableRootPath = tableRootStore.path;
|
|
681
765
|
let { expirationMS: expirationMs, evictionMS: evictionMs, audit, trackDeletes } = options;
|
|
682
766
|
// Set when the TTL exists only on this thread: either application code configured it at runtime, or
|
|
683
767
|
// an isolated application's schema was declared here. Hydrating persisted metadata does not set it:
|
|
@@ -702,6 +786,9 @@ export function makeTable(options) {
|
|
|
702
786
|
let hasSourceGet: any;
|
|
703
787
|
let primaryKeyAttribute: Attribute | undefined;
|
|
704
788
|
let lastEvictionCompletion: Promise<void> = Promise.resolve();
|
|
789
|
+
let recordExpirationCompletion: Promise<void> = Promise.resolve();
|
|
790
|
+
const maintenanceCommits = new Set<Promise<unknown>>();
|
|
791
|
+
let maintenanceClosed = false;
|
|
705
792
|
let droppingTable = false;
|
|
706
793
|
let createdTimeProperty: Attribute | undefined,
|
|
707
794
|
updatedTimeProperty: Attribute | undefined,
|
|
@@ -721,8 +808,8 @@ export function makeTable(options) {
|
|
|
721
808
|
let cleanupInterval = 86400000;
|
|
722
809
|
let cleanupPriority = 0;
|
|
723
810
|
let lastCleanupInterval: number | undefined;
|
|
724
|
-
let cleanupTimer: NodeJS.Timeout;
|
|
725
|
-
let recordExpirationInterval: NodeJS.Timeout;
|
|
811
|
+
let cleanupTimer: NodeJS.Timeout | undefined;
|
|
812
|
+
let recordExpirationInterval: NodeJS.Timeout | undefined;
|
|
726
813
|
// a reclamation pass awaits a scheduled cleanup, which only settles from its timer
|
|
727
814
|
const pendingCleanupResolvers = new Set<() => void>();
|
|
728
815
|
let disposed = false;
|
|
@@ -903,9 +990,10 @@ export function makeTable(options) {
|
|
|
903
990
|
}
|
|
904
991
|
return { txnLogKey: version, nodeId };
|
|
905
992
|
}
|
|
906
|
-
//
|
|
907
|
-
// dropping one would advance the source cursor past a write that never landed.
|
|
993
|
+
// Teardown rejects every producer; source/replay paths only bypass derived-index lag shedding.
|
|
908
994
|
function assertDerivedIndexAdmission(options: any, transaction: any) {
|
|
995
|
+
if (databaseDropPrepared(tableRootPath) || databaseCommitsSuspended(tableRootStore))
|
|
996
|
+
throw new DatabaseClosingError(databaseName, !transaction?.root && !transaction?.snapshotFree);
|
|
909
997
|
if (options?.isNotification || transaction?.sourceApply || transaction?.isReplay) return;
|
|
910
998
|
const reason = derivedIndexWriteRejection(auditStore, tableId);
|
|
911
999
|
if (reason) throw new DerivedIndexLagError(reason);
|
|
@@ -931,6 +1019,19 @@ export function makeTable(options) {
|
|
|
931
1019
|
error.statusCode = 404;
|
|
932
1020
|
throw error;
|
|
933
1021
|
}
|
|
1022
|
+
function currentFullTextDescriptor(): { key: string; descriptor: any } | undefined {
|
|
1023
|
+
const namedKey = `${tableName}/${primaryKey}`;
|
|
1024
|
+
const namedDescriptor = (dbisDb as any).getSync(namedKey);
|
|
1025
|
+
const key = namedDescriptor?.isPrimaryKey ? namedKey : `${tableName}/`;
|
|
1026
|
+
const descriptor = (dbisDb as any).getSync(key);
|
|
1027
|
+
if (
|
|
1028
|
+
!descriptor ||
|
|
1029
|
+
(descriptor.tableId != null && descriptor.tableId !== tableId) ||
|
|
1030
|
+
descriptor.generation !== tableGeneration
|
|
1031
|
+
)
|
|
1032
|
+
return;
|
|
1033
|
+
return { key, descriptor };
|
|
1034
|
+
}
|
|
934
1035
|
class TableResource<Record extends object = any> extends Resource<Record> {
|
|
935
1036
|
#record: any; // the stored/frozen record from the database and stored in the cache (should not be modified directly)
|
|
936
1037
|
#changes: any; // the changes to the record that have been made (should not be modified directly)
|
|
@@ -981,12 +1082,51 @@ export function makeTable(options) {
|
|
|
981
1082
|
static derivedIndexRuntime:
|
|
982
1083
|
| {
|
|
983
1084
|
close(dropping?: boolean): Promise<void>;
|
|
1085
|
+
fullTextDefinitions?(): readonly FullTextDefinition[];
|
|
1086
|
+
matchesCurrent?(): boolean;
|
|
984
1087
|
restoreAfterFailedDrop?(): typeof TableResource.derivedIndexRuntime;
|
|
1088
|
+
retireAfterConfirmedDrop?(definitions?: readonly Pick<FullTextDefinition, 'name'>[]): Promise<boolean>;
|
|
985
1089
|
completeDrop?(dropped?: boolean): void;
|
|
986
1090
|
}
|
|
987
1091
|
| undefined;
|
|
988
1092
|
static audit = audit;
|
|
989
1093
|
static fullTextIndexes: FullTextDefinition[] = fullTextIndexes;
|
|
1094
|
+
static fullTextIndexGenerations: FullTextIndexGenerations = fullTextIndexGenerations;
|
|
1095
|
+
static fullTextIndexRetirements: string[] = fullTextIndexRetirements;
|
|
1096
|
+
static hasCurrentFullTextIndexRetirements(names: readonly string[]): boolean | Promise<boolean> {
|
|
1097
|
+
if (names.length === 0 || !(primaryStore.rootStore instanceof RocksDatabase)) return false;
|
|
1098
|
+
return withUpdateAttributesLockNonBlocking(
|
|
1099
|
+
primaryStore.rootStore,
|
|
1100
|
+
`verify full-text retirement for '${databaseName}.${tableName}'`,
|
|
1101
|
+
() => {
|
|
1102
|
+
const current = currentFullTextDescriptor();
|
|
1103
|
+
if (!current) return false;
|
|
1104
|
+
const pending = new Set(persistedFullTextIndexNames(current.descriptor.fullTextIndexRetirements));
|
|
1105
|
+
return names.every((name) => pending.has(name));
|
|
1106
|
+
}
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
static completeFullTextIndexRetirements(names: readonly string[]): void | Promise<void> {
|
|
1110
|
+
if (names.length === 0 || !(primaryStore.rootStore instanceof RocksDatabase)) return;
|
|
1111
|
+
const completed = new Set(names);
|
|
1112
|
+
return withUpdateAttributesLockNonBlocking(
|
|
1113
|
+
primaryStore.rootStore,
|
|
1114
|
+
`complete full-text retirement for '${databaseName}.${tableName}'`,
|
|
1115
|
+
() => {
|
|
1116
|
+
const current = currentFullTextDescriptor();
|
|
1117
|
+
if (!current) return;
|
|
1118
|
+
const { key, descriptor } = current;
|
|
1119
|
+
const remaining = persistedFullTextIndexNames(descriptor.fullTextIndexRetirements).filter(
|
|
1120
|
+
(name) => !completed.has(name)
|
|
1121
|
+
);
|
|
1122
|
+
const updated = { ...descriptor };
|
|
1123
|
+
if (remaining.length > 0) updated.fullTextIndexRetirements = remaining.map((name) => ({ name }));
|
|
1124
|
+
else delete updated.fullTextIndexRetirements;
|
|
1125
|
+
(dbisDb as any).putSync(key, updated);
|
|
1126
|
+
this.fullTextIndexRetirements = remaining;
|
|
1127
|
+
}
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
990
1130
|
static databasePath = databasePath;
|
|
991
1131
|
static databaseName = databaseName;
|
|
992
1132
|
static attributes = attributes;
|
|
@@ -1010,6 +1150,9 @@ export function makeTable(options) {
|
|
|
1010
1150
|
static userEmbedders: { [name: string]: Embedder } = {};
|
|
1011
1151
|
static userSetEmbedders: Set<string> = new Set();
|
|
1012
1152
|
static embedAttributes: EmbedAttribute[] = (attributes as any[]).filter((a) => a?.embed);
|
|
1153
|
+
static userDeciders: { [name: string]: Decider } = {};
|
|
1154
|
+
static userSetDeciders: Set<string> = new Set();
|
|
1155
|
+
static decideAttributes: DecideAttribute[] = (attributes as any[]).filter((a) => a?.decide);
|
|
1013
1156
|
static source?: typeof TableResource;
|
|
1014
1157
|
declare static sourceOptions: any;
|
|
1015
1158
|
declare static intermediateSource: boolean;
|
|
@@ -1382,7 +1525,11 @@ export function makeTable(options) {
|
|
|
1382
1525
|
} catch {}
|
|
1383
1526
|
throw error;
|
|
1384
1527
|
}
|
|
1385
|
-
return
|
|
1528
|
+
return commitTrackedRocksTransaction(
|
|
1529
|
+
seqTransaction,
|
|
1530
|
+
dbisDb,
|
|
1531
|
+
(primaryStore as any).rootStore
|
|
1532
|
+
).catch((error) => {
|
|
1386
1533
|
// A rejected commit leaves the handle open too, so release it here as well —
|
|
1387
1534
|
// same reason as the staging failure above, and the same shape as the
|
|
1388
1535
|
// eviction paths' commit failures (see evict/commitItems below).
|
|
@@ -1616,6 +1763,11 @@ export function makeTable(options) {
|
|
|
1616
1763
|
request: Context,
|
|
1617
1764
|
resourceOptions?: any
|
|
1618
1765
|
): Promise<TableResource<Record>> | TableResource<Record> {
|
|
1766
|
+
if (databaseDropPrepared(tableRootPath) || databaseCommitsSuspended(tableRootStore))
|
|
1767
|
+
throw new DatabaseClosingError(
|
|
1768
|
+
databaseName,
|
|
1769
|
+
!(request as any)?.transaction?.root && !(request as any)?.transaction?.snapshotFree
|
|
1770
|
+
);
|
|
1619
1771
|
const resource: TableResource = super.getResource(target, request, resourceOptions) as any;
|
|
1620
1772
|
if (this.loadAsInstance !== false) {
|
|
1621
1773
|
return resource._loadRecord(target, request, resourceOptions);
|
|
@@ -2040,28 +2192,46 @@ export function makeTable(options) {
|
|
|
2040
2192
|
// same-name recreate must not race an owner still applying to the old generation.
|
|
2041
2193
|
const derivedIndexRuntime = TableResource.derivedIndexRuntime;
|
|
2042
2194
|
const restoreDerivedIndexesAfterFailedDrop = () => {
|
|
2195
|
+
TableResource.resumeMaintenance();
|
|
2043
2196
|
try {
|
|
2044
|
-
|
|
2197
|
+
const restored = derivedIndexRuntime?.restoreAfterFailedDrop?.();
|
|
2198
|
+
if (TableResource.derivedIndexRuntime === derivedIndexRuntime) TableResource.derivedIndexRuntime = restored;
|
|
2045
2199
|
} catch (restoreError) {
|
|
2046
|
-
TableResource.derivedIndexRuntime = undefined;
|
|
2200
|
+
if (TableResource.derivedIndexRuntime === derivedIndexRuntime) TableResource.derivedIndexRuntime = undefined;
|
|
2047
2201
|
logger.error?.(
|
|
2048
2202
|
`Could not restore derived indexes after failed drop of ${databaseName}.${TableResource.tableName}`,
|
|
2049
2203
|
restoreError
|
|
2050
2204
|
);
|
|
2051
2205
|
}
|
|
2052
2206
|
};
|
|
2207
|
+
try {
|
|
2208
|
+
await TableResource.closeMaintenance();
|
|
2209
|
+
} catch (error) {
|
|
2210
|
+
TableResource.resumeMaintenance();
|
|
2211
|
+
throw error;
|
|
2212
|
+
}
|
|
2053
2213
|
try {
|
|
2054
2214
|
await derivedIndexRuntime?.close(true);
|
|
2055
2215
|
} catch (error) {
|
|
2056
2216
|
restoreDerivedIndexesAfterFailedDrop();
|
|
2057
2217
|
throw error;
|
|
2058
2218
|
}
|
|
2219
|
+
let releaseFullTextRetirementFence: (() => void) | undefined;
|
|
2220
|
+
const releaseFullTextRetirement = () => {
|
|
2221
|
+
const release = releaseFullTextRetirementFence;
|
|
2222
|
+
releaseFullTextRetirementFence = undefined;
|
|
2223
|
+
release?.();
|
|
2224
|
+
};
|
|
2059
2225
|
const abortStaleDrop = () => {
|
|
2226
|
+
releaseFullTextRetirement();
|
|
2060
2227
|
derivedIndexRuntime?.completeDrop?.(false);
|
|
2061
2228
|
TableResource.derivedIndexRuntime = undefined;
|
|
2062
2229
|
TableResource.cleanup();
|
|
2063
2230
|
if (databases[databaseName]?.[tableName] === TableResource) delete databases[databaseName][tableName];
|
|
2064
2231
|
};
|
|
2232
|
+
let fullTextDefinitionsForRetirement: Array<Pick<FullTextDefinition, 'name'>> = [
|
|
2233
|
+
...TableResource.fullTextIndexes,
|
|
2234
|
+
];
|
|
2065
2235
|
let dropIdentityConfirmed = databaseName !== databasePath;
|
|
2066
2236
|
let primaryCatalogKey = TableResource.tableName + '/';
|
|
2067
2237
|
let storeGeneration: string | undefined;
|
|
@@ -2092,6 +2262,29 @@ export function makeTable(options) {
|
|
|
2092
2262
|
return false;
|
|
2093
2263
|
dropGeneration = primaryMeta.dropGeneration;
|
|
2094
2264
|
storeGeneration = primaryMeta.generation;
|
|
2265
|
+
const durableFullTextDefinitions =
|
|
2266
|
+
rootStore instanceof RocksDatabase
|
|
2267
|
+
? [
|
|
2268
|
+
...persistedFullTextIndexNames(primaryMeta.fullTextIndexes),
|
|
2269
|
+
...persistedFullTextIndexNames(primaryMeta.fullTextIndexRetirements),
|
|
2270
|
+
].map((name) => ({ name }))
|
|
2271
|
+
: [];
|
|
2272
|
+
const attachedFullTextDefinitions = derivedIndexRuntime?.fullTextDefinitions?.() ?? [];
|
|
2273
|
+
const definitionsByName = new Map(
|
|
2274
|
+
[...durableFullTextDefinitions, ...attachedFullTextDefinitions].map((definition) => [
|
|
2275
|
+
definition.name,
|
|
2276
|
+
definition,
|
|
2277
|
+
])
|
|
2278
|
+
);
|
|
2279
|
+
fullTextDefinitionsForRetirement = [...definitionsByName.values()];
|
|
2280
|
+
if (fullTextDefinitionsForRetirement.length > 0 && !releaseFullTextRetirementFence) {
|
|
2281
|
+
releaseFullTextRetirementFence = acquireFullTextRetirementFence(rootStore, TableResource.tableName);
|
|
2282
|
+
if (!releaseFullTextRetirementFence)
|
|
2283
|
+
throw new ClientError(
|
|
2284
|
+
`Cannot drop '${databaseName}.${TableResource.tableName}' while its full-text storage is being retired`,
|
|
2285
|
+
409
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2095
2288
|
if (primaryMeta.dropping) return true;
|
|
2096
2289
|
primaryMeta.dropping = true;
|
|
2097
2290
|
// Stamps this drop's identity so the interrupted-drop retry budget in
|
|
@@ -2123,11 +2316,13 @@ export function makeTable(options) {
|
|
|
2123
2316
|
if (typeof tombstoneWrite?.then === 'function') await tombstoneWrite;
|
|
2124
2317
|
}
|
|
2125
2318
|
} catch (error) {
|
|
2319
|
+
releaseFullTextRetirement();
|
|
2126
2320
|
restoreDerivedIndexesAfterFailedDrop();
|
|
2127
2321
|
throw error;
|
|
2128
2322
|
}
|
|
2129
2323
|
}
|
|
2130
2324
|
if (!dropIdentityConfirmed) {
|
|
2325
|
+
releaseFullTextRetirement();
|
|
2131
2326
|
abortStaleDrop();
|
|
2132
2327
|
return;
|
|
2133
2328
|
}
|
|
@@ -2150,12 +2345,19 @@ export function makeTable(options) {
|
|
|
2150
2345
|
try {
|
|
2151
2346
|
if (!dropGeneration)
|
|
2152
2347
|
throw new Error(`Cannot drop ${databaseName}.${tableName}: its catalog tombstone has no drop generation`);
|
|
2153
|
-
await retireRocksStores(storeGeneration, dropGeneration);
|
|
2348
|
+
const retired = await retireRocksStores(storeGeneration, dropGeneration);
|
|
2349
|
+
if (!retired) {
|
|
2350
|
+
derivedIndexRuntime?.completeDrop?.();
|
|
2351
|
+
releaseFullTextRetirement();
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2154
2354
|
} catch (error) {
|
|
2355
|
+
releaseFullTextRetirement();
|
|
2155
2356
|
derivedIndexRuntime?.completeDrop?.();
|
|
2156
2357
|
throw error;
|
|
2157
2358
|
}
|
|
2158
2359
|
derivedIndexRuntime?.completeDrop?.();
|
|
2360
|
+
releaseFullTextRetirement();
|
|
2159
2361
|
return;
|
|
2160
2362
|
}
|
|
2161
2363
|
try {
|
|
@@ -2165,6 +2367,7 @@ export function makeTable(options) {
|
|
|
2165
2367
|
}
|
|
2166
2368
|
}
|
|
2167
2369
|
} catch (error) {
|
|
2370
|
+
releaseFullTextRetirement();
|
|
2168
2371
|
derivedIndexRuntime?.completeDrop?.();
|
|
2169
2372
|
throw error;
|
|
2170
2373
|
}
|
|
@@ -2206,6 +2409,7 @@ export function makeTable(options) {
|
|
|
2206
2409
|
removed = removeTombstonedCatalog();
|
|
2207
2410
|
if (removed) await dbisDb.committed;
|
|
2208
2411
|
} catch (error) {
|
|
2412
|
+
releaseFullTextRetirement();
|
|
2209
2413
|
derivedIndexRuntime?.completeDrop?.();
|
|
2210
2414
|
throw error;
|
|
2211
2415
|
}
|
|
@@ -2226,22 +2430,41 @@ export function makeTable(options) {
|
|
|
2226
2430
|
await primaryStore.close();
|
|
2227
2431
|
fs.unlinkSync(primaryStore.path);
|
|
2228
2432
|
} catch (error) {
|
|
2433
|
+
releaseFullTextRetirement();
|
|
2229
2434
|
derivedIndexRuntime?.completeDrop?.();
|
|
2230
2435
|
throw error;
|
|
2231
2436
|
}
|
|
2232
2437
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2438
|
+
try {
|
|
2439
|
+
const message: any = new SchemaEventMsg(process.pid, OPERATIONS_ENUM.DROP_TABLE, databaseName, tableName);
|
|
2440
|
+
message.dropTableId = tableId;
|
|
2441
|
+
await signalling.signalSchemaChange(message);
|
|
2442
|
+
await retireFullTextStorage();
|
|
2443
|
+
derivedIndexRuntime?.completeDrop?.();
|
|
2444
|
+
} finally {
|
|
2445
|
+
releaseFullTextRetirement();
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
async function retireFullTextStorage(): Promise<boolean> {
|
|
2449
|
+
if (fullTextDefinitionsForRetirement.length === 0) return true;
|
|
2450
|
+
if (derivedIndexRuntime?.retireAfterConfirmedDrop)
|
|
2451
|
+
return derivedIndexRuntime.retireAfterConfirmedDrop(fullTextDefinitionsForRetirement);
|
|
2452
|
+
else {
|
|
2453
|
+
const { retireFullTextIndexes } = await import('./derivedIndexes.ts');
|
|
2454
|
+
return retireFullTextIndexes(TableResource, fullTextDefinitionsForRetirement);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2237
2457
|
|
|
2238
|
-
async function retireRocksStores(generation: string | undefined, dropGeneration: string) {
|
|
2458
|
+
async function retireRocksStores(generation: string | undefined, dropGeneration: string): Promise<boolean> {
|
|
2239
2459
|
const releaseDropMark = markDropInProgress(dropGeneration);
|
|
2240
2460
|
try {
|
|
2241
2461
|
const message: any = new SchemaEventMsg(process.pid, OPERATIONS_ENUM.DROP_TABLE, databaseName, tableName);
|
|
2242
2462
|
message.dropGeneration = dropGeneration;
|
|
2243
2463
|
message.dropTableId = tableId;
|
|
2244
2464
|
await signalling.signalSchemaChange(message);
|
|
2465
|
+
// Keep the tombstone's native index names durable until every peer writer
|
|
2466
|
+
// has stopped and the wrapper has retired their storage.
|
|
2467
|
+
if (!(await retireFullTextStorage())) return false;
|
|
2245
2468
|
const removed = withUpdateAttributesLock(rootStore, `table '${databaseName}.${tableName}'`, () => {
|
|
2246
2469
|
const stores = storeNamesFor(dbisDb, tableName, generation);
|
|
2247
2470
|
const retiredStores = recordRetiredGeneration(
|
|
@@ -2294,6 +2517,7 @@ export function makeTable(options) {
|
|
|
2294
2517
|
const settled = await settlePhysicalDrops(rootStore, label);
|
|
2295
2518
|
await sweepDroppedTableBlobs(primaryStore, label);
|
|
2296
2519
|
if (!settled) finishDroppedTableBlobSweep(rootStore, primaryStore, label);
|
|
2520
|
+
return true;
|
|
2297
2521
|
} finally {
|
|
2298
2522
|
releaseDropMark();
|
|
2299
2523
|
}
|
|
@@ -2955,6 +3179,7 @@ export function makeTable(options) {
|
|
|
2955
3179
|
* Evicting a record will remove it from a caching table. This is not considered a canonical data change, and it is assumed that retrieving this record from the source will still yield the same record, this is only removing the local copy of the record.
|
|
2956
3180
|
*/
|
|
2957
3181
|
static evict(id, existingRecord, existingVersion) {
|
|
3182
|
+
if (maintenanceClosed) return Promise.resolve();
|
|
2958
3183
|
let entry;
|
|
2959
3184
|
const lmdbTransaction = txnForContext({ transaction: new DatabaseTransaction() });
|
|
2960
3185
|
let transaction = lmdbTransaction.getReadTxn();
|
|
@@ -3001,23 +3226,27 @@ export function makeTable(options) {
|
|
|
3001
3226
|
// a plain resolution object rather than a promise — return the store's write promises instead, so
|
|
3002
3227
|
// the caller gets a real thenable that resolves once the removal is durable.
|
|
3003
3228
|
(lmdbTransaction as any).commit();
|
|
3004
|
-
return
|
|
3005
|
-
|
|
3006
|
-
|
|
3229
|
+
return trackMaintenanceCommit(
|
|
3230
|
+
Promise.resolve(lmdbCompletion).catch((error) => {
|
|
3231
|
+
logger.warn?.('Error evicting record', id, error);
|
|
3232
|
+
})
|
|
3233
|
+
);
|
|
3007
3234
|
}
|
|
3008
3235
|
// RocksDB: eviction writes went directly into the raw transaction via options; commit it directly,
|
|
3009
3236
|
// as DatabaseTransaction.commit() would abort it (no tracked writes). The raw commit bypasses
|
|
3010
3237
|
// DatabaseTransaction's ERR_BUSY retry, so a concurrent-write conflict rejects here — swallow it
|
|
3011
3238
|
// (abandon the eviction) and log anything unexpected, rather than letting it crash the process.
|
|
3012
|
-
return (
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3239
|
+
return trackMaintenanceCommit(
|
|
3240
|
+
commitTrackedRocksTransaction(transaction as RocksTransaction, primaryStore).catch((error) => {
|
|
3241
|
+
// The commit failed, so the read-snapshot/transaction handle is still open — release it, as the
|
|
3242
|
+
// batched-eviction path does on its own commit failures. committed===true skips the finally abort.
|
|
3243
|
+
try {
|
|
3244
|
+
(transaction as any).abort();
|
|
3245
|
+
} catch {}
|
|
3246
|
+
if (error?.code === 'ERR_BUSY') logger.trace?.('Abandoned eviction of busy record', id);
|
|
3247
|
+
else logger.warn?.('Error evicting record', id, error);
|
|
3248
|
+
})
|
|
3249
|
+
);
|
|
3021
3250
|
} finally {
|
|
3022
3251
|
if (!committed) {
|
|
3023
3252
|
// Skip path or thrown error: abort instead of committing so we don't apply
|
|
@@ -4386,22 +4615,33 @@ export function makeTable(options) {
|
|
|
4386
4615
|
},
|
|
4387
4616
|
};
|
|
4388
4617
|
this.#savingOperation = write;
|
|
4389
|
-
//
|
|
4390
|
-
//
|
|
4391
|
-
//
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4618
|
+
// The hooks run before `addWrite` so the derived values are on the record at commit (the
|
|
4619
|
+
// txn `before` slot runs after commit). They see the payload before table validation, and a
|
|
4620
|
+
// tracked-instance mutation that sets the source via accessors after update() is not seen.
|
|
4621
|
+
let modelHooksBefore: WriteHook | undefined;
|
|
4622
|
+
try {
|
|
4623
|
+
modelHooksBefore =
|
|
4624
|
+
(TableResource.embedAttributes.length || TableResource.decideAttributes.length) && !isReadOnlyMode()
|
|
4625
|
+
? combineWriteHooks(
|
|
4626
|
+
buildEmbedBefore(
|
|
4627
|
+
recordUpdate,
|
|
4628
|
+
context,
|
|
4629
|
+
options,
|
|
4630
|
+
TableResource.embedAttributes,
|
|
4631
|
+
TableResource.userEmbedders
|
|
4632
|
+
),
|
|
4633
|
+
buildDecideBefore(
|
|
4634
|
+
recordUpdate,
|
|
4635
|
+
context,
|
|
4636
|
+
options,
|
|
4637
|
+
TableResource.decideAttributes,
|
|
4638
|
+
TableResource.userDeciders
|
|
4639
|
+
)
|
|
4640
|
+
)
|
|
4641
|
+
: undefined;
|
|
4642
|
+
} catch (err) {
|
|
4643
|
+
return Promise.reject(err);
|
|
4644
|
+
}
|
|
4405
4645
|
const proceed = (): any => {
|
|
4406
4646
|
// On a source/replication apply (`isNotification`), the record's already-saved blobs were
|
|
4407
4647
|
// received out-of-band for THIS write, so track them for skip/abort cleanup (harper-pro#406).
|
|
@@ -4414,7 +4654,7 @@ export function makeTable(options) {
|
|
|
4414
4654
|
);
|
|
4415
4655
|
return transaction.addWrite(write as any);
|
|
4416
4656
|
};
|
|
4417
|
-
return
|
|
4657
|
+
return modelHooksBefore ? modelHooksBefore((context as any)?.signal).then(proceed) : proceed();
|
|
4418
4658
|
}
|
|
4419
4659
|
|
|
4420
4660
|
async delete(target: RequestTargetOrId): Promise<boolean> {
|
|
@@ -5684,7 +5924,10 @@ export function makeTable(options) {
|
|
|
5684
5924
|
// messages during this loop because the snapshot:false cursor will pick them up itself.
|
|
5685
5925
|
pendingRealTimeQueue = null;
|
|
5686
5926
|
dropDuringReplay = true;
|
|
5687
|
-
|
|
5927
|
+
// subscription.startTime is the resume cursor (exclusive) and RocksDB gives every record of a
|
|
5928
|
+
// transaction the same txnLogKey, so it only moves to a key once all of that key's records are
|
|
5929
|
+
// handled; an early return leaves it before a partly delivered transaction.
|
|
5930
|
+
let handledTxnLogKey: number | undefined;
|
|
5688
5931
|
try {
|
|
5689
5932
|
for (const auditRecord of auditStore.getRange({
|
|
5690
5933
|
start: startTime,
|
|
@@ -5698,17 +5941,22 @@ export function makeTable(options) {
|
|
|
5698
5941
|
}
|
|
5699
5942
|
if (auditRecord.tableId !== tableId || auditRecord.type === 'evict') continue;
|
|
5700
5943
|
if (isLockControlType(auditRecord.type)) continue;
|
|
5944
|
+
if (handledTxnLogKey !== undefined && auditRecord.txnLogKey !== handledTxnLogKey) {
|
|
5945
|
+
subscription!.startTime = handledTxnLogKey;
|
|
5946
|
+
}
|
|
5701
5947
|
const id = auditRecord.recordId;
|
|
5702
|
-
subscription!.startTime = auditRecord.txnLogKey;
|
|
5703
5948
|
if (thisId == null || isDescendantId(thisId, id)) {
|
|
5704
5949
|
const event = eventFromAudit(id, auditRecord, auditRecord.txnLogKey);
|
|
5705
|
-
if (
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5950
|
+
if (event) {
|
|
5951
|
+
if (!send(event)) return;
|
|
5952
|
+
if (subscription.queue?.length > EVENT_HIGH_WATER_MARK) {
|
|
5953
|
+
if ((await subscription.waitForDrain()) === false) return;
|
|
5954
|
+
}
|
|
5709
5955
|
}
|
|
5710
5956
|
}
|
|
5957
|
+
handledTxnLogKey = auditRecord.txnLogKey;
|
|
5711
5958
|
}
|
|
5959
|
+
if (handledTxnLogKey !== undefined) subscription!.startTime = handledTxnLogKey;
|
|
5712
5960
|
} finally {
|
|
5713
5961
|
// replay is done, we can start sending real-time messages again
|
|
5714
5962
|
dropDuringReplay = false;
|
|
@@ -6683,13 +6931,24 @@ export function makeTable(options) {
|
|
|
6683
6931
|
static updatedAttributes() {
|
|
6684
6932
|
// Refresh on every call: schema reload mutates `attributes` in place, so the
|
|
6685
6933
|
// class-construction snapshot would otherwise go stale.
|
|
6934
|
+
// Declarations are refused before they are saved; here a descriptor an earlier build
|
|
6935
|
+
// accepted must still load, so a violation is logged and the table keeps working.
|
|
6936
|
+
try {
|
|
6937
|
+
assertDerivedFieldOwnership(this.attributes as any[]);
|
|
6938
|
+
} catch (error) {
|
|
6939
|
+
console.error(`Derived attributes of table "${tableName}" conflict: ${(error as Error).message}`);
|
|
6940
|
+
}
|
|
6686
6941
|
this.embedAttributes = (this.attributes as any[]).filter((a) => a?.embed);
|
|
6942
|
+
this.decideAttributes = (this.attributes as any[]).filter((a) => a?.decide);
|
|
6687
6943
|
expiresAtProperty = this.attributes.find((attribute) => attribute.expiresAt);
|
|
6688
|
-
// Drop registry entries for attributes that are no longer `@embed`, so a dropped
|
|
6689
|
-
// directive doesn't leave a stale
|
|
6944
|
+
// Drop registry entries for attributes that are no longer `@embed` / `@decide`, so a dropped
|
|
6945
|
+
// directive doesn't leave a stale hook or block a default refresh on re-add.
|
|
6690
6946
|
const embedNames = new Set(this.embedAttributes.map((a) => a.name));
|
|
6691
6947
|
for (const name of Object.keys(this.userEmbedders)) if (!embedNames.has(name)) delete this.userEmbedders[name];
|
|
6692
6948
|
for (const name of this.userSetEmbedders) if (!embedNames.has(name)) this.userSetEmbedders.delete(name);
|
|
6949
|
+
const decideNames = new Set(this.decideAttributes.map((a) => a.name));
|
|
6950
|
+
for (const name of Object.keys(this.userDeciders)) if (!decideNames.has(name)) delete this.userDeciders[name];
|
|
6951
|
+
for (const name of this.userSetDeciders) if (!decideNames.has(name)) this.userSetDeciders.delete(name);
|
|
6693
6952
|
propertyResolvers = this.propertyResolvers = {
|
|
6694
6953
|
$id: (object, context, entry) => ({ value: entry.key }),
|
|
6695
6954
|
$updatedtime: (object, context, entry) => entry.version,
|
|
@@ -6726,9 +6985,12 @@ export function makeTable(options) {
|
|
|
6726
6985
|
const computed = attribute.computed;
|
|
6727
6986
|
// Register the default embedder unless an author override is set. Sits outside
|
|
6728
6987
|
// the resolver chain below so `@embed` fields still flow through auto-HNSW indexing.
|
|
6729
|
-
if (attribute.embed && !
|
|
6988
|
+
if (attribute.embed && !this.userSetEmbedders.has(attribute.name)) {
|
|
6730
6989
|
this.userEmbedders[attribute.name] = createDefaultEmbedder(attribute.embed);
|
|
6731
6990
|
}
|
|
6991
|
+
if (attribute.decide && !this.userSetDeciders.has(attribute.name)) {
|
|
6992
|
+
this.userDeciders[attribute.name] = createDefaultDecider(attribute.decide);
|
|
6993
|
+
}
|
|
6732
6994
|
if (relationship) {
|
|
6733
6995
|
if (attribute.indexed) {
|
|
6734
6996
|
console.error(
|
|
@@ -6960,6 +7222,26 @@ export function makeTable(options) {
|
|
|
6960
7222
|
this.userEmbedders[attribute_name] = embedder;
|
|
6961
7223
|
this.userSetEmbedders.add(attribute_name);
|
|
6962
7224
|
}
|
|
7225
|
+
/**
|
|
7226
|
+
* Override the default decider for a `@decide` attribute. Return `{ value, probability }`
|
|
7227
|
+
* to store at the attribute and its confidence attribute, or `null` to clear both. The
|
|
7228
|
+
* value must be one the directive allows, and the probability is required when the
|
|
7229
|
+
* directive names a confidence attribute. Like an embedder, the decider receives the write
|
|
7230
|
+
* payload, not the post-merge record, and a `signal` that aborts when a sibling hook fails.
|
|
7231
|
+
*/
|
|
7232
|
+
static setDecideAttribute(attribute_name: string, decider: Decider): void {
|
|
7233
|
+
const attribute = findAttribute(attributes, attribute_name);
|
|
7234
|
+
if (!attribute) {
|
|
7235
|
+
console.error(`The attribute "${attribute_name}" does not exist in the table "${tableName}"`);
|
|
7236
|
+
return;
|
|
7237
|
+
}
|
|
7238
|
+
if (!attribute.decide) {
|
|
7239
|
+
console.error(`The attribute "${attribute_name}" is not declared with @decide in the table "${tableName}"`);
|
|
7240
|
+
return;
|
|
7241
|
+
}
|
|
7242
|
+
this.userDeciders[attribute_name] = decider;
|
|
7243
|
+
this.userSetDeciders.add(attribute_name);
|
|
7244
|
+
}
|
|
6963
7245
|
static async deleteHistory(endTime = 0, cleanupDeletedRecords = false): Promise<number> {
|
|
6964
7246
|
const maxConcurrentRemovals = isRocksDB ? MAX_CONCURRENT_HISTORY_REMOVALS : MAX_CONCURRENT_LMDB_HISTORY_REMOVALS;
|
|
6965
7247
|
const inFlightRemovals = new Set<Promise<void>>();
|
|
@@ -7136,31 +7418,99 @@ export function makeTable(options) {
|
|
|
7136
7418
|
} while (count < 1000 && nextVersion);
|
|
7137
7419
|
return history.reverse();
|
|
7138
7420
|
}
|
|
7139
|
-
static clear() {
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7421
|
+
static async clear() {
|
|
7422
|
+
const rootStore = primaryStore.rootStore;
|
|
7423
|
+
const assertNoFullTextDeclaration = () => {
|
|
7424
|
+
const namedPrimaryDescriptor = (dbisDb as any).getSync(`${tableName}/${primaryKey}`);
|
|
7425
|
+
const primaryDescriptor = namedPrimaryDescriptor?.isPrimaryKey
|
|
7426
|
+
? namedPrimaryDescriptor
|
|
7427
|
+
: (dbisDb as any).getSync(`${tableName}/`);
|
|
7428
|
+
if (
|
|
7429
|
+
TableResource.fullTextIndexes.length > 0 ||
|
|
7430
|
+
(rootStore instanceof RocksDatabase &&
|
|
7431
|
+
persistedFullTextIndexNames(primaryDescriptor?.fullTextIndexes).length > 0)
|
|
7432
|
+
)
|
|
7433
|
+
throw new ClientError(
|
|
7434
|
+
`Table.clear() is not supported on full-text table '${databaseName}.${tableName}' until whole-table invalidation is crash-safe`,
|
|
7435
|
+
501
|
|
7436
|
+
);
|
|
7437
|
+
};
|
|
7438
|
+
let releaseFullTextClearFence: (() => void) | undefined;
|
|
7439
|
+
let waitForExistingClear: Promise<boolean> | undefined;
|
|
7440
|
+
let admission: void | Promise<void>;
|
|
7441
|
+
if (rootStore instanceof RocksDatabase) {
|
|
7442
|
+
admission = withUpdateAttributesLockNonBlocking(rootStore, `clear table '${databaseName}.${tableName}'`, () => {
|
|
7443
|
+
assertNoFullTextDeclaration();
|
|
7444
|
+
releaseFullTextClearFence = acquireFullTextClearFence(rootStore, tableId);
|
|
7445
|
+
if (!releaseFullTextClearFence) waitForExistingClear = waitForFullTextClear(rootStore, tableId);
|
|
7446
|
+
});
|
|
7447
|
+
} else assertNoFullTextDeclaration();
|
|
7448
|
+
const startClear = () => {
|
|
7449
|
+
if (waitForExistingClear) return waitForExistingClear.then(() => TableResource.clear());
|
|
7450
|
+
// clear the primary store and every secondary index dbi (same pattern used by
|
|
7451
|
+
// runIndexing when rebuilding from scratch), so clear() doesn't leave stale
|
|
7452
|
+
// index entries pointing at records that no longer exist.
|
|
7453
|
+
const promises = [];
|
|
7454
|
+
const settleStartedClears = (synchronousFailure?: { error: unknown }) =>
|
|
7455
|
+
Promise.allSettled(promises)
|
|
7456
|
+
.then((results) => {
|
|
7457
|
+
if (synchronousFailure) throw synchronousFailure.error;
|
|
7458
|
+
const values = [];
|
|
7459
|
+
for (const result of results) {
|
|
7460
|
+
if (result.status === 'rejected') throw result.reason;
|
|
7461
|
+
values.push(result.value);
|
|
7462
|
+
}
|
|
7463
|
+
return values;
|
|
7464
|
+
})
|
|
7465
|
+
.finally(releaseFullTextClearFence);
|
|
7466
|
+
try {
|
|
7467
|
+
promises.push(primaryStore.clear());
|
|
7468
|
+
for (const key in indices) {
|
|
7469
|
+
const index = indices[key];
|
|
7470
|
+
index.customIndex?.resetDerivedStorage?.();
|
|
7471
|
+
promises.push(index.clearAsync ? index.clearAsync() : index.clear());
|
|
7472
|
+
}
|
|
7473
|
+
return settleStartedClears();
|
|
7474
|
+
} catch (error) {
|
|
7475
|
+
if (promises.length > 0) return settleStartedClears({ error });
|
|
7476
|
+
releaseFullTextClearFence?.();
|
|
7477
|
+
throw error;
|
|
7478
|
+
}
|
|
7479
|
+
};
|
|
7480
|
+
return admission instanceof Promise ? admission.then(startClear) : startClear();
|
|
7150
7481
|
}
|
|
7151
7482
|
/** Release everything makeTable() registered process-wide; the class must not be used afterwards. */
|
|
7152
7483
|
static cleanup() {
|
|
7153
7484
|
disposed = true;
|
|
7154
7485
|
TableResource.getResource = unavailableResource;
|
|
7155
|
-
TableResource.derivedIndexRuntime
|
|
7486
|
+
void TableResource.derivedIndexRuntime
|
|
7156
7487
|
?.close()
|
|
7157
7488
|
.catch((error) => logger.warn?.(`Derived index shutdown failed for ${databaseName}.${tableName}`, error));
|
|
7158
|
-
|
|
7159
|
-
settlePendingCleanup();
|
|
7160
|
-
clearInterval(recordExpirationInterval);
|
|
7489
|
+
stopMaintenance();
|
|
7161
7490
|
deleteCallbackHandle?.remove();
|
|
7162
7491
|
removeStorageReclamationHandler(primaryStore.path, reclamationHandler);
|
|
7163
7492
|
}
|
|
7493
|
+
static async closeMaintenance(deadline?: number): Promise<void> {
|
|
7494
|
+
const startedAt = Date.now();
|
|
7495
|
+
const defaultTimeout = getDatabaseCommitDrainTimeoutMilliseconds();
|
|
7496
|
+
const timeoutMilliseconds = deadline === undefined ? defaultTimeout : Math.max(0, deadline - startedAt);
|
|
7497
|
+
const resolvedDeadline = deadline ?? startedAt + timeoutMilliseconds;
|
|
7498
|
+
stopMaintenance();
|
|
7499
|
+
const timeoutError = () => new DatabaseDrainTimeoutError(databaseName, timeoutMilliseconds);
|
|
7500
|
+
await settleBeforeDeadline([lastEvictionCompletion, recordExpirationCompletion], resolvedDeadline, timeoutError);
|
|
7501
|
+
for (;;) {
|
|
7502
|
+
const pending = [...maintenanceCommits];
|
|
7503
|
+
if (pending.length === 0) return;
|
|
7504
|
+
await settleBeforeDeadline(pending, resolvedDeadline, timeoutError);
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
static resumeMaintenance(): void {
|
|
7508
|
+
if (disposed || !maintenanceClosed) return;
|
|
7509
|
+
maintenanceClosed = false;
|
|
7510
|
+
lastCleanupInterval = undefined;
|
|
7511
|
+
if (expirationScanScheduled || evictionMs) scheduleCleanup();
|
|
7512
|
+
if (expiresAtProperty && !recordExpirationInterval) runRecordExpirationEviction();
|
|
7513
|
+
}
|
|
7164
7514
|
static _readTxnForContext(context) {
|
|
7165
7515
|
return txnForContext(context).getReadTxn();
|
|
7166
7516
|
}
|
|
@@ -7178,7 +7528,9 @@ export function makeTable(options) {
|
|
|
7178
7528
|
},
|
|
7179
7529
|
() => {
|
|
7180
7530
|
throw new ServerError('Service unavailable, exceeded request queue limit for resolving cache record', 503);
|
|
7181
|
-
}
|
|
7531
|
+
},
|
|
7532
|
+
undefined,
|
|
7533
|
+
`cache resolution for ${tableName}`
|
|
7182
7534
|
);
|
|
7183
7535
|
|
|
7184
7536
|
try {
|
|
@@ -7803,7 +8155,8 @@ export function makeTable(options) {
|
|
|
7803
8155
|
replacingVersion: existingVersion,
|
|
7804
8156
|
// Once dropTable() has started, no new source-fill write may begin; still resolve the
|
|
7805
8157
|
// caller's read with fresh source data, just don't cache it into a table that's going away.
|
|
7806
|
-
|
|
8158
|
+
// No write can commit on a read-only node, so a fill there is served but never staged.
|
|
8159
|
+
noCacheStore: droppingTable || isReadOnlyMode(),
|
|
7807
8160
|
source: null,
|
|
7808
8161
|
transaction: undefined,
|
|
7809
8162
|
expiresAt: undefined,
|
|
@@ -8145,19 +8498,25 @@ export function makeTable(options) {
|
|
|
8145
8498
|
}
|
|
8146
8499
|
},
|
|
8147
8500
|
};
|
|
8148
|
-
// The
|
|
8149
|
-
//
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8501
|
+
// The fill is shared by every reader, so it takes no request signal; a hook failure aborts
|
|
8502
|
+
// only the cache write.
|
|
8503
|
+
const modelHooksBefore = combineWriteHooks(
|
|
8504
|
+
buildEmbedBefore(
|
|
8505
|
+
updatedRecord,
|
|
8506
|
+
sourceContext,
|
|
8507
|
+
undefined,
|
|
8508
|
+
TableResource.embedAttributes,
|
|
8509
|
+
TableResource.userEmbedders
|
|
8510
|
+
),
|
|
8511
|
+
buildDecideBefore(
|
|
8512
|
+
updatedRecord,
|
|
8513
|
+
sourceContext,
|
|
8514
|
+
undefined,
|
|
8515
|
+
TableResource.decideAttributes,
|
|
8516
|
+
TableResource.userDeciders
|
|
8517
|
+
)
|
|
8159
8518
|
);
|
|
8160
|
-
if (
|
|
8519
|
+
if (modelHooksBefore) await modelHooksBefore();
|
|
8161
8520
|
if (droppingTable) {
|
|
8162
8521
|
// Re-check right before staging the write: dropTable() may have started
|
|
8163
8522
|
// while we were awaiting the embed step above (harper#1381).
|
|
@@ -8195,6 +8554,19 @@ export function makeTable(options) {
|
|
|
8195
8554
|
throw new ClientError('Can not specify replication confirmation without super user permissions', 403);
|
|
8196
8555
|
return true;
|
|
8197
8556
|
}
|
|
8557
|
+
function trackMaintenanceCommit<T>(commit: Promise<T>): Promise<T> {
|
|
8558
|
+
const tracked = commit.finally(() => maintenanceCommits.delete(tracked));
|
|
8559
|
+
maintenanceCommits.add(tracked);
|
|
8560
|
+
return tracked;
|
|
8561
|
+
}
|
|
8562
|
+
function stopMaintenance() {
|
|
8563
|
+
maintenanceClosed = true;
|
|
8564
|
+
clearTimeout(cleanupTimer);
|
|
8565
|
+
cleanupTimer = undefined;
|
|
8566
|
+
settlePendingCleanup();
|
|
8567
|
+
clearInterval(recordExpirationInterval);
|
|
8568
|
+
recordExpirationInterval = undefined;
|
|
8569
|
+
}
|
|
8198
8570
|
// RocksDB-only: coalesces eviction/tombstone removals into shared transactions so the cleanup
|
|
8199
8571
|
// scan pays one commit per batch instead of one per record. Descriptors hold only the decoded
|
|
8200
8572
|
// primary key and the version seen during the scan (both stable primitives — the scanned record
|
|
@@ -8254,7 +8626,7 @@ export function makeTable(options) {
|
|
|
8254
8626
|
return;
|
|
8255
8627
|
}
|
|
8256
8628
|
try {
|
|
8257
|
-
await transaction
|
|
8629
|
+
await commitTrackedRocksTransaction(transaction, primaryStore);
|
|
8258
8630
|
return;
|
|
8259
8631
|
} catch (error: any) {
|
|
8260
8632
|
try {
|
|
@@ -8303,7 +8675,7 @@ export function makeTable(options) {
|
|
|
8303
8675
|
}
|
|
8304
8676
|
function scheduleCleanup(priority?: number): Promise<void> | void {
|
|
8305
8677
|
// a reclamation run may still hold this class's handler after cleanup(); a promise here would never settle
|
|
8306
|
-
if (disposed) return;
|
|
8678
|
+
if (disposed || maintenanceClosed) return;
|
|
8307
8679
|
let runImmediately = false;
|
|
8308
8680
|
if (priority) {
|
|
8309
8681
|
// run immediately if there is a big increase in priority
|
|
@@ -8312,7 +8684,9 @@ export function makeTable(options) {
|
|
|
8312
8684
|
}
|
|
8313
8685
|
// Periodically evict expired records and deleted records searching for records who expiresAt timestamp is before now
|
|
8314
8686
|
if (cleanupInterval === lastCleanupInterval && !runImmediately) return;
|
|
8315
|
-
|
|
8687
|
+
// Left unrecorded by a thread with no worker index yet: under threads: 0 the main thread loads tables
|
|
8688
|
+
// before it becomes worker 0, and its next call for this interval has to arm the scan then.
|
|
8689
|
+
if (getWorkerIndex() !== undefined) lastCleanupInterval = cleanupInterval;
|
|
8316
8690
|
if (ownsStoreMaintenance(primaryStore.path) || (ttlConfiguredByApplication && isDedicatedWorker())) {
|
|
8317
8691
|
// run on the last thread so we aren't overloading lower-numbered threads
|
|
8318
8692
|
if (cleanupTimer) clearTimeout(cleanupTimer);
|
|
@@ -8340,12 +8714,13 @@ export function makeTable(options) {
|
|
|
8340
8714
|
? Date.now()
|
|
8341
8715
|
: Math.ceil((Date.now() - startOfYear.getTime()) / nextInterval) * nextInterval + startOfYear.getTime();
|
|
8342
8716
|
const startNextTimer = (nextScheduled) => {
|
|
8343
|
-
if (disposed) return;
|
|
8717
|
+
if (disposed || maintenanceClosed) return;
|
|
8344
8718
|
logger.trace?.(`Scheduled next cleanup scan at ${new Date(nextScheduled)}`);
|
|
8345
8719
|
// noinspection JSVoidFunctionReturnValueUsed
|
|
8346
8720
|
cleanupTimer = setTimeout(
|
|
8347
8721
|
() =>
|
|
8348
8722
|
(lastEvictionCompletion = lastEvictionCompletion.then(async () => {
|
|
8723
|
+
if (disposed || maintenanceClosed) return;
|
|
8349
8724
|
// schedule the next run for when the next cleanup interval should occur (or now if it is in the past)
|
|
8350
8725
|
startNextTimer(Math.max(nextScheduled + cleanupInterval, Date.now()));
|
|
8351
8726
|
const rootStore = primaryStore.rootStore;
|
|
@@ -8399,6 +8774,7 @@ export function makeTable(options) {
|
|
|
8399
8774
|
versions: true,
|
|
8400
8775
|
lazy: true, // only want to access metadata most of the time
|
|
8401
8776
|
})) {
|
|
8777
|
+
if (maintenanceClosed) break;
|
|
8402
8778
|
const { key, value: record, version, expiresAt, metadataFlags } = entry;
|
|
8403
8779
|
// if there is no auditing cleanup and we are tracking deletion, need to do cleanup of
|
|
8404
8780
|
// these deletion entries (LMDB audit cleanup has its own scheduled job for this)
|
|
@@ -8430,6 +8806,7 @@ export function makeTable(options) {
|
|
|
8430
8806
|
}
|
|
8431
8807
|
await rest();
|
|
8432
8808
|
}
|
|
8809
|
+
await Promise.all(outstandingCleanupOperations.filter(Boolean));
|
|
8433
8810
|
if (batcher) await batcher.drain();
|
|
8434
8811
|
logger.debug?.(`Finished cleanup scan for ${tableName}, evicted ${count} entries`);
|
|
8435
8812
|
} catch (error) {
|
|
@@ -8457,40 +8834,56 @@ export function makeTable(options) {
|
|
|
8457
8834
|
// Periodically evict expired records, searching for records who expiresAt timestamp is before now
|
|
8458
8835
|
if (ownsStoreExpiration(primaryStore.path) || (ttlConfiguredByApplication && isDedicatedWorker())) {
|
|
8459
8836
|
// we want to run the pruning of expired records on only one thread so we don't have conflicts in evicting
|
|
8460
|
-
recordExpirationInterval = setInterval(
|
|
8837
|
+
recordExpirationInterval = setInterval(() => {
|
|
8461
8838
|
// go through each database and table and then search for expired entries
|
|
8462
8839
|
// find any entries that are set to expire before now
|
|
8463
8840
|
// updatedAttributes() clears expiresAtProperty when a live redeclaration drops the directive,
|
|
8464
8841
|
// and there is nothing left for this interval to scan by
|
|
8465
|
-
if (disposed || runningRecordExpiration || !expiresAtProperty) return;
|
|
8842
|
+
if (disposed || maintenanceClosed || runningRecordExpiration || !expiresAtProperty) return;
|
|
8466
8843
|
runningRecordExpiration = true;
|
|
8467
|
-
|
|
8844
|
+
recordExpirationCompletion = (async () => {
|
|
8468
8845
|
const expiresAtName = expiresAtProperty.name;
|
|
8469
8846
|
const index = indices[expiresAtName];
|
|
8470
8847
|
if (!index) throw new Error(`expiresAt attribute ${expiresAtProperty} must be indexed`);
|
|
8471
|
-
|
|
8848
|
+
const inFlight = new Set<Promise<unknown>>();
|
|
8849
|
+
const trackInFlight = (operation: Promise<unknown>) => {
|
|
8850
|
+
const tracked = operation.finally(() => inFlight.delete(tracked));
|
|
8851
|
+
inFlight.add(tracked);
|
|
8852
|
+
if (inFlight.size >= 50) return Promise.race(inFlight);
|
|
8853
|
+
};
|
|
8854
|
+
expirationScan: for (const key of index.getRange({
|
|
8472
8855
|
start: true,
|
|
8473
8856
|
values: false,
|
|
8474
8857
|
end: Date.now(),
|
|
8475
8858
|
snapshot: false,
|
|
8476
8859
|
})) {
|
|
8477
8860
|
for (const id of index.getValues(key)) {
|
|
8861
|
+
if (maintenanceClosed) break expirationScan;
|
|
8478
8862
|
const recordEntry = primaryStore.getEntry(id);
|
|
8479
8863
|
if (!recordEntry?.value) {
|
|
8480
8864
|
// cleanup the index if the record is gone
|
|
8481
|
-
|
|
8865
|
+
const repair = trackMaintenanceCommit(
|
|
8866
|
+
Promise.resolve(primaryStore.ifVersion(id, recordEntry?.version, () => index.remove(key, id))).catch(
|
|
8867
|
+
(error) => logger.warn?.('Error removing stale expiration index entry', id, error)
|
|
8868
|
+
)
|
|
8869
|
+
);
|
|
8870
|
+
const backpressure = trackInFlight(repair);
|
|
8871
|
+
if (backpressure) await backpressure;
|
|
8482
8872
|
} else if (recordEntry.value[expiresAtName] < Date.now()) {
|
|
8483
8873
|
// make sure the record hasn't changed and won't change while removing
|
|
8484
|
-
TableResource.evict(id, recordEntry.value, recordEntry.version);
|
|
8874
|
+
const eviction = TableResource.evict(id, recordEntry.value, recordEntry.version);
|
|
8875
|
+
if (eviction) {
|
|
8876
|
+
const backpressure = trackInFlight(eviction);
|
|
8877
|
+
if (backpressure) await backpressure;
|
|
8878
|
+
}
|
|
8485
8879
|
}
|
|
8486
8880
|
}
|
|
8487
8881
|
await rest();
|
|
8488
8882
|
}
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
runningRecordExpiration = false;
|
|
8493
|
-
}
|
|
8883
|
+
await Promise.all(inFlight);
|
|
8884
|
+
})()
|
|
8885
|
+
.catch((error) => logger.error?.('Error in evicting old records', error))
|
|
8886
|
+
.finally(() => (runningRecordExpiration = false));
|
|
8494
8887
|
}, RECORD_PRUNING_INTERVAL).unref();
|
|
8495
8888
|
}
|
|
8496
8889
|
}
|