moost 0.6.44 → 0.6.46

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.44",
51
+ version: "0.6.46",
52
52
  path: typeof __filename === "string" ? __filename : require("url").pathToFileURL(__filename).href
53
53
  });
54
54
  }
@@ -2323,6 +2323,7 @@ function _define_property(obj, key, value) {
2323
2323
  }
2324
2324
  if (typeof a.getProvideRegistry === "function") this.setProvideRegistry(a.getProvideRegistry());
2325
2325
  }
2326
+ this.initHookPhase = "pending";
2326
2327
  this.unregisteredControllers.unshift({ controller: this });
2327
2328
  let auditError;
2328
2329
  try {
@@ -2337,16 +2338,36 @@ function _define_property(obj, key, value) {
2337
2338
  this.initialized = true;
2338
2339
  }
2339
2340
  /**
2340
- * Runs every `@MoostInit`-decorated controller method exactly once, after all
2341
- * controllers are bound (complete `getControllersOverview()`) and before the
2342
- * `adapter.onInit` loop. Hooks run in ascending `priority`, then registration
2343
- * order. Each runs on its controller's SINGLETON instance inside a synthetic
2344
- * init context (no interceptors; params resolve via the RESOLVE pipe only).
2345
- * A throwing hook rejects `init()` (fail-fast).
2341
+ * Runs every init hook exactly once, after all controllers are bound
2342
+ * (complete `getControllersOverview()`) and before the `adapter.onInit`
2343
+ * loop: `@MoostInit` controller methods and functions registered with
2344
+ * {@link addInitHook}. Hooks run in ascending `priority`, then registration
2345
+ * order. Each runs inside a synthetic init context (no interceptors; params
2346
+ * of `@MoostInit` methods resolve via the RESOLVE pipe only). Hooks added
2347
+ * while the pass runs are picked up. A throwing hook rejects `init()`
2348
+ * (fail-fast).
2346
2349
  */ async runInitHooks() {
2347
- if (this.initHooks.length === 0) return;
2348
- const hooks = this.initHooks.toSorted((a, b) => a.priority - b.priority);
2349
- for (const hook of hooks) await (0, _wooksjs_event_core.createEventContext)({ logger: this.logger }, async () => {
2350
+ this.initHookPhase = "running";
2351
+ const done = /* @__PURE__ */ new Set();
2352
+ try {
2353
+ for (;;) {
2354
+ let next;
2355
+ for (const hook of this.initHooks) if (!done.has(hook) && (!next || hook.priority < next.priority)) next = hook;
2356
+ if (!next) break;
2357
+ done.add(next);
2358
+ await this.runInitHook(next);
2359
+ }
2360
+ } finally {
2361
+ this.initHookPhase = "done";
2362
+ }
2363
+ }
2364
+ async runInitHook(hook) {
2365
+ await (0, _wooksjs_event_core.createEventContext)({ logger: this.logger }, async () => {
2366
+ if (hook.kind === "fn") {
2367
+ setControllerContext(this, "init", "", { prefix: "" });
2368
+ await hook.fn(this);
2369
+ return;
2370
+ }
2350
2371
  const instance = await hook.getInstance();
2351
2372
  setControllerContext(instance, hook.method, "", { prefix: hook.computedPrefix });
2352
2373
  const args = hook.resolveArgs ? await hook.resolveArgs() : [];
@@ -2354,6 +2375,37 @@ function _define_property(obj, key, value) {
2354
2375
  });
2355
2376
  }
2356
2377
  /**
2378
+ * ### addInitHook
2379
+ * Programmatic counterpart of `@MoostInit`: runs `fn` once during `init()`,
2380
+ * after every controller is bound (complete `getControllersOverview()`) and
2381
+ * before the adapters' `onInit`, in the same pass as `@MoostInit` hooks:
2382
+ * ascending `priority` (default 0), then registration order. Runs inside the
2383
+ * same synthetic init context (controller context = the app; no
2384
+ * interceptors, no request composables). A throwing hook rejects `init()`.
2385
+ *
2386
+ * May be called at any time:
2387
+ * - before the hook pass starts (including from a controller constructor
2388
+ * during binding) — queued;
2389
+ * - 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
2392
+ * reject.
2393
+ *
2394
+ * No de-duplication: registering the same function twice runs it twice.
2395
+ * Hooks re-run on re-init exactly like `@MoostInit` hooks.
2396
+ * @since 0.6.46
2397
+ */ addInitHook(fn, opts) {
2398
+ const hook = {
2399
+ kind: "fn",
2400
+ priority: opts?.priority ?? 0,
2401
+ fn
2402
+ };
2403
+ 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
+ }
2408
+ /**
2357
2409
  * ### dispose
2358
2410
  * Graceful shutdown: stops the adapters, then runs every `@MoostDispose`
2359
2411
  * hook of every singleton this app can reach — DI registry singletons
@@ -2457,15 +2509,8 @@ function _define_property(obj, key, value) {
2457
2509
  if (fatal.length > 0 && this.paramAuditMode === "error") return formatParamAuditError(fatal);
2458
2510
  }
2459
2511
  async bindControllers() {
2460
- const thisMeta = getMoostMate().read(this);
2461
- const provide = {
2462
- ...thisMeta?.provide,
2463
- ...this.provide
2464
- };
2465
- const replace = {
2466
- ...thisMeta?.replace,
2467
- ...this.replace
2468
- };
2512
+ const provide = this.appProvideRegistry();
2513
+ const replace = this.appReplaceRegistry();
2469
2514
  const globalPrefix = this.options?.globalPrefix || "";
2470
2515
  for (const { controller, prependPrefix, replaceOwnPrefix } of this.unregisteredControllers) await this.bindController(controller, provide, replace, prependPrefix ? `${globalPrefix}/${prependPrefix}` : globalPrefix, replaceOwnPrefix);
2471
2516
  this.unregisteredControllers = [];
@@ -2523,7 +2568,7 @@ function _define_property(obj, key, value) {
2523
2568
  ...classMeta.provide
2524
2569
  };
2525
2570
  const mergedReplace = {
2526
- ...this.replace,
2571
+ ...this.appReplaceRegistry(),
2527
2572
  ...classMeta.replace
2528
2573
  };
2529
2574
  for (const ic of classMeta.importController) if (ic.typeResolver) {
@@ -2590,10 +2635,17 @@ function _define_property(obj, key, value) {
2590
2635
  * controller — it flows parent → child through `@ImportController`, never
2591
2636
  * to siblings.
2592
2637
  * @param provide - Provide Registry (use createProvideRegistry from '\@prostojs/infact')
2638
+ * @param opts - `{ override: false }` registers a default: used only when neither `@Provide`
2639
+ * on the app subclass nor a normal `setProvideRegistry` entry exists for the token,
2640
+ * regardless of call order. @since 0.6.45
2593
2641
  * @returns
2594
- */ setProvideRegistry(provide) {
2642
+ */ setProvideRegistry(provide, opts) {
2595
2643
  if (this.initialized) this.logger.warn("[moost] setProvideRegistry() called after init() — already-bound controllers will not see these providers. Register providers before init(), or provide them via @Provide on a parent controller.");
2596
- this.provide = {
2644
+ if (opts?.override === false) this.defaultProvide = {
2645
+ ...this.defaultProvide,
2646
+ ...provide
2647
+ };
2648
+ else this.provide = {
2597
2649
  ...this.provide,
2598
2650
  ...provide
2599
2651
  };
@@ -2606,16 +2658,61 @@ function _define_property(obj, key, value) {
2606
2658
  * replace registry once when binding controllers, so replacements added
2607
2659
  * later are never seen by already-bound controllers.
2608
2660
  * @param replace - Replace Registry (use createReplaceRegistry from '\@prostojs/infact')
2661
+ * @param opts - `{ override: false }` registers a default: used only when neither `@Replace`
2662
+ * on the app subclass nor a normal `setReplaceRegistry` entry exists for the class,
2663
+ * regardless of call order. @since 0.6.45
2609
2664
  * @returns
2610
- */ setReplaceRegistry(replace) {
2665
+ */ setReplaceRegistry(replace, opts) {
2611
2666
  if (this.initialized) this.logger.warn("[moost] setReplaceRegistry() called after init() — already-bound controllers will not see these replacements. Register replacements before init().");
2612
- this.replace = {
2667
+ if (opts?.override === false) this.defaultReplace = {
2668
+ ...this.defaultReplace,
2669
+ ...replace
2670
+ };
2671
+ else this.replace = {
2613
2672
  ...this.replace,
2614
2673
  ...replace
2615
2674
  };
2616
2675
  return this;
2617
2676
  }
2618
2677
  /**
2678
+ * Effective app-level replace registry, lowest to highest precedence: defaults
2679
+ * (`override: false`), `@Replace` on the app subclass, `setReplaceRegistry()` entries.
2680
+ * A controller's own `@Replace` is NOT app-level (it only reaches that controller's direct imports).
2681
+ */ appReplaceRegistry() {
2682
+ return {
2683
+ ...this.defaultReplace,
2684
+ ...getMoostMate().read(this)?.replace,
2685
+ ...this.replace
2686
+ };
2687
+ }
2688
+ /** Effective app-level provide registry (same layering as `appReplaceRegistry`). */ appProvideRegistry() {
2689
+ return {
2690
+ ...this.defaultProvide,
2691
+ ...getMoostMate().read(this)?.provide,
2692
+ ...this.provide
2693
+ };
2694
+ }
2695
+ /**
2696
+ * The class `type` is replaced with app-wide (one hop, like the DI container), or `undefined`.
2697
+ * Works before and after `init()`; never warns. Per-controller `@Replace` is not reported.
2698
+ * @since 0.6.45
2699
+ */ getReplacement(type) {
2700
+ return this.appReplaceRegistry()[(0, _prostojs_infact.getClassKey)(type)];
2701
+ }
2702
+ /**
2703
+ * `getReplacement(type) !== undefined`.
2704
+ * @since 0.6.45
2705
+ */ hasReplacement(type) {
2706
+ return this.getReplacement(type) !== void 0;
2707
+ }
2708
+ /**
2709
+ * Frozen shallow copy of the effective app-level replace registry
2710
+ * (keys are `getClassKey(Class)` symbols). Mutating it never affects the app.
2711
+ * @since 0.6.45
2712
+ */ getReplaceRegistry() {
2713
+ return Object.freeze(this.appReplaceRegistry());
2714
+ }
2715
+ /**
2619
2716
  * Register controllers with the app (similar to the `@ImportController` decorator).
2620
2717
  *
2621
2718
  * Accepted forms (mixable in one call):
@@ -2666,7 +2763,7 @@ function _define_property(obj, key, value) {
2666
2763
  this.logger.info(`${prefix || ""}${c}${eventName} ${"\x1B[0m\x1B[2m\x1B[32m" + c}→ ${classConstructor.name}.${"\x1B[36m" + c}${method}()${coff}`);
2667
2764
  }
2668
2765
  constructor(options) {
2669
- super(), _define_property(this, "options", void 0), _define_property(this, "logger", void 0), _define_property(this, "pipes", void 0), _define_property(this, "interceptors", void 0), _define_property(this, "adapters", void 0), _define_property(this, "controllersOverview", void 0), _define_property(this, "handlerOverviewIndex", void 0), _define_property(this, "initHooks", void 0), _define_property(this, "provide", void 0), _define_property(this, "replace", void 0), _define_property(this, "unregisteredControllers", void 0), _define_property(this, "paramAuditFindings", void 0), _define_property(this, "paramAuditMode", void 0), _define_property(this, "inheritanceAuditMode", void 0), _define_property(this, "initialized", void 0), _define_property(this, "singletonInstances", void 0), _define_property(this, "disposePromise", void 0), _define_property(this, "globalInterceptorHandler", void 0), this.options = options, this.pipes = Array.from(sharedPipes), this.interceptors = [], this.adapters = [], this.controllersOverview = [], this.initHooks = [], this.provide = (0, _prostojs_infact.createProvideRegistry)([_prostojs_infact.Infact, getMoostInfact], [_prostojs_mate.Mate, getMoostMate]), this.replace = {}, this.unregisteredControllers = [], this.paramAuditFindings = [], this.initialized = false, this.singletonInstances = /* @__PURE__ */ new Set();
2766
+ super(), _define_property(this, "options", void 0), _define_property(this, "logger", void 0), _define_property(this, "pipes", void 0), _define_property(this, "interceptors", void 0), _define_property(this, "adapters", void 0), _define_property(this, "controllersOverview", void 0), _define_property(this, "handlerOverviewIndex", void 0), _define_property(this, "initHooks", void 0), _define_property(this, "initHookPhase", void 0), _define_property(this, "provide", void 0), _define_property(this, "replace", void 0), _define_property(this, "defaultProvide", void 0), _define_property(this, "defaultReplace", void 0), _define_property(this, "unregisteredControllers", void 0), _define_property(this, "paramAuditFindings", void 0), _define_property(this, "paramAuditMode", void 0), _define_property(this, "inheritanceAuditMode", void 0), _define_property(this, "initialized", void 0), _define_property(this, "singletonInstances", void 0), _define_property(this, "disposePromise", void 0), _define_property(this, "globalInterceptorHandler", void 0), this.options = options, this.pipes = Array.from(sharedPipes), this.interceptors = [], this.adapters = [], this.controllersOverview = [], this.initHooks = [], this.initHookPhase = "pending", this.provide = (0, _prostojs_infact.createProvideRegistry)([_prostojs_infact.Infact, getMoostInfact], [_prostojs_mate.Mate, getMoostMate]), this.replace = {}, this.defaultProvide = {}, this.defaultReplace = {}, this.unregisteredControllers = [], this.paramAuditFindings = [], this.initialized = false, this.singletonInstances = /* @__PURE__ */ new Set();
2670
2767
  this.paramAuditMode = resolveParamAuditMode(options?.diagnostics?.paramTypes);
2671
2768
  this.inheritanceAuditMode = options?.diagnostics?.inheritance || "warn";
2672
2769
  this.logger = options?.logger || getDefaultLogger(`moost`);
package/dist/index.d.ts CHANGED
@@ -468,17 +468,25 @@ interface TParamAuditFinding {
468
468
  type TParamAuditMode = 'error' | 'warn' | 'off';
469
469
 
470
470
  /**
471
- * A `@MoostInit`-decorated controller method, collected at bind time and run
472
- * once by `Moost.init()` after all controllers are bound. Interceptors are not
473
- * applied; params resolve through the RESOLVE pipe only.
474
- */
475
- interface TInitHook {
471
+ * An init hook collected for `Moost.init()`: either a `@MoostInit`-decorated
472
+ * controller method (collected at bind time) or a function registered with
473
+ * `Moost.addInitHook`. Interceptors are not applied; params of method hooks
474
+ * resolve through the RESOLVE pipe only.
475
+ */
476
+ type TInitHook = TMethodInitHook | TFnInitHook;
477
+ interface TMethodInitHook {
478
+ kind?: 'method';
476
479
  priority: number;
477
480
  method: string;
478
481
  computedPrefix: string;
479
482
  getInstance: () => Promise<TObject> | TObject;
480
483
  resolveArgs?: () => unknown[] | Promise<unknown[]>;
481
484
  }
485
+ interface TFnInitHook {
486
+ kind: 'fn';
487
+ priority: number;
488
+ fn: (app: never) => unknown;
489
+ }
482
490
 
483
491
  /** Mode for the bind-time inheritance audit (see `TMoostOptions['diagnostics']`). */
484
492
  type TInheritanceAuditMode = 'warn' | 'off';
@@ -623,6 +631,16 @@ interface TControllerRegistration {
623
631
  */
624
632
  prependPrefix?: string;
625
633
  }
634
+ /** Options of `setProvideRegistry` / `setReplaceRegistry`. @since 0.6.45 */
635
+ interface TRegistryMergeOptions {
636
+ /**
637
+ * `true` (default): entries replace existing ones for the same token.
638
+ * `false`: register a lowest-precedence default, used only when neither the app subclass's
639
+ * class-level decorator nor a normal `set*Registry()` entry exists for the token — regardless
640
+ * of call order.
641
+ */
642
+ override?: boolean;
643
+ }
626
644
  /**
627
645
  * ## Moost
628
646
  * Main moostjs class that serves as a shell for Moost Adapters
@@ -682,8 +700,12 @@ declare class Moost extends Hookable {
682
700
  protected controllersOverview: TControllerOverview[];
683
701
  protected handlerOverviewIndex?: Map<TFunction, Map<string, THandlerOverview[]>>;
684
702
  protected initHooks: TInitHook[];
703
+ protected initHookPhase: 'pending' | 'running' | 'done';
685
704
  protected provide: TProvideRegistry;
686
705
  protected replace: TReplaceRegistry;
706
+ /** Lowest-precedence layers, filled by `set*Registry(reg, { override: false })`. */
707
+ protected defaultProvide: TProvideRegistry;
708
+ protected defaultReplace: TReplaceRegistry;
687
709
  protected unregisteredControllers: TControllerRegistration[];
688
710
  /** D1 param-audit findings collected while binding controllers, flushed by `init()`. */
689
711
  protected paramAuditFindings: TParamAuditFinding[];
@@ -731,14 +753,41 @@ declare class Moost extends Hookable {
731
753
  */
732
754
  init(): Promise<void>;
733
755
  /**
734
- * Runs every `@MoostInit`-decorated controller method exactly once, after all
735
- * controllers are bound (complete `getControllersOverview()`) and before the
736
- * `adapter.onInit` loop. Hooks run in ascending `priority`, then registration
737
- * order. Each runs on its controller's SINGLETON instance inside a synthetic
738
- * init context (no interceptors; params resolve via the RESOLVE pipe only).
739
- * A throwing hook rejects `init()` (fail-fast).
756
+ * Runs every init hook exactly once, after all controllers are bound
757
+ * (complete `getControllersOverview()`) and before the `adapter.onInit`
758
+ * loop: `@MoostInit` controller methods and functions registered with
759
+ * {@link addInitHook}. Hooks run in ascending `priority`, then registration
760
+ * order. Each runs inside a synthetic init context (no interceptors; params
761
+ * of `@MoostInit` methods resolve via the RESOLVE pipe only). Hooks added
762
+ * while the pass runs are picked up. A throwing hook rejects `init()`
763
+ * (fail-fast).
740
764
  */
741
765
  protected runInitHooks(): Promise<void>;
766
+ protected runInitHook(hook: TInitHook): Promise<void>;
767
+ /**
768
+ * ### addInitHook
769
+ * Programmatic counterpart of `@MoostInit`: runs `fn` once during `init()`,
770
+ * after every controller is bound (complete `getControllersOverview()`) and
771
+ * before the adapters' `onInit`, in the same pass as `@MoostInit` hooks:
772
+ * ascending `priority` (default 0), then registration order. Runs inside the
773
+ * same synthetic init context (controller context = the app; no
774
+ * interceptors, no request composables). A throwing hook rejects `init()`.
775
+ *
776
+ * May be called at any time:
777
+ * - before the hook pass starts (including from a controller constructor
778
+ * during binding) — queued;
779
+ * - 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
782
+ * reject.
783
+ *
784
+ * No de-duplication: registering the same function twice runs it twice.
785
+ * Hooks re-run on re-init exactly like `@MoostInit` hooks.
786
+ * @since 0.6.46
787
+ */
788
+ addInitHook(fn: (app: this) => unknown, opts?: {
789
+ priority?: number;
790
+ }): void;
742
791
  /**
743
792
  * ### dispose
744
793
  * Graceful shutdown: stops the adapters, then runs every `@MoostDispose`
@@ -833,9 +882,12 @@ declare class Moost extends Hookable {
833
882
  * controller — it flows parent → child through `@ImportController`, never
834
883
  * to siblings.
835
884
  * @param provide - Provide Registry (use createProvideRegistry from '\@prostojs/infact')
885
+ * @param opts - `{ override: false }` registers a default: used only when neither `@Provide`
886
+ * on the app subclass nor a normal `setProvideRegistry` entry exists for the token,
887
+ * regardless of call order. @since 0.6.45
836
888
  * @returns
837
889
  */
838
- setProvideRegistry(provide: TProvideRegistry): this;
890
+ setProvideRegistry(provide: TProvideRegistry, opts?: TRegistryMergeOptions): this;
839
891
  /**
840
892
  * Register replace classes to provide as dependency injections
841
893
  *
@@ -843,9 +895,37 @@ declare class Moost extends Hookable {
843
895
  * replace registry once when binding controllers, so replacements added
844
896
  * later are never seen by already-bound controllers.
845
897
  * @param replace - Replace Registry (use createReplaceRegistry from '\@prostojs/infact')
898
+ * @param opts - `{ override: false }` registers a default: used only when neither `@Replace`
899
+ * on the app subclass nor a normal `setReplaceRegistry` entry exists for the class,
900
+ * regardless of call order. @since 0.6.45
846
901
  * @returns
847
902
  */
848
- setReplaceRegistry(replace: TReplaceRegistry): this;
903
+ setReplaceRegistry(replace: TReplaceRegistry, opts?: TRegistryMergeOptions): this;
904
+ /**
905
+ * Effective app-level replace registry, lowest to highest precedence: defaults
906
+ * (`override: false`), `@Replace` on the app subclass, `setReplaceRegistry()` entries.
907
+ * A controller's own `@Replace` is NOT app-level (it only reaches that controller's direct imports).
908
+ */
909
+ protected appReplaceRegistry(): TReplaceRegistry;
910
+ /** Effective app-level provide registry (same layering as `appReplaceRegistry`). */
911
+ protected appProvideRegistry(): TProvideRegistry;
912
+ /**
913
+ * The class `type` is replaced with app-wide (one hop, like the DI container), or `undefined`.
914
+ * Works before and after `init()`; never warns. Per-controller `@Replace` is not reported.
915
+ * @since 0.6.45
916
+ */
917
+ getReplacement<T>(type: TClassConstructor<T>): TClassConstructor<T> | undefined;
918
+ /**
919
+ * `getReplacement(type) !== undefined`.
920
+ * @since 0.6.45
921
+ */
922
+ hasReplacement(type: TClassConstructor): boolean;
923
+ /**
924
+ * Frozen shallow copy of the effective app-level replace registry
925
+ * (keys are `getClassKey(Class)` symbols). Mutating it never affects the app.
926
+ * @since 0.6.45
927
+ */
928
+ getReplaceRegistry(): Readonly<TReplaceRegistry>;
849
929
  /**
850
930
  * Register controllers with the app (similar to the `@ImportController` decorator).
851
931
  *
@@ -1541,4 +1621,4 @@ declare function createLogger(opts?: Partial<TProstoLoggerOptions>): ProstoLogge
1541
1621
  declare const loggerConsoleTransport: _prostojs_logger.TProstoLoggerTransportFn<any>;
1542
1622
 
1543
1623
  export { After, ApplyDecorators, Before, Circular, Const, ConstFactory, Controller, Description, HandlerPaths, Id, ImportController, Inherit, Inject, InjectEventLogger, InjectFromScope, InjectMoost, InjectMoostLogger, InjectScopeVars, Injectable, Intercept, Interceptor, InterceptorHandler, Label, LoggerTopic, MOOST_ADAPTER_BRAND, MOOST_MODULE_IDENTITY_KEY, Moost, MoostDispose, MoostInit, OnError, Optional, Overtake, Param, Params, Pipe, Provide, Replace, Required, Resolve, Response, TInterceptorPriority, TPipePriority, Value, createLogger, defineAfterInterceptor, defineBeforeInterceptor, defineErrorInterceptor, defineInfactScope, defineInterceptor, defineMoostEventHandler, definePipeFn, disposeInstances, forkEventContext, getHandlerPaths, getInfactScopeVars, getInfactSingletonInstances, getInstanceOwnMethods, getInstanceOwnProps, getMoostInfact, getMoostMate, getNewMoostInfact, globalKey, isThenable, loggerConsoleTransport, mergeSorted, registerEventScope, resolvePipe, setControllerContext, setInfactLoggingOptions, setInterceptResult, setOvertake, stampModuleIdentity, stampOnce, useControllerContext, useHandlerPaths, useInterceptResult, useOvertake, useScopeId, withControllerContext };
1544
- export type { TAny, TAnyFn, TClassConstructor, TContextInjectorHook, TControllerOverview, TControllerRegistration, TControllersGroup, TDisposeError, TDisposeOptions, TDisposeResult, TEmpty, TForkEventContextOptions, TFunction, TGetHandlerPathsOptions, TInjectableScope, TInterceptorAfterFn, TInterceptorBeforeFn, TInterceptorData, TInterceptorDef, TInterceptorDefFactory, TInterceptorEntry, TInterceptorErrorFn, TIsolatedSlot, TLogger, TMoostAdapter, TMoostAdapterOptions, TMoostEventHandlerHookOptions, TMoostEventHandlerOptions, TMoostHandler, TMoostMetadata, TMoostModuleIdentity, TMoostOptions, TMoostParamsMetadata, TObject, TOvertakeFn, TPipeData, TPipeFn, TPipeMetas, TPrimitives, TWithControllerContextOptions };
1624
+ export type { TAny, TAnyFn, TClassConstructor, TContextInjectorHook, TControllerOverview, TControllerRegistration, TControllersGroup, TDisposeError, TDisposeOptions, TDisposeResult, TEmpty, TForkEventContextOptions, TFunction, TGetHandlerPathsOptions, TInjectableScope, TInterceptorAfterFn, TInterceptorBeforeFn, TInterceptorData, TInterceptorDef, TInterceptorDefFactory, TInterceptorEntry, TInterceptorErrorFn, TIsolatedSlot, TLogger, TMoostAdapter, TMoostAdapterOptions, TMoostEventHandlerHookOptions, TMoostEventHandlerOptions, TMoostHandler, TMoostMetadata, TMoostModuleIdentity, TMoostOptions, TMoostParamsMetadata, TObject, TOvertakeFn, TPipeData, TPipeFn, TPipeMetas, TPrimitives, TRegistryMergeOptions, TWithControllerContextOptions };
package/dist/index.mjs CHANGED
@@ -47,7 +47,7 @@ let stamped = false;
47
47
  if (stamped) return;
48
48
  stamped = true;
49
49
  stampModuleIdentity({
50
- version: "0.6.44",
50
+ version: "0.6.46",
51
51
  path: typeof __filename === "string" ? __filename : import.meta.url
52
52
  });
53
53
  }
@@ -2322,6 +2322,7 @@ function _define_property(obj, key, value) {
2322
2322
  }
2323
2323
  if (typeof a.getProvideRegistry === "function") this.setProvideRegistry(a.getProvideRegistry());
2324
2324
  }
2325
+ this.initHookPhase = "pending";
2325
2326
  this.unregisteredControllers.unshift({ controller: this });
2326
2327
  let auditError;
2327
2328
  try {
@@ -2336,16 +2337,36 @@ function _define_property(obj, key, value) {
2336
2337
  this.initialized = true;
2337
2338
  }
2338
2339
  /**
2339
- * Runs every `@MoostInit`-decorated controller method exactly once, after all
2340
- * controllers are bound (complete `getControllersOverview()`) and before the
2341
- * `adapter.onInit` loop. Hooks run in ascending `priority`, then registration
2342
- * order. Each runs on its controller's SINGLETON instance inside a synthetic
2343
- * init context (no interceptors; params resolve via the RESOLVE pipe only).
2344
- * A throwing hook rejects `init()` (fail-fast).
2340
+ * Runs every init hook exactly once, after all controllers are bound
2341
+ * (complete `getControllersOverview()`) and before the `adapter.onInit`
2342
+ * loop: `@MoostInit` controller methods and functions registered with
2343
+ * {@link addInitHook}. Hooks run in ascending `priority`, then registration
2344
+ * order. Each runs inside a synthetic init context (no interceptors; params
2345
+ * of `@MoostInit` methods resolve via the RESOLVE pipe only). Hooks added
2346
+ * while the pass runs are picked up. A throwing hook rejects `init()`
2347
+ * (fail-fast).
2345
2348
  */ async runInitHooks() {
2346
- if (this.initHooks.length === 0) return;
2347
- const hooks = this.initHooks.toSorted((a, b) => a.priority - b.priority);
2348
- for (const hook of hooks) await createEventContext$1({ logger: this.logger }, async () => {
2349
+ this.initHookPhase = "running";
2350
+ const done = /* @__PURE__ */ new Set();
2351
+ try {
2352
+ for (;;) {
2353
+ let next;
2354
+ for (const hook of this.initHooks) if (!done.has(hook) && (!next || hook.priority < next.priority)) next = hook;
2355
+ if (!next) break;
2356
+ done.add(next);
2357
+ await this.runInitHook(next);
2358
+ }
2359
+ } finally {
2360
+ this.initHookPhase = "done";
2361
+ }
2362
+ }
2363
+ async runInitHook(hook) {
2364
+ await createEventContext$1({ logger: this.logger }, async () => {
2365
+ if (hook.kind === "fn") {
2366
+ setControllerContext(this, "init", "", { prefix: "" });
2367
+ await hook.fn(this);
2368
+ return;
2369
+ }
2349
2370
  const instance = await hook.getInstance();
2350
2371
  setControllerContext(instance, hook.method, "", { prefix: hook.computedPrefix });
2351
2372
  const args = hook.resolveArgs ? await hook.resolveArgs() : [];
@@ -2353,6 +2374,37 @@ function _define_property(obj, key, value) {
2353
2374
  });
2354
2375
  }
2355
2376
  /**
2377
+ * ### addInitHook
2378
+ * Programmatic counterpart of `@MoostInit`: runs `fn` once during `init()`,
2379
+ * after every controller is bound (complete `getControllersOverview()`) and
2380
+ * before the adapters' `onInit`, in the same pass as `@MoostInit` hooks:
2381
+ * ascending `priority` (default 0), then registration order. Runs inside the
2382
+ * same synthetic init context (controller context = the app; no
2383
+ * interceptors, no request composables). A throwing hook rejects `init()`.
2384
+ *
2385
+ * May be called at any time:
2386
+ * - before the hook pass starts (including from a controller constructor
2387
+ * during binding) — queued;
2388
+ * - 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
2391
+ * reject.
2392
+ *
2393
+ * No de-duplication: registering the same function twice runs it twice.
2394
+ * Hooks re-run on re-init exactly like `@MoostInit` hooks.
2395
+ * @since 0.6.46
2396
+ */ addInitHook(fn, opts) {
2397
+ const hook = {
2398
+ kind: "fn",
2399
+ priority: opts?.priority ?? 0,
2400
+ fn
2401
+ };
2402
+ 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
+ }
2407
+ /**
2356
2408
  * ### dispose
2357
2409
  * Graceful shutdown: stops the adapters, then runs every `@MoostDispose`
2358
2410
  * hook of every singleton this app can reach — DI registry singletons
@@ -2456,15 +2508,8 @@ function _define_property(obj, key, value) {
2456
2508
  if (fatal.length > 0 && this.paramAuditMode === "error") return formatParamAuditError(fatal);
2457
2509
  }
2458
2510
  async bindControllers() {
2459
- const thisMeta = getMoostMate().read(this);
2460
- const provide = {
2461
- ...thisMeta?.provide,
2462
- ...this.provide
2463
- };
2464
- const replace = {
2465
- ...thisMeta?.replace,
2466
- ...this.replace
2467
- };
2511
+ const provide = this.appProvideRegistry();
2512
+ const replace = this.appReplaceRegistry();
2468
2513
  const globalPrefix = this.options?.globalPrefix || "";
2469
2514
  for (const { controller, prependPrefix, replaceOwnPrefix } of this.unregisteredControllers) await this.bindController(controller, provide, replace, prependPrefix ? `${globalPrefix}/${prependPrefix}` : globalPrefix, replaceOwnPrefix);
2470
2515
  this.unregisteredControllers = [];
@@ -2522,7 +2567,7 @@ function _define_property(obj, key, value) {
2522
2567
  ...classMeta.provide
2523
2568
  };
2524
2569
  const mergedReplace = {
2525
- ...this.replace,
2570
+ ...this.appReplaceRegistry(),
2526
2571
  ...classMeta.replace
2527
2572
  };
2528
2573
  for (const ic of classMeta.importController) if (ic.typeResolver) {
@@ -2589,10 +2634,17 @@ function _define_property(obj, key, value) {
2589
2634
  * controller — it flows parent → child through `@ImportController`, never
2590
2635
  * to siblings.
2591
2636
  * @param provide - Provide Registry (use createProvideRegistry from '\@prostojs/infact')
2637
+ * @param opts - `{ override: false }` registers a default: used only when neither `@Provide`
2638
+ * on the app subclass nor a normal `setProvideRegistry` entry exists for the token,
2639
+ * regardless of call order. @since 0.6.45
2592
2640
  * @returns
2593
- */ setProvideRegistry(provide) {
2641
+ */ setProvideRegistry(provide, opts) {
2594
2642
  if (this.initialized) this.logger.warn("[moost] setProvideRegistry() called after init() — already-bound controllers will not see these providers. Register providers before init(), or provide them via @Provide on a parent controller.");
2595
- this.provide = {
2643
+ if (opts?.override === false) this.defaultProvide = {
2644
+ ...this.defaultProvide,
2645
+ ...provide
2646
+ };
2647
+ else this.provide = {
2596
2648
  ...this.provide,
2597
2649
  ...provide
2598
2650
  };
@@ -2605,16 +2657,61 @@ function _define_property(obj, key, value) {
2605
2657
  * replace registry once when binding controllers, so replacements added
2606
2658
  * later are never seen by already-bound controllers.
2607
2659
  * @param replace - Replace Registry (use createReplaceRegistry from '\@prostojs/infact')
2660
+ * @param opts - `{ override: false }` registers a default: used only when neither `@Replace`
2661
+ * on the app subclass nor a normal `setReplaceRegistry` entry exists for the class,
2662
+ * regardless of call order. @since 0.6.45
2608
2663
  * @returns
2609
- */ setReplaceRegistry(replace) {
2664
+ */ setReplaceRegistry(replace, opts) {
2610
2665
  if (this.initialized) this.logger.warn("[moost] setReplaceRegistry() called after init() — already-bound controllers will not see these replacements. Register replacements before init().");
2611
- this.replace = {
2666
+ if (opts?.override === false) this.defaultReplace = {
2667
+ ...this.defaultReplace,
2668
+ ...replace
2669
+ };
2670
+ else this.replace = {
2612
2671
  ...this.replace,
2613
2672
  ...replace
2614
2673
  };
2615
2674
  return this;
2616
2675
  }
2617
2676
  /**
2677
+ * Effective app-level replace registry, lowest to highest precedence: defaults
2678
+ * (`override: false`), `@Replace` on the app subclass, `setReplaceRegistry()` entries.
2679
+ * A controller's own `@Replace` is NOT app-level (it only reaches that controller's direct imports).
2680
+ */ appReplaceRegistry() {
2681
+ return {
2682
+ ...this.defaultReplace,
2683
+ ...getMoostMate().read(this)?.replace,
2684
+ ...this.replace
2685
+ };
2686
+ }
2687
+ /** Effective app-level provide registry (same layering as `appReplaceRegistry`). */ appProvideRegistry() {
2688
+ return {
2689
+ ...this.defaultProvide,
2690
+ ...getMoostMate().read(this)?.provide,
2691
+ ...this.provide
2692
+ };
2693
+ }
2694
+ /**
2695
+ * The class `type` is replaced with app-wide (one hop, like the DI container), or `undefined`.
2696
+ * Works before and after `init()`; never warns. Per-controller `@Replace` is not reported.
2697
+ * @since 0.6.45
2698
+ */ getReplacement(type) {
2699
+ return this.appReplaceRegistry()[getClassKey$1(type)];
2700
+ }
2701
+ /**
2702
+ * `getReplacement(type) !== undefined`.
2703
+ * @since 0.6.45
2704
+ */ hasReplacement(type) {
2705
+ return this.getReplacement(type) !== void 0;
2706
+ }
2707
+ /**
2708
+ * Frozen shallow copy of the effective app-level replace registry
2709
+ * (keys are `getClassKey(Class)` symbols). Mutating it never affects the app.
2710
+ * @since 0.6.45
2711
+ */ getReplaceRegistry() {
2712
+ return Object.freeze(this.appReplaceRegistry());
2713
+ }
2714
+ /**
2618
2715
  * Register controllers with the app (similar to the `@ImportController` decorator).
2619
2716
  *
2620
2717
  * Accepted forms (mixable in one call):
@@ -2665,7 +2762,7 @@ function _define_property(obj, key, value) {
2665
2762
  this.logger.info(`${prefix || ""}${c}${eventName} ${"\x1B[0m\x1B[2m\x1B[32m" + c}→ ${classConstructor.name}.${"\x1B[36m" + c}${method}()${coff}`);
2666
2763
  }
2667
2764
  constructor(options) {
2668
- super(), _define_property(this, "options", void 0), _define_property(this, "logger", void 0), _define_property(this, "pipes", void 0), _define_property(this, "interceptors", void 0), _define_property(this, "adapters", void 0), _define_property(this, "controllersOverview", void 0), _define_property(this, "handlerOverviewIndex", void 0), _define_property(this, "initHooks", void 0), _define_property(this, "provide", void 0), _define_property(this, "replace", void 0), _define_property(this, "unregisteredControllers", void 0), _define_property(this, "paramAuditFindings", void 0), _define_property(this, "paramAuditMode", void 0), _define_property(this, "inheritanceAuditMode", void 0), _define_property(this, "initialized", void 0), _define_property(this, "singletonInstances", void 0), _define_property(this, "disposePromise", void 0), _define_property(this, "globalInterceptorHandler", void 0), this.options = options, this.pipes = Array.from(sharedPipes), this.interceptors = [], this.adapters = [], this.controllersOverview = [], this.initHooks = [], this.provide = createProvideRegistry$1([Infact, getMoostInfact], [Mate, getMoostMate]), this.replace = {}, this.unregisteredControllers = [], this.paramAuditFindings = [], this.initialized = false, this.singletonInstances = /* @__PURE__ */ new Set();
2765
+ 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();
2669
2766
  this.paramAuditMode = resolveParamAuditMode(options?.diagnostics?.paramTypes);
2670
2767
  this.inheritanceAuditMode = options?.diagnostics?.inheritance || "warn";
2671
2768
  this.logger = options?.logger || getDefaultLogger(`moost`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moost",
3
- "version": "0.6.44",
3
+ "version": "0.6.46",
4
4
  "description": "moost",
5
5
  "keywords": [
6
6
  "composables",
@@ -49,7 +49,7 @@
49
49
  "@wooksjs/event-http": "^0.7.26",
50
50
  "@wooksjs/http-body": "^0.7.26",
51
51
  "vitest": "3.2.7",
52
- "@moostjs/event-http": "^0.6.44"
52
+ "@moostjs/event-http": "^0.6.46"
53
53
  },
54
54
  "scripts": {
55
55
  "pub": "pnpm publish --access public",