@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
@@ -6,9 +6,10 @@
6
6
  const { realExit } = require('./workerProcessGuard.ts');
7
7
 
8
8
  const { Worker, MessageChannel, parentPort, isMainThread, threadId, workerData } = require('worker_threads');
9
- const { spawn, spawnSync } = require('node:child_process');
9
+ const { spawnSync } = require('node:child_process');
10
10
  const { readdirSync, readFileSync, readlinkSync } = require('node:fs');
11
11
  const { setTimeout: delay } = require('node:timers/promises');
12
+ const { confirmWindowsProcessTreeGone, ROOT_SPAWN_ALLOWANCE_MS } = require('./windowsProcessTree.ts');
12
13
  const { join, isAbsolute, extname } = require('path');
13
14
  const { pathToFileURL } = require('url');
14
15
  const { server } = require('../Server.ts');
@@ -24,6 +25,10 @@ const { resolvePreloadModules } = require('./resolvePreload.ts');
24
25
  const { resolveThreadHeapMemoryMb } = require('./threadHeapMemory.ts');
25
26
  const { getConfigPath } = require('../../config/configUtils.ts');
26
27
  const { resolveWatchTarget } = require('../../utility/watchPath.ts');
28
+ const {
29
+ databaseDropPreparationSnapshot,
30
+ handleDatabaseDropPreparationOwnerExit,
31
+ } = require('../../resources/databaseDropPreparation.ts');
27
32
  const {
28
33
  DIRECTORY_POLLING_FALLBACK_OPTIONS,
29
34
  claimLostNativeWatchError,
@@ -143,6 +148,14 @@ function restoreShutdownDeadline() {
143
148
  } catch {}
144
149
  }
145
150
 
151
+ function notifyJobCleanupComplete() {
152
+ for (const port of connectedPorts) {
153
+ try {
154
+ port.postMessage({ type: hdbTerms.ITC_EVENT_TYPES.JOB_CLEANUP_COMPLETE });
155
+ } catch {}
156
+ }
157
+ }
158
+
146
159
  const listenersByType = new Map();
147
160
  const messagesQueuedByType = new Map();
148
161
 
@@ -170,6 +183,7 @@ module.exports = {
170
183
  onMessageByType,
171
184
  broadcast,
172
185
  broadcastWithAcknowledgement,
186
+ broadcastWithStrictAcknowledgement,
173
187
  getWorkerIndex,
174
188
  getWorkerCount,
175
189
  getEligibleBroadcastRecipientThreadIds,
@@ -178,6 +192,7 @@ module.exports = {
178
192
  setTerminateTimeout,
179
193
  extendShutdownDeadline,
180
194
  restoreShutdownDeadline,
195
+ notifyJobCleanupComplete,
181
196
  beginProcessShutdown,
182
197
  registerWorkerDataProvider,
183
198
  onThreadExit,
@@ -185,9 +200,11 @@ module.exports = {
185
200
  notifyThreadExit,
186
201
  registerProcessGroup,
187
202
  unregisterProcessGroup,
203
+ addProcessGroup,
204
+ removeProcessGroup,
205
+ terminateProcessGroupsForThread,
188
206
  isProcessGroupAlive,
189
207
  isThreadRunning,
190
- waitUntilConfirmedGone,
191
208
  restartNumber: workerData?.restartNumber || 1,
192
209
  // Identifies this process incarnation, where the PID cannot: a container reuses PID 1. Minted once
193
210
  // on the main thread and carried to workers, so live siblings agree on it — one derived per thread
@@ -360,6 +377,7 @@ const RESERVED_WORKER_DATA_KEYS = [
360
377
  'restartNumber',
361
378
  'processIncarnation',
362
379
  'ticketKeys',
380
+ 'databaseDropPreparations',
363
381
  'noServerStart',
364
382
  'isolatedApplication',
365
383
  '__proto__', // never a legitimate payload name; spread would define it as an own property
@@ -571,6 +589,7 @@ function startWorker(path, options = {}) {
571
589
  restartNumber: module.exports.restartNumber,
572
590
  processIncarnation: module.exports.processIncarnation,
573
591
  ticketKeys: getTicketKeys(),
592
+ databaseDropPreparations: databaseDropPreparationSnapshot(),
574
593
  },
575
594
  transferList: portsToSend,
576
595
  ...options,
@@ -1060,14 +1079,41 @@ async function broadcast(message, includeSelf) {
1060
1079
  const awaitingResponses = new Map();
1061
1080
  let nextId = 1;
1062
1081
  // Backstop so a wedged-but-alive worker (one whose event loop is blocked and never acks, yet
1063
- // whose port hasn't closed) can't hang a mutating admin/DDL op forever. The durable write has
1064
- // already succeeded by the time we broadcast, and the health monitor restarts a truly stuck
1065
- // worker (its port close fires the same ack handlers), so on timeout we proceed best-effort.
1082
+ // whose port hasn't closed) can't hang a mutating admin/DDL op forever. Ordinary broadcasts happen
1083
+ // after the durable write and proceed best-effort; strict preparation broadcasts reject on timeout.
1066
1084
  const DEFAULT_ACK_TIMEOUT_MS = 30000;
1067
- function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS) {
1068
- return new Promise((resolve) => {
1085
+ function settleAcknowledgementsForClosedWorker(matches, jobCleanupComplete, exitConfirmed) {
1086
+ for (const [, ackHandler] of awaitingResponses) {
1087
+ if (!matches(ackHandler.port)) continue;
1088
+ if (ackHandler.allowNormalJobExit && !jobCleanupComplete && !exitConfirmed) continue;
1089
+ ackHandler(ackHandler.allowNormalJobExit && jobCleanupComplete ? undefined : ackHandler.closeResponse);
1090
+ }
1091
+ }
1092
+
1093
+ function settleAcknowledgementsForClosedPort(port, jobCleanupComplete = false, exitConfirmed = false) {
1094
+ settleAcknowledgementsForClosedWorker((candidate) => candidate === port, jobCleanupComplete, exitConfirmed);
1095
+ }
1096
+
1097
+ function settleAcknowledgementsForClosedThread(threadId, jobCleanupComplete = false, exitConfirmed = false) {
1098
+ settleAcknowledgementsForClosedWorker(
1099
+ (candidate) => candidate.threadId === threadId,
1100
+ jobCleanupComplete,
1101
+ exitConfirmed
1102
+ );
1103
+ }
1104
+
1105
+ /** @param {boolean|'active'} includeJobWorkers */
1106
+ function broadcastWithAcknowledgement(
1107
+ message,
1108
+ timeout = DEFAULT_ACK_TIMEOUT_MS,
1109
+ strict = false,
1110
+ includeJobWorkers = false
1111
+ ) {
1112
+ return new Promise((resolve, reject) => {
1069
1113
  let waitingCount = 0;
1070
1114
  let timer;
1115
+ let initializing = true;
1116
+ const failures = [];
1071
1117
  // Tracks the handlers still awaiting an ack for THIS broadcast. Doubles as an
1072
1118
  // idempotency guard: a port's handler runs at most once whether it's driven by an ack,
1073
1119
  // the close listener, or the timeout below.
@@ -1077,20 +1123,48 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
1077
1123
  clearTimeout(timer);
1078
1124
  timer = undefined;
1079
1125
  }
1080
- resolve();
1126
+ if (strict && failures.length > 0) {
1127
+ if (
1128
+ failures.length === 1 &&
1129
+ (failures[0].name === 'DatabaseDroppingError' || failures[0].name === 'DatabaseDrainTimeoutError')
1130
+ ) {
1131
+ reject(failures[0]);
1132
+ return;
1133
+ }
1134
+ const error = new AggregateError(failures, 'A worker could not prepare for the schema change');
1135
+ for (const property of ['name', 'code', 'statusCode', 'retryable']) {
1136
+ const value = failures[0][property];
1137
+ if (property === 'name' && value === 'Error') continue;
1138
+ if (value != null && failures.every((failure) => failure[property] === value)) error[property] = value;
1139
+ }
1140
+ reject(error);
1141
+ } else resolve();
1081
1142
  };
1082
1143
  for (let port of connectedPorts) {
1083
- // Job workers run a single isolated task and exit; they don't participate in
1084
- // schema-change gossip. Including them causes a deadlock: the broadcast waits for
1085
- // the job worker's ACK while the job worker's event loop is busy waiting for the
1086
- // same broadcast to complete (re-entrant schema change triggered by the job op).
1087
- if (!isEligibleBroadcastRecipient(port)) continue;
1144
+ // Ordinary schema gossip excludes job workers to avoid re-entrant waits. Destructive
1145
+ // preparation opts them in because every native handle must be closed before deletion.
1146
+ if (
1147
+ port.isJobWorker &&
1148
+ (!includeJobWorkers || (includeJobWorkers === 'active' && port.jobCleanupComplete === true))
1149
+ )
1150
+ continue;
1151
+ let ackHandler;
1088
1152
  try {
1089
1153
  let requestId = nextId++;
1090
- const ackHandler = () => {
1154
+ ackHandler = (response) => {
1091
1155
  if (!pending.delete(ackHandler)) return; // already settled for this port
1156
+ if (response?.error) {
1157
+ const error = new Error(
1158
+ `Worker ${port.threadId} could not prepare for the schema change: ${response.error.message ?? response.error}`
1159
+ );
1160
+ error.cause = response.error;
1161
+ for (const property of ['name', 'code', 'statusCode', 'retryable']) {
1162
+ if (response.error[property] != null) error[property] = response.error[property];
1163
+ }
1164
+ failures.push(error);
1165
+ }
1092
1166
  awaitingResponses.delete(requestId);
1093
- if (--waitingCount === 0) {
1167
+ if (--waitingCount === 0 && !initializing) {
1094
1168
  finish();
1095
1169
  }
1096
1170
  if (port !== parentPort && --port.refCount === 0) {
@@ -1098,38 +1172,40 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
1098
1172
  }
1099
1173
  };
1100
1174
  ackHandler.port = port;
1175
+ ackHandler.closeResponse = strict
1176
+ ? { error: { message: 'exited before acknowledging preparation' } }
1177
+ : undefined;
1178
+ ackHandler.allowNormalJobExit = strict && includeJobWorkers && port.isJobWorker;
1101
1179
  pending.add(ackHandler);
1102
- port.ref();
1180
+ waitingCount++;
1103
1181
  port.refCount = (port.refCount || 0) + 1;
1182
+ port.ref();
1104
1183
  awaitingResponses.set((message.requestId = requestId), ackHandler);
1105
1184
  if (!port.hasAckCloseListener) {
1106
1185
  // just set a single close listener that can clean up all the ack handlers for a port that is closed
1107
1186
  port.hasAckCloseListener = true;
1108
- port.on(port.close ? 'close' : 'exit', () => {
1109
- for (let [, ackHandler] of awaitingResponses) {
1110
- if (ackHandler.port === port) {
1111
- ackHandler();
1112
- }
1113
- }
1114
- });
1187
+ port.on(port.close ? 'close' : 'exit', () =>
1188
+ settleAcknowledgementsForClosedPort(port, port.jobCleanupComplete === true)
1189
+ );
1115
1190
  }
1116
1191
  port.postMessage(message);
1117
- waitingCount++;
1118
1192
  } catch (error) {
1119
1193
  harperLogger.error(`Unable to send message to worker`, error);
1194
+ ackHandler?.({ error: { message: error.message ?? String(error) } });
1120
1195
  }
1121
1196
  }
1122
- if (waitingCount === 0) return resolve();
1197
+ initializing = false;
1198
+ if (waitingCount === 0) return finish();
1123
1199
  if (timeout > 0) {
1124
1200
  timer = setTimeout(() => {
1125
1201
  timer = undefined;
1126
1202
  const stuck = [];
1127
1203
  for (let ackHandler of [...pending]) {
1128
1204
  stuck.push(ackHandler.port);
1129
- ackHandler(); // same cleanup path as an ack/close; drives waitingCount to 0 and resolves
1205
+ ackHandler(strict ? { error: { message: `did not acknowledge within ${timeout}ms` } } : undefined); // same cleanup path as an ack/close; drives waitingCount to 0 and settles
1130
1206
  }
1131
1207
  harperLogger.warn(
1132
- `ITC broadcast (type ${message.type}) not acknowledged by worker thread(s) ${stuck.map((port) => port?.threadId).join(', ')} within ${timeout}ms; proceeding best-effort`
1208
+ `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'}`
1133
1209
  );
1134
1210
  if (isMainThread) for (let port of stuck) logStuckWorkerDiagnostics(port);
1135
1211
  else if (parentPort) {
@@ -1143,6 +1219,11 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
1143
1219
  });
1144
1220
  }
1145
1221
 
1222
+ /** @param {boolean|'active'} includeJobWorkers */
1223
+ function broadcastWithStrictAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS, includeJobWorkers = false) {
1224
+ return broadcastWithAcknowledgement(message, timeout, true, includeJobWorkers);
1225
+ }
1226
+
1146
1227
  // Linux only: /proc/thread-self resolves to <pid>/task/<tid> for the calling thread.
1147
1228
  function getOsThreadId() {
1148
1229
  try {
@@ -1492,6 +1573,10 @@ const PROCESS_GROUP_LIVENESS_WARNING_MS = 30000;
1492
1573
  const THREAD_RUNNING_TERMINATION_BACKSTOP_MS = PROCESS_GROUP_LIVENESS_WARNING_MS;
1493
1574
  const zombieGroupScanTimes = new Map();
1494
1575
  const processGroupLivenessStates = new Map();
1576
+ // When each group's root was registered — by then it was already running, which is what lets the
1577
+ // Windows scan tell our root from a later process that recycled its PID.
1578
+ const processGroupSpawnedAt = new Map();
1579
+ let processGroupRegistrationGeneration = 0;
1495
1580
 
1496
1581
  function processGroupExists(processGroupId) {
1497
1582
  try {
@@ -1623,107 +1708,115 @@ function processGroupIsAlive(processGroupId) {
1623
1708
  return isProcessGroupAlive(processGroupId);
1624
1709
  }
1625
1710
 
1626
- async function waitForProcessGroupExit(processGroupId) {
1627
- while (processGroupIsAlive(processGroupId)) await delay(PROCESS_GROUP_TERMINATION_POLL_MS);
1628
- }
1629
-
1630
- // Mirrors Application.ts's windowsProcessTreeIsAlive: a descendant retains its ParentProcessId
1631
- // after its parent exits, which is exactly the taskkill "process not found" race, so query the
1632
- // process table by walking parentage rather than trusting only the root pid.
1633
- function windowsProcessTreeIsAlive(rootPid) {
1634
- // Exit code 1 must mean "queried the process table and positively found nothing" — never
1635
- // "the query itself failed" (e.g. Get-CimInstance denied or WMI unavailable), which would
1636
- // otherwise read identically to a confirmed-gone tree and release the lock while a descendant
1637
- // may still be alive. ErrorActionPreference=Stop plus the wrapping try/catch turns a query
1638
- // failure into its own exit code (2), which the caller below already treats as unknown.
1639
- const script =
1640
- "$ErrorActionPreference = 'Stop'; try { " +
1641
- `$rootPid = ${rootPid}; ` +
1642
- '$all = @(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId); ' +
1643
- '$frontier = @($rootPid); $seen = @{}; $found = $false; ' +
1644
- 'while ($frontier.Count -gt 0) { ' +
1645
- '$next = @(); foreach ($parentPid in $frontier) { if ($seen[$parentPid]) { continue }; ' +
1646
- '$seen[$parentPid] = $true; foreach ($p in $all) { ' +
1647
- 'if ($p.ProcessId -eq $parentPid) { $found = $true }; ' +
1648
- 'if ($p.ParentProcessId -eq $parentPid) { $found = $true; $next += [int]$p.ProcessId } } }; ' +
1649
- '$frontier = $next }; ' +
1650
- 'if ($found) { exit 0 } else { exit 1 } ' +
1651
- '} catch { exit 2 }';
1652
- return new Promise((resolve) => {
1653
- const query = spawn('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], {
1654
- stdio: 'ignore',
1655
- windowsHide: true,
1656
- });
1657
- query.once('close', (code) => resolve(code === 0 ? true : code === 1 ? false : null));
1658
- query.once('error', () => resolve(null));
1659
- });
1660
- }
1661
-
1662
- // The initial taskkill in terminateProcessGroupsForThread is fired synchronously (required so
1663
- // that call still works from a process `exit` handler) but its result is not checked there, since
1664
- // a nonzero exit is ambiguous between a real failure and the target having already exited. Confirm
1665
- // via the process table and keep retrying taskkill until the whole tree is positively gone —
1666
- // reclamation must not proceed on an unconfirmed guess either way. Exported so a test can drive the
1667
- // retry logic with injected callbacks instead of real Windows processes.
1668
- async function waitUntilConfirmedGone(attemptTermination, treeIsAlive, pollMs) {
1669
- for (;;) {
1670
- // A successful taskkill exit only proves the request was accepted, not that the whole tree
1671
- // has actually exited — Windows termination is asynchronous, and taskkill can report overall
1672
- // success even when a descendant is not yet (or never) reaped. Only an explicit `false` from
1673
- // treeIsAlive, independently confirming no member of the tree remains, is safe to return on;
1674
- // `true` or `null` (unknown) must keep the loop retrying.
1675
- await attemptTermination();
1676
- if ((await treeIsAlive()) === false) return;
1677
- await delay(pollMs);
1711
+ async function waitForProcessGroupExit(processGroupId, registration) {
1712
+ while (true) {
1713
+ const currentRegistration = processGroupSpawnedAt.get(processGroupId);
1714
+ if (currentRegistration !== undefined && currentRegistration !== registration) return;
1715
+ if (!processGroupIsAlive(processGroupId)) return;
1716
+ await delay(PROCESS_GROUP_TERMINATION_POLL_MS);
1678
1717
  }
1679
1718
  }
1680
1719
 
1681
- function waitForWindowsGroupExit(processGroupId) {
1682
- return waitUntilConfirmedGone(
1683
- () =>
1684
- new Promise((resolve) => {
1685
- const taskkill = spawn('taskkill', ['/pid', String(processGroupId), '/T', '/F'], {
1686
- stdio: 'ignore',
1687
- windowsHide: true,
1688
- });
1689
- taskkill.once('close', (code) => resolve(code === 0));
1690
- taskkill.once('error', () => resolve(false));
1691
- }),
1692
- () => windowsProcessTreeIsAlive(processGroupId),
1693
- PROCESS_GROUP_TERMINATION_POLL_MS
1720
+ // The initial taskkill in terminateProcessGroupsForThread is fired synchronously (required so
1721
+ // that call still works from a process `exit` handler). A nonzero exit there is ambiguous between
1722
+ // a real failure and the target having already exited, so only a reported success bounds the
1723
+ // root's lifetime up front (a terminated process cannot spawn); otherwise the scan latches the
1724
+ // root's exit itself, re-terminating the root while it is still found running as ours. Either
1725
+ // way the wait confirms via the process table — reclamation must not proceed on a guess. The
1726
+ // root was created inside the spawner's spawn() call, whose start and return times travel with the
1727
+ // registration so the cross-thread hop adds nothing to the window before it.
1728
+ function waitForWindowsGroupExit(processGroupId, spawn, killedAt) {
1729
+ return confirmWindowsProcessTreeGone(
1730
+ {
1731
+ rootPid: processGroupId,
1732
+ rootKnownAt: spawn?.spawnedAt ?? killedAt ?? Date.now(),
1733
+ rootStartedWithinMs:
1734
+ spawn?.spawnStartedAt !== undefined ? spawn.spawnedAt - spawn.spawnStartedAt : ROOT_SPAWN_ALLOWANCE_MS,
1735
+ rootExitedAt: killedAt,
1736
+ },
1737
+ { pollMs: PROCESS_GROUP_TERMINATION_POLL_MS, label: `process group ${processGroupId}` }
1694
1738
  );
1695
1739
  }
1696
1740
 
1697
- function addProcessGroup(ownerThreadId, processGroupId) {
1741
+ function addProcessGroup(ownerThreadId, processGroupId, spawnedAt, spawnStartedAt, registrationGeneration) {
1698
1742
  if (!Number.isInteger(processGroupId) || processGroupId <= 0) return;
1743
+ const previousRegistration = processGroupSpawnedAt.get(processGroupId);
1744
+ if (
1745
+ previousRegistration &&
1746
+ (previousRegistration.ownerThreadId !== ownerThreadId ||
1747
+ previousRegistration.registrationGeneration !== registrationGeneration)
1748
+ ) {
1749
+ clearProcessGroupLivenessState(processGroupId);
1750
+ }
1699
1751
  let processGroups = processGroupsByThread.get(ownerThreadId);
1700
1752
  if (!processGroups) processGroupsByThread.set(ownerThreadId, (processGroups = new Set()));
1701
1753
  processGroups.add(processGroupId);
1754
+ // The map is keyed by PID, which the OS reuses the instant a process exits. Owner distinguishes
1755
+ // threads; generation distinguishes two children of the same thread that receive the same PID.
1756
+ processGroupSpawnedAt.set(processGroupId, {
1757
+ ownerThreadId,
1758
+ registrationGeneration,
1759
+ spawnedAt: Number.isFinite(spawnedAt) ? spawnedAt : Date.now(),
1760
+ spawnStartedAt: Number.isFinite(spawnStartedAt) && spawnStartedAt <= spawnedAt ? spawnStartedAt : undefined,
1761
+ });
1702
1762
  }
1703
1763
 
1704
- function removeProcessGroup(ownerThreadId, processGroupId) {
1764
+ function removeProcessGroup(ownerThreadId, processGroupId, registrationGeneration) {
1765
+ const currentRegistration = processGroupSpawnedAt.get(processGroupId);
1766
+ const sameOwnerNewerGeneration =
1767
+ currentRegistration?.ownerThreadId === ownerThreadId &&
1768
+ currentRegistration.registrationGeneration !== registrationGeneration;
1769
+ if (!sameOwnerNewerGeneration) {
1770
+ const processGroups = processGroupsByThread.get(ownerThreadId);
1771
+ if (processGroups?.delete(processGroupId) && processGroups.size === 0) {
1772
+ processGroupsByThread.delete(ownerThreadId);
1773
+ }
1774
+ }
1775
+ if (currentRegistration?.ownerThreadId !== ownerThreadId || sameOwnerNewerGeneration) return;
1705
1776
  clearProcessGroupLivenessState(processGroupId);
1706
- const processGroups = processGroupsByThread.get(ownerThreadId);
1707
- if (!processGroups) return;
1708
- processGroups.delete(processGroupId);
1709
- if (processGroups.size === 0) processGroupsByThread.delete(ownerThreadId);
1777
+ processGroupSpawnedAt.delete(processGroupId);
1710
1778
  }
1711
1779
 
1712
1780
  // Returns a promise that resolves once every process group tracked for `ownerThreadId` is
1713
1781
  // confirmed terminated. Callers that only need to fire the termination (e.g. the `exit` handler
1714
1782
  // below) can ignore the returned promise; isThreadRunning awaits it before declaring a dead
1715
- // owner reclaimable. The kill signal for every group is sent synchronously, before any `await` —
1716
- // this runs from a process `exit` handler too, where nothing queued after a suspension point is
1717
- // guaranteed to run.
1718
- function terminateProcessGroupsForThread(ownerThreadId) {
1783
+ // owner reclaimable. On POSIX the kill signal for every group is sent synchronously, before any
1784
+ // `await` — this runs from a process `exit` handler too, where nothing queued after a suspension
1785
+ // point is guaranteed to run, and a process-group SIGKILL is directed at the group id, not a PID
1786
+ // Windows could have already reissued. On Windows a synchronous `taskkill /pid` has no such
1787
+ // process-group semantics — a PID whose process already exited is indistinguishable from one that
1788
+ // never will be, so an unconditional pre-kill can hit whatever now holds a recycled PID (the exact
1789
+ // harper#2273 unrelated-process kill this module exists to prevent). `fromExitHandler` restricts
1790
+ // that blind pre-kill to the one caller that genuinely cannot await anything first (a synchronous
1791
+ // scan via spawnSync before killing is possible there too, just not done); every other
1792
+ // caller lets the identity-checked confirmation loop below issue the first kill, after its own
1793
+ // scan has verified who the PID currently belongs to — UNLESS the process itself is already
1794
+ // shutting down (`processShuttingDown`, set before a restart tears its workers down too): that
1795
+ // path has no guarantee the async loop gets even one scan in before `process.exit()` runs, and by
1796
+ // then this function has already dropped the registration the exit handler would otherwise have
1797
+ // caught, so the blind kill has to fire here instead.
1798
+ function terminateProcessGroupsForThread(ownerThreadId, { fromExitHandler = false } = {}) {
1719
1799
  const processGroups = processGroupsByThread.get(ownerThreadId);
1720
1800
  if (!processGroups) return pendingProcessGroupTerminations.get(ownerThreadId) ?? Promise.resolve();
1721
1801
  processGroupsByThread.delete(ownerThreadId);
1722
- const groupIds = [...processGroups];
1802
+ // Membership is not ownership: a PID is reusable the moment its process exits.
1803
+ const groupIds = [...processGroups].filter((processGroupId) => {
1804
+ if (processGroupSpawnedAt.get(processGroupId)?.ownerThreadId === ownerThreadId) return true;
1805
+ harperLogger.warn(
1806
+ `Not terminating process group ${processGroupId} for thread ${ownerThreadId}: it is no longer registered to this thread`
1807
+ );
1808
+ return false;
1809
+ });
1810
+ const killedAt = new Map();
1723
1811
  for (const processGroupId of groupIds) {
1724
1812
  try {
1725
1813
  if (process.platform === 'win32') {
1726
- spawnSync('taskkill', ['/pid', String(processGroupId), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
1814
+ if (!fromExitHandler && !processShuttingDown) continue;
1815
+ const result = spawnSync('taskkill', ['/pid', String(processGroupId), '/T', '/F'], {
1816
+ stdio: 'ignore',
1817
+ windowsHide: true,
1818
+ });
1819
+ if (result.status === 0) killedAt.set(processGroupId, Date.now());
1727
1820
  } else {
1728
1821
  process.kill(-processGroupId, 'SIGKILL');
1729
1822
  }
@@ -1732,9 +1825,18 @@ function terminateProcessGroupsForThread(ownerThreadId) {
1732
1825
  }
1733
1826
  }
1734
1827
  const termination = Promise.all(
1735
- groupIds.map((processGroupId) =>
1736
- process.platform === 'win32' ? waitForWindowsGroupExit(processGroupId) : waitForProcessGroupExit(processGroupId)
1737
- )
1828
+ groupIds.map((processGroupId) => {
1829
+ const registration = processGroupSpawnedAt.get(processGroupId);
1830
+ const wait =
1831
+ process.platform === 'win32'
1832
+ ? waitForWindowsGroupExit(processGroupId, registration, killedAt.get(processGroupId))
1833
+ : waitForProcessGroupExit(processGroupId, registration);
1834
+ return wait.finally(() => {
1835
+ if (processGroupSpawnedAt.get(processGroupId) === registration) {
1836
+ processGroupSpawnedAt.delete(processGroupId);
1837
+ }
1838
+ });
1839
+ })
1738
1840
  ).finally(() => {
1739
1841
  if (pendingProcessGroupTerminations.get(ownerThreadId) === termination) {
1740
1842
  pendingProcessGroupTerminations.delete(ownerThreadId);
@@ -1744,14 +1846,26 @@ function terminateProcessGroupsForThread(ownerThreadId) {
1744
1846
  return termination;
1745
1847
  }
1746
1848
 
1747
- function registerProcessGroup(processGroupId) {
1748
- if (isMainThread) addProcessGroup(threadId, processGroupId);
1749
- else parentPort?.postMessage({ type: REGISTER_PROCESS_GROUP, processGroupId });
1849
+ // `spawnedAt` / `spawnStartedAt`: the caller's clock as its spawn() of the group's root returned and
1850
+ // as it was called — the root was created between the two.
1851
+ function registerProcessGroup(processGroupId, spawnedAt = Date.now(), spawnStartedAt) {
1852
+ const registrationGeneration = ++processGroupRegistrationGeneration;
1853
+ if (isMainThread) addProcessGroup(threadId, processGroupId, spawnedAt, spawnStartedAt, registrationGeneration);
1854
+ else {
1855
+ parentPort?.postMessage({
1856
+ type: REGISTER_PROCESS_GROUP,
1857
+ processGroupId,
1858
+ spawnedAt,
1859
+ spawnStartedAt,
1860
+ registrationGeneration,
1861
+ });
1862
+ }
1863
+ return registrationGeneration;
1750
1864
  }
1751
1865
 
1752
- function unregisterProcessGroup(processGroupId) {
1753
- if (isMainThread) removeProcessGroup(threadId, processGroupId);
1754
- else parentPort?.postMessage({ type: UNREGISTER_PROCESS_GROUP, processGroupId });
1866
+ function unregisterProcessGroup(processGroupId, registrationGeneration) {
1867
+ if (isMainThread) removeProcessGroup(threadId, processGroupId, registrationGeneration);
1868
+ else parentPort?.postMessage({ type: UNREGISTER_PROCESS_GROUP, processGroupId, registrationGeneration });
1755
1869
  }
1756
1870
 
1757
1871
  class ProcessGroupTerminationUnconfirmedError extends Error {
@@ -1801,7 +1915,8 @@ async function isThreadRunning(ownerThreadId, timeoutMs = THREAD_INFO_REQUEST_TI
1801
1915
 
1802
1916
  if (isMainThread) {
1803
1917
  process.on('exit', () => {
1804
- for (const ownerThreadId of [...processGroupsByThread.keys()]) terminateProcessGroupsForThread(ownerThreadId);
1918
+ for (const ownerThreadId of [...processGroupsByThread.keys()])
1919
+ terminateProcessGroupsForThread(ownerThreadId, { fromExitHandler: true });
1805
1920
  });
1806
1921
  }
1807
1922
 
@@ -1817,6 +1932,7 @@ function hasThreadExited(threadId) {
1817
1932
  function notifyThreadExit(deadThreadId) {
1818
1933
  if (deadThreadId == null || notifiedDeadThreadIds.has(deadThreadId)) return;
1819
1934
  notifiedDeadThreadIds.add(deadThreadId);
1935
+ handleDatabaseDropPreparationOwnerExit(deadThreadId);
1820
1936
  for (const listener of threadExitListeners) {
1821
1937
  try {
1822
1938
  listener(deadThreadId);
@@ -1830,6 +1946,8 @@ function removePort(port, deadThreadId) {
1830
1946
  // A sibling may already have announced this dead thread and removed its port. Process-group
1831
1947
  // cleanup must still run when the authoritative close/exit event reaches this thread.
1832
1948
  if (deadThreadId != null) terminateProcessGroupsForThread(deadThreadId);
1949
+ const exitConfirmed = !port.close;
1950
+ settleAcknowledgementsForClosedPort(port, port.jobCleanupComplete === true, exitConfirmed);
1833
1951
  const idx = connectedPorts.indexOf(port);
1834
1952
  if (idx === -1) return;
1835
1953
  connectedPorts.splice(idx, 1);
@@ -1842,7 +1960,12 @@ function removePort(port, deadThreadId) {
1842
1960
  if (deadThreadId != null) {
1843
1961
  for (let remainingPort of connectedPorts) {
1844
1962
  try {
1845
- remainingPort.postMessage({ type: REMOVE_PORT, threadId: deadThreadId });
1963
+ remainingPort.postMessage({
1964
+ type: REMOVE_PORT,
1965
+ threadId: deadThreadId,
1966
+ jobCleanupComplete: port.jobCleanupComplete === true,
1967
+ exitConfirmed,
1968
+ });
1846
1969
  } catch {
1847
1970
  // port may already be dead; ignore
1848
1971
  }
@@ -1858,20 +1981,39 @@ function addPort(port, keepRef, isJobWorker) {
1858
1981
  port
1859
1982
  .on('message', (message) => {
1860
1983
  if (message.type === REGISTER_PROCESS_GROUP) {
1861
- addProcessGroup(portThreadId, message.processGroupId);
1984
+ addProcessGroup(
1985
+ portThreadId,
1986
+ message.processGroupId,
1987
+ message.spawnedAt,
1988
+ message.spawnStartedAt,
1989
+ message.registrationGeneration
1990
+ );
1862
1991
  } else if (message.type === UNREGISTER_PROCESS_GROUP) {
1863
- removeProcessGroup(portThreadId, message.processGroupId);
1992
+ removeProcessGroup(portThreadId, message.processGroupId, message.registrationGeneration);
1993
+ } else if (message.type === hdbTerms.ITC_EVENT_TYPES.JOB_CLEANUP_COMPLETE) {
1994
+ port.jobCleanupComplete = true;
1995
+ settleAcknowledgementsForClosedPort(port, true);
1864
1996
  } else if (message.type === ADDED_PORT) {
1865
1997
  message.port.threadId = message.threadId;
1866
1998
  addPort(message.port, false, message.isJobWorker);
1867
1999
  } else if (message.type === ACKNOWLEDGEMENT) {
1868
2000
  let completion = awaitingResponses.get(message.id);
1869
2001
  if (completion) {
1870
- completion();
2002
+ completion(message);
1871
2003
  }
1872
2004
  } else if (message.type === REMOVE_PORT) {
1873
- const idx = connectedPorts.findIndex((p) => p.threadId === message.threadId);
1874
- if (idx !== -1) connectedPorts.splice(idx, 1);
2005
+ const removedPort = connectedPorts.find((candidate) => candidate.threadId === message.threadId);
2006
+ if (removedPort && !removedPort.close && !message.exitConfirmed) return;
2007
+ settleAcknowledgementsForClosedThread(
2008
+ message.threadId,
2009
+ message.jobCleanupComplete === true,
2010
+ message.exitConfirmed === true
2011
+ );
2012
+ if (removedPort) {
2013
+ if (message.jobCleanupComplete) removedPort.jobCleanupComplete = true;
2014
+ settleAcknowledgementsForClosedPort(removedPort, removedPort.jobCleanupComplete === true);
2015
+ connectedPorts.splice(connectedPorts.indexOf(removedPort), 1);
2016
+ }
1875
2017
  // A sibling's port-to-the-dead-worker can close (and broadcast this) before this
1876
2018
  // thread's OWN port to that worker fires its 'close'/'exit' — at which point
1877
2019
  // removePort() would no-op (already spliced) and threadExitListeners would never
@@ -1882,10 +2024,14 @@ function addPort(port, keepRef, isJobWorker) {
1882
2024
  }
1883
2025
  })
1884
2026
  .on('close', () => {
1885
- removePort(port, portThreadId);
2027
+ // A worker's parentPort closing is this worker leaving, not main exiting (server/DESIGN.md).
2028
+ removePort(port, port === parentPort ? undefined : portThreadId);
1886
2029
  })
1887
2030
  .on('exit', () => {
1888
- removePort(port, portThreadId);
2031
+ // Let a cleanup proof already queued by the worker reach this port before exit becomes
2032
+ // authoritative. The next turn still fails closed if no proof arrives.
2033
+ if (port.isJobWorker && !port.jobCleanupComplete) setImmediate(() => removePort(port, portThreadId));
2034
+ else removePort(port, portThreadId);
1889
2035
  });
1890
2036
  if (keepRef) port.refCount = 100;
1891
2037
  else port.unref();
@@ -8,6 +8,8 @@ let componentsLoadedResolve;
8
8
  exports.whenComponentsLoaded = new Promise((resolve) => {
9
9
  componentsLoadedResolve = resolve;
10
10
  });
11
+ let bootLoadStarted = false;
12
+ exports.bootLoadsComponents = () => bootLoadStarted;
11
13
 
12
14
  const harperLogger = require('../../utility/logging/harper_logger.ts');
13
15
  const env = require('../../utility/environment/environmentManager.ts');
@@ -174,6 +176,7 @@ function closeServers() {
174
176
  }
175
177
 
176
178
  function startServers() {
179
+ bootLoadStarted = true;
177
180
  // A worker that has not yet posted child_started owns no ref'd handle: addPort()
178
181
  // (manageThreads) unrefs parentPort, component watchers are persistent:false, and the
179
182
  // reporting timers are unref'd. An await inside loadRootComponents whose completion
@@ -230,7 +233,16 @@ function startServers() {
230
233
  })
231
234
  .then(() => closeServers())
232
235
  .then(() => whenScopesClosed())
233
- .then(() => require('../../resources/databases.ts').closeBranchDatabases())
236
+ .then(async () => {
237
+ const { branchDatabasesHaveWork, closeBranchDatabases } = require('../../resources/databases.ts');
238
+ if (!branchDatabasesHaveWork()) return closeBranchDatabases();
239
+ extendShutdownDeadline(Date.now() + getShutdownDrainCeilingMs());
240
+ try {
241
+ await closeBranchDatabases();
242
+ } finally {
243
+ restoreShutdownDeadline();
244
+ }
245
+ })
234
246
  .then(() => {
235
247
  realExit(0);
236
248
  });