@harperfast/harper 5.3.0-beta.3 → 5.3.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/bin/cliOperations.ts +25 -3
  2. package/bin/run.ts +18 -0
  3. package/components/Application.ts +52 -78
  4. package/components/DESIGN.md +76 -3
  5. package/components/Scope.ts +10 -4
  6. package/components/anthropic/index.ts +10 -1
  7. package/components/bedrock/index.ts +10 -1
  8. package/components/componentLoader.ts +23 -14
  9. package/components/mcp/tools/operations.ts +3 -1
  10. package/components/ollama/index.ts +10 -1
  11. package/components/openai/index.ts +265 -18
  12. package/components/operations.js +40 -1
  13. package/config/configUtils.ts +3 -19
  14. package/dataLayer/DESIGN.md +29 -5
  15. package/dataLayer/harperBridge/ResourceBridge.ts +39 -3
  16. package/dataLayer/restoreMarker.ts +250 -14
  17. package/dataLayer/schema.ts +2 -7
  18. package/dataLayer/schemaDescribe.ts +10 -0
  19. package/dist/bin/cliOperations.js +28 -3
  20. package/dist/bin/cliOperations.js.map +1 -1
  21. package/dist/bin/run.js +19 -0
  22. package/dist/bin/run.js.map +1 -1
  23. package/dist/components/Application.d.ts +8 -2
  24. package/dist/components/Application.js +43 -69
  25. package/dist/components/Application.js.map +1 -1
  26. package/dist/components/Scope.d.ts +1 -1
  27. package/dist/components/Scope.js +9 -4
  28. package/dist/components/Scope.js.map +1 -1
  29. package/dist/components/anthropic/index.d.ts +0 -10
  30. package/dist/components/anthropic/index.js +9 -1
  31. package/dist/components/anthropic/index.js.map +1 -1
  32. package/dist/components/bedrock/index.d.ts +0 -12
  33. package/dist/components/bedrock/index.js +9 -1
  34. package/dist/components/bedrock/index.js.map +1 -1
  35. package/dist/components/componentLoader.js +23 -14
  36. package/dist/components/componentLoader.js.map +1 -1
  37. package/dist/components/mcp/tools/operations.js +1 -1
  38. package/dist/components/mcp/tools/operations.js.map +1 -1
  39. package/dist/components/ollama/index.d.ts +0 -13
  40. package/dist/components/ollama/index.js +9 -1
  41. package/dist/components/ollama/index.js.map +1 -1
  42. package/dist/components/openai/index.d.ts +11 -15
  43. package/dist/components/openai/index.js +224 -12
  44. package/dist/components/openai/index.js.map +1 -1
  45. package/dist/components/operations.d.ts +9 -0
  46. package/dist/components/operations.js +32 -2
  47. package/dist/components/operations.js.map +1 -1
  48. package/dist/config/configUtils.d.ts +1 -2
  49. package/dist/config/configUtils.js +7 -22
  50. package/dist/config/configUtils.js.map +1 -1
  51. package/dist/dataLayer/harperBridge/ResourceBridge.js +29 -2
  52. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  53. package/dist/dataLayer/restoreMarker.d.ts +25 -3
  54. package/dist/dataLayer/restoreMarker.js +213 -13
  55. package/dist/dataLayer/restoreMarker.js.map +1 -1
  56. package/dist/dataLayer/schema.js +1 -4
  57. package/dist/dataLayer/schema.js.map +1 -1
  58. package/dist/dataLayer/schemaDescribe.js +10 -0
  59. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/json/systemSchema.json +28 -0
  63. package/dist/resources/DatabaseTransaction.d.ts +21 -1
  64. package/dist/resources/DatabaseTransaction.js +147 -8
  65. package/dist/resources/DatabaseTransaction.js.map +1 -1
  66. package/dist/resources/PrimaryRocksDatabase.js +1 -1
  67. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  68. package/dist/resources/Table.d.ts +31 -3
  69. package/dist/resources/Table.js +397 -79
  70. package/dist/resources/Table.js.map +1 -1
  71. package/dist/resources/auditStore.js +16 -10
  72. package/dist/resources/auditStore.js.map +1 -1
  73. package/dist/resources/blob.d.ts +1 -0
  74. package/dist/resources/blob.js +14 -5
  75. package/dist/resources/blob.js.map +1 -1
  76. package/dist/resources/branchDatabase.js +3 -3
  77. package/dist/resources/branchDatabase.js.map +1 -1
  78. package/dist/resources/databaseDropPreparation.d.ts +20 -0
  79. package/dist/resources/databaseDropPreparation.js +106 -0
  80. package/dist/resources/databaseDropPreparation.js.map +1 -0
  81. package/dist/resources/databases.d.ts +26 -21
  82. package/dist/resources/databases.js +1040 -204
  83. package/dist/resources/databases.js.map +1 -1
  84. package/dist/resources/derivedIndexRegistry.d.ts +23 -0
  85. package/dist/resources/derivedIndexRegistry.js +92 -0
  86. package/dist/resources/derivedIndexRegistry.js.map +1 -1
  87. package/dist/resources/derivedIndexRuntime.d.ts +10 -0
  88. package/dist/resources/derivedIndexRuntime.js +136 -25
  89. package/dist/resources/derivedIndexRuntime.js.map +1 -1
  90. package/dist/resources/derivedIndexes.d.ts +39 -0
  91. package/dist/resources/derivedIndexes.js +733 -0
  92. package/dist/resources/derivedIndexes.js.map +1 -0
  93. package/dist/resources/fullTextSchema.d.ts +8 -0
  94. package/dist/resources/fullTextSchema.js +42 -0
  95. package/dist/resources/fullTextSchema.js.map +1 -1
  96. package/dist/resources/fullTextSchemaLifecycle.d.ts +0 -5
  97. package/dist/resources/fullTextSchemaLifecycle.js +0 -9
  98. package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
  99. package/dist/resources/graphql.js +101 -9
  100. package/dist/resources/graphql.js.map +1 -1
  101. package/dist/resources/indexes/hnswDerivedIndex.d.ts +0 -10
  102. package/dist/resources/indexes/hnswDerivedIndex.js +0 -230
  103. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -1
  104. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +3 -0
  105. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +57 -8
  106. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
  107. package/dist/resources/models/Models.d.ts +26 -3
  108. package/dist/resources/models/Models.js +256 -13
  109. package/dist/resources/models/Models.js.map +1 -1
  110. package/dist/resources/models/TestBackend.d.ts +3 -1
  111. package/dist/resources/models/TestBackend.js +35 -1
  112. package/dist/resources/models/TestBackend.js.map +1 -1
  113. package/dist/resources/models/analyticsTable.d.ts +2 -2
  114. package/dist/resources/models/analyticsTable.js +4 -2
  115. package/dist/resources/models/analyticsTable.js.map +1 -1
  116. package/dist/resources/models/backendHelpers.d.ts +24 -0
  117. package/dist/resources/models/backendHelpers.js +18 -1
  118. package/dist/resources/models/backendHelpers.js.map +1 -1
  119. package/dist/resources/models/backendRegistry.d.ts +10 -21
  120. package/dist/resources/models/backendRegistry.js +74 -18
  121. package/dist/resources/models/backendRegistry.js.map +1 -1
  122. package/dist/resources/models/bootstrap.d.ts +7 -0
  123. package/dist/resources/models/bootstrap.js +36 -4
  124. package/dist/resources/models/bootstrap.js.map +1 -1
  125. package/dist/resources/models/decideHook.d.ts +33 -0
  126. package/dist/resources/models/decideHook.js +105 -0
  127. package/dist/resources/models/decideHook.js.map +1 -0
  128. package/dist/resources/models/decision.d.ts +64 -0
  129. package/dist/resources/models/decision.js +507 -0
  130. package/dist/resources/models/decision.js.map +1 -0
  131. package/dist/resources/models/decisionStore.d.ts +134 -0
  132. package/dist/resources/models/decisionStore.js +350 -0
  133. package/dist/resources/models/decisionStore.js.map +1 -0
  134. package/dist/resources/models/embedHook.d.ts +66 -8
  135. package/dist/resources/models/embedHook.js +228 -61
  136. package/dist/resources/models/embedHook.js.map +1 -1
  137. package/dist/resources/models/generativeDecision.d.ts +58 -0
  138. package/dist/resources/models/generativeDecision.js +315 -0
  139. package/dist/resources/models/generativeDecision.js.map +1 -0
  140. package/dist/resources/models/types.d.ts +232 -4
  141. package/dist/security/authn/oidc/tokenExchange.js +3 -33
  142. package/dist/security/authn/oidc/tokenExchange.js.map +1 -1
  143. package/dist/security/authn/oidc/tokenUseTable.d.ts +9 -0
  144. package/dist/security/authn/oidc/tokenUseTable.js +28 -0
  145. package/dist/security/authn/oidc/tokenUseTable.js.map +1 -0
  146. package/dist/security/certificateVerification/certificateVerificationSource.d.ts +0 -1
  147. package/dist/security/certificateVerification/certificateVerificationSource.js +3 -3
  148. package/dist/security/certificateVerification/certificateVerificationSource.js.map +1 -1
  149. package/dist/security/certificateVerification/crlVerification.js +77 -165
  150. package/dist/security/certificateVerification/crlVerification.js.map +1 -1
  151. package/dist/security/certificateVerification/types.d.ts +0 -3
  152. package/dist/security/certificateVerification/verificationTables.d.ts +19 -0
  153. package/dist/security/certificateVerification/verificationTables.js +125 -0
  154. package/dist/security/certificateVerification/verificationTables.js.map +1 -0
  155. package/dist/security/certificateVerification/verificationUtils.d.ts +1 -1
  156. package/dist/security/certificateVerification/verificationUtils.js +6 -31
  157. package/dist/security/certificateVerification/verificationUtils.js.map +1 -1
  158. package/dist/security/jsLoader.d.ts +5 -0
  159. package/dist/security/jsLoader.js +8 -0
  160. package/dist/security/jsLoader.js.map +1 -1
  161. package/dist/server/Server.d.ts +1 -0
  162. package/dist/server/Server.js.map +1 -1
  163. package/dist/server/http.js +2 -2
  164. package/dist/server/http.js.map +1 -1
  165. package/dist/server/itc/serverHandlers.js +21 -2
  166. package/dist/server/itc/serverHandlers.js.map +1 -1
  167. package/dist/server/jobs/jobOwnership.d.ts +25 -0
  168. package/dist/server/jobs/jobOwnership.js +127 -0
  169. package/dist/server/jobs/jobOwnership.js.map +1 -0
  170. package/dist/server/jobs/jobProcess.js +9 -2
  171. package/dist/server/jobs/jobProcess.js.map +1 -1
  172. package/dist/server/jobs/jobs.js +18 -5
  173. package/dist/server/jobs/jobs.js.map +1 -1
  174. package/dist/server/loadRootComponents.js +10 -0
  175. package/dist/server/loadRootComponents.js.map +1 -1
  176. package/dist/server/serverHelpers/contentTypes.js +11 -1
  177. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  178. package/dist/server/serverHelpers/serverHandlers.js +12 -2
  179. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  180. package/dist/server/threads/itc.d.ts +4 -1
  181. package/dist/server/threads/itc.js +29 -6
  182. package/dist/server/threads/itc.js.map +1 -1
  183. package/dist/server/threads/manageThreads.d.ts +12 -4
  184. package/dist/server/threads/manageThreads.js +241 -119
  185. package/dist/server/threads/manageThreads.js.map +1 -1
  186. package/dist/server/threads/threadServer.d.ts +1 -0
  187. package/dist/server/threads/threadServer.js +15 -1
  188. package/dist/server/threads/threadServer.js.map +1 -1
  189. package/dist/server/threads/windowsProcessTree.d.ts +91 -0
  190. package/dist/server/threads/windowsProcessTree.js +361 -0
  191. package/dist/server/threads/windowsProcessTree.js.map +1 -0
  192. package/dist/server/throttle.d.ts +1 -1
  193. package/dist/server/throttle.js +8 -1
  194. package/dist/server/throttle.js.map +1 -1
  195. package/dist/upgrade/directives/5-3-0.js +53 -46
  196. package/dist/upgrade/directives/5-3-0.js.map +1 -1
  197. package/dist/utility/errors/hdbError.d.ts +10 -1
  198. package/dist/utility/errors/hdbError.js +55 -11
  199. package/dist/utility/errors/hdbError.js.map +1 -1
  200. package/dist/utility/fsync.d.ts +2 -0
  201. package/dist/utility/fsync.js +20 -0
  202. package/dist/utility/fsync.js.map +1 -0
  203. package/dist/utility/globalSchema.d.ts +18 -0
  204. package/dist/utility/hdbTerms.d.ts +3 -0
  205. package/dist/utility/hdbTerms.js +3 -0
  206. package/dist/utility/hdbTerms.js.map +1 -1
  207. package/dist/utility/signalling.d.ts +7 -1
  208. package/dist/utility/signalling.js +27 -3
  209. package/dist/utility/signalling.js.map +1 -1
  210. package/dist/utility/when.d.ts +1 -0
  211. package/dist/utility/when.js +21 -0
  212. package/dist/utility/when.js.map +1 -1
  213. package/dist/validation/configValidator.js +30 -3
  214. package/dist/validation/configValidator.js.map +1 -1
  215. package/index.ts +13 -0
  216. package/json/systemSchema.json +28 -0
  217. package/npm-shrinkwrap.json +151 -55
  218. package/package.json +14 -13
  219. package/resources/DESIGN.md +80 -100
  220. package/resources/DatabaseTransaction.ts +157 -10
  221. package/resources/PrimaryRocksDatabase.ts +2 -2
  222. package/resources/Table.ts +494 -101
  223. package/resources/auditStore.ts +18 -10
  224. package/resources/blob.ts +13 -6
  225. package/resources/branchDatabase.ts +3 -3
  226. package/resources/databaseDropPreparation.ts +120 -0
  227. package/resources/databases.ts +1155 -205
  228. package/resources/derivedIndexRegistry.ts +122 -0
  229. package/resources/derivedIndexRuntime.ts +157 -30
  230. package/resources/derivedIndexes.ts +856 -0
  231. package/resources/fullTextSchema.ts +55 -0
  232. package/resources/fullTextSchemaLifecycle.ts +0 -15
  233. package/resources/graphql.ts +107 -12
  234. package/resources/indexes/DESIGN.md +7 -0
  235. package/resources/indexes/hnswDerivedIndex.ts +0 -260
  236. package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +72 -9
  237. package/resources/models/DESIGN.md +39 -0
  238. package/resources/models/Models.ts +361 -18
  239. package/resources/models/TestBackend.ts +56 -1
  240. package/resources/models/analyticsTable.ts +6 -4
  241. package/resources/models/backendHelpers.ts +17 -0
  242. package/resources/models/backendRegistry.ts +76 -21
  243. package/resources/models/bootstrap.ts +57 -7
  244. package/resources/models/decideHook.ts +135 -0
  245. package/resources/models/decision.ts +505 -0
  246. package/resources/models/decisionStore.ts +419 -0
  247. package/resources/models/embedHook.ts +272 -55
  248. package/resources/models/generativeDecision.ts +459 -0
  249. package/resources/models/types.ts +231 -4
  250. package/security/DESIGN.md +22 -2
  251. package/security/authn/oidc/tokenExchange.ts +6 -34
  252. package/security/authn/oidc/tokenUseTable.ts +27 -0
  253. package/security/certificateVerification/certificateVerificationSource.ts +2 -3
  254. package/security/certificateVerification/crlVerification.ts +96 -174
  255. package/security/certificateVerification/types.ts +0 -3
  256. package/security/certificateVerification/verificationTables.ts +124 -0
  257. package/security/certificateVerification/verificationUtils.ts +8 -32
  258. package/security/jsLoader.ts +9 -1
  259. package/server/DESIGN.md +27 -1
  260. package/server/Server.ts +2 -1
  261. package/server/http.ts +4 -2
  262. package/server/itc/serverHandlers.js +29 -1
  263. package/server/jobs/jobOwnership.ts +89 -0
  264. package/server/jobs/jobProcess.ts +8 -2
  265. package/server/jobs/jobs.ts +15 -4
  266. package/server/loadRootComponents.js +11 -0
  267. package/server/serverHelpers/contentTypes.ts +13 -1
  268. package/server/serverHelpers/serverHandlers.js +11 -2
  269. package/server/threads/itc.js +32 -6
  270. package/server/threads/manageThreads.js +268 -122
  271. package/server/threads/threadServer.js +13 -1
  272. package/server/threads/windowsProcessTree.ts +409 -0
  273. package/server/throttle.ts +11 -1
  274. package/studio/web/assets/{Chat-CveGbxe6.js → Chat-Bq60AO-g.js} +1 -1
  275. package/studio/web/assets/{FloatingChat-IpCFgYWL.js → FloatingChat-D4560T0p.js} +3 -3
  276. package/studio/web/assets/{apiToken-CS8fFTgL.js → apiToken-BEvbFPMc.js} +1 -1
  277. package/studio/web/assets/{applications-D7F4Jopl.js → applications-CSUVTH4E.js} +1 -1
  278. package/studio/web/assets/{cssMode-Daw_oLPq.js → cssMode-DJyc9mHj.js} +1 -1
  279. package/studio/web/assets/{editor-B3Bs1zWX.js → editor-DDIsQfj6.js} +1 -1
  280. package/studio/web/assets/{html-CDaQwWiE.js → html-g1BJHxLy.js} +1 -1
  281. package/studio/web/assets/{htmlMode-Cb6emO4U.js → htmlMode-Vh5IglCQ.js} +1 -1
  282. package/studio/web/assets/{index-B4g2Fu_2.js → index-DGRAzvO4.js} +5 -5
  283. package/studio/web/assets/{index.lazy-NwhvF1q1.js → index.lazy-vgSic7et.js} +1 -1
  284. package/studio/web/assets/{javascript-2_86gB9Q.js → javascript-5VIolqf7.js} +1 -1
  285. package/studio/web/assets/{jsonMode-DX-i86uQ.js → jsonMode-DG4zQraI.js} +1 -1
  286. package/studio/web/assets/{languageServices-B85WN-Ej.js → languageServices-t5FBpX-i.js} +1 -1
  287. package/studio/web/assets/{lspLanguageFeatures-JCWHIsKg.js → lspLanguageFeatures-DgVnNPh-.js} +1 -1
  288. package/studio/web/assets/{notifications-D9ejzt52.js → notifications-BeWcX04m.js} +1 -1
  289. package/studio/web/assets/{notifications-B7hjKhai.js → notifications-DUGWrWoO.js} +1 -1
  290. package/studio/web/assets/{profile-D8B6No3F.js → profile-BvUEDBzW.js} +1 -1
  291. package/studio/web/assets/{regions-DFazIfTK.js → regions-CB-_2m7M.js} +1 -1
  292. package/studio/web/assets/{register-CwFLviLF.js → register-1w1Zr3b2.js} +2 -2
  293. package/studio/web/assets/{setComponentFile-VnJ80SSJ.js → setComponentFile-z4-qJCbA.js} +1 -1
  294. package/studio/web/assets/{setup-BYQxFy-C.js → setup-CS50uJ4x.js} +2 -2
  295. package/studio/web/assets/{status-Cn5XjmsL.js → status-CI0wOdOE.js} +1 -1
  296. package/studio/web/assets/{toggleHighContrast-g9xIh6TX.js → toggleHighContrast-BsAE4jPe.js} +1 -1
  297. package/studio/web/assets/{tsMode-EC-n0Wqc.js → tsMode-D83Gg3cq.js} +1 -1
  298. package/studio/web/assets/{typescript-DHmM9JCO.js → typescript-Dfp-GrNq.js} +1 -1
  299. package/studio/web/assets/{useEntityRestURL-BUdnVSVv.js → useEntityRestURL-BKetlINf.js} +1 -1
  300. package/studio/web/assets/{workers-BVl9bBB-.js → workers-CCvmDLH3.js} +1 -1
  301. package/studio/web/assets/{xml-C4_sqOkX.js → xml-06pZp00U.js} +1 -1
  302. package/studio/web/assets/{yaml-CVkbDqtW.js → yaml-iSnWMVlV.js} +1 -1
  303. package/studio/web/index.html +1 -1
  304. package/upgrade/directives/5-3-0.ts +61 -54
  305. package/utility/DESIGN.md +5 -1
  306. package/utility/errors/hdbError.ts +56 -8
  307. package/utility/fsync.ts +16 -0
  308. package/utility/hdbTerms.ts +3 -0
  309. package/utility/signalling.ts +33 -4
  310. package/utility/when.ts +22 -0
  311. package/validation/configValidator.ts +36 -7
@@ -4,9 +4,10 @@
4
4
  // intercepted in workers.
5
5
  const { realExit } = require("./workerProcessGuard.js");
6
6
  const { Worker, MessageChannel, parentPort, isMainThread, threadId, workerData } = require('worker_threads');
7
- const { spawn, spawnSync } = require('node:child_process');
7
+ const { spawnSync } = require('node:child_process');
8
8
  const { readdirSync, readFileSync, readlinkSync } = require('node:fs');
9
9
  const { setTimeout: delay } = require('node:timers/promises');
10
+ const { confirmWindowsProcessTreeGone, ROOT_SPAWN_ALLOWANCE_MS } = require("./windowsProcessTree.js");
10
11
  const { join, isAbsolute, extname } = require('path');
11
12
  const { pathToFileURL } = require('url');
12
13
  const { server } = require("../Server.js");
@@ -22,6 +23,7 @@ const { resolvePreloadModules } = require("./resolvePreload.js");
22
23
  const { resolveThreadHeapMemoryMb } = require("./threadHeapMemory.js");
23
24
  const { getConfigPath } = require("../../config/configUtils.js");
24
25
  const { resolveWatchTarget } = require("../../utility/watchPath.js");
26
+ const { databaseDropPreparationSnapshot, handleDatabaseDropPreparationOwnerExit, } = require("../../resources/databaseDropPreparation.js");
25
27
  const { DIRECTORY_POLLING_FALLBACK_OPTIONS, claimLostNativeWatchError, guardedWatch, isWatcherExhaustionError, warnWatcherFallback, } = require("../../utility/watcherFallback.js");
26
28
  let importModules;
27
29
  function getImportModules() {
@@ -125,6 +127,14 @@ function restoreShutdownDeadline() {
125
127
  }
126
128
  catch { }
127
129
  }
130
+ function notifyJobCleanupComplete() {
131
+ for (const port of connectedPorts) {
132
+ try {
133
+ port.postMessage({ type: hdbTerms.ITC_EVENT_TYPES.JOB_CLEANUP_COMPLETE });
134
+ }
135
+ catch { }
136
+ }
137
+ }
128
138
  const listenersByType = new Map();
129
139
  const messagesQueuedByType = new Map();
130
140
  module.exports = {
@@ -151,6 +161,7 @@ module.exports = {
151
161
  onMessageByType,
152
162
  broadcast,
153
163
  broadcastWithAcknowledgement,
164
+ broadcastWithStrictAcknowledgement,
154
165
  getWorkerIndex,
155
166
  getWorkerCount,
156
167
  getEligibleBroadcastRecipientThreadIds,
@@ -159,6 +170,7 @@ module.exports = {
159
170
  setTerminateTimeout,
160
171
  extendShutdownDeadline,
161
172
  restoreShutdownDeadline,
173
+ notifyJobCleanupComplete,
162
174
  beginProcessShutdown,
163
175
  registerWorkerDataProvider,
164
176
  onThreadExit,
@@ -166,9 +178,11 @@ module.exports = {
166
178
  notifyThreadExit,
167
179
  registerProcessGroup,
168
180
  unregisterProcessGroup,
181
+ addProcessGroup,
182
+ removeProcessGroup,
183
+ terminateProcessGroupsForThread,
169
184
  isProcessGroupAlive,
170
185
  isThreadRunning,
171
- waitUntilConfirmedGone,
172
186
  restartNumber: workerData?.restartNumber || 1,
173
187
  // Identifies this process incarnation, where the PID cannot: a container reuses PID 1. Minted once
174
188
  // on the main thread and carried to workers, so live siblings agree on it — one derived per thread
@@ -347,6 +361,7 @@ const RESERVED_WORKER_DATA_KEYS = [
347
361
  'restartNumber',
348
362
  'processIncarnation',
349
363
  'ticketKeys',
364
+ 'databaseDropPreparations',
350
365
  'noServerStart',
351
366
  'isolatedApplication',
352
367
  '__proto__', // never a legitimate payload name; spread would define it as an own property
@@ -565,6 +580,7 @@ function startWorker(path, options = {}) {
565
580
  restartNumber: module.exports.restartNumber,
566
581
  processIncarnation: module.exports.processIncarnation,
567
582
  ticketKeys: getTicketKeys(),
583
+ databaseDropPreparations: databaseDropPreparationSnapshot(),
568
584
  },
569
585
  transferList: portsToSend,
570
586
  ...options,
@@ -1045,14 +1061,31 @@ async function broadcast(message, includeSelf) {
1045
1061
  const awaitingResponses = new Map();
1046
1062
  let nextId = 1;
1047
1063
  // Backstop so a wedged-but-alive worker (one whose event loop is blocked and never acks, yet
1048
- // whose port hasn't closed) can't hang a mutating admin/DDL op forever. The durable write has
1049
- // already succeeded by the time we broadcast, and the health monitor restarts a truly stuck
1050
- // worker (its port close fires the same ack handlers), so on timeout we proceed best-effort.
1064
+ // whose port hasn't closed) can't hang a mutating admin/DDL op forever. Ordinary broadcasts happen
1065
+ // after the durable write and proceed best-effort; strict preparation broadcasts reject on timeout.
1051
1066
  const DEFAULT_ACK_TIMEOUT_MS = 30000;
1052
- function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS) {
1053
- return new Promise((resolve) => {
1067
+ function settleAcknowledgementsForClosedWorker(matches, jobCleanupComplete, exitConfirmed) {
1068
+ for (const [, ackHandler] of awaitingResponses) {
1069
+ if (!matches(ackHandler.port))
1070
+ continue;
1071
+ if (ackHandler.allowNormalJobExit && !jobCleanupComplete && !exitConfirmed)
1072
+ continue;
1073
+ ackHandler(ackHandler.allowNormalJobExit && jobCleanupComplete ? undefined : ackHandler.closeResponse);
1074
+ }
1075
+ }
1076
+ function settleAcknowledgementsForClosedPort(port, jobCleanupComplete = false, exitConfirmed = false) {
1077
+ settleAcknowledgementsForClosedWorker((candidate) => candidate === port, jobCleanupComplete, exitConfirmed);
1078
+ }
1079
+ function settleAcknowledgementsForClosedThread(threadId, jobCleanupComplete = false, exitConfirmed = false) {
1080
+ settleAcknowledgementsForClosedWorker((candidate) => candidate.threadId === threadId, jobCleanupComplete, exitConfirmed);
1081
+ }
1082
+ /** @param {boolean|'active'} includeJobWorkers */
1083
+ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS, strict = false, includeJobWorkers = false) {
1084
+ return new Promise((resolve, reject) => {
1054
1085
  let waitingCount = 0;
1055
1086
  let timer;
1087
+ let initializing = true;
1088
+ const failures = [];
1056
1089
  // Tracks the handlers still awaiting an ack for THIS broadcast. Doubles as an
1057
1090
  // idempotency guard: a port's handler runs at most once whether it's driven by an ack,
1058
1091
  // the close listener, or the timeout below.
@@ -1062,22 +1095,48 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
1062
1095
  clearTimeout(timer);
1063
1096
  timer = undefined;
1064
1097
  }
1065
- resolve();
1098
+ if (strict && failures.length > 0) {
1099
+ if (failures.length === 1 &&
1100
+ (failures[0].name === 'DatabaseDroppingError' || failures[0].name === 'DatabaseDrainTimeoutError')) {
1101
+ reject(failures[0]);
1102
+ return;
1103
+ }
1104
+ const error = new AggregateError(failures, 'A worker could not prepare for the schema change');
1105
+ for (const property of ['name', 'code', 'statusCode', 'retryable']) {
1106
+ const value = failures[0][property];
1107
+ if (property === 'name' && value === 'Error')
1108
+ continue;
1109
+ if (value != null && failures.every((failure) => failure[property] === value))
1110
+ error[property] = value;
1111
+ }
1112
+ reject(error);
1113
+ }
1114
+ else
1115
+ resolve();
1066
1116
  };
1067
1117
  for (let port of connectedPorts) {
1068
- // Job workers run a single isolated task and exit; they don't participate in
1069
- // schema-change gossip. Including them causes a deadlock: the broadcast waits for
1070
- // the job worker's ACK while the job worker's event loop is busy waiting for the
1071
- // same broadcast to complete (re-entrant schema change triggered by the job op).
1072
- if (!isEligibleBroadcastRecipient(port))
1118
+ // Ordinary schema gossip excludes job workers to avoid re-entrant waits. Destructive
1119
+ // preparation opts them in because every native handle must be closed before deletion.
1120
+ if (port.isJobWorker &&
1121
+ (!includeJobWorkers || (includeJobWorkers === 'active' && port.jobCleanupComplete === true)))
1073
1122
  continue;
1123
+ let ackHandler;
1074
1124
  try {
1075
1125
  let requestId = nextId++;
1076
- const ackHandler = () => {
1126
+ ackHandler = (response) => {
1077
1127
  if (!pending.delete(ackHandler))
1078
1128
  return; // already settled for this port
1129
+ if (response?.error) {
1130
+ const error = new Error(`Worker ${port.threadId} could not prepare for the schema change: ${response.error.message ?? response.error}`);
1131
+ error.cause = response.error;
1132
+ for (const property of ['name', 'code', 'statusCode', 'retryable']) {
1133
+ if (response.error[property] != null)
1134
+ error[property] = response.error[property];
1135
+ }
1136
+ failures.push(error);
1137
+ }
1079
1138
  awaitingResponses.delete(requestId);
1080
- if (--waitingCount === 0) {
1139
+ if (--waitingCount === 0 && !initializing) {
1081
1140
  finish();
1082
1141
  }
1083
1142
  if (port !== parentPort && --port.refCount === 0) {
@@ -1085,39 +1144,39 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
1085
1144
  }
1086
1145
  };
1087
1146
  ackHandler.port = port;
1147
+ ackHandler.closeResponse = strict
1148
+ ? { error: { message: 'exited before acknowledging preparation' } }
1149
+ : undefined;
1150
+ ackHandler.allowNormalJobExit = strict && includeJobWorkers && port.isJobWorker;
1088
1151
  pending.add(ackHandler);
1089
- port.ref();
1152
+ waitingCount++;
1090
1153
  port.refCount = (port.refCount || 0) + 1;
1154
+ port.ref();
1091
1155
  awaitingResponses.set((message.requestId = requestId), ackHandler);
1092
1156
  if (!port.hasAckCloseListener) {
1093
1157
  // just set a single close listener that can clean up all the ack handlers for a port that is closed
1094
1158
  port.hasAckCloseListener = true;
1095
- port.on(port.close ? 'close' : 'exit', () => {
1096
- for (let [, ackHandler] of awaitingResponses) {
1097
- if (ackHandler.port === port) {
1098
- ackHandler();
1099
- }
1100
- }
1101
- });
1159
+ port.on(port.close ? 'close' : 'exit', () => settleAcknowledgementsForClosedPort(port, port.jobCleanupComplete === true));
1102
1160
  }
1103
1161
  port.postMessage(message);
1104
- waitingCount++;
1105
1162
  }
1106
1163
  catch (error) {
1107
1164
  harperLogger.error(`Unable to send message to worker`, error);
1165
+ ackHandler?.({ error: { message: error.message ?? String(error) } });
1108
1166
  }
1109
1167
  }
1168
+ initializing = false;
1110
1169
  if (waitingCount === 0)
1111
- return resolve();
1170
+ return finish();
1112
1171
  if (timeout > 0) {
1113
1172
  timer = setTimeout(() => {
1114
1173
  timer = undefined;
1115
1174
  const stuck = [];
1116
1175
  for (let ackHandler of [...pending]) {
1117
1176
  stuck.push(ackHandler.port);
1118
- ackHandler(); // same cleanup path as an ack/close; drives waitingCount to 0 and resolves
1177
+ ackHandler(strict ? { error: { message: `did not acknowledge within ${timeout}ms` } } : undefined); // same cleanup path as an ack/close; drives waitingCount to 0 and settles
1119
1178
  }
1120
- 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`);
1179
+ harperLogger.warn(`ITC broadcast (type ${message.type}) not acknowledged by worker thread(s) ${stuck.map((port) => port?.threadId).join(', ')} within ${timeout}ms; ${strict ? 'failing the coordinated operation' : 'proceeding best-effort'}`);
1121
1180
  if (isMainThread)
1122
1181
  for (let port of stuck)
1123
1182
  logStuckWorkerDiagnostics(port);
@@ -1132,6 +1191,10 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
1132
1191
  }
1133
1192
  });
1134
1193
  }
1194
+ /** @param {boolean|'active'} includeJobWorkers */
1195
+ function broadcastWithStrictAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS, includeJobWorkers = false) {
1196
+ return broadcastWithAcknowledgement(message, timeout, true, includeJobWorkers);
1197
+ }
1135
1198
  // Linux only: /proc/thread-self resolves to <pid>/task/<tid> for the calling thread.
1136
1199
  function getOsThreadId() {
1137
1200
  try {
@@ -1472,6 +1535,10 @@ const PROCESS_GROUP_LIVENESS_WARNING_MS = 30000;
1472
1535
  const THREAD_RUNNING_TERMINATION_BACKSTOP_MS = PROCESS_GROUP_LIVENESS_WARNING_MS;
1473
1536
  const zombieGroupScanTimes = new Map();
1474
1537
  const processGroupLivenessStates = new Map();
1538
+ // When each group's root was registered — by then it was already running, which is what lets the
1539
+ // Windows scan tell our root from a later process that recycled its PID.
1540
+ const processGroupSpawnedAt = new Map();
1541
+ let processGroupRegistrationGeneration = 0;
1475
1542
  function processGroupExists(processGroupId) {
1476
1543
  try {
1477
1544
  process.kill(-processGroupId, 0);
@@ -1604,102 +1671,111 @@ function isProcessGroupAlive(processGroupId, options) {
1604
1671
  function processGroupIsAlive(processGroupId) {
1605
1672
  return isProcessGroupAlive(processGroupId);
1606
1673
  }
1607
- async function waitForProcessGroupExit(processGroupId) {
1608
- while (processGroupIsAlive(processGroupId))
1609
- await delay(PROCESS_GROUP_TERMINATION_POLL_MS);
1610
- }
1611
- // Mirrors Application.ts's windowsProcessTreeIsAlive: a descendant retains its ParentProcessId
1612
- // after its parent exits, which is exactly the taskkill "process not found" race, so query the
1613
- // process table by walking parentage rather than trusting only the root pid.
1614
- function windowsProcessTreeIsAlive(rootPid) {
1615
- // Exit code 1 must mean "queried the process table and positively found nothing" — never
1616
- // "the query itself failed" (e.g. Get-CimInstance denied or WMI unavailable), which would
1617
- // otherwise read identically to a confirmed-gone tree and release the lock while a descendant
1618
- // may still be alive. ErrorActionPreference=Stop plus the wrapping try/catch turns a query
1619
- // failure into its own exit code (2), which the caller below already treats as unknown.
1620
- const script = "$ErrorActionPreference = 'Stop'; try { " +
1621
- `$rootPid = ${rootPid}; ` +
1622
- '$all = @(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId); ' +
1623
- '$frontier = @($rootPid); $seen = @{}; $found = $false; ' +
1624
- 'while ($frontier.Count -gt 0) { ' +
1625
- '$next = @(); foreach ($parentPid in $frontier) { if ($seen[$parentPid]) { continue }; ' +
1626
- '$seen[$parentPid] = $true; foreach ($p in $all) { ' +
1627
- 'if ($p.ProcessId -eq $parentPid) { $found = $true }; ' +
1628
- 'if ($p.ParentProcessId -eq $parentPid) { $found = $true; $next += [int]$p.ProcessId } } }; ' +
1629
- '$frontier = $next }; ' +
1630
- 'if ($found) { exit 0 } else { exit 1 } ' +
1631
- '} catch { exit 2 }';
1632
- return new Promise((resolve) => {
1633
- const query = spawn('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], {
1634
- stdio: 'ignore',
1635
- windowsHide: true,
1636
- });
1637
- query.once('close', (code) => resolve(code === 0 ? true : code === 1 ? false : null));
1638
- query.once('error', () => resolve(null));
1639
- });
1640
- }
1641
- // The initial taskkill in terminateProcessGroupsForThread is fired synchronously (required so
1642
- // that call still works from a process `exit` handler) but its result is not checked there, since
1643
- // a nonzero exit is ambiguous between a real failure and the target having already exited. Confirm
1644
- // via the process table and keep retrying taskkill until the whole tree is positively gone —
1645
- // reclamation must not proceed on an unconfirmed guess either way. Exported so a test can drive the
1646
- // retry logic with injected callbacks instead of real Windows processes.
1647
- async function waitUntilConfirmedGone(attemptTermination, treeIsAlive, pollMs) {
1648
- for (;;) {
1649
- // A successful taskkill exit only proves the request was accepted, not that the whole tree
1650
- // has actually exited — Windows termination is asynchronous, and taskkill can report overall
1651
- // success even when a descendant is not yet (or never) reaped. Only an explicit `false` from
1652
- // treeIsAlive, independently confirming no member of the tree remains, is safe to return on;
1653
- // `true` or `null` (unknown) must keep the loop retrying.
1654
- await attemptTermination();
1655
- if ((await treeIsAlive()) === false)
1674
+ async function waitForProcessGroupExit(processGroupId, registration) {
1675
+ while (true) {
1676
+ const currentRegistration = processGroupSpawnedAt.get(processGroupId);
1677
+ if (currentRegistration !== undefined && currentRegistration !== registration)
1656
1678
  return;
1657
- await delay(pollMs);
1679
+ if (!processGroupIsAlive(processGroupId))
1680
+ return;
1681
+ await delay(PROCESS_GROUP_TERMINATION_POLL_MS);
1658
1682
  }
1659
1683
  }
1660
- function waitForWindowsGroupExit(processGroupId) {
1661
- return waitUntilConfirmedGone(() => new Promise((resolve) => {
1662
- const taskkill = spawn('taskkill', ['/pid', String(processGroupId), '/T', '/F'], {
1663
- stdio: 'ignore',
1664
- windowsHide: true,
1665
- });
1666
- taskkill.once('close', (code) => resolve(code === 0));
1667
- taskkill.once('error', () => resolve(false));
1668
- }), () => windowsProcessTreeIsAlive(processGroupId), PROCESS_GROUP_TERMINATION_POLL_MS);
1669
- }
1670
- function addProcessGroup(ownerThreadId, processGroupId) {
1684
+ // The initial taskkill in terminateProcessGroupsForThread is fired synchronously (required so
1685
+ // that call still works from a process `exit` handler). A nonzero exit there is ambiguous between
1686
+ // a real failure and the target having already exited, so only a reported success bounds the
1687
+ // root's lifetime up front (a terminated process cannot spawn); otherwise the scan latches the
1688
+ // root's exit itself, re-terminating the root while it is still found running as ours. Either
1689
+ // way the wait confirms via the process table — reclamation must not proceed on a guess. The
1690
+ // root was created inside the spawner's spawn() call, whose start and return times travel with the
1691
+ // registration so the cross-thread hop adds nothing to the window before it.
1692
+ function waitForWindowsGroupExit(processGroupId, spawn, killedAt) {
1693
+ return confirmWindowsProcessTreeGone({
1694
+ rootPid: processGroupId,
1695
+ rootKnownAt: spawn?.spawnedAt ?? killedAt ?? Date.now(),
1696
+ rootStartedWithinMs: spawn?.spawnStartedAt !== undefined ? spawn.spawnedAt - spawn.spawnStartedAt : ROOT_SPAWN_ALLOWANCE_MS,
1697
+ rootExitedAt: killedAt,
1698
+ }, { pollMs: PROCESS_GROUP_TERMINATION_POLL_MS, label: `process group ${processGroupId}` });
1699
+ }
1700
+ function addProcessGroup(ownerThreadId, processGroupId, spawnedAt, spawnStartedAt, registrationGeneration) {
1671
1701
  if (!Number.isInteger(processGroupId) || processGroupId <= 0)
1672
1702
  return;
1703
+ const previousRegistration = processGroupSpawnedAt.get(processGroupId);
1704
+ if (previousRegistration &&
1705
+ (previousRegistration.ownerThreadId !== ownerThreadId ||
1706
+ previousRegistration.registrationGeneration !== registrationGeneration)) {
1707
+ clearProcessGroupLivenessState(processGroupId);
1708
+ }
1673
1709
  let processGroups = processGroupsByThread.get(ownerThreadId);
1674
1710
  if (!processGroups)
1675
1711
  processGroupsByThread.set(ownerThreadId, (processGroups = new Set()));
1676
1712
  processGroups.add(processGroupId);
1713
+ // The map is keyed by PID, which the OS reuses the instant a process exits. Owner distinguishes
1714
+ // threads; generation distinguishes two children of the same thread that receive the same PID.
1715
+ processGroupSpawnedAt.set(processGroupId, {
1716
+ ownerThreadId,
1717
+ registrationGeneration,
1718
+ spawnedAt: Number.isFinite(spawnedAt) ? spawnedAt : Date.now(),
1719
+ spawnStartedAt: Number.isFinite(spawnStartedAt) && spawnStartedAt <= spawnedAt ? spawnStartedAt : undefined,
1720
+ });
1677
1721
  }
1678
- function removeProcessGroup(ownerThreadId, processGroupId) {
1679
- clearProcessGroupLivenessState(processGroupId);
1680
- const processGroups = processGroupsByThread.get(ownerThreadId);
1681
- if (!processGroups)
1722
+ function removeProcessGroup(ownerThreadId, processGroupId, registrationGeneration) {
1723
+ const currentRegistration = processGroupSpawnedAt.get(processGroupId);
1724
+ const sameOwnerNewerGeneration = currentRegistration?.ownerThreadId === ownerThreadId &&
1725
+ currentRegistration.registrationGeneration !== registrationGeneration;
1726
+ if (!sameOwnerNewerGeneration) {
1727
+ const processGroups = processGroupsByThread.get(ownerThreadId);
1728
+ if (processGroups?.delete(processGroupId) && processGroups.size === 0) {
1729
+ processGroupsByThread.delete(ownerThreadId);
1730
+ }
1731
+ }
1732
+ if (currentRegistration?.ownerThreadId !== ownerThreadId || sameOwnerNewerGeneration)
1682
1733
  return;
1683
- processGroups.delete(processGroupId);
1684
- if (processGroups.size === 0)
1685
- processGroupsByThread.delete(ownerThreadId);
1734
+ clearProcessGroupLivenessState(processGroupId);
1735
+ processGroupSpawnedAt.delete(processGroupId);
1686
1736
  }
1687
1737
  // Returns a promise that resolves once every process group tracked for `ownerThreadId` is
1688
1738
  // confirmed terminated. Callers that only need to fire the termination (e.g. the `exit` handler
1689
1739
  // below) can ignore the returned promise; isThreadRunning awaits it before declaring a dead
1690
- // owner reclaimable. The kill signal for every group is sent synchronously, before any `await` —
1691
- // this runs from a process `exit` handler too, where nothing queued after a suspension point is
1692
- // guaranteed to run.
1693
- function terminateProcessGroupsForThread(ownerThreadId) {
1740
+ // owner reclaimable. On POSIX the kill signal for every group is sent synchronously, before any
1741
+ // `await` — this runs from a process `exit` handler too, where nothing queued after a suspension
1742
+ // point is guaranteed to run, and a process-group SIGKILL is directed at the group id, not a PID
1743
+ // Windows could have already reissued. On Windows a synchronous `taskkill /pid` has no such
1744
+ // process-group semantics — a PID whose process already exited is indistinguishable from one that
1745
+ // never will be, so an unconditional pre-kill can hit whatever now holds a recycled PID (the exact
1746
+ // harper#2273 unrelated-process kill this module exists to prevent). `fromExitHandler` restricts
1747
+ // that blind pre-kill to the one caller that genuinely cannot await anything first (a synchronous
1748
+ // scan via spawnSync before killing is possible there too, just not done); every other
1749
+ // caller lets the identity-checked confirmation loop below issue the first kill, after its own
1750
+ // scan has verified who the PID currently belongs to — UNLESS the process itself is already
1751
+ // shutting down (`processShuttingDown`, set before a restart tears its workers down too): that
1752
+ // path has no guarantee the async loop gets even one scan in before `process.exit()` runs, and by
1753
+ // then this function has already dropped the registration the exit handler would otherwise have
1754
+ // caught, so the blind kill has to fire here instead.
1755
+ function terminateProcessGroupsForThread(ownerThreadId, { fromExitHandler = false } = {}) {
1694
1756
  const processGroups = processGroupsByThread.get(ownerThreadId);
1695
1757
  if (!processGroups)
1696
1758
  return pendingProcessGroupTerminations.get(ownerThreadId) ?? Promise.resolve();
1697
1759
  processGroupsByThread.delete(ownerThreadId);
1698
- const groupIds = [...processGroups];
1760
+ // Membership is not ownership: a PID is reusable the moment its process exits.
1761
+ const groupIds = [...processGroups].filter((processGroupId) => {
1762
+ if (processGroupSpawnedAt.get(processGroupId)?.ownerThreadId === ownerThreadId)
1763
+ return true;
1764
+ harperLogger.warn(`Not terminating process group ${processGroupId} for thread ${ownerThreadId}: it is no longer registered to this thread`);
1765
+ return false;
1766
+ });
1767
+ const killedAt = new Map();
1699
1768
  for (const processGroupId of groupIds) {
1700
1769
  try {
1701
1770
  if (process.platform === 'win32') {
1702
- spawnSync('taskkill', ['/pid', String(processGroupId), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
1771
+ if (!fromExitHandler && !processShuttingDown)
1772
+ continue;
1773
+ const result = spawnSync('taskkill', ['/pid', String(processGroupId), '/T', '/F'], {
1774
+ stdio: 'ignore',
1775
+ windowsHide: true,
1776
+ });
1777
+ if (result.status === 0)
1778
+ killedAt.set(processGroupId, Date.now());
1703
1779
  }
1704
1780
  else {
1705
1781
  process.kill(-processGroupId, 'SIGKILL');
@@ -1710,7 +1786,17 @@ function terminateProcessGroupsForThread(ownerThreadId) {
1710
1786
  harperLogger.warn(`Failed to terminate process group ${processGroupId}:`, error);
1711
1787
  }
1712
1788
  }
1713
- const termination = Promise.all(groupIds.map((processGroupId) => process.platform === 'win32' ? waitForWindowsGroupExit(processGroupId) : waitForProcessGroupExit(processGroupId))).finally(() => {
1789
+ const termination = Promise.all(groupIds.map((processGroupId) => {
1790
+ const registration = processGroupSpawnedAt.get(processGroupId);
1791
+ const wait = process.platform === 'win32'
1792
+ ? waitForWindowsGroupExit(processGroupId, registration, killedAt.get(processGroupId))
1793
+ : waitForProcessGroupExit(processGroupId, registration);
1794
+ return wait.finally(() => {
1795
+ if (processGroupSpawnedAt.get(processGroupId) === registration) {
1796
+ processGroupSpawnedAt.delete(processGroupId);
1797
+ }
1798
+ });
1799
+ })).finally(() => {
1714
1800
  if (pendingProcessGroupTerminations.get(ownerThreadId) === termination) {
1715
1801
  pendingProcessGroupTerminations.delete(ownerThreadId);
1716
1802
  }
@@ -1718,17 +1804,28 @@ function terminateProcessGroupsForThread(ownerThreadId) {
1718
1804
  pendingProcessGroupTerminations.set(ownerThreadId, termination);
1719
1805
  return termination;
1720
1806
  }
1721
- function registerProcessGroup(processGroupId) {
1807
+ // `spawnedAt` / `spawnStartedAt`: the caller's clock as its spawn() of the group's root returned and
1808
+ // as it was called — the root was created between the two.
1809
+ function registerProcessGroup(processGroupId, spawnedAt = Date.now(), spawnStartedAt) {
1810
+ const registrationGeneration = ++processGroupRegistrationGeneration;
1722
1811
  if (isMainThread)
1723
- addProcessGroup(threadId, processGroupId);
1724
- else
1725
- parentPort?.postMessage({ type: REGISTER_PROCESS_GROUP, processGroupId });
1812
+ addProcessGroup(threadId, processGroupId, spawnedAt, spawnStartedAt, registrationGeneration);
1813
+ else {
1814
+ parentPort?.postMessage({
1815
+ type: REGISTER_PROCESS_GROUP,
1816
+ processGroupId,
1817
+ spawnedAt,
1818
+ spawnStartedAt,
1819
+ registrationGeneration,
1820
+ });
1821
+ }
1822
+ return registrationGeneration;
1726
1823
  }
1727
- function unregisterProcessGroup(processGroupId) {
1824
+ function unregisterProcessGroup(processGroupId, registrationGeneration) {
1728
1825
  if (isMainThread)
1729
- removeProcessGroup(threadId, processGroupId);
1826
+ removeProcessGroup(threadId, processGroupId, registrationGeneration);
1730
1827
  else
1731
- parentPort?.postMessage({ type: UNREGISTER_PROCESS_GROUP, processGroupId });
1828
+ parentPort?.postMessage({ type: UNREGISTER_PROCESS_GROUP, processGroupId, registrationGeneration });
1732
1829
  }
1733
1830
  class ProcessGroupTerminationUnconfirmedError extends Error {
1734
1831
  code = 'ERR_PROCESS_GROUP_TERMINATION_UNCONFIRMED';
@@ -1776,7 +1873,7 @@ async function isThreadRunning(ownerThreadId, timeoutMs = THREAD_INFO_REQUEST_TI
1776
1873
  if (isMainThread) {
1777
1874
  process.on('exit', () => {
1778
1875
  for (const ownerThreadId of [...processGroupsByThread.keys()])
1779
- terminateProcessGroupsForThread(ownerThreadId);
1876
+ terminateProcessGroupsForThread(ownerThreadId, { fromExitHandler: true });
1780
1877
  });
1781
1878
  }
1782
1879
  /**
@@ -1791,6 +1888,7 @@ function notifyThreadExit(deadThreadId) {
1791
1888
  if (deadThreadId == null || notifiedDeadThreadIds.has(deadThreadId))
1792
1889
  return;
1793
1890
  notifiedDeadThreadIds.add(deadThreadId);
1891
+ handleDatabaseDropPreparationOwnerExit(deadThreadId);
1794
1892
  for (const listener of threadExitListeners) {
1795
1893
  try {
1796
1894
  listener(deadThreadId);
@@ -1805,6 +1903,8 @@ function removePort(port, deadThreadId) {
1805
1903
  // cleanup must still run when the authoritative close/exit event reaches this thread.
1806
1904
  if (deadThreadId != null)
1807
1905
  terminateProcessGroupsForThread(deadThreadId);
1906
+ const exitConfirmed = !port.close;
1907
+ settleAcknowledgementsForClosedPort(port, port.jobCleanupComplete === true, exitConfirmed);
1808
1908
  const idx = connectedPorts.indexOf(port);
1809
1909
  if (idx === -1)
1810
1910
  return;
@@ -1819,7 +1919,12 @@ function removePort(port, deadThreadId) {
1819
1919
  if (deadThreadId != null) {
1820
1920
  for (let remainingPort of connectedPorts) {
1821
1921
  try {
1822
- remainingPort.postMessage({ type: REMOVE_PORT, threadId: deadThreadId });
1922
+ remainingPort.postMessage({
1923
+ type: REMOVE_PORT,
1924
+ threadId: deadThreadId,
1925
+ jobCleanupComplete: port.jobCleanupComplete === true,
1926
+ exitConfirmed,
1927
+ });
1823
1928
  }
1824
1929
  catch {
1825
1930
  // port may already be dead; ignore
@@ -1836,10 +1941,14 @@ function addPort(port, keepRef, isJobWorker) {
1836
1941
  port
1837
1942
  .on('message', (message) => {
1838
1943
  if (message.type === REGISTER_PROCESS_GROUP) {
1839
- addProcessGroup(portThreadId, message.processGroupId);
1944
+ addProcessGroup(portThreadId, message.processGroupId, message.spawnedAt, message.spawnStartedAt, message.registrationGeneration);
1840
1945
  }
1841
1946
  else if (message.type === UNREGISTER_PROCESS_GROUP) {
1842
- removeProcessGroup(portThreadId, message.processGroupId);
1947
+ removeProcessGroup(portThreadId, message.processGroupId, message.registrationGeneration);
1948
+ }
1949
+ else if (message.type === hdbTerms.ITC_EVENT_TYPES.JOB_CLEANUP_COMPLETE) {
1950
+ port.jobCleanupComplete = true;
1951
+ settleAcknowledgementsForClosedPort(port, true);
1843
1952
  }
1844
1953
  else if (message.type === ADDED_PORT) {
1845
1954
  message.port.threadId = message.threadId;
@@ -1848,13 +1957,20 @@ function addPort(port, keepRef, isJobWorker) {
1848
1957
  else if (message.type === ACKNOWLEDGEMENT) {
1849
1958
  let completion = awaitingResponses.get(message.id);
1850
1959
  if (completion) {
1851
- completion();
1960
+ completion(message);
1852
1961
  }
1853
1962
  }
1854
1963
  else if (message.type === REMOVE_PORT) {
1855
- const idx = connectedPorts.findIndex((p) => p.threadId === message.threadId);
1856
- if (idx !== -1)
1857
- connectedPorts.splice(idx, 1);
1964
+ const removedPort = connectedPorts.find((candidate) => candidate.threadId === message.threadId);
1965
+ if (removedPort && !removedPort.close && !message.exitConfirmed)
1966
+ return;
1967
+ settleAcknowledgementsForClosedThread(message.threadId, message.jobCleanupComplete === true, message.exitConfirmed === true);
1968
+ if (removedPort) {
1969
+ if (message.jobCleanupComplete)
1970
+ removedPort.jobCleanupComplete = true;
1971
+ settleAcknowledgementsForClosedPort(removedPort, removedPort.jobCleanupComplete === true);
1972
+ connectedPorts.splice(connectedPorts.indexOf(removedPort), 1);
1973
+ }
1858
1974
  // A sibling's port-to-the-dead-worker can close (and broadcast this) before this
1859
1975
  // thread's OWN port to that worker fires its 'close'/'exit' — at which point
1860
1976
  // removePort() would no-op (already spliced) and threadExitListeners would never
@@ -1866,10 +1982,16 @@ function addPort(port, keepRef, isJobWorker) {
1866
1982
  }
1867
1983
  })
1868
1984
  .on('close', () => {
1869
- removePort(port, portThreadId);
1985
+ // A worker's parentPort closing is this worker leaving, not main exiting (server/DESIGN.md).
1986
+ removePort(port, port === parentPort ? undefined : portThreadId);
1870
1987
  })
1871
1988
  .on('exit', () => {
1872
- removePort(port, portThreadId);
1989
+ // Let a cleanup proof already queued by the worker reach this port before exit becomes
1990
+ // authoritative. The next turn still fails closed if no proof arrives.
1991
+ if (port.isJobWorker && !port.jobCleanupComplete)
1992
+ setImmediate(() => removePort(port, portThreadId));
1993
+ else
1994
+ removePort(port, portThreadId);
1873
1995
  });
1874
1996
  if (keepRef)
1875
1997
  port.refCount = 100;