@znight/core 0.6.1 → 0.6.3
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/README.md +20 -11
- package/dist/index.d.mts +6 -5
- package/dist/index.mjs +42 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/application.ts +20 -11
- package/src/container.ts +26 -10
- package/src/scope.ts +4 -1
package/README.md
CHANGED
|
@@ -153,7 +153,10 @@ token; keys may be tokens or classes. Override values that look like a provider
|
|
|
153
153
|
(start listening), then providers' and plugins' `onReady`. Signal handlers are installed when the start
|
|
154
154
|
begins. If a step fails, what was started is stopped, everything is disposed and the error is rethrown.
|
|
155
155
|
|
|
156
|
-
**`app.stop()`** is idempotent and waits for an in-progress start
|
|
156
|
+
**`app.stop()`** is idempotent and waits for an in-progress start, at most `hookTimeoutMs` (a start
|
|
157
|
+
hook that never settles cannot block the shutdown: past that budget, a `Waiting for start() timed out`
|
|
158
|
+
failure is recorded, the remaining start hooks are skipped and what completed its `onStart` is
|
|
159
|
+
stopped). It runs three phases:
|
|
157
160
|
|
|
158
161
|
1. plugins' `onStop`, in reverse order (stop ingress, drain in-flight work);
|
|
159
162
|
2. for each provider in reverse resolution order: `onStop` (if it was started), then disposal;
|
|
@@ -271,7 +274,8 @@ active scope (`get`, `set`, `has`). Plugins (HTTP, messaging) open the scopes fo
|
|
|
271
274
|
`container.openScope(values?)` opens the same scope without a callback, for hot paths that stay
|
|
272
275
|
synchronous as long as their work is (the HTTP pipeline): `run(fn, ...args)` calls `fn` in the scope
|
|
273
276
|
and returns its result (its continuations stay in the scope), `close()` disposes the instances created
|
|
274
|
-
in it. `close()` returns `undefined` when there is nothing to dispose
|
|
277
|
+
in it. `close()` returns `undefined` when there is nothing to dispose (no instance created in the scope
|
|
278
|
+
has a dispose method, `[Symbol.asyncDispose]` or `[Symbol.dispose]`), otherwise a promise that
|
|
275
279
|
rejects with `ZNIGHT021` when a disposal fails; it must be called once the work is over.
|
|
276
280
|
|
|
277
281
|
```ts
|
|
@@ -368,13 +372,17 @@ return what is known (`[]`).
|
|
|
368
372
|
declared in core, so producers and observers (`@znight/observability`, your plugins) share them
|
|
369
373
|
without depending on each other:
|
|
370
374
|
|
|
371
|
-
| Hook
|
|
372
|
-
|
|
|
373
|
-
| `http:request`
|
|
374
|
-
| `messaging:message`
|
|
375
|
-
| `messaging:publish`
|
|
376
|
-
| `schedule:run`
|
|
377
|
-
| `graphql:operation`
|
|
375
|
+
| Hook | Event | Produced by |
|
|
376
|
+
| ---------------------- | -------------------------- | ---------------------------------------------------------------------------- |
|
|
377
|
+
| `http:request` | `HttpRequestEvent` | `@znight/http`, one request |
|
|
378
|
+
| `messaging:message` | `MessageDeliveryEvent` | `@znight/messaging`, one delivery |
|
|
379
|
+
| `messaging:publish` | `MessagePublishEvent` | `@znight/messaging`, one `MessageBroker` publication |
|
|
380
|
+
| `schedule:run` | `ScheduledRunEvent` | `@znight/schedule`, one occurrence of a job (executed or not, see `outcome`) |
|
|
381
|
+
| `graphql:operation` | `GraphQLOperationEvent` | `@znight/graphql` (RFC 0005), one GraphQL operation |
|
|
382
|
+
| `websocket:message` | `WebSocketMessageEvent` | `@znight/websocket` (RFC 0003), one inbound message (`$ping` excluded) |
|
|
383
|
+
| `websocket:connection` | `WebSocketConnectionEvent` | `@znight/websocket`, a connection opened or closed (emitted only) |
|
|
384
|
+
| `events:publish` | `EventPublishEvent` | `@znight/events` (RFC 0011), one publication with at least one listener |
|
|
385
|
+
| `events:listener` | `EventListenerEvent` | `@znight/events`, one listener invocation, inline or detached |
|
|
378
386
|
|
|
379
387
|
`ScheduledRunEvent` carries `job`, `kind` (`cron` / `interval` / `timeout`), `host`, `method`,
|
|
380
388
|
`runId`, `trigger` (`schedule` / `start` / `manual` / `queued`), `scheduledAt` (epoch ms),
|
|
@@ -461,6 +469,7 @@ the [architecture specification](../../docs/internals/architecture.md#48-validat
|
|
|
461
469
|
The catalogue is shared by the compiler and every first-party package. `ZNIGHT001`–`ZNIGHT029`
|
|
462
470
|
belong to the core, the compiler and the features of the first release (`ZNIGHT023`, a missing
|
|
463
471
|
route decision, is reported by `@znight/http`). From `ZNIGHT030`, each package owns a block of ten:
|
|
464
|
-
`ZNIGHT030`–`039` `@znight/openapi`, `ZNIGHT040`–`049` `@znight/schedule`, `
|
|
465
|
-
`@znight/
|
|
472
|
+
`ZNIGHT030`–`039` `@znight/openapi`, `ZNIGHT040`–`049` `@znight/schedule`, `ZNIGHT050`–`059`
|
|
473
|
+
`@znight/websocket` (RFC 0003), `ZNIGHT060`–`069` `@znight/graphql` (RFC 0005, RFC 0007),
|
|
474
|
+
`ZNIGHT070`–`079` `@znight/events` (RFC 0011).
|
|
466
475
|
Inside a block, codes are allocated in order; third-party packages get no `ZNIGHT` code.
|
package/dist/index.d.mts
CHANGED
|
@@ -1167,11 +1167,12 @@ export declare class Application {
|
|
|
1167
1167
|
*/
|
|
1168
1168
|
start(): Promise<void>;
|
|
1169
1169
|
/**
|
|
1170
|
-
* Stops the application (idempotent; waits for an in-progress `start()
|
|
1171
|
-
*
|
|
1172
|
-
* plugins' `
|
|
1173
|
-
*
|
|
1174
|
-
*
|
|
1170
|
+
* Stops the application (idempotent; waits for an in-progress `start()`, as a step bounded like
|
|
1171
|
+
* the others: past it, the remaining start hooks are skipped and what completed its `onStart` is
|
|
1172
|
+
* stopped): plugins' `onStop` (reverse) → providers' `onStop` + disposal (reverse resolution
|
|
1173
|
+
* order) → plugins' `onDispose` (reverse). Every step is attempted, each within `hookTimeoutMs`
|
|
1174
|
+
* and all within `timeoutMs`; failures are collected into a ZnightAggregateError (ZNIGHT021).
|
|
1175
|
+
* Signal handlers are removed.
|
|
1175
1176
|
*
|
|
1176
1177
|
* Called from a start hook (`onStart`/`onReady` of a provider or plugin, or work it awaits), it
|
|
1177
1178
|
* does not wait for the start, which waits for that hook: the shutdown runs right away, stopping
|
package/dist/index.mjs
CHANGED
|
@@ -938,6 +938,11 @@ var Container = class Container {
|
|
|
938
938
|
#storage = new AsyncLocalStorage();
|
|
939
939
|
/** Instantiated singletons, in creation order. */
|
|
940
940
|
#created = [];
|
|
941
|
+
/**
|
|
942
|
+
* Request-scoped and transient slots whose factory is running. Creation is synchronous, so a slot
|
|
943
|
+
* found here again was requested by its own construction (a `Lazy<T>.get()` in a constructor).
|
|
944
|
+
*/
|
|
945
|
+
#constructing = /* @__PURE__ */ new Set();
|
|
941
946
|
#order = [];
|
|
942
947
|
#names;
|
|
943
948
|
#status = "open";
|
|
@@ -1261,21 +1266,29 @@ var Container = class Container {
|
|
|
1261
1266
|
/** Instantiates a request-scoped or transient provider (synchronous factories only). */
|
|
1262
1267
|
#create(slot) {
|
|
1263
1268
|
const definition = slot.definition;
|
|
1269
|
+
const constructing = this.#constructing;
|
|
1270
|
+
if (constructing.has(slot)) throw reentrantCreationError(slot);
|
|
1271
|
+
constructing.add(slot);
|
|
1264
1272
|
let deps;
|
|
1265
|
-
try {
|
|
1266
|
-
deps = this.#resolveDeps(slot);
|
|
1267
|
-
} catch (error) {
|
|
1268
|
-
throw error instanceof InstantiationError ? error.withParents([slot.token]) : error;
|
|
1269
|
-
}
|
|
1270
1273
|
let instance;
|
|
1271
1274
|
try {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
+
try {
|
|
1276
|
+
deps = this.#resolveDeps(slot);
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
throw error instanceof InstantiationError ? error.withParents([slot.token]) : error;
|
|
1279
|
+
}
|
|
1280
|
+
try {
|
|
1281
|
+
instance = definition.factory(deps);
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
throw new InstantiationError(slot.token, [slot.token], error, definition.source);
|
|
1284
|
+
}
|
|
1285
|
+
} finally {
|
|
1286
|
+
constructing.delete(slot);
|
|
1275
1287
|
}
|
|
1276
1288
|
if (definition.interceptors !== void 0) try {
|
|
1277
1289
|
applyInterceptors(instance, definition.interceptors, deps, slot.displayName);
|
|
1278
1290
|
} catch (error) {
|
|
1291
|
+
disposeQuietly(instance, definition.dispose);
|
|
1279
1292
|
throw new InstantiationError(slot.token, [slot.token], error, definition.source);
|
|
1280
1293
|
}
|
|
1281
1294
|
return instance;
|
|
@@ -1290,10 +1303,11 @@ var Container = class Container {
|
|
|
1290
1303
|
this.#assertResolvable(slot);
|
|
1291
1304
|
const instance = this.#create(slot);
|
|
1292
1305
|
values.set(slot.token, instance);
|
|
1293
|
-
|
|
1306
|
+
const dispose = slot.definition?.dispose;
|
|
1307
|
+
if (isDisposable(instance, dispose)) scope.created.push({
|
|
1294
1308
|
token: slot.token,
|
|
1295
1309
|
instance,
|
|
1296
|
-
dispose
|
|
1310
|
+
dispose
|
|
1297
1311
|
});
|
|
1298
1312
|
return instance;
|
|
1299
1313
|
}
|
|
@@ -2112,11 +2126,12 @@ var Application = class Application {
|
|
|
2112
2126
|
} catch {}
|
|
2113
2127
|
}
|
|
2114
2128
|
/**
|
|
2115
|
-
* Stops the application (idempotent; waits for an in-progress `start()
|
|
2116
|
-
*
|
|
2117
|
-
* plugins' `
|
|
2118
|
-
*
|
|
2119
|
-
*
|
|
2129
|
+
* Stops the application (idempotent; waits for an in-progress `start()`, as a step bounded like
|
|
2130
|
+
* the others: past it, the remaining start hooks are skipped and what completed its `onStart` is
|
|
2131
|
+
* stopped): plugins' `onStop` (reverse) → providers' `onStop` + disposal (reverse resolution
|
|
2132
|
+
* order) → plugins' `onDispose` (reverse). Every step is attempted, each within `hookTimeoutMs`
|
|
2133
|
+
* and all within `timeoutMs`; failures are collected into a ZnightAggregateError (ZNIGHT021).
|
|
2134
|
+
* Signal handlers are removed.
|
|
2120
2135
|
*
|
|
2121
2136
|
* Called from a start hook (`onStart`/`onReady` of a provider or plugin, or work it awaits), it
|
|
2122
2137
|
* does not wait for the start, which waits for that hook: the shutdown runs right away, stopping
|
|
@@ -2131,13 +2146,21 @@ var Application = class Application {
|
|
|
2131
2146
|
return this.#stopPromise;
|
|
2132
2147
|
}
|
|
2133
2148
|
async #runStop() {
|
|
2134
|
-
|
|
2149
|
+
const errors = [];
|
|
2150
|
+
const step = this.#stepRunner(errors);
|
|
2151
|
+
if (this.#state === "starting" && !this.#startAborted) {
|
|
2152
|
+
await step("Waiting for start()", () => this.#waitForStart());
|
|
2153
|
+
if (this.#state === "starting") {
|
|
2154
|
+
this.#startAborted = true;
|
|
2155
|
+
this.#releaseStopWait = void 0;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2135
2158
|
if (this.#state === "failed" || this.#state === "stopped") {
|
|
2136
2159
|
this.#removeSignals();
|
|
2137
2160
|
return;
|
|
2138
2161
|
}
|
|
2139
2162
|
this.#state = "stopping";
|
|
2140
|
-
|
|
2163
|
+
await this.#shutdown(errors, step);
|
|
2141
2164
|
this.#removeSignals();
|
|
2142
2165
|
this.#state = "stopped";
|
|
2143
2166
|
if (errors.length > 0) throw new ZnightAggregateError(ErrorCode.LifecycleFailed, `The application stopped with ${errors.length} error(s)`, errors);
|
|
@@ -2158,11 +2181,9 @@ var Application = class Application {
|
|
|
2158
2181
|
}
|
|
2159
2182
|
/**
|
|
2160
2183
|
* The three shutdown phases (also used to roll back a failed create/start: only what was started
|
|
2161
|
-
* is stopped). Never throws: returns the errors of the failed steps.
|
|
2184
|
+
* is stopped). Never throws: returns the errors of the failed steps (appended to `errors`).
|
|
2162
2185
|
*/
|
|
2163
|
-
async #shutdown() {
|
|
2164
|
-
const errors = [];
|
|
2165
|
-
const step = this.#stepRunner(errors);
|
|
2186
|
+
async #shutdown(errors = [], step = this.#stepRunner(errors)) {
|
|
2166
2187
|
for (const plugin of [...this.#startedPlugins].reverse()) if (typeof plugin.onStop === "function") await step(`Plugin "${plugin.name}" onStop()`, () => plugin.onStop?.(this.#context));
|
|
2167
2188
|
for (const provider of this.container.instances().reverse()) {
|
|
2168
2189
|
if (this.#startedProviders.has(provider.token) && hasLifecycleHook(provider.instance, "onStop")) await step(`${provider.displayName}.onStop()`, () => callLifecycleHook(provider.instance, "onStop"));
|