moost 0.6.36 → 0.6.37

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.cjs CHANGED
@@ -48,7 +48,7 @@ let stamped = false;
48
48
  if (stamped) return;
49
49
  stamped = true;
50
50
  stampModuleIdentity({
51
- version: "0.6.36",
51
+ version: "0.6.37",
52
52
  path: typeof __filename === "string" ? __filename : require("url").pathToFileURL(__filename).href
53
53
  });
54
54
  }
@@ -670,7 +670,7 @@ const disposeSymbol = typeof Symbol.dispose === "symbol" ? Symbol.dispose : void
670
670
  /** Class name of an instance, read through the prototype (never touches own props). */ function classNameOf(instance) {
671
671
  return Object.getPrototypeOf(instance)?.constructor?.name || "Object";
672
672
  }
673
- function errorMessage(error) {
673
+ /** @internal Message of a thrown value, whatever it is. */ function errorMessage(error) {
674
674
  return error instanceof Error ? error.message : String(error);
675
675
  }
676
676
  /** `Class.method (message)` pairs for every failed hook. */ function formatDisposeErrors(errors) {
@@ -1952,6 +1952,67 @@ function lostCtorParamsMessage(input, ancestor, count) {
1952
1952
  return [auditRouteDrop(input), auditLostCtorParams(input)].filter((f) => f !== void 0);
1953
1953
  }
1954
1954
 
1955
+ //#endregion
1956
+ //#region packages/moost/src/dispose-signals.ts
1957
+ /**
1958
+ * Global key for the record above. Global (rather than module-scoped) for the
1959
+ * same reason the module-identity stamp is: two copies of moost, or the entry
1960
+ * re-executing under the `@moostjs/vite` dev server, must share one set of
1961
+ * process listeners instead of stacking one per app.
1962
+ */ const DISPOSE_SIGNALS_KEY = Symbol.for("moost:dispose-on-signals");
1963
+ /**
1964
+ * Removes every registered listener and forgets the record. A no-op for a record
1965
+ * that is no longer the live one (a stale unregister fn, or a signal that already
1966
+ * dropped it).
1967
+ */ function dropSignalsRecord(record) {
1968
+ const holder = globalThis;
1969
+ if (holder[DISPOSE_SIGNALS_KEY] !== record) return;
1970
+ for (const [signal, listener] of record.listeners) process.off(signal, listener);
1971
+ record.listeners.clear();
1972
+ delete holder[DISPOSE_SIGNALS_KEY];
1973
+ }
1974
+ /**
1975
+ * The one listener body per signal (NestJS `enableShutdownHooks` semantics):
1976
+ * unhook every listener first, dispose the newest app, then re-raise `signal`
1977
+ * so the process exits with Node's default status for it. Because the listeners
1978
+ * are already gone, a second signal while disposal runs gets Node's default
1979
+ * handling (exit `128 + n`) — an impatient Ctrl-C is never stuck.
1980
+ */ async function onSignal(signal) {
1981
+ const record = globalThis[DISPOSE_SIGNALS_KEY];
1982
+ if (!record) return;
1983
+ const { app } = record;
1984
+ dropSignalsRecord(record);
1985
+ try {
1986
+ await app.dispose();
1987
+ } catch (error) {
1988
+ app.getLogger().warn(`[moost] dispose on ${signal} failed: ${errorMessage(error)}`);
1989
+ }
1990
+ process.kill(process.pid, signal);
1991
+ }
1992
+ /**
1993
+ * @internal Backs `Moost.disposeOnSignals()` — see that method's JSDoc for the
1994
+ * full contract. Registers at most one process listener per signal, ever, and
1995
+ * re-targets the existing ones at `app`.
1996
+ */ function registerDisposeOnSignals(app, signals) {
1997
+ var _holder, _DISPOSE_SIGNALS_KEY;
1998
+ const record = (_holder = globalThis)[_DISPOSE_SIGNALS_KEY = DISPOSE_SIGNALS_KEY] ?? (_holder[_DISPOSE_SIGNALS_KEY] = {
1999
+ app,
2000
+ listeners: /* @__PURE__ */ new Map()
2001
+ });
2002
+ record.app = app;
2003
+ for (const signal of signals) {
2004
+ if (record.listeners.has(signal)) continue;
2005
+ const listener = () => {
2006
+ onSignal(signal);
2007
+ };
2008
+ record.listeners.set(signal, listener);
2009
+ process.on(signal, listener);
2010
+ }
2011
+ return () => {
2012
+ dropSignalsRecord(record);
2013
+ };
2014
+ }
2015
+
1955
2016
  //#endregion
1956
2017
  //#region packages/moost/src/moost.ts
1957
2018
  function _define_property(obj, key, value) {
@@ -2136,14 +2197,38 @@ function _define_property(obj, key, value) {
2136
2197
  * cleanup helpers when a clean container is what you need.
2137
2198
  *
2138
2199
  * ```ts
2139
- * for (const signal of ['SIGTERM', 'SIGINT'] as const) {
2140
- * process.once(signal, () => { void app.dispose().finally(() => process.exit(0)) })
2141
- * }
2200
+ * app.disposeOnSignals() // SIGTERM + SIGINT → dispose() → re-raise
2142
2201
  * ```
2143
2202
  */ dispose() {
2144
2203
  if (!this.disposePromise) this.disposePromise = this.runDispose();
2145
2204
  return this.disposePromise;
2146
2205
  }
2206
+ /**
2207
+ * ### disposeOnSignals
2208
+ * Disposes this app when the process receives one of `signals` (default
2209
+ * `['SIGTERM', 'SIGINT']`), then re-raises the signal with no listener left,
2210
+ * so the process exits with Node's default status for it.
2211
+ *
2212
+ * Registers each process listener **once per process**: calling it again —
2213
+ * e.g. the entry re-executing under the `@moostjs/vite` dev server — re-targets
2214
+ * the existing listeners at the newest app instead of stacking one handler per
2215
+ * old app (which is what a plain `process.once(...)` in the entry does, until
2216
+ * Node warns about a listener leak). Signals are unioned across calls; an
2217
+ * already-registered one is never replaced or removed.
2218
+ *
2219
+ * A second signal while disposal runs gets Node's default handling, since the
2220
+ * listeners are already removed. A failing `dispose()` is logged as a warning
2221
+ * and the signal is re-raised all the same.
2222
+ *
2223
+ * Returns a function that unregisters the listeners (tests, embedded runners).
2224
+ *
2225
+ * ```ts
2226
+ * await app.init()
2227
+ * app.disposeOnSignals()
2228
+ * ```
2229
+ */ disposeOnSignals(signals = ["SIGTERM", "SIGINT"]) {
2230
+ return registerDisposeOnSignals(this, signals);
2231
+ }
2147
2232
  /** Adapters' `onDispose` in registration order; errors are collected, not fatal. */ async disposeAdapters() {
2148
2233
  const errors = [];
2149
2234
  for (const a of this.adapters) try {
@@ -2154,7 +2239,7 @@ function _define_property(obj, key, value) {
2154
2239
  method: "onDispose",
2155
2240
  error
2156
2241
  });
2157
- this.logger.warn(`[moost] adapter "${a.name}" onDispose failed: ${error instanceof Error ? error.message : String(error)}`);
2242
+ this.logger.warn(`[moost] adapter "${a.name}" onDispose failed: ${errorMessage(error)}`);
2158
2243
  }
2159
2244
  return errors;
2160
2245
  }
package/dist/index.d.ts CHANGED
@@ -762,12 +762,35 @@ declare class Moost extends Hookable {
762
762
  * cleanup helpers when a clean container is what you need.
763
763
  *
764
764
  * ```ts
765
- * for (const signal of ['SIGTERM', 'SIGINT'] as const) {
766
- * process.once(signal, () => { void app.dispose().finally(() => process.exit(0)) })
767
- * }
765
+ * app.disposeOnSignals() // SIGTERM + SIGINT → dispose() → re-raise
768
766
  * ```
769
767
  */
770
768
  dispose(): Promise<void>;
769
+ /**
770
+ * ### disposeOnSignals
771
+ * Disposes this app when the process receives one of `signals` (default
772
+ * `['SIGTERM', 'SIGINT']`), then re-raises the signal with no listener left,
773
+ * so the process exits with Node's default status for it.
774
+ *
775
+ * Registers each process listener **once per process**: calling it again —
776
+ * e.g. the entry re-executing under the `@moostjs/vite` dev server — re-targets
777
+ * the existing listeners at the newest app instead of stacking one handler per
778
+ * old app (which is what a plain `process.once(...)` in the entry does, until
779
+ * Node warns about a listener leak). Signals are unioned across calls; an
780
+ * already-registered one is never replaced or removed.
781
+ *
782
+ * A second signal while disposal runs gets Node's default handling, since the
783
+ * listeners are already removed. A failing `dispose()` is logged as a warning
784
+ * and the signal is re-raised all the same.
785
+ *
786
+ * Returns a function that unregisters the listeners (tests, embedded runners).
787
+ *
788
+ * ```ts
789
+ * await app.init()
790
+ * app.disposeOnSignals()
791
+ * ```
792
+ */
793
+ disposeOnSignals(signals?: NodeJS.Signals[]): () => void;
771
794
  /** Adapters' `onDispose` in registration order; errors are collected, not fatal. */
772
795
  protected disposeAdapters(): Promise<TDisposeError[]>;
773
796
  /** The one-shot body behind {@link Moost.dispose} (see its docs for the contract). */
package/dist/index.mjs CHANGED
@@ -47,7 +47,7 @@ let stamped = false;
47
47
  if (stamped) return;
48
48
  stamped = true;
49
49
  stampModuleIdentity({
50
- version: "0.6.36",
50
+ version: "0.6.37",
51
51
  path: typeof __filename === "string" ? __filename : import.meta.url
52
52
  });
53
53
  }
@@ -669,7 +669,7 @@ const disposeSymbol = typeof Symbol.dispose === "symbol" ? Symbol.dispose : void
669
669
  /** Class name of an instance, read through the prototype (never touches own props). */ function classNameOf(instance) {
670
670
  return Object.getPrototypeOf(instance)?.constructor?.name || "Object";
671
671
  }
672
- function errorMessage(error) {
672
+ /** @internal Message of a thrown value, whatever it is. */ function errorMessage(error) {
673
673
  return error instanceof Error ? error.message : String(error);
674
674
  }
675
675
  /** `Class.method (message)` pairs for every failed hook. */ function formatDisposeErrors(errors) {
@@ -1951,6 +1951,67 @@ function lostCtorParamsMessage(input, ancestor, count) {
1951
1951
  return [auditRouteDrop(input), auditLostCtorParams(input)].filter((f) => f !== void 0);
1952
1952
  }
1953
1953
 
1954
+ //#endregion
1955
+ //#region packages/moost/src/dispose-signals.ts
1956
+ /**
1957
+ * Global key for the record above. Global (rather than module-scoped) for the
1958
+ * same reason the module-identity stamp is: two copies of moost, or the entry
1959
+ * re-executing under the `@moostjs/vite` dev server, must share one set of
1960
+ * process listeners instead of stacking one per app.
1961
+ */ const DISPOSE_SIGNALS_KEY = Symbol.for("moost:dispose-on-signals");
1962
+ /**
1963
+ * Removes every registered listener and forgets the record. A no-op for a record
1964
+ * that is no longer the live one (a stale unregister fn, or a signal that already
1965
+ * dropped it).
1966
+ */ function dropSignalsRecord(record) {
1967
+ const holder = globalThis;
1968
+ if (holder[DISPOSE_SIGNALS_KEY] !== record) return;
1969
+ for (const [signal, listener] of record.listeners) process.off(signal, listener);
1970
+ record.listeners.clear();
1971
+ delete holder[DISPOSE_SIGNALS_KEY];
1972
+ }
1973
+ /**
1974
+ * The one listener body per signal (NestJS `enableShutdownHooks` semantics):
1975
+ * unhook every listener first, dispose the newest app, then re-raise `signal`
1976
+ * so the process exits with Node's default status for it. Because the listeners
1977
+ * are already gone, a second signal while disposal runs gets Node's default
1978
+ * handling (exit `128 + n`) — an impatient Ctrl-C is never stuck.
1979
+ */ async function onSignal(signal) {
1980
+ const record = globalThis[DISPOSE_SIGNALS_KEY];
1981
+ if (!record) return;
1982
+ const { app } = record;
1983
+ dropSignalsRecord(record);
1984
+ try {
1985
+ await app.dispose();
1986
+ } catch (error) {
1987
+ app.getLogger().warn(`[moost] dispose on ${signal} failed: ${errorMessage(error)}`);
1988
+ }
1989
+ process.kill(process.pid, signal);
1990
+ }
1991
+ /**
1992
+ * @internal Backs `Moost.disposeOnSignals()` — see that method's JSDoc for the
1993
+ * full contract. Registers at most one process listener per signal, ever, and
1994
+ * re-targets the existing ones at `app`.
1995
+ */ function registerDisposeOnSignals(app, signals) {
1996
+ var _holder, _DISPOSE_SIGNALS_KEY;
1997
+ const record = (_holder = globalThis)[_DISPOSE_SIGNALS_KEY = DISPOSE_SIGNALS_KEY] ?? (_holder[_DISPOSE_SIGNALS_KEY] = {
1998
+ app,
1999
+ listeners: /* @__PURE__ */ new Map()
2000
+ });
2001
+ record.app = app;
2002
+ for (const signal of signals) {
2003
+ if (record.listeners.has(signal)) continue;
2004
+ const listener = () => {
2005
+ onSignal(signal);
2006
+ };
2007
+ record.listeners.set(signal, listener);
2008
+ process.on(signal, listener);
2009
+ }
2010
+ return () => {
2011
+ dropSignalsRecord(record);
2012
+ };
2013
+ }
2014
+
1954
2015
  //#endregion
1955
2016
  //#region packages/moost/src/moost.ts
1956
2017
  function _define_property(obj, key, value) {
@@ -2135,14 +2196,38 @@ function _define_property(obj, key, value) {
2135
2196
  * cleanup helpers when a clean container is what you need.
2136
2197
  *
2137
2198
  * ```ts
2138
- * for (const signal of ['SIGTERM', 'SIGINT'] as const) {
2139
- * process.once(signal, () => { void app.dispose().finally(() => process.exit(0)) })
2140
- * }
2199
+ * app.disposeOnSignals() // SIGTERM + SIGINT → dispose() → re-raise
2141
2200
  * ```
2142
2201
  */ dispose() {
2143
2202
  if (!this.disposePromise) this.disposePromise = this.runDispose();
2144
2203
  return this.disposePromise;
2145
2204
  }
2205
+ /**
2206
+ * ### disposeOnSignals
2207
+ * Disposes this app when the process receives one of `signals` (default
2208
+ * `['SIGTERM', 'SIGINT']`), then re-raises the signal with no listener left,
2209
+ * so the process exits with Node's default status for it.
2210
+ *
2211
+ * Registers each process listener **once per process**: calling it again —
2212
+ * e.g. the entry re-executing under the `@moostjs/vite` dev server — re-targets
2213
+ * the existing listeners at the newest app instead of stacking one handler per
2214
+ * old app (which is what a plain `process.once(...)` in the entry does, until
2215
+ * Node warns about a listener leak). Signals are unioned across calls; an
2216
+ * already-registered one is never replaced or removed.
2217
+ *
2218
+ * A second signal while disposal runs gets Node's default handling, since the
2219
+ * listeners are already removed. A failing `dispose()` is logged as a warning
2220
+ * and the signal is re-raised all the same.
2221
+ *
2222
+ * Returns a function that unregisters the listeners (tests, embedded runners).
2223
+ *
2224
+ * ```ts
2225
+ * await app.init()
2226
+ * app.disposeOnSignals()
2227
+ * ```
2228
+ */ disposeOnSignals(signals = ["SIGTERM", "SIGINT"]) {
2229
+ return registerDisposeOnSignals(this, signals);
2230
+ }
2146
2231
  /** Adapters' `onDispose` in registration order; errors are collected, not fatal. */ async disposeAdapters() {
2147
2232
  const errors = [];
2148
2233
  for (const a of this.adapters) try {
@@ -2153,7 +2238,7 @@ function _define_property(obj, key, value) {
2153
2238
  method: "onDispose",
2154
2239
  error
2155
2240
  });
2156
- this.logger.warn(`[moost] adapter "${a.name}" onDispose failed: ${error instanceof Error ? error.message : String(error)}`);
2241
+ this.logger.warn(`[moost] adapter "${a.name}" onDispose failed: ${errorMessage(error)}`);
2157
2242
  }
2158
2243
  return errors;
2159
2244
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moost",
3
- "version": "0.6.36",
3
+ "version": "0.6.37",
4
4
  "description": "moost",
5
5
  "keywords": [
6
6
  "composables",
@@ -49,7 +49,7 @@
49
49
  "@wooksjs/event-http": "^0.7.23",
50
50
  "@wooksjs/http-body": "^0.7.23",
51
51
  "vitest": "3.2.7",
52
- "@moostjs/event-http": "^0.6.36"
52
+ "@moostjs/event-http": "^0.6.37"
53
53
  },
54
54
  "scripts": {
55
55
  "pub": "pnpm publish --access public",