harper 5.1.20 → 5.2.0-alpha.5

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 (395) hide show
  1. package/agent/operations.ts +13 -16
  2. package/components/Application.ts +188 -7
  3. package/components/ApplicationScope.ts +3 -0
  4. package/components/Scope.ts +12 -0
  5. package/components/anthropic/index.ts +9 -2
  6. package/components/bedrock/index.ts +25 -2
  7. package/components/componentLoader.ts +56 -7
  8. package/components/componentSecrets.ts +451 -0
  9. package/components/deploymentRecorder.ts +7 -1
  10. package/components/mcp/adapters/fastify.ts +3 -0
  11. package/components/mcp/adapters/harperHttp.ts +3 -0
  12. package/components/mcp/audit.ts +21 -6
  13. package/components/mcp/customResourceRegistry.ts +17 -38
  14. package/components/mcp/listChanged.ts +37 -20
  15. package/components/mcp/quota.ts +145 -0
  16. package/components/mcp/rateLimit.ts +152 -14
  17. package/components/mcp/resources.ts +2 -4
  18. package/components/mcp/toolRegistry.ts +73 -4
  19. package/components/mcp/tools/application.ts +77 -8
  20. package/components/mcp/tools/operations.ts +106 -36
  21. package/components/mcp/tools/schemas/operationDescriptions.ts +4 -4
  22. package/components/mcp/tools/schemas/operations.ts +5 -0
  23. package/components/mcp/transport.ts +55 -4
  24. package/components/ollama/index.ts +6 -2
  25. package/components/openai/index.ts +9 -2
  26. package/components/operations.js +204 -10
  27. package/components/operationsValidation.js +173 -0
  28. package/components/secretOperations.ts +498 -0
  29. package/config/configUtils.ts +25 -1
  30. package/config-app.schema.json +81 -0
  31. package/config-root.schema.json +8 -1
  32. package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
  33. package/dataLayer/readAuditLog.ts +14 -0
  34. package/dataLayer/schemaDescribe.ts +1 -0
  35. package/dist/agent/operations.d.ts +7 -4
  36. package/dist/agent/operations.js +13 -15
  37. package/dist/agent/operations.js.map +1 -1
  38. package/dist/components/Application.d.ts +25 -2
  39. package/dist/components/Application.js +166 -7
  40. package/dist/components/Application.js.map +1 -1
  41. package/dist/components/ApplicationScope.d.ts +2 -0
  42. package/dist/components/ApplicationScope.js +3 -0
  43. package/dist/components/ApplicationScope.js.map +1 -1
  44. package/dist/components/Scope.d.ts +8 -0
  45. package/dist/components/Scope.js +11 -0
  46. package/dist/components/Scope.js.map +1 -1
  47. package/dist/components/anthropic/index.d.ts +4 -1
  48. package/dist/components/anthropic/index.js +4 -2
  49. package/dist/components/anthropic/index.js.map +1 -1
  50. package/dist/components/bedrock/index.d.ts +4 -1
  51. package/dist/components/bedrock/index.js +22 -2
  52. package/dist/components/bedrock/index.js.map +1 -1
  53. package/dist/components/componentLoader.d.ts +2 -0
  54. package/dist/components/componentLoader.js +52 -10
  55. package/dist/components/componentLoader.js.map +1 -1
  56. package/dist/components/componentSecrets.d.ts +63 -0
  57. package/dist/components/componentSecrets.js +405 -0
  58. package/dist/components/componentSecrets.js.map +1 -0
  59. package/dist/components/deploymentRecorder.d.ts +3 -0
  60. package/dist/components/deploymentRecorder.js +4 -3
  61. package/dist/components/deploymentRecorder.js.map +1 -1
  62. package/dist/components/mcp/adapters/fastify.d.ts +2 -0
  63. package/dist/components/mcp/adapters/fastify.js +1 -0
  64. package/dist/components/mcp/adapters/fastify.js.map +1 -1
  65. package/dist/components/mcp/adapters/harperHttp.d.ts +2 -0
  66. package/dist/components/mcp/adapters/harperHttp.js +1 -0
  67. package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
  68. package/dist/components/mcp/audit.d.ts +2 -2
  69. package/dist/components/mcp/audit.js +20 -5
  70. package/dist/components/mcp/audit.js.map +1 -1
  71. package/dist/components/mcp/customResourceRegistry.d.ts +5 -12
  72. package/dist/components/mcp/customResourceRegistry.js +15 -30
  73. package/dist/components/mcp/customResourceRegistry.js.map +1 -1
  74. package/dist/components/mcp/listChanged.d.ts +7 -2
  75. package/dist/components/mcp/listChanged.js +28 -0
  76. package/dist/components/mcp/listChanged.js.map +1 -1
  77. package/dist/components/mcp/quota.d.ts +31 -0
  78. package/dist/components/mcp/quota.js +155 -0
  79. package/dist/components/mcp/quota.js.map +1 -0
  80. package/dist/components/mcp/rateLimit.d.ts +19 -3
  81. package/dist/components/mcp/rateLimit.js +129 -13
  82. package/dist/components/mcp/rateLimit.js.map +1 -1
  83. package/dist/components/mcp/resources.js.map +1 -1
  84. package/dist/components/mcp/toolRegistry.d.ts +39 -1
  85. package/dist/components/mcp/toolRegistry.js +60 -5
  86. package/dist/components/mcp/toolRegistry.js.map +1 -1
  87. package/dist/components/mcp/tools/application.d.ts +8 -1
  88. package/dist/components/mcp/tools/application.js +65 -5
  89. package/dist/components/mcp/tools/application.js.map +1 -1
  90. package/dist/components/mcp/tools/operations.d.ts +15 -3
  91. package/dist/components/mcp/tools/operations.js +99 -38
  92. package/dist/components/mcp/tools/operations.js.map +1 -1
  93. package/dist/components/mcp/tools/schemas/operationDescriptions.js +4 -4
  94. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  95. package/dist/components/mcp/tools/schemas/operations.js +4 -0
  96. package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
  97. package/dist/components/mcp/transport.d.ts +6 -0
  98. package/dist/components/mcp/transport.js +47 -3
  99. package/dist/components/mcp/transport.js.map +1 -1
  100. package/dist/components/ollama/index.d.ts +4 -1
  101. package/dist/components/ollama/index.js +4 -2
  102. package/dist/components/ollama/index.js.map +1 -1
  103. package/dist/components/openai/index.d.ts +4 -1
  104. package/dist/components/openai/index.js +4 -2
  105. package/dist/components/openai/index.js.map +1 -1
  106. package/dist/components/operations.d.ts +39 -0
  107. package/dist/components/operations.js +194 -2
  108. package/dist/components/operations.js.map +1 -1
  109. package/dist/components/operationsValidation.d.ts +38 -0
  110. package/dist/components/operationsValidation.js +155 -0
  111. package/dist/components/operationsValidation.js.map +1 -1
  112. package/dist/components/secretOperations.d.ts +113 -0
  113. package/dist/components/secretOperations.js +475 -0
  114. package/dist/components/secretOperations.js.map +1 -0
  115. package/dist/config/configUtils.d.ts +4 -1
  116. package/dist/config/configUtils.js +18 -1
  117. package/dist/config/configUtils.js.map +1 -1
  118. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
  119. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
  120. package/dist/dataLayer/readAuditLog.js +11 -0
  121. package/dist/dataLayer/readAuditLog.js.map +1 -1
  122. package/dist/dataLayer/schemaDescribe.js +2 -0
  123. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  124. package/dist/globals.d.ts +1 -1
  125. package/dist/globals.js +1 -0
  126. package/dist/globals.js.map +1 -1
  127. package/dist/index.d.ts +3 -0
  128. package/dist/index.js +1 -0
  129. package/dist/index.js.map +1 -1
  130. package/dist/json/systemSchema.json +38 -0
  131. package/dist/resources/DatabaseTransaction.d.ts +31 -3
  132. package/dist/resources/DatabaseTransaction.js +190 -29
  133. package/dist/resources/DatabaseTransaction.js.map +1 -1
  134. package/dist/resources/LMDBTransaction.d.ts +1 -1
  135. package/dist/resources/LMDBTransaction.js +43 -5
  136. package/dist/resources/LMDBTransaction.js.map +1 -1
  137. package/dist/resources/PrimaryRocksDatabase.d.ts +51 -0
  138. package/dist/resources/PrimaryRocksDatabase.js +194 -0
  139. package/dist/resources/PrimaryRocksDatabase.js.map +1 -0
  140. package/dist/resources/RecordEncoder.d.ts +1 -0
  141. package/dist/resources/RecordEncoder.js +81 -75
  142. package/dist/resources/RecordEncoder.js.map +1 -1
  143. package/dist/resources/RequestTarget.d.ts +19 -0
  144. package/dist/resources/RequestTarget.js.map +1 -1
  145. package/dist/resources/Resource.d.ts +0 -1
  146. package/dist/resources/Resource.js +103 -16
  147. package/dist/resources/Resource.js.map +1 -1
  148. package/dist/resources/ResourceInterface.d.ts +6 -2
  149. package/dist/resources/ResourceInterface.js.map +1 -1
  150. package/dist/resources/Resources.js.map +1 -1
  151. package/dist/resources/Table.d.ts +6 -4
  152. package/dist/resources/Table.js +247 -24
  153. package/dist/resources/Table.js.map +1 -1
  154. package/dist/resources/analytics/read.js +25 -28
  155. package/dist/resources/analytics/read.js.map +1 -1
  156. package/dist/resources/blob.js +165 -22
  157. package/dist/resources/blob.js.map +1 -1
  158. package/dist/resources/crdt.d.ts +2 -0
  159. package/dist/resources/crdt.js +45 -36
  160. package/dist/resources/crdt.js.map +1 -1
  161. package/dist/resources/databases.d.ts +1 -0
  162. package/dist/resources/databases.js +134 -7
  163. package/dist/resources/databases.js.map +1 -1
  164. package/dist/resources/graphql.js +8 -0
  165. package/dist/resources/graphql.js.map +1 -1
  166. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +35 -5
  167. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +239 -14
  168. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  169. package/dist/resources/jsResource.js +6 -0
  170. package/dist/resources/jsResource.js.map +1 -1
  171. package/dist/resources/loadEnv.js +30 -3
  172. package/dist/resources/loadEnv.js.map +1 -1
  173. package/dist/resources/login.js +2 -2
  174. package/dist/resources/login.js.map +1 -1
  175. package/dist/resources/models/Models.js +14 -3
  176. package/dist/resources/models/Models.js.map +1 -1
  177. package/dist/resources/models/embedHook.js +10 -2
  178. package/dist/resources/models/embedHook.js.map +1 -1
  179. package/dist/resources/models/openaiStream.d.ts +56 -0
  180. package/dist/resources/models/openaiStream.js +94 -0
  181. package/dist/resources/models/openaiStream.js.map +1 -0
  182. package/dist/resources/replayLogs.js +4 -3
  183. package/dist/resources/replayLogs.js.map +1 -1
  184. package/dist/resources/search.d.ts +1 -1
  185. package/dist/resources/search.js +104 -11
  186. package/dist/resources/search.js.map +1 -1
  187. package/dist/resources/secretDecryptor.d.ts +54 -0
  188. package/dist/resources/secretDecryptor.js +131 -0
  189. package/dist/resources/secretDecryptor.js.map +1 -0
  190. package/dist/resources/tracked.js +15 -5
  191. package/dist/resources/tracked.js.map +1 -1
  192. package/dist/security/auth.js +71 -8
  193. package/dist/security/auth.js.map +1 -1
  194. package/dist/security/jsLoader.js +11 -0
  195. package/dist/security/jsLoader.js.map +1 -1
  196. package/dist/security/tokenAuthentication.d.ts +9 -1
  197. package/dist/security/tokenAuthentication.js +31 -1
  198. package/dist/security/tokenAuthentication.js.map +1 -1
  199. package/dist/server/DurableSubscriptionsSession.d.ts +1 -1
  200. package/dist/server/DurableSubscriptionsSession.js +8 -4
  201. package/dist/server/DurableSubscriptionsSession.js.map +1 -1
  202. package/dist/server/REST.js +28 -9
  203. package/dist/server/REST.js.map +1 -1
  204. package/dist/server/fastifyRoutes.js +11 -1
  205. package/dist/server/fastifyRoutes.js.map +1 -1
  206. package/dist/server/graphqlQuerying.js +4 -3
  207. package/dist/server/graphqlQuerying.js.map +1 -1
  208. package/dist/server/http.d.ts +40 -2
  209. package/dist/server/http.js +560 -77
  210. package/dist/server/http.js.map +1 -1
  211. package/dist/server/itc/serverHandlers.js +64 -0
  212. package/dist/server/itc/serverHandlers.js.map +1 -1
  213. package/dist/server/liveSubscriptionAuth.d.ts +12 -0
  214. package/dist/server/liveSubscriptionAuth.js +140 -0
  215. package/dist/server/liveSubscriptionAuth.js.map +1 -0
  216. package/dist/server/loadRootComponents.js +2 -1
  217. package/dist/server/loadRootComponents.js.map +1 -1
  218. package/dist/server/middlewareChain.d.ts +44 -1
  219. package/dist/server/middlewareChain.js +86 -10
  220. package/dist/server/middlewareChain.js.map +1 -1
  221. package/dist/server/mqtt.js +2 -1
  222. package/dist/server/mqtt.js.map +1 -1
  223. package/dist/server/operationsServer.js +1 -1
  224. package/dist/server/operationsServer.js.map +1 -1
  225. package/dist/server/serverHelpers/Headers.d.ts +9 -0
  226. package/dist/server/serverHelpers/Headers.js +22 -0
  227. package/dist/server/serverHelpers/Headers.js.map +1 -1
  228. package/dist/server/serverHelpers/JSONStream.js +3 -3
  229. package/dist/server/serverHelpers/JSONStream.js.map +1 -1
  230. package/dist/server/serverHelpers/Request.d.ts +52 -1
  231. package/dist/server/serverHelpers/Request.js +122 -1
  232. package/dist/server/serverHelpers/Request.js.map +1 -1
  233. package/dist/server/serverHelpers/contentTypes.js +3 -8
  234. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  235. package/dist/server/serverHelpers/progressEmitter.d.ts +19 -0
  236. package/dist/server/serverHelpers/progressEmitter.js +52 -4
  237. package/dist/server/serverHelpers/progressEmitter.js.map +1 -1
  238. package/dist/server/serverHelpers/registeredOperations.d.ts +40 -0
  239. package/dist/server/serverHelpers/registeredOperations.js +286 -0
  240. package/dist/server/serverHelpers/registeredOperations.js.map +1 -0
  241. package/dist/server/serverHelpers/serverHandlers.js +11 -5
  242. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  243. package/dist/server/serverHelpers/serverUtilities.d.ts +1 -0
  244. package/dist/server/serverHelpers/serverUtilities.js +67 -5
  245. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  246. package/dist/server/serverHelpers/uwsServer.d.ts +80 -0
  247. package/dist/server/serverHelpers/uwsServer.js +433 -0
  248. package/dist/server/serverHelpers/uwsServer.js.map +1 -0
  249. package/dist/server/static.d.ts +0 -15
  250. package/dist/server/static.js +161 -2
  251. package/dist/server/static.js.map +1 -1
  252. package/dist/server/status/index.d.ts +4 -1
  253. package/dist/server/status/index.js +64 -4
  254. package/dist/server/status/index.js.map +1 -1
  255. package/dist/server/threads/manageThreads.d.ts +10 -0
  256. package/dist/server/threads/manageThreads.js +107 -4
  257. package/dist/server/threads/manageThreads.js.map +1 -1
  258. package/dist/server/threads/socketRouter.js +5 -1
  259. package/dist/server/threads/socketRouter.js.map +1 -1
  260. package/dist/server/threads/threadServer.js +92 -14
  261. package/dist/server/threads/threadServer.js.map +1 -1
  262. package/dist/upgrade/directives/5-2-0.d.ts +7 -0
  263. package/dist/upgrade/directives/5-2-0.js +108 -0
  264. package/dist/upgrade/directives/5-2-0.js.map +1 -0
  265. package/dist/upgrade/directives/directivesController.js +2 -1
  266. package/dist/upgrade/directives/directivesController.js.map +1 -1
  267. package/dist/utility/common_utils.js +5 -0
  268. package/dist/utility/common_utils.js.map +1 -1
  269. package/dist/utility/envFile.d.ts +41 -0
  270. package/dist/utility/envFile.js +221 -0
  271. package/dist/utility/envFile.js.map +1 -0
  272. package/dist/utility/globalSchema.d.ts +9 -0
  273. package/dist/utility/hdbTerms.d.ts +25 -0
  274. package/dist/utility/hdbTerms.js +31 -0
  275. package/dist/utility/hdbTerms.js.map +1 -1
  276. package/dist/utility/logging/harper_logger.d.ts +21 -0
  277. package/dist/utility/logging/harper_logger.js +160 -8
  278. package/dist/utility/logging/harper_logger.js.map +1 -1
  279. package/dist/utility/logging/logRotator.js +29 -17
  280. package/dist/utility/logging/logRotator.js.map +1 -1
  281. package/dist/utility/logging/readLog.d.ts +1 -1
  282. package/dist/utility/logging/readLog.js +305 -2
  283. package/dist/utility/logging/readLog.js.map +1 -1
  284. package/dist/utility/operationPermissions.d.ts +18 -0
  285. package/dist/utility/operationPermissions.js +35 -1
  286. package/dist/utility/operationPermissions.js.map +1 -1
  287. package/dist/utility/operation_authorization.d.ts +17 -0
  288. package/dist/utility/operation_authorization.js +38 -0
  289. package/dist/utility/operation_authorization.js.map +1 -1
  290. package/dist/utility/secretEnvelope.d.ts +30 -0
  291. package/dist/utility/secretEnvelope.js +94 -0
  292. package/dist/utility/secretEnvelope.js.map +1 -0
  293. package/dist/utility/signalling.d.ts +7 -0
  294. package/dist/utility/signalling.js +16 -0
  295. package/dist/utility/signalling.js.map +1 -1
  296. package/dist/validation/configValidator.js +70 -24
  297. package/dist/validation/configValidator.js.map +1 -1
  298. package/index.ts +4 -0
  299. package/json/systemSchema.json +38 -0
  300. package/npm-shrinkwrap.json +15066 -9347
  301. package/package.json +13 -8
  302. package/resources/DESIGN.md +15 -12
  303. package/resources/DatabaseTransaction.ts +206 -37
  304. package/resources/LMDBTransaction.ts +43 -9
  305. package/resources/PrimaryRocksDatabase.ts +201 -0
  306. package/resources/RecordEncoder.ts +81 -69
  307. package/resources/RequestTarget.ts +21 -0
  308. package/resources/Resource.ts +109 -25
  309. package/resources/ResourceInterface.ts +6 -2
  310. package/resources/Resources.ts +1 -3
  311. package/resources/Table.ts +238 -29
  312. package/resources/analytics/read.ts +25 -30
  313. package/resources/blob.ts +159 -23
  314. package/resources/crdt.ts +37 -31
  315. package/resources/databases.ts +143 -6
  316. package/resources/graphql.ts +8 -0
  317. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +251 -17
  318. package/resources/jsResource.ts +6 -0
  319. package/resources/loadEnv.ts +34 -3
  320. package/resources/login.ts +2 -2
  321. package/resources/models/Models.ts +19 -3
  322. package/resources/models/embedHook.ts +13 -2
  323. package/resources/models/openaiStream.ts +133 -0
  324. package/resources/replayLogs.ts +4 -3
  325. package/resources/search.ts +113 -11
  326. package/resources/secretDecryptor.ts +159 -0
  327. package/resources/tracked.ts +24 -17
  328. package/security/auth.ts +70 -10
  329. package/security/jsLoader.ts +12 -0
  330. package/security/tokenAuthentication.ts +42 -1
  331. package/server/DESIGN.md +10 -0
  332. package/server/DurableSubscriptionsSession.ts +8 -4
  333. package/server/REST.ts +26 -9
  334. package/server/fastifyRoutes.ts +10 -1
  335. package/server/graphqlQuerying.ts +4 -3
  336. package/server/http.ts +563 -77
  337. package/server/itc/serverHandlers.js +70 -0
  338. package/server/liveSubscriptionAuth.ts +152 -0
  339. package/server/loadRootComponents.js +2 -1
  340. package/server/middlewareChain.ts +107 -17
  341. package/server/mqtt.ts +2 -1
  342. package/server/operationsServer.ts +2 -2
  343. package/server/serverHelpers/Headers.ts +24 -0
  344. package/server/serverHelpers/JSONStream.ts +3 -3
  345. package/server/serverHelpers/Request.ts +127 -0
  346. package/server/serverHelpers/contentTypes.ts +3 -8
  347. package/server/serverHelpers/progressEmitter.ts +55 -5
  348. package/server/serverHelpers/registeredOperations.ts +259 -0
  349. package/server/serverHelpers/serverHandlers.js +11 -5
  350. package/server/serverHelpers/serverUtilities.ts +88 -5
  351. package/server/serverHelpers/uwsServer.ts +488 -0
  352. package/server/static.ts +180 -2
  353. package/server/status/index.ts +72 -4
  354. package/server/threads/manageThreads.js +102 -4
  355. package/server/threads/socketRouter.ts +5 -1
  356. package/server/threads/threadServer.js +93 -10
  357. package/studio/web/assets/{Chat-SdD1EZca.js → Chat-Cv_2paZE.js} +2 -2
  358. package/studio/web/assets/{Chat-SdD1EZca.js.map → Chat-Cv_2paZE.js.map} +1 -1
  359. package/studio/web/assets/{FloatingChat-GMq6tHK9.js → FloatingChat-CPyPIcVR.js} +4 -4
  360. package/studio/web/assets/{FloatingChat-GMq6tHK9.js.map → FloatingChat-CPyPIcVR.js.map} +1 -1
  361. package/studio/web/assets/{applications-B5hhv7uA.js → applications-C0jT2vdZ.js} +2 -2
  362. package/studio/web/assets/{applications-B5hhv7uA.js.map → applications-C0jT2vdZ.js.map} +1 -1
  363. package/studio/web/assets/{index-VFcpNWgq.js → index-D_GKOkhn.js} +6 -6
  364. package/studio/web/assets/index-D_GKOkhn.js.map +1 -0
  365. package/studio/web/assets/{index.lazy-DgcJojIA.js → index.lazy-CAmCIA65.js} +4 -4
  366. package/studio/web/assets/{index.lazy-DgcJojIA.js.map → index.lazy-CAmCIA65.js.map} +1 -1
  367. package/studio/web/assets/{profile-BqN-8vzm.js → profile-CaF-4aZe.js} +2 -2
  368. package/studio/web/assets/{profile-BqN-8vzm.js.map → profile-CaF-4aZe.js.map} +1 -1
  369. package/studio/web/assets/{setComponentFile-CHFvFq9u.js → setComponentFile-DLW1DHCt.js} +2 -2
  370. package/studio/web/assets/{setComponentFile-CHFvFq9u.js.map → setComponentFile-DLW1DHCt.js.map} +1 -1
  371. package/studio/web/assets/{setup-CQ3vWJSr.js → setup-TGCErIhq.js} +2 -2
  372. package/studio/web/assets/{setup-CQ3vWJSr.js.map → setup-TGCErIhq.js.map} +1 -1
  373. package/studio/web/assets/{status-BsW5fjuh.js → status-DG0Maoao.js} +2 -2
  374. package/studio/web/assets/{status-BsW5fjuh.js.map → status-DG0Maoao.js.map} +1 -1
  375. package/studio/web/assets/{swagger-ui-react-DITdeB9-.js → swagger-ui-react-8T4SgQ1m.js} +2 -2
  376. package/studio/web/assets/{swagger-ui-react-DITdeB9-.js.map → swagger-ui-react-8T4SgQ1m.js.map} +1 -1
  377. package/studio/web/assets/{tsMode-Dyph-OUs.js → tsMode-DJ6Sl24Q.js} +2 -2
  378. package/studio/web/assets/{tsMode-Dyph-OUs.js.map → tsMode-DJ6Sl24Q.js.map} +1 -1
  379. package/studio/web/assets/{useEntityRestURL-D0QTUqex.js → useEntityRestURL-JO2mfWTQ.js} +2 -2
  380. package/studio/web/assets/{useEntityRestURL-D0QTUqex.js.map → useEntityRestURL-JO2mfWTQ.js.map} +1 -1
  381. package/studio/web/index.html +1 -1
  382. package/upgrade/directives/5-2-0.ts +82 -0
  383. package/upgrade/directives/directivesController.ts +2 -1
  384. package/utility/common_utils.ts +5 -0
  385. package/utility/envFile.ts +218 -0
  386. package/utility/hdbTerms.ts +31 -0
  387. package/utility/logging/harper_logger.ts +156 -8
  388. package/utility/logging/logRotator.ts +24 -14
  389. package/utility/logging/readLog.ts +323 -2
  390. package/utility/operationPermissions.ts +35 -1
  391. package/utility/operation_authorization.ts +74 -1
  392. package/utility/secretEnvelope.ts +113 -0
  393. package/utility/signalling.ts +15 -0
  394. package/validation/configValidator.ts +78 -25
  395. package/studio/web/assets/index-VFcpNWgq.js.map +0 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harper",
3
3
  "description": "Harper is an open-source Node.js performance platform that unifies database, cache, application, and messaging layers into one in-memory process.",
4
- "version": "5.1.20",
4
+ "version": "5.2.0-alpha.5",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://harper.fast",
7
7
  "bugs": {
@@ -54,6 +54,8 @@
54
54
  "scripts": {
55
55
  "build": "tsc --project tsconfig.build.json",
56
56
  "build:watch": "npm run build -- --watch --incremental",
57
+ "typecheck": "tsc --project tsconfig.json",
58
+ "typecheck:fast": "npx -y -p typescript@7.0.2 tsc --noEmit --project tsconfig.json",
57
59
  "package": "./build-tools/build.sh",
58
60
  "lint": "oxlint --format stylish --deny-warnings .",
59
61
  "lint:required": "oxlint --format stylish --quiet .",
@@ -69,7 +71,8 @@
69
71
  "test:unit:all": "npm run test:unit:main && npm run test:unit:apitests && npm run test:unit:resources && npm run test:unit:lmdb",
70
72
  "test:unit:lmdb": "HARPER_STORAGE_ENGINE=lmdb npm run test:unit:resources && HARPER_STORAGE_ENGINE=lmdb npm run test:unit:apitests && HARPER_STORAGE_ENGINE=lmdb npm run test:unit:bin",
71
73
  "test:unit:components": "mocha \"unitTests/components/**/*.js\"",
72
- "test:unit:resources": "mocha \"unitTests/resources/**/*.js\"",
74
+ "test:unit:resources": "mocha \"unitTests/resources/**/*.js\" --exclude \"unitTests/resources/**/*.bench.js\"",
75
+ "bench": "mocha \"unitTests/resources/caching-rocks-database.bench.js\"",
73
76
  "test:unit:bin": "mocha \"unitTests/bin/**/*.js\"",
74
77
  "test:unit:apitests": "node ./dist/bin/harper.js stop && mocha \"unitTests/apiTests/**/*-test.mjs\"",
75
78
  "test:unit:logging": "mocha \"unitTests/utility/logging/*.js\"",
@@ -136,7 +139,7 @@
136
139
  "@types/micromatch": "^4.0.9",
137
140
  "@types/mocha": "^10.0.10",
138
141
  "@types/properties-reader": "^2.1.3",
139
- "@types/sinon": "^21.0.0",
142
+ "@types/sinon": "^22.0.0",
140
143
  "@types/tar-fs": "^2.0.4",
141
144
  "@typescript-eslint/parser": "^8.48.1",
142
145
  "axios": "1.18.1",
@@ -144,13 +147,13 @@
144
147
  "chai-as-promised": "^8.0.2",
145
148
  "chai-integer": "^0.1.0",
146
149
  "eventsource": "^4.0.0",
147
- "globals": "^16.5.0",
150
+ "globals": "^17.0.0",
148
151
  "intercept-stdout": "0.1.2",
149
152
  "mkcert": "^3.2.0",
150
153
  "mocha": "^11.7.5",
151
154
  "mqtt": "^5.15.1",
152
155
  "oxlint": "^1.31.0",
153
- "prettier": "~3.8.0",
156
+ "prettier": "~3.9.0",
154
157
  "rewire": "^9.0.1",
155
158
  "sinon": "21.0.3",
156
159
  "sinon-chai": "^4.0.1",
@@ -163,14 +166,14 @@
163
166
  },
164
167
  "dependencies": {
165
168
  "@aws-sdk/client-s3": "^3.1012.0",
166
- "@aws-sdk/lib-storage": "3.1075.0",
169
+ "@aws-sdk/lib-storage": "3.1080.0",
167
170
  "@endo/static-module-record": "^1.1.2",
168
171
  "@fastify/autoload": "^6.3.1",
169
172
  "@fastify/compress": "^8.3.1",
170
173
  "@fastify/cors": "^11.2.0",
171
174
  "@fastify/static": "^9.1.3",
172
175
  "@harperfast/extended-iterable": "^1.0.1",
173
- "@harperfast/rocksdb-js": "^2.3.0",
176
+ "@harperfast/rocksdb-js": "^2.4.0",
174
177
  "@turf/area": "6.5.0",
175
178
  "@turf/boolean-contains": "6.5.0",
176
179
  "@turf/boolean-disjoint": "6.5.0",
@@ -195,7 +198,7 @@
195
198
  "fast-glob": "3.3.3",
196
199
  "fastify": "^5.8.2",
197
200
  "fastify-plugin": "^5.1.0",
198
- "fs-extra": "11.3.5",
201
+ "fs-extra": "11.3.6",
199
202
  "graphql": "^16.10.0",
200
203
  "graphql-http": "^1.22.4",
201
204
  "gunzip-maybe": "1.4.2",
@@ -207,6 +210,7 @@
207
210
  "jsonata": "1.8.7",
208
211
  "jsonwebtoken": "9.0.3",
209
212
  "lmdb": "3.5.6",
213
+ "weak-lru-cache": "^1.2.2",
210
214
  "lodash": "^4.17.23",
211
215
  "mathjs": "11.12.0",
212
216
  "micromatch": "^4.0.8",
@@ -251,6 +255,7 @@
251
255
  "optionalDependencies": {
252
256
  "bufferutil": "^4.0.9",
253
257
  "segfault-handler": "^1.3.0",
258
+ "uWebSockets.js": "https://github.com/uNetworking/uWebSockets.js/archive/624987739d4da0acb628aaf2a10fc43e7ce27c5c.tar.gz",
254
259
  "utf-8-validate": "^5.0.10"
255
260
  },
256
261
  "peerDependencies": {
@@ -75,18 +75,21 @@ One giant `makeTable()` factory that returns a `TableResource extends Resource`
75
75
 
76
76
  ## "Where is X" cheat sheet
77
77
 
78
- | Question | Where |
79
- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
80
- | How is a CRUD request authorized? | `Table.ts → #section: authz-hooks`; defaults in `Resource.ts` (`allowRead` etc.) |
81
- | Where does versioning / conflict resolution happen? | `Table.ts → _writeUpdate` (`#section: write-path-internals`) |
82
- | How does `search()` choose an index? | `Table.ts → search` (`#section: search-query`) |
83
- | How are subscriptions replayed? | `Table.ts → subscribe` (`#section: pub-sub`) |
84
- | How is the response body shaped (select clause)? | `Table.ts → transformEntryForSelect` (`#section: search-query`) |
85
- | Where is record-level TTL evaluated? | `Table.ts → setTTLExpiration` (`#section: lifecycle-admin`); `Updatable.getExpiresAt` (`#section: setup-and-factory`) |
86
- | How are residencies enforced (replication)? | `Table.ts → #section: lifecycle-admin` (residency block: `getResidencyRecord`, `setResidency`, `setResidencyById`, `getResidency`) |
87
- | How is the RecordObject prototype applied? | `RecordEncoder.ts` (see `../DESIGN.md`) |
88
- | Where is the per-request transaction stored? | `transaction.ts` + `contextStorage` (AsyncLocalStorage) |
89
- | How does a URL path map to a Resource? | `Resources.ts → getMatch` (exact/prefix fast path) then `matchParamRoute` (parameterised routes); see "Path routing" below |
78
+ | Question | Where |
79
+ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80
+ | How is a CRUD request authorized? | `Table.ts → #section: authz-hooks`; defaults in `Resource.ts` (`allowRead` etc.) |
81
+ | Where does versioning / conflict resolution happen? | `Table.ts → _writeUpdate` (`#section: write-path-internals`) |
82
+ | How does `search()` choose an index? | `Table.ts → search` (`#section: search-query`) |
83
+ | How are subscriptions replayed? | `Table.ts → subscribe` (`#section: pub-sub`) |
84
+ | How is the response body shaped (select clause)? | `Table.ts → transformEntryForSelect` (`#section: search-query`) |
85
+ | Where is record-level TTL evaluated? | `Table.ts → setTTLExpiration` (`#section: lifecycle-admin`); `Updatable.getExpiresAt` (`#section: setup-and-factory`) |
86
+ | How are residencies enforced (replication)? | `Table.ts → #section: lifecycle-admin` (residency block: `getResidencyRecord`, `setResidency`, `setResidencyById`, `getResidency`) |
87
+ | How is the RecordObject prototype applied? | `RecordEncoder.ts` (see `../DESIGN.md`) |
88
+ | Where is the per-request transaction stored? | `transaction.ts` + `contextStorage` (AsyncLocalStorage) |
89
+ | How does a query opt out of a read snapshot? | Pass `snapshot: false` on the search request (e.g. `get_analytics`). `Table.ts → search` calls `txn.useReadTxn(snapshot === false)`; on RocksDB `DatabaseTransaction.getReadTxn` then builds the read txn with `{ disableSnapshot: true }` so a long scan reads latest without pinning a snapshot. No-op on LMDB (`LMDBTransaction.useReadTxn`). |
90
+ | How does a URL path map to a Resource? | `Resources.ts → getMatch` (exact/prefix fast path) then `matchParamRoute` (parameterised routes); see "Path routing" below |
91
+ | How does HNSW keep the graph connected on delete? | `indexes/HierarchicalNavigableSmallWorld.ts → index()` delete path: zero-degree orphans reindexed via `needsReindexing`; severed multi-node islands detected and reconnected by `repairSeveredNeighbors` (#1712) |
92
+ | How is a filter applied _during_ a vector search? | Predicate-aware traversal (#1241): `search.ts → executeConditions` composes companion AND conditions + a request `vectorFilter` + a static `allowReadRecord` RBAC hook into one `(primaryKey) => boolean` (`composeRecordFilter`) and passes it to `HierarchicalNavigableSmallWorld.search(cond, ctx, filter)`. The filter gates result admission at layer 0 only (routing ignores it, ACORN-style); a visit budget (`filterExpansion`) bounds the under-filled/selective case. Very selective _condition_ filters are instead diverted to the exact brute-force path by the query planner's `estimateCountAsSort` ordering. |
90
93
 
91
94
  ---
92
95
 
@@ -9,7 +9,8 @@ import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
9
9
  import { convertToMS } from '../utility/common_utils.ts';
10
10
  import { when } from '../utility/when.ts';
11
11
  import { setTimeout as delay } from 'node:timers/promises';
12
- import { Transaction as RocksTransaction, type Store as RocksStore } from '@harperfast/rocksdb-js';
12
+ import { Transaction as RocksTransaction, type Store as RocksStore, constants } from '@harperfast/rocksdb-js';
13
+ const RETRY_NOW_VALUE = constants.RETRY_NOW_VALUE;
13
14
  import type { RootDatabaseKind } from './databases.ts';
14
15
  import type { Entry } from './RecordEncoder.ts';
15
16
 
@@ -34,6 +35,23 @@ let txnExpiration = envMngr.get(CONFIG_PARAMS.STORAGE_MAXTRANSACTIONOPENTIME) ??
34
35
 
35
36
  class StartedTransaction extends Error {}
36
37
 
38
+ /**
39
+ * Built when the long-transaction monitor aborts a write-bearing transaction that stayed open past the
40
+ * limit (STORAGE_MAXTRANSACTIONOPENTIME). Surfacing this instead of silently force-committing a partial
41
+ * write set preserves atomicity and avoids the index corruption described in issue #1407: the
42
+ * application gets an actionable error and owns how it splits long-running work into smaller
43
+ * transactions, while core keeps the consistency guarantee.
44
+ */
45
+ export function transactionOpenTooLongError(): ServerError {
46
+ // 422 rather than 503: the condition is deterministic for a given transaction shape, so a retryable
47
+ // status (503/408) would invite clients and gateways to auto-retry the same doomed long transaction.
48
+ // 422 signals the request itself must change (split the work), which is the actionable response.
49
+ return new ServerError(
50
+ 'Transaction was aborted after exceeding the open-transaction limit; split long-running work into smaller transactions',
51
+ 422
52
+ );
53
+ }
54
+
37
55
  type MaybePromise<T> = T | Promise<T>;
38
56
 
39
57
  export type CommitOptions = {
@@ -72,6 +90,9 @@ export type TransactionWrite = {
72
90
  // the commit handler's most recent decision: true means it took an early-return that left savedBlobs unreferenced.
73
91
  // reset at the top of each commit-handler invocation so retries see a fresh state.
74
92
  skipped?: boolean;
93
+ // sticky: a non-isRetry staging of this write appended its audit entry (set in save(); the retry
94
+ // dedup guards in the commit handler read it to ignore the write's own orphaned entry)
95
+ appendedAuditEntry?: boolean;
75
96
  };
76
97
 
77
98
  type RocksTransactionWithRetry = RocksTransaction & { isRetry?: boolean };
@@ -109,8 +130,12 @@ export class DatabaseTransaction implements Transaction {
109
130
  // An explicit marker rather than overloading `retries`, which is also bumped by transient
110
131
  // conflict retries and never reset, so it cannot reliably signal "this is a replay".
111
132
  declare isReplay?: boolean;
133
+ // Set by the long-transaction monitor when it aborts a write-bearing transaction that exceeded the
134
+ // open-transaction limit. Once poisoned, any further addWrite/commit throws transactionOpenTooLongError
135
+ // so the request rolls back cleanly instead of silently committing a partial write set (issue #1407).
136
+ declare timedOut?: boolean;
112
137
 
113
- getReadTxn(): ReadTransaction {
138
+ getReadTxn(disableSnapshot?: boolean): ReadTransaction {
114
139
  this.readTxnRefCount = (this.readTxnRefCount || 0) + 1;
115
140
  this.timeout = txnExpiration; // reset the timeout
116
141
  if (this.transaction) {
@@ -119,7 +144,12 @@ export class DatabaseTransaction implements Transaction {
119
144
  }
120
145
  if (this.open !== TRANSACTION_STATE.OPEN) return; // can not start a new read transaction as there is no future commit that will take place, just have to allow the read to latest database state
121
146
 
122
- this.transaction = new RocksTransaction(this.db.store);
147
+ // `disableSnapshot` (requested via `snapshot: false` on a query) reads against the latest
148
+ // committed data without pinning a consistent snapshot — so a long scan does not hold a
149
+ // snapshot that blocks compaction. Only applied when creating the transaction fresh; an
150
+ // already-open transaction keeps whatever snapshot mode it was created with.
151
+ // `coordinatedRetry` signals IsBusy write conflicts as RETRY_NOW rather than ERR_BUSY.
152
+ this.transaction = new RocksTransaction(this.db.store, { coordinatedRetry: true, disableSnapshot });
123
153
 
124
154
  if (this.timestamp) {
125
155
  this.transaction.setTimestamp(this.timestamp);
@@ -134,8 +164,8 @@ export class DatabaseTransaction implements Transaction {
134
164
  return this.transaction;
135
165
  }
136
166
 
137
- useReadTxn() {
138
- const readTxn = this.getReadTxn();
167
+ useReadTxn(disableSnapshot?: boolean) {
168
+ const readTxn = this.getReadTxn(disableSnapshot);
139
169
  if (DEBUG_LONG_TXNS) this.stackTraces.push(new StartedTransaction());
140
170
  this.readTxnsUsed++;
141
171
  return readTxn;
@@ -173,6 +203,7 @@ export class DatabaseTransaction implements Transaction {
173
203
  }
174
204
 
175
205
  addWrite(operation: TransactionWrite) {
206
+ if (this.timedOut) throw transactionOpenTooLongError();
176
207
  this.writes.push(operation);
177
208
  if (!operation.deferSave) {
178
209
  // Setting saved to false means to defer saving
@@ -187,7 +218,7 @@ export class DatabaseTransaction implements Transaction {
187
218
  return operation;
188
219
  }
189
220
 
190
- save(operation: TransactionWrite, transaction?: RocksTransaction, reloadEntry = false) {
221
+ save(operation: TransactionWrite, transaction?: RocksTransaction, reloadEntry = false, options?: CommitOptions) {
191
222
  let txnTime = this.timestamp;
192
223
  transaction ??= this.transaction;
193
224
  let immediateCommit = false;
@@ -205,11 +236,13 @@ export class DatabaseTransaction implements Transaction {
205
236
  if (txnTime) {
206
237
  transaction.setTimestamp(txnTime);
207
238
  }
208
- } else {
209
239
  }
210
- if (this.retries > 0) {
211
- // This marks the Rocks transaction as a retry so we don't write the transaction log again
212
- (transaction as any).isRetry = true;
240
+ if (this.isReplay) {
241
+ // Replayed writes came FROM the transaction log; never re-append them
242
+ // replay iterates that same log, so re-appending prevents convergence
243
+ // (boot hangs replaying its own output). Conflict retries stamp isRetry
244
+ // at the retry sites in commit(); this is the replay-path equivalent.
245
+ (transaction as RocksTransactionWithRetry).isRetry = true;
213
246
  }
214
247
  if (!txnTime) txnTime = this.timestamp = transaction.getTimestamp();
215
248
  if (reloadEntry || operation.entry === undefined) {
@@ -228,8 +261,16 @@ export class DatabaseTransaction implements Transaction {
228
261
  if (result?.then) this.completions.push(result);
229
262
  }
230
263
  operation.commit(txnTime, operation.entry, this.retries > 0, transaction);
264
+ // Sticky record that THIS write staged with its audit entry appended (log entries are written
265
+ // at staging and are not part of the transaction, so they survive an abort). isRetry stagings
266
+ // skip the log write, so they never set it. The retry dedup guards in the commit handler key
267
+ // off this: a launderable proxy (like last attempt's skipped state) breaks under multi-round
268
+ // retries where a recommit round self-skips before a fresh-transaction replay.
269
+ if (!operation.skipped && !(transaction as RocksTransactionWithRetry).isRetry) {
270
+ operation.appendedAuditEntry = true;
271
+ }
231
272
  if (immediateCommit) {
232
- return this.commit({ transaction }); // immediately commit if the harper transaction is closed
273
+ return this.commit({ ...options, transaction }); // immediately commit if the harper transaction is closed
233
274
  }
234
275
  }
235
276
 
@@ -237,11 +278,12 @@ export class DatabaseTransaction implements Transaction {
237
278
  * Resolves with information on the timestamp and success of the commit
238
279
  */
239
280
  commit(options: CommitOptions = {}): MaybePromise<CommitResolution> {
281
+ if (this.timedOut) throw transactionOpenTooLongError();
240
282
  let transaction = options.transaction ?? this.transaction; // we need to preserve this transaction as we might to resurrect it if we have to retry
241
283
  for (let i = 0; i < this.writes.length; i++) {
242
284
  let operation = this.writes[i];
243
285
  if (!operation || (this.retries === 0 && operation.saved)) continue;
244
- this.save(operation, transaction, i < this.validated);
286
+ this.save(operation, transaction, i < this.validated, options);
245
287
  }
246
288
  this.validated = this.writes.length;
247
289
  const completions = this.completions;
@@ -254,7 +296,9 @@ export class DatabaseTransaction implements Transaction {
254
296
  return this.commit(options);
255
297
  }
256
298
  this.open = TRANSACTION_STATE.CLOSED;
257
- let commitResolution: MaybePromise<void>;
299
+ // RocksTransaction.commit() resolves with RETRY_NOW_VALUE (a number) under
300
+ // coordinatedRetry, or void on a normal commit/abort.
301
+ let commitResolution: Promise<number | void> | void;
258
302
  if (--this.readTxnsUsed > 0) {
259
303
  // we still have outstanding iterators using the transaction, we can't just commit/abort it, we will still
260
304
  // need to use it
@@ -279,10 +323,11 @@ export class DatabaseTransaction implements Transaction {
279
323
  if (transaction) {
280
324
  this.writes = this.writes.filter((write) => write); // filter out removed entries
281
325
  if (this.writes.length > 0) {
282
- // rocksdb-js 2.2.0 widened commit()'s resolution to include the
283
- // coordinated-retry sentinel (RETRY_NOW); we don't pass
284
- // coordinatedRetry, so it never resolves to the sentinel here. Cast
285
- // away the sentinel to keep commitResolution void.
326
+ // The transaction was created with coordinatedRetry:true (see
327
+ // getReadTxn), so commit() can resolve to RETRY_NOW_VALUE.
328
+ // That sentinel is handled in the resolve callback below and the
329
+ // cast to Promise<void> is safe — the sentinel never propagates
330
+ // past that branch.
286
331
  commitResolution = transaction.commit() as Promise<void>;
287
332
  } else {
288
333
  try {
@@ -312,7 +357,29 @@ export class DatabaseTransaction implements Transaction {
312
357
  }
313
358
  const completions = [];
314
359
  return commitResolution.then(
315
- () => {
360
+ (commitResult) => {
361
+ if (commitResult === RETRY_NOW_VALUE) {
362
+ this.retries++;
363
+ harperLogger.debug?.('coordinated retry', transaction.id, this.retries);
364
+ // Mark this specific native transaction as a retry so RocksTransactionLogStore
365
+ // skips re-writing its already-staged txn-log entries (#2).
366
+ (transaction as RocksTransactionWithRetry).isRetry = true;
367
+ // Mirror the ERR_BUSY cap/warn policy: non-sourceApply transactions abort
368
+ // at MAX_RETRIES; sourceApply transactions keep retrying with periodic warn.
369
+ if (this.retries > MAX_RETRIES) {
370
+ if (!this.sourceApply) {
371
+ throw new ServerError(
372
+ `After ${MAX_RETRIES} coordinated retries, unable to commit transaction, transaction is in conflict with ongoing writes`
373
+ );
374
+ }
375
+ if (this.retries % MAX_RETRIES === 0) {
376
+ harperLogger.warn?.(
377
+ `Source-applied transaction ${transaction.id} still in conflict after ${this.retries} coordinated retries; continuing to retry`
378
+ );
379
+ }
380
+ }
381
+ return this.commit({ ...options, transaction });
382
+ }
316
383
  // onCommit may be async (e.g. RocksTransactionLogStore emits 'aftercommit'). Surface a
317
384
  // rejection — or a synchronous throw — via logging rather than failing the commit, since
318
385
  // the write is already durable.
@@ -351,6 +418,7 @@ export class DatabaseTransaction implements Transaction {
351
418
  cleanupUnusedBlobs(write.savedBlobs, collectRetainedFileIds(write.store.getEntry(write.key)?.value));
352
419
  }
353
420
  // now reset transactions tracking; this transaction be reused and committed again
421
+ this.retries = 0; // reset per-native-transaction retry counter so a reused DatabaseTransaction's next batch starts fresh
354
422
  this.writes = [];
355
423
  if (this.#context?.resourceCache) this.#context.resourceCache = null;
356
424
  this.next = null;
@@ -363,6 +431,13 @@ export class DatabaseTransaction implements Transaction {
363
431
  });
364
432
  },
365
433
  (error) => {
434
+ // Coordinated transactions surface conflicts as RETRY_NOW (handled in the
435
+ // resolve branch above) and never reach here with ERR_BUSY. But not every
436
+ // write transaction is coordinated — a write that reaches save() with no
437
+ // prior getReadTxn() (immediate/publish/invalidate writes) is created
438
+ // without coordinatedRetry and still rejects with ERR_BUSY on conflict.
439
+ // Keep the backoff retry as the fallback for those paths.
440
+ //
366
441
  // ERR_BUSY: optimistic-transaction write conflict. ERR_TRY_AGAIN: RocksDB kTryAgain —
367
442
  // the transaction's snapshot sequence fell outside the memtable conflict-check window
368
443
  // (max_write_buffer_size_to_maintain), which happens under bulk-ingest bursts such as a
@@ -374,6 +449,36 @@ export class DatabaseTransaction implements Transaction {
374
449
  // for future transactions
375
450
  this.retries++;
376
451
  harperLogger.debug?.('retrying', transaction.id, this.retries);
452
+ if (error.code === 'ERR_TRY_AGAIN') {
453
+ // ERR_BUSY recovers on recommit: the save loop re-writes each key, re-tracking it at
454
+ // the current sequence, so validation passes once the contention clears. ERR_TRY_AGAIN
455
+ // never does: the memtable history validation needs is gone (flushed during a
456
+ // bulk-ingest burst), and recommitting re-checks the same stranded snapshot, so it
457
+ // fails forever even on an idle database. A source-apply transaction's uncapped retry
458
+ // then spins for good and wedges the replication apply loop at its commit await,
459
+ // freezing every leg of that database on the node. Replay onto a fresh transaction
460
+ // instead: the save loop reloads each entry through it and re-resolves against
461
+ // current state. Carry over the commit hook the transaction-log store attached
462
+ // (aftercommit emit / structure watermarks; it reads its state off the original
463
+ // transaction object, which abort() leaves intact); the retry-site isRetry stamp
464
+ // below keeps the log entries themselves from being re-added.
465
+ const retryTransaction: RocksTransactionWithRetry = new RocksTransaction(
466
+ (this.writes.find((write) => write)?.store.store ?? this.db.store) as RocksStore
467
+ );
468
+ if (this.timestamp) retryTransaction.setTimestamp(this.timestamp);
469
+ (retryTransaction as any).onCommit = (transaction as any).onCommit;
470
+ try {
471
+ transaction.abort();
472
+ } catch (abortError) {
473
+ // usually already released by the failed commit; log for the unexpected case
474
+ harperLogger.debug?.('aborting stranded transaction after failed commit', abortError);
475
+ }
476
+ transaction = retryTransaction;
477
+ }
478
+ // Mark the native transaction as a retry so RocksTransactionLogStore skips re-staging entries.
479
+ // Stamp AFTER the possible ERR_TRY_AGAIN swap above so the fresh replay transaction is
480
+ // marked too; otherwise it would re-stage audit/change-feed log entries on recommit.
481
+ (transaction as RocksTransactionWithRetry).isRetry = true;
377
482
  if (this.retries > 2) {
378
483
  // Transactions applying data from a canonical source of truth (replication peer or
379
484
  // external caching source) must never drop a write on a transient conflict: there is no
@@ -384,6 +489,13 @@ export class DatabaseTransaction implements Transaction {
384
489
  const neverDropOnConflict = this.sourceApply;
385
490
  if (this.retries > MAX_RETRIES) {
386
491
  if (!neverDropOnConflict) {
492
+ // giving up: release the current transaction (original or the fresh replay above)
493
+ // so the throw does not leak its native handle
494
+ try {
495
+ transaction.abort();
496
+ } catch (abortError) {
497
+ harperLogger.debug?.('aborting conflicted transaction after exhausting retries', abortError);
498
+ }
387
499
  throw new ServerError(
388
500
  `After ${MAX_RETRIES} retries, unable to commit transaction, transaction is in conflict with ongoing writes`
389
501
  );
@@ -398,10 +510,10 @@ export class DatabaseTransaction implements Transaction {
398
510
  }
399
511
  // start delaying, back off to try to space out transactions and avoid excessive conflicts
400
512
  return delay(Math.min(this.retries * this.retries, MAX_RETRY_DELAY_MS)).then(() =>
401
- this.commit({ transaction })
513
+ this.commit({ ...options, transaction })
402
514
  );
403
515
  }
404
- return this.commit({ transaction }); // try again
516
+ return this.commit({ ...options, transaction }); // try again
405
517
  } else throw error;
406
518
  }
407
519
  );
@@ -445,6 +557,45 @@ export class DatabaseTransaction implements Transaction {
445
557
  this.writes = [];
446
558
  if (this.#context?.resourceCache) this.#context.resourceCache = null;
447
559
  }
560
+ /**
561
+ * True if this transaction — or any database in its multi-store `next` chain — has writes accumulated
562
+ * that have not yet been committed. Writes to a second database live on `next` (see txnForContext), so a
563
+ * transaction that reads database A (head, tracked via its read snapshot, empty `writes`) and writes
564
+ * database B (`next`) must still count as write-bearing, or the monitor would misclassify it as read-only
565
+ * and force-commit B's writes via the commit cascade (issue #1407, multi-store path).
566
+ */
567
+ hasPendingWrites(): boolean {
568
+ for (let txn: DatabaseTransaction = this; txn; txn = txn.next) {
569
+ if (txn.writes.some((write) => write)) return true;
570
+ }
571
+ return false;
572
+ }
573
+ /**
574
+ * Abort and poison this transaction because it exceeded the open-transaction limit. The next write or
575
+ * commit throws transactionOpenTooLongError so the request fails cleanly and rolls back, rather than the
576
+ * monitor force-committing a partial write set on the application's behalf (issue #1407). The whole
577
+ * multi-store `next` chain is poisoned and aborted: writes to a second database live on `next`, so
578
+ * leaving it un-poisoned would let the head's commit cascade force-commit them (or orphan its resources
579
+ * until it self-times-out).
580
+ */
581
+ abortDueToTimeout(): void {
582
+ // Poison every link first, then abort each, so a throw from one link's abort() can't leave later links
583
+ // in the chain un-poisoned (and thus eligible to be force-committed by a later commit cascade).
584
+ for (let txn: DatabaseTransaction = this; txn; txn = txn.next) {
585
+ txn.timedOut = true;
586
+ // Force the CLOSED path when releasing the read snapshot: doneReadTxn() flushes lingering writes via
587
+ // commit() while open === LINGERING, and commit() now throws transactionOpenTooLongError once poisoned.
588
+ // Closing first makes the release discard (abort) the uncommitted writes instead, which is the intent.
589
+ txn.open = TRANSACTION_STATE.CLOSED;
590
+ }
591
+ for (let txn: DatabaseTransaction = this; txn; txn = txn.next) {
592
+ try {
593
+ txn.abort();
594
+ } catch (error) {
595
+ harperLogger.debug?.(`Error aborting timed-out transaction in chain: ${error.message}`);
596
+ }
597
+ }
598
+ }
448
599
  directCommitSync(): void {
449
600
  trackedTxns.delete(this);
450
601
  this.transaction?.commitSync();
@@ -504,25 +655,43 @@ function startMonitoringTxns() {
504
655
  for (const txn of trackedTxns) {
505
656
  if (txn.timeout <= 0) {
506
657
  const url = (txn.getContext() as any)?.url;
507
- harperLogger.error(
508
- `Transaction was open too long and has been committed, from table: ${
509
- (txn.db as any)?.name + (url ? ' path: ' + url : '')
510
- }`,
511
- ...(txn.startedFrom ? [`was started from ${txn.startedFrom.resourceName}.${txn.startedFrom.method}`] : []),
512
- ...(DEBUG_LONG_TXNS ? ['starting stack trace', txn.stackTraces] : [])
513
- );
514
- // reset the transaction
515
- try {
516
- const result = txn.commit();
517
- if ((result as any)?.then) {
518
- (result as any).catch((error) => {
519
- harperLogger.debug?.(`Error committing timed out transaction: ${error.message}`);
520
- });
658
+ if (txn.hasPendingWrites() && !txn.sourceApply && !txn.isReplay) {
659
+ // Abort and surface an error rather than force-committing a partial write set: silently
660
+ // committing on the application's behalf breaks atomicity and can leave orphaned
661
+ // secondary-index entries that only a full index rebuild repairs (issue #1407). The app
662
+ // owns long-running work (split into smaller transactions); core owns consistency.
663
+ // Canonical-source applies (replication peer / external caching source) and crash-recovery
664
+ // replay are excluded: they have no resubscribe/resume path, so aborting a write would drop
665
+ // it while the resume cursor advances past it — a permanent divergence (harper-pro#348). For
666
+ // those, keep the prior force-commit behavior below.
667
+ harperLogger.error(
668
+ `Transaction was open too long and has been aborted after exceeding the open-transaction limit, from table: ${
669
+ (txn.db as any)?.name + (url ? ' path: ' + url : '')
670
+ }`,
671
+ ...(txn.startedFrom ? [`was started from ${txn.startedFrom.resourceName}.${txn.startedFrom.method}`] : []),
672
+ ...(DEBUG_LONG_TXNS ? ['starting stack trace', txn.stackTraces] : [])
673
+ );
674
+ try {
675
+ txn.abortDueToTimeout();
676
+ } catch (error) {
677
+ harperLogger.debug?.(`Error aborting timed out transaction: ${error.message}`);
678
+ }
679
+ } else {
680
+ // Read-only long transaction (no atomicity/index risk — e.g. a large scan or export), or a
681
+ // canonical-source apply/replay that must never drop a write: preserve the prior behavior of
682
+ // committing to close out the snapshot without poisoning the transaction.
683
+ try {
684
+ const result = txn.commit();
685
+ if ((result as any)?.then) {
686
+ (result as any).catch((error) => {
687
+ harperLogger.debug?.(`Error committing timed out transaction: ${error.message}`);
688
+ });
689
+ }
690
+ } catch (error) {
691
+ harperLogger.debug?.(`Error committing timed out transaction: ${error.message}`);
521
692
  }
522
- } catch (error) {
523
- harperLogger.debug?.(`Error committing timed out transaction: ${error.message}`);
693
+ txn.timeout = txnExpiration;
524
694
  }
525
- txn.timeout = txnExpiration;
526
695
  } else {
527
696
  txn.timeout -= txnExpiration;
528
697
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  DatabaseTransaction,
3
+ transactionOpenTooLongError,
3
4
  type CommitOptions,
4
5
  type TransactionWrite,
5
6
  type CommitResolution,
@@ -54,7 +55,11 @@ export class LMDBTransaction extends DatabaseTransaction {
54
55
  return this.readTxn;
55
56
  }
56
57
 
57
- useReadTxn() {
58
+ // `disableSnapshot` is accepted for parity with the RocksDB transaction, but LMDB has no
59
+ // per-transaction snapshot toggle (its only lever is `snapshot: false` on `getRange`, which
60
+ // throws on the dupSort secondary-index stores we use). So LMDB-backed reads always hold a
61
+ // snapshot for now; `snapshot: false` queries are honored on RocksDB-backed tables.
62
+ useReadTxn(_disableSnapshot?: boolean) {
58
63
  this.getReadTxn();
59
64
  if (this.readTxn) {
60
65
  (this.readTxn as any).use();
@@ -83,6 +88,7 @@ export class LMDBTransaction extends DatabaseTransaction {
83
88
  }
84
89
 
85
90
  addWrite(operation: TransactionWrite): any {
91
+ if (this.timedOut) throw transactionOpenTooLongError();
86
92
  if (this.open === TRANSACTION_STATE.CLOSED) {
87
93
  throw new Error('Can not use a transaction that is no longer open');
88
94
  }
@@ -112,6 +118,7 @@ export class LMDBTransaction extends DatabaseTransaction {
112
118
  * Resolves with information on the timestamp and success of the commit
113
119
  */
114
120
  commit(options: CommitOptions = {}): any {
121
+ if (this.timedOut) throw transactionOpenTooLongError();
115
122
  options = options || {};
116
123
  let txnTime = this.timestamp;
117
124
  if (!txnTime) txnTime = this.timestamp = options.timestamp || getNextMonotonicTime();
@@ -347,14 +354,41 @@ function startMonitoringTxns() {
347
354
  for (const txn of trackedTxns) {
348
355
  if (txn.timeout <= 0) {
349
356
  const url = (txn.getContext() as any)?.url;
350
- harperLogger.error(
351
- `Transaction was open too long and has been committed, from table: ${
352
- (txn.db as any)?.name + (url ? ' path: ' + url : '')
353
- }`
354
- );
355
- // reset the transaction
356
- txn.commit();
357
- txn.timeout = txnExpiration;
357
+ if (txn.hasPendingWrites() && !txn.sourceApply && !txn.isReplay) {
358
+ // Abort and surface an error rather than force-committing a partial write set: silently
359
+ // committing on the application's behalf breaks atomicity and can leave orphaned
360
+ // secondary-index entries that only a full index rebuild repairs (issue #1407). The app
361
+ // owns long-running work (split into smaller transactions); core owns consistency.
362
+ // Canonical-source applies (replication peer / external caching source) and crash-recovery
363
+ // replay are excluded: they have no resubscribe/resume path, so aborting a write would drop
364
+ // it while the resume cursor advances past it — a permanent divergence (harper-pro#348). For
365
+ // those, keep the prior force-commit behavior below.
366
+ harperLogger.error(
367
+ `Transaction was open too long and has been aborted after exceeding the open-transaction limit, from table: ${
368
+ (txn.db as any)?.name + (url ? ' path: ' + url : '')
369
+ }`
370
+ );
371
+ try {
372
+ txn.abortDueToTimeout();
373
+ } catch (error) {
374
+ harperLogger.debug?.(`Error aborting timed out transaction: ${error.message}`);
375
+ }
376
+ } else {
377
+ // Read-only long transaction (no atomicity/index risk — e.g. a large scan or export), or a
378
+ // canonical-source apply/replay that must never drop a write: preserve the prior behavior of
379
+ // committing to close out the snapshot without poisoning the transaction.
380
+ try {
381
+ const result = txn.commit();
382
+ if ((result as any)?.then) {
383
+ (result as any).catch((error) => {
384
+ harperLogger.debug?.(`Error committing timed out transaction: ${error.message}`);
385
+ });
386
+ }
387
+ } catch (error) {
388
+ harperLogger.debug?.(`Error committing timed out transaction: ${error.message}`);
389
+ }
390
+ txn.timeout = txnExpiration;
391
+ }
358
392
  } else {
359
393
  txn.timeout -= txnExpiration;
360
394
  }