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 +243 -2
- package/dist/index.d.ts +164 -2
- package/dist/index.mjs +241 -3
- package/package.json +6 -6
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.
|
|
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 = `[2m[35mmoost`;
|
|
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}[32m()${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(`[2m[35mmoost`);
|
|
@@ -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.
|
|
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 = `[2m[35mmoost`;
|
|
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}[32m()${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(`[2m[35mmoost`);
|
|
@@ -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.
|
|
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.
|
|
44
|
+
"@wooksjs/event-core": "^0.7.23",
|
|
45
45
|
"hookable": "^5.5.3",
|
|
46
|
-
"wooks": "^0.7.
|
|
46
|
+
"wooks": "^0.7.23"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@wooksjs/event-http": "^0.7.
|
|
50
|
-
"@wooksjs/http-body": "^0.7.
|
|
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.
|
|
52
|
+
"@moostjs/event-http": "^0.6.36"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"pub": "pnpm publish --access public",
|