@harperfast/harper 5.2.13 → 5.3.0-beta.1

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 (655) hide show
  1. package/agent/mcpTools.ts +1 -1
  2. package/agent/session.ts +25 -14
  3. package/bin/cliOperations.ts +58 -9
  4. package/bin/copyDb.ts +282 -59
  5. package/bin/deploySetup.ts +16 -5
  6. package/bin/harper.ts +1 -1
  7. package/bin/help.ts +4 -1
  8. package/bin/lite.ts +4 -1
  9. package/bin/restart.ts +120 -4
  10. package/bin/run.ts +6 -11
  11. package/bin/upgrade.js +7 -3
  12. package/bin/workloadIdentity.ts +119 -0
  13. package/components/Application.ts +3436 -242
  14. package/components/ApplicationScope.ts +8 -0
  15. package/components/EntryHandler.ts +59 -39
  16. package/components/OptionsWatcher.ts +440 -98
  17. package/components/RuntimeModuleTracker.ts +38 -7
  18. package/components/Scope.ts +56 -15
  19. package/components/awaitRestart.ts +84 -0
  20. package/components/componentLoader.ts +381 -32
  21. package/components/componentPreparationLock.ts +16 -5
  22. package/components/deploymentOperations.ts +4 -1
  23. package/components/deploymentRecorder.ts +9 -2
  24. package/components/mcp/adapters/harperHttp.ts +4 -0
  25. package/components/mcp/listChanged.ts +4 -0
  26. package/components/mcp/toolRegistry.ts +2 -0
  27. package/components/mcp/tools/operations.ts +9 -0
  28. package/components/mcp/tools/schemas/operationDescriptions.ts +2 -2
  29. package/components/operations.js +537 -113
  30. package/components/operationsValidation.js +98 -3
  31. package/components/packageComponent.ts +25 -1
  32. package/components/requestRestart.ts +11 -0
  33. package/components/status/ComponentStatusRegistry.ts +59 -0
  34. package/config/RootConfigWatcher.ts +240 -40
  35. package/config/configReadRetry.ts +62 -0
  36. package/config/configUtils.ts +357 -48
  37. package/config/harperConfigEnvVars.ts +170 -27
  38. package/config/parseConfigFile.ts +34 -0
  39. package/config/readConfigFileSync.ts +44 -0
  40. package/config/watcherArming.ts +59 -0
  41. package/config-root.schema.json +33 -0
  42. package/dataLayer/blobBackup.ts +160 -50
  43. package/dataLayer/delete.ts +6 -1
  44. package/dataLayer/harperBridge/ResourceBridge.ts +80 -10
  45. package/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
  46. package/dataLayer/hdbInfoController.ts +34 -1
  47. package/dataLayer/insert.ts +44 -1
  48. package/dataLayer/rocksdbBackup.ts +53 -10
  49. package/dataLayer/schema.ts +11 -1
  50. package/dataLayer/schemaDescribe.ts +8 -1
  51. package/dist/agent/mcpTools.js +1 -1
  52. package/dist/agent/mcpTools.js.map +1 -1
  53. package/dist/agent/session.d.ts +22 -0
  54. package/dist/agent/session.js +26 -15
  55. package/dist/agent/session.js.map +1 -1
  56. package/dist/bin/cliOperations.js +61 -9
  57. package/dist/bin/cliOperations.js.map +1 -1
  58. package/dist/bin/copyDb.d.ts +12 -1
  59. package/dist/bin/copyDb.js +248 -60
  60. package/dist/bin/copyDb.js.map +1 -1
  61. package/dist/bin/deploySetup.d.ts +2 -0
  62. package/dist/bin/deploySetup.js +11 -3
  63. package/dist/bin/deploySetup.js.map +1 -1
  64. package/dist/bin/harper.js +1 -1
  65. package/dist/bin/harper.js.map +1 -1
  66. package/dist/bin/help.js +4 -1
  67. package/dist/bin/help.js.map +1 -1
  68. package/dist/bin/lite.js +4 -1
  69. package/dist/bin/lite.js.map +1 -1
  70. package/dist/bin/restart.js +91 -6
  71. package/dist/bin/restart.js.map +1 -1
  72. package/dist/bin/run.js +4 -10
  73. package/dist/bin/run.js.map +1 -1
  74. package/dist/bin/upgrade.js +4 -3
  75. package/dist/bin/upgrade.js.map +1 -1
  76. package/dist/bin/workloadIdentity.d.ts +18 -0
  77. package/dist/bin/workloadIdentity.js +100 -0
  78. package/dist/bin/workloadIdentity.js.map +1 -0
  79. package/dist/components/Application.d.ts +235 -17
  80. package/dist/components/Application.js +3038 -238
  81. package/dist/components/Application.js.map +1 -1
  82. package/dist/components/ApplicationScope.d.ts +8 -0
  83. package/dist/components/ApplicationScope.js +7 -0
  84. package/dist/components/ApplicationScope.js.map +1 -1
  85. package/dist/components/EntryHandler.js +26 -10
  86. package/dist/components/EntryHandler.js.map +1 -1
  87. package/dist/components/OptionsWatcher.d.ts +4 -0
  88. package/dist/components/OptionsWatcher.js +440 -99
  89. package/dist/components/OptionsWatcher.js.map +1 -1
  90. package/dist/components/RuntimeModuleTracker.js +40 -6
  91. package/dist/components/RuntimeModuleTracker.js.map +1 -1
  92. package/dist/components/Scope.js +52 -13
  93. package/dist/components/Scope.js.map +1 -1
  94. package/dist/components/awaitRestart.d.ts +33 -0
  95. package/dist/components/awaitRestart.js +61 -0
  96. package/dist/components/awaitRestart.js.map +1 -0
  97. package/dist/components/componentLoader.d.ts +38 -1
  98. package/dist/components/componentLoader.js +313 -24
  99. package/dist/components/componentLoader.js.map +1 -1
  100. package/dist/components/componentPreparationLock.d.ts +5 -0
  101. package/dist/components/componentPreparationLock.js +14 -6
  102. package/dist/components/componentPreparationLock.js.map +1 -1
  103. package/dist/components/deploymentOperations.js +4 -1
  104. package/dist/components/deploymentOperations.js.map +1 -1
  105. package/dist/components/deploymentRecorder.d.ts +4 -2
  106. package/dist/components/deploymentRecorder.js +1 -0
  107. package/dist/components/deploymentRecorder.js.map +1 -1
  108. package/dist/components/mcp/adapters/harperHttp.js +4 -0
  109. package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
  110. package/dist/components/mcp/listChanged.js +5 -0
  111. package/dist/components/mcp/listChanged.js.map +1 -1
  112. package/dist/components/mcp/toolRegistry.d.ts +1 -0
  113. package/dist/components/mcp/toolRegistry.js.map +1 -1
  114. package/dist/components/mcp/tools/operations.d.ts +5 -0
  115. package/dist/components/mcp/tools/operations.js +9 -0
  116. package/dist/components/mcp/tools/operations.js.map +1 -1
  117. package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -2
  118. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  119. package/dist/components/operations.d.ts +28 -0
  120. package/dist/components/operations.js +476 -113
  121. package/dist/components/operations.js.map +1 -1
  122. package/dist/components/operationsValidation.js +97 -3
  123. package/dist/components/operationsValidation.js.map +1 -1
  124. package/dist/components/packageComponent.js +24 -0
  125. package/dist/components/packageComponent.js.map +1 -1
  126. package/dist/components/requestRestart.d.ts +1 -0
  127. package/dist/components/requestRestart.js +7 -0
  128. package/dist/components/requestRestart.js.map +1 -1
  129. package/dist/components/status/ComponentStatusRegistry.d.ts +0 -4
  130. package/dist/components/status/ComponentStatusRegistry.js +63 -0
  131. package/dist/components/status/ComponentStatusRegistry.js.map +1 -1
  132. package/dist/config/RootConfigWatcher.d.ts +9 -1
  133. package/dist/config/RootConfigWatcher.js +228 -37
  134. package/dist/config/RootConfigWatcher.js.map +1 -1
  135. package/dist/config/configReadRetry.d.ts +8 -0
  136. package/dist/config/configReadRetry.js +62 -0
  137. package/dist/config/configReadRetry.js.map +1 -0
  138. package/dist/config/configUtils.d.ts +11 -2
  139. package/dist/config/configUtils.js +304 -47
  140. package/dist/config/configUtils.js.map +1 -1
  141. package/dist/config/harperConfigEnvVars.d.ts +16 -0
  142. package/dist/config/harperConfigEnvVars.js +162 -25
  143. package/dist/config/harperConfigEnvVars.js.map +1 -1
  144. package/dist/config/parseConfigFile.d.ts +4 -0
  145. package/dist/config/parseConfigFile.js +35 -0
  146. package/dist/config/parseConfigFile.js.map +1 -0
  147. package/dist/config/readConfigFileSync.d.ts +1 -0
  148. package/dist/config/readConfigFileSync.js +47 -0
  149. package/dist/config/readConfigFileSync.js.map +1 -0
  150. package/dist/config/watcherArming.d.ts +15 -0
  151. package/dist/config/watcherArming.js +59 -0
  152. package/dist/config/watcherArming.js.map +1 -0
  153. package/dist/dataLayer/blobBackup.d.ts +49 -20
  154. package/dist/dataLayer/blobBackup.js +139 -50
  155. package/dist/dataLayer/blobBackup.js.map +1 -1
  156. package/dist/dataLayer/delete.js +1 -1
  157. package/dist/dataLayer/delete.js.map +1 -1
  158. package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +14 -1
  159. package/dist/dataLayer/harperBridge/ResourceBridge.js +72 -12
  160. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  161. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.d.ts +3 -1
  162. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
  163. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js.map +1 -1
  164. package/dist/dataLayer/hdbInfoController.d.ts +10 -0
  165. package/dist/dataLayer/hdbInfoController.js +30 -1
  166. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  167. package/dist/dataLayer/insert.d.ts +9 -1
  168. package/dist/dataLayer/insert.js +30 -0
  169. package/dist/dataLayer/insert.js.map +1 -1
  170. package/dist/dataLayer/rocksdbBackup.d.ts +2 -2
  171. package/dist/dataLayer/rocksdbBackup.js +45 -8
  172. package/dist/dataLayer/rocksdbBackup.js.map +1 -1
  173. package/dist/dataLayer/schema.js +8 -0
  174. package/dist/dataLayer/schema.js.map +1 -1
  175. package/dist/dataLayer/schemaDescribe.js +8 -1
  176. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  177. package/dist/index.d.ts +2 -1
  178. package/dist/index.js +4 -1
  179. package/dist/index.js.map +1 -1
  180. package/dist/json/systemSchema.json +55 -0
  181. package/dist/resources/DatabaseTransaction.d.ts +77 -0
  182. package/dist/resources/DatabaseTransaction.js +586 -54
  183. package/dist/resources/DatabaseTransaction.js.map +1 -1
  184. package/dist/resources/LMDBTransaction.d.ts +2 -1
  185. package/dist/resources/LMDBTransaction.js +43 -6
  186. package/dist/resources/LMDBTransaction.js.map +1 -1
  187. package/dist/resources/PrimaryRocksDatabase.d.ts +1 -0
  188. package/dist/resources/PrimaryRocksDatabase.js +49 -5
  189. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  190. package/dist/resources/RecordEncoder.d.ts +20 -0
  191. package/dist/resources/RecordEncoder.js +110 -9
  192. package/dist/resources/RecordEncoder.js.map +1 -1
  193. package/dist/resources/RequestTarget.d.ts +2 -0
  194. package/dist/resources/RequestTarget.js.map +1 -1
  195. package/dist/resources/Resource.js +117 -24
  196. package/dist/resources/Resource.js.map +1 -1
  197. package/dist/resources/ResourceInterface.d.ts +28 -1
  198. package/dist/resources/ResourceInterface.js.map +1 -1
  199. package/dist/resources/RocksIndexStore.d.ts +6 -1
  200. package/dist/resources/RocksIndexStore.js +24 -9
  201. package/dist/resources/RocksIndexStore.js.map +1 -1
  202. package/dist/resources/RocksTransactionLogStore.d.ts +24 -1
  203. package/dist/resources/RocksTransactionLogStore.js +160 -49
  204. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  205. package/dist/resources/Table.d.ts +165 -10
  206. package/dist/resources/Table.js +1805 -303
  207. package/dist/resources/Table.js.map +1 -1
  208. package/dist/resources/analytics/write.js +10 -3
  209. package/dist/resources/analytics/write.js.map +1 -1
  210. package/dist/resources/auditStore.d.ts +181 -2
  211. package/dist/resources/auditStore.js +640 -29
  212. package/dist/resources/auditStore.js.map +1 -1
  213. package/dist/resources/blob.d.ts +129 -9
  214. package/dist/resources/blob.js +992 -114
  215. package/dist/resources/blob.js.map +1 -1
  216. package/dist/resources/branchDatabase.d.ts +48 -0
  217. package/dist/resources/branchDatabase.js +892 -0
  218. package/dist/resources/branchDatabase.js.map +1 -0
  219. package/dist/resources/crdt.js +50 -12
  220. package/dist/resources/crdt.js.map +1 -1
  221. package/dist/resources/dataLoader.js +3 -4
  222. package/dist/resources/dataLoader.js.map +1 -1
  223. package/dist/resources/databases.d.ts +157 -14
  224. package/dist/resources/databases.js +1462 -325
  225. package/dist/resources/databases.js.map +1 -1
  226. package/dist/resources/defineTable.d.ts +10 -2
  227. package/dist/resources/defineTable.js +9 -1
  228. package/dist/resources/defineTable.js.map +1 -1
  229. package/dist/resources/derivedIndexRegistry.d.ts +5 -0
  230. package/dist/resources/derivedIndexRegistry.js +68 -0
  231. package/dist/resources/derivedIndexRegistry.js.map +1 -0
  232. package/dist/resources/derivedIndexRuntime.d.ts +215 -0
  233. package/dist/resources/derivedIndexRuntime.js +2027 -0
  234. package/dist/resources/derivedIndexRuntime.js.map +1 -0
  235. package/dist/resources/graphql.d.ts +1 -1
  236. package/dist/resources/graphql.js +52 -16
  237. package/dist/resources/graphql.js.map +1 -1
  238. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +103 -9
  239. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +854 -41
  240. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  241. package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
  242. package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
  243. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
  244. package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
  245. package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
  246. package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
  247. package/dist/resources/longLivedTransactions.d.ts +71 -0
  248. package/dist/resources/longLivedTransactions.js +358 -0
  249. package/dist/resources/longLivedTransactions.js.map +1 -0
  250. package/dist/resources/models/backendRegistry.d.ts +26 -0
  251. package/dist/resources/models/backendRegistry.js +60 -2
  252. package/dist/resources/models/backendRegistry.js.map +1 -1
  253. package/dist/resources/models/bootstrap.d.ts +33 -1
  254. package/dist/resources/models/bootstrap.js +416 -31
  255. package/dist/resources/models/bootstrap.js.map +1 -1
  256. package/dist/resources/nodeIdMapping.d.ts +5 -0
  257. package/dist/resources/nodeIdMapping.js +49 -0
  258. package/dist/resources/nodeIdMapping.js.map +1 -1
  259. package/dist/resources/recordLock.d.ts +123 -0
  260. package/dist/resources/recordLock.js +315 -0
  261. package/dist/resources/recordLock.js.map +1 -0
  262. package/dist/resources/recordLockCoordinator.d.ts +557 -0
  263. package/dist/resources/recordLockCoordinator.js +2565 -0
  264. package/dist/resources/recordLockCoordinator.js.map +1 -0
  265. package/dist/resources/replayLogs.d.ts +14 -1
  266. package/dist/resources/replayLogs.js +157 -24
  267. package/dist/resources/replayLogs.js.map +1 -1
  268. package/dist/resources/replayLogsGuards.d.ts +94 -7
  269. package/dist/resources/replayLogsGuards.js +111 -7
  270. package/dist/resources/replayLogsGuards.js.map +1 -1
  271. package/dist/resources/replicatedApplyFailure.d.ts +16 -0
  272. package/dist/resources/replicatedApplyFailure.js +63 -0
  273. package/dist/resources/replicatedApplyFailure.js.map +1 -0
  274. package/dist/resources/scheduler/scheduler.js +3 -3
  275. package/dist/resources/scheduler/scheduler.js.map +1 -1
  276. package/dist/resources/search.d.ts +10 -4
  277. package/dist/resources/search.js +283 -37
  278. package/dist/resources/search.js.map +1 -1
  279. package/dist/resources/tracked.d.ts +5 -1
  280. package/dist/resources/tracked.js +74 -23
  281. package/dist/resources/tracked.js.map +1 -1
  282. package/dist/resources/transactionBroadcast.d.ts +1 -1
  283. package/dist/resources/transactionBroadcast.js +2 -2
  284. package/dist/resources/transactionBroadcast.js.map +1 -1
  285. package/dist/security/auth.js +61 -30
  286. package/dist/security/auth.js.map +1 -1
  287. package/dist/security/authn/oidc/claims.d.ts +22 -0
  288. package/dist/security/authn/oidc/claims.js +71 -0
  289. package/dist/security/authn/oidc/claims.js.map +1 -0
  290. package/dist/security/authn/oidc/identityToken.d.ts +27 -0
  291. package/dist/security/authn/oidc/identityToken.js +111 -0
  292. package/dist/security/authn/oidc/identityToken.js.map +1 -0
  293. package/dist/security/authn/oidc/jwks.d.ts +25 -0
  294. package/dist/security/authn/oidc/jwks.js +261 -0
  295. package/dist/security/authn/oidc/jwks.js.map +1 -0
  296. package/dist/security/authn/oidc/providers/generic.d.ts +13 -0
  297. package/dist/security/authn/oidc/providers/generic.js +34 -0
  298. package/dist/security/authn/oidc/providers/generic.js.map +1 -0
  299. package/dist/security/authn/oidc/providers/githubActions.d.ts +11 -0
  300. package/dist/security/authn/oidc/providers/githubActions.js +129 -0
  301. package/dist/security/authn/oidc/providers/githubActions.js.map +1 -0
  302. package/dist/security/authn/oidc/providers/index.d.ts +37 -0
  303. package/dist/security/authn/oidc/providers/index.js +24 -0
  304. package/dist/security/authn/oidc/providers/index.js.map +1 -0
  305. package/dist/security/authn/oidc/tokenExchange.d.ts +12 -0
  306. package/dist/security/authn/oidc/tokenExchange.js +306 -0
  307. package/dist/security/authn/oidc/tokenExchange.js.map +1 -0
  308. package/dist/security/authn/oidc/trustPolicyOperations.d.ts +49 -0
  309. package/dist/security/authn/oidc/trustPolicyOperations.js +358 -0
  310. package/dist/security/authn/oidc/trustPolicyOperations.js.map +1 -0
  311. package/dist/security/authn/oidc/types.d.ts +38 -0
  312. package/dist/security/authn/oidc/types.js +6 -0
  313. package/dist/security/authn/oidc/types.js.map +1 -0
  314. package/dist/security/certificateVerification/index.js +40 -11
  315. package/dist/security/certificateVerification/index.js.map +1 -1
  316. package/dist/security/certificateVerification/trustedIssuers.d.ts +24 -0
  317. package/dist/security/certificateVerification/trustedIssuers.js +79 -0
  318. package/dist/security/certificateVerification/trustedIssuers.js.map +1 -0
  319. package/dist/security/certificateVerification/types.d.ts +1 -0
  320. package/dist/security/credentialProvenance.d.ts +35 -0
  321. package/dist/security/credentialProvenance.js +51 -0
  322. package/dist/security/credentialProvenance.js.map +1 -0
  323. package/dist/security/credentialRejection.d.ts +4 -0
  324. package/dist/security/credentialRejection.js +24 -0
  325. package/dist/security/credentialRejection.js.map +1 -0
  326. package/dist/security/deferredAuthentication.d.ts +36 -0
  327. package/dist/security/deferredAuthentication.js +70 -0
  328. package/dist/security/deferredAuthentication.js.map +1 -0
  329. package/dist/security/impersonation.d.ts +21 -0
  330. package/dist/security/impersonation.js +108 -9
  331. package/dist/security/impersonation.js.map +1 -1
  332. package/dist/security/jsLoader.d.ts +6 -0
  333. package/dist/security/jsLoader.js +77 -15
  334. package/dist/security/jsLoader.js.map +1 -1
  335. package/dist/security/keys.js +301 -71
  336. package/dist/security/keys.js.map +1 -1
  337. package/dist/security/operationScope.d.ts +21 -0
  338. package/dist/security/operationScope.js +36 -0
  339. package/dist/security/operationScope.js.map +1 -0
  340. package/dist/security/permissionsTranslator.js +21 -0
  341. package/dist/security/permissionsTranslator.js.map +1 -1
  342. package/dist/security/tokenAuthentication.d.ts +19 -1
  343. package/dist/security/tokenAuthentication.js +191 -10
  344. package/dist/security/tokenAuthentication.js.map +1 -1
  345. package/dist/security/user.js +4 -3
  346. package/dist/security/user.js.map +1 -1
  347. package/dist/server/DurableSubscriptionsSession.d.ts +2 -2
  348. package/dist/server/DurableSubscriptionsSession.js +67 -10
  349. package/dist/server/DurableSubscriptionsSession.js.map +1 -1
  350. package/dist/server/REST.js +106 -2
  351. package/dist/server/REST.js.map +1 -1
  352. package/dist/server/graphqlQuerying.js +4 -0
  353. package/dist/server/graphqlQuerying.js.map +1 -1
  354. package/dist/server/http.d.ts +16 -1
  355. package/dist/server/http.js +122 -17
  356. package/dist/server/http.js.map +1 -1
  357. package/dist/server/itc/serverHandlers.js +8 -1
  358. package/dist/server/itc/serverHandlers.js.map +1 -1
  359. package/dist/server/jobs/jobProcess.js +6 -2
  360. package/dist/server/jobs/jobProcess.js.map +1 -1
  361. package/dist/server/jobs/jobs.js +4 -1
  362. package/dist/server/jobs/jobs.js.map +1 -1
  363. package/dist/server/liveSubscriptionAuth.d.ts +26 -4
  364. package/dist/server/liveSubscriptionAuth.js +105 -39
  365. package/dist/server/liveSubscriptionAuth.js.map +1 -1
  366. package/dist/server/loadRootComponents.js +49 -10
  367. package/dist/server/loadRootComponents.js.map +1 -1
  368. package/dist/server/mqtt.d.ts +2 -0
  369. package/dist/server/mqtt.js +165 -30
  370. package/dist/server/mqtt.js.map +1 -1
  371. package/dist/server/nodeName.d.ts +2 -0
  372. package/dist/server/nodeName.js +107 -23
  373. package/dist/server/nodeName.js.map +1 -1
  374. package/dist/server/serverHelpers/Headers.d.ts +27 -0
  375. package/dist/server/serverHelpers/Headers.js +145 -1
  376. package/dist/server/serverHelpers/Headers.js.map +1 -1
  377. package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
  378. package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
  379. package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
  380. package/dist/server/serverHelpers/Request.d.ts +5 -10
  381. package/dist/server/serverHelpers/Request.js +38 -136
  382. package/dist/server/serverHelpers/Request.js.map +1 -1
  383. package/dist/server/serverHelpers/contentTypes.d.ts +11 -0
  384. package/dist/server/serverHelpers/contentTypes.js +221 -40
  385. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  386. package/dist/server/serverHelpers/deployValidationState.d.ts +3 -0
  387. package/dist/server/serverHelpers/deployValidationState.js +9 -19
  388. package/dist/server/serverHelpers/deployValidationState.js.map +1 -1
  389. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +12 -0
  390. package/dist/server/serverHelpers/operationAuthorizationState.js +24 -2
  391. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -1
  392. package/dist/server/serverHelpers/registeredOperations.d.ts +5 -4
  393. package/dist/server/serverHelpers/registeredOperations.js +74 -21
  394. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  395. package/dist/server/serverHelpers/requestSanitization.d.ts +11 -0
  396. package/dist/server/serverHelpers/requestSanitization.js +20 -0
  397. package/dist/server/serverHelpers/requestSanitization.js.map +1 -0
  398. package/dist/server/serverHelpers/serverHandlers.js +6 -3
  399. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  400. package/dist/server/serverHelpers/serverUtilities.d.ts +18 -0
  401. package/dist/server/serverHelpers/serverUtilities.js +178 -29
  402. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  403. package/dist/server/serverHelpers/sharedMessageEncoding.d.ts +67 -0
  404. package/dist/server/serverHelpers/sharedMessageEncoding.js +280 -0
  405. package/dist/server/serverHelpers/sharedMessageEncoding.js.map +1 -0
  406. package/dist/server/serverHelpers/uwsServer.js +19 -1
  407. package/dist/server/serverHelpers/uwsServer.js.map +1 -1
  408. package/dist/server/static.js +24 -28
  409. package/dist/server/static.js.map +1 -1
  410. package/dist/server/storageReclamation.d.ts +5 -0
  411. package/dist/server/storageReclamation.js +17 -1
  412. package/dist/server/storageReclamation.js.map +1 -1
  413. package/dist/server/threads/isolatedApplications.d.ts +47 -0
  414. package/dist/server/threads/isolatedApplications.js +171 -0
  415. package/dist/server/threads/isolatedApplications.js.map +1 -0
  416. package/dist/server/threads/itc.d.ts +7 -2
  417. package/dist/server/threads/itc.js +5 -1
  418. package/dist/server/threads/itc.js.map +1 -1
  419. package/dist/server/threads/logRotationTransport.d.ts +1 -0
  420. package/dist/server/threads/logRotationTransport.js +33 -0
  421. package/dist/server/threads/logRotationTransport.js.map +1 -0
  422. package/dist/server/threads/manageThreads.d.ts +83 -3
  423. package/dist/server/threads/manageThreads.js +769 -75
  424. package/dist/server/threads/manageThreads.js.map +1 -1
  425. package/dist/server/threads/socketRouter.d.ts +1 -0
  426. package/dist/server/threads/socketRouter.js +277 -31
  427. package/dist/server/threads/socketRouter.js.map +1 -1
  428. package/dist/server/threads/threadHeapMemory.d.ts +2 -0
  429. package/dist/server/threads/threadHeapMemory.js +31 -0
  430. package/dist/server/threads/threadHeapMemory.js.map +1 -0
  431. package/dist/server/threads/threadServer.js +76 -22
  432. package/dist/server/threads/threadServer.js.map +1 -1
  433. package/dist/sqlEngine/config.d.ts +1 -3
  434. package/dist/sqlEngine/config.js +19 -16
  435. package/dist/sqlEngine/config.js.map +1 -1
  436. package/dist/sqlTranslator/index.d.ts +1 -1
  437. package/dist/sqlTranslator/index.js +30 -7
  438. package/dist/sqlTranslator/index.js.map +1 -1
  439. package/dist/upgrade/directives/5-3-0.d.ts +7 -0
  440. package/dist/upgrade/directives/5-3-0.js +148 -0
  441. package/dist/upgrade/directives/5-3-0.js.map +1 -0
  442. package/dist/upgrade/directives/directivesController.js +2 -1
  443. package/dist/upgrade/directives/directivesController.js.map +1 -1
  444. package/dist/utility/OperationFunctionCaller.js +2 -1
  445. package/dist/utility/OperationFunctionCaller.js.map +1 -1
  446. package/dist/utility/common_utils.d.ts +16 -0
  447. package/dist/utility/common_utils.js +32 -6
  448. package/dist/utility/common_utils.js.map +1 -1
  449. package/dist/utility/componentNames.d.ts +8 -0
  450. package/dist/utility/componentNames.js +12 -1
  451. package/dist/utility/componentNames.js.map +1 -1
  452. package/dist/utility/environment/environmentManager.js +3 -6
  453. package/dist/utility/environment/environmentManager.js.map +1 -1
  454. package/dist/utility/environment/systemInformation.d.ts +1 -0
  455. package/dist/utility/environment/systemInformation.js +1 -0
  456. package/dist/utility/environment/systemInformation.js.map +1 -1
  457. package/dist/utility/errors/commonErrors.d.ts +2 -0
  458. package/dist/utility/errors/commonErrors.js +2 -0
  459. package/dist/utility/errors/commonErrors.js.map +1 -1
  460. package/dist/utility/errors/hdbError.d.ts +33 -0
  461. package/dist/utility/errors/hdbError.js +58 -1
  462. package/dist/utility/errors/hdbError.js.map +1 -1
  463. package/dist/utility/globalSchema.d.ts +18 -0
  464. package/dist/utility/hdbTerms.d.ts +18 -0
  465. package/dist/utility/hdbTerms.js +20 -2
  466. package/dist/utility/hdbTerms.js.map +1 -1
  467. package/dist/utility/logging/harper_logger.d.ts +2 -0
  468. package/dist/utility/logging/harper_logger.js +286 -30
  469. package/dist/utility/logging/harper_logger.js.map +1 -1
  470. package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
  471. package/dist/utility/logging/logGenerationCoordinator.js +184 -0
  472. package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
  473. package/dist/utility/logging/logRotation.d.ts +46 -0
  474. package/dist/utility/logging/logRotation.js +365 -0
  475. package/dist/utility/logging/logRotation.js.map +1 -0
  476. package/dist/utility/logging/logRotator.d.ts +1 -1
  477. package/dist/utility/logging/logRotator.js +192 -85
  478. package/dist/utility/logging/logRotator.js.map +1 -1
  479. package/dist/utility/nodeIdentity.d.ts +9 -0
  480. package/dist/utility/nodeIdentity.js +58 -0
  481. package/dist/utility/nodeIdentity.js.map +1 -0
  482. package/dist/utility/npmUtilities.js +11 -7
  483. package/dist/utility/npmUtilities.js.map +1 -1
  484. package/dist/utility/operationPermissions.d.ts +3 -1
  485. package/dist/utility/operationPermissions.js +16 -1
  486. package/dist/utility/operationPermissions.js.map +1 -1
  487. package/dist/utility/operation_authorization.d.ts +10 -7
  488. package/dist/utility/operation_authorization.js +212 -41
  489. package/dist/utility/operation_authorization.js.map +1 -1
  490. package/dist/utility/watchPath.d.ts +29 -0
  491. package/dist/utility/watchPath.js +68 -0
  492. package/dist/utility/watchPath.js.map +1 -0
  493. package/dist/utility/watcherFallback.d.ts +41 -0
  494. package/dist/utility/watcherFallback.js +153 -0
  495. package/dist/utility/watcherFallback.js.map +1 -1
  496. package/dist/validation/configValidator.d.ts +12 -0
  497. package/dist/validation/configValidator.js +205 -75
  498. package/dist/validation/configValidator.js.map +1 -1
  499. package/dist/validation/installValidator.js +12 -0
  500. package/dist/validation/installValidator.js.map +1 -1
  501. package/dist/validation/validationWrapper.d.ts +11 -0
  502. package/dist/validation/validationWrapper.js +16 -3
  503. package/dist/validation/validationWrapper.js.map +1 -1
  504. package/index.ts +8 -0
  505. package/json/systemSchema.json +55 -0
  506. package/npm-shrinkwrap.json +286 -194
  507. package/package.json +15 -7
  508. package/resources/DESIGN.md +219 -52
  509. package/resources/DatabaseTransaction.ts +657 -52
  510. package/resources/LMDBTransaction.ts +46 -6
  511. package/resources/PrimaryRocksDatabase.ts +46 -7
  512. package/resources/RecordEncoder.ts +124 -9
  513. package/resources/RequestTarget.ts +2 -0
  514. package/resources/Resource.ts +114 -22
  515. package/resources/ResourceInterface.ts +31 -0
  516. package/resources/RocksIndexStore.ts +30 -9
  517. package/resources/RocksTransactionLogStore.ts +188 -51
  518. package/resources/Table.ts +1960 -301
  519. package/resources/analytics/write.ts +10 -3
  520. package/resources/auditStore.ts +647 -32
  521. package/resources/blob.ts +1029 -110
  522. package/resources/branchDatabase.ts +941 -0
  523. package/resources/crdt.ts +76 -12
  524. package/resources/dataLoader.ts +3 -4
  525. package/resources/databases.ts +1584 -320
  526. package/resources/defineTable.ts +18 -2
  527. package/resources/derivedIndexRegistry.ts +56 -0
  528. package/resources/derivedIndexRuntime.ts +2292 -0
  529. package/resources/graphql.ts +72 -17
  530. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +911 -48
  531. package/resources/indexes/hnswDerivedIndex.ts +531 -0
  532. package/resources/indexes/hnswPlaneBinding.ts +174 -0
  533. package/resources/longLivedTransactions.ts +360 -0
  534. package/resources/models/backendRegistry.ts +84 -2
  535. package/resources/models/bootstrap.ts +473 -28
  536. package/resources/nodeIdMapping.ts +50 -0
  537. package/resources/recordLock.ts +419 -0
  538. package/resources/recordLockCoordinator.ts +3043 -0
  539. package/resources/replayLogs.ts +152 -26
  540. package/resources/replayLogsGuards.ts +171 -8
  541. package/resources/replicatedApplyFailure.ts +77 -0
  542. package/resources/scheduler/scheduler.ts +4 -4
  543. package/resources/search.ts +288 -46
  544. package/resources/tracked.ts +73 -22
  545. package/resources/transactionBroadcast.ts +3 -3
  546. package/security/auth.ts +68 -29
  547. package/security/authn/oidc/claims.ts +72 -0
  548. package/security/authn/oidc/identityToken.ts +129 -0
  549. package/security/authn/oidc/jwks.ts +260 -0
  550. package/security/authn/oidc/providers/generic.ts +40 -0
  551. package/security/authn/oidc/providers/githubActions.ts +137 -0
  552. package/security/authn/oidc/providers/index.ts +52 -0
  553. package/security/authn/oidc/tokenExchange.ts +300 -0
  554. package/security/authn/oidc/trustPolicyOperations.ts +343 -0
  555. package/security/authn/oidc/types.ts +41 -0
  556. package/security/certificateVerification/index.ts +54 -13
  557. package/security/certificateVerification/trustedIssuers.ts +76 -0
  558. package/security/certificateVerification/types.ts +1 -0
  559. package/security/credentialProvenance.ts +47 -0
  560. package/security/credentialRejection.ts +22 -0
  561. package/security/deferredAuthentication.ts +71 -0
  562. package/security/impersonation.ts +117 -12
  563. package/security/jsLoader.ts +83 -18
  564. package/security/keys.ts +298 -72
  565. package/security/operationScope.ts +33 -0
  566. package/security/permissionsTranslator.js +23 -0
  567. package/security/tokenAuthentication.ts +233 -12
  568. package/security/user.ts +4 -3
  569. package/server/DESIGN.md +194 -16
  570. package/server/DurableSubscriptionsSession.ts +71 -11
  571. package/server/REST.ts +115 -4
  572. package/server/graphqlQuerying.ts +4 -0
  573. package/server/http.ts +133 -20
  574. package/server/itc/serverHandlers.js +8 -1
  575. package/server/jobs/jobProcess.ts +8 -2
  576. package/server/jobs/jobs.ts +4 -1
  577. package/server/liveSubscriptionAuth.ts +129 -46
  578. package/server/loadRootComponents.js +50 -8
  579. package/server/mqtt.ts +179 -38
  580. package/server/nodeName.ts +103 -21
  581. package/server/serverHelpers/Headers.ts +135 -0
  582. package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
  583. package/server/serverHelpers/Request.ts +33 -131
  584. package/server/serverHelpers/contentTypes.ts +217 -36
  585. package/server/serverHelpers/deployValidationState.ts +24 -13
  586. package/server/serverHelpers/operationAuthorizationState.ts +34 -3
  587. package/server/serverHelpers/registeredOperations.ts +79 -22
  588. package/server/serverHelpers/requestSanitization.ts +15 -0
  589. package/server/serverHelpers/serverHandlers.js +6 -3
  590. package/server/serverHelpers/serverUtilities.ts +232 -40
  591. package/server/serverHelpers/sharedMessageEncoding.ts +307 -0
  592. package/server/serverHelpers/uwsServer.ts +17 -2
  593. package/server/static.ts +23 -29
  594. package/server/storageReclamation.ts +15 -2
  595. package/server/threads/isolatedApplications.ts +157 -0
  596. package/server/threads/itc.js +11 -1
  597. package/server/threads/logRotationTransport.ts +40 -0
  598. package/server/threads/manageThreads.js +771 -66
  599. package/server/threads/socketRouter.ts +291 -30
  600. package/server/threads/threadHeapMemory.ts +26 -0
  601. package/server/threads/threadServer.js +73 -22
  602. package/sqlTranslator/index.ts +31 -8
  603. package/studio/web/assets/{Chat-4RrB5134.js → Chat-D3j-1yY1.js} +1 -1
  604. package/studio/web/assets/{FloatingChat-omlNMDcJ.js → FloatingChat-BxJGYcfB.js} +3 -3
  605. package/studio/web/assets/{apiToken-Bke3wvfZ.js → apiToken-CT55oWOe.js} +1 -1
  606. package/studio/web/assets/{applications-DvFDYJgK.js → applications-D9Ct9_vm.js} +1 -1
  607. package/studio/web/assets/{cssMode-C1vRa7zh.js → cssMode-DV8H7VwA.js} +1 -1
  608. package/studio/web/assets/{editor-3XRWEDWX.js → editor-uatc0unt.js} +1 -1
  609. package/studio/web/assets/{html-CTY5tdMr.js → html-Bm6D6paN.js} +1 -1
  610. package/studio/web/assets/{htmlMode-CG1vSD9t.js → htmlMode-CEn7tpLG.js} +1 -1
  611. package/studio/web/assets/{index-Dfpeofdu.js → index-BIXW6Pu4.js} +5 -5
  612. package/studio/web/assets/{index.lazy-B1VOIv-t.js → index.lazy-UI7L-Vrk.js} +1 -1
  613. package/studio/web/assets/{javascript-CJeJzGnI.js → javascript-CJ0G3AFZ.js} +1 -1
  614. package/studio/web/assets/{jsonMode-CYPBwM82.js → jsonMode-DQADAYEa.js} +1 -1
  615. package/studio/web/assets/{languageServices-IKH4GUHl.js → languageServices-CAQJXWcI.js} +1 -1
  616. package/studio/web/assets/{lspLanguageFeatures-BC8_gxKG.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
  617. package/studio/web/assets/{notifications-BfKBpYcq.js → notifications-BbxTU6Aw.js} +1 -1
  618. package/studio/web/assets/{notifications-DFbArTfC.js → notifications-Cvb3P1lB.js} +1 -1
  619. package/studio/web/assets/{profile-Q4-T6c-S.js → profile-Yyb7gsvL.js} +1 -1
  620. package/studio/web/assets/{regions-CtkV0xje.js → regions-OgjGHlU5.js} +1 -1
  621. package/studio/web/assets/{register-1ZZuMsiA.js → register-6qwNEOY3.js} +2 -2
  622. package/studio/web/assets/{setComponentFile-ZWLcWv5X.js → setComponentFile-BilDMtgB.js} +1 -1
  623. package/studio/web/assets/{setup-DWprJyJy.js → setup-J6qJ7OIU.js} +2 -2
  624. package/studio/web/assets/{status--aNm8isn.js → status-0RWGcfyD.js} +1 -1
  625. package/studio/web/assets/{toggleHighContrast-Cq_lt3XD.js → toggleHighContrast-BIn-vErT.js} +1 -1
  626. package/studio/web/assets/{tsMode-pjgytARx.js → tsMode-DgUXku4d.js} +1 -1
  627. package/studio/web/assets/{typescript-Cdg0mqUh.js → typescript-C9orXcsM.js} +1 -1
  628. package/studio/web/assets/{useEntityRestURL-RZhaY8Rn.js → useEntityRestURL-BEoXXbUB.js} +1 -1
  629. package/studio/web/assets/{workers-BgoXIqQe.js → workers-JVzSDmgx.js} +1 -1
  630. package/studio/web/assets/{xml-BwSeDMiP.js → xml-Cq-S8S4X.js} +1 -1
  631. package/studio/web/assets/{yaml-DotCUG5l.js → yaml-sfoRdh1M.js} +1 -1
  632. package/studio/web/index.html +1 -1
  633. package/upgrade/directives/5-3-0.ts +132 -0
  634. package/upgrade/directives/directivesController.ts +2 -1
  635. package/utility/OperationFunctionCaller.ts +2 -1
  636. package/utility/common_utils.ts +30 -5
  637. package/utility/componentNames.ts +12 -0
  638. package/utility/environment/environmentManager.ts +3 -7
  639. package/utility/environment/systemInformation.ts +7 -0
  640. package/utility/errors/commonErrors.ts +4 -0
  641. package/utility/errors/hdbError.ts +57 -0
  642. package/utility/hdbTerms.ts +19 -0
  643. package/utility/logging/harper_logger.ts +278 -26
  644. package/utility/logging/logGenerationCoordinator.ts +196 -0
  645. package/utility/logging/logRotation.ts +367 -0
  646. package/utility/logging/logRotator.ts +213 -81
  647. package/utility/nodeIdentity.ts +45 -0
  648. package/utility/npmUtilities.ts +12 -8
  649. package/utility/operationPermissions.ts +18 -1
  650. package/utility/operation_authorization.ts +231 -42
  651. package/utility/watchPath.ts +63 -0
  652. package/utility/watcherFallback.ts +148 -0
  653. package/validation/configValidator.ts +215 -75
  654. package/validation/installValidator.ts +15 -0
  655. package/validation/validationWrapper.ts +18 -4
@@ -12,23 +12,31 @@
12
12
  * - Note that for compressed data, the size is the uncompressed size, and the compressed size in the file
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.databasePaths = exports.BLOB_UNAVAILABLE_STATUS = exports.blobsWereEncoded = exports.Blob = void 0;
15
+ exports.databasePaths = exports.BLOB_UNAVAILABLE_STATUS = exports.blobsWereEncoded = exports.Blob = exports.DEFLATE_TYPE = void 0;
16
16
  exports.registerBlobReceiveInFlight = registerBlobReceiveInFlight;
17
17
  exports.unregisterBlobReceiveInFlight = unregisterBlobReceiveInFlight;
18
18
  exports.isBlobReceiveInFlight = isBlobReceiveInFlight;
19
19
  exports.createPendingMarkerBarrier = createPendingMarkerBarrier;
20
+ exports.watchInProgressFile = watchInProgressFile;
20
21
  exports.getBlobHoldStateForTesting = getBlobHoldStateForTesting;
21
22
  exports.holdBlobFile = holdBlobFile;
22
23
  exports.deleteBlob = deleteBlob;
23
24
  exports.setDeletionDelay = setDeletionDelay;
25
+ exports.resolveBlobCompression = resolveBlobCompression;
24
26
  exports.createBlob = createBlob;
27
+ exports.createBlobFromStoredBody = createBlobFromStoredBody;
25
28
  exports.saveBlob = saveBlob;
26
29
  exports.shouldDestroyIdleBlobSource = shouldDestroyIdleBlobSource;
30
+ exports.createStoredDeflateVerifier = createStoredDeflateVerifier;
27
31
  exports.getFileId = getFileId;
28
32
  exports.isSaving = isSaving;
29
33
  exports.getFilePathForBlob = getFilePathForBlob;
30
34
  exports.repairBlobFile = repairBlobFile;
35
+ exports.isSystemicIoError = isSystemicIoError;
36
+ exports.classifyBlobFileForCapture = classifyBlobFileForCapture;
37
+ exports.createCaptureMarker = createCaptureMarker;
31
38
  exports.blobHeaderIndicatesIncomplete = blobHeaderIndicatesIncomplete;
39
+ exports.blobFileMissingOrIncompleteAsync = blobFileMissingOrIncompleteAsync;
32
40
  exports.blobFileMissingOrIncomplete = blobFileMissingOrIncomplete;
33
41
  exports.getRootBlobPathsForDB = getRootBlobPathsForDB;
34
42
  exports.getBlobPathsForDatabaseName = getBlobPathsForDatabaseName;
@@ -47,7 +55,9 @@ exports.startPreCommitBlobsForRecord = startPreCommitBlobsForRecord;
47
55
  exports.cleanupUnusedBlobs = cleanupUnusedBlobs;
48
56
  exports.collectRetainedFileIds = collectRetainedFileIds;
49
57
  exports.cleanupOrphans = cleanupOrphans;
58
+ exports.inflatesToExactly = inflatesToExactly;
50
59
  exports.isBlobComplete = isBlobComplete;
60
+ exports.openStoredBlobBody = openStoredBlobBody;
51
61
  exports.findIncompleteBlobRefs = findIncompleteBlobRefs;
52
62
  const msgpackr_1 = require("msgpackr");
53
63
  const promises_1 = require("node:fs/promises");
@@ -59,25 +69,31 @@ const environmentManager_ts_1 = require("../utility/environment/environmentManag
59
69
  const hdbTerms_ts_1 = require("../utility/hdbTerms.js");
60
70
  const path_1 = require("path");
61
71
  const logger_ts_1 = require("../utility/logging/logger.js");
72
+ const watchPath_ts_1 = require("../utility/watchPath.js");
62
73
  const contentTypes_ts_1 = require("../server/serverHelpers/contentTypes.js");
63
74
  const node_v8_1 = require("node:v8");
64
75
  const promises_2 = require("node:timers/promises");
65
76
  const globals_js_1 = require("../globals.js");
77
+ function discardStorage(storageInfo) {
78
+ (storageInfo.fileState ??= {}).discarded = true;
79
+ }
66
80
  const FILE_STORAGE_THRESHOLD = 8192; // if the file is below this size, we will store it in memory, or within the record itself, otherwise we will store it in a file
67
81
  // We want to keep the file path private (but accessible to the extension)
68
82
  const HEADER_SIZE = 8;
69
83
  const UNCOMPRESSED_TYPE = 0;
70
- const DEFLATE_TYPE = 1;
84
+ exports.DEFLATE_TYPE = 1;
71
85
  const ERROR_TYPE = 0xff;
72
86
  // A write that aborted on a re-streamable external source (replication receive / origin fetch) stamps the
73
87
  // file with this type so a downstream read returns 503 (retry) rather than 500 (confidently incomplete).
74
- // The bytes are still expected — the receive side holds a blob gap and re-streams on reconnect, which
75
- // overwrites this stub; a terminal give-up unlinks the file (→ 404). Distinct from ERROR_TYPE (a permanent
76
- // corrupt/error stub, replicated as-is). See harper-pro#481.
88
+ // The re-stream builds a fresh blob and takes a NEW file id (harper-pro's `createBlob` → `saveBlob`), so
89
+ // this stub is orphaned rather than overwritten; a terminal give-up unlinks it (→ 404). No path rewrites
90
+ // a published blob file in place: `repairBlobFile` is the only same-id writer and it publishes via a
91
+ // `.repair` temp + rename. Distinct from ERROR_TYPE (a permanent corrupt/error stub, replicated as-is).
92
+ // See harper-pro#481.
77
93
  const PENDING_TYPE = 0xfe;
78
94
  const BLOB_REPAIR_SUFFIX = '.repair';
79
95
  const DEFAULT_HEADER = new Uint8Array([0, UNCOMPRESSED_TYPE, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
80
- const COMPRESS_HEADER = new Uint8Array([0, DEFLATE_TYPE, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
96
+ const COMPRESS_HEADER = new Uint8Array([0, exports.DEFLATE_TYPE, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
81
97
  const UNKNOWN_SIZE = 0xffffffffffff;
82
98
  const storageInfoForBlob = new WeakMap();
83
99
  // Cross-thread receive-in-flight signal. The lock lives on the shared store (same primitive
@@ -215,6 +231,50 @@ const BLOB_GONE_STATUS = 404;
215
231
  // retryable class from the permanent ones without duplicating the status code.
216
232
  exports.BLOB_UNAVAILABLE_STATUS = 503;
217
233
  const BLOB_CORRUPT_STATUS = 500;
234
+ /**
235
+ * Arm the one-shot watch that wakes an in-progress blob read of `filePath`, or return `undefined`
236
+ * for a read that has to poll instead. `isLive` decides whether the read still owns a callback's
237
+ * watcher: a callback from one it has already replaced must not act, or it would close the live
238
+ * watcher and start a second read at the same position. `onFailure` runs only for a live watcher
239
+ * that fails after registration — a registration that throws latches `mustPoll` and leaves the
240
+ * caller in its own no-watcher branch.
241
+ */
242
+ function watchInProgressFile(filePath, watchTarget, handlers, watchFile = node_fs_1.watch) {
243
+ if (watchTarget.mustPoll)
244
+ return undefined;
245
+ let watcher;
246
+ try {
247
+ watcher = watchFile(watchTarget.path, { persistent: false }, () => {
248
+ if (handlers.isLive(watcher))
249
+ handlers.onChange();
250
+ });
251
+ }
252
+ catch (error) {
253
+ // fs.watch throws synchronously when the OS watcher pool is exhausted (EMFILE/ENOSPC). Latch
254
+ // on the stream-scoped target, or the caller's re-poll re-enters here and re-attempts the same
255
+ // failing registration for the rest of the read.
256
+ logger_ts_1.logger.debug?.(`Could not watch ${filePath} for in-progress writes, polling instead:`, error);
257
+ watchTarget.mustPoll = true;
258
+ return undefined;
259
+ }
260
+ // An FSWatcher that fails after registration emits 'error'; with no listener Node rethrows it out
261
+ // of the watcher callback.
262
+ watcher.on('error', (error) => {
263
+ if (!handlers.isLive(watcher))
264
+ return;
265
+ logger_ts_1.logger.debug?.(`Watch of ${filePath} failed, polling instead:`, error);
266
+ watchTarget.mustPoll = true;
267
+ try {
268
+ watcher.close();
269
+ }
270
+ catch {
271
+ // A close() that throws here must not skip onFailure — the poll fallback is what recovers
272
+ // the read, and the exception would otherwise escape this listener uncaught.
273
+ }
274
+ handlers.onFailure();
275
+ });
276
+ return watcher;
277
+ }
218
278
  class BlobReadError extends Error {
219
279
  statusCode;
220
280
  code;
@@ -231,6 +291,41 @@ class BlobReadError extends Error {
231
291
  this.code = 'ENOENT';
232
292
  }
233
293
  }
294
+ /**
295
+ * Wait until no writer holds the blob's write lock, bounded by the blob read timeout. This is the
296
+ * completeness authority for a compressed body: its header stores the uncompressed size, so unlike
297
+ * an uncompressed blob its length on disk proves nothing, and inflating a partial deflate stream
298
+ * errors. Resolving means the writer (if any) has finished — not that this caller holds the lock.
299
+ */
300
+ function waitForBlobWriteCompletion(storageInfo) {
301
+ const store = storageInfo.store;
302
+ const lockKey = storageInfo.fileId + ':blob';
303
+ return new Promise((resolve, reject) => {
304
+ // Probe first: the writer lock is free on the overwhelming majority of reads, so arm the
305
+ // timeout only when a writer actually holds it — an uncontended compressed read pays a
306
+ // tryLock, not a setTimeout.
307
+ let settled = false;
308
+ let timer;
309
+ function onReleased() {
310
+ if (settled)
311
+ return;
312
+ settled = true;
313
+ clearTimeout(timer);
314
+ resolve();
315
+ }
316
+ if (store.tryLock(lockKey, onReleased)) {
317
+ store.unlock(lockKey);
318
+ return resolve();
319
+ }
320
+ timer = setTimeout(() => {
321
+ if (settled)
322
+ return;
323
+ settled = true;
324
+ reject(new BlobReadError(`Blob ${storageInfo.fileId} is unavailable; the in-progress write did not complete in time`, exports.BLOB_UNAVAILABLE_STATUS));
325
+ }, getBlobReadTimeout());
326
+ timer.unref();
327
+ });
328
+ }
234
329
  // We want FileBackedBlob instances to be an instanceof Blob, but we don't want to actually extend the class and call Blob's constructor, which is quite expensive because it has to set it up as a transferrable.
235
330
  function InstanceOfBlobWithNoConstructor() { }
236
331
  InstanceOfBlobWithNoConstructor.prototype = exports.Blob.prototype;
@@ -329,8 +424,6 @@ class FileBackedBlob extends exports.Blob {
329
424
  throw new BlobReadError('Blob pending replication for ' + filePath, exports.BLOB_UNAVAILABLE_STATUS);
330
425
  }
331
426
  size = Number(headerValue & 0xffffffffffffn);
332
- if (size < end)
333
- size = end;
334
427
  if (size < UNKNOWN_SIZE) {
335
428
  if (isFullRead && descriptorSize != null && descriptorSize < UNKNOWN_SIZE && size !== descriptorSize) {
336
429
  // the header claims a different (uncompressed) size than the record descriptor: the file
@@ -449,7 +542,7 @@ class FileBackedBlob extends exports.Blob {
449
542
  // Only sniff the storage-type byte and take the decompress branch when a full header is present.
450
543
  // A file shorter than the header (truncated/corrupted, #1424) would otherwise be mis-read as a
451
544
  // deflate body and yield garbage; instead it falls through to checkCompletion as incomplete.
452
- if (rawBytes.length >= HEADER_SIZE && rawBytes[1] === DEFLATE_TYPE) {
545
+ if (rawBytes.length >= HEADER_SIZE && rawBytes[1] === exports.DEFLATE_TYPE) {
453
546
  // An in-flight compressed file still carrying the UNKNOWN_SIZE placeholder is definitely
454
547
  // incomplete; otherwise the size is finalized but the compressed body may still be streaming,
455
548
  // so verify via the write lock before inflating (inflating a partial deflate stream errors).
@@ -457,10 +550,22 @@ class FileBackedBlob extends exports.Blob {
457
550
  if (pending)
458
551
  return pending;
459
552
  return new Promise((resolve, reject) => {
460
- // start/end index into the UNCOMPRESSED content, so inflate first, then slice
461
- (0, node_zlib_1.inflate)(rawBytes.subarray(HEADER_SIZE), (error, result) => {
462
- if (error)
463
- reject(error);
553
+ // start/end index into the UNCOMPRESSED content, so inflate first, then slice — with
554
+ // output capped at the header's declared size, so a body that inflates past its own
555
+ // header (a lying or corrupted header) is refused instead of allocating without bound,
556
+ // matching the streaming read path's ceiling. zlib rejects a cap of 0, so an empty
557
+ // blob's cap is 1 and the exact-length check below covers it.
558
+ (0, node_zlib_1.inflate)(rawBytes.subarray(HEADER_SIZE), { maxOutputLength: Math.max(size, 1) }, (error, result) => {
559
+ if (error) {
560
+ const code = error.code ?? '';
561
+ reject(code === 'ERR_BUFFER_TOO_LARGE'
562
+ ? new BlobReadError(`Blob ${filePath} inflates past its declared size of ${size}`, BLOB_CORRUPT_STATUS)
563
+ : code.startsWith('Z_')
564
+ ? new BlobReadError(`Blob ${filePath} is not a valid deflate stream: ${error.message}`, BLOB_CORRUPT_STATUS)
565
+ : error);
566
+ }
567
+ else if (result.length !== size)
568
+ reject(new BlobReadError(`Blob ${filePath} inflates to ${result.length} bytes, not its declared size of ${size}`, BLOB_CORRUPT_STATUS));
464
569
  else
465
570
  resolve(sliceContent(result));
466
571
  });
@@ -507,6 +612,20 @@ class FileBackedBlob extends exports.Blob {
507
612
  let position = 0;
508
613
  let totalContentRead = 0;
509
614
  let watcher;
615
+ // Drop the live watcher before closing it, so a callback racing the close cannot pass its
616
+ // `isLive` check — and so a close() on an already-failed handle cannot abandon the teardown
617
+ // it was part of. Every site that retires this watcher goes through here.
618
+ const closeWatcher = () => {
619
+ const opened = watcher;
620
+ watcher = null;
621
+ try {
622
+ opened?.close();
623
+ }
624
+ catch (error) {
625
+ logger_ts_1.logger.debug?.(`Could not close the in-progress watch of ${filePath}:`, error);
626
+ }
627
+ };
628
+ let watchTarget;
510
629
  let timer;
511
630
  // The start() open-retry timer lives in a different scope/phase than pull()'s `timer`; track it
512
631
  // separately so a cancel() during the file-creation wait clears it instead of leaking an fd (#1457).
@@ -535,6 +654,174 @@ class FileBackedBlob extends exports.Blob {
535
654
  // full reads can be compared against the header.
536
655
  const descriptorSize = this.size;
537
656
  const isFullRead = start === undefined && end === undefined;
657
+ let compressedRead;
658
+ let teardownCompressedRead;
659
+ // `bodyPrefix` is what the first raw read already pulled past the header; `bodyPosition` is where
660
+ // the file read continues. Both stay valid across the writer wait because a body is only ever
661
+ // appended to, and the stamps that do overwrite it (PENDING/ERROR) change the header re-read below.
662
+ async function* readCompressedBlob(bodyPrefix, bodyPosition) {
663
+ // The writer's lock is the only completeness authority for a compressed body (the header
664
+ // stores the uncompressed size, so body length proves nothing; a partial deflate stream
665
+ // errors on inflate). Wait for it — bounded, like every other blob read wait (#1423).
666
+ await waitForBlobWriteCompletion(storageInfo);
667
+ if (cancelled || fd == null)
668
+ return;
669
+ // An in-place repair renames a fresh (uncompressed) file over the path while holding the
670
+ // writer lock, so the descriptor we opened before the wait can be left on the orphaned inode.
671
+ // The repair may finish either during our wait or in the gap between the first read and the
672
+ // lock probe, so detect it by file identity rather than by whether we waited: if the path now
673
+ // resolves to a different inode than our descriptor, the file was swapped. Reopen so the header
674
+ // re-read and the stream below see the current file — a repaired (uncompressed) header is then
675
+ // reported retryable 503 (mirroring openStoredBlobBody), and the prefix read from the old inode
676
+ // is discarded. If the inode is unchanged the descriptor is still valid and the same-fd re-read
677
+ // still catches an in-place PENDING/ERROR stamp (harper-pro#481).
678
+ let fileSwapped = false;
679
+ try {
680
+ const currentFd = fd;
681
+ const [openStat, pathStat] = await Promise.all([
682
+ new Promise((res, rej) => (0, node_fs_1.fstat)(currentFd, (error, s) => (error ? rej(error) : res(s)))),
683
+ new Promise((res, rej) => (0, node_fs_1.stat)(filePath, (error, s) => (error ? rej(error) : res(s)))),
684
+ ]);
685
+ fileSwapped = openStat.ino !== pathStat.ino;
686
+ }
687
+ catch {
688
+ // the path is momentarily unstatable, or the descriptor was closed by a concurrent cancel;
689
+ // fall through — the cancellation recheck below returns, and otherwise the header re-read
690
+ // reports the outcome. The blob hold makes a vanished path unlikely here.
691
+ }
692
+ if (cancelled || fd == null)
693
+ return; // the identity stat awaited; a cancel may have landed
694
+ if (fileSwapped) {
695
+ // Null `fd` before closing it: a closed descriptor number left in `fd` across the await
696
+ // below would be closed a second time by a concurrent cancel()/closeFd() — after the OS may
697
+ // have reassigned it to an unrelated file or socket (#1457).
698
+ const staleFd = fd;
699
+ fd = null;
700
+ (0, node_fs_1.close)(staleFd);
701
+ fd = await new Promise((resolveOpen, rejectOpen) => (0, node_fs_1.open)(filePath, 'r', (error, openedFd) => (error ? rejectOpen(error) : resolveOpen(openedFd))));
702
+ if (cancelled) {
703
+ closeFd();
704
+ return;
705
+ }
706
+ bodyPrefix = Buffer.alloc(0);
707
+ bodyPosition = HEADER_SIZE;
708
+ }
709
+ // Re-read the header from the (current) fd: the writer we just waited for finalizes the size
710
+ // in place, and an aborted receive stamps PENDING/ERROR over the same inode (harper-pro#481).
711
+ const header = Buffer.allocUnsafe(HEADER_SIZE);
712
+ const headerBytes = await new Promise((resolveRead, rejectRead) => (0, node_fs_1.read)(fd, header, 0, HEADER_SIZE, 0, (error, bytesRead) => (error ? rejectRead(error) : resolveRead(bytesRead))));
713
+ if (cancelled || fd == null)
714
+ return;
715
+ if (headerBytes < HEADER_SIZE)
716
+ throw new BlobReadError(`Incomplete blob for ${filePath}`, BLOB_CORRUPT_STATUS);
717
+ const headerValue = new DataView(header.buffer, header.byteOffset, HEADER_SIZE).getBigUint64(0);
718
+ const type = Number(headerValue >> 48n);
719
+ if (type === ERROR_TYPE || type === PENDING_TYPE) {
720
+ const message = Buffer.allocUnsafe(256);
721
+ const messageBytes = await new Promise((resolveRead) => (0, node_fs_1.read)(fd, message, 0, message.length, HEADER_SIZE, (error, bytesRead) => resolveRead(error ? 0 : bytesRead)));
722
+ throw type === ERROR_TYPE
723
+ ? new BlobReadError('Error in blob: ' + message.subarray(0, messageBytes), BLOB_CORRUPT_STATUS)
724
+ : new BlobReadError('Blob pending replication for ' + filePath, exports.BLOB_UNAVAILABLE_STATUS);
725
+ }
726
+ const declaredSize = Number(headerValue & 0xffffffffffffn);
727
+ if (type === UNCOMPRESSED_TYPE) {
728
+ // We sniffed DEFLATE, waited, and the file is now uncompressed: an in-place repair
729
+ // replaced it with a healthy uncompressed body. Report retryable so the caller re-reads
730
+ // the current file through the ordinary path rather than misclassifying it as corrupt.
731
+ throw new BlobReadError(`Blob ${filePath} was replaced while reading; retry`, exports.BLOB_UNAVAILABLE_STATUS);
732
+ }
733
+ if (type !== exports.DEFLATE_TYPE || declaredSize === UNKNOWN_SIZE) {
734
+ // the writer is confirmed done, so a placeholder size (or a header no longer readable as
735
+ // deflate) is a write that never finished — confidently incomplete, not merely in progress
736
+ throw new BlobReadError(`Incomplete blob for ${filePath}`, BLOB_CORRUPT_STATUS);
737
+ }
738
+ if (isFullRead && descriptorSize != null && descriptorSize < UNKNOWN_SIZE && declaredSize !== descriptorSize) {
739
+ throw new BlobReadError(`Blob size mismatch for ${filePath}: record descriptor expects ${descriptorSize} bytes, on-disk header reports ${declaredSize}`, BLOB_CORRUPT_STATUS);
740
+ }
741
+ if (blob.size !== declaredSize) {
742
+ blob.size = declaredSize;
743
+ if (blob.#onSize) {
744
+ for (const callback of blob.#onSize)
745
+ callback(declaredSize);
746
+ }
747
+ }
748
+ // The read stream takes sole ownership of the descriptor (nulling `fd` leaves closeFd() only
749
+ // the hold to release): an fs stream's worker-thread close(2) races any other user of the fd.
750
+ const source = (0, node_fs_1.createReadStream)(filePath, { fd, start: bodyPosition, autoClose: true });
751
+ fd = null;
752
+ const inflater = (0, node_zlib_1.createInflate)();
753
+ teardownCompressedRead = () => {
754
+ source.destroy();
755
+ inflater.destroy();
756
+ };
757
+ // pipe() does not forward source errors to its destination; without this a disk fault
758
+ // mid-read would be an unhandled 'error' on the source stream.
759
+ source.on('error', (error) => inflater.destroy(error));
760
+ if (bodyPrefix.length > 0)
761
+ inflater.write(bodyPrefix);
762
+ source.pipe(inflater);
763
+ const sliceStart = start ?? 0;
764
+ let inflatedTotal = 0; // content offset consumed so far (emitted or discarded)
765
+ try {
766
+ for await (const chunk of inflater) {
767
+ const chunkStart = inflatedTotal;
768
+ inflatedTotal += chunk.length;
769
+ if (inflatedTotal > declaredSize) {
770
+ // refused before emitting, so a body that lies about its length cannot stream unbounded output
771
+ throw new BlobReadError(`Blob ${filePath} inflates past its declared size of ${declaredSize}`, BLOB_CORRUPT_STATUS);
772
+ }
773
+ const emitFrom = Math.max(sliceStart - chunkStart, 0);
774
+ const emitTo = end === undefined ? chunk.length : Math.min(end - chunkStart, chunk.length);
775
+ if (emitTo > emitFrom)
776
+ yield emitFrom === 0 && emitTo === chunk.length ? chunk : chunk.subarray(emitFrom, emitTo);
777
+ if (end !== undefined && inflatedTotal >= end)
778
+ return; // slice satisfied; stop inflating
779
+ }
780
+ if (inflatedTotal !== declaredSize) {
781
+ // clean deflate end short of the declared size: torn despite the writer being done
782
+ throw new BlobReadError(`Incomplete blob for ${filePath}: inflated to ${inflatedTotal} bytes of a declared ${declaredSize}`, BLOB_CORRUPT_STATUS);
783
+ }
784
+ }
785
+ catch (error) {
786
+ if (cancelled)
787
+ return;
788
+ if (!(error instanceof BlobReadError) && error.code?.startsWith?.('Z_')) {
789
+ // the writer is confirmed done, so a zlib failure is real corruption, not an in-progress body
790
+ throw new BlobReadError(`Error reading compressed blob for ${filePath}: ${error.message}`, BLOB_CORRUPT_STATUS);
791
+ }
792
+ throw error;
793
+ }
794
+ finally {
795
+ teardownCompressedRead();
796
+ teardownCompressedRead = undefined;
797
+ }
798
+ }
799
+ const pumpCompressedRead = (controller) => compressedRead.next().then((step) => {
800
+ if (cancelled)
801
+ return;
802
+ if (step.done) {
803
+ closeFd();
804
+ try {
805
+ controller.close();
806
+ }
807
+ catch {
808
+ // controller may already be closed
809
+ }
810
+ return;
811
+ }
812
+ try {
813
+ controller.enqueue(step.value);
814
+ }
815
+ catch (error) {
816
+ logger_ts_1.logger.debug?.('Error enqueuing chunk', error);
817
+ }
818
+ }, (error) => {
819
+ closeFd();
820
+ if (cancelled)
821
+ return;
822
+ blob.#onError?.forEach((callback) => callback(error));
823
+ throw error;
824
+ });
538
825
  return new ReadableStream({
539
826
  start() {
540
827
  // Bound the wait for the file to appear so a write that was abandoned (e.g. the file was
@@ -586,6 +873,8 @@ class FileBackedBlob extends exports.Blob {
586
873
  return new Promise(openFile);
587
874
  },
588
875
  pull: (controller) => {
876
+ if (compressedRead)
877
+ return pumpCompressedRead(controller);
589
878
  let size = 0;
590
879
  let retries = 100;
591
880
  // No-progress deadline for the incomplete-content wait below, mirroring the open-retry loop's
@@ -608,8 +897,7 @@ class FileBackedBlob extends exports.Blob {
608
897
  settled = true;
609
898
  closeFd();
610
899
  clearTimeout(timer);
611
- if (watcher)
612
- watcher.close();
900
+ closeWatcher();
613
901
  reject(error);
614
902
  blob.#onError?.forEach((callback) => callback(error));
615
903
  }
@@ -652,35 +940,12 @@ class FileBackedBlob extends exports.Blob {
652
940
  // half-replicated blob whose source stream aborted; the bytes are still expected — retry (harper-pro#481)
653
941
  return onError(new BlobReadError('Blob pending replication for ' + filePath, exports.BLOB_UNAVAILABLE_STATUS));
654
942
  }
655
- if (buffer[1] === DEFLATE_TYPE) {
656
- // We can't seek/slice a deflate stream by uncompressed offset, so hand off to the
657
- // buffered inflate path (bytes() inflates then slices) and emit it as one chunk.
658
- // Safe by construction: the read loop never streams the raw compressed body.
659
- return blob.bytes().then((bytes) => {
660
- // bytes() resolves asynchronously; the consumer may have cancelled meanwhile.
661
- // Settle exactly once and route the close through closeFd() so we never touch a
662
- // reassigned/nulled descriptor (#1457).
663
- if (settled || cancelled)
664
- return resolve();
665
- settled = true;
666
- closeFd();
667
- if (bytes.length > 0) {
668
- try {
669
- controller.enqueue(bytes);
670
- }
671
- catch (error) {
672
- logger_ts_1.logger.debug?.('Error enqueuing chunk', error);
673
- return resolve();
674
- }
675
- }
676
- try {
677
- controller.close();
678
- }
679
- catch {
680
- // controller may already be closed
681
- }
682
- resolve();
683
- }, (error) => onError(error));
943
+ if (buffer[1] === exports.DEFLATE_TYPE) {
944
+ // a deflate body cannot be length-checked or offset-seeked: this and every later
945
+ // pull go through the streaming inflate iterator instead of the raw read loop
946
+ settled = true;
947
+ compressedRead = readCompressedBlob(buffer.subarray(HEADER_SIZE, bytesRead), bytesRead);
948
+ return pumpCompressedRead(controller).then(resolve, reject);
684
949
  }
685
950
  size = Number(headerValue & 0xffffffffffffn);
686
951
  if (size < UNKNOWN_SIZE) {
@@ -728,10 +993,7 @@ class FileBackedBlob extends exports.Blob {
728
993
  if (updatedSize === UNKNOWN_SIZE)
729
994
  return false;
730
995
  size = updatedSize;
731
- if (watcher) {
732
- watcher.close();
733
- watcher = null;
734
- }
996
+ closeWatcher();
735
997
  // The header reports a known final size but the bytes at `position` have not arrived.
736
998
  // Re-entering readMore() synchronously here busy-spins the worker at ~100% CPU on a
737
999
  // present-but-truncated blob (header rewritten to a self-consistent smaller size, lock
@@ -749,26 +1011,39 @@ class FileBackedBlob extends exports.Blob {
749
1011
  return true;
750
1012
  };
751
1013
  // the file is not finished being written, watch the file for changes to resume reading
752
- // set up a watcher to be notified of file changes
753
- watcher = (0, node_fs_1.watch)(filePath, { persistent: false }, () => {
754
- if (watcher) {
755
- watcher.close();
756
- watcher = null;
1014
+ watchTarget ??= (0, watchPath_ts_1.resolveWatchTarget)(filePath);
1015
+ watcher = watchInProgressFile(filePath, watchTarget, {
1016
+ isLive: (candidate) => watcher === candidate,
1017
+ onChange: () => {
1018
+ closeWatcher();
757
1019
  clearTimeout(timer); // clear it
758
1020
  readMore(resolve, reject);
759
- }
1021
+ },
1022
+ onFailure: () => {
1023
+ watcher = null;
1024
+ clearTimeout(timer);
1025
+ timer = setTimeout(() => readMore(resolve, reject), 20).unref();
1026
+ },
760
1027
  });
761
1028
  // immediately try to read again in case there was a change before we started watching,
762
1029
  // readSync should be fine here, the data should be in memory
763
1030
  if ((0, node_fs_1.readSync)(fd, buffer, 0, buffer.length, position) > 0) {
764
1031
  // never mind with the watcher, let's read more data
765
- if (watcher) {
766
- watcher.close();
767
- watcher = null;
768
- }
1032
+ closeWatcher();
769
1033
  readMore(resolve, reject);
770
1034
  }
771
1035
  else if (!resumeIfWriterFinished()) {
1036
+ if (!watcher) {
1037
+ // Nothing will wake this read, so poll on the deadline resumeIfWriterFinished uses
1038
+ // rather than sitting out the full read timeout and 503-ing a healthy write.
1039
+ if (Date.now() >= incompleteDeadline) {
1040
+ onError(new BlobReadError(`File read timed out reading from ${filePath}, read ${totalContentRead} bytes, but size is supposed to be ${size} bytes`, exports.BLOB_UNAVAILABLE_STATUS));
1041
+ }
1042
+ else {
1043
+ timer = setTimeout(() => readMore(resolve, reject), 20).unref();
1044
+ }
1045
+ return;
1046
+ }
772
1047
  // set a timer for the watcher too. A write that stalls past the bound returns a
773
1048
  // prompt 503 (retryable) instead of holding the connection for the full 60s (#1423).
774
1049
  timer = setTimeout(() => {
@@ -851,11 +1126,11 @@ class FileBackedBlob extends exports.Blob {
851
1126
  },
852
1127
  cancel() {
853
1128
  cancelled = true;
1129
+ teardownCompressedRead?.();
854
1130
  closeFd(); // releases the hold, including when cancelled before any open succeeded
855
1131
  clearTimeout(timer);
856
1132
  clearTimeout(openTimer);
857
- if (watcher)
858
- watcher.close();
1133
+ closeWatcher();
859
1134
  },
860
1135
  });
861
1136
  function checkIfIsBeingWritten() {
@@ -877,6 +1152,7 @@ class FileBackedBlob extends exports.Blob {
877
1152
  if (sourceStorageInfo?.fileId) {
878
1153
  const slicedStorageInfo = {
879
1154
  ...sourceStorageInfo,
1155
+ fileState: (sourceStorageInfo.fileState ??= {}),
880
1156
  start,
881
1157
  end,
882
1158
  };
@@ -1153,8 +1429,24 @@ function deleteBlob(blob) {
1153
1429
  Atomics.store(state.table, state.slot + REREFERENCED, 0);
1154
1430
  }
1155
1431
  // Reusing the queued entry when two writes supersede the same file keeps the age cap measuring
1156
- // from the first supersession.
1157
- const pending = pendingReclamation.get(filePath) ?? { blob, deadline: 0, enqueuedAt: now, supersededAt: now };
1432
+ // from the first supersession and tracks every live blob instance that carries the condemned fileId.
1433
+ const pending = pendingReclamation.get(filePath) ?? {
1434
+ blobs: [],
1435
+ seenBlobs: new WeakSet(),
1436
+ fileInfo: { store: storageInfo?.store, fileId: storageInfo?.fileId },
1437
+ deadline: 0,
1438
+ enqueuedAt: now,
1439
+ supersededAt: now,
1440
+ };
1441
+ if (!pending.seenBlobs.has(blob)) {
1442
+ pending.seenBlobs.add(blob);
1443
+ pending.blobs.push(new WeakRef(blob));
1444
+ }
1445
+ if (pending.unlinking) {
1446
+ if (storageInfo)
1447
+ discardStorage(storageInfo);
1448
+ return;
1449
+ }
1158
1450
  scheduleReclamation(enqueue(filePath, pending, Math.max(pending.deadline, now + getReclamationDelay())));
1159
1451
  }
1160
1452
  /**
@@ -1201,7 +1493,7 @@ function runReclamation() {
1201
1493
  earliest = pending.deadline;
1202
1494
  break; // insertion order is deadline order; nothing behind this entry is due
1203
1495
  }
1204
- const storageInfo = storageInfoForBlob.get(pending.blob);
1496
+ const storageInfo = pending.fileInfo;
1205
1497
  const expired = now - pending.enqueuedAt >= ageCap;
1206
1498
  let held;
1207
1499
  try {
@@ -1255,6 +1547,16 @@ function runReclamation() {
1255
1547
  // Keep the entry until the unlink lands so a concurrent re-reference can tell that the file is
1256
1548
  // already going away instead of silently adopting a doomed path.
1257
1549
  pending.unlinking = true;
1550
+ // Once reclamation has claimed this file, a later write must not preserve its soon-to-be-deleted
1551
+ // fileId. The retention window above still permits legitimate re-references before this point.
1552
+ for (const blobRef of pending.blobs) {
1553
+ const blob = blobRef.deref();
1554
+ if (!blob)
1555
+ continue;
1556
+ const instanceStorageInfo = storageInfoForBlob.get(blob);
1557
+ if (instanceStorageInfo)
1558
+ discardStorage(instanceStorageInfo);
1559
+ }
1258
1560
  (0, node_fs_1.unlink)(filePath, (error) => {
1259
1561
  pendingReclamation.delete(filePath);
1260
1562
  if (pendingReclamation.size === 0)
@@ -1276,6 +1578,87 @@ function runReclamation() {
1276
1578
  function setDeletionDelay(delay) {
1277
1579
  reclamationDelayOverride = delay;
1278
1580
  }
1581
+ /**
1582
+ * `StorageInfo.contentBuffer` must always hold a `Buffer`: `Buffer.prototype.toString()` decodes
1583
+ * UTF-8, while the inherited `Uint8Array.prototype.toString()` joins the byte values with commas.
1584
+ */
1585
+ function asBuffer(bytes) {
1586
+ return Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1587
+ }
1588
+ const DEFAULT_COMPRESSION_THRESHOLD = FILE_STORAGE_THRESHOLD;
1589
+ /**
1590
+ * Already-compressed containers (the deployment recorder writes `application/gzip`). An operator entry
1591
+ * for the same key overrides these; a broader operator wildcard does not — specificity beats origin.
1592
+ */
1593
+ const BUILT_IN_COMPRESSION_ENTRIES = {
1594
+ 'application/gzip': false,
1595
+ 'application/zip': false,
1596
+ 'application/zstd': false,
1597
+ 'image/*': false,
1598
+ 'video/*': false,
1599
+ 'audio/*': false,
1600
+ };
1601
+ /** The matcher works on the canonical lowercase base media type, with any `;` parameters stripped. */
1602
+ function normalizeContentType(type) {
1603
+ if (!type)
1604
+ return '';
1605
+ const parametersStart = type.indexOf(';');
1606
+ return (parametersStart === -1 ? type : type.slice(0, parametersStart)).trim().toLowerCase();
1607
+ }
1608
+ // The normalized policy map is rebuilt only when the configured value object changes identity, so
1609
+ // the per-save cost is one config lookup and a reference compare.
1610
+ let compressionPolicyForConfig;
1611
+ let compressionPolicy = null;
1612
+ let invalidCompressionConfigLogged = false;
1613
+ function getBlobCompressionPolicy() {
1614
+ const configured = (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_BLOBS_COMPRESSION);
1615
+ if (configured === compressionPolicyForConfig)
1616
+ return compressionPolicy;
1617
+ compressionPolicyForConfig = configured;
1618
+ compressionPolicy = null;
1619
+ if (configured == null)
1620
+ return null;
1621
+ if (typeof configured !== 'object' || Array.isArray(configured)) {
1622
+ if (!invalidCompressionConfigLogged) {
1623
+ invalidCompressionConfigLogged = true;
1624
+ logger_ts_1.logger.warn?.('Ignoring invalid storage.blobs.compression value; expected a map of content types');
1625
+ }
1626
+ return null;
1627
+ }
1628
+ const policy = new Map(Object.entries(BUILT_IN_COMPRESSION_ENTRIES));
1629
+ for (const [key, entry] of Object.entries(configured)) {
1630
+ policy.set(normalizeContentType(key) || 'default', entry);
1631
+ }
1632
+ compressionPolicy = policy;
1633
+ return policy;
1634
+ }
1635
+ /**
1636
+ * Whether a new blob of `type` and (uncompressed) `size` should be deflate-compressed on disk,
1637
+ * per the opt-in `storage.blobs.compression` map. Absent config means never. Matching precedence
1638
+ * is exact type, then `type/*`, then `default`. An unknown size never compresses: the threshold
1639
+ * cannot be evaluated, and compressing a small streamed write under a large configured threshold
1640
+ * would violate the operator's limit.
1641
+ */
1642
+ function resolveBlobCompression(type, size) {
1643
+ const policy = getBlobCompressionPolicy();
1644
+ if (!policy)
1645
+ return false;
1646
+ const normalized = normalizeContentType(type);
1647
+ let entry = policy.get(normalized);
1648
+ if (entry === undefined && normalized) {
1649
+ const slash = normalized.indexOf('/');
1650
+ if (slash > 0)
1651
+ entry = policy.get(normalized.slice(0, slash + 1) + '*');
1652
+ }
1653
+ entry ??= policy.get('default');
1654
+ if (!entry)
1655
+ return false;
1656
+ if (entry.codec !== undefined && entry.codec !== 'deflate')
1657
+ return false; // enforced by config validation; fail safe
1658
+ if (size === undefined)
1659
+ return false;
1660
+ return size >= (entry.threshold ?? DEFAULT_COMPRESSION_THRESHOLD);
1661
+ }
1279
1662
  /**
1280
1663
  * Create a blob from a readable stream or a buffer by creating a file in the blob storage path with a new unique internal id, that
1281
1664
  * can be saved/stored.
@@ -1292,7 +1675,7 @@ function createBlob(source, options) {
1292
1675
  storageInfoForBlob.set(blob, storageInfo);
1293
1676
  if (source instanceof Uint8Array) {
1294
1677
  blob.size = source.length;
1295
- storageInfo.contentBuffer = source;
1678
+ storageInfo.contentBuffer = asBuffer(source);
1296
1679
  }
1297
1680
  else if (source instanceof node_stream_1.Readable) {
1298
1681
  storageInfo.source = source;
@@ -1306,6 +1689,22 @@ function createBlob(source, options) {
1306
1689
  return blob;
1307
1690
  }
1308
1691
  (0, globals_js_1._assignPackageExport)('createBlob', createBlob);
1692
+ /**
1693
+ * Create a blob whose source stream carries the STORED representation — an already-deflated body —
1694
+ * rather than content bytes, so a replication receiver can land a peer's compressed blob without
1695
+ * recompressing it. `size` is the uncompressed content length (it goes in the header; a raw
1696
+ * compressed source cannot derive it) and the body is verified on write: it must be a well-formed
1697
+ * deflate stream inflating to exactly `size` bytes, or the save rejects (see
1698
+ * createStoredDeflateVerifier). Internal to replication on purpose — not part of BlobCreationOptions
1699
+ * and not exported to components.
1700
+ */
1701
+ function createBlobFromStoredBody(source, options) {
1702
+ if (options.codec !== 'deflate')
1703
+ throw new Error(`Unsupported stored blob codec: ${options.codec}`);
1704
+ const blob = createBlob(source, { type: options.type, size: options.size });
1705
+ storageInfoForBlob.get(blob).storedCodec = options.codec;
1706
+ return blob;
1707
+ }
1309
1708
  // When set (during a migration via encodeBlobsWithFilePath), saveBlob pushes the in-flight
1310
1709
  // writeBlob save promise here so the migration can await every blob's durable write before
1311
1710
  // declaring the database done. Without this, the migration's `await targetDbi.put(...)`
@@ -1319,6 +1718,13 @@ function saveBlob(blob, deleteOnFailure = false) {
1319
1718
  storageInfoForBlob.set(blob, storageInfo);
1320
1719
  }
1321
1720
  else {
1721
+ if (storageInfo.fileState?.discarded) {
1722
+ // The file this blob was saved to has been deleted (an aborted/skipped write's cleanup, or an
1723
+ // explicit delete). Re-storing it would commit a reference to a file that no longer exists —
1724
+ // a permanently unreadable record, and for a replicated one a blob the peer can never fetch.
1725
+ // Fail here, where the cause is still known, instead of at the eventual read (issue #2062).
1726
+ throw new Error('Blob was discarded (its file was deleted by an aborted or superseded write) and can no longer be stored; the data must be re-supplied');
1727
+ }
1322
1728
  if (storageInfo.fileId)
1323
1729
  return storageInfo; // if there is any file id, we are already saving and can return the info
1324
1730
  storageInfo.store = currentStore;
@@ -1331,6 +1737,14 @@ function saveBlob(blob, deleteOnFailure = false) {
1331
1737
  }
1332
1738
  return storageInfo; // nothing more to do if it supposed to be saved in the record
1333
1739
  }
1740
+ // The policy resolves here, not in createBlob, because the HTTP-upload path never sees creation
1741
+ // options — every local write funnels through saveBlob. An explicit `compress` option (either
1742
+ // value) and a pre-compressed source both take precedence.
1743
+ if (storageInfo.compress === undefined && !storageInfo.storedCodec) {
1744
+ const size = blob.size ?? storageInfo.contentBuffer?.length;
1745
+ if (resolveBlobCompression(blob.type, size))
1746
+ storageInfo.compress = true;
1747
+ }
1334
1748
  generateFilePath(storageInfo);
1335
1749
  if (storageInfo.source)
1336
1750
  writeBlobWithStream(blob, storageInfo.source, storageInfo);
@@ -1406,11 +1820,92 @@ function getBlobStreamIdleTimeoutMs(stream) {
1406
1820
  function shouldDestroyIdleBlobSource(paused, bytesWritten, lastProgressBytes) {
1407
1821
  return !(paused && bytesWritten > lastProgressBytes);
1408
1822
  }
1823
+ /**
1824
+ * Pass-through for a pre-compressed (stored deflate) blob body that concurrently inflates what it
1825
+ * forwards and fails the pipeline unless the body is a well-formed deflate stream inflating to
1826
+ * exactly `expectedSize` bytes. This is what lets a raw peer-preserved body land under a finalized
1827
+ * DEFLATE header without trusting the peer: a truncated, corrupt, oversized (bomb), or trailing-
1828
+ * garbage body rejects the save instead of being published.
1829
+ */
1830
+ function createStoredDeflateVerifier(expectedSize, fileId) {
1831
+ const inflater = (0, node_zlib_1.createInflate)();
1832
+ let inflatedLength = 0;
1833
+ let failure;
1834
+ // The in-flight transform() callback, kept so a failure can settle it directly. A mid-write
1835
+ // zlib data error (a corrupt, not merely truncated, body) fires 'error'/'close' but NEVER invokes
1836
+ // the pending `inflater.write` callback, so a transform that waited only on that callback would
1837
+ // stall forever — hanging the save pipeline and leaking the blob write lock. settlePending() is
1838
+ // the escape: whichever of the write callback or the failure runs first clears it; the other is a
1839
+ // no-op, so callback() is still called exactly once per chunk.
1840
+ let pending;
1841
+ const settlePending = () => {
1842
+ if (!pending)
1843
+ return;
1844
+ const { callback, chunk } = pending;
1845
+ pending = undefined;
1846
+ callback(failure, failure ? undefined : chunk);
1847
+ };
1848
+ const fail = (error) => {
1849
+ failure ??= error;
1850
+ inflater.destroy();
1851
+ settlePending();
1852
+ };
1853
+ inflater.on('data', (inflated) => {
1854
+ inflatedLength += inflated.length;
1855
+ if (inflatedLength > expectedSize)
1856
+ fail(new Error(`Pre-compressed blob body for ${fileId} inflates past its declared size of ${expectedSize}`));
1857
+ });
1858
+ inflater.on('error', (error) => fail(new Error(`Pre-compressed blob body for ${fileId} is not a valid deflate stream`, { cause: error })));
1859
+ // Trailing data past the final deflate block is detected best-effort (zlib may acknowledge a write
1860
+ // before emitting the early 'end' it triggers); readers ignore trailing bytes, so the residue is
1861
+ // only wasted disk, while truncation, wrong length, invalid stream, and oversize are deterministic.
1862
+ const trailingData = () => new Error(`Pre-compressed blob body for ${fileId} continues past the end of its deflate stream`);
1863
+ let sawDeflateEnd = false;
1864
+ inflater.on('end', () => {
1865
+ if (!inflater.writableEnded)
1866
+ fail(trailingData());
1867
+ sawDeflateEnd = true;
1868
+ });
1869
+ return new node_stream_1.Transform({
1870
+ transform(chunk, _encoding, callback) {
1871
+ if (!failure && sawDeflateEnd)
1872
+ fail(trailingData());
1873
+ if (failure)
1874
+ return callback(failure);
1875
+ // Forward the chunk only once zlib has consumed it: this is the backpressure coupling and
1876
+ // what keeps the end/overflow/error detections ordered ahead of the next chunk. If zlib
1877
+ // data-errors while consuming this chunk it drops the write callback, so fail()/settlePending()
1878
+ // settles this transform instead (see the `pending` note above).
1879
+ pending = { callback, chunk };
1880
+ inflater.write(chunk, (writeError) => {
1881
+ if (!pending)
1882
+ return; // already settled by a failure; do not call callback twice
1883
+ pending = undefined;
1884
+ const error = failure ?? writeError;
1885
+ callback(error, error ? undefined : chunk);
1886
+ });
1887
+ },
1888
+ flush(callback) {
1889
+ if (failure)
1890
+ return callback(failure);
1891
+ inflater.on('close', () => {
1892
+ if (!failure && inflatedLength !== expectedSize)
1893
+ failure = new Error(`Pre-compressed blob body for ${fileId} inflated to ${inflatedLength} bytes; expected ${expectedSize}`);
1894
+ callback(failure);
1895
+ });
1896
+ inflater.end();
1897
+ },
1898
+ destroy(error, callback) {
1899
+ inflater.destroy();
1900
+ callback(error);
1901
+ },
1902
+ });
1903
+ }
1409
1904
  function writeBlobWithStream(blob, stream, storageInfo, options) {
1410
1905
  const repairTargetPath = options?.repairTargetPath;
1411
1906
  const repairing = repairTargetPath !== undefined;
1412
1907
  const repairTempLockKey = options?.repairTempLockKey;
1413
- const { filePath, fileId, store, compress, flush } = storageInfo;
1908
+ const { filePath, fileId, store, compress, storedCodec, flush } = storageInfo;
1414
1909
  storageInfo.saving = new Promise((resolve, reject) => {
1415
1910
  const lockKey = fileId + ':blob';
1416
1911
  let lockAcquired = repairing;
@@ -1458,7 +1953,7 @@ function writeBlobWithStream(blob, stream, storageInfo, options) {
1458
1953
  let headerValue = BigInt(size);
1459
1954
  const header = new Uint8Array(HEADER_SIZE);
1460
1955
  const headerView = new DataView(header.buffer);
1461
- headerValue |= BigInt(compress ? DEFLATE_TYPE : UNCOMPRESSED_TYPE) << 48n;
1956
+ headerValue |= BigInt(compress || storedCodec ? exports.DEFLATE_TYPE : UNCOMPRESSED_TYPE) << 48n;
1462
1957
  headerView.setBigInt64(0, headerValue);
1463
1958
  return header;
1464
1959
  }
@@ -1499,8 +1994,8 @@ function writeBlobWithStream(blob, stream, storageInfo, options) {
1499
1994
  // half-replicated blob returns 503 (retry) instead of 500 (confidently incomplete → the peer
1500
1995
  // advances its resume cursor past it = silent loss, harper-pro#481). Hold the write lock until the
1501
1996
  // marker is durable so no concurrent read/send observes the bare partial file (lock-free + short =
1502
- // classified 500 = the very loss this prevents). The re-stream overwrites this stub
1503
- // (createWriteStream flags 'w'); a terminal give-up on the receive side unlinks it (→ 404). Build
1997
+ // classified 500 = the very loss this prevents). The re-stream builds a fresh blob and so takes a
1998
+ // new file id, leaving this stub for orphan GC; a terminal give-up unlinks it (→ 404). Build
1504
1999
  // the header directly rather than via createHeader so its compress-type OR can't collide with the
1505
2000
  // PENDING type bits.
1506
2001
  // Bounded so a `writeFile` that never calls back cannot leave `saving` un-settled for the
@@ -1606,6 +2101,20 @@ function writeBlobWithStream(blob, stream, storageInfo, options) {
1606
2101
  void replaceTarget();
1607
2102
  return;
1608
2103
  }
2104
+ if (compressedStream && blob.size !== compressedStream.bytesWritten) {
2105
+ // A known-size compressed write stamped the declared size into the header up front, but
2106
+ // the source ended with a different content length. Unlike the uncompressed path — whose
2107
+ // short body reads as retryable-incomplete (503) — this file would inflate to the wrong
2108
+ // size and every reader would reject it as permanently corrupt (500). Fail the save so the
2109
+ // record never commits a reference to an unreadable file (matches the storedCodec verifier
2110
+ // and the repair size check); the unreferenced file is left for orphan GC. The unknown-size
2111
+ // path syncs blob.size to bytesWritten before reaching here, so it never trips this.
2112
+ store.unlock(lockKey);
2113
+ reject(new Error(`Blob ${fileId} deflated ${compressedStream.bytesWritten} bytes but its header declares ${blob.size}`));
2114
+ (0, node_fs_1.close)(fd);
2115
+ writeStream.fd = null;
2116
+ return;
2117
+ }
1609
2118
  store.unlock(lockKey);
1610
2119
  if (flush) {
1611
2120
  // we just use fdatasync because we really aren't that concerned with flushing file metadata
@@ -1625,6 +2134,12 @@ function writeBlobWithStream(blob, stream, storageInfo, options) {
1625
2134
  }
1626
2135
  }
1627
2136
  try {
2137
+ if (storedCodec && blob.size === undefined) {
2138
+ // The header must carry the uncompressed length, and it cannot be derived from a raw
2139
+ // compressed source (bytesWritten would be the compressed count). Refuse before any file
2140
+ // exists at the path so the failure reads as a cleanly absent blob, not a torn one.
2141
+ throw new Error(`Cannot store a pre-compressed blob without a known uncompressed size (fileId=${fileId})`);
2142
+ }
1628
2143
  if (!lockAcquired) {
1629
2144
  if (!store.tryLock(lockKey))
1630
2145
  throw new Error(`Unable to get lock for blob file ${fileId}`);
@@ -1654,7 +2169,14 @@ function writeBlobWithStream(blob, stream, storageInfo, options) {
1654
2169
  stream.on('resume', armIdleTimer);
1655
2170
  armIdleTimer();
1656
2171
  }
1657
- if (compress) {
2172
+ if (storedCodec) {
2173
+ // The source bytes are already the stored deflate representation (a peer preserving its
2174
+ // codec). Verify — not just copy — so a truncated or forged body can never land under a
2175
+ // finalized DEFLATE header: the verifier inflates the pass-through bytes concurrently and
2176
+ // fails the pipeline unless they inflate to exactly the declared uncompressed size.
2177
+ (0, node_stream_1.pipeline)(stream, createStoredDeflateVerifier(blob.size, fileId), writeStream, finished);
2178
+ }
2179
+ else if (compress) {
1658
2180
  if (!wroteSize)
1659
2181
  writeStream.write(COMPRESS_HEADER);
1660
2182
  compressedStream = (0, node_zlib_1.createDeflate)();
@@ -1701,9 +2223,11 @@ function getFilePathForBlob(blob) {
1701
2223
  * Repairs a damaged file-backed blob under its existing fileId. The damage check and atomic file
1702
2224
  * replacement share the blob lock, so a failed repair never modifies the referenced file. Callers
1703
2225
  * must establish an exact record identity tie (same version and source node) and positional blob
1704
- * pairing before calling. The received byte count must match the reported source size and, when
1705
- * present, the stored descriptor. On a synchronous `undefined`, no repair started and `source`
1706
- * remains owned by the caller.
2226
+ * pairing before calling, and classify the file with `blobFileMissingOrIncompleteAsync` first — a
2227
+ * compressed body's damage is only provable by inflating it, which the locked recheck here will
2228
+ * not do. The received byte count must match the reported source size and, when present, the
2229
+ * stored descriptor. On a synchronous `undefined`, no repair started and `source` remains owned
2230
+ * by the caller.
1707
2231
  */
1708
2232
  function repairBlobFile(blob, source, sourceSize = blob.size) {
1709
2233
  let blobLockKey;
@@ -1765,7 +2289,9 @@ function repairBlobFile(blob, source, sourceSize = blob.size) {
1765
2289
  filePath: repairFilePath,
1766
2290
  saving: undefined,
1767
2291
  flush: true,
2292
+ // the repair source is inflated content; its size verification counts uncompressed bytes
1768
2293
  compress: false,
2294
+ storedCodec: undefined,
1769
2295
  };
1770
2296
  writeBlobWithStream(blob, source, repairStorageInfo, {
1771
2297
  expectedSize,
@@ -1792,11 +2318,84 @@ function repairBlobFile(blob, source, sourceSize = blob.size) {
1792
2318
  return undefined;
1793
2319
  }
1794
2320
  }
2321
+ /** Fail-closed storage and descriptor errors that must abort the capture instead of producing an unverified backup. */
2322
+ const SYSTEMIC_IO_ERRORS = new Set(['EMFILE', 'ENFILE', 'ENOSPC', 'EIO', 'EROFS']);
2323
+ function isSystemicIoError(error) {
2324
+ return SYSTEMIC_IO_ERRORS.has(error?.code ?? '');
2325
+ }
2326
+ /**
2327
+ * How a consumer that captures a blob root (backup snapshot, backup archive) should treat one file.
2328
+ * `skip` is not a blob to capture at all; `pending` and `gone` require markers; `capture` is taken as-is.
2329
+ * `capture` means settled as far as the path can show, which is short of a guarantee: a known-size write
2330
+ * that has landed every byte is indistinguishable here from a finished one, and if it then aborts, the
2331
+ * PENDING stamp rewrites that inode in place, truncating any same-filesystem hard link taken from it.
2332
+ * Telling the two apart needs the blob write lock, which is keyed by file id and unreachable from a walk.
2333
+ */
2334
+ async function classifyBlobFileForCapture(filePath) {
2335
+ if (filePath.endsWith(BLOB_REPAIR_SUFFIX))
2336
+ return 'skip';
2337
+ let header;
2338
+ let fileSize;
2339
+ let handle;
2340
+ try {
2341
+ handle = await (0, promises_1.open)(filePath, 'r');
2342
+ }
2343
+ catch (error) {
2344
+ // Reclamation can unlink a superseded blob after the engine checkpoint but before this walk. The
2345
+ // checkpointed record may still reference it, so reserve the id. Treat local absence as terminal
2346
+ // rather than making every read wait for repair; this gives up peer repair of that checkpointed version.
2347
+ if (error.code === 'ENOENT')
2348
+ return 'gone';
2349
+ throw error;
2350
+ }
2351
+ try {
2352
+ fileSize = (await handle.stat()).size;
2353
+ header = Buffer.alloc(HEADER_SIZE);
2354
+ const { bytesRead } = await handle.read(header, 0, HEADER_SIZE, 0);
2355
+ header = header.subarray(0, bytesRead);
2356
+ }
2357
+ finally {
2358
+ await handle.close();
2359
+ }
2360
+ if (blobHeaderIsAbortMarker(header))
2361
+ return 'capture';
2362
+ if (blobHeaderIndicatesIncomplete(header, fileSize))
2363
+ return 'pending';
2364
+ // A deflate header records the *uncompressed* length, so the check above compares lengths only for
2365
+ // UNCOMPRESSED_TYPE and a short compressed body reaches here looking whole. Both producers of one
2366
+ // need this: saveBlob stamps a known size before the first compressed byte, so a live write is
2367
+ // invisible above and caught only here, and an unclean shutdown can leave a torn body the
2368
+ // asynchronous repair sweep has not reached yet. Compression is opt-in via storage.blobs.compression
2369
+ // (off by default): on a corpus with it disabled this branch is never taken, but where an operator
2370
+ // enables it each completeness check fully inflates the compressed body it reaches.
2371
+ if (header[1] !== exports.DEFLATE_TYPE)
2372
+ return 'capture';
2373
+ const uncompressedSize = Number(new DataView(header.buffer, header.byteOffset, HEADER_SIZE).getBigUint64(0) & 0xffffffffffffn);
2374
+ return (await inflatesToExactly(filePath, uncompressedSize)) ? 'capture' : 'pending';
2375
+ }
2376
+ /**
2377
+ * Whether the header is a deliberate abort marker rather than content. Written once and never
2378
+ * rewritten, so a consumer sharing blob inodes can keep them: dropping a PENDING marker downgrades a
2379
+ * retryable 503 to a 404 the replication layer reads as "cleanly gone" (harper-pro#481).
2380
+ */
2381
+ function blobHeaderIsAbortMarker(header) {
2382
+ if (header.length < HEADER_SIZE)
2383
+ return false;
2384
+ const type = header.readUInt16BE(0);
2385
+ return type === PENDING_TYPE || type === ERROR_TYPE;
2386
+ }
2387
+ /** The stand-in bytes for a blob that could not be captured whole. */
2388
+ function createCaptureMarker(disposition, message) {
2389
+ const messageBuffer = Buffer.from(message);
2390
+ const header = Buffer.alloc(HEADER_SIZE);
2391
+ new DataView(header.buffer, header.byteOffset, HEADER_SIZE).setBigInt64(0, BigInt(messageBuffer.length) | (BigInt(disposition === 'gone' ? ERROR_TYPE : PENDING_TYPE) << 48n));
2392
+ return Buffer.concat([header, messageBuffer]);
2393
+ }
1795
2394
  function blobHeaderIndicatesIncomplete(header, fileSize) {
1796
2395
  if (fileSize < HEADER_SIZE || header.length < HEADER_SIZE)
1797
2396
  return true;
1798
2397
  const type = header.readUInt16BE(0);
1799
- if (type !== UNCOMPRESSED_TYPE && type !== DEFLATE_TYPE)
2398
+ if (type !== UNCOMPRESSED_TYPE && type !== exports.DEFLATE_TYPE)
1800
2399
  return true;
1801
2400
  const storedSize = header.readUIntBE(2, 6);
1802
2401
  if (storedSize === UNKNOWN_SIZE)
@@ -1804,10 +2403,83 @@ function blobHeaderIndicatesIncomplete(header, fileSize) {
1804
2403
  return type === UNCOMPRESSED_TYPE && fileSize !== HEADER_SIZE + storedSize;
1805
2404
  }
1806
2405
  /**
1807
- * Whether a file-backed blob's backing file is missing or incomplete on disk — the gate for the
1808
- * copy-delivery repair. This blocking probe is the locked final recheck after the caller's async
1809
- * exact-duplicate prefilter. Compressed bodies require the asynchronous repair sweep for
1810
- * verification. Returns undefined for blobs the question does not apply to.
2406
+ * What a blob file's header and length alone say about its completeness: `true` when the file is
2407
+ * missing, short, unfinished, an error/pending stub, or disagrees with the record descriptor; `false`
2408
+ * when an uncompressed body is whole; `null` for a deflate body, whose header records the uncompressed
2409
+ * length and so proves nothing about the body — only inflating it can. `undefined` when the question
2410
+ * does not apply or the file could not be inspected.
2411
+ */
2412
+ function classifyBlobFileByHeader(blob, fileSize, header, headerBytes) {
2413
+ if (fileSize < HEADER_SIZE || headerBytes < HEADER_SIZE)
2414
+ return true;
2415
+ if (blobHeaderIndicatesIncomplete(header, fileSize))
2416
+ return true;
2417
+ const descriptorSize = blob.size;
2418
+ if (descriptorSize !== undefined && header.readUIntBE(2, 6) !== descriptorSize)
2419
+ return true;
2420
+ return header.readUInt16BE(0) === exports.DEFLATE_TYPE ? null : false;
2421
+ }
2422
+ /**
2423
+ * Whether a file-backed blob's backing file is missing or incomplete on disk — the unlocked,
2424
+ * non-blocking classification a copy-delivery repair candidate gets first (harper-pro#699). A deflate
2425
+ * body is inflated (streamed, off the event loop) to decide; the file it judged damaged is remembered
2426
+ * so the locked recheck in `repairBlobFile` can recognize it without inflating again. Returns
2427
+ * undefined for blobs the question does not apply to, or that could not be inspected.
2428
+ */
2429
+ async function blobFileMissingOrIncompleteAsync(blob) {
2430
+ if (!(blob instanceof FileBackedBlob))
2431
+ return undefined;
2432
+ const storageInfo = storageInfoForBlob.get(blob);
2433
+ if (!storageInfo?.fileId)
2434
+ return undefined;
2435
+ storageInfo.probedDamage = undefined; // every probe starts over; no early exit may leave a stale verdict behind
2436
+ const filePath = getFilePath(storageInfo);
2437
+ let file;
2438
+ try {
2439
+ file = await (0, promises_1.open)(filePath, 'r');
2440
+ }
2441
+ catch (error) {
2442
+ if (error?.code === 'ENOENT')
2443
+ return true;
2444
+ logger_ts_1.logger.debug?.('Unable to open blob file for in-place repair inspection', error);
2445
+ return undefined;
2446
+ }
2447
+ let fileSize;
2448
+ const header = Buffer.allocUnsafe(HEADER_SIZE);
2449
+ let verdict;
2450
+ try {
2451
+ fileSize = (await file.stat()).size;
2452
+ const { bytesRead } = await file.read(header, 0, HEADER_SIZE, 0);
2453
+ verdict = classifyBlobFileByHeader(blob, fileSize, header, bytesRead);
2454
+ }
2455
+ catch (error) {
2456
+ logger_ts_1.logger.debug?.('Unable to inspect blob file for in-place repair', error);
2457
+ return undefined;
2458
+ }
2459
+ finally {
2460
+ await file.close().catch(() => { });
2461
+ }
2462
+ if (verdict !== null)
2463
+ return verdict;
2464
+ try {
2465
+ verdict = !(await inflatesToExactly(filePath, header.readUIntBE(2, 6)));
2466
+ }
2467
+ catch (error) {
2468
+ logger_ts_1.logger.debug?.('Unable to inspect blob file for in-place repair', error);
2469
+ return undefined;
2470
+ }
2471
+ if (verdict)
2472
+ storageInfo.probedDamage = { fileSize, header };
2473
+ return verdict;
2474
+ }
2475
+ /**
2476
+ * The locked, synchronous recheck of `blobFileMissingOrIncompleteAsync`'s verdict, run by
2477
+ * `repairBlobFile` under the blob's write lock so a repair can only replace a file no writer is
2478
+ * still producing. It repeats the header classification, and for a deflate body — which it cannot
2479
+ * afford to inflate on the event loop — answers `true` only while the file is still the one the
2480
+ * probe found damaged: a body can only grow (writers append; a repair replaces the file) so an
2481
+ * unchanged length and header mean unchanged bytes, and anything else is `undefined` (not provably
2482
+ * damaged — decline, and let the next probe or the repair sweep classify it).
1811
2483
  */
1812
2484
  function blobFileMissingOrIncomplete(blob) {
1813
2485
  try {
@@ -1828,16 +2500,13 @@ function blobFileMissingOrIncomplete(blob) {
1828
2500
  return undefined;
1829
2501
  }
1830
2502
  try {
1831
- const size = (0, node_fs_1.fstatSync)(fd).size;
1832
- if (size < HEADER_SIZE)
1833
- return true;
2503
+ const fileSize = (0, node_fs_1.fstatSync)(fd).size;
1834
2504
  const header = Buffer.allocUnsafe(HEADER_SIZE);
1835
- if ((0, node_fs_1.readSync)(fd, header, 0, HEADER_SIZE, 0) < HEADER_SIZE)
1836
- return true;
1837
- if (blobHeaderIndicatesIncomplete(header, size))
1838
- return true;
1839
- const descriptorSize = blob.size;
1840
- return descriptorSize !== undefined && header.readUIntBE(2, 6) !== descriptorSize;
2505
+ const verdict = classifyBlobFileByHeader(blob, fileSize, header, (0, node_fs_1.readSync)(fd, header, 0, HEADER_SIZE, 0));
2506
+ if (verdict !== null)
2507
+ return verdict;
2508
+ const probed = storageInfo.probedDamage;
2509
+ return probed?.fileSize === fileSize && probed.header.equals(header) ? true : undefined;
1841
2510
  }
1842
2511
  finally {
1843
2512
  (0, node_fs_1.closeSync)(fd);
@@ -2349,6 +3018,10 @@ function cleanupUnusedBlobs(blobs, retainedFileIds) {
2349
3018
  continue; // no file written, nothing to clean up
2350
3019
  if (retainedFileIds?.has(storageInfo.fileId))
2351
3020
  continue; // the committed record still references this blob
3021
+ // Tombstone the instance as soon as the deletion is DECIDED, not when the unlink is issued: the
3022
+ // unlink waits for an in-flight save to settle, and a re-store in that window would otherwise
3023
+ // mint a reference to a file that is already condemned (issue #2062).
3024
+ discardStorage(storageInfo);
2352
3025
  const settle = storageInfo.saving ?? Promise.resolve();
2353
3026
  settle.then(() => deleteBlob(blob), () => deleteBlob(blob) // even on save failure, attempt cleanup in case a partial file remains
2354
3027
  );
@@ -2402,7 +3075,7 @@ const copyingUnpacker = new msgpackr_1.Packr({ copyBuffers: true, mapsAsObjects:
2402
3075
  storageInfoForBlob.set(blob, {
2403
3076
  storageIndex: 0,
2404
3077
  fileId: null,
2405
- contentBuffer: blobInfo[1],
3078
+ contentBuffer: asBuffer(blobInfo[1]),
2406
3079
  });
2407
3080
  blob.size = blobInfo[1]?.length;
2408
3081
  }
@@ -2436,6 +3109,12 @@ const copyingUnpacker = new msgpackr_1.Packr({ copyBuffers: true, mapsAsObjects:
2436
3109
  throw new Error('Unable to save blob without file id');
2437
3110
  }
2438
3111
  storageInfo.recordId = encodeForStorageForRecordId;
3112
+ // Per-node hint only — a replication sender uses it to skip the header sniff for the
3113
+ // uncompressed majority, but always confirms against the local file header before sending
3114
+ // raw: a relayed record (or an in-place repair) can outlive the storage form recorded here.
3115
+ // Namespaced away from `codec`, which a media blob plausibly carries as its own property.
3116
+ if (storageInfo.compress || storageInfo.storedCodec)
3117
+ options.storedCodec = 'deflate';
2439
3118
  // A record version being written now references this file, so any reclamation queued by an
2440
3119
  // earlier supersession is void — the retain-on-update check in RecordEncoder only covers the
2441
3120
  // write that supersedes, not a file already awaiting reclamation from a previous one.
@@ -2548,7 +3227,7 @@ function polyfillBlob() {
2548
3227
  * @param database
2549
3228
  */
2550
3229
  async function cleanupOrphans(database, databaseName) {
2551
- const { HAS_BLOBS } = await import("./auditStore.js");
3230
+ const { HAS_BLOBS, isAuditEntryWrite } = await import("./auditStore.js");
2552
3231
  let store;
2553
3232
  let auditStore;
2554
3233
  let orphansDeleted = 0;
@@ -2632,7 +3311,10 @@ async function cleanupOrphans(database, databaseName) {
2632
3311
  if (!primaryStore)
2633
3312
  continue;
2634
3313
  const entry = primaryStore?.getEntry(auditRecord.recordId);
2635
- if (!entry || entry.version !== auditRecord.version || !entry.value) {
3314
+ // Only the write this audit record describes had its blobs scanned by the table loop above.
3315
+ // Identity is the log key, not the version: a version match can name a different write and
3316
+ // would skip an audit value whose blobs are still referenced.
3317
+ if (!entry?.value || !isAuditEntryWrite(entry, auditRecord)) {
2636
3318
  checkObjectForReferences(auditRecord.getValue(primaryStore));
2637
3319
  }
2638
3320
  // slow this down a bit to reduce excessive load, this runs approximately at 10k per second
@@ -2697,10 +3379,11 @@ async function cleanupOrphans(database, databaseName) {
2697
3379
  }
2698
3380
  }
2699
3381
  async function isBlobFileComplete(storageInfo) {
2700
- let filePath;
3382
+ return isBlobFileCompleteAtPath(getFilePath(storageInfo));
3383
+ }
3384
+ async function isBlobFileCompleteAtPath(filePath) {
2701
3385
  let fileSize;
2702
3386
  try {
2703
- filePath = getFilePath(storageInfo);
2704
3387
  fileSize = (0, node_fs_1.statSync)(filePath).size;
2705
3388
  }
2706
3389
  catch (e) {
@@ -2726,32 +3409,56 @@ async function isBlobFileComplete(storageInfo) {
2726
3409
  // uncompressed byte count). For an uncompressed blob that equals the on-disk body length;
2727
3410
  // for a compressed blob it does not, so the body length can't be compared to it directly.
2728
3411
  const size = Number(headerValue & 0xffffffffffffn);
2729
- if (header[1] === DEFLATE_TYPE) {
2730
- // A compressed blob's header size is the uncompressed length, so it can't be compared to the
2731
- // compressed on-disk body. Verify by streaming the body through inflate and counting the
2732
- // decompressed bytes: a fully-written deflate stream inflates to exactly `size` bytes; a
2733
- // truncated one errors (Z_BUF_ERROR) or yields fewer. Streaming (rather than inflateSync on
2734
- // the whole buffer) keeps memory bounded during the repair sweep, which may touch many large
2735
- // blobs.
2736
- return new Promise((resolve) => {
2737
- let inflatedLength = 0;
2738
- const source = (0, node_fs_1.createReadStream)(filePath, { start: HEADER_SIZE });
2739
- const inflate = (0, node_zlib_1.createInflate)();
2740
- const fail = () => {
2741
- source.destroy();
2742
- resolve(false);
2743
- };
2744
- source.on('error', fail);
2745
- inflate.on('error', fail);
2746
- inflate.on('data', (chunk) => {
2747
- inflatedLength += chunk.length;
2748
- });
2749
- inflate.on('end', () => resolve(inflatedLength === size));
2750
- source.pipe(inflate);
2751
- });
3412
+ if (header[1] === exports.DEFLATE_TYPE) {
3413
+ // This function's contract is to resolve true/false; harper-pro's repair sweep awaits it without a
3414
+ // catch (replication/blobRepair.ts), so an I/O fault must not become a rejection here. The capture
3415
+ // classifier calls inflatesToExactly directly, where the distinction does matter.
3416
+ return inflatesToExactly(filePath, size).catch(() => false);
2752
3417
  }
2753
3418
  return true;
2754
3419
  }
3420
+ /**
3421
+ * Whether the deflate body after the header inflates to exactly `size` bytes. A compressed blob's
3422
+ * header records the uncompressed length, so it cannot be compared to the on-disk body; a truncated
3423
+ * stream errors (Z_BUF_ERROR) or yields fewer bytes. Streamed rather than inflateSync so memory stays
3424
+ * bounded over a sweep that may touch many large blobs, and abandoned as soon as the output passes
3425
+ * `size` so a body that lies about its length costs no more than the length it claimed.
3426
+ */
3427
+ function inflatesToExactly(filePath, size) {
3428
+ return new Promise((resolve, reject) => {
3429
+ let inflatedLength = 0;
3430
+ const source = (0, node_fs_1.createReadStream)(filePath, { start: HEADER_SIZE });
3431
+ const inflate = (0, node_zlib_1.createInflate)();
3432
+ // Both: pipe() does not tear down the destination when the source errors, so the inflate's
3433
+ // native zlib handle would sit allocated until GC — once per blob, on every backup walk.
3434
+ const stop = () => {
3435
+ source.destroy();
3436
+ inflate.destroy();
3437
+ };
3438
+ // A zlib error is the answer (body truncated or corrupt); an I/O error is a failure to answer and
3439
+ // must propagate, or a systemic fault would classify a corpus of complete blobs as incomplete.
3440
+ // The reject arm is unverified: it needs a read fault raised mid-inflate on a file that opened
3441
+ // cleanly, which no test here can produce.
3442
+ const fail = (error) => {
3443
+ stop();
3444
+ if (error?.code?.startsWith('Z_'))
3445
+ resolve(false);
3446
+ else
3447
+ reject(error);
3448
+ };
3449
+ source.on('error', fail);
3450
+ inflate.on('error', fail);
3451
+ inflate.on('data', (chunk) => {
3452
+ inflatedLength += chunk.length;
3453
+ if (inflatedLength > size) {
3454
+ stop();
3455
+ resolve(false);
3456
+ }
3457
+ });
3458
+ inflate.on('end', () => resolve(inflatedLength === size));
3459
+ source.pipe(inflate);
3460
+ });
3461
+ }
2755
3462
  /**
2756
3463
  * Resolves true if the given blob's backing file is present and complete: no error stub, not an
2757
3464
  * in-flight placeholder, and the content matches the header (uncompressed blobs by body length,
@@ -2766,6 +3473,177 @@ async function isBlobComplete(blob) {
2766
3473
  return false;
2767
3474
  return isBlobFileComplete(storageInfo);
2768
3475
  }
3476
+ /**
3477
+ * Open a blob's STORED body for transfer, without inflating it — the sender half of replication's
3478
+ * codec preservation. Returns undefined whenever the stored form is not a settled deflate file
3479
+ * (uncompressed, sliced, still being written, missing, or already condemned); the caller then falls
3480
+ * back to the ordinary content stream, which knows how to wait and how to report errors.
3481
+ *
3482
+ * Deliberately synchronous through the decision: the caller decides raw-vs-inflated inside the same
3483
+ * synchronous block that puts the owning record frame on the wire. The stream opens its own
3484
+ * descriptor later and re-reads the header first: a settled file's header never changes except by
3485
+ * a repair publishing a fresh (uncompressed) file over the path, so a header that differs from the
3486
+ * sniffed one means the announced form is gone — reported as transient (503), since a retry
3487
+ * re-decides against the new file. The stream verifies as it sends — a concurrent inflate must
3488
+ * land on exactly `size` bytes — so a torn body that survived the header checks becomes a loud,
3489
+ * permanently-classified error instead of replicated garbage.
3490
+ */
3491
+ function openStoredBlobBody(blob) {
3492
+ if (!(blob instanceof FileBackedBlob))
3493
+ return undefined;
3494
+ const storageInfo = storageInfoForBlob.get(blob);
3495
+ if (!storageInfo?.fileId || !storageInfo.store)
3496
+ return undefined;
3497
+ if (storageInfo.start !== undefined || storageInfo.end !== undefined)
3498
+ return undefined;
3499
+ if (storageInfo.fileState?.discarded)
3500
+ return undefined;
3501
+ const releaseHold = holdBlobFile(blob);
3502
+ if (!releaseHold)
3503
+ return undefined; // already being reclaimed
3504
+ let fd;
3505
+ let filePath;
3506
+ const decline = () => {
3507
+ if (fd !== undefined) {
3508
+ try {
3509
+ (0, node_fs_1.closeSync)(fd);
3510
+ }
3511
+ catch (error) {
3512
+ logger_ts_1.logger.debug?.('Error closing stored blob body descriptor', error);
3513
+ }
3514
+ }
3515
+ releaseHold();
3516
+ return undefined;
3517
+ };
3518
+ let size;
3519
+ const header = Buffer.allocUnsafe(HEADER_SIZE);
3520
+ try {
3521
+ const lockKey = storageInfo.fileId + ':blob';
3522
+ // A held write lock means the body may still be streaming to disk; the normal read path knows
3523
+ // how to wait for it, so decline rather than duplicate that machinery here.
3524
+ if (!storageInfo.store.tryLock(lockKey))
3525
+ return decline();
3526
+ storageInfo.store.unlock(lockKey);
3527
+ filePath = getFilePath(storageInfo);
3528
+ fd = (0, node_fs_1.openSync)(filePath, 'r');
3529
+ if ((0, node_fs_1.readSync)(fd, header, 0, HEADER_SIZE, 0) < HEADER_SIZE)
3530
+ return decline();
3531
+ const headerValue = new DataView(header.buffer, header.byteOffset, HEADER_SIZE).getBigUint64(0);
3532
+ if (Number(headerValue >> 48n) !== exports.DEFLATE_TYPE)
3533
+ return decline();
3534
+ size = Number(headerValue & 0xffffffffffffn);
3535
+ if (size === UNKNOWN_SIZE)
3536
+ return decline();
3537
+ const descriptorSize = blob.size;
3538
+ if (descriptorSize !== undefined && descriptorSize < UNKNOWN_SIZE && descriptorSize !== size)
3539
+ return decline();
3540
+ (0, node_fs_1.closeSync)(fd);
3541
+ fd = undefined;
3542
+ }
3543
+ catch (error) {
3544
+ logger_ts_1.logger.debug?.('Unable to open stored blob body', storageInfo.fileId, error);
3545
+ return decline();
3546
+ }
3547
+ let holdReleased = false;
3548
+ const releaseHoldOnce = () => {
3549
+ if (holdReleased)
3550
+ return;
3551
+ holdReleased = true;
3552
+ releaseHold();
3553
+ };
3554
+ let streamStarted = false;
3555
+ const failVerification = (detail) => new BlobReadError(`Stored blob body for ${filePath} ${detail}`, BLOB_CORRUPT_STATUS);
3556
+ return {
3557
+ codec: 'deflate',
3558
+ size,
3559
+ close: () => {
3560
+ streamStarted = true;
3561
+ releaseHoldOnce();
3562
+ },
3563
+ stream: async function* () {
3564
+ if (streamStarted)
3565
+ throw new Error(`Stored blob body for ${filePath} was already consumed or closed`);
3566
+ streamStarted = true;
3567
+ let streamFd;
3568
+ let source;
3569
+ try {
3570
+ streamFd = (0, node_fs_1.openSync)(filePath, 'r');
3571
+ const currentHeader = Buffer.allocUnsafe(HEADER_SIZE);
3572
+ const headerBytes = (0, node_fs_1.readSync)(streamFd, currentHeader, 0, HEADER_SIZE, 0);
3573
+ if (headerBytes < HEADER_SIZE || !currentHeader.equals(header)) {
3574
+ throw new BlobReadError(`Stored blob body for ${filePath} was replaced before it was streamed`, exports.BLOB_UNAVAILABLE_STATUS);
3575
+ }
3576
+ // the stream owns the descriptor from here (an fs stream's worker-thread close(2) races any other user)
3577
+ source = (0, node_fs_1.createReadStream)(filePath, { fd: streamFd, start: HEADER_SIZE, autoClose: true });
3578
+ }
3579
+ catch (error) {
3580
+ if (streamFd !== undefined)
3581
+ (0, node_fs_1.closeSync)(streamFd);
3582
+ releaseHoldOnce();
3583
+ throw error;
3584
+ }
3585
+ const verifier = (0, node_zlib_1.createInflate)();
3586
+ let inflated = 0;
3587
+ let verifyFailure;
3588
+ // A mid-write zlib data error (a corrupt, not merely truncated, body) fires 'error'/'close'
3589
+ // but drops the pending `verifier.write` callback, so the per-chunk await below would hang
3590
+ // forever — suspending this generator inside the try and leaking the descriptor and the file
3591
+ // hold its finally releases. settleWrite() unblocks it from error/close too; whichever runs
3592
+ // first resolves the promise and clears the ref, so the write callback is a harmless no-op.
3593
+ let resolvePendingWrite;
3594
+ const settleWrite = () => {
3595
+ const resolve = resolvePendingWrite;
3596
+ resolvePendingWrite = undefined;
3597
+ resolve?.();
3598
+ };
3599
+ verifier.on('data', (chunk) => {
3600
+ inflated += chunk.length;
3601
+ if (inflated > size && !verifyFailure) {
3602
+ verifyFailure = failVerification(`inflates past its declared size of ${size}`);
3603
+ verifier.destroy();
3604
+ }
3605
+ });
3606
+ verifier.on('error', (error) => {
3607
+ verifyFailure ??= failVerification(`is not a valid deflate stream: ${error.message}`);
3608
+ settleWrite();
3609
+ });
3610
+ verifier.on('close', settleWrite); // a destroy() with no 'error' (oversize) also drops the write callback
3611
+ try {
3612
+ for await (const chunk of source) {
3613
+ if (verifyFailure)
3614
+ throw verifyFailure;
3615
+ if (!verifier.destroyed) {
3616
+ // Yield only once the verifier consumed the chunk, so a detection surfaces before the
3617
+ // bytes it condemns are handed to the caller's wire frame (and never after the caller
3618
+ // has already seen a clean end).
3619
+ await new Promise((resolveWrite) => {
3620
+ resolvePendingWrite = resolveWrite;
3621
+ verifier.write(chunk, settleWrite);
3622
+ });
3623
+ if (verifyFailure)
3624
+ throw verifyFailure;
3625
+ }
3626
+ yield chunk;
3627
+ }
3628
+ if (!verifier.destroyed) {
3629
+ await new Promise((resolveEnd) => {
3630
+ verifier.on('close', resolveEnd);
3631
+ verifier.end();
3632
+ });
3633
+ }
3634
+ if (verifyFailure)
3635
+ throw verifyFailure;
3636
+ if (inflated !== size)
3637
+ throw failVerification(`inflated to ${inflated} bytes of a declared ${size}`);
3638
+ }
3639
+ finally {
3640
+ source.destroy();
3641
+ verifier.destroy();
3642
+ releaseHoldOnce();
3643
+ }
3644
+ },
3645
+ };
3646
+ }
2769
3647
  /**
2770
3648
  * Async generator that yields records whose referenced blob files are missing, truncated, or in an
2771
3649
  * error state. Used by the blob repair sweep to find candidates for peer-fetched recovery.