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