@nextrush/core 3.0.6 → 4.0.0-beta.0

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.
package/dist/index.d.ts CHANGED
@@ -1,12 +1,14 @@
1
- import { Middleware, Context, Plugin, Next } from '@nextrush/types';
2
- export { ContentType, Context, ContextState, HttpMethod, HttpStatus, HttpStatusCode, Middleware, Next, Plugin, PluginWithHooks, QueryParams, RouteHandler, RouteParams } from '@nextrush/types';
3
- export { BadRequestError, ForbiddenError, HttpError, InternalServerError, NextRushError, NotFoundError, UnauthorizedError, createError as createHttpError } from '@nextrush/errors';
1
+ import { Logger, Router, Container, Middleware, RouteEntry, Context, Extension, Next } from '@nextrush/types';
2
+ export { ContentType, Context, ContextState, Extension, ExtensionContext, ExtensionHost, HttpMethod, HttpStatus, HttpStatusCode, Logger, Middleware, Next, QueryParams, RouteEntry, RouteHandler, RouteParams, Router } from '@nextrush/types';
3
+ export { BadRequestError, ForbiddenError, HttpError, InternalServerError, NextRushError, NotFoundError, UnauthorizedError } from '@nextrush/errors';
4
4
 
5
5
  /**
6
6
  * @nextrush/core - Application Class
7
7
  *
8
8
  * The Application class is the main entry point for NextRush.
9
- * It manages middleware registration and plugin installation.
9
+ * It manages middleware registration and the extension lifecycle.
10
+ *
11
+ * See docs/RFC/class-runtime/005-plugin-system.md for the extension model.
10
12
  *
11
13
  * @packageDocumentation
12
14
  */
@@ -16,13 +18,21 @@ export { BadRequestError, ForbiddenError, HttpError, InternalServerError, NextRu
16
18
  */
17
19
  type ErrorHandler = (error: Error, ctx: Context) => void | Promise<void>;
18
20
  /**
19
- * Logger interface for pluggable logging
21
+ * Options for {@link Application.close}.
22
+ *
23
+ * @see RFC-022 (`docs/RFC/class-runtime/022-bounded-teardown-lifecycle.md`) / ADR-0012.
20
24
  */
21
- interface Logger {
22
- error(...args: unknown[]): void;
23
- warn(...args: unknown[]): void;
24
- info(...args: unknown[]): void;
25
- debug(...args: unknown[]): void;
25
+ interface CloseOptions {
26
+ /**
27
+ * Bound, in milliseconds, on total teardown time. Every teardown unit
28
+ * (extension `destroy()`) races against this budget independently — a
29
+ * unit that does not finish in time is reported as a {@link TeardownTimeoutError}
30
+ * in the returned array, and never blocks `close()` past the budget.
31
+ *
32
+ * @remarks Omitted = unbounded, identical to calling `close()` with no
33
+ * arguments today (`Promise.allSettled`, no timeout).
34
+ */
35
+ timeout?: number;
26
36
  }
27
37
  /**
28
38
  * Application options
@@ -42,26 +52,31 @@ interface ApplicationOptions {
42
52
  * Custom logger. Defaults to no-op (silent).
43
53
  *
44
54
  * @remarks
45
- * Pass `console` for quick development logging:
46
- * ```ts
47
- * const app = createApp({ logger: console });
48
- * ```
49
- *
50
- * For production, provide a structured logger (e.g. pino, winston)
51
- * that implements the {@link Logger} interface.
52
- *
53
- * Adapters and internal hooks use `app.logger` — configuring it here
54
- * ensures consistent logging across the framework.
55
+ * Pass `console` for quick development logging. For production, provide a
56
+ * structured logger (pino, winston, `@nextrush/logger`) implementing {@link Logger}.
55
57
  */
56
58
  logger?: Logger;
59
+ /**
60
+ * A router the app owns. Route methods (`app.get`, `app.post`, …) delegate to
61
+ * it, and it is mounted last at `ready()`. The `nextrush` meta-package's
62
+ * `createApp` injects one automatically; pass it explicitly when using
63
+ * `@nextrush/core` directly.
64
+ */
65
+ router?: Router;
66
+ /**
67
+ * A per-app DI container. Exposed to extensions via `ExtensionContext.container`
68
+ * and used by class-based registrars (e.g. `registerControllers`). Injected by
69
+ * `nextrush/class`; omitted for functional, DI-free apps.
70
+ */
71
+ container?: Container;
57
72
  }
58
73
  /**
59
74
  * Listener callback type
60
75
  */
61
76
  type ListenCallback = () => void;
62
77
  /**
63
- * Routable interface - any object with routes() method
64
- * This allows mounting Router instances without circular dependency
78
+ * Routable interface - any object with routes() method.
79
+ * Allows mounting Router instances without a circular dependency.
65
80
  */
66
81
  interface Routable {
67
82
  routes(): Middleware;
@@ -77,195 +92,173 @@ interface Routable {
77
92
  * ctx.json({ message: 'Hello World' });
78
93
  * });
79
94
  *
80
- * // Mount with an adapter
81
- * listen(app, { port: 3000 });
95
+ * // Extensions (rare long-lived services) are booted at ready(), which
96
+ * // adapters call automatically before start().
97
+ * app.extend(events());
98
+ *
99
+ * listen(app, { port: 8080 });
82
100
  * ```
83
101
  */
84
102
  declare class Application {
85
- /**
86
- * Middleware stack
87
- */
103
+ /** Middleware stack */
88
104
  private readonly middlewareStack;
105
+ /** Registered extensions, in registration order (setup runs at ready()) */
106
+ private readonly extensions;
107
+ /** Registered extension names — enforces uniqueness */
108
+ private readonly extensionNames;
89
109
  /**
90
- * Installed plugins
91
- */
92
- private readonly plugins;
93
- /**
94
- * Custom error handler
110
+ * Combined teardown-unit registration order: extension `destroy()`s and
111
+ * {@link onClose} hooks interleaved in the exact order they were registered,
112
+ * so `_shutdown()` can run the whole set in one reverse-of-registration pass
113
+ * (RFC-022 §7.3 — "teardown becomes a flat list of independently-isolated,
114
+ * budget-raced units"). Populated lazily by {@link extend} (for extensions
115
+ * declaring `destroy()`) and {@link onClose}.
95
116
  */
117
+ private readonly teardownUnits;
118
+ /** Whether {@link close} has started — {@link onClose} is a pre-shutdown-only registration point (RFC-022 §8.6). */
119
+ private _closeStarted;
120
+ /** Names decorated onto the app — enforces collision detection */
121
+ private readonly decorations;
122
+ /** Custom error handler */
96
123
  private _errorHandler;
97
- /**
98
- * Pluggable logger
99
- */
124
+ /** Pluggable logger */
100
125
  readonly logger: Logger;
101
- /**
102
- * Application options
103
- */
126
+ /** Application options */
104
127
  readonly options: ApplicationOptions;
105
- /**
106
- * Whether the app is running
107
- */
128
+ /** Whether the app is running (server listening) */
108
129
  private _isRunning;
130
+ /** Whether the app has been booted (extensions set up, config frozen) */
131
+ private _isReady;
132
+ /** Whether ready() mounted the app-owned router (so close() can undo it) */
133
+ private _routerMounted;
134
+ /** In-flight boot promise — memoized so concurrent ready() calls share one boot (H-1) */
135
+ private _readyPromise;
136
+ /** In-flight shutdown promise — memoized so concurrent close() calls share one (H-3) */
137
+ private _closePromise;
138
+ /** The app-owned router (optional). Route methods delegate to it. */
139
+ readonly router?: Router;
140
+ /** The app-owned DI container (optional). Exposed to extensions and registrars. */
141
+ readonly container?: Container;
109
142
  constructor(options?: ApplicationOptions);
110
- /**
111
- * Check if running in production
112
- */
143
+ /** Check if running in production */
113
144
  get isProduction(): boolean;
114
- /**
115
- * Check if app is running
116
- */
145
+ /** Check if app is running */
117
146
  get isRunning(): boolean;
118
147
  /**
119
- * Get middleware count
148
+ * Whether a graceful shutdown is currently in progress — `true` from the
149
+ * moment {@link close} begins tearing down until it completes (F-12:
150
+ * shutdown observability, RFC-022). A readiness check or operator tooling
151
+ * can read this to reflect an in-progress drain.
120
152
  */
153
+ get isDraining(): boolean;
154
+ /** Check if app has been booted via ready() */
155
+ get isReady(): boolean;
156
+ /** Get middleware count */
121
157
  get middlewareCount(): number;
158
+ /** Get registered extension count */
159
+ get extensionCount(): number;
122
160
  /**
123
- * Throws if the app is already running.
124
- * Prevents configuration mutations after start().
161
+ * Throws if the app configuration is frozen (after ready() or start()).
162
+ * Prevents unsafe mutations once the app has booted or is serving traffic.
125
163
  */
126
- private assertNotRunning;
164
+ private assertConfigurable;
127
165
  /**
128
166
  * Register middleware function(s)
129
167
  *
130
- * @param middleware - Middleware function or array of middleware
168
+ * @param middleware - Middleware function(s)
131
169
  * @returns this for chaining
132
- *
133
- * @example
134
- * ```typescript
135
- * // Single middleware
136
- * app.use(async (ctx) => {
137
- * console.log(ctx.method, ctx.path);
138
- * await ctx.next();
139
- * });
140
- *
141
- * // Multiple middleware
142
- * app.use(cors(), helmet(), json());
143
- * ```
144
170
  */
145
171
  use(...middleware: Middleware[]): this;
146
172
  /**
147
- * Mount a router at a path prefix
148
- *
149
- * This is the Hono-style API for mounting routers directly on the app.
150
- * The router's routes will only match requests that start with the given prefix.
173
+ * Mount a router at a path prefix.
151
174
  *
152
- * @param path - Path prefix for the router (e.g., '/api/users')
175
+ * @param path - Path prefix (e.g. '/api/users')
153
176
  * @param router - Router instance to mount
154
177
  * @returns this for chaining
155
- *
156
- * @example
157
- * ```typescript
158
- * // Create modular routers
159
- * const users = createRouter();
160
- * users.get('/', listUsers);
161
- * users.get('/:id', getUser);
162
- * users.post('/', createUser);
163
- *
164
- * const posts = createRouter();
165
- * posts.get('/', listPosts);
166
- * posts.get('/:id', getPost);
167
- *
168
- * // Mount directly on app - clean like Hono!
169
- * const app = createApp();
170
- * app.route('/api/users', users);
171
- * app.route('/api/posts', posts);
172
- *
173
- * listen(app, 3000);
174
- * ```
175
178
  */
176
179
  route(path: string, router: Routable): this;
180
+ private requireRouter;
181
+ /** Register a GET route on the app-owned router. */
182
+ get(path: string, ...entries: RouteEntry[]): this;
183
+ /** Register a POST route on the app-owned router. */
184
+ post(path: string, ...entries: RouteEntry[]): this;
185
+ /** Register a PUT route on the app-owned router. */
186
+ put(path: string, ...entries: RouteEntry[]): this;
187
+ /** Register a PATCH route on the app-owned router. */
188
+ patch(path: string, ...entries: RouteEntry[]): this;
189
+ /** Register a DELETE route on the app-owned router. */
190
+ delete(path: string, ...entries: RouteEntry[]): this;
191
+ /** Register a HEAD route on the app-owned router. */
192
+ head(path: string, ...entries: RouteEntry[]): this;
177
193
  /**
178
- * Set the application error handler.
194
+ * Register a route for all HTTP methods on the app-owned router.
179
195
  *
180
- * Replaces any previously set handler. Only one error handler is active
181
- * at a time. For additive error handling, compose logic within a single
182
- * handler or use the `errorHandler()` middleware from `@nextrush/errors`.
183
- *
184
- * @param handler - Error handler function
185
- * @returns this for chaining
186
- *
187
- * @example
188
- * ```typescript
189
- * app.setErrorHandler((error, ctx) => {
190
- * console.error('Request failed:', error);
191
- *
192
- * if (error instanceof ValidationError) {
193
- * ctx.status = 400;
194
- * ctx.json({ error: error.message, details: error.details });
195
- * return;
196
- * }
197
- *
198
- * ctx.status = 500;
199
- * ctx.json({ error: 'Internal Server Error' });
200
- * });
201
- * ```
196
+ * @remarks
197
+ * There is intentionally no `app.options()` verb method it would collide
198
+ * with the `app.options` configuration property. Register OPTIONS routes via
199
+ * `app.all()`, the router directly, or let CORS middleware handle preflight.
202
200
  */
203
- setErrorHandler(handler: ErrorHandler): this;
201
+ all(path: string, ...entries: RouteEntry[]): this;
204
202
  /**
205
- * Set custom error handler.
203
+ * Set the application error handler. Replaces any previously set handler.
206
204
  *
207
205
  * @param handler - Error handler function
208
206
  * @returns this for chaining
209
- *
210
- * @deprecated Use `setErrorHandler()` instead — `onError()` implies
211
- * event subscription (additive), but this is a setter (replaces).
212
207
  */
213
- onError(handler: ErrorHandler): this;
208
+ setErrorHandler(handler: ErrorHandler): this;
214
209
  /**
215
- * Install a plugin.
210
+ * Register an extension. Queues it — `setup()` runs later, at `ready()`,
211
+ * in registration order. Synchronous and chainable.
216
212
  *
217
- * Handles both sync and async `install()` methods automatically.
218
- * Returns `this` for sync plugins and `Promise<this>` for async ones.
219
- *
220
- * @param plugin - Plugin to install
221
- * @returns this (sync) or Promise<this> (async)
213
+ * @param extension - Extension to register. If it declares a decorated
214
+ * shape via `Extension<TDecorated>`, the return type carries that shape
215
+ * `app.extend(events<MyEvents>()).events` is statically known, no
216
+ * `declare module` augmentation required.
217
+ * @returns this, intersected with the extension's declared decorated shape
222
218
  *
223
219
  * @example
224
220
  * ```typescript
225
- * // Sync plugin
226
- * app.plugin(loggerPlugin({ level: 'info' }));
227
- *
228
- * // Async plugin — just await it
229
- * await app.plugin(databasePlugin({ uri: '...' }));
221
+ * const extended = app.extend(events<MyEvents>());
222
+ * await app.ready(); // adapters call this automatically — runs setup()/decorate()
223
+ * extended.events.emit('user:created', { id: '1' }); // available only AFTER ready()
230
224
  * ```
231
225
  */
232
- plugin(plugin: Plugin): this | Promise<this>;
226
+ extend<TDecorated = Record<string, never>>(extension: Extension<TDecorated>): this & TDecorated;
233
227
  /**
234
- * Install a plugin asynchronously.
228
+ * Boot the application: run every registered extension's `setup()` once, in
229
+ * registration order, awaiting async setups. Idempotent — safe to call twice.
230
+ * Adapters call this automatically before `start()`.
235
231
  *
236
- * @param plugin - Plugin to install
237
- * @returns Promise that resolves when plugin is installed
232
+ * After `ready()` resolves, the configuration is frozen (`use`/`route`/`extend`
233
+ * throw).
238
234
  *
239
- * @deprecated Use `plugin()` instead — it handles both sync and async
240
- * plugins automatically.
241
- *
242
- * @example
243
- * ```typescript
244
- * await app.plugin(new DatabasePlugin({ connectionString: '...' }));
245
- * ```
235
+ * @returns this
236
+ * @throws if an extension declares a `needs` dependency not yet registered
237
+ */
238
+ ready(): Promise<this>;
239
+ /**
240
+ * Run the boot sequence once (extension setup + router mount). Guarded and
241
+ * memoized by {@link ready}.
246
242
  */
247
- pluginAsync(plugin: Plugin): Promise<this>;
243
+ private _boot;
248
244
  /**
249
- * Get an installed plugin by name
245
+ * Attach a value to the app under `name`. Extension-author primitive, invoked
246
+ * through {@link ExtensionContext.decorate} — there is intentionally no public
247
+ * `app.decorate()` (RFC §6.1). Throws on collision.
250
248
  *
251
- * @param name - Plugin name
252
- * @returns Plugin instance or undefined
249
+ * @internal
253
250
  */
254
- getPlugin<T extends Plugin>(name: string): T | undefined;
251
+ private decorate;
255
252
  /**
256
- * Check if a plugin is installed
253
+ * Whether a decoration already occupies `name`.
257
254
  */
258
- hasPlugin(name: string): boolean;
255
+ hasDecorator(name: string): boolean;
259
256
  /**
260
- * Create the request handler callback
261
- *
262
- * This is the function that adapters call to handle each request.
263
- * It composes all middleware and returns a function that processes context.
257
+ * Create the request handler callback.
264
258
  *
265
- * **Important:** The middleware stack is snapshot at call time.
266
- * Middleware or plugins added after `callback()` will NOT take effect
267
- * on the returned handler. Call `callback()` again to get a
268
- * handler that includes subsequent registrations.
259
+ * The middleware stack is snapshot at call time — call `ready()` before
260
+ * `callback()` so extension-registered middleware is included. Adapters do
261
+ * this automatically.
269
262
  *
270
263
  * @returns Request handler function
271
264
  */
@@ -275,46 +268,58 @@ declare class Application {
275
268
  */
276
269
  private handleError;
277
270
  /**
278
- * Default error handler
279
- *
280
- * Uses the `expose` flag on errors (set by HttpError/NextRushError)
281
- * to decide whether to include the error message in the response.
282
- * 4xx errors expose messages by default; 5xx errors never do.
283
- * This prevents leaking internal details (paths, SQL, stack info)
284
- * regardless of environment.
271
+ * Default error handler — delegates to the shared error serializer so the
272
+ * default response matches `@nextrush/errors`' `errorHandler()` (audit C-1).
285
273
  */
286
274
  private defaultErrorHandler;
287
275
  /**
288
276
  * Mark app as running and freeze configuration.
289
277
  *
290
- * Called by adapters when the server starts listening. After this call,
291
- * `use()`, `route()`, `plugin()`, and `pluginAsync()` will throw.
292
- * This prevents unsafe middleware mutations while requests are in flight.
293
- *
294
- * To re-enable registration (e.g. for testing), call `close()` first.
278
+ * Called by adapters when the server starts listening (after `ready()`).
295
279
  */
296
280
  start(): void;
297
281
  /**
298
- * Graceful shutdown
299
- * Destroys all plugins that have a destroy method.
300
- * Uses Promise.allSettled to ensure all plugins are destroyed
301
- * even if some throw errors.
282
+ * Register a teardown hook for a resource outside the extension system
283
+ * (stateful middleware, a manually-attached service, …). Run during
284
+ * {@link close} under the SAME bounded/isolated guarantee as extension
285
+ * `destroy()` one throwing/hanging hook never strands the others — in one
286
+ * combined reverse-of-registration order together with every `extend()`ed
287
+ * extension's `destroy()` (RFC-022 §8.1/§8.2).
288
+ *
289
+ * @param hook - Teardown callback. May be sync or async; a thrown/rejected
290
+ * hook is isolated and its error collected in {@link close}'s returned array.
302
291
  *
303
- * @returns Array of errors from plugins that failed to destroy (empty on success)
292
+ * @remarks Registration is pre-shutdown only: a hook registered after
293
+ * `close()` has already started is not run in that shutdown (RFC-022 §8.6).
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * const store = new MemoryStore(options);
298
+ * app.onClose(() => store.shutdown());
299
+ * ```
300
+ */
301
+ onClose(hook: () => void | Promise<void>): void;
302
+ /**
303
+ * Graceful shutdown. Destroys extensions in reverse registration order,
304
+ * each isolated from the others' failures — one failing/hanging `destroy()`
305
+ * never strands the rest (RFC-022 / ADR-0012).
306
+ *
307
+ * @param options - Optional teardown budget. Omitted = unbounded, byte-identical
308
+ * to today's behavior (`Promise.allSettled`, no timeout). When `timeout` is
309
+ * given, every teardown unit races against it independently; a unit that
310
+ * does not finish in time is reported as a {@link TeardownTimeoutError} in
311
+ * the returned array instead of blocking `close()` past the budget.
312
+ * @returns Array of errors from units that failed or timed out (empty on success)
304
313
  */
305
- close(): Promise<Error[]>;
314
+ close(options?: CloseOptions): Promise<Error[]>;
315
+ /** Run the shutdown sequence once. Guarded and memoized by {@link close}. */
316
+ private _shutdown;
306
317
  }
307
318
  /**
308
319
  * Create a new Application instance
309
320
  *
310
321
  * @param options - Application options
311
322
  * @returns New Application instance
312
- *
313
- * @example
314
- * ```typescript
315
- * const app = createApp();
316
- * const app = createApp({ env: 'production' });
317
- * ```
318
323
  */
319
324
  declare function createApp(options?: ApplicationOptions): Application;
320
325
 
@@ -337,8 +342,14 @@ type ComposedMiddleware = (ctx: Context, next?: Next) => Promise<void>;
337
342
  interface ComposeOptions {
338
343
  /**
339
344
  * Warn when a middleware sends a response AND calls next().
340
- * Enabled by default in non-production environments.
341
- * @default process.env.NODE_ENV !== 'production'
345
+ *
346
+ * @remarks
347
+ * Opt-in and defaults to `false` — `@nextrush/core` is a runtime-agnostic
348
+ * package and must not read `process.env` (global-rules §2, audit C-4). The
349
+ * `Application` enables this in non-production by passing the flag from its
350
+ * own `env` option.
351
+ *
352
+ * @default false
342
353
  */
343
354
  warnDoubleResponse?: boolean;
344
355
  }
@@ -385,4 +396,4 @@ declare function isMiddleware(fn: unknown): fn is Middleware;
385
396
  */
386
397
  declare function flattenMiddleware(arr: (Middleware | Middleware[])[]): Middleware[];
387
398
 
388
- export { Application, type ApplicationOptions, type ComposeOptions, type ComposedMiddleware, type ErrorHandler, type ListenCallback, type Logger, type Routable, compose, createApp, flattenMiddleware, isMiddleware };
399
+ export { Application, type ApplicationOptions, type ComposeOptions, type ComposedMiddleware, type ErrorHandler, type ListenCallback, type Routable, compose, createApp, flattenMiddleware, isMiddleware };