@znight/core 0.6.3 → 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 CHANGED
@@ -156,7 +156,8 @@ begins. If a step fails, what was started is stopped, everything is disposed and
156
156
  **`app.stop()`** is idempotent and waits for an in-progress start, at most `hookTimeoutMs` (a start
157
157
  hook that never settles cannot block the shutdown: past that budget, a `Waiting for start() timed out`
158
158
  failure is recorded, the remaining start hooks are skipped and what completed its `onStart` is
159
- stopped). It runs three phases:
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:
160
161
 
161
162
  1. plugins' `onStop`, in reverse order (stop ingress, drain in-flight work);
162
163
  2. for each provider in reverse resolution order: `onStop` (if it was started), then disposal;
package/dist/index.mjs CHANGED
@@ -1956,6 +1956,13 @@ var Application = class Application {
1956
1956
  #startAborted = false;
1957
1957
  /** Releases a `stop()` waiting for the start, when a start hook asks to stop in turn. */
1958
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;
1959
1966
  constructor(options) {
1960
1967
  const plugins = [...options.plugins ?? []];
1961
1968
  const byName = /* @__PURE__ */ new Map();
@@ -2089,11 +2096,11 @@ var Application = class Application {
2089
2096
  async #runStartHooks() {
2090
2097
  const providers = this.container.instances();
2091
2098
  for (const provider of providers) {
2092
- if (!await this.#startHook(() => this.#callProvider(provider, "onStart"))) return false;
2099
+ if (!await this.#startHook(() => this.#callProvider(provider, "onStart"), { provider })) return false;
2093
2100
  this.#startedProviders.add(provider.token);
2094
2101
  }
2095
2102
  for (const plugin of this.#plugins) {
2096
- if (!await this.#startHook(() => this.#callPlugin(plugin, "onStart"))) return false;
2103
+ if (!await this.#startHook(() => this.#callPlugin(plugin, "onStart"), { plugin })) return false;
2097
2104
  this.#startedPlugins.push(plugin);
2098
2105
  }
2099
2106
  for (const provider of providers) if (!await this.#startHook(() => this.#callProvider(provider, "onReady"))) return false;
@@ -2103,17 +2110,41 @@ var Application = class Application {
2103
2110
  /**
2104
2111
  * Runs one start hook in an async context carrying a fresh marker, so that a `stop()` called from
2105
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`).
2106
2115
  */
2107
- async #startHook(hook) {
2116
+ async #startHook(hook, owner) {
2108
2117
  const marker = {};
2109
2118
  this.#startHookMarker = marker;
2119
+ this.#starting = owner;
2120
+ let started = false;
2110
2121
  try {
2111
2122
  await startHookContext.run(marker, hook);
2123
+ started = true;
2112
2124
  } finally {
2113
2125
  this.#startHookMarker = void 0;
2126
+ this.#starting = void 0;
2127
+ if (owner !== void 0 && this.#lateStart === owner) await this.#stopLateStart(owner, started);
2114
2128
  }
2115
2129
  return !this.#startAborted;
2116
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
+ }
2117
2148
  /** Whether the caller runs in the async context of the start hook in progress. */
2118
2149
  #calledFromStartHook() {
2119
2150
  const marker = this.#startHookMarker;
@@ -2153,6 +2184,7 @@ var Application = class Application {
2153
2184
  if (this.#state === "starting") {
2154
2185
  this.#startAborted = true;
2155
2186
  this.#releaseStopWait = void 0;
2187
+ this.#lateStart = this.#starting;
2156
2188
  }
2157
2189
  }
2158
2190
  if (this.#state === "failed" || this.#state === "stopped") {
@@ -2195,7 +2227,7 @@ var Application = class Application {
2195
2227
  } catch (error) {
2196
2228
  errors.push(...flatten(error));
2197
2229
  }
2198
- 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));
2199
2231
  this.#startedPlugins.length = 0;
2200
2232
  this.#startedProviders.clear();
2201
2233
  return errors;