@znight/core 0.6.2 → 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 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. It runs three phases:
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, otherwise a promise that
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
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
- * plugins' `onStop` (reverse) → providers' `onStop` + disposal (reverse resolution order) →
1172
- * plugins' `onDispose` (reverse). Every step is attempted, each within `hookTimeoutMs` and all
1173
- * within `timeoutMs`; failures are collected into a ZnightAggregateError (ZNIGHT021). Signal
1174
- * handlers are removed.
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
- instance = definition.factory(deps);
1273
- } catch (error) {
1274
- throw new InstantiationError(slot.token, [slot.token], error, definition.source);
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
- scope.created.push({
1306
+ const dispose = slot.definition?.dispose;
1307
+ if (isDisposable(instance, dispose)) scope.created.push({
1294
1308
  token: slot.token,
1295
1309
  instance,
1296
- dispose: slot.definition?.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
- * plugins' `onStop` (reverse) → providers' `onStop` + disposal (reverse resolution order) →
2117
- * plugins' `onDispose` (reverse). Every step is attempted, each within `hookTimeoutMs` and all
2118
- * within `timeoutMs`; failures are collected into a ZnightAggregateError (ZNIGHT021). Signal
2119
- * handlers are removed.
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
- if (this.#state === "starting" && !this.#startAborted) await this.#waitForStart();
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
- const errors = await this.#shutdown();
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"));