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