@znight/core 0.6.2 → 0.6.4
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 +7 -2
- package/dist/index.d.mts +6 -5
- package/dist/index.mjs +78 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/application.ts +73 -15
- package/src/container.ts +26 -10
- package/src/scope.ts +4 -1
package/README.md
CHANGED
|
@@ -153,7 +153,11 @@ 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; a plugin whose `onStart` is still in flight is stopped and disposed when that hook settles,
|
|
160
|
+
not before). It runs three phases:
|
|
157
161
|
|
|
158
162
|
1. plugins' `onStop`, in reverse order (stop ingress, drain in-flight work);
|
|
159
163
|
2. for each provider in reverse resolution order: `onStop` (if it was started), then disposal;
|
|
@@ -271,7 +275,8 @@ active scope (`get`, `set`, `has`). Plugins (HTTP, messaging) open the scopes fo
|
|
|
271
275
|
`container.openScope(values?)` opens the same scope without a callback, for hot paths that stay
|
|
272
276
|
synchronous as long as their work is (the HTTP pipeline): `run(fn, ...args)` calls `fn` in the scope
|
|
273
277
|
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
|
|
278
|
+
in it. `close()` returns `undefined` when there is nothing to dispose (no instance created in the scope
|
|
279
|
+
has a dispose method, `[Symbol.asyncDispose]` or `[Symbol.dispose]`), otherwise a promise that
|
|
275
280
|
rejects with `ZNIGHT021` when a disposal fails; it must be called once the work is over.
|
|
276
281
|
|
|
277
282
|
```ts
|
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
|
}
|
|
@@ -1942,6 +1956,13 @@ var Application = class Application {
|
|
|
1942
1956
|
#startAborted = false;
|
|
1943
1957
|
/** Releases a `stop()` waiting for the start, when a start hook asks to stop in turn. */
|
|
1944
1958
|
#releaseStopWait;
|
|
1959
|
+
/** Owner of the `onStart` hook in progress. */
|
|
1960
|
+
#starting;
|
|
1961
|
+
/**
|
|
1962
|
+
* Owner of the `onStart` hook still in flight when the wait of `stop()` timed out: the shutdown
|
|
1963
|
+
* skips its `onDispose`, and it is stopped and disposed when that hook settles.
|
|
1964
|
+
*/
|
|
1965
|
+
#lateStart;
|
|
1945
1966
|
constructor(options) {
|
|
1946
1967
|
const plugins = [...options.plugins ?? []];
|
|
1947
1968
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -2075,11 +2096,11 @@ var Application = class Application {
|
|
|
2075
2096
|
async #runStartHooks() {
|
|
2076
2097
|
const providers = this.container.instances();
|
|
2077
2098
|
for (const provider of providers) {
|
|
2078
|
-
if (!await this.#startHook(() => this.#callProvider(provider, "onStart"))) return false;
|
|
2099
|
+
if (!await this.#startHook(() => this.#callProvider(provider, "onStart"), { provider })) return false;
|
|
2079
2100
|
this.#startedProviders.add(provider.token);
|
|
2080
2101
|
}
|
|
2081
2102
|
for (const plugin of this.#plugins) {
|
|
2082
|
-
if (!await this.#startHook(() => this.#callPlugin(plugin, "onStart"))) return false;
|
|
2103
|
+
if (!await this.#startHook(() => this.#callPlugin(plugin, "onStart"), { plugin })) return false;
|
|
2083
2104
|
this.#startedPlugins.push(plugin);
|
|
2084
2105
|
}
|
|
2085
2106
|
for (const provider of providers) if (!await this.#startHook(() => this.#callProvider(provider, "onReady"))) return false;
|
|
@@ -2089,17 +2110,41 @@ var Application = class Application {
|
|
|
2089
2110
|
/**
|
|
2090
2111
|
* Runs one start hook in an async context carrying a fresh marker, so that a `stop()` called from
|
|
2091
2112
|
* it (or from work it awaits) is recognised. Returns `false` when the hook stopped the application.
|
|
2113
|
+
* `owner` is given for `onStart` hooks: one that settles after `stop()` stopped waiting for it is
|
|
2114
|
+
* stopped and disposed then (see `#stopLateStart`).
|
|
2092
2115
|
*/
|
|
2093
|
-
async #startHook(hook) {
|
|
2116
|
+
async #startHook(hook, owner) {
|
|
2094
2117
|
const marker = {};
|
|
2095
2118
|
this.#startHookMarker = marker;
|
|
2119
|
+
this.#starting = owner;
|
|
2120
|
+
let started = false;
|
|
2096
2121
|
try {
|
|
2097
2122
|
await startHookContext.run(marker, hook);
|
|
2123
|
+
started = true;
|
|
2098
2124
|
} finally {
|
|
2099
2125
|
this.#startHookMarker = void 0;
|
|
2126
|
+
this.#starting = void 0;
|
|
2127
|
+
if (owner !== void 0 && this.#lateStart === owner) await this.#stopLateStart(owner, started);
|
|
2100
2128
|
}
|
|
2101
2129
|
return !this.#startAborted;
|
|
2102
2130
|
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Stops what an `onStart` hook settling after the shutdown started (best effort, failures logged):
|
|
2133
|
+
* a plugin gets `onStop` (when its `onStart` succeeded) then the `onDispose` the shutdown skipped;
|
|
2134
|
+
* a provider, already disposed by the shutdown, gets `onStop` when its `onStart` succeeded.
|
|
2135
|
+
*/
|
|
2136
|
+
async #stopLateStart(owner, started) {
|
|
2137
|
+
const { plugin, provider } = owner;
|
|
2138
|
+
const label = plugin === void 0 ? `${provider?.displayName}.onStart()` : `Plugin "${plugin.name}" onStart()`;
|
|
2139
|
+
this.#logger.warn(`${label} settled after the shutdown stopped waiting for it: stopping it now`);
|
|
2140
|
+
const errors = [];
|
|
2141
|
+
const step = this.#stepRunner(errors);
|
|
2142
|
+
if (plugin !== void 0) {
|
|
2143
|
+
if (started && typeof plugin.onStop === "function") await step(`Plugin "${plugin.name}" onStop()`, () => plugin.onStop?.(this.#context));
|
|
2144
|
+
if (typeof plugin.onDispose === "function") await step(`Plugin "${plugin.name}" onDispose()`, () => plugin.onDispose?.(this.#context));
|
|
2145
|
+
} else if (provider !== void 0 && started && hasLifecycleHook(provider.instance, "onStop")) await step(`${provider.displayName}.onStop()`, () => callLifecycleHook(provider.instance, "onStop"));
|
|
2146
|
+
for (const error of errors) this.#logger.error("Failed to stop a late start", { err: error });
|
|
2147
|
+
}
|
|
2103
2148
|
/** Whether the caller runs in the async context of the start hook in progress. */
|
|
2104
2149
|
#calledFromStartHook() {
|
|
2105
2150
|
const marker = this.#startHookMarker;
|
|
@@ -2112,11 +2157,12 @@ var Application = class Application {
|
|
|
2112
2157
|
} catch {}
|
|
2113
2158
|
}
|
|
2114
2159
|
/**
|
|
2115
|
-
* Stops the application (idempotent; waits for an in-progress `start()
|
|
2116
|
-
*
|
|
2117
|
-
* plugins' `
|
|
2118
|
-
*
|
|
2119
|
-
*
|
|
2160
|
+
* Stops the application (idempotent; waits for an in-progress `start()`, as a step bounded like
|
|
2161
|
+
* the others: past it, the remaining start hooks are skipped and what completed its `onStart` is
|
|
2162
|
+
* stopped): plugins' `onStop` (reverse) → providers' `onStop` + disposal (reverse resolution
|
|
2163
|
+
* order) → plugins' `onDispose` (reverse). Every step is attempted, each within `hookTimeoutMs`
|
|
2164
|
+
* and all within `timeoutMs`; failures are collected into a ZnightAggregateError (ZNIGHT021).
|
|
2165
|
+
* Signal handlers are removed.
|
|
2120
2166
|
*
|
|
2121
2167
|
* Called from a start hook (`onStart`/`onReady` of a provider or plugin, or work it awaits), it
|
|
2122
2168
|
* does not wait for the start, which waits for that hook: the shutdown runs right away, stopping
|
|
@@ -2131,13 +2177,22 @@ var Application = class Application {
|
|
|
2131
2177
|
return this.#stopPromise;
|
|
2132
2178
|
}
|
|
2133
2179
|
async #runStop() {
|
|
2134
|
-
|
|
2180
|
+
const errors = [];
|
|
2181
|
+
const step = this.#stepRunner(errors);
|
|
2182
|
+
if (this.#state === "starting" && !this.#startAborted) {
|
|
2183
|
+
await step("Waiting for start()", () => this.#waitForStart());
|
|
2184
|
+
if (this.#state === "starting") {
|
|
2185
|
+
this.#startAborted = true;
|
|
2186
|
+
this.#releaseStopWait = void 0;
|
|
2187
|
+
this.#lateStart = this.#starting;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2135
2190
|
if (this.#state === "failed" || this.#state === "stopped") {
|
|
2136
2191
|
this.#removeSignals();
|
|
2137
2192
|
return;
|
|
2138
2193
|
}
|
|
2139
2194
|
this.#state = "stopping";
|
|
2140
|
-
|
|
2195
|
+
await this.#shutdown(errors, step);
|
|
2141
2196
|
this.#removeSignals();
|
|
2142
2197
|
this.#state = "stopped";
|
|
2143
2198
|
if (errors.length > 0) throw new ZnightAggregateError(ErrorCode.LifecycleFailed, `The application stopped with ${errors.length} error(s)`, errors);
|
|
@@ -2158,11 +2213,9 @@ var Application = class Application {
|
|
|
2158
2213
|
}
|
|
2159
2214
|
/**
|
|
2160
2215
|
* 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.
|
|
2216
|
+
* is stopped). Never throws: returns the errors of the failed steps (appended to `errors`).
|
|
2162
2217
|
*/
|
|
2163
|
-
async #shutdown() {
|
|
2164
|
-
const errors = [];
|
|
2165
|
-
const step = this.#stepRunner(errors);
|
|
2218
|
+
async #shutdown(errors = [], step = this.#stepRunner(errors)) {
|
|
2166
2219
|
for (const plugin of [...this.#startedPlugins].reverse()) if (typeof plugin.onStop === "function") await step(`Plugin "${plugin.name}" onStop()`, () => plugin.onStop?.(this.#context));
|
|
2167
2220
|
for (const provider of this.container.instances().reverse()) {
|
|
2168
2221
|
if (this.#startedProviders.has(provider.token) && hasLifecycleHook(provider.instance, "onStop")) await step(`${provider.displayName}.onStop()`, () => callLifecycleHook(provider.instance, "onStop"));
|
|
@@ -2174,7 +2227,7 @@ var Application = class Application {
|
|
|
2174
2227
|
} catch (error) {
|
|
2175
2228
|
errors.push(...flatten(error));
|
|
2176
2229
|
}
|
|
2177
|
-
for (const plugin of [...this.#registered].reverse()) if (typeof plugin.onDispose === "function") await step(`Plugin "${plugin.name}" onDispose()`, () => plugin.onDispose?.(this.#context));
|
|
2230
|
+
for (const plugin of [...this.#registered].reverse()) if (typeof plugin.onDispose === "function" && plugin !== this.#lateStart?.plugin) await step(`Plugin "${plugin.name}" onDispose()`, () => plugin.onDispose?.(this.#context));
|
|
2178
2231
|
this.#startedPlugins.length = 0;
|
|
2179
2232
|
this.#startedProviders.clear();
|
|
2180
2233
|
return errors;
|