@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
@@ -5,7 +5,7 @@
5
5
  const { realExit } = require("./workerProcessGuard.js");
6
6
  const { Worker, MessageChannel, parentPort, isMainThread, threadId, workerData } = require('worker_threads');
7
7
  const { spawn, spawnSync } = require('node:child_process');
8
- const { readFileSync } = require('node:fs');
8
+ const { readdirSync, readFileSync, readlinkSync } = require('node:fs');
9
9
  const { setTimeout: delay } = require('node:timers/promises');
10
10
  const { join, isAbsolute, extname } = require('path');
11
11
  const { pathToFileURL } = require('url');
@@ -19,7 +19,10 @@ const { randomBytes } = require('crypto');
19
19
  const { _assignPackageExport } = require('../../globals.js');
20
20
  const { PACKAGE_ROOT } = require('../../utility/packageUtils.js');
21
21
  const { resolvePreloadModules } = require("./resolvePreload.js");
22
+ const { resolveThreadHeapMemoryMb } = require("./threadHeapMemory.js");
22
23
  const { getConfigPath } = require("../../config/configUtils.js");
24
+ const { resolveWatchTarget } = require("../../utility/watchPath.js");
25
+ const { DIRECTORY_POLLING_FALLBACK_OPTIONS, claimLostNativeWatchError, guardedWatch, isWatcherExhaustionError, warnWatcherFallback, } = require("../../utility/watcherFallback.js");
23
26
  let importModules;
24
27
  function getImportModules() {
25
28
  if (importModules === undefined)
@@ -32,7 +35,6 @@ function getRequireModules() {
32
35
  requireModules = resolvePreloadModules(envMgr.get(hdbTerms.CONFIG_PARAMS.THREADS_PRELOADREQUIRE), getConfigPath(hdbTerms.CONFIG_PARAMS.COMPONENTSROOT), 'threads.preloadRequire');
33
36
  return requireModules;
34
37
  }
35
- const chokidar = require('chokidar');
36
38
  const isBun = typeof globalThis.Bun !== 'undefined';
37
39
  const MB = 1024 * 1024;
38
40
  const workers = []; // these are our child workers that we are managing
@@ -44,9 +46,14 @@ const MAX_UNEXPECTED_RESTARTS = 50;
44
46
  // down mid-disposal crashes the process, so give that teardown more room before the forced backstop.
45
47
  let threadTerminationTimeout = process.env.DEV_MODE === 'true' || process.env.DEV_MODE === '1' ? 30000 : 10000;
46
48
  const RESTART_TYPE = 'restart';
49
+ const RESTART_PROGRESS_HEARTBEAT_MS = 15000;
47
50
  const REQUEST_THREAD_INFO = 'request_thread_info';
51
+ const REQUEST_RUNNING_ISOLATED_APPLICATIONS = 'request-running-isolated-applications';
48
52
  const RESOURCE_REPORT = 'resource_report';
53
+ const OS_THREAD_ID = 'os-thread-id';
54
+ const STUCK_WORKER_REPORT = 'stuck-worker-report';
49
55
  const THREAD_INFO = 'thread_info';
56
+ const RUNNING_ISOLATED_APPLICATIONS = 'running-isolated-applications';
50
57
  const ADDED_PORT = 'added-port';
51
58
  const ACKNOWLEDGEMENT = 'ack';
52
59
  const REMOVE_PORT = 'remove-port';
@@ -64,6 +71,7 @@ const AWAIT_PROCESS_GROUP_TERMINATION = 'await-process-group-termination';
64
71
  const PROCESS_GROUP_TERMINATION_CONFIRMED = 'process-group-termination-confirmed';
65
72
  const THREAD_INFO_REQUEST_TIMEOUT_MS = 1000;
66
73
  let getThreadInfo;
74
+ let getRunningIsolatedApplications;
67
75
  let awaitProcessGroupTermination;
68
76
  // Worker-side backstop that force-exits if the graceful shutdown sequence doesn't finish in time.
69
77
  let selfExitTimer;
@@ -127,6 +135,19 @@ module.exports = {
127
135
  workers,
128
136
  setMonitorListener,
129
137
  onMessageFromWorkers,
138
+ setIsolatedWorkerReconciler,
139
+ setRunningIsolatedApplicationsGetter,
140
+ isApplicationPrimaryWorker,
141
+ applicationWorkerIndex,
142
+ runsApplicationCodeSingletons,
143
+ isDedicatedWorker,
144
+ markBranchStorePath,
145
+ ownsStoreMaintenance,
146
+ ownsStoreExpiration,
147
+ workersForApplication,
148
+ stopWorker,
149
+ encodeRestartScope,
150
+ decodeRestartScope,
130
151
  onMessageByType,
131
152
  broadcast,
132
153
  broadcastWithAcknowledgement,
@@ -141,8 +162,11 @@ module.exports = {
141
162
  beginProcessShutdown,
142
163
  registerWorkerDataProvider,
143
164
  onThreadExit,
165
+ hasThreadExited,
166
+ notifyThreadExit,
144
167
  registerProcessGroup,
145
168
  unregisterProcessGroup,
169
+ isProcessGroupAlive,
146
170
  isThreadRunning,
147
171
  waitUntilConfirmedGone,
148
172
  restartNumber: workerData?.restartNumber || 1,
@@ -172,6 +196,9 @@ connectedPorts.sendToThread = function (threadId, message) {
172
196
  throw err;
173
197
  }
174
198
  };
199
+ // Direct thread-to-thread send, so a worker can reach a sibling (e.g. the record lock owner worker)
200
+ // without a hop through main. Returns false when no port for the thread is connected.
201
+ module.exports.sendToThread = connectedPorts.sendToThread;
175
202
  module.exports.whenThreadsStarted = new Promise((resolve) => {
176
203
  module.exports.threadsHaveStarted = resolve;
177
204
  });
@@ -186,6 +213,106 @@ function setTerminateTimeout(newTimeout) {
186
213
  function getWorkerIndex() {
187
214
  return workerData ? workerData.workerIndex : isMainWorker ? 0 : undefined;
188
215
  }
216
+ /**
217
+ * The worker that owns `applicationName`'s per-application singletons (its scheduled jobs, its data
218
+ * files): pool worker 0 for a shared application and for root-level plugins (which pass no name), the
219
+ * dedicated worker for an isolated one -- never index 0, so node-wide duties stay on the pool, and
220
+ * never a dedicated worker for anything but its own application.
221
+ */
222
+ function isApplicationPrimaryWorker(applicationName) {
223
+ if (workerData?.isolatedApplication !== undefined)
224
+ return applicationName === workerData.isolatedApplication;
225
+ return getWorkerIndex() === 0;
226
+ }
227
+ /**
228
+ * Stores that exist only on this thread: the branch stores an isolated application opened in its
229
+ * dedicated worker. Registered by openBranchDatabase, so the ownership predicates below can tell them
230
+ * from the shared stores every thread has open.
231
+ */
232
+ const branchStorePaths = new Set();
233
+ function markBranchStorePath(path, isBranch = true) {
234
+ if (isBranch)
235
+ branchStorePaths.add(path);
236
+ else
237
+ branchStorePaths.delete(path);
238
+ }
239
+ /**
240
+ * Whether this thread runs the "last worker" per-store maintenance for `storePath` (TTL scans, storage
241
+ * reclamation, audit cleanup). Shared stores belong to the last pool worker; a dedicated worker
242
+ * maintains only its own branch stores, which no other thread has open.
243
+ */
244
+ function ownsStoreMaintenance(storePath) {
245
+ if (workerData?.isolatedApplication !== undefined)
246
+ return branchStorePaths.has(storePath);
247
+ return getWorkerIndex() === getWorkerCount() - 1;
248
+ }
249
+ /** The "worker 0" counterpart: expiration eviction for `storePath`. */
250
+ function ownsStoreExpiration(storePath) {
251
+ if (workerData?.isolatedApplication !== undefined)
252
+ return branchStorePaths.has(storePath);
253
+ return getWorkerIndex() === 0;
254
+ }
255
+ /**
256
+ * Whether a singleton set up by APPLICATION CODE runs here (a caching table's `sourcedFrom`
257
+ * subscription): pool worker 0, or a dedicated worker -- the only code that runs on one is its own
258
+ * application's, so whatever it sets up is that application's to run. Not for root-level plugins,
259
+ * which load on every thread and must use isApplicationPrimaryWorker(name).
260
+ */
261
+ function runsApplicationCodeSingletons() {
262
+ return getWorkerIndex() === 0 || workerData?.isolatedApplication !== undefined;
263
+ }
264
+ /** Whether this thread is a worker dedicated to one isolated application. */
265
+ function isDedicatedWorker() {
266
+ return workerData?.isolatedApplication !== undefined;
267
+ }
268
+ /** The worker index as the application sees it: a dedicated worker is its application's worker 0. */
269
+ function applicationWorkerIndex() {
270
+ return workerData?.isolatedApplication !== undefined ? 0 : getWorkerIndex();
271
+ }
272
+ /** Every started worker dedicated to `application`, including a replacement still booting. */
273
+ function workersForApplication(application) {
274
+ return workers.filter((worker) => worker.application === application);
275
+ }
276
+ /**
277
+ * Stop one worker for good: shut it down, honour the drain extension it may ask for, force it after
278
+ * the same backstop the rolling restart uses (FORCE_EXIT on Bun, where terminate() segfaults), and
279
+ * resolve once it has exited. Marked as shut down first so its exit does not start a replacement.
280
+ */
281
+ function stopWorker(worker) {
282
+ worker.wasShutdown = true;
283
+ return new Promise((resolve) => {
284
+ const armTerminate = (delay) => setTimeout(() => {
285
+ harperLogger.warn('Thread did not voluntarily terminate, terminating from the outside', worker.threadId);
286
+ if (isBun) {
287
+ try {
288
+ worker.postMessage({ type: FORCE_EXIT });
289
+ }
290
+ catch { }
291
+ }
292
+ else {
293
+ worker.terminate();
294
+ }
295
+ }, delay).unref();
296
+ let timeout = armTerminate(threadTerminationTimeout * 2);
297
+ worker.extendTerminateDeadline = (deadlineMs) => {
298
+ clearTimeout(timeout);
299
+ const { boundedTerminateDelay, getShutdownDrainCeilingMs } = require("../../components/shutdownDrain.js");
300
+ timeout = armTerminate(boundedTerminateDelay(deadlineMs, Date.now(), threadTerminationTimeout * 2, getShutdownDrainCeilingMs()));
301
+ };
302
+ worker.on('exit', () => {
303
+ clearTimeout(timeout);
304
+ worker.extendTerminateDeadline = undefined;
305
+ resolve();
306
+ });
307
+ try {
308
+ worker.postMessage({ restartNumber: module.exports.restartNumber, type: hdbTerms.ITC_EVENT_TYPES.SHUTDOWN });
309
+ }
310
+ catch {
311
+ clearTimeout(timeout);
312
+ resolve(); // already gone
313
+ }
314
+ });
315
+ }
189
316
  function getWorkerCount() {
190
317
  return workerData ? workerData.workerCount : isMainWorker ? 1 : undefined;
191
318
  }
@@ -221,6 +348,7 @@ const RESERVED_WORKER_DATA_KEYS = [
221
348
  'processIncarnation',
222
349
  'ticketKeys',
223
350
  'noServerStart',
351
+ 'isolatedApplication',
224
352
  '__proto__', // never a legitimate payload name; spread would define it as an own property
225
353
  ];
226
354
  const workerDataProviders = new Map();
@@ -280,14 +408,17 @@ function getTicketKeys() {
280
408
  ticketKeys = isMainThread ? randomBytes(48) : workerData.ticketKeys;
281
409
  return ticketKeys;
282
410
  }
411
+ // What application code sees: a dedicated worker is its application's only worker, index 0 of 1, so
412
+ // the documented partitioning idiom (`hash % server.workerCount === server.workerIndex`) covers
413
+ // everything there. Node-wide duties keep using the raw getWorkerIndex/getWorkerCount.
283
414
  Object.defineProperty(server, 'workerIndex', {
284
415
  get() {
285
- return getWorkerIndex();
416
+ return applicationWorkerIndex();
286
417
  },
287
418
  });
288
419
  Object.defineProperty(server, 'workerCount', {
289
420
  get() {
290
- return getWorkerCount();
421
+ return workerData?.isolatedApplication !== undefined ? 1 : getWorkerCount();
291
422
  },
292
423
  });
293
424
  if (!parentPort) {
@@ -295,10 +426,29 @@ if (!parentPort) {
295
426
  if (worker)
296
427
  sendThreadInfo(worker);
297
428
  });
429
+ onMessageByType(REQUEST_RUNNING_ISOLATED_APPLICATIONS, (message, worker) => {
430
+ if (worker)
431
+ worker.postMessage({
432
+ type: RUNNING_ISOLATED_APPLICATIONS,
433
+ requestId: message.requestId,
434
+ applications: runningIsolatedApplicationsGetter(),
435
+ });
436
+ });
298
437
  onMessageByType(RESOURCE_REPORT, (message, worker) => {
299
438
  if (worker)
300
439
  recordResourceReport(worker, message);
301
440
  });
441
+ onMessageByType(OS_THREAD_ID, (message, worker) => {
442
+ if (worker)
443
+ worker.osThreadId = message.osThreadId;
444
+ });
445
+ onMessageByType(STUCK_WORKER_REPORT, (message) => {
446
+ for (const threadId of message.threadIds) {
447
+ const worker = workers.find((worker) => worker.threadId === threadId);
448
+ if (worker)
449
+ logStuckWorkerDiagnostics(worker);
450
+ }
451
+ });
302
452
  onMessageByType(AWAIT_PROCESS_GROUP_TERMINATION, async (message, worker) => {
303
453
  if (!worker)
304
454
  return;
@@ -311,6 +461,7 @@ if (!parentPort) {
311
461
  }
312
462
  // postMessage type listeners that are registered in other ways or can be registered later
313
463
  listenersByType.set(hdbTerms.ITC_EVENT_TYPES.CHILD_STARTED, null);
464
+ listenersByType.set(hdbTerms.ITC_EVENT_TYPES.CHILD_STARTUP_PHASE, null);
314
465
  listenersByType.set(hdbTerms.ITC_EVENT_TYPES.SCHEMA, null);
315
466
  listenersByType.set(hdbTerms.ITC_EVENT_TYPES.USER, null);
316
467
  listenersByType.set(hdbTerms.ITC_EVENT_TYPES.COMPONENT_STATUS_REQUEST, null);
@@ -321,11 +472,12 @@ listenersByType.set(hdbTerms.ITC_EVENT_TYPES.MIDDLEWARE_CHAINS_RESPONSE, null);
321
472
  listenersByType.set(hdbTerms.ITC_EVENT_TYPES.OPERATION_REGISTERED, null);
322
473
  listenersByType.set(hdbTerms.ITC_EVENT_TYPES.OPERATION_EXECUTE_REQUEST, null);
323
474
  listenersByType.set(hdbTerms.ITC_EVENT_TYPES.OPERATION_EXECUTE_RESPONSE, null);
324
- // getThreadInfo/awaitProcessGroupTermination register their own one-shot parentPort listener per
475
+ // These request/response functions register their own one-shot parentPort listener per
325
476
  // call rather than going through onMessageByType, so without this, every reply would also reach
326
477
  // addPort's permanent dispatcher as an "unregistered" type: notifyMessageListeners would warn and
327
478
  // queue each one in messagesQueuedByType forever, and a lock-wait polls every 50ms.
328
479
  listenersByType.set(THREAD_INFO, null);
480
+ listenersByType.set(RUNNING_ISOLATED_APPLICATIONS, null);
329
481
  listenersByType.set(PROCESS_GROUP_TERMINATION_CONFIRMED, null);
330
482
  function startWorker(path, options = {}) {
331
483
  if (processShuttingDown) {
@@ -346,8 +498,8 @@ function startWorker(path, options = {}) {
346
498
  let availableMemory = process.constrainedMemory?.() || totalmem(); // used constrained memory if it is available
347
499
  // and lower than total memory
348
500
  availableMemory = Math.min(availableMemory, totalmem(), 20000 * MB);
349
- const maxOldMemory = envMgr.get(hdbTerms.CONFIG_PARAMS.THREADS_MAXHEAPMEMORY) ??
350
- Math.max(Math.floor(availableMemory / MB / (10 + (options.threadCount || 1) / 4)), 512);
501
+ const maxOldMemory = resolveThreadHeapMemoryMb(envMgr.get(hdbTerms.CONFIG_PARAMS.THREADS_MAXHEAPMEMORY)) ??
502
+ Math.max(Math.floor(availableMemory / MB / (10 + (options.heapShareCount || options.threadCount || 1) / 4)), 512);
351
503
  // Max young memory space (semi-space for scavenger) is 1/128 of max memory (limited to 16-64). For most of our m5
352
504
  // machines this will be 64MB (less for t3's). This is based on recommendations from:
353
505
  // https://www.alibabacloud.com/blog/node-js-application-troubleshooting-manual---comprehensive-gc-problems-and-optimization594965
@@ -391,6 +543,9 @@ function startWorker(path, options = {}) {
391
543
  for (const requirePath of getRequireModules())
392
544
  execArgv.push('--require', requirePath);
393
545
  }
546
+ // Only a start that declares the serving topology may write it; see DESIGN.md on the two workerCounts.
547
+ if (typeof options.threadCount === 'number')
548
+ workerCount = options.threadCount;
394
549
  const worker = new Worker(isAbsolute(path) ? path : join(PACKAGE_ROOT, path), {
395
550
  resourceLimits: {
396
551
  maxOldGenerationSizeMb: maxOldMemory,
@@ -405,8 +560,9 @@ function startWorker(path, options = {}) {
405
560
  addThreadIds: channelsToConnect.map((channel) => channel.existingPort.threadId),
406
561
  addPortIsJobWorkers: channelsToConnect.map((channel) => channel.existingPort.isJobWorker === true),
407
562
  workerIndex: options.workerIndex,
408
- workerCount: (workerCount = options.threadCount),
563
+ workerCount: options.threadCount,
409
564
  name: options.name,
565
+ isolatedApplication: options.application,
410
566
  restartNumber: module.exports.restartNumber,
411
567
  processIncarnation: module.exports.processIncarnation,
412
568
  ticketKeys: getTicketKeys(),
@@ -439,14 +595,19 @@ function startWorker(path, options = {}) {
439
595
  });
440
596
  worker.on('exit', (_code) => {
441
597
  workers.splice(workers.indexOf(worker), 1);
442
- if (!processShuttingDown && !worker.wasShutdown && options.autoRestart !== false) {
598
+ if (!processShuttingDown &&
599
+ !worker.wasShutdown &&
600
+ options.autoRestart !== false &&
601
+ options.shouldAutoRestart?.(worker) !== false) {
443
602
  // if this wasn't an intentional shutdown, restart now (unless we have tried too many times)
444
603
  if (worker.unexpectedRestarts < MAX_UNEXPECTED_RESTARTS) {
445
604
  options.unexpectedRestarts = worker.unexpectedRestarts + 1;
446
605
  startWorker(path, options);
447
606
  }
448
- else
449
- harperLogger.error(`Thread has been restarted ${worker.restarts} times and will not be restarted`);
607
+ else {
608
+ harperLogger.error(`Thread has been restarted ${worker.unexpectedRestarts} times and will not be restarted`);
609
+ options.onRestartExhausted?.(worker);
610
+ }
450
611
  }
451
612
  });
452
613
  workers.push(worker);
@@ -454,6 +615,7 @@ function startWorker(path, options = {}) {
454
615
  if (options.onStarted)
455
616
  options.onStarted(worker); // notify that it is ready
456
617
  worker.name = options.name;
618
+ worker.application = options.application; // the isolated application this worker is dedicated to, if any
457
619
  return worker;
458
620
  }
459
621
  const OVERLAPPING_RESTART_TYPES = [hdbTerms.THREAD_TYPES.HTTP];
@@ -465,13 +627,27 @@ const OVERLAPPING_RESTART_TYPES = [hdbTerms.THREAD_TYPES.HTTP];
465
627
  * threads at the same time we shutdown new ones. However, we usually want to limit how many we do at once to avoid
466
628
  * excessive load and to keep things responsive. This parameter throttles the restarts to minimize load from
467
629
  * thread startups.
468
- * @returns {Promise<void>}
630
+ * @param onProgress Called each time a worker has been replaced (or its replacement has been given
631
+ * up on), so a caller waiting on a wide pool can tell a slow restart from a stalled one.
632
+ * @returns {Promise<{workersKeptOnOldCode: number, replacementsNotStarted: number}|{declined: true}|undefined>}
633
+ * from the main thread, how many workers were left running the old code and how many replacements
634
+ * never reported that they started — or `{declined: true}` when the process is already shutting down;
635
+ * from a worker, nothing — the restart is handed to the main thread.
636
+ */
637
+ /**
638
+ * `application` selects which workers of the type restart: omitted preserves the historic all-worker
639
+ * behavior, explicit undefined restarts the shared pool only, a name restarts only that application's
640
+ * dedicated worker, and '*' restarts all.
469
641
  */
470
642
  async function restartWorkers(name = null, maxWorkersDown = Math.max(Math.floor(workerCount / 8), 1), // restart 1/8 of the threads at a time, but at least 1
471
- startReplacementThreads = true) {
643
+ startReplacementThreads = true, onProgress = null, application = undefined) {
644
+ if (arguments.length < 5)
645
+ application = '*';
472
646
  if (isMainThread) {
647
+ // Declining is not the same as delegating: a caller reporting on the restart must not read this
648
+ // as "another thread is completing it".
473
649
  if (processShuttingDown && startReplacementThreads)
474
- return;
650
+ return { declined: true };
475
651
  try {
476
652
  // we do this because it is possible for a component to chdir to itself, get re-deployed and then the cwd
477
653
  // inode link is invalid and it can cause a lot of problems. But process.cwd() still returns the path, for
@@ -481,22 +657,65 @@ startReplacementThreads = true) {
481
657
  catch (e) {
482
658
  harperLogger.error('Unable to reestablish current working directory', e);
483
659
  }
660
+ const freshlyStarted = new Set(); // dedicated workers the reconcile just started: already on the new code
484
661
  // problematic cyclic dependency, bind late
485
662
  const { resetRestartNeeded } = require("../../components/requestRestart.js");
486
- resetRestartNeeded();
663
+ // One process-wide bit cannot say WHICH application is pending, so it may only be cleared by a
664
+ // restart that demonstrably covers every worker the bit could stand for. A restart scoped to one
665
+ // application leaves the whole pool on its old code; a pool restart replaces every pool worker and
666
+ // reconciles dedicated slots, but leaves an already-running dedicated worker on its old modules.
667
+ // Clearing from either would report restartRequired: false while a deployed component is still
668
+ // loaded nowhere. So: all workers always, the pool only while no dedicated worker is running --
669
+ // which is every node that uses no isolated application, i.e. the historic behavior unchanged.
670
+ const coversEveryWorker = application === '*' || (application === undefined && !workers.some((worker) => worker.application));
671
+ if (coversEveryWorker)
672
+ resetRestartNeeded();
487
673
  // This is here to prevent circular dependencies
488
674
  if (startReplacementThreads) {
489
675
  const { loadRootComponents } = require('../loadRootComponents.js');
490
- await loadRootComponents();
676
+ // Installing and loading every root component reports nothing and can outlast a caller's idle
677
+ // window on its own (a cold npm cache, a large dependency graph), so beat while it runs. The
678
+ // caller's absolute ceiling is what bounds a load that never finishes.
679
+ const loading = setInterval(() => onProgress?.(), RESTART_PROGRESS_HEARTBEAT_MS).unref();
680
+ try {
681
+ await loadRootComponents();
682
+ // isolated applications added or removed by the reload get their dedicated worker started or
683
+ // stopped; a crash-looping newcomer can take a while, so the heartbeat runs through this too
684
+ try {
685
+ for (const application of (await isolatedWorkerReconciler?.()) ?? [])
686
+ freshlyStarted.add(application);
687
+ }
688
+ catch (error) {
689
+ harperLogger.error('Could not reconcile isolated application workers', error);
690
+ }
691
+ }
692
+ finally {
693
+ clearInterval(loading);
694
+ }
695
+ onProgress?.();
491
696
  }
492
697
  module.exports.restartNumber++;
493
- if (maxWorkersDown < 1) {
698
+ // `Infinity` is shutdownWorkers' "all at once" sentinel and must survive.
699
+ if (typeof maxWorkersDown !== 'number' || Number.isNaN(maxWorkersDown)) {
700
+ maxWorkersDown = 1;
701
+ }
702
+ else if (maxWorkersDown < 1) {
494
703
  // we accept a ratio of workers, and compute absolute maximum being down at a time from the total number of
495
704
  // threads
496
705
  maxWorkersDown = maxWorkersDown * workers.length;
497
706
  }
498
707
  // make a copy of the workers before iterating them, as the workers array mutates a lot during this
499
- let waitingToFinish = []; // promises for workers we have shut down and are waiting to exit
708
+ let waitingToFinish = []; // promises for workers we are replacing, spliced as each is replaced
709
+ // Every replacement that was started without being awaited first, so this function can still
710
+ // resolve only once each one is accepting connections.
711
+ let replacementsStarting = [];
712
+ // A replacement that never came up leaves the pool in one of two very different states, and a
713
+ // caller waiting on this restart needs them apart: the pre-start path keeps the *old* worker
714
+ // serving old code, while a replacement that exits after its predecessor is already gone only
715
+ // costs capacity until startWorker's auto-restart brings a fresh one up.
716
+ let workersKeptOnOldCode = 0;
717
+ let replacementsNotStarted = 0;
718
+ let replacementsFailedToStart = 0;
500
719
  // We can only start the replacement *before* the old worker releases its port when the OS lets
501
720
  // both listen on the same port at once (SO_REUSEPORT). Without that — Windows (no SO_REUSEPORT),
502
721
  // macOS (unreliable SO_REUSEPORT, so workers bind exclusively), and Bun — the replacement can't
@@ -507,14 +726,21 @@ startReplacementThreads = true) {
507
726
  // replacement — an unavoidable brief gap, but only for worker-owned listeners, since the main
508
727
  // thread keeps serving the HTTP ports throughout. This ordering is also what lets
509
728
  // listenOnPorts() treat a dedicated listener's EADDRINUSE as an external conflict.
510
- const canPreStartReplacement = process.platform !== 'win32' && process.platform !== 'darwin' && !isBun;
511
- for (let worker of workers.slice(0)) {
729
+ const platformCanPreStartReplacement = process.platform !== 'win32' && process.platform !== 'darwin' && !isBun;
730
+ const restarting = workers.slice(0);
731
+ for (let index = 0; index < restarting.length; index++) {
732
+ const worker = restarting[index];
512
733
  // Terminal shutdown: stop replacing workers mid-loop — the guard for every replacement start below.
513
734
  if (processShuttingDown && startReplacementThreads)
514
735
  break;
515
736
  if ((name && worker.name !== name) || worker.wasShutdown)
516
737
  continue; // filter by type, if specified
738
+ if (application !== '*' && worker.application !== application)
739
+ continue; // and by isolated application
740
+ if (worker.application && freshlyStarted.has(worker.application))
741
+ continue;
517
742
  const overlapping = OVERLAPPING_RESTART_TYPES.indexOf(worker.name) > -1;
743
+ const canPreStartReplacement = platformCanPreStartReplacement && !worker.application;
518
744
  if (overlapping && startReplacementThreads && canPreStartReplacement) {
519
745
  // Overlapping restart: start the replacement and wait until it is accepting connections
520
746
  // *before* shutting down the worker it replaces. The replacement joins the (SO_REUSEPORT)
@@ -569,6 +795,8 @@ startReplacementThreads = true) {
569
795
  // protection if it is still alive (it may have exited on its own during the wait).
570
796
  if (workers.includes(worker))
571
797
  worker.wasShutdown = false;
798
+ workersKeptOnOldCode++;
799
+ onProgress?.();
572
800
  continue;
573
801
  }
574
802
  }
@@ -591,8 +819,16 @@ startReplacementThreads = true) {
591
819
  // Overlapping types we couldn't pre-start (Windows/Bun): start the replacement now that the old
592
820
  // worker is releasing its port. server.close() stops accepting immediately, so the port frees up
593
821
  // well before the replacement finishes booting and binds.
594
- if (overlapping && startReplacementThreads && !canPreStartReplacement && !processShuttingDown)
595
- worker.startCopy();
822
+ let replacementStarting;
823
+ if (overlapping && startReplacementThreads && !canPreStartReplacement && !processShuttingDown) {
824
+ replacementStarting = whenWorkerStarted(worker.startCopy()).then((started) => {
825
+ if (!started)
826
+ replacementsFailedToStart++;
827
+ onProgress?.();
828
+ return started;
829
+ });
830
+ replacementsStarting.push(replacementStarting);
831
+ }
596
832
  let whenDone = new Promise((resolve) => {
597
833
  // in case the exit inside the thread doesn't timeout, force it from the outside
598
834
  const armTerminate = (delay) => setTimeout(() => {
@@ -620,39 +856,113 @@ startReplacementThreads = true) {
620
856
  // buggy/rogue message can't defer the force-kill unboundedly; a shrink (drain-done reset)
621
857
  // passes through untouched. See boundedTerminateDelay for the arithmetic + its unit tests.
622
858
  const { boundedTerminateDelay, getShutdownDrainCeilingMs } = require("../../components/shutdownDrain.js");
623
- timeout = armTerminate(boundedTerminateDelay(deadlineMs, Date.now(), threadTerminationTimeout * 2, getShutdownDrainCeilingMs()));
859
+ const delay = boundedTerminateDelay(deadlineMs, Date.now(), threadTerminationTimeout * 2, getShutdownDrainCeilingMs());
860
+ timeout = armTerminate(delay);
861
+ // The worker is telling us it has work still moving and how long it may take, so pass that
862
+ // on: a caller waiting on the restart must not treat a live drain as a stalled one.
863
+ onProgress?.(Date.now() + delay);
624
864
  };
625
865
  worker.on('exit', () => {
626
866
  clearTimeout(timeout);
867
+ onProgress?.();
627
868
  worker.extendTerminateDeadline = undefined;
628
- const index = waitingToFinish.indexOf(whenDone);
629
- if (index > -1)
630
- waitingToFinish.splice(index, 1);
631
869
  // non-overlapping types have no advance replacement, so start it once the old one is gone
632
870
  if (!overlapping && startReplacementThreads && !processShuttingDown)
633
871
  worker.startCopy();
634
872
  resolve();
635
873
  });
636
874
  });
637
- waitingToFinish.push(whenDone);
875
+ // A worker counts as replaced only once its replacement is accepting connections, not merely
876
+ // once it has exited. This promise is held unawaited between throttle points, so it must not
877
+ // be able to reject.
878
+ const replaced = (replacementStarting ? Promise.all([whenDone, replacementStarting]) : whenDone)
879
+ .catch((error) => harperLogger.warn('Error waiting for a worker to be replaced', error))
880
+ .then(() => {
881
+ const at = waitingToFinish.indexOf(replaced);
882
+ if (at > -1)
883
+ waitingToFinish.splice(at, 1);
884
+ });
885
+ waitingToFinish.push(replaced);
638
886
  if (waitingToFinish.length >= maxWorkersDown) {
639
- // throttle how many workers are draining/down at once to limit load
887
+ // throttle how many workers are down at once to limit load
640
888
  await Promise.race(waitingToFinish);
641
889
  }
890
+ // Readiness throttling bounds how many workers are down at once, but not how many *fail*: with
891
+ // replacements that never come up, walking the rest of the pool would leave nothing serving.
892
+ // The workers not yet touched are still running the old code, which beats none running at all.
893
+ if (replacementsFailedToStart >= maxWorkersDown) {
894
+ const untouched = restarting
895
+ .slice(index + 1)
896
+ // a worker that exited on its own mid-restart is spliced out of `workers` and
897
+ // auto-restarted onto the new code (see the exit handler above); it is not still on
898
+ // the previous code even though this loop never got to it.
899
+ .filter((other) => (!name || other.name === name) && !other.wasShutdown && workers.includes(other)).length;
900
+ harperLogger.error(`${replacementsFailedToStart} replacement worker thread(s) did not start; stopping this restart with ${untouched} worker(s) still on the previous code`);
901
+ workersKeptOnOldCode += untouched;
902
+ break;
903
+ }
642
904
  }
643
- // seems appropriate to wait for this to finish, but the API doesn't actually wait for this function
644
- // to finish, so not that important
645
905
  await Promise.all(waitingToFinish);
906
+ // A caller awaiting this needs it to mean "the pool is serving the new code", so wait out the
907
+ // replacements that could only be started once their predecessor released its exclusive ports.
908
+ replacementsNotStarted = (await Promise.all(replacementsStarting)).filter((started) => !started).length;
909
+ return { workersKeptOnOldCode, replacementsNotStarted };
646
910
  }
647
911
  else {
648
912
  parentPort.postMessage({
649
913
  type: RESTART_TYPE,
650
914
  workerType: name,
915
+ scope: encodeRestartScope(application),
651
916
  });
652
917
  }
653
918
  }
919
+ /**
920
+ * Resolve once a newly started worker reports that it is accepting connections, or gives up on it.
921
+ * There is no old worker left to fall back on here, so a replacement that fails to start is left to
922
+ * startWorker's own auto-restart handling; this only stops waiting on it.
923
+ * @param newWorker The replacement worker
924
+ * @returns {Promise<boolean>} whether the worker reported that it started
925
+ */
926
+ function whenWorkerStarted(newWorker) {
927
+ return new Promise((resolve) => {
928
+ const cleanup = () => {
929
+ clearTimeout(timeout);
930
+ newWorker.off('message', startListener);
931
+ newWorker.off('exit', exitListener);
932
+ };
933
+ const timeout = setTimeout(() => {
934
+ harperLogger.error('Replacement worker did not start in time', newWorker.threadId);
935
+ cleanup();
936
+ // Its predecessor is already gone, so a replacement wedged in boot is a worker slot serving
937
+ // nothing until the process restarts. Stop it and let startWorker's exit handling replace it.
938
+ if (isBun) {
939
+ // terminate() triggers a NAPI segfault in Bun; ask the worker to self-exit instead.
940
+ try {
941
+ newWorker.postMessage({ type: FORCE_EXIT });
942
+ }
943
+ catch { }
944
+ }
945
+ else
946
+ newWorker.terminate();
947
+ resolve(false);
948
+ }, Math.max(threadTerminationTimeout * 2, 60000)).unref();
949
+ const startListener = (message) => {
950
+ if (message.type === hdbTerms.ITC_EVENT_TYPES.CHILD_STARTED) {
951
+ cleanup();
952
+ resolve(true);
953
+ }
954
+ };
955
+ const exitListener = () => {
956
+ harperLogger.warn('Replacement worker exited before starting', newWorker.threadId);
957
+ cleanup();
958
+ resolve(false);
959
+ };
960
+ newWorker.on('message', startListener);
961
+ newWorker.on('exit', exitListener);
962
+ });
963
+ }
654
964
  function shutdownWorkers(name) {
655
- return restartWorkers(name, Infinity, false);
965
+ return restartWorkers(name, Infinity, false, null, '*');
656
966
  }
657
967
  function beginProcessShutdown() {
658
968
  processShuttingDown = true;
@@ -674,6 +984,27 @@ async function shutdownWorkersNow(name) {
674
984
  await Promise.all(workers.map((worker) => worker.terminate()));
675
985
  }
676
986
  }
987
+ /**
988
+ * The restart scope on the wire: a worker cannot post `undefined` and have it mean "the pool" (a
989
+ * message with no scope at all means "everything", as every pre-existing sender intends).
990
+ */
991
+ function encodeRestartScope(application) {
992
+ return application === undefined ? '' : application; // '' is not a legal application name
993
+ }
994
+ function decodeRestartScope(message) {
995
+ if (message.scope === undefined)
996
+ return '*';
997
+ return message.scope === '' ? undefined : message.scope;
998
+ }
999
+ let isolatedWorkerReconciler = null;
1000
+ let runningIsolatedApplicationsGetter = () => [];
1001
+ /** Registered by socketRouter: starts and stops dedicated workers to match the root config's isolated applications. */
1002
+ function setIsolatedWorkerReconciler(reconcile) {
1003
+ isolatedWorkerReconciler = reconcile;
1004
+ }
1005
+ function setRunningIsolatedApplicationsGetter(getter) {
1006
+ runningIsolatedApplicationsGetter = getter;
1007
+ }
677
1008
  const messageListeners = [];
678
1009
  function onMessageFromWorkers(listener) {
679
1010
  messageListeners.push(listener);
@@ -784,15 +1115,146 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
784
1115
  timer = undefined;
785
1116
  const stuck = [];
786
1117
  for (let ackHandler of [...pending]) {
787
- stuck.push(ackHandler.port?.threadId);
1118
+ stuck.push(ackHandler.port);
788
1119
  ackHandler(); // same cleanup path as an ack/close; drives waitingCount to 0 and resolves
789
1120
  }
790
- harperLogger.warn(`ITC broadcast (type ${message.type}) not acknowledged by worker thread(s) ${stuck.join(', ')} within ${timeout}ms; proceeding best-effort`);
1121
+ harperLogger.warn(`ITC broadcast (type ${message.type}) not acknowledged by worker thread(s) ${stuck.map((port) => port?.threadId).join(', ')} within ${timeout}ms; proceeding best-effort`);
1122
+ if (isMainThread)
1123
+ for (let port of stuck)
1124
+ logStuckWorkerDiagnostics(port);
1125
+ else if (parentPort) {
1126
+ // Only main holds the Worker objects (and tids), so a worker-originated timeout is sampled there.
1127
+ const threadIds = stuck.map((port) => port?.threadId).filter((threadId) => threadId > 0);
1128
+ if (threadIds.length > 0)
1129
+ parentPort.postMessage({ type: STUCK_WORKER_REPORT, threadIds });
1130
+ }
791
1131
  }, timeout);
792
1132
  timer.unref?.();
793
1133
  }
794
1134
  });
795
1135
  }
1136
+ // Linux only: /proc/thread-self resolves to <pid>/task/<tid> for the calling thread.
1137
+ function getOsThreadId() {
1138
+ try {
1139
+ const tid = Number(readlinkSync('/proc/thread-self').split('/').pop());
1140
+ return Number.isInteger(tid) && tid > 0 ? tid : undefined;
1141
+ }
1142
+ catch {
1143
+ return undefined;
1144
+ }
1145
+ }
1146
+ function readTaskFile(tid, name) {
1147
+ try {
1148
+ return readFileSync(`/proc/self/task/${tid}/${name}`, 'utf8').trim();
1149
+ }
1150
+ catch {
1151
+ return undefined;
1152
+ }
1153
+ }
1154
+ // Kernel-side view of one thread. Every field is best-effort and reported individually, since
1155
+ // wchan/syscall need ptrace read access that a hardened container may deny while stat is open.
1156
+ function finiteOrUndefined(value) {
1157
+ const number = Number(value);
1158
+ return Number.isFinite(number) ? number : undefined;
1159
+ }
1160
+ function readOsThreadState(tid) {
1161
+ const thread = { tid };
1162
+ const stat = readTaskFile(tid, 'stat');
1163
+ if (stat !== undefined) {
1164
+ // Fields after the parenthesized comm, so state is [0], utime/stime [11]/[12], starttime [19].
1165
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
1166
+ thread.state = fields[0];
1167
+ const utime = finiteOrUndefined(fields[11]);
1168
+ const stime = finiteOrUndefined(fields[12]);
1169
+ if (utime !== undefined && stime !== undefined)
1170
+ thread.cpuTicks = utime + stime;
1171
+ thread.startTime = fields[19];
1172
+ }
1173
+ const wchan = readTaskFile(tid, 'wchan');
1174
+ if (wchan !== undefined)
1175
+ thread.wchan = wchan;
1176
+ // First token only: the syscall number, "running", or -1 (blocked outside a syscall). The
1177
+ // remainder is argument registers and the stack/instruction pointers, which don't belong in a log.
1178
+ const syscall = readTaskFile(tid, 'syscall');
1179
+ if (syscall !== undefined)
1180
+ thread.syscall = syscall.split(' ')[0];
1181
+ const status = readTaskFile(tid, 'status');
1182
+ if (status !== undefined) {
1183
+ thread.voluntarySwitches = finiteOrUndefined(/^voluntary_ctxt_switches:\s*(\d+)/m.exec(status)?.[1]);
1184
+ thread.nonvoluntarySwitches = finiteOrUndefined(/^nonvoluntary_ctxt_switches:\s*(\d+)/m.exec(status)?.[1]);
1185
+ }
1186
+ return thread;
1187
+ }
1188
+ function snapshotWorkerThread(worker) {
1189
+ const snapshot = { at: Date.now() };
1190
+ if (worker.resources?.updated)
1191
+ snapshot.sinceResourceReport = snapshot.at - worker.resources.updated;
1192
+ const eventLoop = worker.performance?.eventLoopUtilization?.();
1193
+ if (eventLoop)
1194
+ snapshot.eventLoop = { idle: eventLoop.idle, active: eventLoop.active };
1195
+ if (worker.osThreadId)
1196
+ snapshot.osThread = readOsThreadState(worker.osThreadId);
1197
+ return snapshot;
1198
+ }
1199
+ function describeThreadState(thread) {
1200
+ return `state=${thread.state ?? '?'} wchan=${thread.wchan ?? '?'} syscall=${thread.syscall ?? '?'}`;
1201
+ }
1202
+ function describeSnapshot(snapshot) {
1203
+ const parts = [
1204
+ snapshot.sinceResourceReport === undefined
1205
+ ? 'no resource report received'
1206
+ : `last resource report ${snapshot.sinceResourceReport}ms ago`,
1207
+ ];
1208
+ if (snapshot.eventLoop)
1209
+ parts.push(`event loop active ${Math.round(snapshot.eventLoop.active)}ms idle ${Math.round(snapshot.eventLoop.idle)}ms`);
1210
+ const thread = snapshot.osThread;
1211
+ if (!thread)
1212
+ parts.push('os thread state unavailable');
1213
+ else
1214
+ parts.push(`os tid ${thread.tid} ${describeThreadState(thread)} cpuTicks=${thread.cpuTicks ?? '?'} ctxtSwitches=${thread.voluntarySwitches ?? '?'}/${thread.nonvoluntarySwitches ?? '?'}`);
1215
+ return parts.join('; ');
1216
+ }
1217
+ function describeDelta(before, after) {
1218
+ return Number.isFinite(before) && Number.isFinite(after) ? `+${after - before}` : '?';
1219
+ }
1220
+ function describeProgress(first, second) {
1221
+ const parts = [];
1222
+ if (first.eventLoop && second.eventLoop)
1223
+ parts.push(`event loop active +${Math.round(second.eventLoop.active - first.eventLoop.active)}ms idle +${Math.round(second.eventLoop.idle - first.eventLoop.idle)}ms`);
1224
+ const before = first.osThread;
1225
+ const after = second.osThread;
1226
+ if (before && after)
1227
+ parts.push(`cpuTicks ${describeDelta(before.cpuTicks, after.cpuTicks)} ctxtSwitches ${describeDelta(before.voluntarySwitches, after.voluntarySwitches)}/${describeDelta(before.nonvoluntarySwitches, after.nonvoluntarySwitches)} ${describeThreadState(after)}`);
1228
+ return parts.join('; ');
1229
+ }
1230
+ // A blocked event loop cannot report itself; two kernel-state samples a second apart separate
1231
+ // "parked on a lock" (no CPU ticks, no context switches) from "spinning".
1232
+ const STUCK_WORKER_SAMPLE_INTERVAL_MS = 1000;
1233
+ const STUCK_WORKER_DIAGNOSTIC_COOLDOWN_MS = 30000;
1234
+ function logStuckWorkerDiagnostics(worker) {
1235
+ if (!worker || !workers.includes(worker))
1236
+ return;
1237
+ const now = Date.now();
1238
+ if (worker.stuckDiagnosticAt !== undefined && now - worker.stuckDiagnosticAt < STUCK_WORKER_DIAGNOSTIC_COOLDOWN_MS)
1239
+ return;
1240
+ worker.stuckDiagnosticAt = now;
1241
+ const threadId = worker.threadId; // Node resets it to -1 once the worker exits
1242
+ const first = snapshotWorkerThread(worker);
1243
+ harperLogger.warn(`Worker thread ${threadId} at ack timeout: ${describeSnapshot(first)}`);
1244
+ if (!first.eventLoop && !first.osThread)
1245
+ return;
1246
+ setTimeout(() => {
1247
+ if (!workers.includes(worker)) {
1248
+ harperLogger.warn(`Worker thread ${threadId} exited before its follow-up sample`);
1249
+ return;
1250
+ }
1251
+ const second = snapshotWorkerThread(worker);
1252
+ // A recycled tid after a thread exit would attribute another thread's activity to this worker.
1253
+ if (second.osThread && second.osThread.startTime !== first.osThread?.startTime)
1254
+ second.osThread = undefined;
1255
+ harperLogger.warn(`Worker thread ${threadId} over the next ${second.at - first.at}ms: ${describeProgress(first, second) || 'no further state available'}`);
1256
+ }, STUCK_WORKER_SAMPLE_INTERVAL_MS).unref();
1257
+ }
796
1258
  function sendThreadInfo(targetWorker) {
797
1259
  targetWorker.postMessage({
798
1260
  type: THREAD_INFO,
@@ -804,6 +1266,7 @@ function getChildWorkerInfo() {
804
1266
  return workers.map((worker) => ({
805
1267
  threadId: worker.threadId,
806
1268
  name: worker.name,
1269
+ application: worker.application,
807
1270
  heapTotal: worker.resources?.heapTotal,
808
1271
  heapUsed: worker.resources?.heapUsed,
809
1272
  externalMemory: worker.resources?.external,
@@ -864,6 +1327,9 @@ if (parentPort && workerData?.addPorts) {
864
1327
  // parentPort so sendToThread(0, ...) and similar lookups can route back to main.
865
1328
  parentPort.threadId = 0;
866
1329
  addPort(parentPort);
1330
+ const osThreadId = getOsThreadId();
1331
+ if (osThreadId !== undefined)
1332
+ parentPort.postMessage({ type: OS_THREAD_ID, osThreadId });
867
1333
  for (let i = 0, l = workerData.addPorts.length; i < l; i++) {
868
1334
  let port = workerData.addPorts[i];
869
1335
  port.threadId = workerData.addThreadIds[i];
@@ -912,17 +1378,56 @@ if (parentPort && workerData?.addPorts) {
912
1378
  }, timeoutMs);
913
1379
  }
914
1380
  });
1381
+ let nextRunningIsolatedApplicationsRequestId = 0;
1382
+ getRunningIsolatedApplications = (timeoutMs) => new Promise((resolve, reject) => {
1383
+ const requestId = ++nextRunningIsolatedApplicationsRequestId;
1384
+ let timeout;
1385
+ parentPort.on('message', receiveApplications);
1386
+ try {
1387
+ parentPort.postMessage({ type: REQUEST_RUNNING_ISOLATED_APPLICATIONS, requestId });
1388
+ }
1389
+ catch (error) {
1390
+ cleanup();
1391
+ reject(error);
1392
+ return;
1393
+ }
1394
+ function receiveApplications(message) {
1395
+ if (message.type === RUNNING_ISOLATED_APPLICATIONS && message.requestId === requestId) {
1396
+ cleanup();
1397
+ resolve(message.applications);
1398
+ }
1399
+ }
1400
+ function cleanup() {
1401
+ if (timeout)
1402
+ clearTimeout(timeout);
1403
+ parentPort.off('message', receiveApplications);
1404
+ }
1405
+ if (timeoutMs != null) {
1406
+ timeout = setTimeout(() => {
1407
+ cleanup();
1408
+ const error = new Error(`Timed out waiting for isolated application topology after ${timeoutMs}ms`);
1409
+ error.code = 'ERR_ISOLATED_APPLICATIONS_TIMEOUT';
1410
+ reject(error);
1411
+ }, timeoutMs);
1412
+ }
1413
+ });
915
1414
  let awaitTerminationRequestId = 0;
916
- awaitProcessGroupTermination = (ownerThreadId) => new Promise((resolve) => {
1415
+ awaitProcessGroupTermination = (ownerThreadId, signal) => new Promise((resolve) => {
917
1416
  // Deliberately no timeout: a contender must not reclaim a dead owner's lock while its
918
1417
  // process group might still be alive and mutating files, so this mirrors the unbounded
919
- // wait Application.ts's waitForConfirmedTermination uses for the same reason.
1418
+ // wait Application.ts's waitForConfirmedTermination uses for the same reason. A caller
1419
+ // that needs its own bound (isThreadRunning) passes `signal` so this listener still gets
1420
+ // torn down when that caller gives up, instead of leaking for the life of the worker.
920
1421
  const requestId = ++awaitTerminationRequestId;
921
1422
  parentPort.on('message', receiveConfirmation);
1423
+ signal?.addEventListener('abort', cleanup, { once: true });
922
1424
  parentPort.postMessage({ type: AWAIT_PROCESS_GROUP_TERMINATION, ownerThreadId, requestId });
1425
+ function cleanup() {
1426
+ parentPort.off('message', receiveConfirmation);
1427
+ }
923
1428
  function receiveConfirmation(message) {
924
1429
  if (message.type === PROCESS_GROUP_TERMINATION_CONFIRMED && message.requestId === requestId) {
925
- parentPort.off('message', receiveConfirmation);
1430
+ cleanup();
926
1431
  resolve();
927
1432
  }
928
1433
  }
@@ -930,9 +1435,17 @@ if (parentPort && workerData?.addPorts) {
930
1435
  }
931
1436
  else {
932
1437
  getThreadInfo = getChildWorkerInfo;
1438
+ getRunningIsolatedApplications = isMainThread
1439
+ ? () => runningIsolatedApplicationsGetter()
1440
+ : () => {
1441
+ const error = new Error('No channel to the main thread for isolated application topology');
1442
+ error.code = 'ERR_ISOLATED_APPLICATIONS_UNAVAILABLE';
1443
+ return Promise.reject(error);
1444
+ };
933
1445
  awaitProcessGroupTermination = (ownerThreadId) => pendingProcessGroupTerminations.get(ownerThreadId) ?? Promise.resolve();
934
1446
  }
935
1447
  module.exports.getThreadInfo = getThreadInfo;
1448
+ module.exports.getRunningIsolatedApplications = getRunningIsolatedApplications;
936
1449
  // Listeners notified when a connected thread's port closes (worker exit/restart), so
937
1450
  // modules holding per-thread state (e.g. registeredOperations' registry and in-flight
938
1451
  // forwards) can clean up.
@@ -953,34 +1466,144 @@ const processGroupsByThread = new Map();
953
1466
  // window component-preparation locking exists to close.
954
1467
  const pendingProcessGroupTerminations = new Map();
955
1468
  const PROCESS_GROUP_TERMINATION_POLL_MS = 25;
956
- // A process-group leader spawned by a worker thread's own event loop is normally reaped by that
957
- // same loop when it dies. But the only caller of this check (waitForProcessGroupExit, for a
958
- // *dead* owner thread) runs after that loop is already gone, so nothing will ever reap it here:
959
- // kill(pid, 0) keeps succeeding forever against the zombie's still-allocated PID slot. A zombie
960
- // can no longer touch the filesystem, so treat a confirmed zombie as dead rather than poll
961
- // forever for a reap that will never come. Reproduced by killing a worker mid-install: the
962
- // installer's process-group leader becomes a permanent `Z` in /proc without this check.
963
- function isZombieProcessGroupLeader(processGroupId) {
964
- if (process.platform !== 'linux')
965
- return false;
1469
+ const ZOMBIE_GROUP_MEMBER_SCAN_INTERVAL_MS = 1000;
1470
+ const PROCESS_GROUP_LIVENESS_WARNING_MS = 30000;
1471
+ // Bounds isThreadRunning's own wait (below); Application.ts's waitForConfirmedTermination stays
1472
+ // deliberately unbounded.
1473
+ const THREAD_RUNNING_TERMINATION_BACKSTOP_MS = PROCESS_GROUP_LIVENESS_WARNING_MS;
1474
+ const zombieGroupScanTimes = new Map();
1475
+ const processGroupLivenessStates = new Map();
1476
+ function processGroupExists(processGroupId) {
1477
+ try {
1478
+ process.kill(-processGroupId, 0);
1479
+ return true;
1480
+ }
1481
+ catch (error) {
1482
+ return error.code === 'EPERM';
1483
+ }
1484
+ }
1485
+ function processProbeError(error) {
1486
+ return error?.code ?? error?.message ?? String(error ?? 'unknown error');
1487
+ }
1488
+ function processGroupLeaderState(processGroupId, platform, readStat) {
1489
+ if (platform !== 'linux') {
1490
+ return { state: 'unknown', reason: `zombie-process detection is unavailable on ${platform}` };
1491
+ }
966
1492
  let stat;
967
1493
  try {
968
- stat = readFileSync(`/proc/${processGroupId}/stat`, 'utf8');
1494
+ stat = readStat(`/proc/${processGroupId}/stat`, 'utf8');
969
1495
  }
970
- catch {
971
- return false;
1496
+ catch (error) {
1497
+ if (error.code === 'ENOENT' || error.code === 'ESRCH')
1498
+ return { state: 'missing' };
1499
+ return {
1500
+ state: 'unknown',
1501
+ reason: `reading /proc/${processGroupId}/stat failed (${processProbeError(error)})`,
1502
+ };
972
1503
  }
973
- // Format is "pid (comm) state ..."; comm can contain spaces/parens, so anchor on the last ')'.
974
- return stat[stat.lastIndexOf(')') + 2] === 'Z';
1504
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
1505
+ if (Number(fields[2]) !== processGroupId)
1506
+ return { state: 'missing' };
1507
+ return { state: fields[0] === 'Z' ? 'zombie' : 'alive' };
975
1508
  }
976
- function processGroupIsAlive(processGroupId) {
1509
+ function scanLinuxProcessGroup(processGroupId, readDirectory, readStat) {
1510
+ let processIds;
977
1511
  try {
978
- process.kill(-processGroupId, 0);
1512
+ processIds = readDirectory('/proc');
979
1513
  }
980
1514
  catch (error) {
981
- return error.code === 'EPERM';
1515
+ return {
1516
+ isAlive: null,
1517
+ reason: `reading /proc failed (${processProbeError(error)})`,
1518
+ };
1519
+ }
1520
+ for (const processId of processIds) {
1521
+ if (!/^\d+$/.test(processId))
1522
+ continue;
1523
+ let stat;
1524
+ try {
1525
+ stat = readStat(`/proc/${processId}/stat`, 'utf8');
1526
+ }
1527
+ catch (error) {
1528
+ if (error.code === 'ENOENT' || error.code === 'ESRCH')
1529
+ continue; // already gone
1530
+ // EACCES/EPERM (e.g. hidepid/ProtectProc) can only happen on a pid we don't own — we
1531
+ // spawned our own group's members as this same user, so their stat is always readable —
1532
+ // so it's not one of ours. Anything else is genuinely unknown, not "not ours".
1533
+ if (error.code === 'EACCES' || error.code === 'EPERM')
1534
+ continue;
1535
+ return {
1536
+ isAlive: null,
1537
+ reason: `reading /proc/${processId}/stat failed (${processProbeError(error)})`,
1538
+ };
1539
+ }
1540
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
1541
+ const pgrp = Number(fields[2]);
1542
+ if (!Number.isFinite(pgrp)) {
1543
+ return { isAlive: null, reason: `/proc/${processId}/stat had an unparseable process-group field` };
1544
+ }
1545
+ if (pgrp !== processGroupId)
1546
+ continue;
1547
+ if (fields[0] !== 'Z')
1548
+ return { isAlive: true, reason: `a live member was observed at pid ${processId}` };
982
1549
  }
983
- return !isZombieProcessGroupLeader(processGroupId);
1550
+ return { isAlive: false };
1551
+ }
1552
+ function keepProcessGroupAlive(processGroupId, reason, observationTime, warn) {
1553
+ let livenessState = processGroupLivenessStates.get(processGroupId);
1554
+ if (!livenessState) {
1555
+ livenessState = { observedAt: observationTime, reason, warned: false };
1556
+ processGroupLivenessStates.set(processGroupId, livenessState);
1557
+ }
1558
+ else {
1559
+ livenessState.reason = reason;
1560
+ }
1561
+ if (!livenessState.warned && observationTime - livenessState.observedAt >= PROCESS_GROUP_LIVENESS_WARNING_MS) {
1562
+ livenessState.warned = true;
1563
+ warn(`Process group ${processGroupId} termination remains unconfirmed after ${PROCESS_GROUP_LIVENESS_WARNING_MS}ms: ${livenessState.reason}`);
1564
+ }
1565
+ return true;
1566
+ }
1567
+ function clearProcessGroupLivenessState(processGroupId) {
1568
+ zombieGroupScanTimes.delete(processGroupId);
1569
+ processGroupLivenessStates.delete(processGroupId);
1570
+ }
1571
+ function isProcessGroupAlive(processGroupId, options) {
1572
+ const platform = options?.platform ?? process.platform;
1573
+ const groupExists = options?.processGroupExists ?? processGroupExists;
1574
+ const readDirectory = options?.readDirectory ?? readdirSync;
1575
+ const readStat = options?.readStat ?? readFileSync;
1576
+ const observationTime = options?.now?.() ?? performance.now();
1577
+ const warn = options?.warn ?? ((message) => harperLogger.warn(message));
1578
+ if (!groupExists(processGroupId)) {
1579
+ clearProcessGroupLivenessState(processGroupId);
1580
+ return false;
1581
+ }
1582
+ const leaderState = processGroupLeaderState(processGroupId, platform, readStat);
1583
+ if (leaderState.state === 'alive')
1584
+ return keepProcessGroupAlive(processGroupId, 'the process-group leader is still alive', observationTime, warn);
1585
+ if (leaderState.state === 'unknown')
1586
+ return keepProcessGroupAlive(processGroupId, leaderState.reason, observationTime, warn);
1587
+ const scanTime = observationTime;
1588
+ const lastScan = zombieGroupScanTimes.get(processGroupId);
1589
+ if (lastScan !== undefined && scanTime >= lastScan && scanTime - lastScan < ZOMBIE_GROUP_MEMBER_SCAN_INTERVAL_MS) {
1590
+ const reason = processGroupLivenessStates.get(processGroupId)?.reason ?? 'the previous Linux scan is still current';
1591
+ return keepProcessGroupAlive(processGroupId, reason, observationTime, warn);
1592
+ }
1593
+ zombieGroupScanTimes.set(processGroupId, scanTime);
1594
+ let scanResult = scanLinuxProcessGroup(processGroupId, readDirectory, readStat);
1595
+ if (scanResult.isAlive !== false)
1596
+ return keepProcessGroupAlive(processGroupId, scanResult.reason, observationTime, warn);
1597
+ // A second snapshot catches a member omitted after forking inside the first readdir/stat window.
1598
+ // It narrows rather than closes the race because the second synchronous pass has the same gap.
1599
+ scanResult = scanLinuxProcessGroup(processGroupId, readDirectory, readStat);
1600
+ if (scanResult.isAlive !== false)
1601
+ return keepProcessGroupAlive(processGroupId, scanResult.reason, observationTime, warn);
1602
+ clearProcessGroupLivenessState(processGroupId);
1603
+ return false;
1604
+ }
1605
+ function processGroupIsAlive(processGroupId) {
1606
+ return isProcessGroupAlive(processGroupId);
984
1607
  }
985
1608
  async function waitForProcessGroupExit(processGroupId) {
986
1609
  while (processGroupIsAlive(processGroupId))
@@ -1054,6 +1677,7 @@ function addProcessGroup(ownerThreadId, processGroupId) {
1054
1677
  processGroups.add(processGroupId);
1055
1678
  }
1056
1679
  function removeProcessGroup(ownerThreadId, processGroupId) {
1680
+ clearProcessGroupLivenessState(processGroupId);
1057
1681
  const processGroups = processGroupsByThread.get(ownerThreadId);
1058
1682
  if (!processGroups)
1059
1683
  return;
@@ -1107,6 +1731,39 @@ function unregisterProcessGroup(processGroupId) {
1107
1731
  else
1108
1732
  parentPort?.postMessage({ type: UNREGISTER_PROCESS_GROUP, processGroupId });
1109
1733
  }
1734
+ class ProcessGroupTerminationUnconfirmedError extends Error {
1735
+ code = 'ERR_PROCESS_GROUP_TERMINATION_UNCONFIRMED';
1736
+ constructor(ownerThreadId, timeoutMs) {
1737
+ super(`Thread ${ownerThreadId}'s process-group termination could not be confirmed after ${timeoutMs}ms`);
1738
+ this.name = 'ProcessGroupTerminationUnconfirmedError';
1739
+ }
1740
+ }
1741
+ // Bounds awaitProcessGroupTermination for isThreadRunning below. Throws rather than resolving to
1742
+ // "not running": componentPreparationLock's ownerIsAlive/ownerLivenessConfirmed already treat a
1743
+ // throwing isOwnerAlive as "can't confirm — don't steal the claim, don't renew the deadline", so
1744
+ // the lock's own bounded wait is what eventually fails the waiter.
1745
+ async function awaitConfirmedProcessGroupTermination(ownerThreadId) {
1746
+ const abortController = new AbortController();
1747
+ let timedOut = false;
1748
+ const timer = setTimeout(() => {
1749
+ timedOut = true;
1750
+ abortController.abort();
1751
+ }, THREAD_RUNNING_TERMINATION_BACKSTOP_MS);
1752
+ timer.unref?.();
1753
+ try {
1754
+ await Promise.race([
1755
+ awaitProcessGroupTermination(ownerThreadId, abortController.signal),
1756
+ new Promise((resolve) => abortController.signal.addEventListener('abort', resolve, { once: true })),
1757
+ ]);
1758
+ }
1759
+ finally {
1760
+ clearTimeout(timer);
1761
+ }
1762
+ if (timedOut) {
1763
+ harperLogger.warn(`Thread ${ownerThreadId}'s process-group termination could not be confirmed after ${THREAD_RUNNING_TERMINATION_BACKSTOP_MS}ms`);
1764
+ throw new ProcessGroupTerminationUnconfirmedError(ownerThreadId, THREAD_RUNNING_TERMINATION_BACKSTOP_MS);
1765
+ }
1766
+ }
1110
1767
  async function isThreadRunning(ownerThreadId, timeoutMs = THREAD_INFO_REQUEST_TIMEOUT_MS) {
1111
1768
  if (ownerThreadId === threadId || ownerThreadId === 0)
1112
1769
  return true;
@@ -1114,7 +1771,7 @@ async function isThreadRunning(ownerThreadId, timeoutMs = THREAD_INFO_REQUEST_TI
1114
1771
  return true;
1115
1772
  // The thread itself is gone, but it may still own process groups whose forced termination is
1116
1773
  // in flight — wait for that to be confirmed before reporting the owner as reclaimable.
1117
- await awaitProcessGroupTermination(ownerThreadId);
1774
+ await awaitConfirmedProcessGroupTermination(ownerThreadId);
1118
1775
  return false;
1119
1776
  }
1120
1777
  if (isMainThread) {
@@ -1123,6 +1780,14 @@ if (isMainThread) {
1123
1780
  terminateProcessGroupsForThread(ownerThreadId);
1124
1781
  });
1125
1782
  }
1783
+ /**
1784
+ * Whether a thread has already been reported dead. Sync, unlike `isThreadRunning`, because it only
1785
+ * reads the tombstone `notifyThreadExit` records below — callers on the exit path need an answer
1786
+ * without awaiting process-group confirmation.
1787
+ */
1788
+ function hasThreadExited(threadId) {
1789
+ return notifiedDeadThreadIds.has(threadId);
1790
+ }
1126
1791
  function notifyThreadExit(deadThreadId) {
1127
1792
  if (deadThreadId == null || notifiedDeadThreadIds.has(deadThreadId))
1128
1793
  return;
@@ -1246,25 +1911,51 @@ if (isMainThread) {
1246
1911
  const watchDir = async (dir, beforeRestartCallback) => {
1247
1912
  if (beforeRestartCallback)
1248
1913
  beforeRestart = beforeRestartCallback;
1249
- chokidar
1250
- .watch(dir, {
1251
- persistent: false,
1252
- ignored: (path) => {
1253
- return ignoredPaths.some((ignoredPath) => path.includes(ignoredPath));
1254
- },
1255
- })
1256
- .on('change', (path) => {
1257
- changedFiles.add(path);
1258
- if (queuedRestart)
1259
- clearTimeout(queuedRestart);
1260
- queuedRestart = setTimeout(async () => {
1261
- if (beforeRestart)
1262
- await beforeRestart();
1263
- await restartWorkers();
1264
- console.log('Reloaded Harper components, changed files:', Array.from(changedFiles));
1265
- changedFiles.clear();
1266
- }, 100);
1267
- });
1914
+ const watchTarget = resolveWatchTarget(dir);
1915
+ let usingPolling = watchTarget.mustPoll;
1916
+ let liveWatcher;
1917
+ const openWatcher = () => {
1918
+ const opened = (liveWatcher = guardedWatch(watchTarget.path, {
1919
+ persistent: false,
1920
+ ...(usingPolling ? DIRECTORY_POLLING_FALLBACK_OPTIONS : {}),
1921
+ ignored: (path) => {
1922
+ return ignoredPaths.some((ignoredPath) => path.includes(ignoredPath));
1923
+ },
1924
+ }));
1925
+ opened
1926
+ // This runs on the thread that owns every worker, and chokidar emits 'error' unguarded for
1927
+ // anything but ENOENT/ENOTDIR.
1928
+ .on('error', (error) => {
1929
+ if (claimLostNativeWatchError(error))
1930
+ return;
1931
+ if (isWatcherExhaustionError(error)) {
1932
+ if (usingPolling || liveWatcher !== opened)
1933
+ return;
1934
+ warnWatcherFallback(dir);
1935
+ usingPolling = true;
1936
+ Promise.resolve()
1937
+ .then(() => opened.close())
1938
+ .catch(() => { })
1939
+ .then(openWatcher)
1940
+ .catch((reopenError) => console.error(`Could not reopen the ${dir} component-reload watch on polling:`, reopenError));
1941
+ return;
1942
+ }
1943
+ console.error(`Error watching ${dir} for component reloads:`, error);
1944
+ })
1945
+ .on('change', (path) => {
1946
+ changedFiles.add(path);
1947
+ if (queuedRestart)
1948
+ clearTimeout(queuedRestart);
1949
+ queuedRestart = setTimeout(async () => {
1950
+ if (beforeRestart)
1951
+ await beforeRestart();
1952
+ await restartWorkers(undefined, undefined, true, null, '*');
1953
+ console.log('Reloaded Harper components, changed files:', Array.from(changedFiles));
1954
+ changedFiles.clear();
1955
+ }, 100);
1956
+ });
1957
+ };
1958
+ openWatcher();
1268
1959
  };
1269
1960
  module.exports.watchDir = watchDir;
1270
1961
  if (process.env.WATCH_DIR)
@@ -1282,4 +1973,7 @@ else {
1282
1973
  realExit(0);
1283
1974
  });
1284
1975
  }
1976
+ // Required here, not from logging, which must never import this module; last in the file because it
1977
+ // calls onThreadExit.
1978
+ require("./logRotationTransport.js");
1285
1979
  //# sourceMappingURL=manageThreads.js.map