@spine-event-engine/server 2.0.0-snapshot.22 → 2.0.0-snapshot.24

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 (199) hide show
  1. package/README.md +104 -7
  2. package/REFERENCE.md +272 -31
  3. package/dist/agent/agent-admission.d.ts +80 -0
  4. package/dist/agent/agent-admission.d.ts.map +1 -0
  5. package/dist/agent/agent-admission.js +127 -0
  6. package/dist/agent/agent-admission.js.map +1 -0
  7. package/dist/agent/agent-ai-binding.d.ts +24 -0
  8. package/dist/agent/agent-ai-binding.d.ts.map +1 -0
  9. package/dist/agent/agent-ai-binding.js +48 -0
  10. package/dist/agent/agent-ai-binding.js.map +1 -0
  11. package/dist/agent/agent-ai-runtime.d.ts +76 -0
  12. package/dist/agent/agent-ai-runtime.d.ts.map +1 -0
  13. package/dist/agent/agent-ai-runtime.js +1269 -0
  14. package/dist/agent/agent-ai-runtime.js.map +1 -0
  15. package/dist/agent/agent-execution-capacity.d.ts +43 -0
  16. package/dist/agent/agent-execution-capacity.d.ts.map +1 -0
  17. package/dist/agent/agent-execution-capacity.js +147 -0
  18. package/dist/agent/agent-execution-capacity.js.map +1 -0
  19. package/dist/agent/agent-execution-fault.d.ts +18 -0
  20. package/dist/agent/agent-execution-fault.d.ts.map +1 -0
  21. package/dist/agent/agent-execution-fault.js +31 -0
  22. package/dist/agent/agent-execution-fault.js.map +1 -0
  23. package/dist/agent/agent-execution-session.d.ts +103 -0
  24. package/dist/agent/agent-execution-session.d.ts.map +1 -0
  25. package/dist/agent/agent-execution-session.js +238 -0
  26. package/dist/agent/agent-execution-session.js.map +1 -0
  27. package/dist/agent/agent-history.d.ts +148 -0
  28. package/dist/agent/agent-history.d.ts.map +1 -0
  29. package/dist/agent/agent-history.js +340 -0
  30. package/dist/agent/agent-history.js.map +1 -0
  31. package/dist/agent/agent-interaction-audit.d.ts +58 -0
  32. package/dist/agent/agent-interaction-audit.d.ts.map +1 -0
  33. package/dist/agent/agent-interaction-audit.js +114 -0
  34. package/dist/agent/agent-interaction-audit.js.map +1 -0
  35. package/dist/agent/agent-interaction-events.d.ts +40 -0
  36. package/dist/agent/agent-interaction-events.d.ts.map +1 -0
  37. package/dist/agent/agent-interaction-events.js +129 -0
  38. package/dist/agent/agent-interaction-events.js.map +1 -0
  39. package/dist/agent/agent-mcp-budget.d.ts +125 -0
  40. package/dist/agent/agent-mcp-budget.d.ts.map +1 -0
  41. package/dist/agent/agent-mcp-budget.js +566 -0
  42. package/dist/agent/agent-mcp-budget.js.map +1 -0
  43. package/dist/agent/agent-mcp-host.d.ts +125 -0
  44. package/dist/agent/agent-mcp-host.d.ts.map +1 -0
  45. package/dist/agent/agent-mcp-host.js +15 -0
  46. package/dist/agent/agent-mcp-host.js.map +1 -0
  47. package/dist/agent/agent-mcp-runtime.d.ts +199 -0
  48. package/dist/agent/agent-mcp-runtime.d.ts.map +1 -0
  49. package/dist/agent/agent-mcp-runtime.js +621 -0
  50. package/dist/agent/agent-mcp-runtime.js.map +1 -0
  51. package/dist/agent/agent-model-selection.d.ts +129 -0
  52. package/dist/agent/agent-model-selection.d.ts.map +1 -0
  53. package/dist/agent/agent-model-selection.js +342 -0
  54. package/dist/agent/agent-model-selection.js.map +1 -0
  55. package/dist/agent/agent-read-runtime.d.ts +70 -0
  56. package/dist/agent/agent-read-runtime.d.ts.map +1 -0
  57. package/dist/agent/agent-read-runtime.js +320 -0
  58. package/dist/agent/agent-read-runtime.js.map +1 -0
  59. package/dist/agent/agent-revisions.d.ts +35 -0
  60. package/dist/agent/agent-revisions.d.ts.map +1 -0
  61. package/dist/agent/agent-revisions.js +133 -0
  62. package/dist/agent/agent-revisions.js.map +1 -0
  63. package/dist/agent/agent-scheduler.d.ts +99 -0
  64. package/dist/agent/agent-scheduler.d.ts.map +1 -0
  65. package/dist/agent/agent-scheduler.js +398 -0
  66. package/dist/agent/agent-scheduler.js.map +1 -0
  67. package/dist/bus/command-bus.d.ts +62 -0
  68. package/dist/bus/command-bus.d.ts.map +1 -1
  69. package/dist/bus/command-bus.js +51 -2
  70. package/dist/bus/command-bus.js.map +1 -1
  71. package/dist/bus/event-bus.d.ts +101 -2
  72. package/dist/bus/event-bus.d.ts.map +1 -1
  73. package/dist/bus/event-bus.js +74 -13
  74. package/dist/bus/event-bus.js.map +1 -1
  75. package/dist/bus/saved-dispatcher-binding.d.ts +53 -0
  76. package/dist/bus/saved-dispatcher-binding.d.ts.map +1 -0
  77. package/dist/bus/saved-dispatcher-binding.js +27 -0
  78. package/dist/bus/saved-dispatcher-binding.js.map +1 -0
  79. package/dist/context/bounded-context.d.ts +45 -12
  80. package/dist/context/bounded-context.d.ts.map +1 -1
  81. package/dist/context/bounded-context.js +306 -86
  82. package/dist/context/bounded-context.js.map +1 -1
  83. package/dist/context/entity-inbox.d.ts.map +1 -1
  84. package/dist/context/entity-inbox.js +21 -7
  85. package/dist/context/entity-inbox.js.map +1 -1
  86. package/dist/context/local-inbox-handoff.d.ts +1 -0
  87. package/dist/context/local-inbox-handoff.d.ts.map +1 -1
  88. package/dist/context/local-inbox-handoff.js +31 -1
  89. package/dist/context/local-inbox-handoff.js.map +1 -1
  90. package/dist/context/projection-handoff.d.ts.map +1 -1
  91. package/dist/context/projection-handoff.js +2 -0
  92. package/dist/context/projection-handoff.js.map +1 -1
  93. package/dist/context/tenant-index.d.ts +25 -0
  94. package/dist/context/tenant-index.d.ts.map +1 -1
  95. package/dist/context/tenant-index.js +45 -1
  96. package/dist/context/tenant-index.js.map +1 -1
  97. package/dist/delivery/delivery-monitor.d.ts +12 -9
  98. package/dist/delivery/delivery-monitor.d.ts.map +1 -1
  99. package/dist/delivery/delivery-monitor.js +15 -9
  100. package/dist/delivery/delivery-monitor.js.map +1 -1
  101. package/dist/delivery/delivery-ports.d.ts +6 -13
  102. package/dist/delivery/delivery-ports.d.ts.map +1 -1
  103. package/dist/delivery/delivery-run-control.d.ts +15 -0
  104. package/dist/delivery/delivery-run-control.d.ts.map +1 -1
  105. package/dist/delivery/delivery-run-control.js.map +1 -1
  106. package/dist/delivery/delivery-supervisor.d.ts +33 -5
  107. package/dist/delivery/delivery-supervisor.d.ts.map +1 -1
  108. package/dist/delivery/delivery-supervisor.js +150 -22
  109. package/dist/delivery/delivery-supervisor.js.map +1 -1
  110. package/dist/delivery/delivery.d.ts.map +1 -1
  111. package/dist/delivery/delivery.js +128 -54
  112. package/dist/delivery/delivery.js.map +1 -1
  113. package/dist/delivery/inbox-storage.d.ts +8 -11
  114. package/dist/delivery/inbox-storage.d.ts.map +1 -1
  115. package/dist/delivery/inbox-storage.js +33 -76
  116. package/dist/delivery/inbox-storage.js.map +1 -1
  117. package/dist/delivery/inbox.d.ts +9 -12
  118. package/dist/delivery/inbox.d.ts.map +1 -1
  119. package/dist/delivery/inbox.js +10 -13
  120. package/dist/delivery/inbox.js.map +1 -1
  121. package/dist/delivery/reception-observation.d.ts +56 -0
  122. package/dist/delivery/reception-observation.d.ts.map +1 -0
  123. package/dist/delivery/reception-observation.js +67 -0
  124. package/dist/delivery/reception-observation.js.map +1 -0
  125. package/dist/entity/entity.d.ts +82 -3
  126. package/dist/entity/entity.d.ts.map +1 -1
  127. package/dist/entity/entity.js +121 -22
  128. package/dist/entity/entity.js.map +1 -1
  129. package/dist/handler/generated-handler-registry.js +8 -3
  130. package/dist/handler/generated-handler-registry.js.map +1 -1
  131. package/dist/handler/handler-metadata.d.ts +17 -1
  132. package/dist/handler/handler-metadata.d.ts.map +1 -1
  133. package/dist/handler/handler-metadata.js +10 -4
  134. package/dist/handler/handler-metadata.js.map +1 -1
  135. package/dist/index.d.ts +2 -2
  136. package/dist/index.d.ts.map +1 -1
  137. package/dist/index.js +1 -1
  138. package/dist/index.js.map +1 -1
  139. package/dist/repository/commit-fence.d.ts +11 -0
  140. package/dist/repository/commit-fence.d.ts.map +1 -1
  141. package/dist/repository/commit-fence.js +13 -0
  142. package/dist/repository/commit-fence.js.map +1 -1
  143. package/dist/repository/repository.d.ts +221 -13
  144. package/dist/repository/repository.d.ts.map +1 -1
  145. package/dist/repository/repository.js +2323 -134
  146. package/dist/repository/repository.js.map +1 -1
  147. package/dist/runtime/runtime.d.ts +11 -0
  148. package/dist/runtime/runtime.d.ts.map +1 -1
  149. package/dist/runtime/runtime.js +13 -0
  150. package/dist/runtime/runtime.js.map +1 -1
  151. package/dist/runtime/signal-metadata.d.ts.map +1 -1
  152. package/dist/runtime/signal-metadata.js +3 -10
  153. package/dist/runtime/signal-metadata.js.map +1 -1
  154. package/dist/runtime/signal-publisher.d.ts +14 -30
  155. package/dist/runtime/signal-publisher.d.ts.map +1 -1
  156. package/dist/runtime/signal-publisher.js +75 -47
  157. package/dist/runtime/signal-publisher.js.map +1 -1
  158. package/dist/runtime/standalone-handler-runtime.d.ts +7 -0
  159. package/dist/runtime/standalone-handler-runtime.d.ts.map +1 -1
  160. package/dist/runtime/standalone-handler-runtime.js +118 -11
  161. package/dist/runtime/standalone-handler-runtime.js.map +1 -1
  162. package/dist/server/environment-delivery-worker.d.ts.map +1 -1
  163. package/dist/server/environment-delivery-worker.js +305 -5
  164. package/dist/server/environment-delivery-worker.js.map +1 -1
  165. package/dist/server/managed-server-application.d.ts +2 -2
  166. package/dist/server/managed-server-application.d.ts.map +1 -1
  167. package/dist/server/managed-server-application.js +467 -177
  168. package/dist/server/managed-server-application.js.map +1 -1
  169. package/dist/server/node-coordinator.d.ts +14 -0
  170. package/dist/server/node-coordinator.d.ts.map +1 -1
  171. package/dist/server/node-coordinator.js +266 -45
  172. package/dist/server/node-coordinator.js.map +1 -1
  173. package/dist/server/server-log.d.ts.map +1 -1
  174. package/dist/server/server-log.js +4 -0
  175. package/dist/server/server-log.js.map +1 -1
  176. package/dist/server/server.d.ts +9 -0
  177. package/dist/server/server.d.ts.map +1 -1
  178. package/dist/server/server.js +33 -8
  179. package/dist/server/server.js.map +1 -1
  180. package/dist/services/spine-services.d.ts.map +1 -1
  181. package/dist/services/spine-services.js +200 -72
  182. package/dist/services/spine-services.js.map +1 -1
  183. package/dist/stand/event-origin-filter.d.ts +16 -0
  184. package/dist/stand/event-origin-filter.d.ts.map +1 -0
  185. package/dist/stand/event-origin-filter.js +124 -0
  186. package/dist/stand/event-origin-filter.js.map +1 -0
  187. package/dist/stand/subscription-observer.d.ts.map +1 -1
  188. package/dist/stand/subscription-observer.js +7 -0
  189. package/dist/stand/subscription-observer.js.map +1 -1
  190. package/dist/stand/subscription-runtime.d.ts +18 -1
  191. package/dist/stand/subscription-runtime.d.ts.map +1 -1
  192. package/dist/stand/subscription-runtime.js +32 -4
  193. package/dist/stand/subscription-runtime.js.map +1 -1
  194. package/dist/testing/index.d.ts +57 -1
  195. package/dist/testing/index.d.ts.map +1 -1
  196. package/dist/testing/index.js +88 -1
  197. package/dist/testing/index.js.map +1 -1
  198. package/dist/tsconfig.tsbuildinfo +1 -1
  199. package/package.json +15 -13
@@ -106,115 +106,7 @@ const ManagedServerValues = Object.freeze({
106
106
  }
107
107
  throw error;
108
108
  }
109
- // Node may emit an IPC EPIPE after a parent has already disappeared. The
110
- // disconnect handler below handles shutdown; this listener prevents that
111
- // transport detail from becoming an unhandled child-process exception.
112
- const containIpcError = () => undefined;
113
- process.on("error", containIpcError);
114
- managedChildSubscriptionAccess.install((subscriptionId) => {
115
- const notification = ManagedServerValues.send({
116
- type: "subscription-installed",
117
- slot: process.env[slotMarker],
118
- incarnation: process.env[incarnationMarker],
119
- subscriptionId,
120
- });
121
- // spine-log-boundary: server.managed_replica_subscription_ipc
122
- void notification.catch(() => undefined);
123
- });
124
- let activated = false;
125
- let releaseActivation;
126
- const activation = new Promise((resolve) => {
127
- releaseActivation = resolve;
128
- });
129
- let closing;
130
- const close = () => {
131
- if (closing !== undefined)
132
- return closing;
133
- const draining = ManagedServerValues.send({
134
- type: "draining",
135
- slot: process.env[slotMarker],
136
- incarnation: process.env[incarnationMarker],
137
- });
138
- // spine-log-boundary: server.managed_replica_drain_ipc
139
- const attempt = draining
140
- .catch(() => undefined)
141
- .then(() => {
142
- environmentDeliveryWorkerAccess.cancelManagedChild();
143
- managedChildSubscriptionAccess.clear();
144
- releaseActivation();
145
- return runningServerAccess.drainDelivery(server);
146
- })
147
- .then(() => {
148
- return server.close();
149
- })
150
- .then(() => {
151
- const closed = ManagedServerValues.send({
152
- type: "closed",
153
- slot: process.env[slotMarker],
154
- incarnation: process.env[incarnationMarker],
155
- });
156
- // spine-log-boundary: server.managed_replica_closed_ipc
157
- return closed.catch(() => undefined);
158
- }, (error) => {
159
- const failed = ManagedServerValues.send({
160
- type: "close-failed",
161
- slot: process.env[slotMarker],
162
- incarnation: process.env[incarnationMarker],
163
- });
164
- // spine-log-boundary: server.managed_replica_close_failed_ipc
165
- void failed.catch(() => undefined);
166
- if (closing === attempt)
167
- closing = undefined;
168
- throw error;
169
- })
170
- .then(() => {
171
- process.off("disconnect", onDisconnect);
172
- if (process.connected && typeof process.disconnect === "function")
173
- process.disconnect();
174
- process.off("error", containIpcError);
175
- process.off("message", onMessage);
176
- });
177
- closing = attempt;
178
- return attempt;
179
- };
180
- const onMessage = (message) => {
181
- if (message.type === "activate" && !activated) {
182
- activated = true;
183
- environmentDeliveryWorkerAccess.activateManagedChild();
184
- releaseActivation();
185
- }
186
- else if (message.type === "close") {
187
- ManagedServerValues.closeFromEvent(close);
188
- }
189
- };
190
- const onDisconnect = () => {
191
- ManagedServerValues.closeFromEvent(close);
192
- };
193
- process.on("message", onMessage);
194
- process.once("disconnect", onDisconnect);
195
- try {
196
- await ManagedServerValues.send({
197
- type: "synchronizing",
198
- slot: process.env[slotMarker],
199
- incarnation: process.env[incarnationMarker],
200
- endpoint: server.baseUrl,
201
- });
202
- }
203
- catch (error) {
204
- await close();
205
- throw error;
206
- }
207
- await activation;
208
- await ManagedServerValues.send({
209
- type: "ready",
210
- slot: process.env[slotMarker],
211
- incarnation: process.env[incarnationMarker],
212
- endpoint: server.baseUrl,
213
- });
214
- return {
215
- ready: true,
216
- close,
217
- };
109
+ return new ManagedChildLifecycle(server).start();
218
110
  },
219
111
  /**
220
112
  * Starts a coordinator for the configured complete-replica cohort.
@@ -277,6 +169,174 @@ const ManagedServerValues = Object.freeze({
277
169
  });
278
170
  },
279
171
  });
172
+ /**
173
+ * Runs the private child IPC lifecycle while keeping its listener open for relay cleanup.
174
+ */
175
+ class ManagedChildLifecycle {
176
+ #activation = ManagedServerCoordinatorValues.deferred();
177
+ #serverCloseRelease = ManagedServerCoordinatorValues.deferred();
178
+ #server;
179
+ #activated = false;
180
+ #waitForParentClose = false;
181
+ #closing;
182
+ /**
183
+ * Creates the lifecycle around an assembled child server.
184
+ *
185
+ * @param server The already assembled and synchronized child server.
186
+ */
187
+ constructor(server) {
188
+ this.#server = server;
189
+ }
190
+ /**
191
+ *
192
+ * Starts the child and waits for Coordinator activation.
193
+ *
194
+ * @returns A child handle after the private READY notification.
195
+ */
196
+ async start() {
197
+ process.on("error", this.#containIpcError);
198
+ managedChildSubscriptionAccess.install((subscriptionId) => {
199
+ const notification = ManagedServerValues.send({
200
+ type: "subscription-installed",
201
+ slot: process.env[slotMarker],
202
+ incarnation: process.env[incarnationMarker],
203
+ subscriptionId,
204
+ });
205
+ // spine-log-boundary: server.managed_replica_subscription_ipc
206
+ void notification.catch(() => undefined);
207
+ });
208
+ process.on("message", this.#onMessage);
209
+ process.once("disconnect", this.#onDisconnect);
210
+ try {
211
+ await this.#notify("synchronizing", this.#server.baseUrl);
212
+ }
213
+ catch (error) {
214
+ this.#serverCloseRelease.resolve(undefined);
215
+ await this.close();
216
+ throw error;
217
+ }
218
+ await this.#activation.promise;
219
+ await this.#notify("ready", this.#server.baseUrl);
220
+ return { ready: true, close: () => this.close() };
221
+ }
222
+ /**
223
+ *
224
+ * Closes Delivery before closing the child listener and its server.
225
+ *
226
+ * @returns The shared close attempt; a failed attempt may be retried.
227
+ */
228
+ close() {
229
+ this.#closing ??= this.#closeOnce().catch((error) => {
230
+ this.#closing = undefined;
231
+ throw error;
232
+ });
233
+ return this.#closing;
234
+ }
235
+ /**
236
+ *
237
+ * Performs child shutdown and observes a failed completion notification.
238
+ *
239
+ * @returns Completion after the child server closes.
240
+ */
241
+ async #closeOnce() {
242
+ try {
243
+ await this.#drainDelivery();
244
+ await this.#closeServerAfterRelayCleanup();
245
+ // spine-log-boundary: server.managed_replica_closed_ipc
246
+ await this.#notify("closed").catch(() => undefined);
247
+ this.#disconnect();
248
+ }
249
+ catch (error) {
250
+ // spine-log-boundary: server.managed_replica_close_failed_ipc
251
+ await this.#notify("close-failed").catch(() => undefined);
252
+ throw error;
253
+ }
254
+ }
255
+ /**
256
+ *
257
+ * Stops new child Delivery work and awaits admitted effects.
258
+ *
259
+ * @returns Completion when admitted work drains.
260
+ */
261
+ async #drainDelivery() {
262
+ environmentDeliveryWorkerAccess.cancelManagedChild();
263
+ managedChildSubscriptionAccess.clear();
264
+ this.#activation.resolve(undefined);
265
+ // spine-log-boundary: server.managed_replica_drain_ipc
266
+ void this.#notify("draining").catch(() => undefined);
267
+ await runningServerAccess.drainDelivery(this.#server);
268
+ }
269
+ /**
270
+ *
271
+ * Waits for parent relay cancellation before closing the child listener.
272
+ *
273
+ * @returns Completion after final server close.
274
+ */
275
+ async #closeServerAfterRelayCleanup() {
276
+ // spine-log-boundary: server.managed_replica_delivery_drained_ipc
277
+ void this.#notify("delivery-drained").catch(() => undefined);
278
+ if (this.#waitForParentClose)
279
+ await this.#serverCloseRelease.promise;
280
+ await this.#server.close();
281
+ }
282
+ /**
283
+ *
284
+ * Sends one child lifecycle notification on the private IPC channel.
285
+ *
286
+ * @param type The lifecycle phase to report.
287
+ * @param endpoint The listening endpoint for readiness messages.
288
+ * @returns Completion after the parent accepts the frame.
289
+ */
290
+ #notify(type, endpoint) {
291
+ return ManagedServerValues.send({
292
+ type,
293
+ slot: process.env[slotMarker],
294
+ incarnation: process.env[incarnationMarker],
295
+ ...(endpoint === undefined ? {} : { endpoint }),
296
+ });
297
+ }
298
+ /**
299
+ *
300
+ * Releases child startup and close gates for matching parent messages.
301
+ *
302
+ * @param message The private parent frame.
303
+ */
304
+ #onMessage = (message) => {
305
+ if (message.type === "activate" && !this.#activated) {
306
+ this.#activated = true;
307
+ environmentDeliveryWorkerAccess.activateManagedChild();
308
+ this.#activation.resolve(undefined);
309
+ }
310
+ else if (message.type === "close") {
311
+ this.#waitForParentClose = true;
312
+ ManagedServerValues.closeFromEvent(() => this.close());
313
+ }
314
+ else if (message.type === "finish-close") {
315
+ this.#serverCloseRelease.resolve(undefined);
316
+ }
317
+ };
318
+ /**
319
+ * Releases the final close phase if the parent disappears.
320
+ */
321
+ #onDisconnect = () => {
322
+ this.#serverCloseRelease.resolve(undefined);
323
+ ManagedServerValues.closeFromEvent(() => this.close());
324
+ };
325
+ /**
326
+ * Contains IPC EPIPE after an already disappeared parent.
327
+ */
328
+ #containIpcError = () => undefined;
329
+ /**
330
+ * Removes private listeners once the child server has closed.
331
+ */
332
+ #disconnect() {
333
+ process.off("disconnect", this.#onDisconnect);
334
+ if (process.connected && typeof process.disconnect === "function")
335
+ process.disconnect();
336
+ process.off("error", this.#containIpcError);
337
+ process.off("message", this.#onMessage);
338
+ }
339
+ }
280
340
  /**
281
341
  *
282
342
  * Coordinates the parent-only lifecycle of managed application replicas.
@@ -288,6 +348,8 @@ export class ManagedServerCoordinator {
288
348
  #restart;
289
349
  #options;
290
350
  #dependencies;
351
+ #terminalChildFailure;
352
+ #drainStart;
291
353
  #closing = false;
292
354
  #starts = 0;
293
355
  #ready;
@@ -394,6 +456,16 @@ export class ManagedServerCoordinator {
394
456
  this.#drainStarts();
395
457
  return;
396
458
  }
459
+ this.#installReplica(slot, incarnation, child);
460
+ }
461
+ /**
462
+ * Records a spawned child and observes its private lifecycle messages.
463
+ *
464
+ * @param slot The configured replica slot.
465
+ * @param incarnation The current child incarnation identifier.
466
+ * @param child The spawned process handle.
467
+ */
468
+ #installReplica(slot, incarnation, child) {
397
469
  const replica = {
398
470
  slot: slot.slot,
399
471
  incarnation,
@@ -404,6 +476,9 @@ export class ManagedServerCoordinator {
404
476
  draining: false,
405
477
  subscriptionWaiters: new Map(),
406
478
  closeAttempt: undefined,
479
+ closePromise: undefined,
480
+ closeFailure: undefined,
481
+ forcedFailure: undefined,
407
482
  expectedExit: false,
408
483
  terminal: false,
409
484
  };
@@ -428,43 +503,86 @@ export class ManagedServerCoordinator {
428
503
  * @param message The untrusted child IPC notification.
429
504
  */
430
505
  #onMessage(slot, replica, message) {
506
+ if (this.#onCloseMessage(slot, replica, message))
507
+ return;
508
+ if (ManagedServerCoordinatorValues.subscriptionInstalledMessage(message, slot.slot, replica.incarnation)) {
509
+ if (slot.replica === replica)
510
+ this.#settleSubscriptionWaiter(replica, message.subscriptionId);
511
+ return;
512
+ }
513
+ if (this.#onTransitionMessage(slot, replica, message))
514
+ return;
515
+ this.#onReadyMessage(slot, replica, message);
516
+ }
517
+ /**
518
+ * Settles the private child shutdown notifications.
519
+ *
520
+ * @param slot The reported replica slot.
521
+ * @param replica The matching child incarnation.
522
+ * @param message The candidate notification.
523
+ * @returns Whether a close-phase notification was consumed.
524
+ */
525
+ #onCloseMessage(slot, replica, message) {
431
526
  const closeResult = ManagedServerCoordinatorValues.closeResultMessage(message, slot.slot, replica.incarnation);
432
527
  if (closeResult !== undefined) {
433
528
  if (slot.replica !== replica || !replica.draining || replica.closeAttempt === undefined)
434
- return;
529
+ return true;
435
530
  if (closeResult === "closed")
436
531
  replica.closeAttempt.outcome.resolve(undefined);
437
- else
532
+ else {
533
+ replica.closeAttempt.drained.reject(new Error("Managed child Delivery drain failed."));
438
534
  replica.closeAttempt.outcome.reject(new Error("Managed child server close failed."));
439
- return;
535
+ }
536
+ return true;
440
537
  }
441
- if (ManagedServerCoordinatorValues.subscriptionInstalledMessage(message, slot.slot, replica.incarnation)) {
442
- if (slot.replica !== replica)
443
- return;
444
- this.#settleSubscriptionWaiter(replica, message.subscriptionId);
445
- return;
538
+ if (ManagedServerCoordinatorValues.deliveryDrainedMessage(message, slot.slot, replica.incarnation)) {
539
+ if (replica.closeAttempt !== undefined)
540
+ replica.closeAttempt.drainedAcknowledged = true;
541
+ replica.closeAttempt?.drained.resolve(undefined);
542
+ return true;
446
543
  }
544
+ return false;
545
+ }
546
+ /**
547
+ * Advances a synchronizing or draining child without completing public readiness.
548
+ *
549
+ * @param slot The reported replica slot.
550
+ * @param replica The matching child incarnation.
551
+ * @param message The candidate notification.
552
+ * @returns Whether a transition notification was consumed.
553
+ */
554
+ #onTransitionMessage(slot, replica, message) {
447
555
  if (ManagedServerCoordinatorValues.synchronizingMessage(message, slot.slot, replica.incarnation)) {
448
556
  if (slot.replica !== replica || replica.endpoint !== undefined)
449
- return;
557
+ return true;
450
558
  replica.endpoint = message.endpoint;
451
559
  replica.synchronizing = true;
452
560
  this.#notifyReadyMembers();
453
561
  if (this.#nodeCoordinator === undefined)
454
562
  replica.child.send({ type: "activate" }, () => undefined);
455
- return;
563
+ return true;
456
564
  }
457
565
  if (ManagedServerCoordinatorValues.drainingMessage(message, slot.slot, replica.incarnation)) {
458
566
  if (slot.replica !== replica)
459
- return;
567
+ return true;
460
568
  replica.closeAttempt?.acknowledged.resolve(undefined);
461
569
  if (replica.draining)
462
- return;
570
+ return true;
463
571
  replica.draining = true;
464
572
  replica.expectedExit = true;
465
573
  this.#notifyReadyMembers();
466
- return;
574
+ return true;
467
575
  }
576
+ return false;
577
+ }
578
+ /**
579
+ * Publishes one fully ready child after its synchronization phase.
580
+ *
581
+ * @param slot The reported replica slot.
582
+ * @param replica The matching child incarnation.
583
+ * @param message The candidate notification.
584
+ */
585
+ #onReadyMessage(slot, replica, message) {
468
586
  if (!ManagedServerCoordinatorValues.readyMessage(message, slot.slot, replica.incarnation))
469
587
  return;
470
588
  if (slot.replica !== replica || replica.readyAt !== undefined || this.#closing)
@@ -490,6 +608,9 @@ export class ManagedServerCoordinator {
490
608
  if (slot.replica !== replica || replica.terminal)
491
609
  return;
492
610
  replica.terminal = true;
611
+ if (this.#closing && replica.closeFailure !== undefined)
612
+ this.#terminalChildFailure ??= replica.closeFailure;
613
+ this.#failIncompleteDrain(replica.closeAttempt);
493
614
  this.#clearSubscriptionWaiters(replica, new Error("Managed child exited before subscription installed."));
494
615
  slot.replica = undefined;
495
616
  this.#notifyReadyMembers();
@@ -499,10 +620,10 @@ export class ManagedServerCoordinator {
499
620
  }
500
621
  if (this.#closing || replica.expectedExit)
501
622
  return;
502
- if (replica.readyAt !== undefined &&
503
- this.#dependencies.clock.now() - replica.readyAt >= this.#restart.healthyReadyMs) {
623
+ const healthy = replica.readyAt !== undefined &&
624
+ this.#dependencies.clock.now() - replica.readyAt >= this.#restart.healthyReadyMs;
625
+ if (healthy)
504
626
  slot.failures = 0;
505
- }
506
627
  // spine-log-boundary: server.managed_replica_exit
507
628
  emitServerWarning(lifecycleLogger, "Managed replica exited unexpectedly.", {
508
629
  operation: "managed_replica.exit",
@@ -514,6 +635,20 @@ export class ManagedServerCoordinator {
514
635
  this.#scheduleReplacement(slot);
515
636
  this.#drainStarts();
516
637
  }
638
+ /**
639
+ * Retains an exited child's missing drain acknowledgement across close attempts.
640
+ *
641
+ * @param closeAttempt The pending close attempt, if any.
642
+ */
643
+ #failIncompleteDrain(closeAttempt) {
644
+ if (closeAttempt === undefined || closeAttempt.drainedAcknowledged)
645
+ return;
646
+ const failure = new Error("Managed child exited before Delivery drained.");
647
+ closeAttempt.exitFailure = failure;
648
+ if (this.#closing)
649
+ this.#terminalChildFailure ??= failure;
650
+ closeAttempt.drained.reject(failure);
651
+ }
517
652
  /**
518
653
  * Retires a child after a process error and starts bounded termination.
519
654
  *
@@ -571,37 +706,7 @@ export class ManagedServerCoordinator {
571
706
  const closing = this.#close;
572
707
  if (closing !== undefined)
573
708
  return closing;
574
- const close = (async () => {
575
- this.#closing = true;
576
- this.#removeSignalHandlers();
577
- for (const slot of this.#slots) {
578
- if (slot.replacementTimer !== undefined)
579
- this.#dependencies.clock.clearTimeout(slot.replacementTimer);
580
- slot.replacementTimer = undefined;
581
- }
582
- // `beginDrain()` makes the unary snapshot empty synchronously. Start every
583
- // child quiescence attempt without waiting for its reconciliation promise:
584
- // child close IPC must be issued in this turn, while the Coordinator keeps
585
- // subscription relays alive until every child has settled.
586
- const draining = this.#nodeCoordinator?.beginDrain();
587
- for (const slot of this.#slots)
588
- if (slot.replica !== undefined)
589
- this.#clearSubscriptionWaiters(slot.replica, new Error("Managed application is closing before subscription installed."));
590
- const childCloses = [
591
- ...this.#slots.map((slot) => this.#closeReplica(slot.replica)),
592
- ...[...this.#retired].map((replica) => this.#closeRetired(replica)),
593
- ];
594
- const results = await Promise.allSettled([draining, ...childCloses]);
595
- const failures = [];
596
- for (const result of results)
597
- if (result.status === "rejected")
598
- failures.push(result.reason);
599
- if (failures.length === 1)
600
- throw failures[0];
601
- if (failures.length > 1)
602
- throw new AggregateError(failures, "Managed child quiescence failed during drain.");
603
- await this.#nodeCoordinator?.close();
604
- })();
709
+ const close = this.#closeGracefully();
605
710
  this.#close = close;
606
711
  void close.then(undefined, () => {
607
712
  if (this.#close === close)
@@ -609,6 +714,91 @@ export class ManagedServerCoordinator {
609
714
  });
610
715
  return close;
611
716
  }
717
+ /**
718
+ * Stops replacement starts and awaits all cleanup phases.
719
+ */
720
+ async #closeGracefully() {
721
+ this.#closing = true;
722
+ this.#removeSignalHandlers();
723
+ for (const slot of this.#slots) {
724
+ if (slot.replacementTimer !== undefined)
725
+ this.#dependencies.clock.clearTimeout(slot.replacementTimer);
726
+ slot.replacementTimer = undefined;
727
+ }
728
+ await this.#drainAndCloseReplicas();
729
+ }
730
+ /**
731
+ * Drains admitted child work, removes relays while listeners live, then closes children.
732
+ *
733
+ * @returns Completion after Coordinator and child cleanup attempts settle.
734
+ */
735
+ async #drainAndCloseReplicas() {
736
+ this.#drainStart ??= this.#nodeCoordinator?.beginDrain() ?? Promise.resolve();
737
+ const closing = new Error("Managed application is closing before subscription installed.");
738
+ for (const slot of this.#slots)
739
+ if (slot.replica !== undefined)
740
+ this.#clearSubscriptionWaiters(slot.replica, closing);
741
+ const replicas = this.#slots.flatMap((slot) => slot.replica === undefined ? [] : [slot.replica]);
742
+ const childCloses = [
743
+ ...replicas.map((replica) => this.#closeReplica(replica)),
744
+ ...[...this.#retired].map((replica) => this.#closeRetired(replica)),
745
+ ];
746
+ const children = Promise.allSettled(childCloses);
747
+ const failures = await this.#awaitDeliveryDrains(replicas);
748
+ await this.#finishClose(replicas, children, failures);
749
+ }
750
+ /**
751
+ * Cancels relays, releases children, and closes the Coordinator after a drain attempt.
752
+ * @param replicas The active children being closed.
753
+ * @param children The already-running bounded child close attempts.
754
+ * @param failures Errors collected from the drain phase.
755
+ */
756
+ async #finishClose(replicas, children, failures) {
757
+ try {
758
+ await this.#nodeCoordinator?.closeSubscriptions();
759
+ }
760
+ catch (error) {
761
+ failures.push(error);
762
+ }
763
+ for (const replica of replicas) {
764
+ try {
765
+ if (replica.child.connected)
766
+ replica.child.send({ type: "finish-close" }, () => undefined);
767
+ }
768
+ catch (error) {
769
+ failures.push(error);
770
+ }
771
+ }
772
+ const childResults = await children;
773
+ const coordinator = Promise.resolve().then(() => this.#nodeCoordinator?.close());
774
+ const [coordinatorResult] = await Promise.allSettled([coordinator]);
775
+ for (const result of childResults)
776
+ if (result.status === "rejected" && !failures.includes(result.reason))
777
+ failures.push(result.reason);
778
+ if (coordinatorResult.status === "rejected")
779
+ failures.push(coordinatorResult.reason);
780
+ if (this.#terminalChildFailure !== undefined && !failures.includes(this.#terminalChildFailure))
781
+ failures.push(this.#terminalChildFailure);
782
+ if (failures.length === 1)
783
+ throw failures[0];
784
+ if (failures.length > 1)
785
+ throw new AggregateError(failures, "Managed child quiescence failed during drain.");
786
+ }
787
+ /**
788
+ * Waits for actual child Delivery drains without awaiting final child exits.
789
+ *
790
+ * @param replicas The active child incarnations.
791
+ * @returns Drain failures to report after child and Coordinator cleanup.
792
+ */
793
+ async #awaitDeliveryDrains(replicas) {
794
+ const deliveryDrains = replicas.map((replica) => replica.closeAttempt?.drained.promise ?? Promise.resolve());
795
+ const results = await Promise.allSettled([this.#drainStart, ...deliveryDrains]);
796
+ const failures = [];
797
+ for (const result of results)
798
+ if (result.status === "rejected")
799
+ failures.push(result.reason);
800
+ return failures;
801
+ }
612
802
  /**
613
803
  * Closes the current replica through the bounded child shutdown procedure.
614
804
  *
@@ -616,7 +806,28 @@ export class ManagedServerCoordinator {
616
806
  * @returns Completion after child shutdown.
617
807
  */
618
808
  #closeReplica(replica) {
619
- return ManagedServerCoordinatorValues.close(replica, this.#dependencies.clock);
809
+ if (replica === undefined)
810
+ return Promise.resolve();
811
+ if (replica.closePromise !== undefined)
812
+ return replica.closePromise;
813
+ replica.closeFailure = undefined;
814
+ const close = ManagedServerCoordinatorValues.close(replica, this.#dependencies.clock);
815
+ replica.closePromise = close;
816
+ void close.then(() => {
817
+ replica.closeFailure = undefined;
818
+ }, (error) => {
819
+ replica.closeFailure = error;
820
+ if (replica.forcedFailure !== undefined || replica.terminal)
821
+ this.#terminalChildFailure ??= replica.forcedFailure ?? error;
822
+ });
823
+ // spine-log-boundary: server.managed_replica_close_finally_observer
824
+ void close
825
+ .finally(() => {
826
+ if (replica.closePromise === close)
827
+ replica.closePromise = undefined;
828
+ })
829
+ .catch(() => undefined);
830
+ return close;
620
831
  }
621
832
  /**
622
833
  * Terminates a retired replica and removes it from the pending set.
@@ -928,6 +1139,8 @@ const ManagedServerCoordinatorValues = Object.freeze({
928
1139
  resolve = next;
929
1140
  reject = fail;
930
1141
  });
1142
+ // spine-log-boundary: server.managed_deferred_rejection_observer
1143
+ void promise.catch(() => undefined);
931
1144
  return { promise, resolve, reject };
932
1145
  },
933
1146
  /**
@@ -943,7 +1156,7 @@ const ManagedServerCoordinatorValues = Object.freeze({
943
1156
  },
944
1157
  dependencies: {
945
1158
  clock: {
946
- now: () => Time.currentTimeMillis(),
1159
+ now: () => Time.monotonicTime(),
947
1160
  setTimeout: (onTimeout, delay) => setTimeout(onTimeout, delay),
948
1161
  clearTimeout: (timer) => {
949
1162
  clearTimeout(timer);
@@ -1053,6 +1266,22 @@ const ManagedServerCoordinatorValues = Object.freeze({
1053
1266
  candidate.slot === String(slot) &&
1054
1267
  candidate.incarnation === incarnation);
1055
1268
  },
1269
+ /**
1270
+ * Matches the child's completed Delivery drain before its listener closes.
1271
+ *
1272
+ * @param message The candidate child IPC notification.
1273
+ * @param slot The expected replica slot.
1274
+ * @param incarnation The expected child incarnation token.
1275
+ * @returns Whether the notification belongs to that child.
1276
+ */
1277
+ deliveryDrainedMessage(message, slot, incarnation) {
1278
+ if (typeof message !== "object" || message === null)
1279
+ return false;
1280
+ const candidate = message;
1281
+ return (candidate.type === "delivery-drained" &&
1282
+ candidate.slot === String(slot) &&
1283
+ candidate.incarnation === incarnation);
1284
+ },
1056
1285
  /**
1057
1286
  * Matches a CLOSED or CLOSE-FAILED notification from the expected child.
1058
1287
  *
@@ -1090,35 +1319,93 @@ const ManagedServerCoordinatorValues = Object.freeze({
1090
1319
  }));
1091
1320
  const closeAttempt = {
1092
1321
  acknowledged: ManagedServerCoordinatorValues.deferred(),
1322
+ drained: ManagedServerCoordinatorValues.deferred(),
1093
1323
  outcome: ManagedServerCoordinatorValues.deferred(),
1094
1324
  };
1095
1325
  replica.closeAttempt = closeAttempt;
1096
1326
  try {
1097
- if (child.connected)
1098
- child.send({ type: "close" }, () => undefined);
1099
- const observation = Promise.race([
1100
- closeAttempt.acknowledged.promise.then(() => "acknowledged"),
1101
- exited.then(() => "exited"),
1102
- ]);
1103
- if (await ManagedServerCoordinatorValues.within(observation, closeGraceMs, clock)) {
1104
- if ((await observation) === "exited")
1105
- return;
1106
- await Promise.race([
1107
- closeAttempt.outcome.promise,
1108
- exited.then(() => {
1109
- throw new Error("Managed child exited before graceful close completed.");
1110
- }),
1111
- ]);
1112
- await ManagedServerCoordinatorValues.terminate(child, clock, exited);
1113
- return;
1114
- }
1115
- await ManagedServerCoordinatorValues.terminate(child, clock, exited, false);
1327
+ await ManagedServerCoordinatorValues.awaitChildClose(child, closeAttempt, exited, clock);
1328
+ }
1329
+ catch (error) {
1330
+ replica.forcedFailure = closeAttempt.forcedFailure;
1331
+ throw error;
1116
1332
  }
1117
1333
  finally {
1118
1334
  if (replica.closeAttempt === closeAttempt)
1119
1335
  replica.closeAttempt = undefined;
1120
1336
  }
1121
1337
  },
1338
+ /**
1339
+ * Waits for child drain and final close, escalating only after bounded acknowledgement.
1340
+ *
1341
+ * @param child The child process to close.
1342
+ * @param closeAttempt Tracks the child's IPC phase acknowledgements.
1343
+ * @param exited Resolves when the child exits.
1344
+ * @param clock Supplies bounded shutdown timers.
1345
+ * @returns Completion after the child has exited or close failed.
1346
+ */
1347
+ async awaitChildClose(child, closeAttempt, exited, clock) {
1348
+ if (child.connected)
1349
+ child.send({ type: "close" }, () => undefined);
1350
+ const observation = Promise.race([
1351
+ closeAttempt.acknowledged.promise.then(() => "acknowledged"),
1352
+ exited.then(() => "exited"),
1353
+ ]);
1354
+ if (!(await ManagedServerCoordinatorValues.within(observation, closeGraceMs, clock))) {
1355
+ await this.missingDrainAck(child, closeAttempt, exited, clock);
1356
+ }
1357
+ if ((await observation) === "exited") {
1358
+ this.verifyExitedDrain(closeAttempt);
1359
+ return;
1360
+ }
1361
+ await ManagedServerCoordinatorValues.awaitChildOutcome(closeAttempt, exited);
1362
+ await ManagedServerCoordinatorValues.terminate(child, clock, exited);
1363
+ },
1364
+ /**
1365
+ * Rejects an unacknowledged Delivery drain after bounded child termination.
1366
+ *
1367
+ * @param child The unresponsive child process.
1368
+ * @param closeAttempt The current close attempt.
1369
+ * @param exited Resolves when the child exits.
1370
+ * @param clock Supplies bounded shutdown timers.
1371
+ * @returns No value; the missing acknowledgement always rejects.
1372
+ */
1373
+ async missingDrainAck(child, closeAttempt, exited, clock) {
1374
+ const failure = new Error("Managed child did not acknowledge Delivery drain.");
1375
+ closeAttempt.forcedFailure = failure;
1376
+ closeAttempt.drained.reject(failure);
1377
+ try {
1378
+ await this.terminate(child, clock, exited, false);
1379
+ }
1380
+ catch (termination) {
1381
+ throw new AggregateError([failure, termination], "Managed child shutdown failed.");
1382
+ }
1383
+ throw failure;
1384
+ },
1385
+ /**
1386
+ * Rejects a child exit that preceded its Delivery drain acknowledgement.
1387
+ *
1388
+ * @param closeAttempt The observed close attempt.
1389
+ */
1390
+ verifyExitedDrain(closeAttempt) {
1391
+ if (closeAttempt.exitFailure !== undefined)
1392
+ throw closeAttempt.exitFailure;
1393
+ },
1394
+ /**
1395
+ * Waits for the real child close outcome before process reaping.
1396
+ *
1397
+ * @param closeAttempt Tracks the child's close notifications.
1398
+ * @param exited Resolves when the child process exits.
1399
+ * @returns Completion after the child's final close fact.
1400
+ */
1401
+ async awaitChildOutcome(closeAttempt, exited) {
1402
+ await Promise.race([
1403
+ closeAttempt.outcome.promise,
1404
+ exited.then(() => {
1405
+ throw new Error("Managed child exited before graceful close completed.");
1406
+ }),
1407
+ ]);
1408
+ },
1122
1409
  /**
1123
1410
  * Waits for child exit, then escalates through SIGTERM and SIGKILL within bounded intervals.
1124
1411
  *
@@ -1161,6 +1448,9 @@ const ManagedServerCoordinatorValues = Object.freeze({
1161
1448
  void promise.then(() => {
1162
1449
  clock.clearTimeout(timer);
1163
1450
  resolve(true);
1451
+ }, () => {
1452
+ clock.clearTimeout(timer);
1453
+ resolve(true);
1164
1454
  });
1165
1455
  });
1166
1456
  },