@c9up/ream 0.2.16 → 0.2.18

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 (93) hide show
  1. package/dist/Application.d.ts +54 -8
  2. package/dist/Application.d.ts.map +1 -1
  3. package/dist/Application.js +77 -7
  4. package/dist/Application.js.map +1 -1
  5. package/dist/HttpKernel.d.ts +10 -0
  6. package/dist/HttpKernel.d.ts.map +1 -1
  7. package/dist/HttpKernel.js +61 -5
  8. package/dist/HttpKernel.js.map +1 -1
  9. package/dist/Ignitor.d.ts.map +1 -1
  10. package/dist/Ignitor.js +76 -18
  11. package/dist/Ignitor.js.map +1 -1
  12. package/dist/commands/ScheduleList.d.ts.map +1 -1
  13. package/dist/commands/ScheduleList.js +29 -0
  14. package/dist/commands/ScheduleList.js.map +1 -1
  15. package/dist/container/Container.d.ts.map +1 -1
  16. package/dist/container/Container.js +174 -45
  17. package/dist/container/Container.js.map +1 -1
  18. package/dist/container/types.d.ts +7 -0
  19. package/dist/container/types.d.ts.map +1 -1
  20. package/dist/dev/hmr.d.ts +37 -0
  21. package/dist/dev/hmr.d.ts.map +1 -0
  22. package/dist/dev/hmr.js +72 -0
  23. package/dist/dev/hmr.js.map +1 -0
  24. package/dist/graphql/GraphQLProvider.d.ts +14 -3
  25. package/dist/graphql/GraphQLProvider.d.ts.map +1 -1
  26. package/dist/graphql/GraphQLProvider.js +14 -3
  27. package/dist/graphql/GraphQLProvider.js.map +1 -1
  28. package/dist/hot.d.ts +29 -0
  29. package/dist/hot.d.ts.map +1 -0
  30. package/dist/hot.js +135 -0
  31. package/dist/hot.js.map +1 -0
  32. package/dist/http/Exception.d.ts +2 -2
  33. package/dist/http/Exception.d.ts.map +1 -1
  34. package/dist/http/Exception.js +3 -3
  35. package/dist/http/Exception.js.map +1 -1
  36. package/dist/http/Request.d.ts +11 -0
  37. package/dist/http/Request.d.ts.map +1 -1
  38. package/dist/http/Request.js +40 -0
  39. package/dist/http/Request.js.map +1 -1
  40. package/dist/http/RequestValidator.d.ts +80 -0
  41. package/dist/http/RequestValidator.d.ts.map +1 -0
  42. package/dist/http/RequestValidator.js +93 -0
  43. package/dist/http/RequestValidator.js.map +1 -0
  44. package/dist/index.d.ts +3 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/middleware/Pipeline.d.ts +2 -2
  49. package/dist/middleware/Pipeline.js +2 -2
  50. package/dist/middleware/Pipeline.js.map +1 -1
  51. package/dist/migrations/MigrationRegistry.d.ts +16 -0
  52. package/dist/migrations/MigrationRegistry.d.ts.map +1 -1
  53. package/dist/migrations/MigrationRegistry.js +24 -0
  54. package/dist/migrations/MigrationRegistry.js.map +1 -1
  55. package/dist/openapi/OpenApiGenerator.d.ts +16 -0
  56. package/dist/openapi/OpenApiGenerator.d.ts.map +1 -1
  57. package/dist/openapi/OpenApiGenerator.js +76 -4
  58. package/dist/openapi/OpenApiGenerator.js.map +1 -1
  59. package/dist/openapi/OpenApiProvider.d.ts +16 -5
  60. package/dist/openapi/OpenApiProvider.d.ts.map +1 -1
  61. package/dist/openapi/OpenApiProvider.js +21 -6
  62. package/dist/openapi/OpenApiProvider.js.map +1 -1
  63. package/dist/rpc/RpcProvider.d.ts +13 -2
  64. package/dist/rpc/RpcProvider.d.ts.map +1 -1
  65. package/dist/rpc/RpcProvider.js +13 -2
  66. package/dist/rpc/RpcProvider.js.map +1 -1
  67. package/dist/storage/StaticProvider.d.ts +12 -2
  68. package/dist/storage/StaticProvider.d.ts.map +1 -1
  69. package/dist/storage/StaticProvider.js +12 -2
  70. package/dist/storage/StaticProvider.js.map +1 -1
  71. package/events.win32-x64-msvc.node +0 -0
  72. package/index.win32-x64-msvc.node +0 -0
  73. package/package.json +14 -2
  74. package/scheduler.win32-x64-msvc.node +0 -0
  75. package/src/Application.ts +89 -8
  76. package/src/HttpKernel.ts +87 -5
  77. package/src/Ignitor.ts +82 -19
  78. package/src/commands/ScheduleList.ts +30 -0
  79. package/src/container/Container.ts +178 -48
  80. package/src/container/types.ts +11 -0
  81. package/src/dev/hmr.ts +83 -0
  82. package/src/graphql/GraphQLProvider.ts +14 -3
  83. package/src/hot.ts +165 -0
  84. package/src/http/Exception.ts +4 -4
  85. package/src/http/Request.ts +54 -0
  86. package/src/http/RequestValidator.ts +143 -0
  87. package/src/index.ts +8 -1
  88. package/src/middleware/Pipeline.ts +4 -4
  89. package/src/migrations/MigrationRegistry.ts +23 -0
  90. package/src/openapi/OpenApiGenerator.ts +84 -4
  91. package/src/openapi/OpenApiProvider.ts +21 -6
  92. package/src/rpc/RpcProvider.ts +13 -2
  93. package/src/storage/StaticProvider.ts +12 -2
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * `OpenApiProvider` — serves an auto-generated OpenAPI 3.1 spec + Swagger UI.
3
3
  *
4
- * `boot()` mounts a global middleware that, on the first GET to `/api-docs`
4
+ * `start()` mounts a global middleware that, on the first GET to `/api-docs`
5
5
  * (JSON) or `/docs` (Swagger UI), lazily generates the spec from the router's
6
- * registered routes — so every route registered during boot/start is included
7
- * (the spec must NOT be generated eagerly at boot, before other providers have
8
- * registered their routes). Opt-out via `config.openapi.enabled = false`.
6
+ * registered routes — so every route registered while the application starts
7
+ * up is included (the spec must NOT be generated eagerly, before the other
8
+ * providers and the preloads have registered their routes). Opt-out via
9
+ * `config.openapi.enabled = false`.
9
10
  *
10
11
  * @implements Story 24.4
11
12
  */
@@ -31,7 +32,17 @@ export interface OpenApiProviderOptions {
31
32
  export declare class OpenApiProvider extends Provider {
32
33
  #private;
33
34
  constructor(app: AppContext, options?: OpenApiProviderOptions);
34
- boot(): Promise<void>;
35
+ /**
36
+ * Routes and server middleware go in `start`, not `boot`.
37
+ *
38
+ * Upstream documents that phase for exactly this, and the order is what makes
39
+ * it matter: providers boot, then providers START, then the preloads run —
40
+ * and the preloads are where an application writes its own routes and kernel.
41
+ * Mounted in `boot`, a framework route landed ahead of every application
42
+ * route, so an overlapping path was answered by the framework rather than by
43
+ * the app that meant to override it.
44
+ */
45
+ start(): Promise<void>;
35
46
  }
36
47
  export default OpenApiProvider;
37
48
  //# sourceMappingURL=OpenApiProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"OpenApiProvider.d.ts","sourceRoot":"","sources":["../../src/openapi/OpenApiProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAGxD,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,gBAAgB,CAAA;CAC7B;AAED,qBAAa,eAAgB,SAAQ,QAAQ;;gBAG/B,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,sBAA2B;IAKlD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAqBrC;AAGD,eAAe,eAAe,CAAA"}
1
+ {"version":3,"file":"OpenApiProvider.d.ts","sourceRoot":"","sources":["../../src/openapi/OpenApiProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAGxD,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,gBAAgB,CAAA;CAC7B;AAED,qBAAa,eAAgB,SAAQ,QAAQ;;gBAG/B,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,sBAA2B;IAKjE;;;;;;;;;OASG;IACY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAyBtC;AAGD,eAAe,eAAe,CAAA"}
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * `OpenApiProvider` — serves an auto-generated OpenAPI 3.1 spec + Swagger UI.
3
3
  *
4
- * `boot()` mounts a global middleware that, on the first GET to `/api-docs`
4
+ * `start()` mounts a global middleware that, on the first GET to `/api-docs`
5
5
  * (JSON) or `/docs` (Swagger UI), lazily generates the spec from the router's
6
- * registered routes — so every route registered during boot/start is included
7
- * (the spec must NOT be generated eagerly at boot, before other providers have
8
- * registered their routes). Opt-out via `config.openapi.enabled = false`.
6
+ * registered routes — so every route registered while the application starts
7
+ * up is included (the spec must NOT be generated eagerly, before the other
8
+ * providers and the preloads have registered their routes). Opt-out via
9
+ * `config.openapi.enabled = false`.
9
10
  *
10
11
  * @implements Story 24.4
11
12
  */
@@ -18,7 +19,17 @@ export class OpenApiProvider extends Provider {
18
19
  super(app);
19
20
  this.#override = options.generator;
20
21
  }
21
- async boot() {
22
+ /**
23
+ * Routes and server middleware go in `start`, not `boot`.
24
+ *
25
+ * Upstream documents that phase for exactly this, and the order is what makes
26
+ * it matter: providers boot, then providers START, then the preloads run —
27
+ * and the preloads are where an application writes its own routes and kernel.
28
+ * Mounted in `boot`, a framework route landed ahead of every application
29
+ * route, so an overlapping path was answered by the framework rather than by
30
+ * the app that meant to override it.
31
+ */
32
+ async start() {
22
33
  const config = this.app.config.get('openapi') ?? {};
23
34
  if (config.enabled === false)
24
35
  return;
@@ -33,10 +44,14 @@ export class OpenApiProvider extends Provider {
33
44
  }
34
45
  async #buildGenerator(config) {
35
46
  const router = await this.app.container.make('router');
36
- return new OpenApiGenerator(router, {
47
+ const generator = new OpenApiGenerator(router, {
37
48
  title: config.title ?? 'API',
38
49
  version: config.version ?? '1.0.0',
39
50
  });
51
+ // The routes already name their validators; ask each one to describe
52
+ // itself instead of making the app repeat the shape by hand.
53
+ await generator.hydrateSchemas((token) => this.app.container.make(token));
54
+ return generator;
40
55
  }
41
56
  }
42
57
  // Default export so reamrc's provider loader can `() => import('@c9up/ream/<feature>/provider')` (resolves to { default }), matching events/rpc. Named export above stays.
@@ -1 +1 @@
1
- {"version":3,"file":"OpenApiProvider.js","sourceRoot":"","sources":["../../src/openapi/OpenApiProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAoB1D,MAAM,OAAO,eAAgB,SAAQ,QAAQ;IAClC,SAAS,CAAmB;IAErC,YAAY,GAAe,EAAE,UAAkC,EAAE;QAC/D,KAAK,CAAC,GAAG,CAAC,CAAA;QACV,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAA;IACpC,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAoB,SAAS,CAAC,IAAI,EAAE,CAAA;QACtE,IAAI,MAAM,CAAC,OAAO,KAAK,KAAK;YAAE,OAAM;QAEpC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAA;QACxE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,MAAM,UAAU,GAAG,IAAI,iBAAiB,CAAC;YACvC,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE;SACjC,CAAC,CAAA;QACF,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3D,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,MAAyB;QAC7C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,OAAO,IAAI,gBAAgB,CAAC,MAAM,EAAE;YAClC,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,KAAK;YAC5B,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,OAAO;SACnC,CAAC,CAAA;IACJ,CAAC;CACF;AAED,2KAA2K;AAC3K,eAAe,eAAe,CAAA"}
1
+ {"version":3,"file":"OpenApiProvider.js","sourceRoot":"","sources":["../../src/openapi/OpenApiProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAoB1D,MAAM,OAAO,eAAgB,SAAQ,QAAQ;IAClC,SAAS,CAAmB;IAErC,YAAY,GAAe,EAAE,UAAkC,EAAE;QAC/D,KAAK,CAAC,GAAG,CAAC,CAAA;QACV,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAA;IACpC,CAAC;IAED;;;;;;;;;OASG;IACM,KAAK,CAAC,KAAK;QAClB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAoB,SAAS,CAAC,IAAI,EAAE,CAAA;QACtE,IAAI,MAAM,CAAC,OAAO,KAAK,KAAK;YAAE,OAAM;QAEpC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAA;QACxE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,MAAM,UAAU,GAAG,IAAI,iBAAiB,CAAC;YACvC,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE;SACjC,CAAC,CAAA;QACF,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3D,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,MAAyB;QAC7C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,MAAM,SAAS,GAAG,IAAI,gBAAgB,CAAC,MAAM,EAAE;YAC7C,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,KAAK;YAC5B,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,OAAO;SACnC,CAAC,CAAA;QACF,qEAAqE;QACrE,6DAA6D;QAC7D,MAAM,SAAS,CAAC,cAAc,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAU,KAAK,CAAC,CAAC,CAAA;QAClF,OAAO,SAAS,CAAA;IAClB,CAAC;CACF;AAED,2KAA2K;AAC3K,eAAe,eAAe,CAAA"}
@@ -9,8 +9,9 @@
9
9
  * rpc.method('task.validate', handler).guard('jwt')
10
10
  * rpc.namespace('user', UserController)
11
11
  *
12
- * - `boot()` — mount `POST <config.rpc.path ?? '/rpc'>` on the core router,
12
+ * - `start()` — mount `POST <config.rpc.path ?? '/rpc'>` on the core router,
13
13
  * dispatching every request body (single or batch) to `RpcRouter.handle()`.
14
+ * In `start`, which is the phase upstream documents for routes.
14
15
  *
15
16
  * @implements Story 24.1, 24.2
16
17
  */
@@ -38,7 +39,17 @@ export declare class RpcProvider extends Provider {
38
39
  readonly rpc: RpcRouter;
39
40
  constructor(app: AppContext, options?: RpcProviderOptions);
40
41
  register(): void;
41
- boot(): Promise<void>;
42
+ /**
43
+ * Routes and server middleware go in `start`, not `boot`.
44
+ *
45
+ * Upstream documents that phase for exactly this, and the order is what makes
46
+ * it matter: providers boot, then providers START, then the preloads run —
47
+ * and the preloads are where an application writes its own routes and kernel.
48
+ * Mounted in `boot`, a framework route landed ahead of every application
49
+ * route, so an overlapping path was answered by the framework rather than by
50
+ * the app that meant to override it.
51
+ */
52
+ start(): Promise<void>;
42
53
  }
43
54
  export default RpcProvider;
44
55
  //# sourceMappingURL=RpcProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RpcProvider.d.ts","sourceRoot":"","sources":["../../src/rpc/RpcProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,qBAAa,WAAY,SAAQ,QAAQ;;IACvC,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAA;gBAGX,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,kBAAuB;IAKpD,QAAQ,IAAI,IAAI;IAmBV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAarC;AAMD,eAAe,WAAW,CAAA"}
1
+ {"version":3,"file":"RpcProvider.d.ts","sourceRoot":"","sources":["../../src/rpc/RpcProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,qBAAa,WAAY,SAAQ,QAAQ;;IACvC,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAA;gBAGX,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,kBAAuB;IAKpD,QAAQ,IAAI,IAAI;IAmBzB;;;;;;;;;OASG;IACY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAatC;AAMD,eAAe,WAAW,CAAA"}
@@ -9,8 +9,9 @@
9
9
  * rpc.method('task.validate', handler).guard('jwt')
10
10
  * rpc.namespace('user', UserController)
11
11
  *
12
- * - `boot()` — mount `POST <config.rpc.path ?? '/rpc'>` on the core router,
12
+ * - `start()` — mount `POST <config.rpc.path ?? '/rpc'>` on the core router,
13
13
  * dispatching every request body (single or batch) to `RpcRouter.handle()`.
14
+ * In `start`, which is the phase upstream documents for routes.
14
15
  *
15
16
  * @implements Story 24.1, 24.2
16
17
  */
@@ -39,7 +40,17 @@ export class RpcProvider extends Provider {
39
40
  container.singleton('rpc', () => this.rpc);
40
41
  this.#registered = true;
41
42
  }
42
- async boot() {
43
+ /**
44
+ * Routes and server middleware go in `start`, not `boot`.
45
+ *
46
+ * Upstream documents that phase for exactly this, and the order is what makes
47
+ * it matter: providers boot, then providers START, then the preloads run —
48
+ * and the preloads are where an application writes its own routes and kernel.
49
+ * Mounted in `boot`, a framework route landed ahead of every application
50
+ * route, so an overlapping path was answered by the framework rather than by
51
+ * the app that meant to override it.
52
+ */
53
+ async start() {
43
54
  const config = this.app.config.get('rpc');
44
55
  const path = config?.path ?? '/rpc';
45
56
  const router = await this.app.container.make('router');
@@ -1 +1 @@
1
- {"version":3,"file":"RpcProvider.js","sourceRoot":"","sources":["../../src/rpc/RpcProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAGlD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAoB1C,MAAM,OAAO,WAAY,SAAQ,QAAQ;IAC9B,GAAG,CAAW;IACvB,WAAW,GAAG,KAAK,CAAA;IAEnB,YAAY,GAAe,EAAE,UAA8B,EAAE;QAC3D,KAAK,CAAC,GAAG,CAAC,CAAA;QACV,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,SAAS,EAAE,CAAA;IAC9C,CAAC;IAEQ,QAAQ;QACf,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAA;QACpC,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,0DAA0D;YAC1D,8DAA8D;YAC9D,IAAI,IAAI,CAAC,WAAW;gBAAE,OAAM;YAC5B,MAAM,IAAI,SAAS,CACjB,mCAAmC,EACnC,gEAAgE,EAChE;gBACE,IAAI,EAAE,kGAAkG;aACzG,CACF,CAAA;QACH,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC,CAAA;QAChC,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAC1C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;IACzB,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAY,KAAK,CAAC,CAAA;QACpD,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,IAAI,MAAM,CAAA;QACnC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,0EAA0E;QAC1E,2DAA2D;QAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAgB,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QAC3E,sEAAsE;QACtE,wEAAwE;QACxE,gCAAgC;QAChC,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,IAAI,EAAE,CAAA;QACnC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,CAAA;IAC/C,CAAC;CACF;AAED,uEAAuE;AACvE,+EAA+E;AAC/E,gFAAgF;AAChF,4BAA4B;AAC5B,eAAe,WAAW,CAAA"}
1
+ {"version":3,"file":"RpcProvider.js","sourceRoot":"","sources":["../../src/rpc/RpcProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAGlD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAoB1C,MAAM,OAAO,WAAY,SAAQ,QAAQ;IAC9B,GAAG,CAAW;IACvB,WAAW,GAAG,KAAK,CAAA;IAEnB,YAAY,GAAe,EAAE,UAA8B,EAAE;QAC3D,KAAK,CAAC,GAAG,CAAC,CAAA;QACV,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,SAAS,EAAE,CAAA;IAC9C,CAAC;IAEQ,QAAQ;QACf,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAA;QACpC,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,0DAA0D;YAC1D,8DAA8D;YAC9D,IAAI,IAAI,CAAC,WAAW;gBAAE,OAAM;YAC5B,MAAM,IAAI,SAAS,CACjB,mCAAmC,EACnC,gEAAgE,EAChE;gBACE,IAAI,EAAE,kGAAkG;aACzG,CACF,CAAA;QACH,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC,CAAA;QAChC,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAC1C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;IACzB,CAAC;IAED;;;;;;;;;OASG;IACM,KAAK,CAAC,KAAK;QAClB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAY,KAAK,CAAC,CAAA;QACpD,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,IAAI,MAAM,CAAA;QACnC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,0EAA0E;QAC1E,2DAA2D;QAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAgB,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QAC3E,sEAAsE;QACtE,wEAAwE;QACxE,gCAAgC;QAChC,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,IAAI,EAAE,CAAA;QACnC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,CAAA;IAC/C,CAAC;CACF;AAED,uEAAuE;AACvE,+EAA+E;AAC/E,gFAAgF;AAChF,4BAA4B;AAC5B,eAAe,WAAW,CAAA"}
@@ -2,7 +2,7 @@
2
2
  * `StaticProvider` — serves static files from a directory.
3
3
  *
4
4
  * Opt-in: does nothing unless `config.static.root` is set (or a middleware is
5
- * injected for tests). When configured, `boot()` mounts a global
5
+ * injected for tests). When configured, `start()` mounts a global
6
6
  * `StaticMiddleware` (path-traversal + symlink-escape guarded) that serves
7
7
  * matching requests and falls through (`next()`) otherwise.
8
8
  *
@@ -21,7 +21,17 @@ export interface StaticProviderOptions {
21
21
  export declare class StaticProvider extends Provider {
22
22
  #private;
23
23
  constructor(app: AppContext, options?: StaticProviderOptions);
24
- boot(): Promise<void>;
24
+ /**
25
+ * Routes and server middleware go in `start`, not `boot`.
26
+ *
27
+ * Upstream documents that phase for exactly this, and the order is what makes
28
+ * it matter: providers boot, then providers START, then the preloads run —
29
+ * and the preloads are where an application writes its own routes and kernel.
30
+ * Mounted in `boot`, a framework route landed ahead of every application
31
+ * route, so an overlapping path was answered by the framework rather than by
32
+ * the app that meant to override it.
33
+ */
34
+ start(): Promise<void>;
25
35
  }
26
36
  export default StaticProvider;
27
37
  //# sourceMappingURL=StaticProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"StaticProvider.d.ts","sourceRoot":"","sources":["../../src/storage/StaticProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAqB,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAE3E,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,UAAU,CAAC,EAAE,gBAAgB,CAAA;CAC9B;AAED,qBAAa,cAAe,SAAQ,QAAQ;;gBAG9B,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,qBAA0B;IAKjD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAOrC;AAGD,eAAe,cAAc,CAAA"}
1
+ {"version":3,"file":"StaticProvider.d.ts","sourceRoot":"","sources":["../../src/storage/StaticProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAqB,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAE3E,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,UAAU,CAAC,EAAE,gBAAgB,CAAA;CAC9B;AAED,qBAAa,cAAe,SAAQ,QAAQ;;gBAG9B,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,qBAA0B;IAKhE;;;;;;;;;OASG;IACY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAOtC;AAGD,eAAe,cAAc,CAAA"}
@@ -2,7 +2,7 @@
2
2
  * `StaticProvider` — serves static files from a directory.
3
3
  *
4
4
  * Opt-in: does nothing unless `config.static.root` is set (or a middleware is
5
- * injected for tests). When configured, `boot()` mounts a global
5
+ * injected for tests). When configured, `start()` mounts a global
6
6
  * `StaticMiddleware` (path-traversal + symlink-escape guarded) that serves
7
7
  * matching requests and falls through (`next()`) otherwise.
8
8
  *
@@ -19,7 +19,17 @@ export class StaticProvider extends Provider {
19
19
  super(app);
20
20
  this.#override = options.middleware;
21
21
  }
22
- async boot() {
22
+ /**
23
+ * Routes and server middleware go in `start`, not `boot`.
24
+ *
25
+ * Upstream documents that phase for exactly this, and the order is what makes
26
+ * it matter: providers boot, then providers START, then the preloads run —
27
+ * and the preloads are where an application writes its own routes and kernel.
28
+ * Mounted in `boot`, a framework route landed ahead of every application
29
+ * route, so an overlapping path was answered by the framework rather than by
30
+ * the app that meant to override it.
31
+ */
32
+ async start() {
23
33
  const config = this.app.config.get('static');
24
34
  const middleware = this.#override ?? (config?.root ? new StaticMiddleware(config) : undefined);
25
35
  if (!middleware)
@@ -1 +1 @@
1
- {"version":3,"file":"StaticProvider.js","sourceRoot":"","sources":["../../src/storage/StaticProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAqB,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAO3E,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACjC,SAAS,CAAmB;IAErC,YAAY,GAAe,EAAE,UAAiC,EAAE;QAC9D,KAAK,CAAC,GAAG,CAAC,CAAA;QACV,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,UAAU,CAAA;IACrC,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAe,QAAQ,CAAC,CAAA;QAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAC9F,IAAI,CAAC,UAAU;YAAE,OAAM,CAAC,2BAA2B;QACnD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3D,CAAC;CACF;AAED,yKAAyK;AACzK,eAAe,cAAc,CAAA"}
1
+ {"version":3,"file":"StaticProvider.js","sourceRoot":"","sources":["../../src/storage/StaticProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAqB,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAO3E,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACjC,SAAS,CAAmB;IAErC,YAAY,GAAe,EAAE,UAAiC,EAAE;QAC9D,KAAK,CAAC,GAAG,CAAC,CAAA;QACV,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,UAAU,CAAA;IACrC,CAAC;IAED;;;;;;;;;OASG;IACM,KAAK,CAAC,KAAK;QAClB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAe,QAAQ,CAAC,CAAA;QAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAC9F,IAAI,CAAC,UAAU;YAAE,OAAM,CAAC,2BAA2B;QACnD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3D,CAAC;CACF;AAED,yKAAyK;AACzK,eAAe,cAAc,CAAA"}
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/ream",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "description": "Ream — Rust-powered Node.js application framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -202,6 +202,14 @@
202
202
  "./bodyparser_middleware": {
203
203
  "types": "./dist/bodyparser/BodyParserMiddleware.d.ts",
204
204
  "import": "./dist/bodyparser/BodyParserMiddleware.js"
205
+ },
206
+ "./hot": {
207
+ "types": "./dist/hot.d.ts",
208
+ "import": "./dist/hot.js"
209
+ },
210
+ "./dev/hmr": {
211
+ "types": "./dist/dev/hmr.d.ts",
212
+ "import": "./dist/dev/hmr.js"
205
213
  }
206
214
  },
207
215
  "files": [
@@ -225,7 +233,8 @@
225
233
  },
226
234
  "peerDependencies": {
227
235
  "@c9up/comet": "^0.1.0",
228
- "@c9up/quasar": "^0.1.0"
236
+ "@c9up/quasar": "^0.1.0",
237
+ "hot-hook": "^1.0.0"
229
238
  },
230
239
  "peerDependenciesMeta": {
231
240
  "@c9up/comet": {
@@ -233,6 +242,9 @@
233
242
  },
234
243
  "@c9up/quasar": {
235
244
  "optional": true
245
+ },
246
+ "hot-hook": {
247
+ "optional": true
236
248
  }
237
249
  },
238
250
  "devDependencies": {
Binary file
@@ -56,11 +56,26 @@ export type ApplicationState =
56
56
  | 'terminating'
57
57
  | 'terminated'
58
58
 
59
+ /**
60
+ * What the application keeps of the rc file.
61
+ *
62
+ * Structurally a subset of `ReamrcConfig`, declared here rather than imported:
63
+ * that type lives in `Ignitor.ts`, which imports this module, and naming it
64
+ * from here would close the cycle. Only the keys something reads back through
65
+ * `app.rcFile` are spelt out; the rest rides along untouched.
66
+ */
67
+ export interface ReamrcContents {
68
+ directories?: Partial<DirectoriesNode>
69
+ /** `app/modules/**` auto-loading — opt-in, and scoped to named files. */
70
+ modules?: { path?: string; autoload?: string[] }
71
+ }
72
+
59
73
  export class Application implements AppContext {
60
74
  readonly container: Container
61
75
  readonly config: ConfigStore
62
76
  #appRoot?: URL
63
77
  #directories: DirectoriesNode = { ...defaultDirectories }
78
+ #rcFile: ReamrcContents = {}
64
79
  #providers: ProviderContract[] = []
65
80
  #booted = false
66
81
  #environment: AppEnvironment = 'unknown'
@@ -68,6 +83,10 @@ export class Application implements AppContext {
68
83
  #packageJsonRead = false
69
84
  #packageJsonCache: { name?: string; version?: string } | undefined
70
85
  #mode: ApplicationMode = 'run'
86
+ /** Hooks that run — in reverse — when `terminate()` begins. */
87
+ #terminatingHooks: Array<() => Promise<void> | void> = []
88
+ /** Re-entrancy guard: two signals must terminate the application once. */
89
+ #terminating = false
71
90
  #bootingHooks: Array<() => Promise<void> | void> = []
72
91
  #bootedHooks: Array<() => Promise<void> | void> = []
73
92
  #shutdownHooks: Array<() => Promise<void> | void> = []
@@ -161,14 +180,27 @@ export class Application implements AppContext {
161
180
  return this.#directories
162
181
  }
163
182
 
183
+ /**
184
+ * The parsed rc file (AdonisJS `app.rcFile`).
185
+ *
186
+ * Kept whole, not just the keys this class acts on. A console command has the
187
+ * application and nothing else, so anything it needs to explain itself — why
188
+ * `app/modules/**` loaded nothing, say — has to be reachable from here. Empty
189
+ * until `rcContents()` has run.
190
+ */
191
+ get rcFile(): ReamrcContents {
192
+ return this.#rcFile
193
+ }
194
+
164
195
  /**
165
196
  * Feed the loaded rc file to the application (AdonisJS `rcContents`).
166
197
  *
167
- * Today it reads one key, `directories`, merged over the defaults; the rest
168
- * of the rc file is consumed by the Ignitor and the CLI, which own providers,
169
- * preloads, commands and test suites.
198
+ * `directories` is merged over the defaults; the rest is stored as-is and
199
+ * read back through {@link rcFile}. Providers, preloads, commands and test
200
+ * suites are still consumed by the Ignitor and the CLI, which own them.
170
201
  */
171
- rcContents(value: { directories?: Partial<DirectoriesNode> }): this {
202
+ rcContents(value: ReamrcContents): this {
203
+ this.#rcFile = value
172
204
  if (value.directories) {
173
205
  // Skip the undefined entries a `Partial` may carry: spreading them would
174
206
  // blank a default rather than leave it alone.
@@ -576,12 +608,56 @@ export class Application implements AppContext {
576
608
  }
577
609
 
578
610
  /**
579
- * Graceful shutdown — stop all providers and exit.
580
- * Like AdonisJS app.terminate().
611
+ * Register a hook that runs when the application begins to terminate
612
+ * (AdonisJS `app.terminating`).
613
+ *
614
+ * This is how the HTTP server closes on a signal: whoever opened the socket
615
+ * registers a hook that closes it, and `terminate()` runs them in reverse —
616
+ * last opened, first closed. Upstream's HTTP process does exactly this.
617
+ */
618
+ terminating(callback: () => Promise<void> | void): void {
619
+ this.#terminatingHooks.push(callback)
620
+ }
621
+
622
+ /**
623
+ * Graceful shutdown (AdonisJS `app.terminate()`).
624
+ *
625
+ * Runs the `terminating` hooks in reverse, then shuts the providers down.
626
+ * Two things it deliberately does NOT do:
627
+ *
628
+ * - It does not close the HTTP server itself. Whoever opened the socket
629
+ * registers a `terminating` hook for it; the application does not know
630
+ * what a server is. Terminating without one used to skip the close, the
631
+ * error boundary and the service locators entirely.
632
+ * - It does not call `process.exit()`. Exiting from here cut the shutdown
633
+ * short whenever a second path was already draining the server — and the
634
+ * documented signal wiring set up exactly that race. The process ends when
635
+ * nothing is left holding the loop, which is what "graceful" means.
636
+ *
637
+ * Re-entrant calls are ignored, so a SIGTERM handler and a crash handler
638
+ * firing together terminate once.
581
639
  */
582
640
  async terminate(): Promise<void> {
583
- await this.shutdown()
584
- process.exit(0)
641
+ if (this.#terminating) return
642
+ this.#terminating = true
643
+ const errors: unknown[] = []
644
+ for (const hook of [...this.#terminatingHooks].reverse()) {
645
+ try {
646
+ await hook()
647
+ } catch (err) {
648
+ errors.push(err)
649
+ }
650
+ }
651
+ this.#terminatingHooks = []
652
+ try {
653
+ await this.shutdown()
654
+ } catch (err) {
655
+ errors.push(err)
656
+ }
657
+ if (errors.length === 1) throw errors[0]
658
+ if (errors.length > 1) {
659
+ throw new AggregateError(errors, 'Application.terminate() completed with errors')
660
+ }
585
661
  }
586
662
 
587
663
  // ─── Provider lifecycle ───────────────────────────────────
@@ -640,6 +716,11 @@ export class Application implements AppContext {
640
716
  * from closing their resources (DB pools, queues). Errors are aggregated.
641
717
  */
642
718
  async shutdown(): Promise<void> {
719
+ // Idempotent: `terminate()` runs the `terminating` hooks and one of them is
720
+ // the Ignitor's `stop()`, which shuts the application down on its way. The
721
+ // second pass must not call every provider's `shutdown()` again — a pool
722
+ // closed twice, a socket destroyed twice.
723
+ if (this.#state === 'terminated') return
643
724
  const errors: unknown[] = []
644
725
  this.#state = 'terminating'
645
726
 
package/src/HttpKernel.ts CHANGED
@@ -32,6 +32,16 @@ export interface HttpKernelConfig {
32
32
  container?: Container
33
33
  exceptionHandler?: ExceptionHandler
34
34
  serverMiddleware?: MiddlewareFunction[]
35
+ /**
36
+ * A `<script>` appended to every HTML response, dev only.
37
+ *
38
+ * Set by the Ignitor when the application runs in development; `undefined`
39
+ * everywhere else, so a production response is byte-for-byte what the
40
+ * handler produced. It lives here rather than in a middleware because this
41
+ * is the one place every response passes through on its way out, whichever
42
+ * route or error handler produced it.
43
+ */
44
+ devReloadScript?: (nonce?: string) => string
35
45
  routerMiddleware?: MiddlewareFunction[]
36
46
  onError?: (error: unknown, ctx: HttpContext) => void
37
47
  debug?: boolean
@@ -345,7 +355,7 @@ export function createHttpKernel(
345
355
  correlationId,
346
356
  )
347
357
  }
348
- const serialized = await serializeResponse(ctx)
358
+ const serialized = await serializeResponse(ctx, config.devReloadScript)
349
359
  // The body is built; anything registered with `response.onFinish()` runs
350
360
  // now — after the answer is ready, before we hand it back.
351
361
  ctx.response.runFinishCallbacks()
@@ -401,7 +411,7 @@ export function createHttpKernel(
401
411
  correlationId,
402
412
  )
403
413
  }
404
- const serialized = await serializeResponse(ctx)
414
+ const serialized = await serializeResponse(ctx, config.devReloadScript)
405
415
  // The body is built; anything registered with `response.onFinish()` runs
406
416
  // now — after the answer is ready, before we hand it back.
407
417
  ctx.response.runFinishCallbacks()
@@ -452,15 +462,87 @@ function readControllerGuardMetadata(
452
462
  }
453
463
  }
454
464
 
455
- async function serializeResponse(ctx: HttpContext): Promise<HttpKernelResponse> {
465
+ async function serializeResponse(
466
+ ctx: HttpContext,
467
+ devReloadScript?: (nonce?: string) => string,
468
+ ): Promise<HttpKernelResponse> {
456
469
  // The terminal step, as AdonisJS' server calls `response.finish()`: it
457
470
  // stamps the request id and waits for a body still being produced.
458
471
  await ctx.response.finish()
459
472
  const streamId = ctx.response.getStreamId()
473
+ const headers = ctx.response.getHeaders()
474
+ const body = withDevReloadScript(
475
+ ctx.response.getBody(),
476
+ headers,
477
+ streamId,
478
+ devReloadScript === undefined ? undefined : devReloadScript(ctx.response.nonce),
479
+ )
460
480
  return {
461
481
  status: ctx.response.getStatus(),
462
- headers: ctx.response.getHeaders(),
463
- body: ctx.response.getBody(),
482
+ headers: reconcileRepresentation(headers, ctx.response.getBody(), body),
483
+ body,
464
484
  ...(streamId !== undefined ? { streamId } : {}),
465
485
  }
466
486
  }
487
+
488
+ /**
489
+ * Keep the representation headers describing the body that is actually sent.
490
+ *
491
+ * `content-length`, `etag` and the digest headers are computed from the body
492
+ * BEFORE the dev reload script is appended. Left alone they describe the
493
+ * previous one: a `content-length` short by the length of the script truncates
494
+ * it, and an `etag` computed before the change caches modified content under an
495
+ * unchanged validator.
496
+ *
497
+ * `content-length` is recomputed because it is cheap and exact; the validators
498
+ * are DROPPED rather than recomputed, because a validator this layer invents is
499
+ * not the one the handler meant.
500
+ */
501
+ function reconcileRepresentation(
502
+ headers: Record<string, string>,
503
+ original: string,
504
+ sent: string,
505
+ ): Record<string, string> {
506
+ if (sent === original) return headers
507
+ const reconciled = { ...headers }
508
+ if (reconciled['content-length'] !== undefined) {
509
+ reconciled['content-length'] = String(Buffer.byteLength(sent, 'utf8'))
510
+ }
511
+ // Every validator, not the two that came to mind. `Content-Digest` and
512
+ // `Repr-Digest` (RFC 9530) are what replaced `Digest`, and leaving them
513
+ // describing the old body is the same defect one header along — a client
514
+ // that checks them rejects a response that is perfectly fine.
515
+ for (const name of [
516
+ 'etag',
517
+ 'content-md5',
518
+ 'digest',
519
+ 'content-digest',
520
+ 'repr-digest',
521
+ 'want-digest',
522
+ 'want-content-digest',
523
+ 'want-repr-digest',
524
+ ]) {
525
+ delete reconciled[name]
526
+ }
527
+ return reconciled
528
+ }
529
+
530
+ /**
531
+ * Append the dev reload script to an HTML body.
532
+ *
533
+ * Only HTML, and never a stream: appending to an SSE or a download would
534
+ * corrupt it, and a page is the only thing that can act on the script anyway.
535
+ * Before `</body>` when there is one, so the script is inside the document
536
+ * rather than trailing after it.
537
+ */
538
+ function withDevReloadScript(
539
+ body: string,
540
+ headers: Record<string, string>,
541
+ streamId: string | undefined,
542
+ script?: string,
543
+ ): string {
544
+ if (script === undefined || streamId !== undefined || body === '') return body
545
+ if (!(headers['content-type'] ?? '').includes('text/html')) return body
546
+ const closing = body.lastIndexOf('</body>')
547
+ return closing === -1 ? body + script : body.slice(0, closing) + script + body.slice(closing)
548
+ }