moost 0.6.46 → 0.6.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -48,7 +48,7 @@ let stamped = false;
48
48
  if (stamped) return;
49
49
  stamped = true;
50
50
  stampModuleIdentity({
51
- version: "0.6.46",
51
+ version: "0.6.48",
52
52
  path: typeof __filename === "string" ? __filename : require("url").pathToFileURL(__filename).href
53
53
  });
54
54
  }
@@ -124,7 +124,7 @@ function getDefaultLogger(topic) {
124
124
 
125
125
  //#endregion
126
126
  //#region packages/moost/src/event-scope.ts
127
- function _define_property$2(obj, key, value) {
127
+ function _define_property$1(obj, key, value) {
128
128
  if (key in obj) Object.defineProperty(obj, key, {
129
129
  value,
130
130
  enumerable: true,
@@ -143,13 +143,64 @@ function nextScopeId() {
143
143
  }
144
144
  return `__moost_${_scopeChar}_${++_scopeNum}`;
145
145
  }
146
- const scopeIdSlot = (0, _wooksjs_event_core.cached)(() => nextScopeId());
146
+ const eventScopeSlot = (0, _wooksjs_event_core.cached)(() => ({
147
+ id: nextScopeId(),
148
+ held: false,
149
+ registered: false
150
+ }));
151
+ /**
152
+ * The DI scope of an event (see {@link useScopeId}) — cached on the event context itself.
153
+ * @internal
154
+ */ const getEventScope = (ctx) => (ctx ?? (0, _wooksjs_event_core.current)()).getOwn(eventScopeSlot);
147
155
  /**
148
156
  * Composable returning the moost DI scope ID of the current event. Generated on first call
149
157
  * and cached on the event context itself — never inherited from a parent context, so a child
150
158
  * event (a workflow run started with `eventContext`, a WebSocket message) owns its scope
151
159
  * instead of sharing, and later releasing, its parent's.
152
- */ const useScopeId = (ctx) => (ctx ?? (0, _wooksjs_event_core.current)()).getOwn(scopeIdSlot);
160
+ */ const useScopeId = (ctx) => getEventScope(ctx).id;
161
+ /**
162
+ * Holds the event scope for a handler lifecycle — registering it, deferred to the first
163
+ * `FOR_EVENT` resolution (see {@link resolveEventScopeId}). Returns the release, which drops
164
+ * the scope like the release of an eager registration.
165
+ * @internal
166
+ */ function holdEventScopeLazily(scope) {
167
+ scope.held = true;
168
+ return () => {
169
+ scope.held = false;
170
+ if (scope.registered) {
171
+ scope.registered = false;
172
+ getMoostInfact().unregisterScope(scope.id);
173
+ }
174
+ };
175
+ }
176
+ /**
177
+ * The scope id a `FOR_EVENT` resolution in the current event uses: a held scope is registered
178
+ * in the DI container now, on first use.
179
+ * @internal
180
+ */ function resolveEventScopeId() {
181
+ const scope = getEventScope();
182
+ if (scope.held && !scope.registered) {
183
+ scope.registered = true;
184
+ getMoostInfact().registerScope(scope.id);
185
+ }
186
+ return scope.id;
187
+ }
188
+ /**
189
+ * Keeps the current event's scope state in step with a direct `registerScope` /
190
+ * `unregisterScope` of its id on the moost DI container — `registerEventScope(useScopeId())`,
191
+ * or an adapter's `cleanup: () => getMoostInfact().unregisterScope(useScopeId())`. As with an
192
+ * eager registration: a registered scope is dropped with the handler lifecycle, and an
193
+ * unregistered one stays gone for the rest of the lifecycle (a later `FOR_EVENT` resolution
194
+ * fails instead of registering it again — and leaking it, when nothing releases it after).
195
+ * @internal
196
+ */ function syncEventScope(scopeId, registered) {
197
+ const ctx = (0, _wooksjs_event_core.tryGetCurrent)();
198
+ const scope = ctx?.hasOwn(eventScopeSlot) ? ctx.getOwn(eventScopeSlot) : void 0;
199
+ if (scope?.id === scopeId) {
200
+ scope.registered = registered;
201
+ if (!registered) scope.held = false;
202
+ }
203
+ }
153
204
  /** Registers a DI scope for the given ID and returns an unscope function. */ function registerEventScope(scopeId) {
154
205
  const infact = getMoostInfact();
155
206
  infact.registerScope(scopeId);
@@ -169,7 +220,7 @@ const NO_ISOLATION = /* @__PURE__ */ new Set();
169
220
  super({
170
221
  logger: parent.logger,
171
222
  parent
172
- }), _define_property$2(this, "isolated", void 0), this.isolated = isolated;
223
+ }), _define_property$1(this, "isolated", void 0), this.isolated = isolated;
173
224
  }
174
225
  };
175
226
  /**
@@ -222,7 +273,7 @@ const NO_ISOLATION = /* @__PURE__ */ new Set();
222
273
  ctx: child
223
274
  });
224
275
  if (opts?.shareScope !== false) {
225
- child.setOwn(scopeIdSlot, useScopeId(parent));
276
+ child.setOwn(eventScopeSlot, getEventScope(parent));
226
277
  return (0, _wooksjs_event_core.run)(child, fn);
227
278
  }
228
279
  const release = registerEventScope(useScopeId(child));
@@ -347,6 +398,22 @@ const moostMate = new _prostojs_mate.Mate(METADATA_WORKSPACE, {
347
398
  return moostMate;
348
399
  }
349
400
 
401
+ //#endregion
402
+ //#region packages/moost/src/metadata/moost-infact.ts
403
+ /**
404
+ * The moost DI container. A direct scope (un)registration of the current event's own scope
405
+ * keeps its on-demand registration state in step (see `syncEventScope`).
406
+ */ var MoostInfact = class extends _prostojs_infact.Infact {
407
+ registerScope(scopeId) {
408
+ super.registerScope(scopeId);
409
+ syncEventScope(scopeId, true);
410
+ }
411
+ unregisterScope(scopeId) {
412
+ super.unregisterScope(scopeId);
413
+ syncEventScope(scopeId, false);
414
+ }
415
+ };
416
+
350
417
  //#endregion
351
418
  //#region packages/moost/src/metadata/infact.ts
352
419
  const sharedMoostInfact = getNewMoostInfact();
@@ -458,7 +525,7 @@ function formatNewInstanceArg(a) {
458
525
  /**
459
526
  * Get Infact instance (used for Dependency Injections)
460
527
  */ function getNewMoostInfact() {
461
- return new _prostojs_infact.Infact({
528
+ return new MoostInfact({
462
529
  describeClass(classConstructor) {
463
530
  const meta = getMoostMate().read(classConstructor);
464
531
  return {
@@ -467,7 +534,7 @@ function formatNewInstanceArg(a) {
467
534
  constructorParams: meta?.params || [],
468
535
  provide: meta?.provide,
469
536
  properties: meta?.properties || [],
470
- scopeId: meta?.injectable === "FOR_EVENT" ? useScopeId() : void 0
537
+ scopeId: meta?.injectable === "FOR_EVENT" ? resolveEventScopeId() : void 0
471
538
  };
472
539
  },
473
540
  resolveParam({ paramMeta, customData, classConstructor, index, scopeId, instantiate }) {
@@ -519,19 +586,27 @@ const registry = (_g = globalThis).__moost_keys ?? (_g.__moost_keys = /* @__PURE
519
586
 
520
587
  //#endregion
521
588
  //#region packages/moost/src/composables/controller.composable.ts
522
- const controllerInstanceKey = globalKey("controller.instance");
523
- const controllerMethodKey = globalKey("controller.method");
524
- const controllerRouteKey = globalKey("controller.route");
525
- const controllerPrefixKey = globalKey("controller.prefix");
589
+ const controllerContextKey = globalKey("controller.context");
526
590
  /**
527
591
  * Sets the controller context for the current event scope.
528
592
  * Called internally by adapters when dispatching events to handlers.
529
593
  */ function setControllerContext(controller, method, route, opts) {
530
594
  const _ctx = opts?.ctx || (0, _wooksjs_event_core.current)();
531
- _ctx.set(controllerInstanceKey, controller);
532
- _ctx.set(controllerMethodKey, method);
533
- _ctx.set(controllerRouteKey, route);
534
- if (opts?.prefix !== void 0) _ctx.set(controllerPrefixKey, opts.prefix);
595
+ let prefix = opts?.prefix;
596
+ if (prefix === void 0 && _ctx.has(controllerContextKey)) prefix = _ctx.get(controllerContextKey).prefix;
597
+ _ctx.set(controllerContextKey, {
598
+ instance: controller,
599
+ method,
600
+ route,
601
+ prefix
602
+ });
603
+ }
604
+ /** Reads the controller context; throws the per-field "not set" error when there is none. */ function readControllerContext(ctx, field) {
605
+ try {
606
+ return ctx.get(controllerContextKey);
607
+ } catch {
608
+ throw new Error(`Key "controller.${field}" is not set`);
609
+ }
535
610
  }
536
611
  /** Brands already warned about by {@link brandedAdapterFor} (one warning per process). */ const reportedAdapterCopies = /* @__PURE__ */ new Set();
537
612
  /**
@@ -555,10 +630,14 @@ const controllerPrefixKey = globalKey("controller.prefix");
555
630
  * Returns utilities for accessing the controller instance, method metadata, and DI.
556
631
  */ function useControllerContext(ctx) {
557
632
  const _ctx = ctx || (0, _wooksjs_event_core.current)();
558
- const getController = () => _ctx.get(controllerInstanceKey);
559
- const getMethod = () => _ctx.get(controllerMethodKey);
560
- const getRoute = () => _ctx.get(controllerRouteKey);
561
- const getPrefix = () => _ctx.get(controllerPrefixKey);
633
+ const getController = () => readControllerContext(_ctx, "instance").instance;
634
+ const getMethod = () => readControllerContext(_ctx, "method").method;
635
+ const getRoute = () => readControllerContext(_ctx, "route").route;
636
+ const getPrefix = () => {
637
+ const { prefix } = readControllerContext(_ctx, "prefix");
638
+ if (prefix === void 0) throw new Error("Key \"controller.prefix\" is not set");
639
+ return prefix;
640
+ };
562
641
  const getControllerMeta = () => getMoostMate().read(getController());
563
642
  const getMethodMeta = (name) => getMoostMate().read(getController(), name || getMethod());
564
643
  function instantiate(c) {
@@ -602,13 +681,30 @@ const interceptResultKey = globalKey("interceptor.result");
602
681
  //#region packages/moost/src/adapter-utils.ts
603
682
  const noop = () => {};
604
683
  /**
605
- * Registers an event's DI scope, held by its handler lifecycle and, with `adapterHolds`, by
606
- * the adapter too. Each release is idempotent; the scope is dropped once every holder let go,
607
- * so an adapter signal that fires early (a client disconnect mid-handler) cannot pull the
608
- * scope from under a running handler. Built outside the handler closure so a long-lived
609
- * adapter listener (the response 'close' of a streamed body) retains only the scope.
610
- */ function holdEventScope(scopeId, adapterHolds) {
611
- const drop = registerEventScope(scopeId);
684
+ * The hook options of one event. `logger` is a prototype getter: the topic logger is only
685
+ * derived when a hook reads it (an own accessor property would deopt the object instead).
686
+ */ let EventHookOptions = class EventHookOptions {
687
+ get logger() {
688
+ return this._getLogger();
689
+ }
690
+ constructor(scopeId, unscope, method, getResponse, reply, getLogger) {
691
+ this.scopeId = scopeId;
692
+ this.unscope = unscope;
693
+ this.method = method;
694
+ this.getResponse = getResponse;
695
+ this.reply = reply;
696
+ this._getLogger = getLogger;
697
+ }
698
+ };
699
+ /**
700
+ * Holds an event's DI scope (registered on demand, see `holdEventScopeLazily`) by its handler
701
+ * lifecycle and, with `adapterHolds`, by the adapter too. Each release is idempotent; the
702
+ * scope is dropped once every holder let go, so an adapter signal that fires early (a client
703
+ * disconnect mid-handler) cannot pull the scope from under a running handler. Built outside
704
+ * the handler closure so a long-lived adapter listener (the response 'close' of a streamed
705
+ * body) retains only the scope.
706
+ */ function holdEventScope(scope, adapterHolds) {
707
+ const drop = holdEventScopeLazily(scope);
612
708
  let holders = adapterHolds ? 2 : 1;
613
709
  const hold = () => {
614
710
  let held = true;
@@ -634,7 +730,6 @@ const noop = () => {};
634
730
  * Handles scope registration, controller resolution, interceptors, argument resolution,
635
731
  * handler invocation, and cleanup — optimised for sync-first execution.
636
732
  */ function defineMoostEventHandler(options) {
637
- const ci = (0, _wooksjs_event_core.getContextInjector)();
638
733
  const manualUnscope = !!options.manualUnscope;
639
734
  const handlerSpanName = `Handler:${options.targetPath}`;
640
735
  const handlerAttrs = {
@@ -642,21 +737,17 @@ const noop = () => {};
642
737
  "moost.controller": options.controllerName || ""
643
738
  };
644
739
  return () => {
740
+ const ci = (0, _wooksjs_event_core.getContextInjector)();
645
741
  const ctx = (0, _wooksjs_event_core.current)();
646
- const scopeId = useScopeId(ctx);
647
- const logger = (0, _wooksjs_event_core.useLogger)(options.loggerTitle, ctx);
648
- const { unscope, releaseLifecycle, failLifecycle } = holdEventScope(scopeId, manualUnscope);
742
+ const scope = getEventScope(ctx);
743
+ const scopeId = scope.id;
744
+ const { unscope, releaseLifecycle, failLifecycle } = holdEventScope(scope, manualUnscope);
649
745
  let response;
746
+ let logger;
747
+ const getLogger = () => logger ?? (logger = (0, _wooksjs_event_core.useLogger)(options.loggerTitle, ctx));
650
748
  let hookOptions;
651
749
  function getHookOptions() {
652
- return hookOptions ?? (hookOptions = {
653
- scopeId,
654
- logger,
655
- unscope,
656
- method: options.controllerMethod,
657
- getResponse: () => response,
658
- reply: (r) => response = r
659
- });
750
+ return hookOptions ?? (hookOptions = new EventHookOptions(scopeId, unscope, options.controllerMethod, () => response, (r) => response = r, getLogger));
660
751
  }
661
752
  let interceptorHandler;
662
753
  let raise = false;
@@ -694,7 +785,7 @@ const noop = () => {};
694
785
  response = initResult;
695
786
  if (response !== void 0) return cleanup();
696
787
  } catch (error) {
697
- if (options.logErrors) logger.error(String(error));
788
+ if (options.logErrors) getLogger().error(String(error));
698
789
  response = error;
699
790
  raise = true;
700
791
  return cleanup();
@@ -711,7 +802,7 @@ const noop = () => {};
711
802
  }, handleError);
712
803
  args = argsResult;
713
804
  } catch (error) {
714
- if (options.logErrors) logger.error(String(error));
805
+ if (options.logErrors) getLogger().error(String(error));
715
806
  response = error;
716
807
  raise = true;
717
808
  return cleanup();
@@ -726,21 +817,21 @@ const noop = () => {};
726
817
  response = r;
727
818
  return cleanup();
728
819
  }, (error) => {
729
- if (options.logErrors) logger.error(error);
820
+ if (options.logErrors) getLogger().error(error);
730
821
  response = error;
731
822
  raise = true;
732
823
  return cleanup();
733
824
  });
734
825
  response = handlerResult;
735
826
  } catch (error) {
736
- if (options.logErrors) logger.error(error);
827
+ if (options.logErrors) getLogger().error(error);
737
828
  response = error;
738
829
  raise = true;
739
830
  }
740
831
  return cleanup();
741
832
  }
742
833
  function handleError(error) {
743
- if (options.logErrors) logger.error(String(error));
834
+ if (options.logErrors) getLogger().error(String(error));
744
835
  response = error;
745
836
  raise = true;
746
837
  return cleanup();
@@ -754,7 +845,7 @@ const noop = () => {};
754
845
  if (raise && response !== raisedResponse) raise = false;
755
846
  return finalize();
756
847
  }, (error) => {
757
- if (options.logErrors) logger.error(String(error));
848
+ if (options.logErrors) getLogger().error(String(error));
758
849
  throw error;
759
850
  });
760
851
  response = afterResult;
@@ -1134,10 +1225,14 @@ function ImportController(prefix, controller, provide) {
1134
1225
  */ function Resolve(resolver, label) {
1135
1226
  return (target, key, index) => {
1136
1227
  const i = typeof index === "number" ? index : void 0;
1228
+ let labelFilled = false;
1137
1229
  getMoostMate().decorate("resolver", (metas, level) => {
1138
- let newLabel = label;
1139
- if (!newLabel && level === "PROP" && typeof metas.key === "string") newLabel = metas.key;
1140
- fillLabel(target, key || "", i, newLabel);
1230
+ if (!labelFilled) {
1231
+ labelFilled = true;
1232
+ let newLabel = label;
1233
+ if (!newLabel && level === "PROP" && typeof metas.key === "string") newLabel = metas.key;
1234
+ fillLabel(target, key || "", i, newLabel);
1235
+ }
1141
1236
  return resolver(metas, level);
1142
1237
  })(target, key, i);
1143
1238
  };
@@ -1608,6 +1703,7 @@ const sharedPipes = [{
1608
1703
 
1609
1704
  //#endregion
1610
1705
  //#region packages/moost/src/resolve-arguments.ts
1706
+ /** `metas.instantiate` of a handler argument: resolves through the current controller's DI. */ const instantiateFromController = (t) => useControllerContext().instantiate(t);
1611
1707
  /**
1612
1708
  * Builds an argument-resolver function from pre-computed per-parameter pipe lists.
1613
1709
  *
@@ -1616,21 +1712,25 @@ const sharedPipes = [{
1616
1712
  * `unknown[]` synchronously when possible, or `Promise<unknown[]>` otherwise.
1617
1713
  */ function resolveArguments(argsPipes, context) {
1618
1714
  if (argsPipes.length === 0) return;
1715
+ const params = argsPipes.map(({ meta: paramMeta, pipes }, index) => ({
1716
+ pipes,
1717
+ metas: {
1718
+ classMeta: context.classMeta,
1719
+ methodMeta: context.methodMeta,
1720
+ paramMeta,
1721
+ type: context.type,
1722
+ key: context.key,
1723
+ index,
1724
+ targetMeta: paramMeta,
1725
+ instantiate: instantiateFromController
1726
+ }
1727
+ }));
1619
1728
  return () => {
1620
1729
  const args = [];
1621
1730
  let hasAsync = false;
1622
- for (let i = 0; i < argsPipes.length; i++) {
1623
- const { pipes, meta: paramMeta } = argsPipes[i];
1624
- const result = runPipes(pipes, void 0, {
1625
- classMeta: context.classMeta,
1626
- methodMeta: context.methodMeta,
1627
- paramMeta,
1628
- type: context.type,
1629
- key: context.key,
1630
- index: i,
1631
- targetMeta: paramMeta,
1632
- instantiate: (t) => useControllerContext().instantiate(t)
1633
- }, "PARAM");
1731
+ for (let i = 0; i < params.length; i++) {
1732
+ const { pipes, metas } = params[i];
1733
+ const result = runPipes(pipes, void 0, metas, "PARAM");
1634
1734
  if (!hasAsync && isThenable(result)) hasAsync = true;
1635
1735
  args[i] = result;
1636
1736
  }
@@ -1640,16 +1740,9 @@ const sharedPipes = [{
1640
1740
 
1641
1741
  //#endregion
1642
1742
  //#region packages/moost/src/interceptor-handler.ts
1643
- function _define_property$1(obj, key, value) {
1644
- if (key in obj) Object.defineProperty(obj, key, {
1645
- value,
1646
- enumerable: true,
1647
- configurable: true,
1648
- writable: true
1649
- });
1650
- else obj[key] = value;
1651
- return obj;
1652
- }
1743
+ /** Span attributes of the interceptor stages — shared, never mutated. */ const BEFORE_STAGE = { "moost.interceptor.stage": "before" };
1744
+ const AFTER_STAGE = { "moost.interceptor.stage": "after" };
1745
+ const ON_ERROR_STAGE = { "moost.interceptor.stage": "onError" };
1653
1746
  /**
1654
1747
  * Manages the before/after/error interceptor lifecycle for a single event.
1655
1748
  * Optimised for the common sync path — only allocates promises when an interceptor goes async.
@@ -1674,16 +1767,15 @@ function _define_property$1(obj, key, value) {
1674
1767
  * Returns a pending PromiseLike if `before` went async, or undefined.
1675
1768
  */ registerDef(def, entry, ci) {
1676
1769
  if (def.after) (this.after ?? (this.after = [])).unshift({
1677
- name: entry.name,
1770
+ spanName: entry.spanName,
1678
1771
  fn: def.after
1679
1772
  });
1680
1773
  if (def.error) (this.onError ?? (this.onError = [])).unshift({
1681
- name: entry.name,
1774
+ spanName: entry.spanName,
1682
1775
  fn: def.error
1683
1776
  });
1684
1777
  if (def.before) {
1685
- const spanName = entry.spanName;
1686
- const result = ci ? ci.with(spanName, { "moost.interceptor.stage": "before" }, () => def.before?.(this.getReplyFn())) : def.before(this.getReplyFn());
1778
+ const result = ci ? ci.with(entry.spanName, BEFORE_STAGE, () => def.before?.(this.getReplyFn())) : def.before(this.getReplyFn());
1687
1779
  if (isThenable(result)) return result;
1688
1780
  }
1689
1781
  }
@@ -1736,10 +1828,10 @@ function _define_property$1(obj, key, value) {
1736
1828
  const handlers = isError ? this.onError : this.after;
1737
1829
  if (!handlers) return this.response;
1738
1830
  const ci = (0, _wooksjs_event_core.getContextInjector)();
1739
- const stage = isError ? "onError" : "after";
1831
+ const stage = isError ? ON_ERROR_STAGE : AFTER_STAGE;
1740
1832
  for (let i = 0; i < handlers.length; i++) {
1741
- const { name, fn } = handlers[i];
1742
- const result = ci ? ci.with(`Interceptor:${name}`, { "moost.interceptor.stage": stage }, () => fn(response, this.getReplyFn())) : fn(response, this.getReplyFn());
1833
+ const { spanName, fn } = handlers[i];
1834
+ const result = ci ? ci.with(spanName, stage, () => fn(response, this.getReplyFn())) : fn(response, this.getReplyFn());
1743
1835
  if (isThenable(result)) return this._fireAfterAsync({
1744
1836
  ci,
1745
1837
  handlers,
@@ -1752,21 +1844,19 @@ function _define_property$1(obj, key, value) {
1752
1844
  async _fireAfterAsync(ctx, pending, startIndex) {
1753
1845
  await pending;
1754
1846
  for (let i = startIndex + 1; i < ctx.handlers.length; i++) {
1755
- const { name, fn } = ctx.handlers[i];
1756
- if (ctx.ci) await ctx.ci.with(`Interceptor:${name}`, { "moost.interceptor.stage": ctx.stage }, () => fn(ctx.response, this.getReplyFn()));
1847
+ const { spanName, fn } = ctx.handlers[i];
1848
+ if (ctx.ci) await ctx.ci.with(spanName, ctx.stage, () => fn(ctx.response, this.getReplyFn()));
1757
1849
  else await fn(ctx.response, this.getReplyFn());
1758
1850
  }
1759
1851
  return this.response;
1760
1852
  }
1761
1853
  constructor(handlers) {
1762
- _define_property$1(this, "handlers", void 0);
1763
- _define_property$1(this, "after", void 0);
1764
- _define_property$1(this, "onError", void 0);
1765
- _define_property$1(this, "response", void 0);
1766
- _define_property$1(this, "responseOverwritten", void 0);
1767
- _define_property$1(this, "_boundReplyFn", void 0);
1768
1854
  this.handlers = handlers;
1855
+ this.after = void 0;
1856
+ this.onError = void 0;
1857
+ this.response = void 0;
1769
1858
  this.responseOverwritten = false;
1859
+ this._boundReplyFn = void 0;
1770
1860
  }
1771
1861
  };
1772
1862
 
@@ -1774,6 +1864,7 @@ function _define_property$1(obj, key, value) {
1774
1864
  //#region packages/moost/src/utils.ts
1775
1865
  const mate = getMoostMate();
1776
1866
  const noInterceptors = () => void 0;
1867
+ const isForEvent = (instance) => mate.read(instance)?.injectable === "FOR_EVENT";
1777
1868
  function findInterceptorMethods(handler) {
1778
1869
  const fakeInstance = Object.create(handler.prototype);
1779
1870
  const methods = getInstanceOwnMethods(fakeInstance);
@@ -1812,6 +1903,8 @@ function callMethod(instance, methodName, resolveArgs) {
1812
1903
  }
1813
1904
  function createClassInterceptorFactory(opts) {
1814
1905
  const infact = getMoostInfact();
1906
+ const customData = { pipes: opts.pipes };
1907
+ const defs = opts.cacheable ? /* @__PURE__ */ new WeakMap() : void 0;
1815
1908
  function buildDef(instance) {
1816
1909
  const def = {};
1817
1910
  if (opts.methods.before) {
@@ -1844,9 +1937,21 @@ function createClassInterceptorFactory(opts) {
1844
1937
  return def;
1845
1938
  }
1846
1939
  function fromTarget(targetInstance) {
1847
- const result = infact.getForInstance(targetInstance, opts.handler, { customData: { pipes: opts.pipes } });
1848
- if (isThenable(result)) return result.then(buildDef);
1849
- return buildDef(result);
1940
+ const cached = defs?.get(targetInstance);
1941
+ if (cached) return cached;
1942
+ const registries = infact.getInstanceRegistries(targetInstance);
1943
+ const result = infact.get(opts.handler, {
1944
+ provide: registries.provide || {},
1945
+ replace: registries.replace,
1946
+ customData
1947
+ });
1948
+ const remember = (instance) => {
1949
+ const def = buildDef(instance);
1950
+ if (defs && !isForEvent(instance) && !isForEvent(targetInstance)) defs.set(targetInstance, def);
1951
+ return def;
1952
+ };
1953
+ if (isThenable(result)) return result.then(remember);
1954
+ return remember(result);
1850
1955
  }
1851
1956
  return () => {
1852
1957
  const result = opts.getTargetInstance();
@@ -1888,7 +1993,8 @@ function getIterceptorHandlerFactory(interceptors, getTargetInstance, pipes) {
1888
1993
  methods,
1889
1994
  resolvers,
1890
1995
  getTargetInstance,
1891
- pipes: mergedPipes
1996
+ pipes: mergedPipes,
1997
+ cacheable: classMeta.injectable !== "FOR_EVENT"
1892
1998
  }),
1893
1999
  name,
1894
2000
  spanName
@@ -2387,12 +2493,17 @@ function _define_property(obj, key, value) {
2387
2493
  * - before the hook pass starts (including from a controller constructor
2388
2494
  * during binding) — queued;
2389
2495
  * - while the pass runs (from another hook) — runs later in the same pass;
2390
- * - after the pass finished — runs immediately (asynchronously, in its own
2391
- * init context); a failure is logged, since there is no `init()` left to
2496
+ * - after the pass finished — runs once, immediately (asynchronously, in its
2497
+ * own init context) and is NOT stored, so it does not run again on
2498
+ * re-init and per-request callers (`FOR_EVENT` constructors) do not grow
2499
+ * the hook list. A failure is logged, since there is no `init()` left to
2392
2500
  * reject.
2393
2501
  *
2394
- * No de-duplication: registering the same function twice runs it twice.
2395
- * Hooks re-run on re-init exactly like `@MoostInit` hooks.
2502
+ * No de-duplication: registering the same function twice runs it twice
2503
+ * (per-request callers should guard non-idempotent work themselves). Hooks
2504
+ * registered before or during a pass re-run on re-init exactly like
2505
+ * `@MoostInit` hooks; to get a hook on every future init, register it
2506
+ * before `init()`.
2396
2507
  * @since 0.6.46
2397
2508
  */ addInitHook(fn, opts) {
2398
2509
  const hook = {
@@ -2400,10 +2511,13 @@ function _define_property(obj, key, value) {
2400
2511
  priority: opts?.priority ?? 0,
2401
2512
  fn
2402
2513
  };
2514
+ if (this.initHookPhase === "done") {
2515
+ this.runInitHook(hook).catch((error) => {
2516
+ this.logger.error(`Late init hook failed: ${errorMessage(error)}`);
2517
+ });
2518
+ return;
2519
+ }
2403
2520
  this.initHooks.push(hook);
2404
- if (this.initHookPhase === "done") this.runInitHook(hook).catch((error) => {
2405
- this.logger.error(`Late init hook failed: ${errorMessage(error)}`);
2406
- });
2407
2521
  }
2408
2522
  /**
2409
2523
  * ### dispose
@@ -2536,7 +2650,7 @@ function _define_property(obj, key, value) {
2536
2650
  infact.setInstanceRegistries(instance, provide, replace, { pipes });
2537
2651
  }
2538
2652
  if (instance) this.singletonInstances.add(instance);
2539
- const getInstance = instance ? () => instance : async () => await infact.get(controller, { ...infactOpts });
2653
+ const getInstance = instance ? () => instance : () => infact.get(controller, infactOpts);
2540
2654
  const classConstructor = (0, _prostojs_mate.isConstructor)(controller) ? controller : (0, _prostojs_mate.getConstructor)(controller);
2541
2655
  const controllerOverview = await bindControllerMethods({
2542
2656
  getInstance,
package/dist/index.d.ts CHANGED
@@ -216,6 +216,28 @@ declare function MoostDispose(opts?: {
216
216
  priority?: number;
217
217
  }): MethodDecorator;
218
218
 
219
+ /** Summary of a registered controller, its metadata, prefix, and handlers. */
220
+ interface TControllerOverview {
221
+ meta: TMoostMetadata;
222
+ computedPrefix: string;
223
+ type: TFunction;
224
+ handlers: THandlerOverview[];
225
+ }
226
+ /** Summary of a registered handler method within a controller. */
227
+ interface THandlerOverview {
228
+ meta: TMoostMetadata;
229
+ path?: string;
230
+ type: string;
231
+ method: string;
232
+ handler: TMoostHandler<TEmpty>;
233
+ registeredAs: {
234
+ path: string;
235
+ args: string[];
236
+ }[];
237
+ }
238
+ /** Hook points in the event handler lifecycle where context injectors are invoked. */
239
+ type TContextInjectorHook = 'Event:start' | 'Interceptors:init' | 'Arguments:resolve' | 'Interceptors:before' | 'Handler' | 'Interceptors:after';
240
+
219
241
  type TDecoratorLevel = 'CLASS' | 'METHOD' | 'PROP' | 'PARAM';
220
242
 
221
243
  /** Metadata context passed to pipe functions during argument/property resolution. */
@@ -257,28 +279,6 @@ interface TPipeData {
257
279
 
258
280
  declare const resolvePipe: TPipeFn<TEmpty>;
259
281
 
260
- /** Summary of a registered controller, its metadata, prefix, and handlers. */
261
- interface TControllerOverview {
262
- meta: TMoostMetadata;
263
- computedPrefix: string;
264
- type: TFunction;
265
- handlers: THandlerOverview[];
266
- }
267
- /** Summary of a registered handler method within a controller. */
268
- interface THandlerOverview {
269
- meta: TMoostMetadata;
270
- path?: string;
271
- type: string;
272
- method: string;
273
- handler: TMoostHandler<TEmpty>;
274
- registeredAs: {
275
- path: string;
276
- args: string[];
277
- }[];
278
- }
279
- /** Hook points in the event handler lifecycle where context injectors are invoked. */
280
- type TContextInjectorHook = 'Event:start' | 'Interceptors:init' | 'Arguments:resolve' | 'Interceptors:before' | 'Handler' | 'Interceptors:after';
281
-
282
282
  type TInterceptorBeforeFn = (reply: (response: TAny) => void) => void | Promise<void>;
283
283
  type TInterceptorAfterFn = (response: TAny, reply: (response: TAny) => void) => void | Promise<void>;
284
284
  type TInterceptorErrorFn = (error: Error, reply: (response: TAny) => void) => void | Promise<void>;
@@ -412,6 +412,19 @@ interface TCommonMoostMeta {
412
412
  type?: TFunction;
413
413
  }
414
414
 
415
+ /** Infact `customData` of a moost resolution: the pipes its params and props resolve through. */
416
+ interface TMoostInfactCustom {
417
+ pipes?: TPipeData[];
418
+ }
419
+ /**
420
+ * The moost DI container. A direct scope (un)registration of the current event's own scope
421
+ * keeps its on-demand registration state in step (see `syncEventScope`).
422
+ */
423
+ declare class MoostInfact extends Infact<TMoostMetadata, TMoostMetadata, TMoostParamsMetadata, TMoostInfactCustom> {
424
+ registerScope(scopeId: string | symbol): void;
425
+ unregisterScope(scopeId: string | symbol): void;
426
+ }
427
+
415
428
  interface TInfactLoggingOptions {
416
429
  newInstance?: true | false | 'FOR_EVENT' | 'SINGLETON';
417
430
  warn?: true | false;
@@ -420,7 +433,7 @@ interface TInfactLoggingOptions {
420
433
  /** Configures which Infact DI events are logged (instance creation, warnings, errors). */
421
434
  declare function setInfactLoggingOptions(options: TInfactLoggingOptions): void;
422
435
  /** Returns the shared Infact DI container used by Moost for dependency injection. */
423
- declare function getMoostInfact(): Infact<TMoostMetadata<TEmpty>, TMoostMetadata<TEmpty>, TMoostParamsMetadata, TCustom>;
436
+ declare function getMoostInfact(): MoostInfact;
424
437
  /**
425
438
  * Every SINGLETON instance currently held by an Infact container's **global**
426
439
  * registry (per-event `scopes` registries are excluded — those instances are
@@ -431,9 +444,6 @@ declare function getMoostInfact(): Infact<TMoostMetadata<TEmpty>, TMoostMetadata
431
444
  * (`Moost.dispose()`, the `@moostjs/vite` dev plugin) do not each re-derive it.
432
445
  */
433
446
  declare function getInfactSingletonInstances(infact?: ReturnType<typeof getMoostInfact>): object[];
434
- interface TCustom {
435
- pipes?: TPipeData[];
436
- }
437
447
  /**
438
448
  * Define global scope name to be used with `@InjectFromScope` and `@InjectScopeVars` decorators
439
449
  *
@@ -451,7 +461,7 @@ declare function getInfactScopeVars<T extends object>(name: string | symbol): T
451
461
  /**
452
462
  * Get Infact instance (used for Dependency Injections)
453
463
  */
454
- declare function getNewMoostInfact(): Infact<TMoostMetadata<TEmpty>, TMoostMetadata<TEmpty>, TMoostParamsMetadata, TCustom>;
464
+ declare function getNewMoostInfact(): MoostInfact;
455
465
 
456
466
  /**
457
467
  * `fatal-capable` findings may reject `init()` (only non-optional constructor
@@ -777,12 +787,17 @@ declare class Moost extends Hookable {
777
787
  * - before the hook pass starts (including from a controller constructor
778
788
  * during binding) — queued;
779
789
  * - while the pass runs (from another hook) — runs later in the same pass;
780
- * - after the pass finished — runs immediately (asynchronously, in its own
781
- * init context); a failure is logged, since there is no `init()` left to
790
+ * - after the pass finished — runs once, immediately (asynchronously, in its
791
+ * own init context) and is NOT stored, so it does not run again on
792
+ * re-init and per-request callers (`FOR_EVENT` constructors) do not grow
793
+ * the hook list. A failure is logged, since there is no `init()` left to
782
794
  * reject.
783
795
  *
784
- * No de-duplication: registering the same function twice runs it twice.
785
- * Hooks re-run on re-init exactly like `@MoostInit` hooks.
796
+ * No de-duplication: registering the same function twice runs it twice
797
+ * (per-request callers should guard non-idempotent work themselves). Hooks
798
+ * registered before or during a pass re-run on re-init exactly like
799
+ * `@MoostInit` hooks; to get a hook on every future init, register it
800
+ * before `init()`.
786
801
  * @since 0.6.46
787
802
  */
788
803
  addInitHook(fn: (app: this) => unknown, opts?: {
@@ -1301,24 +1316,24 @@ interface TInterceptorEntry {
1301
1316
  /** Pre-computed span name for ci.with(). */
1302
1317
  spanName: string;
1303
1318
  }
1319
+ /** A registered after/onError hook of one event. */
1320
+ interface TRegisteredHook<F> {
1321
+ /** Pre-computed span name for ci.with(). */
1322
+ spanName: string;
1323
+ fn: F;
1324
+ }
1304
1325
  /**
1305
1326
  * Manages the before/after/error interceptor lifecycle for a single event.
1306
1327
  * Optimised for the common sync path — only allocates promises when an interceptor goes async.
1307
1328
  */
1308
1329
  declare class InterceptorHandler {
1309
- protected handlers: TInterceptorEntry[];
1310
- constructor(handlers: TInterceptorEntry[]);
1311
- protected after: {
1312
- name: string;
1313
- fn: TInterceptorAfterFn;
1314
- }[] | undefined;
1315
- protected onError: {
1316
- name: string;
1317
- fn: TInterceptorErrorFn;
1318
- }[] | undefined;
1330
+ protected after: TRegisteredHook<TInterceptorAfterFn>[] | undefined;
1331
+ protected onError: TRegisteredHook<TInterceptorErrorFn>[] | undefined;
1319
1332
  response?: unknown;
1320
1333
  responseOverwritten: boolean;
1321
1334
  protected _boundReplyFn?: (reply: unknown) => void;
1335
+ protected handlers: TInterceptorEntry[];
1336
+ constructor(handlers: TInterceptorEntry[]);
1322
1337
  protected getReplyFn(): (reply: unknown) => void;
1323
1338
  get count(): number;
1324
1339
  get countAfter(): number;
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, 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, 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";
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,7 +47,7 @@ let stamped = false;
47
47
  if (stamped) return;
48
48
  stamped = true;
49
49
  stampModuleIdentity({
50
- version: "0.6.46",
50
+ version: "0.6.48",
51
51
  path: typeof __filename === "string" ? __filename : import.meta.url
52
52
  });
53
53
  }
@@ -123,7 +123,7 @@ function getDefaultLogger(topic) {
123
123
 
124
124
  //#endregion
125
125
  //#region packages/moost/src/event-scope.ts
126
- function _define_property$2(obj, key, value) {
126
+ function _define_property$1(obj, key, value) {
127
127
  if (key in obj) Object.defineProperty(obj, key, {
128
128
  value,
129
129
  enumerable: true,
@@ -142,13 +142,64 @@ function nextScopeId() {
142
142
  }
143
143
  return `__moost_${_scopeChar}_${++_scopeNum}`;
144
144
  }
145
- const scopeIdSlot = cached$1(() => nextScopeId());
145
+ const eventScopeSlot = cached$1(() => ({
146
+ id: nextScopeId(),
147
+ held: false,
148
+ registered: false
149
+ }));
150
+ /**
151
+ * The DI scope of an event (see {@link useScopeId}) — cached on the event context itself.
152
+ * @internal
153
+ */ const getEventScope = (ctx) => (ctx ?? current$1()).getOwn(eventScopeSlot);
146
154
  /**
147
155
  * Composable returning the moost DI scope ID of the current event. Generated on first call
148
156
  * and cached on the event context itself — never inherited from a parent context, so a child
149
157
  * event (a workflow run started with `eventContext`, a WebSocket message) owns its scope
150
158
  * instead of sharing, and later releasing, its parent's.
151
- */ const useScopeId = (ctx) => (ctx ?? current$1()).getOwn(scopeIdSlot);
159
+ */ const useScopeId = (ctx) => getEventScope(ctx).id;
160
+ /**
161
+ * Holds the event scope for a handler lifecycle — registering it, deferred to the first
162
+ * `FOR_EVENT` resolution (see {@link resolveEventScopeId}). Returns the release, which drops
163
+ * the scope like the release of an eager registration.
164
+ * @internal
165
+ */ function holdEventScopeLazily(scope) {
166
+ scope.held = true;
167
+ return () => {
168
+ scope.held = false;
169
+ if (scope.registered) {
170
+ scope.registered = false;
171
+ getMoostInfact().unregisterScope(scope.id);
172
+ }
173
+ };
174
+ }
175
+ /**
176
+ * The scope id a `FOR_EVENT` resolution in the current event uses: a held scope is registered
177
+ * in the DI container now, on first use.
178
+ * @internal
179
+ */ function resolveEventScopeId() {
180
+ const scope = getEventScope();
181
+ if (scope.held && !scope.registered) {
182
+ scope.registered = true;
183
+ getMoostInfact().registerScope(scope.id);
184
+ }
185
+ return scope.id;
186
+ }
187
+ /**
188
+ * Keeps the current event's scope state in step with a direct `registerScope` /
189
+ * `unregisterScope` of its id on the moost DI container — `registerEventScope(useScopeId())`,
190
+ * or an adapter's `cleanup: () => getMoostInfact().unregisterScope(useScopeId())`. As with an
191
+ * eager registration: a registered scope is dropped with the handler lifecycle, and an
192
+ * unregistered one stays gone for the rest of the lifecycle (a later `FOR_EVENT` resolution
193
+ * fails instead of registering it again — and leaking it, when nothing releases it after).
194
+ * @internal
195
+ */ function syncEventScope(scopeId, registered) {
196
+ const ctx = tryGetCurrent();
197
+ const scope = ctx?.hasOwn(eventScopeSlot) ? ctx.getOwn(eventScopeSlot) : void 0;
198
+ if (scope?.id === scopeId) {
199
+ scope.registered = registered;
200
+ if (!registered) scope.held = false;
201
+ }
202
+ }
152
203
  /** Registers a DI scope for the given ID and returns an unscope function. */ function registerEventScope(scopeId) {
153
204
  const infact = getMoostInfact();
154
205
  infact.registerScope(scopeId);
@@ -168,7 +219,7 @@ const NO_ISOLATION = /* @__PURE__ */ new Set();
168
219
  super({
169
220
  logger: parent.logger,
170
221
  parent
171
- }), _define_property$2(this, "isolated", void 0), this.isolated = isolated;
222
+ }), _define_property$1(this, "isolated", void 0), this.isolated = isolated;
172
223
  }
173
224
  };
174
225
  /**
@@ -221,7 +272,7 @@ const NO_ISOLATION = /* @__PURE__ */ new Set();
221
272
  ctx: child
222
273
  });
223
274
  if (opts?.shareScope !== false) {
224
- child.setOwn(scopeIdSlot, useScopeId(parent));
275
+ child.setOwn(eventScopeSlot, getEventScope(parent));
225
276
  return run$1(child, fn);
226
277
  }
227
278
  const release = registerEventScope(useScopeId(child));
@@ -346,6 +397,22 @@ const moostMate = new Mate(METADATA_WORKSPACE, {
346
397
  return moostMate;
347
398
  }
348
399
 
400
+ //#endregion
401
+ //#region packages/moost/src/metadata/moost-infact.ts
402
+ /**
403
+ * The moost DI container. A direct scope (un)registration of the current event's own scope
404
+ * keeps its on-demand registration state in step (see `syncEventScope`).
405
+ */ var MoostInfact = class extends Infact {
406
+ registerScope(scopeId) {
407
+ super.registerScope(scopeId);
408
+ syncEventScope(scopeId, true);
409
+ }
410
+ unregisterScope(scopeId) {
411
+ super.unregisterScope(scopeId);
412
+ syncEventScope(scopeId, false);
413
+ }
414
+ };
415
+
349
416
  //#endregion
350
417
  //#region packages/moost/src/metadata/infact.ts
351
418
  const sharedMoostInfact = getNewMoostInfact();
@@ -457,7 +524,7 @@ function formatNewInstanceArg(a) {
457
524
  /**
458
525
  * Get Infact instance (used for Dependency Injections)
459
526
  */ function getNewMoostInfact() {
460
- return new Infact({
527
+ return new MoostInfact({
461
528
  describeClass(classConstructor) {
462
529
  const meta = getMoostMate().read(classConstructor);
463
530
  return {
@@ -466,7 +533,7 @@ function formatNewInstanceArg(a) {
466
533
  constructorParams: meta?.params || [],
467
534
  provide: meta?.provide,
468
535
  properties: meta?.properties || [],
469
- scopeId: meta?.injectable === "FOR_EVENT" ? useScopeId() : void 0
536
+ scopeId: meta?.injectable === "FOR_EVENT" ? resolveEventScopeId() : void 0
470
537
  };
471
538
  },
472
539
  resolveParam({ paramMeta, customData, classConstructor, index, scopeId, instantiate }) {
@@ -518,19 +585,27 @@ const registry = (_g = globalThis).__moost_keys ?? (_g.__moost_keys = /* @__PURE
518
585
 
519
586
  //#endregion
520
587
  //#region packages/moost/src/composables/controller.composable.ts
521
- const controllerInstanceKey = globalKey("controller.instance");
522
- const controllerMethodKey = globalKey("controller.method");
523
- const controllerRouteKey = globalKey("controller.route");
524
- const controllerPrefixKey = globalKey("controller.prefix");
588
+ const controllerContextKey = globalKey("controller.context");
525
589
  /**
526
590
  * Sets the controller context for the current event scope.
527
591
  * Called internally by adapters when dispatching events to handlers.
528
592
  */ function setControllerContext(controller, method, route, opts) {
529
593
  const _ctx = opts?.ctx || current$1();
530
- _ctx.set(controllerInstanceKey, controller);
531
- _ctx.set(controllerMethodKey, method);
532
- _ctx.set(controllerRouteKey, route);
533
- if (opts?.prefix !== void 0) _ctx.set(controllerPrefixKey, opts.prefix);
594
+ let prefix = opts?.prefix;
595
+ if (prefix === void 0 && _ctx.has(controllerContextKey)) prefix = _ctx.get(controllerContextKey).prefix;
596
+ _ctx.set(controllerContextKey, {
597
+ instance: controller,
598
+ method,
599
+ route,
600
+ prefix
601
+ });
602
+ }
603
+ /** Reads the controller context; throws the per-field "not set" error when there is none. */ function readControllerContext(ctx, field) {
604
+ try {
605
+ return ctx.get(controllerContextKey);
606
+ } catch {
607
+ throw new Error(`Key "controller.${field}" is not set`);
608
+ }
534
609
  }
535
610
  /** Brands already warned about by {@link brandedAdapterFor} (one warning per process). */ const reportedAdapterCopies = /* @__PURE__ */ new Set();
536
611
  /**
@@ -554,10 +629,14 @@ const controllerPrefixKey = globalKey("controller.prefix");
554
629
  * Returns utilities for accessing the controller instance, method metadata, and DI.
555
630
  */ function useControllerContext(ctx) {
556
631
  const _ctx = ctx || current$1();
557
- const getController = () => _ctx.get(controllerInstanceKey);
558
- const getMethod = () => _ctx.get(controllerMethodKey);
559
- const getRoute = () => _ctx.get(controllerRouteKey);
560
- const getPrefix = () => _ctx.get(controllerPrefixKey);
632
+ const getController = () => readControllerContext(_ctx, "instance").instance;
633
+ const getMethod = () => readControllerContext(_ctx, "method").method;
634
+ const getRoute = () => readControllerContext(_ctx, "route").route;
635
+ const getPrefix = () => {
636
+ const { prefix } = readControllerContext(_ctx, "prefix");
637
+ if (prefix === void 0) throw new Error("Key \"controller.prefix\" is not set");
638
+ return prefix;
639
+ };
561
640
  const getControllerMeta = () => getMoostMate().read(getController());
562
641
  const getMethodMeta = (name) => getMoostMate().read(getController(), name || getMethod());
563
642
  function instantiate(c) {
@@ -601,13 +680,30 @@ const interceptResultKey = globalKey("interceptor.result");
601
680
  //#region packages/moost/src/adapter-utils.ts
602
681
  const noop = () => {};
603
682
  /**
604
- * Registers an event's DI scope, held by its handler lifecycle and, with `adapterHolds`, by
605
- * the adapter too. Each release is idempotent; the scope is dropped once every holder let go,
606
- * so an adapter signal that fires early (a client disconnect mid-handler) cannot pull the
607
- * scope from under a running handler. Built outside the handler closure so a long-lived
608
- * adapter listener (the response 'close' of a streamed body) retains only the scope.
609
- */ function holdEventScope(scopeId, adapterHolds) {
610
- const drop = registerEventScope(scopeId);
683
+ * The hook options of one event. `logger` is a prototype getter: the topic logger is only
684
+ * derived when a hook reads it (an own accessor property would deopt the object instead).
685
+ */ let EventHookOptions = class EventHookOptions {
686
+ get logger() {
687
+ return this._getLogger();
688
+ }
689
+ constructor(scopeId, unscope, method, getResponse, reply, getLogger) {
690
+ this.scopeId = scopeId;
691
+ this.unscope = unscope;
692
+ this.method = method;
693
+ this.getResponse = getResponse;
694
+ this.reply = reply;
695
+ this._getLogger = getLogger;
696
+ }
697
+ };
698
+ /**
699
+ * Holds an event's DI scope (registered on demand, see `holdEventScopeLazily`) by its handler
700
+ * lifecycle and, with `adapterHolds`, by the adapter too. Each release is idempotent; the
701
+ * scope is dropped once every holder let go, so an adapter signal that fires early (a client
702
+ * disconnect mid-handler) cannot pull the scope from under a running handler. Built outside
703
+ * the handler closure so a long-lived adapter listener (the response 'close' of a streamed
704
+ * body) retains only the scope.
705
+ */ function holdEventScope(scope, adapterHolds) {
706
+ const drop = holdEventScopeLazily(scope);
611
707
  let holders = adapterHolds ? 2 : 1;
612
708
  const hold = () => {
613
709
  let held = true;
@@ -633,7 +729,6 @@ const noop = () => {};
633
729
  * Handles scope registration, controller resolution, interceptors, argument resolution,
634
730
  * handler invocation, and cleanup — optimised for sync-first execution.
635
731
  */ function defineMoostEventHandler(options) {
636
- const ci = getContextInjector$1();
637
732
  const manualUnscope = !!options.manualUnscope;
638
733
  const handlerSpanName = `Handler:${options.targetPath}`;
639
734
  const handlerAttrs = {
@@ -641,21 +736,17 @@ const noop = () => {};
641
736
  "moost.controller": options.controllerName || ""
642
737
  };
643
738
  return () => {
739
+ const ci = getContextInjector$1();
644
740
  const ctx = current$1();
645
- const scopeId = useScopeId(ctx);
646
- const logger = useLogger$1(options.loggerTitle, ctx);
647
- const { unscope, releaseLifecycle, failLifecycle } = holdEventScope(scopeId, manualUnscope);
741
+ const scope = getEventScope(ctx);
742
+ const scopeId = scope.id;
743
+ const { unscope, releaseLifecycle, failLifecycle } = holdEventScope(scope, manualUnscope);
648
744
  let response;
745
+ let logger;
746
+ const getLogger = () => logger ?? (logger = useLogger$1(options.loggerTitle, ctx));
649
747
  let hookOptions;
650
748
  function getHookOptions() {
651
- return hookOptions ?? (hookOptions = {
652
- scopeId,
653
- logger,
654
- unscope,
655
- method: options.controllerMethod,
656
- getResponse: () => response,
657
- reply: (r) => response = r
658
- });
749
+ return hookOptions ?? (hookOptions = new EventHookOptions(scopeId, unscope, options.controllerMethod, () => response, (r) => response = r, getLogger));
659
750
  }
660
751
  let interceptorHandler;
661
752
  let raise = false;
@@ -693,7 +784,7 @@ const noop = () => {};
693
784
  response = initResult;
694
785
  if (response !== void 0) return cleanup();
695
786
  } catch (error) {
696
- if (options.logErrors) logger.error(String(error));
787
+ if (options.logErrors) getLogger().error(String(error));
697
788
  response = error;
698
789
  raise = true;
699
790
  return cleanup();
@@ -710,7 +801,7 @@ const noop = () => {};
710
801
  }, handleError);
711
802
  args = argsResult;
712
803
  } catch (error) {
713
- if (options.logErrors) logger.error(String(error));
804
+ if (options.logErrors) getLogger().error(String(error));
714
805
  response = error;
715
806
  raise = true;
716
807
  return cleanup();
@@ -725,21 +816,21 @@ const noop = () => {};
725
816
  response = r;
726
817
  return cleanup();
727
818
  }, (error) => {
728
- if (options.logErrors) logger.error(error);
819
+ if (options.logErrors) getLogger().error(error);
729
820
  response = error;
730
821
  raise = true;
731
822
  return cleanup();
732
823
  });
733
824
  response = handlerResult;
734
825
  } catch (error) {
735
- if (options.logErrors) logger.error(error);
826
+ if (options.logErrors) getLogger().error(error);
736
827
  response = error;
737
828
  raise = true;
738
829
  }
739
830
  return cleanup();
740
831
  }
741
832
  function handleError(error) {
742
- if (options.logErrors) logger.error(String(error));
833
+ if (options.logErrors) getLogger().error(String(error));
743
834
  response = error;
744
835
  raise = true;
745
836
  return cleanup();
@@ -753,7 +844,7 @@ const noop = () => {};
753
844
  if (raise && response !== raisedResponse) raise = false;
754
845
  return finalize();
755
846
  }, (error) => {
756
- if (options.logErrors) logger.error(String(error));
847
+ if (options.logErrors) getLogger().error(String(error));
757
848
  throw error;
758
849
  });
759
850
  response = afterResult;
@@ -1133,10 +1224,14 @@ function ImportController(prefix, controller, provide) {
1133
1224
  */ function Resolve(resolver, label) {
1134
1225
  return (target, key, index) => {
1135
1226
  const i = typeof index === "number" ? index : void 0;
1227
+ let labelFilled = false;
1136
1228
  getMoostMate().decorate("resolver", (metas, level) => {
1137
- let newLabel = label;
1138
- if (!newLabel && level === "PROP" && typeof metas.key === "string") newLabel = metas.key;
1139
- fillLabel(target, key || "", i, newLabel);
1229
+ if (!labelFilled) {
1230
+ labelFilled = true;
1231
+ let newLabel = label;
1232
+ if (!newLabel && level === "PROP" && typeof metas.key === "string") newLabel = metas.key;
1233
+ fillLabel(target, key || "", i, newLabel);
1234
+ }
1140
1235
  return resolver(metas, level);
1141
1236
  })(target, key, i);
1142
1237
  };
@@ -1607,6 +1702,7 @@ const sharedPipes = [{
1607
1702
 
1608
1703
  //#endregion
1609
1704
  //#region packages/moost/src/resolve-arguments.ts
1705
+ /** `metas.instantiate` of a handler argument: resolves through the current controller's DI. */ const instantiateFromController = (t) => useControllerContext().instantiate(t);
1610
1706
  /**
1611
1707
  * Builds an argument-resolver function from pre-computed per-parameter pipe lists.
1612
1708
  *
@@ -1615,21 +1711,25 @@ const sharedPipes = [{
1615
1711
  * `unknown[]` synchronously when possible, or `Promise<unknown[]>` otherwise.
1616
1712
  */ function resolveArguments(argsPipes, context) {
1617
1713
  if (argsPipes.length === 0) return;
1714
+ const params = argsPipes.map(({ meta: paramMeta, pipes }, index) => ({
1715
+ pipes,
1716
+ metas: {
1717
+ classMeta: context.classMeta,
1718
+ methodMeta: context.methodMeta,
1719
+ paramMeta,
1720
+ type: context.type,
1721
+ key: context.key,
1722
+ index,
1723
+ targetMeta: paramMeta,
1724
+ instantiate: instantiateFromController
1725
+ }
1726
+ }));
1618
1727
  return () => {
1619
1728
  const args = [];
1620
1729
  let hasAsync = false;
1621
- for (let i = 0; i < argsPipes.length; i++) {
1622
- const { pipes, meta: paramMeta } = argsPipes[i];
1623
- const result = runPipes(pipes, void 0, {
1624
- classMeta: context.classMeta,
1625
- methodMeta: context.methodMeta,
1626
- paramMeta,
1627
- type: context.type,
1628
- key: context.key,
1629
- index: i,
1630
- targetMeta: paramMeta,
1631
- instantiate: (t) => useControllerContext().instantiate(t)
1632
- }, "PARAM");
1730
+ for (let i = 0; i < params.length; i++) {
1731
+ const { pipes, metas } = params[i];
1732
+ const result = runPipes(pipes, void 0, metas, "PARAM");
1633
1733
  if (!hasAsync && isThenable(result)) hasAsync = true;
1634
1734
  args[i] = result;
1635
1735
  }
@@ -1639,16 +1739,9 @@ const sharedPipes = [{
1639
1739
 
1640
1740
  //#endregion
1641
1741
  //#region packages/moost/src/interceptor-handler.ts
1642
- function _define_property$1(obj, key, value) {
1643
- if (key in obj) Object.defineProperty(obj, key, {
1644
- value,
1645
- enumerable: true,
1646
- configurable: true,
1647
- writable: true
1648
- });
1649
- else obj[key] = value;
1650
- return obj;
1651
- }
1742
+ /** Span attributes of the interceptor stages — shared, never mutated. */ const BEFORE_STAGE = { "moost.interceptor.stage": "before" };
1743
+ const AFTER_STAGE = { "moost.interceptor.stage": "after" };
1744
+ const ON_ERROR_STAGE = { "moost.interceptor.stage": "onError" };
1652
1745
  /**
1653
1746
  * Manages the before/after/error interceptor lifecycle for a single event.
1654
1747
  * Optimised for the common sync path — only allocates promises when an interceptor goes async.
@@ -1673,16 +1766,15 @@ function _define_property$1(obj, key, value) {
1673
1766
  * Returns a pending PromiseLike if `before` went async, or undefined.
1674
1767
  */ registerDef(def, entry, ci) {
1675
1768
  if (def.after) (this.after ?? (this.after = [])).unshift({
1676
- name: entry.name,
1769
+ spanName: entry.spanName,
1677
1770
  fn: def.after
1678
1771
  });
1679
1772
  if (def.error) (this.onError ?? (this.onError = [])).unshift({
1680
- name: entry.name,
1773
+ spanName: entry.spanName,
1681
1774
  fn: def.error
1682
1775
  });
1683
1776
  if (def.before) {
1684
- const spanName = entry.spanName;
1685
- const result = ci ? ci.with(spanName, { "moost.interceptor.stage": "before" }, () => def.before?.(this.getReplyFn())) : def.before(this.getReplyFn());
1777
+ const result = ci ? ci.with(entry.spanName, BEFORE_STAGE, () => def.before?.(this.getReplyFn())) : def.before(this.getReplyFn());
1686
1778
  if (isThenable(result)) return result;
1687
1779
  }
1688
1780
  }
@@ -1735,10 +1827,10 @@ function _define_property$1(obj, key, value) {
1735
1827
  const handlers = isError ? this.onError : this.after;
1736
1828
  if (!handlers) return this.response;
1737
1829
  const ci = getContextInjector$1();
1738
- const stage = isError ? "onError" : "after";
1830
+ const stage = isError ? ON_ERROR_STAGE : AFTER_STAGE;
1739
1831
  for (let i = 0; i < handlers.length; i++) {
1740
- const { name, fn } = handlers[i];
1741
- const result = ci ? ci.with(`Interceptor:${name}`, { "moost.interceptor.stage": stage }, () => fn(response, this.getReplyFn())) : fn(response, this.getReplyFn());
1832
+ const { spanName, fn } = handlers[i];
1833
+ const result = ci ? ci.with(spanName, stage, () => fn(response, this.getReplyFn())) : fn(response, this.getReplyFn());
1742
1834
  if (isThenable(result)) return this._fireAfterAsync({
1743
1835
  ci,
1744
1836
  handlers,
@@ -1751,21 +1843,19 @@ function _define_property$1(obj, key, value) {
1751
1843
  async _fireAfterAsync(ctx, pending, startIndex) {
1752
1844
  await pending;
1753
1845
  for (let i = startIndex + 1; i < ctx.handlers.length; i++) {
1754
- const { name, fn } = ctx.handlers[i];
1755
- if (ctx.ci) await ctx.ci.with(`Interceptor:${name}`, { "moost.interceptor.stage": ctx.stage }, () => fn(ctx.response, this.getReplyFn()));
1846
+ const { spanName, fn } = ctx.handlers[i];
1847
+ if (ctx.ci) await ctx.ci.with(spanName, ctx.stage, () => fn(ctx.response, this.getReplyFn()));
1756
1848
  else await fn(ctx.response, this.getReplyFn());
1757
1849
  }
1758
1850
  return this.response;
1759
1851
  }
1760
1852
  constructor(handlers) {
1761
- _define_property$1(this, "handlers", void 0);
1762
- _define_property$1(this, "after", void 0);
1763
- _define_property$1(this, "onError", void 0);
1764
- _define_property$1(this, "response", void 0);
1765
- _define_property$1(this, "responseOverwritten", void 0);
1766
- _define_property$1(this, "_boundReplyFn", void 0);
1767
1853
  this.handlers = handlers;
1854
+ this.after = void 0;
1855
+ this.onError = void 0;
1856
+ this.response = void 0;
1768
1857
  this.responseOverwritten = false;
1858
+ this._boundReplyFn = void 0;
1769
1859
  }
1770
1860
  };
1771
1861
 
@@ -1773,6 +1863,7 @@ function _define_property$1(obj, key, value) {
1773
1863
  //#region packages/moost/src/utils.ts
1774
1864
  const mate = getMoostMate();
1775
1865
  const noInterceptors = () => void 0;
1866
+ const isForEvent = (instance) => mate.read(instance)?.injectable === "FOR_EVENT";
1776
1867
  function findInterceptorMethods(handler) {
1777
1868
  const fakeInstance = Object.create(handler.prototype);
1778
1869
  const methods = getInstanceOwnMethods(fakeInstance);
@@ -1811,6 +1902,8 @@ function callMethod(instance, methodName, resolveArgs) {
1811
1902
  }
1812
1903
  function createClassInterceptorFactory(opts) {
1813
1904
  const infact = getMoostInfact();
1905
+ const customData = { pipes: opts.pipes };
1906
+ const defs = opts.cacheable ? /* @__PURE__ */ new WeakMap() : void 0;
1814
1907
  function buildDef(instance) {
1815
1908
  const def = {};
1816
1909
  if (opts.methods.before) {
@@ -1843,9 +1936,21 @@ function createClassInterceptorFactory(opts) {
1843
1936
  return def;
1844
1937
  }
1845
1938
  function fromTarget(targetInstance) {
1846
- const result = infact.getForInstance(targetInstance, opts.handler, { customData: { pipes: opts.pipes } });
1847
- if (isThenable(result)) return result.then(buildDef);
1848
- return buildDef(result);
1939
+ const cached = defs?.get(targetInstance);
1940
+ if (cached) return cached;
1941
+ const registries = infact.getInstanceRegistries(targetInstance);
1942
+ const result = infact.get(opts.handler, {
1943
+ provide: registries.provide || {},
1944
+ replace: registries.replace,
1945
+ customData
1946
+ });
1947
+ const remember = (instance) => {
1948
+ const def = buildDef(instance);
1949
+ if (defs && !isForEvent(instance) && !isForEvent(targetInstance)) defs.set(targetInstance, def);
1950
+ return def;
1951
+ };
1952
+ if (isThenable(result)) return result.then(remember);
1953
+ return remember(result);
1849
1954
  }
1850
1955
  return () => {
1851
1956
  const result = opts.getTargetInstance();
@@ -1887,7 +1992,8 @@ function getIterceptorHandlerFactory(interceptors, getTargetInstance, pipes) {
1887
1992
  methods,
1888
1993
  resolvers,
1889
1994
  getTargetInstance,
1890
- pipes: mergedPipes
1995
+ pipes: mergedPipes,
1996
+ cacheable: classMeta.injectable !== "FOR_EVENT"
1891
1997
  }),
1892
1998
  name,
1893
1999
  spanName
@@ -2386,12 +2492,17 @@ function _define_property(obj, key, value) {
2386
2492
  * - before the hook pass starts (including from a controller constructor
2387
2493
  * during binding) — queued;
2388
2494
  * - while the pass runs (from another hook) — runs later in the same pass;
2389
- * - after the pass finished — runs immediately (asynchronously, in its own
2390
- * init context); a failure is logged, since there is no `init()` left to
2495
+ * - after the pass finished — runs once, immediately (asynchronously, in its
2496
+ * own init context) and is NOT stored, so it does not run again on
2497
+ * re-init and per-request callers (`FOR_EVENT` constructors) do not grow
2498
+ * the hook list. A failure is logged, since there is no `init()` left to
2391
2499
  * reject.
2392
2500
  *
2393
- * No de-duplication: registering the same function twice runs it twice.
2394
- * Hooks re-run on re-init exactly like `@MoostInit` hooks.
2501
+ * No de-duplication: registering the same function twice runs it twice
2502
+ * (per-request callers should guard non-idempotent work themselves). Hooks
2503
+ * registered before or during a pass re-run on re-init exactly like
2504
+ * `@MoostInit` hooks; to get a hook on every future init, register it
2505
+ * before `init()`.
2395
2506
  * @since 0.6.46
2396
2507
  */ addInitHook(fn, opts) {
2397
2508
  const hook = {
@@ -2399,10 +2510,13 @@ function _define_property(obj, key, value) {
2399
2510
  priority: opts?.priority ?? 0,
2400
2511
  fn
2401
2512
  };
2513
+ if (this.initHookPhase === "done") {
2514
+ this.runInitHook(hook).catch((error) => {
2515
+ this.logger.error(`Late init hook failed: ${errorMessage(error)}`);
2516
+ });
2517
+ return;
2518
+ }
2402
2519
  this.initHooks.push(hook);
2403
- if (this.initHookPhase === "done") this.runInitHook(hook).catch((error) => {
2404
- this.logger.error(`Late init hook failed: ${errorMessage(error)}`);
2405
- });
2406
2520
  }
2407
2521
  /**
2408
2522
  * ### dispose
@@ -2535,7 +2649,7 @@ function _define_property(obj, key, value) {
2535
2649
  infact.setInstanceRegistries(instance, provide, replace, { pipes });
2536
2650
  }
2537
2651
  if (instance) this.singletonInstances.add(instance);
2538
- const getInstance = instance ? () => instance : async () => await infact.get(controller, { ...infactOpts });
2652
+ const getInstance = instance ? () => instance : () => infact.get(controller, infactOpts);
2539
2653
  const classConstructor = isConstructor$1(controller) ? controller : getConstructor$1(controller);
2540
2654
  const controllerOverview = await bindControllerMethods({
2541
2655
  getInstance,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moost",
3
- "version": "0.6.46",
3
+ "version": "0.6.48",
4
4
  "description": "moost",
5
5
  "keywords": [
6
6
  "composables",
@@ -38,18 +38,18 @@
38
38
  }
39
39
  },
40
40
  "dependencies": {
41
- "@prostojs/infact": "^0.6.0",
41
+ "@prostojs/infact": "^0.6.1",
42
42
  "@prostojs/logger": "^0.4.3",
43
- "@prostojs/mate": "^0.5.0",
44
- "@wooksjs/event-core": "^0.7.26",
43
+ "@prostojs/mate": "^0.5.1",
44
+ "@wooksjs/event-core": "^0.7.27",
45
45
  "hookable": "^5.5.3",
46
- "wooks": "^0.7.26"
46
+ "wooks": "^0.7.27"
47
47
  },
48
48
  "devDependencies": {
49
- "@wooksjs/event-http": "^0.7.26",
50
- "@wooksjs/http-body": "^0.7.26",
49
+ "@wooksjs/event-http": "^0.7.27",
50
+ "@wooksjs/http-body": "^0.7.27",
51
51
  "vitest": "3.2.7",
52
- "@moostjs/event-http": "^0.6.46"
52
+ "@moostjs/event-http": "^0.6.48"
53
53
  },
54
54
  "scripts": {
55
55
  "pub": "pnpm publish --access public",