moost 0.6.35 → 0.6.36

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.35",
51
+ version: "0.6.36",
52
52
  path: typeof __filename === "string" ? __filename : require("url").pathToFileURL(__filename).href
53
53
  });
54
54
  }
@@ -225,6 +225,18 @@ let loggingOptions = {
225
225
  /** Returns the shared Infact DI container used by Moost for dependency injection. */ function getMoostInfact() {
226
226
  return sharedMoostInfact;
227
227
  }
228
+ /**
229
+ * Every SINGLETON instance currently held by an Infact container's **global**
230
+ * registry (per-event `scopes` registries are excluded — those instances are
231
+ * torn down with their event).
232
+ *
233
+ * Infact keeps the registry as a protected symbol-keyed object; this helper
234
+ * centralizes the one cast needed to read it, so consumers
235
+ * (`Moost.dispose()`, the `@moostjs/vite` dev plugin) do not each re-derive it.
236
+ */ function getInfactSingletonInstances(infact = getMoostInfact()) {
237
+ const { registry } = infact;
238
+ return Object.getOwnPropertySymbols(registry).map((k) => registry[k]).filter(Boolean);
239
+ }
228
240
  const scopeVarsMap = /* @__PURE__ */ new Map();
229
241
  /**
230
242
  * Define global scope name to be used with `@InjectFromScope` and `@InjectScopeVars` decorators
@@ -640,6 +652,126 @@ function nextScopeId() {
640
652
  }
641
653
  }
642
654
 
655
+ //#endregion
656
+ //#region packages/moost/src/dispose.ts
657
+ /**
658
+ * Instances whose dispose hooks already ran. Module-level and weak, so
659
+ * disposal is at-most-once per instance no matter which entry point triggers
660
+ * it — `Moost.dispose()` twice, or a hot-reload eject followed by a shutdown,
661
+ * never re-runs a hook.
662
+ */ const disposedInstances = /* @__PURE__ */ new WeakSet();
663
+ const DISPOSE_BANNER = `moost`;
664
+ /**
665
+ * `Symbol.asyncDispose` / `Symbol.dispose` are only present on newer runtimes —
666
+ * resolve them once through a guarded read so older ones simply have no
667
+ * well-known fallback hook.
668
+ */ const asyncDisposeSymbol = typeof Symbol.asyncDispose === "symbol" ? Symbol.asyncDispose : void 0;
669
+ const disposeSymbol = typeof Symbol.dispose === "symbol" ? Symbol.dispose : void 0;
670
+ /** Class name of an instance, read through the prototype (never touches own props). */ function classNameOf(instance) {
671
+ return Object.getPrototypeOf(instance)?.constructor?.name || "Object";
672
+ }
673
+ function errorMessage(error) {
674
+ return error instanceof Error ? error.message : String(error);
675
+ }
676
+ /** `Class.method (message)` pairs for every failed hook. */ function formatDisposeErrors(errors) {
677
+ const list = errors.map((e) => `${classNameOf(e.instance)}.${String(e.method)} (${errorMessage(e.error)})`).join(", ");
678
+ return `[moost] ${errors.length} dispose hook(s) failed: ${list}`;
679
+ }
680
+ /**
681
+ * True when `sym` resolves to a data property holding a function anywhere on the
682
+ * instance/prototype chain. Descriptor based — accessors are never invoked.
683
+ */ function hasSymbolMethod(instance, sym) {
684
+ if (!sym) return false;
685
+ let obj = instance;
686
+ while (obj && obj !== Object.prototype) {
687
+ const desc = Object.getOwnPropertyDescriptor(obj, sym);
688
+ if (desc) return typeof desc.value === "function";
689
+ obj = Object.getPrototypeOf(obj);
690
+ }
691
+ return false;
692
+ }
693
+ /**
694
+ * Dispose hooks of one instance: every `@MoostDispose`-decorated method or —
695
+ * when the class decorates none — its `Symbol.asyncDispose` / `Symbol.dispose`
696
+ * method (preferring the async one) as a single priority-0 hook. The method
697
+ * scan goes through `getInstanceOwnMethods`, which classifies by descriptor and
698
+ * never evaluates getters.
699
+ */ function collectHooks(instance) {
700
+ const mate = getMoostMate();
701
+ const hooks = [];
702
+ for (const method of getInstanceOwnMethods(instance)) {
703
+ const priority = mate.read(instance, method)?.moostDispose?.priority;
704
+ if (typeof priority === "number") hooks.push({
705
+ instance,
706
+ method,
707
+ priority
708
+ });
709
+ }
710
+ if (hooks.length > 0) return hooks;
711
+ const sym = hasSymbolMethod(instance, asyncDisposeSymbol) ? asyncDisposeSymbol : hasSymbolMethod(instance, disposeSymbol) ? disposeSymbol : void 0;
712
+ return sym ? [{
713
+ instance,
714
+ method: sym,
715
+ priority: 0
716
+ }] : [];
717
+ }
718
+ async function runHook(hook, errors, opts) {
719
+ const fn = hook.instance[hook.method];
720
+ try {
721
+ await fn.call(hook.instance);
722
+ } catch (error) {
723
+ errors.push({
724
+ instance: hook.instance,
725
+ method: hook.method,
726
+ error
727
+ });
728
+ if (opts?.onError !== "throw") (opts?.logger || getDefaultLogger(DISPOSE_BANNER)).warn(`[moost] dispose hook ${classNameOf(hook.instance)}.${String(hook.method)} failed: ${errorMessage(error)}`);
729
+ }
730
+ }
731
+ /**
732
+ * Runs the dispose hooks of every given instance, once per instance, ever.
733
+ *
734
+ * A hook is either a `@MoostDispose()`-decorated method or, when the class
735
+ * decorates none, the instance's `[Symbol.asyncDispose]()` / `[Symbol.dispose]()`
736
+ * method. Hooks run **sequentially** in ascending `priority` (ties keep input
737
+ * order) and each one is awaited, so teardown that must happen in order (drain
738
+ * consumers, then close the connection they use) can be expressed with
739
+ * priorities.
740
+ *
741
+ * Failures never stop the run: with `onError: 'warn'` (default) each one is
742
+ * logged and collected; with `onError: 'throw'` every hook still runs and one
743
+ * `AggregateError` is thrown at the end.
744
+ *
745
+ * Instances already disposed (by an earlier call, or by `Moost.dispose()`) are
746
+ * skipped, so passing overlapping sets is safe.
747
+ */ async function disposeInstances(instances, opts) {
748
+ const hooks = [];
749
+ const disposed = [];
750
+ for (const instance of instances) {
751
+ if (!instance || disposedInstances.has(instance)) continue;
752
+ disposedInstances.add(instance);
753
+ const own = collectHooks(instance);
754
+ if (own.length > 0) {
755
+ disposed.push(instance);
756
+ hooks.push(...own);
757
+ }
758
+ }
759
+ const errors = [];
760
+ for (const hook of hooks.toSorted((a, b) => a.priority - b.priority)) await runHook(hook, errors, opts);
761
+ if (opts?.onError === "throw" && errors.length > 0) throw new AggregateError(errors.map((e) => e.error), formatDisposeErrors(errors));
762
+ return {
763
+ hooks: hooks.length,
764
+ errors,
765
+ disposed
766
+ };
767
+ }
768
+ /**
769
+ * @internal Renders the aggregate message used when disposal failures are
770
+ * rethrown (`Moost.dispose()` and `disposeInstances(..., { onError: 'throw' })`).
771
+ */ function describeDisposeErrors(errors) {
772
+ return formatDisposeErrors(errors);
773
+ }
774
+
643
775
  //#endregion
644
776
  //#region packages/moost/src/decorators/circular.decorator.ts
645
777
  /**
@@ -767,6 +899,56 @@ function ImportController(prefix, controller, provide) {
767
899
  }, true);
768
900
  }
769
901
 
902
+ //#endregion
903
+ //#region packages/moost/src/decorators/dispose.decorator.ts
904
+ /**
905
+ * ## MoostDispose
906
+ * ### @Decorator
907
+ * Marks a method to run **once**, when its instance is **disposed** — either by
908
+ * `Moost.dispose()` (graceful shutdown) or by the `@moostjs/vite` dev server
909
+ * when the instance is ejected on hot reload. It is the teardown counterpart of
910
+ * {@link MoostInit}: release what the singleton owns (a queue consumer, a cache
911
+ * client, a database handle, timers, watchers) so a replaced instance does not
912
+ * leak it.
913
+ *
914
+ * Works on SINGLETON controllers **and** on any `@Injectable()` singleton — the
915
+ * hooks are discovered from live instances at dispose time, so a provider that
916
+ * owns a connection is covered without being a controller.
917
+ *
918
+ * The method takes **no arguments** (dispose tears down an existing instance —
919
+ * inject whatever it needs in the constructor). Async methods are awaited.
920
+ * Errors are collected, not fatal: every other hook still runs, `Moost.dispose()`
921
+ * then rejects with an `AggregateError`, and a hot reload only warns.
922
+ *
923
+ * An instance is disposed at most once (tracked per instance), so a second
924
+ * `dispose()` — or an eject followed by a shutdown — never re-runs a hook.
925
+ *
926
+ * When a class defines no `@MoostDispose` method but exposes
927
+ * `[Symbol.asyncDispose]()` / `[Symbol.dispose]()`, that is used instead.
928
+ *
929
+ * Applying `@MoostDispose` to a `FOR_EVENT` **controller** is a configuration
930
+ * error and throws at bind time (per-event instances are torn down with their
931
+ * event — put the hook on the singleton that owns the resource). On a
932
+ * `FOR_EVENT` **injectable** the hook is simply never invoked.
933
+ *
934
+ * @param opts.priority lower runs first across all `@MoostDispose` methods (default 0)
935
+ *
936
+ * @example
937
+ * ```ts
938
+ * │ @Injectable()
939
+ * │ export class CacheClient {
940
+ * │ private readonly client = createCacheClient()
941
+ * │
942
+ * │ @MoostDispose()
943
+ * │ async close() {
944
+ * │ await this.client.quit()
945
+ * │ }
946
+ * │ }
947
+ * ```
948
+ */ function MoostDispose(opts) {
949
+ return getMoostMate().decorate("moostDispose", { priority: opts?.priority ?? 0 }, false);
950
+ }
951
+
770
952
  //#endregion
771
953
  //#region packages/moost/src/decorators/resolve.decorator.ts
772
954
  /**
@@ -1612,6 +1794,7 @@ async function bindControllerMethods(options) {
1612
1794
  className: classConstructor.name,
1613
1795
  methodName: method
1614
1796
  }));
1797
+ if (methodMeta.moostDispose && meta.injectable === "FOR_EVENT") throw new Error(`@MoostDispose is not allowed on a FOR_EVENT controller (${classConstructor.name}.${String(method)}). Dispose hooks run on the SINGLETON instance; FOR_EVENT instances are torn down with their event — put the hook on the singleton that owns the resource.`);
1615
1798
  if (methodMeta.moostInit) {
1616
1799
  if (meta.injectable === "FOR_EVENT") throw new Error(`@MoostInit is not allowed on a FOR_EVENT controller (${classConstructor.name}.${String(method)}). Init hooks run once at boot on the SINGLETON instance; FOR_EVENT controllers have no init-time instance.`);
1617
1800
  const initArgsPipes = (methodMeta.params || []).map((p) => ({
@@ -1932,6 +2115,60 @@ function _define_property(obj, key, value) {
1932
2115
  });
1933
2116
  }
1934
2117
  /**
2118
+ * ### dispose
2119
+ * Graceful shutdown: stops the adapters, then runs every `@MoostDispose`
2120
+ * hook of every singleton this app can reach — DI registry singletons
2121
+ * (`@Injectable()` providers included) plus the controller instances bound at
2122
+ * boot. A class with no decorated hook but an `[Symbol.asyncDispose]()` /
2123
+ * `[Symbol.dispose]()` method is disposed through that.
2124
+ *
2125
+ * Order: every adapter's `onDispose` in registration order (stop taking new
2126
+ * work first), then instance hooks by ascending `priority`. Everything is
2127
+ * awaited, failures never stop the run — once all hooks have run, a failed
2128
+ * disposal rejects the returned promise with an `AggregateError` naming each
2129
+ * failing `Class.method`.
2130
+ *
2131
+ * Idempotent: the promise of the first call is returned for every later one,
2132
+ * and an instance already disposed (e.g. ejected by the `@moostjs/vite` dev
2133
+ * server) is skipped.
2134
+ *
2135
+ * DI registries and metadata caches are left intact — use the existing
2136
+ * cleanup helpers when a clean container is what you need.
2137
+ *
2138
+ * ```ts
2139
+ * for (const signal of ['SIGTERM', 'SIGINT'] as const) {
2140
+ * process.once(signal, () => { void app.dispose().finally(() => process.exit(0)) })
2141
+ * }
2142
+ * ```
2143
+ */ dispose() {
2144
+ if (!this.disposePromise) this.disposePromise = this.runDispose();
2145
+ return this.disposePromise;
2146
+ }
2147
+ /** Adapters' `onDispose` in registration order; errors are collected, not fatal. */ async disposeAdapters() {
2148
+ const errors = [];
2149
+ for (const a of this.adapters) try {
2150
+ await a.onDispose?.(this);
2151
+ } catch (error) {
2152
+ errors.push({
2153
+ instance: a,
2154
+ method: "onDispose",
2155
+ error
2156
+ });
2157
+ this.logger.warn(`[moost] adapter "${a.name}" onDispose failed: ${error instanceof Error ? error.message : String(error)}`);
2158
+ }
2159
+ return errors;
2160
+ }
2161
+ /** The one-shot body behind {@link Moost.dispose} (see its docs for the contract). */ async runDispose() {
2162
+ const errors = await this.disposeAdapters();
2163
+ const result = await disposeInstances([...getInfactSingletonInstances(), ...this.singletonInstances], {
2164
+ logger: this.logger,
2165
+ onError: "warn"
2166
+ });
2167
+ errors.push(...result.errors);
2168
+ this.logger.debug(`[moost] disposed: ${this.adapters.length} adapter(s), ${result.hooks} hook(s), ${errors.length} error(s)`);
2169
+ if (errors.length > 0) throw new AggregateError(errors.map((e) => e.error), describeDisposeErrors(errors));
2170
+ }
2171
+ /**
1935
2172
  * D1 audit of a DI-instantiated controller class' constructor params.
1936
2173
  * Findings are collected for `init()` to flush. Returns `true` when the
1937
2174
  * bind-time SINGLETON instantiation must be skipped: in `'error'` mode a
@@ -1990,6 +2227,7 @@ function _define_property(obj, key, value) {
1990
2227
  instance = controller;
1991
2228
  infact.setInstanceRegistries(instance, provide, replace, { pipes });
1992
2229
  }
2230
+ if (instance) this.singletonInstances.add(instance);
1993
2231
  const getInstance = instance ? () => instance : async () => await infact.get(controller, { ...infactOpts });
1994
2232
  const classConstructor = (0, _prostojs_mate.isConstructor)(controller) ? controller : (0, _prostojs_mate.getConstructor)(controller);
1995
2233
  const controllerOverview = await bindControllerMethods({
@@ -2165,7 +2403,7 @@ function _define_property(obj, key, value) {
2165
2403
  this.logger.info(`${prefix || ""}${c}${eventName} ${"\x1B[0m\x1B[2m\x1B[32m" + c}→ ${classConstructor.name}.${"\x1B[36m" + c}${method}()${coff}`);
2166
2404
  }
2167
2405
  constructor(options) {
2168
- super(), _define_property(this, "options", void 0), _define_property(this, "logger", void 0), _define_property(this, "pipes", void 0), _define_property(this, "interceptors", void 0), _define_property(this, "adapters", void 0), _define_property(this, "controllersOverview", void 0), _define_property(this, "handlerOverviewIndex", void 0), _define_property(this, "initHooks", void 0), _define_property(this, "provide", void 0), _define_property(this, "replace", void 0), _define_property(this, "unregisteredControllers", void 0), _define_property(this, "paramAuditFindings", void 0), _define_property(this, "paramAuditMode", void 0), _define_property(this, "inheritanceAuditMode", void 0), _define_property(this, "initialized", void 0), _define_property(this, "globalInterceptorHandler", void 0), this.options = options, this.pipes = Array.from(sharedPipes), this.interceptors = [], this.adapters = [], this.controllersOverview = [], this.initHooks = [], this.provide = (0, _prostojs_infact.createProvideRegistry)([_prostojs_infact.Infact, getMoostInfact], [_prostojs_mate.Mate, getMoostMate]), this.replace = {}, this.unregisteredControllers = [], this.paramAuditFindings = [], this.initialized = false;
2406
+ super(), _define_property(this, "options", void 0), _define_property(this, "logger", void 0), _define_property(this, "pipes", void 0), _define_property(this, "interceptors", void 0), _define_property(this, "adapters", void 0), _define_property(this, "controllersOverview", void 0), _define_property(this, "handlerOverviewIndex", void 0), _define_property(this, "initHooks", void 0), _define_property(this, "provide", void 0), _define_property(this, "replace", void 0), _define_property(this, "unregisteredControllers", void 0), _define_property(this, "paramAuditFindings", void 0), _define_property(this, "paramAuditMode", void 0), _define_property(this, "inheritanceAuditMode", void 0), _define_property(this, "initialized", void 0), _define_property(this, "singletonInstances", void 0), _define_property(this, "disposePromise", void 0), _define_property(this, "globalInterceptorHandler", void 0), this.options = options, this.pipes = Array.from(sharedPipes), this.interceptors = [], this.adapters = [], this.controllersOverview = [], this.initHooks = [], this.provide = (0, _prostojs_infact.createProvideRegistry)([_prostojs_infact.Infact, getMoostInfact], [_prostojs_mate.Mate, getMoostMate]), this.replace = {}, this.unregisteredControllers = [], this.paramAuditFindings = [], this.initialized = false, this.singletonInstances = /* @__PURE__ */ new Set();
2169
2407
  this.paramAuditMode = resolveParamAuditMode(options?.diagnostics?.paramTypes);
2170
2408
  this.inheritanceAuditMode = options?.diagnostics?.inheritance || "warn";
2171
2409
  this.logger = options?.logger || getDefaultLogger(`moost`);
@@ -2275,6 +2513,7 @@ exports.Label = Label;
2275
2513
  exports.LoggerTopic = LoggerTopic;
2276
2514
  exports.MOOST_MODULE_IDENTITY_KEY = MOOST_MODULE_IDENTITY_KEY;
2277
2515
  exports.Moost = Moost;
2516
+ exports.MoostDispose = MoostDispose;
2278
2517
  exports.MoostInit = MoostInit;
2279
2518
  exports.OnError = OnError;
2280
2519
  exports.Optional = Optional;
@@ -2340,6 +2579,7 @@ exports.defineInfactScope = defineInfactScope;
2340
2579
  exports.defineInterceptor = defineInterceptor;
2341
2580
  exports.defineMoostEventHandler = defineMoostEventHandler;
2342
2581
  exports.definePipeFn = definePipeFn;
2582
+ exports.disposeInstances = disposeInstances;
2343
2583
  Object.defineProperty(exports, 'eventTypeKey', {
2344
2584
  enumerable: true,
2345
2585
  get: function () {
@@ -2366,6 +2606,7 @@ Object.defineProperty(exports, 'getGlobalWooks', {
2366
2606
  });
2367
2607
  exports.getHandlerPaths = getHandlerPaths;
2368
2608
  exports.getInfactScopeVars = getInfactScopeVars;
2609
+ exports.getInfactSingletonInstances = getInfactSingletonInstances;
2369
2610
  exports.getInstanceOwnMethods = getInstanceOwnMethods;
2370
2611
  exports.getInstanceOwnProps = getInstanceOwnProps;
2371
2612
  exports.getMoostInfact = getMoostInfact;
package/dist/index.d.ts CHANGED
@@ -168,6 +168,55 @@ declare function ImportController(controller: TFunction | TObject, provide?: TPr
168
168
  */
169
169
  declare function ImportController(prefix: string, controller: TFunction | TObject, provide?: TProvideRegistry): ClassDecorator;
170
170
 
171
+ /**
172
+ * ## MoostDispose
173
+ * ### @Decorator
174
+ * Marks a method to run **once**, when its instance is **disposed** — either by
175
+ * `Moost.dispose()` (graceful shutdown) or by the `@moostjs/vite` dev server
176
+ * when the instance is ejected on hot reload. It is the teardown counterpart of
177
+ * {@link MoostInit}: release what the singleton owns (a queue consumer, a cache
178
+ * client, a database handle, timers, watchers) so a replaced instance does not
179
+ * leak it.
180
+ *
181
+ * Works on SINGLETON controllers **and** on any `@Injectable()` singleton — the
182
+ * hooks are discovered from live instances at dispose time, so a provider that
183
+ * owns a connection is covered without being a controller.
184
+ *
185
+ * The method takes **no arguments** (dispose tears down an existing instance —
186
+ * inject whatever it needs in the constructor). Async methods are awaited.
187
+ * Errors are collected, not fatal: every other hook still runs, `Moost.dispose()`
188
+ * then rejects with an `AggregateError`, and a hot reload only warns.
189
+ *
190
+ * An instance is disposed at most once (tracked per instance), so a second
191
+ * `dispose()` — or an eject followed by a shutdown — never re-runs a hook.
192
+ *
193
+ * When a class defines no `@MoostDispose` method but exposes
194
+ * `[Symbol.asyncDispose]()` / `[Symbol.dispose]()`, that is used instead.
195
+ *
196
+ * Applying `@MoostDispose` to a `FOR_EVENT` **controller** is a configuration
197
+ * error and throws at bind time (per-event instances are torn down with their
198
+ * event — put the hook on the singleton that owns the resource). On a
199
+ * `FOR_EVENT` **injectable** the hook is simply never invoked.
200
+ *
201
+ * @param opts.priority lower runs first across all `@MoostDispose` methods (default 0)
202
+ *
203
+ * @example
204
+ * ```ts
205
+ * │ @Injectable()
206
+ * │ export class CacheClient {
207
+ * │ private readonly client = createCacheClient()
208
+ * │
209
+ * │ @MoostDispose()
210
+ * │ async close() {
211
+ * │ await this.client.quit()
212
+ * │ }
213
+ * │ }
214
+ * ```
215
+ */
216
+ declare function MoostDispose(opts?: {
217
+ priority?: number;
218
+ }): MethodDecorator;
219
+
171
220
  type TDecoratorLevel = 'CLASS' | 'METHOD' | 'PROP' | 'PARAM';
172
221
 
173
222
  /** Metadata context passed to pipe functions during argument/property resolution. */
@@ -311,6 +360,14 @@ interface TMoostMetadata<H extends TObject = TEmpty> extends TCommonMetaFields,
311
360
  moostInit?: {
312
361
  priority: number;
313
362
  };
363
+ /**
364
+ * Set by `@MoostDispose()` — marks a method to run when the instance is
365
+ * disposed (`Moost.dispose()`, or a hot-reload eject in `@moostjs/vite`).
366
+ * `priority` orders hooks (ascending) across all instances.
367
+ */
368
+ moostDispose?: {
369
+ priority: number;
370
+ };
314
371
  params: (TMateParamMeta & TMoostParamsMetadata)[];
315
372
  }
316
373
  /** Metadata attached to constructor/method parameters by Moost decorators. */
@@ -365,6 +422,16 @@ interface TInfactLoggingOptions {
365
422
  declare function setInfactLoggingOptions(options: TInfactLoggingOptions): void;
366
423
  /** Returns the shared Infact DI container used by Moost for dependency injection. */
367
424
  declare function getMoostInfact(): Infact<TMoostMetadata<TEmpty>, TMoostMetadata<TEmpty>, TMoostParamsMetadata, TCustom>;
425
+ /**
426
+ * Every SINGLETON instance currently held by an Infact container's **global**
427
+ * registry (per-event `scopes` registries are excluded — those instances are
428
+ * torn down with their event).
429
+ *
430
+ * Infact keeps the registry as a protected symbol-keyed object; this helper
431
+ * centralizes the one cast needed to read it, so consumers
432
+ * (`Moost.dispose()`, the `@moostjs/vite` dev plugin) do not each re-derive it.
433
+ */
434
+ declare function getInfactSingletonInstances(infact?: ReturnType<typeof getMoostInfact>): object[];
368
435
  interface TCustom {
369
436
  pipes?: TPipeData[];
370
437
  }
@@ -417,6 +484,53 @@ interface TInitHook {
417
484
  /** Mode for the bind-time inheritance audit (see `TMoostOptions['diagnostics']`). */
418
485
  type TInheritanceAuditMode = 'warn' | 'off';
419
486
 
487
+ /** One failed dispose hook: the instance, the method that threw, and the thrown value. */
488
+ interface TDisposeError {
489
+ instance: object;
490
+ method: string | symbol;
491
+ error: unknown;
492
+ }
493
+ /** Outcome of {@link disposeInstances}: how many hooks ran, and which of them failed. */
494
+ interface TDisposeResult {
495
+ /** Number of hooks invoked (a class may declare several). */
496
+ hooks: number;
497
+ /** Failed hooks, in run order. */
498
+ errors: TDisposeError[];
499
+ /**
500
+ * Instances that had at least one hook invoked — i.e. the ones actually torn
501
+ * down, as opposed to the ones passed in that declare no hook at all.
502
+ */
503
+ disposed: object[];
504
+ }
505
+ /** Options for {@link disposeInstances}. */
506
+ interface TDisposeOptions {
507
+ /** Logger for `onError: 'warn'` diagnostics; defaults to the Moost default logger. */
508
+ logger?: TConsoleBase;
509
+ /**
510
+ * `'warn'` (default) — log every failure and keep going;
511
+ * `'throw'` — still run every hook, then throw one `AggregateError`.
512
+ */
513
+ onError?: 'warn' | 'throw';
514
+ }
515
+ /**
516
+ * Runs the dispose hooks of every given instance, once per instance, ever.
517
+ *
518
+ * A hook is either a `@MoostDispose()`-decorated method or, when the class
519
+ * decorates none, the instance's `[Symbol.asyncDispose]()` / `[Symbol.dispose]()`
520
+ * method. Hooks run **sequentially** in ascending `priority` (ties keep input
521
+ * order) and each one is awaited, so teardown that must happen in order (drain
522
+ * consumers, then close the connection they use) can be expressed with
523
+ * priorities.
524
+ *
525
+ * Failures never stop the run: with `onError: 'warn'` (default) each one is
526
+ * logged and collected; with `onError: 'throw'` every hook still runs and one
527
+ * `AggregateError` is thrown at the end.
528
+ *
529
+ * Instances already disposed (by an earlier call, or by `Moost.dispose()`) are
530
+ * skipped, so passing overlapping sets is safe.
531
+ */
532
+ declare function disposeInstances(instances: Iterable<object>, opts?: TDisposeOptions): Promise<TDisposeResult>;
533
+
420
534
  interface TMoostOptions {
421
535
  /**
422
536
  * Global path prefix — mounts the whole app under one path segment.
@@ -580,6 +694,15 @@ declare class Moost extends Hookable {
580
694
  protected readonly inheritanceAuditMode: TInheritanceAuditMode;
581
695
  /** D5: set once `init()` completes — late provide/replace registrations then warn. */
582
696
  protected initialized: boolean;
697
+ /**
698
+ * SINGLETON controller instances this app obtained at bind time (DI-resolved
699
+ * or registered as objects). They are disposed together with the DI registry
700
+ * singletons — an object-registered controller never enters the Infact
701
+ * registry, so it would otherwise be missed.
702
+ */
703
+ protected singletonInstances: Set<object>;
704
+ /** In-flight/settled `dispose()` run — makes disposal idempotent. */
705
+ protected disposePromise?: Promise<void>;
583
706
  constructor(options?: TMoostOptions | undefined);
584
707
  _fireEventStart(source: TMoostAdapter<unknown>): void;
585
708
  _fireEventEnd(source: TMoostAdapter<unknown>): void;
@@ -617,6 +740,38 @@ declare class Moost extends Hookable {
617
740
  * A throwing hook rejects `init()` (fail-fast).
618
741
  */
619
742
  protected runInitHooks(): Promise<void>;
743
+ /**
744
+ * ### dispose
745
+ * Graceful shutdown: stops the adapters, then runs every `@MoostDispose`
746
+ * hook of every singleton this app can reach — DI registry singletons
747
+ * (`@Injectable()` providers included) plus the controller instances bound at
748
+ * boot. A class with no decorated hook but an `[Symbol.asyncDispose]()` /
749
+ * `[Symbol.dispose]()` method is disposed through that.
750
+ *
751
+ * Order: every adapter's `onDispose` in registration order (stop taking new
752
+ * work first), then instance hooks by ascending `priority`. Everything is
753
+ * awaited, failures never stop the run — once all hooks have run, a failed
754
+ * disposal rejects the returned promise with an `AggregateError` naming each
755
+ * failing `Class.method`.
756
+ *
757
+ * Idempotent: the promise of the first call is returned for every later one,
758
+ * and an instance already disposed (e.g. ejected by the `@moostjs/vite` dev
759
+ * server) is skipped.
760
+ *
761
+ * DI registries and metadata caches are left intact — use the existing
762
+ * cleanup helpers when a clean container is what you need.
763
+ *
764
+ * ```ts
765
+ * for (const signal of ['SIGTERM', 'SIGINT'] as const) {
766
+ * process.once(signal, () => { void app.dispose().finally(() => process.exit(0)) })
767
+ * }
768
+ * ```
769
+ */
770
+ dispose(): Promise<void>;
771
+ /** Adapters' `onDispose` in registration order; errors are collected, not fatal. */
772
+ protected disposeAdapters(): Promise<TDisposeError[]>;
773
+ /** The one-shot body behind {@link Moost.dispose} (see its docs for the contract). */
774
+ protected runDispose(): Promise<void>;
620
775
  /**
621
776
  * D1 audit of a DI-instantiated controller class' constructor params.
622
777
  * Findings are collected for `init()` to flush. Returns `true` when the
@@ -719,6 +874,13 @@ interface TMoostAdapter<H> {
719
874
  name: string;
720
875
  bindHandler: <T extends TObject = TObject>(options: TMoostAdapterOptions<H, T>) => void | Promise<void>;
721
876
  onInit?: (moost: Moost) => void | Promise<void>;
877
+ /**
878
+ * Called by {@link Moost.dispose} **before** any `@MoostDispose` instance
879
+ * hook, in adapter registration order: stop taking new work here (close the
880
+ * server, stop the consumer/engine). Awaited; a throwing `onDispose` is
881
+ * logged and collected, never aborts the rest of the shutdown.
882
+ */
883
+ onDispose?: (moost: Moost) => void | Promise<void>;
722
884
  getProvideRegistry?: () => TProvideRegistry;
723
885
  }
724
886
 
@@ -1270,5 +1432,5 @@ declare function createLogger(opts?: Partial<TProstoLoggerOptions>): ProstoLogge
1270
1432
  /** Default colored console transport used by Moost loggers. */
1271
1433
  declare const loggerConsoleTransport: _prostojs_logger.TProstoLoggerTransportFn<any>;
1272
1434
 
1273
- export { After, ApplyDecorators, Before, Circular, Const, ConstFactory, Controller, Description, HandlerPaths, Id, ImportController, Inherit, Inject, InjectEventLogger, InjectFromScope, InjectMoost, InjectMoostLogger, InjectScopeVars, Injectable, Intercept, Interceptor, InterceptorHandler, Label, LoggerTopic, MOOST_MODULE_IDENTITY_KEY, Moost, MoostInit, OnError, Optional, Overtake, Param, Params, Pipe, Provide, Replace, Required, Resolve, Response, TInterceptorPriority, TPipePriority, Value, createLogger, defineAfterInterceptor, defineBeforeInterceptor, defineErrorInterceptor, defineInfactScope, defineInterceptor, defineMoostEventHandler, definePipeFn, getHandlerPaths, getInfactScopeVars, getInstanceOwnMethods, getInstanceOwnProps, getMoostInfact, getMoostMate, getNewMoostInfact, globalKey, isThenable, loggerConsoleTransport, mergeSorted, registerEventScope, resolvePipe, setControllerContext, setInfactLoggingOptions, setInterceptResult, setOvertake, stampModuleIdentity, stampOnce, useControllerContext, useHandlerPaths, useInterceptResult, useOvertake, useScopeId };
1274
- export type { TAny, TAnyFn, TClassConstructor, TContextInjectorHook, TControllerOverview, TControllerRegistration, TControllersGroup, TEmpty, TFunction, TGetHandlerPathsOptions, TInjectableScope, TInterceptorAfterFn, TInterceptorBeforeFn, TInterceptorData, TInterceptorDef, TInterceptorDefFactory, TInterceptorEntry, TInterceptorErrorFn, TLogger, TMoostAdapter, TMoostAdapterOptions, TMoostEventHandlerHookOptions, TMoostEventHandlerOptions, TMoostHandler, TMoostMetadata, TMoostModuleIdentity, TMoostOptions, TMoostParamsMetadata, TObject, TOvertakeFn, TPipeData, TPipeFn, TPipeMetas, TPrimitives };
1435
+ export { After, ApplyDecorators, Before, Circular, Const, ConstFactory, Controller, Description, HandlerPaths, Id, ImportController, Inherit, Inject, InjectEventLogger, InjectFromScope, InjectMoost, InjectMoostLogger, InjectScopeVars, Injectable, Intercept, Interceptor, InterceptorHandler, Label, LoggerTopic, MOOST_MODULE_IDENTITY_KEY, Moost, MoostDispose, MoostInit, OnError, Optional, Overtake, Param, Params, Pipe, Provide, Replace, Required, Resolve, Response, TInterceptorPriority, TPipePriority, Value, createLogger, defineAfterInterceptor, defineBeforeInterceptor, defineErrorInterceptor, defineInfactScope, defineInterceptor, defineMoostEventHandler, definePipeFn, disposeInstances, getHandlerPaths, getInfactScopeVars, getInfactSingletonInstances, getInstanceOwnMethods, getInstanceOwnProps, getMoostInfact, getMoostMate, getNewMoostInfact, globalKey, isThenable, loggerConsoleTransport, mergeSorted, registerEventScope, resolvePipe, setControllerContext, setInfactLoggingOptions, setInterceptResult, setOvertake, stampModuleIdentity, stampOnce, useControllerContext, useHandlerPaths, useInterceptResult, useOvertake, useScopeId };
1436
+ export type { TAny, TAnyFn, TClassConstructor, TContextInjectorHook, TControllerOverview, TControllerRegistration, TControllersGroup, TDisposeError, TDisposeOptions, TDisposeResult, TEmpty, TFunction, TGetHandlerPathsOptions, TInjectableScope, TInterceptorAfterFn, TInterceptorBeforeFn, TInterceptorData, TInterceptorDef, TInterceptorDefFactory, TInterceptorEntry, TInterceptorErrorFn, TLogger, TMoostAdapter, TMoostAdapterOptions, TMoostEventHandlerHookOptions, TMoostEventHandlerOptions, TMoostHandler, TMoostMetadata, TMoostModuleIdentity, TMoostOptions, TMoostParamsMetadata, TObject, TOvertakeFn, TPipeData, TPipeFn, TPipeMetas, TPrimitives };
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.35",
50
+ version: "0.6.36",
51
51
  path: typeof __filename === "string" ? __filename : import.meta.url
52
52
  });
53
53
  }
@@ -224,6 +224,18 @@ let loggingOptions = {
224
224
  /** Returns the shared Infact DI container used by Moost for dependency injection. */ function getMoostInfact() {
225
225
  return sharedMoostInfact;
226
226
  }
227
+ /**
228
+ * Every SINGLETON instance currently held by an Infact container's **global**
229
+ * registry (per-event `scopes` registries are excluded — those instances are
230
+ * torn down with their event).
231
+ *
232
+ * Infact keeps the registry as a protected symbol-keyed object; this helper
233
+ * centralizes the one cast needed to read it, so consumers
234
+ * (`Moost.dispose()`, the `@moostjs/vite` dev plugin) do not each re-derive it.
235
+ */ function getInfactSingletonInstances(infact = getMoostInfact()) {
236
+ const { registry } = infact;
237
+ return Object.getOwnPropertySymbols(registry).map((k) => registry[k]).filter(Boolean);
238
+ }
227
239
  const scopeVarsMap = /* @__PURE__ */ new Map();
228
240
  /**
229
241
  * Define global scope name to be used with `@InjectFromScope` and `@InjectScopeVars` decorators
@@ -639,6 +651,126 @@ function nextScopeId() {
639
651
  }
640
652
  }
641
653
 
654
+ //#endregion
655
+ //#region packages/moost/src/dispose.ts
656
+ /**
657
+ * Instances whose dispose hooks already ran. Module-level and weak, so
658
+ * disposal is at-most-once per instance no matter which entry point triggers
659
+ * it — `Moost.dispose()` twice, or a hot-reload eject followed by a shutdown,
660
+ * never re-runs a hook.
661
+ */ const disposedInstances = /* @__PURE__ */ new WeakSet();
662
+ const DISPOSE_BANNER = `moost`;
663
+ /**
664
+ * `Symbol.asyncDispose` / `Symbol.dispose` are only present on newer runtimes —
665
+ * resolve them once through a guarded read so older ones simply have no
666
+ * well-known fallback hook.
667
+ */ const asyncDisposeSymbol = typeof Symbol.asyncDispose === "symbol" ? Symbol.asyncDispose : void 0;
668
+ const disposeSymbol = typeof Symbol.dispose === "symbol" ? Symbol.dispose : void 0;
669
+ /** Class name of an instance, read through the prototype (never touches own props). */ function classNameOf(instance) {
670
+ return Object.getPrototypeOf(instance)?.constructor?.name || "Object";
671
+ }
672
+ function errorMessage(error) {
673
+ return error instanceof Error ? error.message : String(error);
674
+ }
675
+ /** `Class.method (message)` pairs for every failed hook. */ function formatDisposeErrors(errors) {
676
+ const list = errors.map((e) => `${classNameOf(e.instance)}.${String(e.method)} (${errorMessage(e.error)})`).join(", ");
677
+ return `[moost] ${errors.length} dispose hook(s) failed: ${list}`;
678
+ }
679
+ /**
680
+ * True when `sym` resolves to a data property holding a function anywhere on the
681
+ * instance/prototype chain. Descriptor based — accessors are never invoked.
682
+ */ function hasSymbolMethod(instance, sym) {
683
+ if (!sym) return false;
684
+ let obj = instance;
685
+ while (obj && obj !== Object.prototype) {
686
+ const desc = Object.getOwnPropertyDescriptor(obj, sym);
687
+ if (desc) return typeof desc.value === "function";
688
+ obj = Object.getPrototypeOf(obj);
689
+ }
690
+ return false;
691
+ }
692
+ /**
693
+ * Dispose hooks of one instance: every `@MoostDispose`-decorated method or —
694
+ * when the class decorates none — its `Symbol.asyncDispose` / `Symbol.dispose`
695
+ * method (preferring the async one) as a single priority-0 hook. The method
696
+ * scan goes through `getInstanceOwnMethods`, which classifies by descriptor and
697
+ * never evaluates getters.
698
+ */ function collectHooks(instance) {
699
+ const mate = getMoostMate();
700
+ const hooks = [];
701
+ for (const method of getInstanceOwnMethods(instance)) {
702
+ const priority = mate.read(instance, method)?.moostDispose?.priority;
703
+ if (typeof priority === "number") hooks.push({
704
+ instance,
705
+ method,
706
+ priority
707
+ });
708
+ }
709
+ if (hooks.length > 0) return hooks;
710
+ const sym = hasSymbolMethod(instance, asyncDisposeSymbol) ? asyncDisposeSymbol : hasSymbolMethod(instance, disposeSymbol) ? disposeSymbol : void 0;
711
+ return sym ? [{
712
+ instance,
713
+ method: sym,
714
+ priority: 0
715
+ }] : [];
716
+ }
717
+ async function runHook(hook, errors, opts) {
718
+ const fn = hook.instance[hook.method];
719
+ try {
720
+ await fn.call(hook.instance);
721
+ } catch (error) {
722
+ errors.push({
723
+ instance: hook.instance,
724
+ method: hook.method,
725
+ error
726
+ });
727
+ if (opts?.onError !== "throw") (opts?.logger || getDefaultLogger(DISPOSE_BANNER)).warn(`[moost] dispose hook ${classNameOf(hook.instance)}.${String(hook.method)} failed: ${errorMessage(error)}`);
728
+ }
729
+ }
730
+ /**
731
+ * Runs the dispose hooks of every given instance, once per instance, ever.
732
+ *
733
+ * A hook is either a `@MoostDispose()`-decorated method or, when the class
734
+ * decorates none, the instance's `[Symbol.asyncDispose]()` / `[Symbol.dispose]()`
735
+ * method. Hooks run **sequentially** in ascending `priority` (ties keep input
736
+ * order) and each one is awaited, so teardown that must happen in order (drain
737
+ * consumers, then close the connection they use) can be expressed with
738
+ * priorities.
739
+ *
740
+ * Failures never stop the run: with `onError: 'warn'` (default) each one is
741
+ * logged and collected; with `onError: 'throw'` every hook still runs and one
742
+ * `AggregateError` is thrown at the end.
743
+ *
744
+ * Instances already disposed (by an earlier call, or by `Moost.dispose()`) are
745
+ * skipped, so passing overlapping sets is safe.
746
+ */ async function disposeInstances(instances, opts) {
747
+ const hooks = [];
748
+ const disposed = [];
749
+ for (const instance of instances) {
750
+ if (!instance || disposedInstances.has(instance)) continue;
751
+ disposedInstances.add(instance);
752
+ const own = collectHooks(instance);
753
+ if (own.length > 0) {
754
+ disposed.push(instance);
755
+ hooks.push(...own);
756
+ }
757
+ }
758
+ const errors = [];
759
+ for (const hook of hooks.toSorted((a, b) => a.priority - b.priority)) await runHook(hook, errors, opts);
760
+ if (opts?.onError === "throw" && errors.length > 0) throw new AggregateError(errors.map((e) => e.error), formatDisposeErrors(errors));
761
+ return {
762
+ hooks: hooks.length,
763
+ errors,
764
+ disposed
765
+ };
766
+ }
767
+ /**
768
+ * @internal Renders the aggregate message used when disposal failures are
769
+ * rethrown (`Moost.dispose()` and `disposeInstances(..., { onError: 'throw' })`).
770
+ */ function describeDisposeErrors(errors) {
771
+ return formatDisposeErrors(errors);
772
+ }
773
+
642
774
  //#endregion
643
775
  //#region packages/moost/src/decorators/circular.decorator.ts
644
776
  /**
@@ -766,6 +898,56 @@ function ImportController(prefix, controller, provide) {
766
898
  }, true);
767
899
  }
768
900
 
901
+ //#endregion
902
+ //#region packages/moost/src/decorators/dispose.decorator.ts
903
+ /**
904
+ * ## MoostDispose
905
+ * ### @Decorator
906
+ * Marks a method to run **once**, when its instance is **disposed** — either by
907
+ * `Moost.dispose()` (graceful shutdown) or by the `@moostjs/vite` dev server
908
+ * when the instance is ejected on hot reload. It is the teardown counterpart of
909
+ * {@link MoostInit}: release what the singleton owns (a queue consumer, a cache
910
+ * client, a database handle, timers, watchers) so a replaced instance does not
911
+ * leak it.
912
+ *
913
+ * Works on SINGLETON controllers **and** on any `@Injectable()` singleton — the
914
+ * hooks are discovered from live instances at dispose time, so a provider that
915
+ * owns a connection is covered without being a controller.
916
+ *
917
+ * The method takes **no arguments** (dispose tears down an existing instance —
918
+ * inject whatever it needs in the constructor). Async methods are awaited.
919
+ * Errors are collected, not fatal: every other hook still runs, `Moost.dispose()`
920
+ * then rejects with an `AggregateError`, and a hot reload only warns.
921
+ *
922
+ * An instance is disposed at most once (tracked per instance), so a second
923
+ * `dispose()` — or an eject followed by a shutdown — never re-runs a hook.
924
+ *
925
+ * When a class defines no `@MoostDispose` method but exposes
926
+ * `[Symbol.asyncDispose]()` / `[Symbol.dispose]()`, that is used instead.
927
+ *
928
+ * Applying `@MoostDispose` to a `FOR_EVENT` **controller** is a configuration
929
+ * error and throws at bind time (per-event instances are torn down with their
930
+ * event — put the hook on the singleton that owns the resource). On a
931
+ * `FOR_EVENT` **injectable** the hook is simply never invoked.
932
+ *
933
+ * @param opts.priority lower runs first across all `@MoostDispose` methods (default 0)
934
+ *
935
+ * @example
936
+ * ```ts
937
+ * │ @Injectable()
938
+ * │ export class CacheClient {
939
+ * │ private readonly client = createCacheClient()
940
+ * │
941
+ * │ @MoostDispose()
942
+ * │ async close() {
943
+ * │ await this.client.quit()
944
+ * │ }
945
+ * │ }
946
+ * ```
947
+ */ function MoostDispose(opts) {
948
+ return getMoostMate().decorate("moostDispose", { priority: opts?.priority ?? 0 }, false);
949
+ }
950
+
769
951
  //#endregion
770
952
  //#region packages/moost/src/decorators/resolve.decorator.ts
771
953
  /**
@@ -1611,6 +1793,7 @@ async function bindControllerMethods(options) {
1611
1793
  className: classConstructor.name,
1612
1794
  methodName: method
1613
1795
  }));
1796
+ if (methodMeta.moostDispose && meta.injectable === "FOR_EVENT") throw new Error(`@MoostDispose is not allowed on a FOR_EVENT controller (${classConstructor.name}.${String(method)}). Dispose hooks run on the SINGLETON instance; FOR_EVENT instances are torn down with their event — put the hook on the singleton that owns the resource.`);
1614
1797
  if (methodMeta.moostInit) {
1615
1798
  if (meta.injectable === "FOR_EVENT") throw new Error(`@MoostInit is not allowed on a FOR_EVENT controller (${classConstructor.name}.${String(method)}). Init hooks run once at boot on the SINGLETON instance; FOR_EVENT controllers have no init-time instance.`);
1616
1799
  const initArgsPipes = (methodMeta.params || []).map((p) => ({
@@ -1931,6 +2114,60 @@ function _define_property(obj, key, value) {
1931
2114
  });
1932
2115
  }
1933
2116
  /**
2117
+ * ### dispose
2118
+ * Graceful shutdown: stops the adapters, then runs every `@MoostDispose`
2119
+ * hook of every singleton this app can reach — DI registry singletons
2120
+ * (`@Injectable()` providers included) plus the controller instances bound at
2121
+ * boot. A class with no decorated hook but an `[Symbol.asyncDispose]()` /
2122
+ * `[Symbol.dispose]()` method is disposed through that.
2123
+ *
2124
+ * Order: every adapter's `onDispose` in registration order (stop taking new
2125
+ * work first), then instance hooks by ascending `priority`. Everything is
2126
+ * awaited, failures never stop the run — once all hooks have run, a failed
2127
+ * disposal rejects the returned promise with an `AggregateError` naming each
2128
+ * failing `Class.method`.
2129
+ *
2130
+ * Idempotent: the promise of the first call is returned for every later one,
2131
+ * and an instance already disposed (e.g. ejected by the `@moostjs/vite` dev
2132
+ * server) is skipped.
2133
+ *
2134
+ * DI registries and metadata caches are left intact — use the existing
2135
+ * cleanup helpers when a clean container is what you need.
2136
+ *
2137
+ * ```ts
2138
+ * for (const signal of ['SIGTERM', 'SIGINT'] as const) {
2139
+ * process.once(signal, () => { void app.dispose().finally(() => process.exit(0)) })
2140
+ * }
2141
+ * ```
2142
+ */ dispose() {
2143
+ if (!this.disposePromise) this.disposePromise = this.runDispose();
2144
+ return this.disposePromise;
2145
+ }
2146
+ /** Adapters' `onDispose` in registration order; errors are collected, not fatal. */ async disposeAdapters() {
2147
+ const errors = [];
2148
+ for (const a of this.adapters) try {
2149
+ await a.onDispose?.(this);
2150
+ } catch (error) {
2151
+ errors.push({
2152
+ instance: a,
2153
+ method: "onDispose",
2154
+ error
2155
+ });
2156
+ this.logger.warn(`[moost] adapter "${a.name}" onDispose failed: ${error instanceof Error ? error.message : String(error)}`);
2157
+ }
2158
+ return errors;
2159
+ }
2160
+ /** The one-shot body behind {@link Moost.dispose} (see its docs for the contract). */ async runDispose() {
2161
+ const errors = await this.disposeAdapters();
2162
+ const result = await disposeInstances([...getInfactSingletonInstances(), ...this.singletonInstances], {
2163
+ logger: this.logger,
2164
+ onError: "warn"
2165
+ });
2166
+ errors.push(...result.errors);
2167
+ this.logger.debug(`[moost] disposed: ${this.adapters.length} adapter(s), ${result.hooks} hook(s), ${errors.length} error(s)`);
2168
+ if (errors.length > 0) throw new AggregateError(errors.map((e) => e.error), describeDisposeErrors(errors));
2169
+ }
2170
+ /**
1934
2171
  * D1 audit of a DI-instantiated controller class' constructor params.
1935
2172
  * Findings are collected for `init()` to flush. Returns `true` when the
1936
2173
  * bind-time SINGLETON instantiation must be skipped: in `'error'` mode a
@@ -1989,6 +2226,7 @@ function _define_property(obj, key, value) {
1989
2226
  instance = controller;
1990
2227
  infact.setInstanceRegistries(instance, provide, replace, { pipes });
1991
2228
  }
2229
+ if (instance) this.singletonInstances.add(instance);
1992
2230
  const getInstance = instance ? () => instance : async () => await infact.get(controller, { ...infactOpts });
1993
2231
  const classConstructor = isConstructor$1(controller) ? controller : getConstructor$1(controller);
1994
2232
  const controllerOverview = await bindControllerMethods({
@@ -2164,7 +2402,7 @@ function _define_property(obj, key, value) {
2164
2402
  this.logger.info(`${prefix || ""}${c}${eventName} ${"\x1B[0m\x1B[2m\x1B[32m" + c}→ ${classConstructor.name}.${"\x1B[36m" + c}${method}()${coff}`);
2165
2403
  }
2166
2404
  constructor(options) {
2167
- super(), _define_property(this, "options", void 0), _define_property(this, "logger", void 0), _define_property(this, "pipes", void 0), _define_property(this, "interceptors", void 0), _define_property(this, "adapters", void 0), _define_property(this, "controllersOverview", void 0), _define_property(this, "handlerOverviewIndex", void 0), _define_property(this, "initHooks", void 0), _define_property(this, "provide", void 0), _define_property(this, "replace", void 0), _define_property(this, "unregisteredControllers", void 0), _define_property(this, "paramAuditFindings", void 0), _define_property(this, "paramAuditMode", void 0), _define_property(this, "inheritanceAuditMode", void 0), _define_property(this, "initialized", void 0), _define_property(this, "globalInterceptorHandler", void 0), this.options = options, this.pipes = Array.from(sharedPipes), this.interceptors = [], this.adapters = [], this.controllersOverview = [], this.initHooks = [], this.provide = createProvideRegistry$1([Infact, getMoostInfact], [Mate, getMoostMate]), this.replace = {}, this.unregisteredControllers = [], this.paramAuditFindings = [], this.initialized = false;
2405
+ super(), _define_property(this, "options", void 0), _define_property(this, "logger", void 0), _define_property(this, "pipes", void 0), _define_property(this, "interceptors", void 0), _define_property(this, "adapters", void 0), _define_property(this, "controllersOverview", void 0), _define_property(this, "handlerOverviewIndex", void 0), _define_property(this, "initHooks", void 0), _define_property(this, "provide", void 0), _define_property(this, "replace", void 0), _define_property(this, "unregisteredControllers", void 0), _define_property(this, "paramAuditFindings", void 0), _define_property(this, "paramAuditMode", void 0), _define_property(this, "inheritanceAuditMode", void 0), _define_property(this, "initialized", void 0), _define_property(this, "singletonInstances", void 0), _define_property(this, "disposePromise", void 0), _define_property(this, "globalInterceptorHandler", void 0), this.options = options, this.pipes = Array.from(sharedPipes), this.interceptors = [], this.adapters = [], this.controllersOverview = [], this.initHooks = [], this.provide = createProvideRegistry$1([Infact, getMoostInfact], [Mate, getMoostMate]), this.replace = {}, this.unregisteredControllers = [], this.paramAuditFindings = [], this.initialized = false, this.singletonInstances = /* @__PURE__ */ new Set();
2168
2406
  this.paramAuditMode = resolveParamAuditMode(options?.diagnostics?.paramTypes);
2169
2407
  this.inheritanceAuditMode = options?.diagnostics?.inheritance || "warn";
2170
2408
  this.logger = options?.logger || getDefaultLogger(`moost`);
@@ -2242,4 +2480,4 @@ function _define_property(obj, key, value) {
2242
2480
  }
2243
2481
 
2244
2482
  //#endregion
2245
- export { After, ApplyDecorators, Before, Circular, Const, ConstFactory, ContextInjector, Controller, Description, HandlerPaths, Id, ImportController, Inherit, Inject, InjectEventLogger, InjectFromScope, InjectMoost, InjectMoostLogger, InjectScopeVars, Injectable, Intercept, Interceptor, InterceptorHandler, Label, LoggerTopic, MOOST_MODULE_IDENTITY_KEY, Moost, MoostInit, OnError, Optional, Overtake, Param, Params, Pipe, ProstoLogger, Provide, Replace, Required, Resolve, Response, TInterceptorPriority, TPipePriority, Value, cached, clearGlobalWooks, createEventContext, createLogger, createProvideRegistry, createReplaceRegistry, current, defineAfterInterceptor, defineBeforeInterceptor, defineErrorInterceptor, defineInfactScope, defineInterceptor, defineMoostEventHandler, definePipeFn, eventTypeKey, getConstructor, getContextInjector, getGlobalWooks, getHandlerPaths, getInfactScopeVars, getInstanceOwnMethods, getInstanceOwnProps, getMoostInfact, getMoostMate, getNewMoostInfact, globalKey, isConstructor, isThenable, key, loggerConsoleTransport, mergeSorted, registerEventScope, replaceContextInjector, resetContextInjector, resolvePipe, run, setControllerContext, setInfactLoggingOptions, setInterceptResult, setOvertake, stampModuleIdentity, stampOnce, useControllerContext, useHandlerPaths, useInterceptResult, useLogger, useOvertake, useScopeId };
2483
+ export { After, ApplyDecorators, Before, Circular, Const, ConstFactory, ContextInjector, Controller, Description, HandlerPaths, Id, ImportController, Inherit, Inject, InjectEventLogger, InjectFromScope, InjectMoost, InjectMoostLogger, InjectScopeVars, Injectable, Intercept, Interceptor, InterceptorHandler, Label, LoggerTopic, MOOST_MODULE_IDENTITY_KEY, Moost, MoostDispose, MoostInit, OnError, Optional, Overtake, Param, Params, Pipe, ProstoLogger, Provide, Replace, Required, Resolve, Response, TInterceptorPriority, TPipePriority, Value, cached, clearGlobalWooks, createEventContext, createLogger, createProvideRegistry, createReplaceRegistry, current, defineAfterInterceptor, defineBeforeInterceptor, defineErrorInterceptor, defineInfactScope, defineInterceptor, defineMoostEventHandler, definePipeFn, disposeInstances, eventTypeKey, getConstructor, getContextInjector, getGlobalWooks, getHandlerPaths, getInfactScopeVars, getInfactSingletonInstances, getInstanceOwnMethods, getInstanceOwnProps, getMoostInfact, getMoostMate, getNewMoostInfact, globalKey, isConstructor, isThenable, key, loggerConsoleTransport, mergeSorted, registerEventScope, replaceContextInjector, resetContextInjector, resolvePipe, run, setControllerContext, setInfactLoggingOptions, setInterceptResult, setOvertake, stampModuleIdentity, stampOnce, useControllerContext, useHandlerPaths, useInterceptResult, useLogger, useOvertake, useScopeId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moost",
3
- "version": "0.6.35",
3
+ "version": "0.6.36",
4
4
  "description": "moost",
5
5
  "keywords": [
6
6
  "composables",
@@ -41,15 +41,15 @@
41
41
  "@prostojs/infact": "^0.5.0",
42
42
  "@prostojs/logger": "^0.4.3",
43
43
  "@prostojs/mate": "^0.5.0",
44
- "@wooksjs/event-core": "^0.7.22",
44
+ "@wooksjs/event-core": "^0.7.23",
45
45
  "hookable": "^5.5.3",
46
- "wooks": "^0.7.22"
46
+ "wooks": "^0.7.23"
47
47
  },
48
48
  "devDependencies": {
49
- "@wooksjs/event-http": "^0.7.22",
50
- "@wooksjs/http-body": "^0.7.22",
49
+ "@wooksjs/event-http": "^0.7.23",
50
+ "@wooksjs/http-body": "^0.7.23",
51
51
  "vitest": "3.2.7",
52
- "@moostjs/event-http": "^0.6.35"
52
+ "@moostjs/event-http": "^0.6.36"
53
53
  },
54
54
  "scripts": {
55
55
  "pub": "pnpm publish --access public",