moost 0.6.53 → 0.7.0

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,12 +48,24 @@ let stamped = false;
48
48
  if (stamped) return;
49
49
  stamped = true;
50
50
  stampModuleIdentity({
51
- version: "0.6.53",
51
+ version: "0.7.0",
52
52
  path: typeof __filename === "string" ? __filename : require("url").pathToFileURL(__filename).href
53
53
  });
54
54
  }
55
55
  stampOnce();
56
56
 
57
+ //#endregion
58
+ //#region packages/moost/src/announce.ts
59
+ /**
60
+ * @internal Tells the context injector (instrumentation, e.g. OpenTelemetry) which handler an
61
+ * event runs: `Handler:routed` with its `route` (when the adapter's router did not already),
62
+ * then `Controller:registered` once the controller context is set.
63
+ */ function announceHandler(ci, kind, route) {
64
+ if (!ci) return;
65
+ if (route !== void 0) ci.hook(kind, "Handler:routed", route);
66
+ ci.hook(kind, "Controller:registered");
67
+ }
68
+
57
69
  //#endregion
58
70
  //#region packages/moost/src/adapter-brand.ts
59
71
  /**
@@ -572,7 +584,7 @@ function formatNewInstanceArg(a) {
572
584
  //#endregion
573
585
  //#region packages/moost/src/composables/global-key.ts
574
586
  var _g;
575
- const registry = (_g = globalThis).__moost_keys ?? (_g.__moost_keys = /* @__PURE__ */ new Map());
587
+ const registry$1 = (_g = globalThis).__moost_keys ?? (_g.__moost_keys = /* @__PURE__ */ new Map());
576
588
  /**
577
589
  * Creates a typed event-context key that stays a single instance across duplicate
578
590
  * moost loads (dual ESM/CJS, or duplicate installs). Use this for every
@@ -580,8 +592,8 @@ const registry = (_g = globalThis).__moost_keys ?? (_g.__moost_keys = /* @__PURE
580
592
  *
581
593
  * @param name - Debug label AND the registry lookup key; must be unique per slot.
582
594
  */ function globalKey(name) {
583
- if (!registry.has(name)) registry.set(name, (0, _wooksjs_event_core.key)(name));
584
- return registry.get(name);
595
+ if (!registry$1.has(name)) registry$1.set(name, (0, _wooksjs_event_core.key)(name));
596
+ return registry$1.get(name);
585
597
  }
586
598
 
587
599
  //#endregion
@@ -664,6 +676,7 @@ const controllerContextKey = globalKey("controller.context");
664
676
  //#region packages/moost/src/composables/interceptor.composable.ts
665
677
  const overtakeKey = globalKey("interceptor.overtake");
666
678
  const interceptResultKey = globalKey("interceptor.result");
679
+ const interceptOriginalKey = globalKey("interceptor.original");
667
680
  /** Stores the overtake (reply) function in the current event context. */ function setOvertake(fn, ctx) {
668
681
  (ctx || (0, _wooksjs_event_core.current)()).set(overtakeKey, fn);
669
682
  }
@@ -673,9 +686,22 @@ const interceptResultKey = globalKey("interceptor.result");
673
686
  /** Stores the interceptor result value in the current event context. */ function setInterceptResult(value, ctx) {
674
687
  (ctx || (0, _wooksjs_event_core.current)()).set(interceptResultKey, value);
675
688
  }
676
- /** Retrieves the interceptor result value from the current event context. */ function useInterceptResult(ctx) {
689
+ /**
690
+ * Retrieves the interceptor result value from the current event context: in an after/error
691
+ * hook, the CURRENT value of the chain — the handler result or error as replaced by the
692
+ * hooks that ran before this one.
693
+ */ function useInterceptResult(ctx) {
677
694
  return (ctx || (0, _wooksjs_event_core.current)()).get(interceptResultKey);
678
695
  }
696
+ /** Stores the value that entered the after/error chain in the current event context. */ function setInterceptOriginal(value, ctx) {
697
+ (ctx || (0, _wooksjs_event_core.current)()).set(interceptOriginalKey, value);
698
+ }
699
+ /**
700
+ * In an after/error hook: the value that entered the after/error chain — the handler result,
701
+ * the error it raised, or a before hook's reply — before any hook replaced it.
702
+ */ function useInterceptOriginal(ctx) {
703
+ return (ctx || (0, _wooksjs_event_core.current)()).get(interceptOriginalKey);
704
+ }
679
705
 
680
706
  //#endregion
681
707
  //#region packages/moost/src/adapter-utils.ts
@@ -772,7 +798,7 @@ const noop = () => {};
772
798
  function afterInstance(instance) {
773
799
  if (instance) {
774
800
  setControllerContext(instance, options.controllerMethod || "", options.targetPath, { prefix: options.controllerPrefix });
775
- ci?.hook(options.handlerType, "Controller:registered");
801
+ announceHandler(ci, options.handlerType);
776
802
  }
777
803
  interceptorHandler = options.getIterceptorHandler();
778
804
  if (interceptorHandler?.count) try {
@@ -837,21 +863,17 @@ const noop = () => {};
837
863
  return cleanup();
838
864
  }
839
865
  function cleanup() {
840
- if (interceptorHandler?.countAfter || interceptorHandler?.countOnError) {
841
- const raisedResponse = response;
842
- const afterResult = ci ? ci.with("Interceptors:after", () => interceptorHandler?.fireAfter(response)) : interceptorHandler?.fireAfter(response);
843
- if (isThenable(afterResult)) return afterResult.then((r) => {
844
- response = r;
845
- if (raise && response !== raisedResponse) raise = false;
846
- return finalize();
847
- }, (error) => {
848
- if (options.logErrors) getLogger().error(String(error));
849
- throw error;
850
- });
851
- response = afterResult;
852
- if (raise && response !== raisedResponse) raise = false;
853
- }
854
- return finalize();
866
+ const handler = interceptorHandler;
867
+ if (!handler?.countHooks) return finalize();
868
+ const original = response;
869
+ const settle = () => {
870
+ response = handler.response;
871
+ if (handler.raised && response !== original && options.logErrors) getLogger().error(String(response));
872
+ raise = handler.raised;
873
+ return finalize();
874
+ };
875
+ const chain = ci ? ci.with("Interceptors:after", () => handler.fireAfter(original, raise)) : handler.fireAfter(original, raise);
876
+ return isThenable(chain) ? chain.then(settle) : settle();
855
877
  }
856
878
  function finalize() {
857
879
  releaseLifecycle();
@@ -981,7 +1003,7 @@ const disposeSymbol = typeof Symbol.dispose === "symbol" ? Symbol.dispose : void
981
1003
  }] : [];
982
1004
  }
983
1005
  async function runHook(hook, errors, opts) {
984
- const fn = hook.instance[hook.method];
1006
+ const fn = hook.fn ?? hook.instance[hook.method];
985
1007
  try {
986
1008
  await fn.call(hook.instance);
987
1009
  } catch (error) {
@@ -1009,7 +1031,13 @@ async function runHook(hook, errors, opts) {
1009
1031
  *
1010
1032
  * Instances already disposed (by an earlier call, or by `Moost.dispose()`) are
1011
1033
  * skipped, so passing overlapping sets is safe.
1012
- */ async function disposeInstances(instances, opts) {
1034
+ */ function disposeInstances(instances, opts) {
1035
+ return disposeWithHooks(instances, [], opts);
1036
+ }
1037
+ /**
1038
+ * @internal {@link disposeInstances} plus function hooks, all in one pass by ascending
1039
+ * `priority` (instance hooks first on ties).
1040
+ */ async function disposeWithHooks(instances, fnHooks, opts) {
1013
1041
  const hooks = [];
1014
1042
  const disposed = [];
1015
1043
  for (const instance of instances) {
@@ -1021,6 +1049,7 @@ async function runHook(hook, errors, opts) {
1021
1049
  hooks.push(...own);
1022
1050
  }
1023
1051
  }
1052
+ hooks.push(...fnHooks);
1024
1053
  const errors = [];
1025
1054
  for (const hook of hooks.toSorted((a, b) => a.priority - b.priority)) await runHook(hook, errors, opts);
1026
1055
  if (opts?.onError === "throw" && errors.length > 0) throw new AggregateError(errors.map((e) => e.error), formatDisposeErrors(errors));
@@ -1262,6 +1291,19 @@ function ImportController(prefix, controller, provide) {
1262
1291
  return getMoostMate().apply(getMoostMate().decorate("paramSource", "ROUTE"), Resolve(() => (0, _wooksjs_event_core.useRouteParams)().params, "params"));
1263
1292
  }
1264
1293
  /**
1294
+ * Get the locale of the current event (`useLocale().get()`), for any event kind.
1295
+ *
1296
+ * Moost only carries the locale: the app decides how it is chosen (for HTTP, the
1297
+ * `resolveLocale` option of `MoostHttp`, typically with `useAcceptLanguage().negotiate()`;
1298
+ * or `useLocale().set()` in an interceptor) and how text is translated (see `setTranslator`).
1299
+ * A workflow or other child event reads the locale of the event that started it.
1300
+ * Resolves `undefined` when no locale is set.
1301
+ * @decorator
1302
+ * @paramType string | undefined
1303
+ */ function Locale() {
1304
+ return Resolve(() => (0, _wooksjs_event_core.useLocale)().get(), "locale");
1305
+ }
1306
+ /**
1265
1307
  * Provide Const Value
1266
1308
  * @decorator
1267
1309
  * @param value - provided value
@@ -1449,8 +1491,11 @@ var TInterceptorPriority = /* @__PURE__ */ function(TInterceptorPriority) {
1449
1491
  * ## @After
1450
1492
  * ### Method Decorator
1451
1493
  * Marks a method as an after-phase interceptor hook.
1452
- * Runs after successful handler execution.
1453
- * Use @Response() param for the handler result, @Overtake() for the reply fn.
1494
+ * Runs when the current value of the after/error chain is not an Error.
1495
+ * Use @Response() param for the current value, @Overtake() for the reply fn,
1496
+ * `useInterceptOriginal()` for the value that entered the chain.
1497
+ * Throwing (or replying with an Error) hands the error to the error hooks
1498
+ * of the interceptors that run after this one.
1454
1499
  */ function After() {
1455
1500
  return getMoostMate().decorate("interceptorHook", "after");
1456
1501
  }
@@ -1458,8 +1503,10 @@ var TInterceptorPriority = /* @__PURE__ */ function(TInterceptorPriority) {
1458
1503
  * ## @OnError
1459
1504
  * ### Method Decorator
1460
1505
  * Marks a method as an error-phase interceptor hook.
1461
- * Runs when the handler throws or returns an Error.
1462
- * Use @Response() param for the error, @Overtake() for the reply fn.
1506
+ * Runs when the current value of the after/error chain is an Error — the handler
1507
+ * threw or returned one, or an earlier after/error hook raised or replied one.
1508
+ * Use @Response() param for the error, @Overtake() for the reply fn
1509
+ * (a non-Error reply recovers the chain).
1463
1510
  */ function OnError() {
1464
1511
  return getMoostMate().decorate("interceptorHook", "error");
1465
1512
  }
@@ -1474,9 +1521,9 @@ var TInterceptorPriority = /* @__PURE__ */ function(TInterceptorPriority) {
1474
1521
  /**
1475
1522
  * ## @Response
1476
1523
  * ### Parameter Decorator
1477
- * Injects the handler result into an interceptor method parameter.
1478
- * In @After methods, this is the successful response.
1479
- * In @OnError methods, this is the Error that was thrown.
1524
+ * Injects the current value of the after/error chain into an interceptor method parameter:
1525
+ * the handler result (or error) as replaced by the hooks that ran before this one.
1526
+ * In @After methods, this is the response; in @OnError methods, the Error.
1480
1527
  */ function Response() {
1481
1528
  return Resolve(() => useInterceptResult(), "response");
1482
1529
  }
@@ -1751,28 +1798,23 @@ const ON_ERROR_STAGE = { "moost.interceptor.stage": "onError" };
1751
1798
  return this._boundReplyFn ?? (this._boundReplyFn = (reply) => {
1752
1799
  this.response = reply;
1753
1800
  this.responseOverwritten = true;
1801
+ if (!(reply instanceof Error)) this.raised = false;
1754
1802
  });
1755
1803
  }
1756
1804
  get count() {
1757
1805
  return this.handlers.length;
1758
1806
  }
1759
- get countAfter() {
1760
- return this.after?.length ?? 0;
1761
- }
1762
- get countOnError() {
1763
- return this.onError?.length ?? 0;
1807
+ /** Number of registered interceptors with an after and/or error hook. */ get countHooks() {
1808
+ return this.hooks?.length ?? 0;
1764
1809
  }
1765
1810
  /**
1766
1811
  * Register hooks from a TInterceptorDef.
1767
1812
  * Returns a pending PromiseLike if `before` went async, or undefined.
1768
1813
  */ registerDef(def, entry, ci) {
1769
- if (def.after) (this.after ?? (this.after = [])).unshift({
1814
+ if (def.after || def.error) (this.hooks ?? (this.hooks = [])).unshift({
1770
1815
  spanName: entry.spanName,
1771
- fn: def.after
1772
- });
1773
- if (def.error) (this.onError ?? (this.onError = [])).unshift({
1774
- spanName: entry.spanName,
1775
- fn: def.error
1816
+ after: def.after,
1817
+ error: def.error
1776
1818
  });
1777
1819
  if (def.before) {
1778
1820
  const result = ci ? ci.with(entry.spanName, BEFORE_STAGE, () => def.before?.(this.getReplyFn())) : def.before(this.getReplyFn());
@@ -1822,39 +1864,68 @@ const ON_ERROR_STAGE = { "moost.interceptor.stage": "onError" };
1822
1864
  if (this.responseOverwritten) return this.response;
1823
1865
  }
1824
1866
  }
1825
- fireAfter(response) {
1867
+ /**
1868
+ * Runs the after/error hooks as ONE chain over the current value, innermost interceptor
1869
+ * first. Each hook sees the current value (the previous hook's reply) and the `original`
1870
+ * value that entered the chain: a raised value or an `Error` runs the hook's `error` fn,
1871
+ * anything else its `after` fn. A hook that throws (anything), or replies with an `Error`,
1872
+ * switches the rest of the chain to error mode; a non-Error reply recovers it.
1873
+ *
1874
+ * `raised` tells whether `response` was thrown (by the handler or a before hook) rather than
1875
+ * returned; read {@link raised} afterwards for the final value's state.
1876
+ */ fireAfter(response, raised = false) {
1826
1877
  this.response = response;
1827
- const isError = response instanceof Error;
1828
- const handlers = isError ? this.onError : this.after;
1829
- if (!handlers) return this.response;
1878
+ this.raised = raised;
1879
+ const hooks = this.hooks;
1880
+ if (!hooks) return response;
1830
1881
  const ci = (0, _wooksjs_event_core.getContextInjector)();
1831
- const stage = isError ? ON_ERROR_STAGE : AFTER_STAGE;
1832
- for (let i = 0; i < handlers.length; i++) {
1833
- const { spanName, fn } = handlers[i];
1834
- const result = ci ? ci.with(spanName, stage, () => fn(response, this.getReplyFn())) : fn(response, this.getReplyFn());
1835
- if (isThenable(result)) return this._fireAfterAsync({
1836
- ci,
1837
- handlers,
1838
- stage,
1839
- response
1840
- }, result, i);
1882
+ for (let i = 0; i < hooks.length; i++) {
1883
+ const pending = this.runHook(ci, hooks[i], response);
1884
+ if (pending) return this._fireAfterAsync(ci, response, pending, i);
1841
1885
  }
1842
1886
  return this.response;
1843
1887
  }
1844
- async _fireAfterAsync(ctx, pending, startIndex) {
1845
- await pending;
1846
- for (let i = startIndex + 1; i < ctx.handlers.length; i++) {
1847
- const { spanName, fn } = ctx.handlers[i];
1848
- if (ctx.ci) await ctx.ci.with(spanName, ctx.stage, () => fn(ctx.response, this.getReplyFn()));
1849
- else await fn(ctx.response, this.getReplyFn());
1888
+ /**
1889
+ * Runs one interceptor's hook for the current value: its `error` fn while the chain is in
1890
+ * error mode (a raised value — even a thrown non-Error — or an `Error`), else its `after` fn.
1891
+ * A sync throw switches the chain to error mode; an async hook's promise is returned for the
1892
+ * caller to await (its rejection switches to error mode there).
1893
+ */ runHook(ci, hook, original) {
1894
+ const value = this.response;
1895
+ const isError = this.raised || value instanceof Error;
1896
+ const fn = isError ? hook.error : hook.after;
1897
+ if (!fn) return;
1898
+ let result;
1899
+ try {
1900
+ result = ci ? ci.with(hook.spanName, isError ? ON_ERROR_STAGE : AFTER_STAGE, () => fn(value, this.getReplyFn(), original)) : fn(value, this.getReplyFn(), original);
1901
+ } catch (error) {
1902
+ this.raise(error);
1903
+ return;
1904
+ }
1905
+ return isThenable(result) ? result : void 0;
1906
+ }
1907
+ /** A hook threw: the error becomes the current value, raised. */ raise(error) {
1908
+ this.response = error;
1909
+ this.raised = true;
1910
+ }
1911
+ async _fireAfterAsync(ci, original, pending, startIndex) {
1912
+ const hooks = this.hooks;
1913
+ let current = pending;
1914
+ for (let i = startIndex + 1;; i++) {
1915
+ if (current) try {
1916
+ await current;
1917
+ } catch (error) {
1918
+ this.raise(error);
1919
+ }
1920
+ if (i >= hooks.length) return this.response;
1921
+ current = this.runHook(ci, hooks[i], original);
1850
1922
  }
1851
- return this.response;
1852
1923
  }
1853
1924
  constructor(handlers) {
1854
1925
  this.handlers = handlers;
1855
- this.after = void 0;
1856
- this.onError = void 0;
1926
+ this.hooks = void 0;
1857
1927
  this.response = void 0;
1928
+ this.raised = false;
1858
1929
  this.responseOverwritten = false;
1859
1930
  this._boundReplyFn = void 0;
1860
1931
  }
@@ -1919,18 +1990,22 @@ function createClassInterceptorFactory(opts) {
1919
1990
  if (opts.methods.after) {
1920
1991
  const methodName = opts.methods.after;
1921
1992
  const resolveArgs = opts.resolvers[methodName];
1922
- def.after = (response, reply) => {
1923
- setOvertake(reply);
1924
- setInterceptResult(response);
1993
+ def.after = (response, reply, original) => {
1994
+ const ctx = (0, _wooksjs_event_core.current)();
1995
+ setOvertake(reply, ctx);
1996
+ setInterceptResult(response, ctx);
1997
+ setInterceptOriginal(original, ctx);
1925
1998
  return callMethod(instance, methodName, resolveArgs);
1926
1999
  };
1927
2000
  }
1928
2001
  if (opts.methods.error) {
1929
2002
  const methodName = opts.methods.error;
1930
2003
  const resolveArgs = opts.resolvers[methodName];
1931
- def.error = (error, reply) => {
1932
- setOvertake(reply);
1933
- setInterceptResult(error);
2004
+ def.error = (error, reply, original) => {
2005
+ const ctx = (0, _wooksjs_event_core.current)();
2006
+ setOvertake(reply, ctx);
2007
+ setInterceptResult(error, ctx);
2008
+ setInterceptOriginal(original, ctx);
1934
2009
  return callMethod(instance, methodName, resolveArgs);
1935
2010
  };
1936
2011
  }
@@ -2306,6 +2381,10 @@ function _define_property(obj, key, value) {
2306
2381
  return obj;
2307
2382
  }
2308
2383
  /**
2384
+ * Event kind of a run started with {@link Moost.runDetached} (`eventTypeKey` = `'DETACHED'`).
2385
+ * Its own event, with no request: HTTP composables are not available inside.
2386
+ */ const detachedKind = (0, _wooksjs_event_core.defineEventKind)("DETACHED", {});
2387
+ /**
2309
2388
  * Detects the object registration form of `registerControllers`: a plain
2310
2389
  * object (no class prototype) carrying a `controllers` array. Controller
2311
2390
  * instances are class instances and never match, so all pre-existing
@@ -2520,6 +2599,72 @@ function _define_property(obj, key, value) {
2520
2599
  this.initHooks.push(hook);
2521
2600
  }
2522
2601
  /**
2602
+ * ### addDisposeHook
2603
+ * Programmatic counterpart of `@MoostDispose`: runs `fn(app)` once on
2604
+ * {@link dispose} — after every adapter's `onDispose` (no new work comes in), in the
2605
+ * same pass as the `@MoostDispose` instance hooks, by ascending `priority`
2606
+ * (default 0; instance hooks first on ties). Awaited; a failure is collected into
2607
+ * the `AggregateError` `dispose()` rejects with, after the other hooks ran.
2608
+ *
2609
+ * Use it for resources the app itself owns — a timer, a periodic sweep, a queue
2610
+ * consumer started in an init hook.
2611
+ *
2612
+ * Hooks registered after `dispose()` was called do not run.
2613
+ *
2614
+ * ```ts
2615
+ * app.addInitHook(() => {
2616
+ * const timer = setInterval(() => void app.runDetached('sweep', sweepExpired), 60_000)
2617
+ * app.addDisposeHook(() => clearInterval(timer))
2618
+ * })
2619
+ * ```
2620
+ */ addDisposeHook(fn, opts) {
2621
+ this.disposeHooks.push({
2622
+ instance: this,
2623
+ method: `addDisposeHook(${fn.name || "anonymous"})`,
2624
+ priority: opts?.priority ?? 0,
2625
+ fn: () => fn(this)
2626
+ });
2627
+ }
2628
+ /**
2629
+ * ### runDetached
2630
+ * Runs `fn` as its own event, detached from the caller: a fresh ROOT event context
2631
+ * (kind {@link detachedKind}, not a child of the calling request, so nothing it does is tied
2632
+ * to the request's lifetime), with its own `FOR_EVENT` DI scope — released when `fn`
2633
+ * settles — and the app as controller context (`useControllerContext().instantiate()` works).
2634
+ * Context-injector instrumentation (OpenTelemetry) sees it as a `DETACHED` event.
2635
+ *
2636
+ * Call it from a handler to do work after the response, without awaiting it. The returned
2637
+ * promise resolves with `fn`'s result, or `undefined` when it fails — a failure is logged
2638
+ * (`opts.logger`, default the app logger topic `name`) and never rejects, so a forgotten
2639
+ * `await` cannot crash the process. It is not a job queue: nothing persists or retries it.
2640
+ *
2641
+ * ```ts
2642
+ * @Post('signup')
2643
+ * signup(@Body() user: TUser) {
2644
+ * void this.app.runDetached('welcome-mail', () => this.mailer.sendWelcome(user.email))
2645
+ * return { ok: true }
2646
+ * }
2647
+ * ```
2648
+ */ runDetached(name, fn, opts) {
2649
+ const logger = opts?.logger ?? this.getLogger(name);
2650
+ const run = (0, _wooksjs_event_core.createEventContext)({ logger }, detachedKind, {}, async () => {
2651
+ const release = holdEventScopeLazily(getEventScope());
2652
+ setControllerContext(this, "runDetached", "", { prefix: "" });
2653
+ announceHandler((0, _wooksjs_event_core.getContextInjector)(), detachedKind.name, name);
2654
+ try {
2655
+ return await fn();
2656
+ } catch (error) {
2657
+ logger.error(`Detached run "${name}" failed: ${errorMessage(error)}`, error);
2658
+ return;
2659
+ } finally {
2660
+ release();
2661
+ }
2662
+ });
2663
+ this.detachedRuns.add(run);
2664
+ run.finally(() => this.detachedRuns.delete(run));
2665
+ return run;
2666
+ }
2667
+ /**
2523
2668
  * ### dispose
2524
2669
  * Graceful shutdown: stops the adapters, then runs every `@MoostDispose`
2525
2670
  * hook of every singleton this app can reach — DI registry singletons
@@ -2528,7 +2673,8 @@ function _define_property(obj, key, value) {
2528
2673
  * `[Symbol.dispose]()` method is disposed through that.
2529
2674
  *
2530
2675
  * Order: every adapter's `onDispose` in registration order (stop taking new
2531
- * work first), then instance hooks by ascending `priority`. Everything is
2676
+ * work first), then the {@link runDetached} runs in flight are awaited, then
2677
+ * instance hooks and {@link addDisposeHook} functions by ascending `priority`. Everything is
2532
2678
  * awaited, failures never stop the run — once all hooks have run, a failed
2533
2679
  * disposal rejects the returned promise with an `AggregateError` naming each
2534
2680
  * failing `Class.method`.
@@ -2588,8 +2734,10 @@ function _define_property(obj, key, value) {
2588
2734
  return errors;
2589
2735
  }
2590
2736
  /** The one-shot body behind {@link Moost.dispose} (see its docs for the contract). */ async runDispose() {
2737
+ const fnHooks = [...this.disposeHooks];
2591
2738
  const errors = await this.disposeAdapters();
2592
- const result = await disposeInstances([...getInfactSingletonInstances(), ...this.singletonInstances], {
2739
+ await Promise.allSettled(this.detachedRuns);
2740
+ const result = await disposeWithHooks([...getInfactSingletonInstances(), ...this.singletonInstances], fnHooks, {
2593
2741
  logger: this.logger,
2594
2742
  onError: "warn"
2595
2743
  });
@@ -2877,7 +3025,7 @@ function _define_property(obj, key, value) {
2877
3025
  this.logger.info(`${prefix || ""}${c}${eventName} ${"\x1B[0m\x1B[2m\x1B[32m" + c}→ ${classConstructor.name}.${"\x1B[36m" + c}${method}()${coff}`);
2878
3026
  }
2879
3027
  constructor(options) {
2880
- 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, "initHookPhase", void 0), _define_property(this, "provide", void 0), _define_property(this, "replace", void 0), _define_property(this, "defaultProvide", void 0), _define_property(this, "defaultReplace", 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.initHookPhase = "pending", this.provide = (0, _prostojs_infact.createProvideRegistry)([_prostojs_infact.Infact, getMoostInfact], [_prostojs_mate.Mate, getMoostMate]), this.replace = {}, this.defaultProvide = {}, this.defaultReplace = {}, this.unregisteredControllers = [], this.paramAuditFindings = [], this.initialized = false, this.singletonInstances = /* @__PURE__ */ new Set();
3028
+ 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, "initHookPhase", void 0), _define_property(this, "provide", void 0), _define_property(this, "replace", void 0), _define_property(this, "defaultProvide", void 0), _define_property(this, "defaultReplace", 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, "disposeHooks", void 0), _define_property(this, "detachedRuns", 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.initHookPhase = "pending", this.provide = (0, _prostojs_infact.createProvideRegistry)([_prostojs_infact.Infact, getMoostInfact], [_prostojs_mate.Mate, getMoostMate]), this.replace = {}, this.defaultProvide = {}, this.defaultReplace = {}, this.unregisteredControllers = [], this.paramAuditFindings = [], this.initialized = false, this.singletonInstances = /* @__PURE__ */ new Set(), this.disposeHooks = [], this.detachedRuns = /* @__PURE__ */ new Set();
2881
3029
  this.paramAuditMode = resolveParamAuditMode(options?.diagnostics?.paramTypes);
2882
3030
  this.inheritanceAuditMode = options?.diagnostics?.inheritance || "warn";
2883
3031
  this.logger = options?.logger || getDefaultLogger(`moost`);
@@ -2954,6 +3102,51 @@ function _define_property(obj, key, value) {
2954
3102
  return getHandlerPaths(moost, getController(), method ?? getMethod() ?? "", opts);
2955
3103
  }
2956
3104
 
3105
+ //#endregion
3106
+ //#region packages/moost/src/i18n.ts
3107
+ const TRANSLATOR = Symbol.for("moost.translator");
3108
+ const registry = globalThis;
3109
+ /**
3110
+ * Registers the app's translator (process-wide), or removes it with `undefined`.
3111
+ * It takes effect where code calls {@link useTranslate} — your own code and packages that opt
3112
+ * in to translating the text they serialise; with no translator registered, text stays as
3113
+ * authored.
3114
+ *
3115
+ * @example
3116
+ * ```ts
3117
+ * const messages = { de: { 'Name taken': 'Name vergeben' } }
3118
+ * setTranslator((text, params, { locale }) => messages[locale ?? '']?.[text])
3119
+ * ```
3120
+ */ function setTranslator(translate) {
3121
+ registry[TRANSLATOR] = translate;
3122
+ }
3123
+ /** The registered translator, or `undefined` (e.g. to skip a per-locale variant entirely). */ function getTranslator() {
3124
+ return registry[TRANSLATOR];
3125
+ }
3126
+ /**
3127
+ * Returns `t(text, params?)` for the current event: the registered translator's result in the
3128
+ * event's locale (`useLocale().get()`), else `text` unchanged — never a missing-key marker.
3129
+ * With no translator registered, `t` returns `text` (params are not interpolated).
3130
+ *
3131
+ * @example
3132
+ * ```ts
3133
+ * const t = useTranslate()
3134
+ * throw new HttpError(409, { message: t('Name taken'), code: 'NAME_TAKEN' })
3135
+ * ```
3136
+ */ function useTranslate(ctx) {
3137
+ const event = ctx ?? (0, _wooksjs_event_core.current)();
3138
+ let translateCtx;
3139
+ return (text, params) => {
3140
+ const translate = getTranslator();
3141
+ if (!translate) return text;
3142
+ translateCtx ?? (translateCtx = {
3143
+ locale: (0, _wooksjs_event_core.useLocale)(event).get(),
3144
+ event
3145
+ });
3146
+ return translate(text, params, translateCtx) ?? text;
3147
+ };
3148
+ }
3149
+
2957
3150
  //#endregion
2958
3151
  exports.After = After;
2959
3152
  exports.ApplyDecorators = ApplyDecorators;
@@ -2984,6 +3177,7 @@ exports.Intercept = Intercept;
2984
3177
  exports.Interceptor = Interceptor;
2985
3178
  exports.InterceptorHandler = InterceptorHandler;
2986
3179
  exports.Label = Label;
3180
+ exports.Locale = Locale;
2987
3181
  exports.LoggerTopic = LoggerTopic;
2988
3182
  exports.MOOST_ADAPTER_BRAND = MOOST_ADAPTER_BRAND;
2989
3183
  exports.MOOST_MODULE_IDENTITY_KEY = MOOST_MODULE_IDENTITY_KEY;
@@ -3054,6 +3248,7 @@ exports.defineInfactScope = defineInfactScope;
3054
3248
  exports.defineInterceptor = defineInterceptor;
3055
3249
  exports.defineMoostEventHandler = defineMoostEventHandler;
3056
3250
  exports.definePipeFn = definePipeFn;
3251
+ exports.detachedKind = detachedKind;
3057
3252
  exports.disposeInstances = disposeInstances;
3058
3253
  Object.defineProperty(exports, 'eventTypeKey', {
3059
3254
  enumerable: true,
@@ -3094,6 +3289,7 @@ exports.getInstanceOwnProps = getInstanceOwnProps;
3094
3289
  exports.getMoostInfact = getMoostInfact;
3095
3290
  exports.getMoostMate = getMoostMate;
3096
3291
  exports.getNewMoostInfact = getNewMoostInfact;
3292
+ exports.getTranslator = getTranslator;
3097
3293
  exports.globalKey = globalKey;
3098
3294
  Object.defineProperty(exports, 'isConstructor', {
3099
3295
  enumerable: true,
@@ -3132,13 +3328,22 @@ Object.defineProperty(exports, 'run', {
3132
3328
  });
3133
3329
  exports.setControllerContext = setControllerContext;
3134
3330
  exports.setInfactLoggingOptions = setInfactLoggingOptions;
3331
+ exports.setInterceptOriginal = setInterceptOriginal;
3135
3332
  exports.setInterceptResult = setInterceptResult;
3136
3333
  exports.setOvertake = setOvertake;
3334
+ exports.setTranslator = setTranslator;
3137
3335
  exports.stampModuleIdentity = stampModuleIdentity;
3138
3336
  exports.stampOnce = stampOnce;
3139
3337
  exports.useControllerContext = useControllerContext;
3140
3338
  exports.useHandlerPaths = useHandlerPaths;
3339
+ exports.useInterceptOriginal = useInterceptOriginal;
3141
3340
  exports.useInterceptResult = useInterceptResult;
3341
+ Object.defineProperty(exports, 'useLocale', {
3342
+ enumerable: true,
3343
+ get: function () {
3344
+ return _wooksjs_event_core.useLocale;
3345
+ }
3346
+ });
3142
3347
  Object.defineProperty(exports, 'useLogger', {
3143
3348
  enumerable: true,
3144
3349
  get: function () {
@@ -3147,4 +3352,5 @@ Object.defineProperty(exports, 'useLogger', {
3147
3352
  });
3148
3353
  exports.useOvertake = useOvertake;
3149
3354
  exports.useScopeId = useScopeId;
3355
+ exports.useTranslate = useTranslate;
3150
3356
  exports.withControllerContext = withControllerContext;