@spine-event-engine/server 2.0.0-snapshot.16 → 2.0.0-snapshot.20

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 (74) hide show
  1. package/README.md +17 -3
  2. package/REFERENCE.md +66 -12
  3. package/dist/context/bounded-context.d.ts +72 -14
  4. package/dist/context/bounded-context.d.ts.map +1 -1
  5. package/dist/context/bounded-context.js +130 -49
  6. package/dist/context/bounded-context.js.map +1 -1
  7. package/dist/context/effective-tenant.d.ts +9 -0
  8. package/dist/context/effective-tenant.d.ts.map +1 -0
  9. package/dist/context/effective-tenant.js +58 -0
  10. package/dist/context/effective-tenant.js.map +1 -0
  11. package/dist/context/tenant-index.d.ts.map +1 -1
  12. package/dist/context/tenant-index.js +70 -1
  13. package/dist/context/tenant-index.js.map +1 -1
  14. package/dist/delivery/delivery.d.ts.map +1 -1
  15. package/dist/delivery/delivery.js +270 -26
  16. package/dist/delivery/delivery.js.map +1 -1
  17. package/dist/delivery/inbox-storage.d.ts +7 -0
  18. package/dist/delivery/inbox-storage.d.ts.map +1 -1
  19. package/dist/delivery/inbox-storage.js +110 -0
  20. package/dist/delivery/inbox-storage.js.map +1 -1
  21. package/dist/entity/entity-metadata.d.ts +2 -2
  22. package/dist/entity/entity-metadata.js +2 -2
  23. package/dist/entity/entity-metadata.js.map +1 -1
  24. package/dist/entity/entity-storage-descriptor.d.ts +24 -4
  25. package/dist/entity/entity-storage-descriptor.d.ts.map +1 -1
  26. package/dist/entity/entity-storage-descriptor.js +69 -23
  27. package/dist/entity/entity-storage-descriptor.js.map +1 -1
  28. package/dist/entity/entity.d.ts +19 -18
  29. package/dist/entity/entity.d.ts.map +1 -1
  30. package/dist/entity/entity.js +61 -24
  31. package/dist/entity/entity.js.map +1 -1
  32. package/dist/entity/spec-scanner.d.ts +13 -1
  33. package/dist/entity/spec-scanner.d.ts.map +1 -1
  34. package/dist/entity/spec-scanner.js +20 -3
  35. package/dist/entity/spec-scanner.js.map +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/repository/command-routing.d.ts +63 -10
  40. package/dist/repository/command-routing.d.ts.map +1 -1
  41. package/dist/repository/command-routing.js +41 -8
  42. package/dist/repository/command-routing.js.map +1 -1
  43. package/dist/repository/event-routing.d.ts +64 -9
  44. package/dist/repository/event-routing.d.ts.map +1 -1
  45. package/dist/repository/event-routing.js +42 -7
  46. package/dist/repository/event-routing.js.map +1 -1
  47. package/dist/repository/repository.d.ts +96 -10
  48. package/dist/repository/repository.d.ts.map +1 -1
  49. package/dist/repository/repository.js +437 -133
  50. package/dist/repository/repository.js.map +1 -1
  51. package/dist/repository/state-update-routing.d.ts +67 -8
  52. package/dist/repository/state-update-routing.d.ts.map +1 -1
  53. package/dist/repository/state-update-routing.js +45 -6
  54. package/dist/repository/state-update-routing.js.map +1 -1
  55. package/dist/server/server.d.ts.map +1 -1
  56. package/dist/server/server.js +407 -109
  57. package/dist/server/server.js.map +1 -1
  58. package/dist/services/registered-targets.d.ts +73 -0
  59. package/dist/services/registered-targets.d.ts.map +1 -0
  60. package/dist/services/registered-targets.js +127 -0
  61. package/dist/services/registered-targets.js.map +1 -0
  62. package/dist/services/spine-services.d.ts +26 -12
  63. package/dist/services/spine-services.d.ts.map +1 -1
  64. package/dist/services/spine-services.js +1044 -962
  65. package/dist/services/spine-services.js.map +1 -1
  66. package/dist/stand/stand.d.ts +73 -1
  67. package/dist/stand/stand.d.ts.map +1 -1
  68. package/dist/stand/stand.js +220 -48
  69. package/dist/stand/stand.js.map +1 -1
  70. package/dist/stand/subscription-observer.d.ts.map +1 -1
  71. package/dist/stand/subscription-observer.js +4 -11
  72. package/dist/stand/subscription-observer.js.map +1 -1
  73. package/dist/tsconfig.tsbuildinfo +1 -1
  74. package/package.json +12 -12
@@ -15,6 +15,7 @@ import * as http2 from "node:http2";
15
15
  import { connectNodeAdapter } from "@connectrpc/connect-node";
16
16
  import { BoundedContext, BoundedContextBuilder, boundedContextAccess, } from "../context/bounded-context.js";
17
17
  import { SpineServices, spineServicesAccess, } from "../services/spine-services.js";
18
+ import { RegisteredTargets } from "../services/registered-targets.js";
18
19
  import { ProcessServerCoordinator } from "./process-server-coordinator.js";
19
20
  import { CloseErrors, RetryableCloseGroup } from "./retryable-close.js";
20
21
  import { ServerEnvironment, serverEnvironmentAccess } from "./server-environment.js";
@@ -25,6 +26,7 @@ const maximumMessageMaxBytes = 0xffff_ffff;
25
26
  const gracefulSessionDrainMs = 100;
26
27
  const runningContexts = new WeakMap();
27
28
  const serverHosts = new WeakMap();
29
+ const assemblingStands = new WeakMap();
28
30
  /**
29
31
  * Configures and starts local Spine Connect/gRPC-compatible services.
30
32
  *
@@ -191,75 +193,236 @@ export class Server {
191
193
  });
192
194
  return running;
193
195
  }
196
+ /**
197
+ * Builds contexts, attaches delivery, and opens the listener for one start attempt.
198
+ *
199
+ * @param ownership Whether the environment is managed by this Server.
200
+ * @returns The running listener after lifecycle startup.
201
+ */
194
202
  async #startOnce(ownership) {
195
- const contexts = await ServerValues.buildContexts(this.#contexts, this.#environment.storageFactory);
196
- const logger = serverEnvironmentAccess.loggerFor(this.#environment);
197
- for (const context of contexts) {
198
- boundedContextAccess.installLogger(context, logger);
199
- serverEnvironmentAccess.warnVolatileRegistry(this.#environment, context);
200
- }
201
- let attachment;
203
+ const admission = { token: {}, contexts: [] };
204
+ const contexts = await this.#prepareContexts(admission);
205
+ const { attachment, routes } = await this.#attachContexts(contexts, ownership, admission);
206
+ const running = await this.#openListener(contexts, attachment, routes);
207
+ await this.#startLifecycles(running);
208
+ return running;
209
+ }
210
+ /**
211
+ * Opens the service listener with retryable cleanup for every preparation phase.
212
+ *
213
+ * @param contexts Contexts exposed by the listener.
214
+ * @param attachment Active environment delivery attachment.
215
+ * @param routes Routes installed for these contexts.
216
+ * @returns The prepared running listener.
217
+ */
218
+ async #openListener(contexts, attachment, routes) {
219
+ const closeables = [...contexts, ...this.#resources];
220
+ const cleanup = {
221
+ closeGroup: new RetryableCloseGroup(closeables, "Server start cleanup failed while closing owned contexts/resources."),
222
+ attachment,
223
+ routes,
224
+ failedStartRollback: undefined,
225
+ };
202
226
  try {
203
- attachment = await serverEnvironmentAccess.attach(this.#environment, {
204
- ownership,
205
- descriptors: contexts.map((context) => boundedContextAccess.delivery(context)),
206
- });
227
+ return await this.#prepareListener(contexts, attachment, routes, closeables, cleanup);
207
228
  }
208
229
  catch (error) {
209
- const closeGroup = new RetryableCloseGroup([...contexts, ...this.#resources], "Server start cleanup failed while closing owned contexts/resources.");
210
- if (serverEnvironmentAccess.failedStartRetryPending(this.#environment, error)) {
211
- this.#failedStartCleanup = {
212
- closeGroup,
213
- failedStartRollback: { rejection: error },
214
- };
215
- }
216
- else {
217
- try {
218
- await closeGroup.close();
219
- }
220
- catch (cleanupError) {
221
- this.#failedStartCleanup = {
222
- closeGroup,
223
- failedStartRollback: undefined,
224
- };
225
- throw ServerValues.attachmentCleanupError(error, cleanupError);
226
- }
227
- this.#failedStartConsumed = true;
228
- }
229
- throw error;
230
- }
231
- const closeables = [...contexts, ...this.#resources];
232
- const services = new SpineServices({
233
- contexts,
234
- ...this.#services,
235
- });
236
- spineServicesAccess.installLogger(services, logger);
237
- const sessions = new Set();
238
- const httpServer = ServerValues.createHttpServer(services, sessions, this.#readMaxBytes, this.#writeMaxBytes);
239
- const listener = { host: this.#host, port: this.#port };
240
- const address = await ServerValues.listen(httpServer, listener.host, listener.port).catch(async (error) => {
241
- const cleanup = {
242
- closeGroup: new RetryableCloseGroup(closeables, "Server start cleanup failed while closing owned contexts/resources."),
243
- network: { server: httpServer, sessions },
244
- attachment,
245
- failedStartRollback: undefined,
246
- };
247
230
  this.#failedStartCleanup = cleanup;
248
231
  return this.#cleanupFailedListenerStart(cleanup, error);
249
- });
232
+ }
233
+ }
234
+ /**
235
+ * Constructs services and the HTTP adapter, then binds the listener.
236
+ *
237
+ * @param contexts Contexts exposed by this Server.
238
+ * @param attachment Active delivery attachment.
239
+ * @param routes Installed state routes.
240
+ * @param closeables Contexts and resources for eventual shutdown.
241
+ * @param cleanup Retained cleanup state if preparation fails.
242
+ * @returns The prepared running listener.
243
+ */
244
+ async #prepareListener(contexts, attachment, routes, closeables, cleanup) {
245
+ const services = new SpineServices({ contexts, ...this.#services });
246
+ cleanup.services = services;
247
+ spineServicesAccess.installLogger(services, serverEnvironmentAccess.loggerFor(this.#environment));
248
+ const { httpServer, sessions } = this.#createHttpListener(services);
249
+ cleanup.network = { server: httpServer, sessions };
250
+ const address = await ServerValues.listen(httpServer, this.#host, this.#port);
250
251
  const host = typeof address.address === "string" ? address.address : this.#host;
251
- const running = new RunningHttp2Server({
252
+ return this.#createRunningServer({
252
253
  server: httpServer,
253
254
  sessions,
254
255
  environment: this.#environment,
255
256
  attachment,
257
+ routes,
256
258
  services,
257
259
  host,
258
260
  port: address.port,
261
+ contexts,
259
262
  closeables,
263
+ });
264
+ }
265
+ /**
266
+ * Prepares an HTTP listener and its tracked sessions for startup.
267
+ *
268
+ * @param services Service routes exposed by the listener.
269
+ * @returns The unbound listener and its session set.
270
+ */
271
+ #createHttpListener(services) {
272
+ const sessions = new Set();
273
+ const httpServer = ServerValues.createHttpServer(services, sessions, this.#readMaxBytes, this.#writeMaxBytes);
274
+ return { httpServer, sessions };
275
+ }
276
+ /**
277
+ * Creates the listener handle and associates its context set.
278
+ *
279
+ * @param options Listener resources prepared during startup.
280
+ * @returns The not-yet-started listener handle.
281
+ */
282
+ #createRunningServer(options) {
283
+ const running = new RunningHttp2Server({
284
+ ...options,
260
285
  listenerLifecycles: this.#listenerLifecycles,
261
286
  });
262
- runningContexts.set(running, contexts);
287
+ runningContexts.set(running, options.contexts);
288
+ return running;
289
+ }
290
+ /**
291
+ * Reserves prebuilt contexts before awaiting builders, then assembles each builder.
292
+ *
293
+ * @param admission Attempt-local reservations retained through cleanup.
294
+ * @returns Prepared contexts for this Server.
295
+ */
296
+ async #prepareContexts(admission) {
297
+ const contexts = [];
298
+ let conflict;
299
+ let conflicted = false;
300
+ for (const entry of this.#contexts) {
301
+ if (boundedContextAccess.isBuilder(entry))
302
+ continue;
303
+ try {
304
+ this.#admitContext(entry, admission);
305
+ }
306
+ catch (error) {
307
+ if (!conflicted)
308
+ conflict = error;
309
+ conflicted = true;
310
+ }
311
+ }
312
+ try {
313
+ if (conflicted)
314
+ throw conflict;
315
+ for (const entry of this.#contexts) {
316
+ const context = boundedContextAccess.isBuilder(entry)
317
+ ? await boundedContextAccess.build(entry, this.#environment.storageFactory)
318
+ : entry;
319
+ if (boundedContextAccess.isBuilder(entry))
320
+ this.#admitContext(context, admission);
321
+ contexts.push(context);
322
+ }
323
+ return contexts;
324
+ }
325
+ catch (error) {
326
+ await this.#cleanupFailedAttachment(admission.contexts, undefined, error, admission);
327
+ throw error;
328
+ }
329
+ }
330
+ /**
331
+ * Reserves one context Stand while this Server assembles its routes.
332
+ *
333
+ * @param context Context to reserve.
334
+ * @param admission Attempt-local reservations and token.
335
+ */
336
+ #admitContext(context, admission) {
337
+ const stand = context.stand();
338
+ const token = assemblingStands.get(stand);
339
+ if (token !== undefined || RegisteredTargets.forStand(stand) !== undefined) {
340
+ throw new Error("Stand is already associated with another Server or assembly.");
341
+ }
342
+ assemblingStands.set(stand, admission.token);
343
+ admission.contexts.push(context);
344
+ }
345
+ /**
346
+ * Clears only reservations made by the given assembly attempt.
347
+ *
348
+ * @param admission Attempt whose reserved Stands may be released.
349
+ */
350
+ #releaseAdmission(admission) {
351
+ for (const context of admission.contexts) {
352
+ const stand = context.stand();
353
+ if (assemblingStands.get(stand) === admission.token)
354
+ assemblingStands.delete(stand);
355
+ }
356
+ admission.contexts.length = 0;
357
+ }
358
+ /**
359
+ * Validates state routes before delivery recovery can invoke handlers.
360
+ *
361
+ * @param contexts Built contexts for this Server.
362
+ * @param ownership Environment attachment mode.
363
+ * @param admission Reservations transferred to installed routes before attachment.
364
+ * @returns The environment attachment and its installed routes.
365
+ */
366
+ async #attachContexts(contexts, ownership, admission) {
367
+ const freshContexts = contexts.filter((context) => RegisteredTargets.forStand(context.stand()) === undefined);
368
+ let routes;
369
+ try {
370
+ routes = new RegisteredTargets(contexts);
371
+ routes.install(contexts);
372
+ this.#releaseAdmission(admission);
373
+ const logger = serverEnvironmentAccess.loggerFor(this.#environment);
374
+ for (const context of contexts) {
375
+ boundedContextAccess.installLogger(context, logger);
376
+ serverEnvironmentAccess.warnVolatileRegistry(this.#environment, context);
377
+ }
378
+ const attachment = await serverEnvironmentAccess.attach(this.#environment, {
379
+ ownership,
380
+ descriptors: contexts.map((context) => boundedContextAccess.delivery(context)),
381
+ });
382
+ return { attachment, routes };
383
+ }
384
+ catch (error) {
385
+ await this.#cleanupFailedAttachment(freshContexts, routes, error, admission);
386
+ throw error;
387
+ }
388
+ }
389
+ /**
390
+ * Retains retryable cleanup when route validation or attachment fails.
391
+ *
392
+ * @param contexts Fresh contexts to close after failure.
393
+ * @param routes Routes installed by this attempt, if any.
394
+ * @param error Original route or attachment failure.
395
+ * @param admission Reservations retained until context cleanup succeeds.
396
+ */
397
+ async #cleanupFailedAttachment(contexts, routes, error, admission) {
398
+ const closeGroup = new RetryableCloseGroup([...contexts, ...this.#resources], "Server start cleanup failed while closing owned contexts/resources.");
399
+ if (serverEnvironmentAccess.failedStartRetryPending(this.#environment, error)) {
400
+ this.#failedStartCleanup = {
401
+ closeGroup,
402
+ failedStartRollback: { rejection: error },
403
+ routes,
404
+ admission,
405
+ };
406
+ return;
407
+ }
408
+ try {
409
+ await closeGroup.close();
410
+ }
411
+ catch (cleanupError) {
412
+ this.#failedStartCleanup = { closeGroup, failedStartRollback: undefined, routes, admission };
413
+ throw ServerValues.attachmentCleanupError(error, cleanupError);
414
+ }
415
+ routes?.release();
416
+ if (admission !== undefined)
417
+ this.#releaseAdmission(admission);
418
+ this.#failedStartConsumed = true;
419
+ }
420
+ /**
421
+ * Starts listener lifecycles and preserves failed close retries.
422
+ *
423
+ * @param running Listener whose lifecycles begin.
424
+ */
425
+ async #startLifecycles(running) {
263
426
  try {
264
427
  await running.startLifecycles();
265
428
  }
@@ -270,7 +433,6 @@ export class Server {
270
433
  this.#failedStartConsumed = true;
271
434
  throw error;
272
435
  }
273
- return running;
274
436
  }
275
437
  async #retryFailedStartCleanup(cleanup) {
276
438
  const errors = [];
@@ -301,6 +463,32 @@ export class Server {
301
463
  if (!(await this.#advanceFailedStartAttachment(cleanup, errors))) {
302
464
  return;
303
465
  }
466
+ await this.#retryFailedStartRollback(cleanup, errors);
467
+ let closeFailed = false;
468
+ try {
469
+ await cleanup.closeGroup.close();
470
+ }
471
+ catch (error) {
472
+ closeFailed = true;
473
+ CloseErrors.collect(error, errors);
474
+ }
475
+ if (!closeFailed && cleanup.attachment === undefined && this.#failedStartCleanup === cleanup) {
476
+ cleanup.routes?.release();
477
+ if (cleanup.admission !== undefined)
478
+ this.#releaseAdmission(cleanup.admission);
479
+ if (cleanup.services !== undefined)
480
+ spineServicesAccess.clearLogger(cleanup.services);
481
+ this.#failedStartCleanup = undefined;
482
+ this.#failedStartConsumed = true;
483
+ }
484
+ }
485
+ /**
486
+ * Retries environment rollback before closing contexts from a failed start.
487
+ *
488
+ * @param cleanup Retained failed-start state.
489
+ * @param errors Errors collected across cleanup phases.
490
+ */
491
+ async #retryFailedStartRollback(cleanup, errors) {
304
492
  const failedStartRollback = cleanup.failedStartRollback;
305
493
  if (failedStartRollback !== undefined &&
306
494
  serverEnvironmentAccess.failedStartRetryPending(this.#environment, failedStartRollback.rejection)) {
@@ -314,18 +502,6 @@ export class Server {
314
502
  CloseErrors.collect(error, errors);
315
503
  }
316
504
  }
317
- let closeFailed = false;
318
- try {
319
- await cleanup.closeGroup.close();
320
- }
321
- catch (error) {
322
- closeFailed = true;
323
- CloseErrors.collect(error, errors);
324
- }
325
- if (!closeFailed && cleanup.attachment === undefined && this.#failedStartCleanup === cleanup) {
326
- this.#failedStartCleanup = undefined;
327
- this.#failedStartConsumed = true;
328
- }
329
505
  }
330
506
  async #closeFailedStartNetwork(cleanup, errors) {
331
507
  const network = cleanup.network;
@@ -409,14 +585,19 @@ export const runningServerAccess = Object.freeze({
409
585
  return server instanceof RunningHttp2Server ? server.drainDelivery() : Promise.resolve();
410
586
  },
411
587
  });
588
+ /**
589
+ * Holds listener resources and closes them in recovery-safe phases.
590
+ */
412
591
  class RunningHttp2Server {
413
592
  #server;
414
593
  #sessions;
415
594
  #closeables;
595
+ #contexts;
416
596
  #listenerLifecycles;
417
597
  #startedLifecycles = [];
418
598
  #environment;
419
599
  #attachment;
600
+ #routes;
420
601
  #services;
421
602
  host;
422
603
  port;
@@ -425,19 +606,31 @@ class RunningHttp2Server {
425
606
  #networkClosed = false;
426
607
  #attachmentDetached = false;
427
608
  #closeGroup;
609
+ /**
610
+ * Captures the running listener's network, environment, and cleanup resources.
611
+ *
612
+ * @param options Resources prepared by Server startup.
613
+ */
428
614
  constructor(options) {
429
615
  this.#server = options.server;
430
616
  this.#sessions = options.sessions;
431
617
  this.#closeables = options.closeables;
618
+ this.#contexts = options.contexts;
432
619
  this.#listenerLifecycles = options.listenerLifecycles;
433
620
  this.#environment = options.environment;
434
621
  this.#attachment = options.attachment;
622
+ this.#routes = options.routes;
435
623
  this.#services = options.services;
436
624
  this.host = options.host;
437
625
  this.port = options.port;
438
626
  this.baseUrl = `http://${ServerValues.formatHostForUrl(options.host)}:${options.port.toString()}`;
439
627
  this.#closeGroup = new RetryableCloseGroup(this.#closeables, "Server close failed while closing owned contexts/resources.");
440
628
  }
629
+ /**
630
+ * Closes this listener once, preserving a failed close for retry.
631
+ *
632
+ * @returns Completion after resources close.
633
+ */
441
634
  close() {
442
635
  this.#closed ??= this.#closeOnce().catch((error) => {
443
636
  this.#closed = undefined;
@@ -467,11 +660,16 @@ class RunningHttp2Server {
467
660
  throw error;
468
661
  }
469
662
  }
663
+ /**
664
+ * Checks whether a failed lifecycle start still needs close retry.
665
+ *
666
+ * @returns `true` when no close completion is retained.
667
+ */
470
668
  hasPendingClose() {
471
669
  return this.#closed === undefined;
472
670
  }
473
671
  /**
474
- * Drains the server-owned Delivery attachment while network sessions remain available.
672
+ * Completes the server's Delivery detachment while network sessions remain available.
475
673
  *
476
674
  * @returns Completion after the attachment drains.
477
675
  */
@@ -486,7 +684,35 @@ class RunningHttp2Server {
486
684
  }
487
685
  this.#attachmentDetached = true;
488
686
  }
687
+ /**
688
+ * Closes network, delivery, accepted context work, and registered resources.
689
+ *
690
+ * @returns Completion after all resources close.
691
+ */
489
692
  async #closeOnce() {
693
+ await this.#closeListenerAndNetwork();
694
+ const { detachErrors, detachRejected } = await this.#detachDelivery();
695
+ await this.#drainContexts();
696
+ try {
697
+ await this.#closeGroup.close();
698
+ this.#routes.release();
699
+ }
700
+ catch (error) {
701
+ if (!detachRejected)
702
+ throw error;
703
+ CloseErrors.collect(error, detachErrors);
704
+ throw new AggregateError(detachErrors, "Server close failed while detaching delivery and closing owned contexts/resources.");
705
+ }
706
+ if (detachRejected)
707
+ ServerValues.throwRunningDetachErrors(detachErrors);
708
+ spineServicesAccess.clearLogger(this.#services);
709
+ }
710
+ /**
711
+ * Closes lifecycle attachments and network intake before delivery detachment.
712
+ *
713
+ * @returns Completion after the listener and sessions close.
714
+ */
715
+ async #closeListenerAndNetwork() {
490
716
  while (this.#startedLifecycles.length > 0) {
491
717
  const lifecycle = this.#startedLifecycles.at(-1);
492
718
  if (lifecycle === undefined)
@@ -498,6 +724,13 @@ class RunningHttp2Server {
498
724
  await ServerValues.closeNetwork(this.#server, this.#sessions);
499
725
  this.#networkClosed = true;
500
726
  }
727
+ }
728
+ /**
729
+ * Completes Delivery detachment while retaining errors for safe endpoint cleanup.
730
+ *
731
+ * @returns Detachment errors and whether detachment itself was rejected.
732
+ */
733
+ async #detachDelivery() {
501
734
  const detachErrors = [];
502
735
  let detachRejected = false;
503
736
  if (!this.#attachmentDetached) {
@@ -527,42 +760,38 @@ class RunningHttp2Server {
527
760
  }
528
761
  }
529
762
  }
530
- try {
531
- await this.#closeGroup.close();
532
- }
533
- catch (error) {
534
- if (!detachRejected) {
535
- throw error;
536
- }
537
- CloseErrors.collect(error, detachErrors);
538
- throw new AggregateError(detachErrors, "Server close failed while detaching delivery and closing owned contexts/resources.");
539
- }
540
- if (detachRejected) {
541
- ServerValues.throwRunningDetachErrors(detachErrors);
763
+ return { detachErrors, detachRejected };
764
+ }
765
+ /**
766
+ * Waits for every context's accepted work before any Stand closes.
767
+ *
768
+ * @returns Completion after all context work drains.
769
+ */
770
+ async #drainContexts() {
771
+ for (const context of this.#contexts)
772
+ boundedContextAccess.beginClose(context);
773
+ const drained = await Promise.allSettled(this.#contexts.map((context) => boundedContextAccess.drainWork(context)));
774
+ const drainErrors = drained.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
775
+ if (drainErrors.length > 0) {
776
+ throw new AggregateError(drainErrors, "Server close failed while draining contexts.");
542
777
  }
543
- spineServicesAccess.clearLogger(this.#services);
544
778
  }
545
779
  }
546
780
  /**
781
+ * Groups private server assembly, network, and shutdown operations.
547
782
  *
548
- * @internal Groups private server assembly, network, and shutdown operations.
783
+ * @internal
549
784
  */
550
785
  const ServerValues = Object.freeze({
551
- async buildContexts(entries, defaultStorageFactory) {
552
- const contexts = [];
553
- try {
554
- for (const entry of entries) {
555
- contexts.push(boundedContextAccess.isBuilder(entry)
556
- ? await boundedContextAccess.build(entry, defaultStorageFactory)
557
- : entry);
558
- }
559
- return contexts;
560
- }
561
- catch (error) {
562
- await ServerValues.cleanupBuiltContexts(contexts, error);
563
- throw error;
564
- }
565
- },
786
+ /**
787
+ * Creates the HTTP/2 listener with Connect routes and session tracking.
788
+ *
789
+ * @param services Public service adapters.
790
+ * @param sessions Set collecting active HTTP/2 sessions.
791
+ * @param readMaxBytes Maximum request size.
792
+ * @param writeMaxBytes Maximum response size.
793
+ * @returns An unbound HTTP/2 server.
794
+ */
566
795
  createHttpServer(services, sessions, readMaxBytes, writeMaxBytes) {
567
796
  const server = http2.createServer(connectNodeAdapter({
568
797
  routes: (router) => {
@@ -577,6 +806,14 @@ const ServerValues = Object.freeze({
577
806
  });
578
807
  return server;
579
808
  },
809
+ /**
810
+ * Binds a listener and resolves its actual address.
811
+ *
812
+ * @param server HTTP/2 server to bind.
813
+ * @param host Network host.
814
+ * @param port Requested port, including zero for an ephemeral port.
815
+ * @returns Bound listener address.
816
+ */
580
817
  listen(server, host, port) {
581
818
  return new Promise((resolve, reject) => {
582
819
  const cleanup = () => {
@@ -596,32 +833,38 @@ const ServerValues = Object.freeze({
596
833
  server.listen(port, host);
597
834
  });
598
835
  },
599
- async cleanupBuiltContexts(contexts, startError) {
600
- try {
601
- await new RetryableCloseGroup(contexts, "Server start cleanup failed while closing owned contexts/resources.").close();
602
- }
603
- catch (error) {
604
- throw new AggregateError([startError, ...ServerValues.toCleanupErrors(error)], "Server start failed while building bounded contexts.");
605
- }
606
- },
607
- toCleanupErrors(error) {
608
- if (error instanceof AggregateError) {
609
- return error.errors;
610
- }
611
- return [error];
612
- },
836
+ /**
837
+ * Collects attachment and immediate cleanup failures for startup reporting.
838
+ *
839
+ * @param startError Attachment or duplicate registration failure.
840
+ * @param cleanupError Failure while closing prepared resources.
841
+ * @returns Both failures in one AggregateError.
842
+ */
613
843
  attachmentCleanupError(startError, cleanupError) {
614
844
  const errors = [];
615
845
  CloseErrors.collect(startError, errors);
616
846
  CloseErrors.collect(cleanupError, errors);
617
847
  return new AggregateError(errors, "Server attachment failed and immediate dependency cleanup also failed.");
618
848
  },
849
+ /**
850
+ * Throws one deferred cleanup failure or groups several.
851
+ *
852
+ * @param errors Deferred cleanup failures.
853
+ * @returns Never; this method throws.
854
+ */
619
855
  throwCleanupErrors(errors) {
620
856
  if (errors.length === 1) {
621
857
  throw errors[0];
622
858
  }
623
859
  throw new AggregateError(errors, "Server deferred failed-start cleanup failed.");
624
860
  },
861
+ /**
862
+ * Throws a listener-start failure with any rollback failures.
863
+ *
864
+ * @param startError Original listener failure.
865
+ * @param cleanupErrors Failures while closing startup resources.
866
+ * @returns Never; this method throws.
867
+ */
625
868
  throwListenerStartError(startError, cleanupErrors) {
626
869
  if (cleanupErrors.length === 0) {
627
870
  throw startError;
@@ -633,18 +876,37 @@ const ServerValues = Object.freeze({
633
876
  }
634
877
  throw new AggregateError(errors, "Server start failed while opening listener and cleanup also failed.");
635
878
  },
879
+ /**
880
+ * Throws one delivery-detachment error or groups several.
881
+ *
882
+ * @param errors Delivery-detachment failures.
883
+ * @returns Never; this method throws.
884
+ */
636
885
  throwRunningDetachErrors(errors) {
637
886
  if (errors.length === 1) {
638
887
  throw errors[0];
639
888
  }
640
889
  throw new AggregateError(errors, "Server close failed while detaching delivery.");
641
890
  },
891
+ /**
892
+ * Closes the HTTP/2 server and all tracked sessions.
893
+ *
894
+ * @param server Listener being closed.
895
+ * @param sessions Sessions admitted by the listener.
896
+ * @returns Completion after the network releases its resources.
897
+ */
642
898
  async closeNetwork(server, sessions) {
643
899
  const closed = ServerValues.closeHttpServer(server);
644
900
  await ServerValues.closeSessions(sessions);
645
901
  await closed;
646
902
  await ServerValues.nextTurn();
647
903
  },
904
+ /**
905
+ * Closes the listener, including a listener that never started.
906
+ *
907
+ * @param server Listener being closed.
908
+ * @returns Completion after the listener closes.
909
+ */
648
910
  closeHttpServer(server) {
649
911
  return new Promise((resolve, reject) => {
650
912
  if (!server.listening) {
@@ -661,9 +923,21 @@ const ServerValues = Object.freeze({
661
923
  });
662
924
  });
663
925
  },
926
+ /**
927
+ * Closes all active HTTP/2 sessions.
928
+ *
929
+ * @param sessions Active sessions to close.
930
+ * @returns Completion after every session closes.
931
+ */
664
932
  async closeSessions(sessions) {
665
933
  await Promise.all([...sessions].map((session) => ServerValues.closeSession(session)));
666
934
  },
935
+ /**
936
+ * Closes one session gracefully or destroys it after a bounded wait.
937
+ *
938
+ * @param session Active HTTP/2 session.
939
+ * @returns Completion after close or forced destruction.
940
+ */
667
941
  closeSession(session) {
668
942
  return new Promise((resolve) => {
669
943
  const timer = setTimeout(() => {
@@ -679,14 +953,31 @@ const ServerValues = Object.freeze({
679
953
  session.close();
680
954
  });
681
955
  },
956
+ /**
957
+ * Waits one event-loop turn for pending close callbacks.
958
+ *
959
+ * @returns Completion on the next turn.
960
+ */
682
961
  nextTurn() {
683
962
  return new Promise((resolve) => {
684
963
  setImmediate(resolve);
685
964
  });
686
965
  },
966
+ /**
967
+ * Wraps an IPv6 host for URL authority syntax.
968
+ *
969
+ * @param host Bound network host.
970
+ * @returns Host formatted for the base URL.
971
+ */
687
972
  formatHostForUrl(host) {
688
973
  return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
689
974
  },
975
+ /**
976
+ * Normalizes a non-blank host for listener binding.
977
+ *
978
+ * @param host Configured host, if supplied.
979
+ * @returns Normalized host or the loopback default.
980
+ */
690
981
  normalizeHost(host) {
691
982
  const normalized = host?.trim() ?? defaultHost;
692
983
  if (normalized.length === 0) {
@@ -694,6 +985,13 @@ const ServerValues = Object.freeze({
694
985
  }
695
986
  return normalized;
696
987
  },
988
+ /**
989
+ * Validates a configured HTTP message-size limit.
990
+ *
991
+ * @param value Requested byte limit.
992
+ * @param name Option name used in diagnostics.
993
+ * @returns Accepted byte limit.
994
+ */
697
995
  normalizeMessageMaxBytes(value, name) {
698
996
  if (!Number.isInteger(value) || value < 1 || value > maximumMessageMaxBytes) {
699
997
  throw new Error(`Server ${name} must be an integer from 1 through 4294967295.`);