moost 0.7.3 → 0.7.5

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.7.3",
51
+ version: "0.7.5",
52
52
  path: typeof __filename === "string" ? __filename : require("url").pathToFileURL(__filename).href
53
53
  });
54
54
  }
@@ -3182,11 +3182,44 @@ function _define_property(obj, key, value) {
3182
3182
  return getHandlerPaths(moost, getController(), method ?? getMethod() ?? "", opts);
3183
3183
  }
3184
3184
 
3185
+ //#endregion
3186
+ //#region packages/moost/src/wooks-owner.ts
3187
+ /** Per Wooks instance: the first claim of each adapter (by `name`) bound into it. */ const owners = /* @__PURE__ */ new WeakMap();
3188
+ /** Wooks instances already warned about: the warning is logged once per instance. */ const warned = /* @__PURE__ */ new WeakSet();
3189
+ /**
3190
+ * Records that `app` binds the handlers of its `adapter` (the adapter's `name`) into `wooks`
3191
+ * (the Wooks instance its routes go to: `wooksApp.getWooks()`). Adapters call it from `onInit`.
3192
+ *
3193
+ * Adapters created without a Wooks app of their own share the process-wide global Wooks, so two
3194
+ * apps in one process (typically in tests) route through one router. When another app already
3195
+ * bound handlers of the same adapter into `wooks`, and either adapter created its Wooks app
3196
+ * (`opts.created`), it warns — once per Wooks instance, with the app's logger. Apps that share
3197
+ * a Wooks app they pass in on purpose stay silent; re-initialising the same app does not warn.
3198
+ * A disposed app still counts: `dispose()` does not remove its routes.
3199
+ */ function claimWooks(app, wooks, adapter, opts) {
3200
+ const created = opts?.created === true;
3201
+ let byAdapter = owners.get(wooks);
3202
+ if (!byAdapter) {
3203
+ byAdapter = /* @__PURE__ */ new Map();
3204
+ owners.set(wooks, byAdapter);
3205
+ }
3206
+ const owner = byAdapter.get(adapter);
3207
+ if (owner === void 0) byAdapter.set(adapter, {
3208
+ app,
3209
+ created
3210
+ });
3211
+ else if (owner.app !== app && (created || owner.created) && !warned.has(wooks)) {
3212
+ warned.add(wooks);
3213
+ app.getLogger().warn(`[moost] A second Moost app binds "${adapter}" handlers into the Wooks instance another app already uses (adapters created without their own Wooks app share the process-wide global one). Both apps' routes now live in one router: an event on a route both register runs the first app's handler chain, and when that throws, its error is swallowed and the next app's chain runs instead. Call clearGlobalWooks() before creating each app (e.g. in a test's beforeEach): the next adapter then gets a fresh Wooks. (Logged once per Wooks instance.)`);
3214
+ }
3215
+ }
3216
+
3185
3217
  //#endregion
3186
3218
  //#region packages/moost/src/i18n.ts
3187
3219
  var _globalThis, _STATE, _state, _state1, _state2, _state3, _state4;
3188
3220
  const STATE = Symbol.for("moost.i18n");
3189
3221
  const NO_LOCALES = Object.freeze([]);
3222
+ /** Distinct scopes given memo keys per generation: keeps every memo keyed by `key` bounded. */ const MAX_SCOPES = 32;
3190
3223
  const state = (_globalThis = globalThis)[_STATE = STATE] ?? (_globalThis[_STATE] = {});
3191
3224
  (_state = state).locales ?? (_state.locales = NO_LOCALES);
3192
3225
  (_state1 = state).generation ?? (_state1.generation = 0);
@@ -3216,9 +3249,15 @@ const state = (_globalThis = globalThis)[_STATE = STATE] ?? (_globalThis[_STATE]
3216
3249
  */ function setTranslator(translate, opts) {
3217
3250
  const isMap = typeof translate === "object";
3218
3251
  state.translate = isMap ? mapTranslator(translate) : translate;
3252
+ state.scoping = opts?.scope && {
3253
+ translate: state.translate,
3254
+ get: opts.scope
3255
+ };
3256
+ state.noPathPrefix = opts?.pathPrefix === false ? state.translate : void 0;
3219
3257
  const locales = opts?.locales ?? (isMap ? Object.keys(translate) : void 0);
3220
3258
  state.locales = translate && locales?.length ? Object.freeze([...locales]) : NO_LOCALES;
3221
3259
  state.failureLogged = false;
3260
+ state.scopeFailureLogged = false;
3222
3261
  newGeneration();
3223
3262
  }
3224
3263
  /** The registered translator (a map wrapped as a function), or `undefined`. */ function getTranslator() {
@@ -3244,7 +3283,7 @@ const state = (_globalThis = globalThis)[_STATE = STATE] ?? (_globalThis[_STATE]
3244
3283
  }
3245
3284
  if (changed) newGeneration();
3246
3285
  }
3247
- /** The only invalidation: drops everything derived from the translator and the templates. */ function newGeneration() {
3286
+ /** Drops everything derived from the translator and the templates. */ function newGeneration() {
3248
3287
  state.generation++;
3249
3288
  state.memo.clear();
3250
3289
  }
@@ -3279,10 +3318,7 @@ const PLACEHOLDER = /\{(\w+)\}/g;
3279
3318
  let translateCtx;
3280
3319
  return (id, params, fallback) => {
3281
3320
  const translate = state.translate;
3282
- return (translate ? callTranslator(translate, id, params, translateCtx ?? (translateCtx = {
3283
- locale: (0, _wooksjs_event_core.useLocale)(event).get(),
3284
- event
3285
- })) : void 0) ?? untranslated(id, params, fallback);
3321
+ return (translate ? callTranslator(translate, id, params, translateCtx ?? (translateCtx = translateContext(event, (0, _wooksjs_event_core.useLocale)(event).get(), readScope(event)))) : void 0) ?? untranslated(id, params, fallback);
3286
3322
  };
3287
3323
  }
3288
3324
  /** `t`'s text without a translation: `fallback` as is, else the filled template (or id). */ function untranslated(id, params, fallback) {
@@ -3298,12 +3334,11 @@ const PLACEHOLDER = /\{(\w+)\}/g;
3298
3334
  const event = ctx ?? (0, _wooksjs_event_core.current)();
3299
3335
  const locale = (0, _wooksjs_event_core.useLocale)(event).get();
3300
3336
  if (locale === void 0) return;
3301
- const translateCtx = {
3302
- locale,
3303
- event
3304
- };
3337
+ const scope = readScope(event);
3338
+ const translateCtx = translateContext(event, locale, scope);
3305
3339
  const generation = state.generation;
3306
- const key = state.locales.includes(locale) ? `${generation}:${locale}` : void 0;
3340
+ const partition = typeof scope === "string" ? `${locale}:${scope}` : locale;
3341
+ const key = state.locales.includes(locale) && (scope === void 0 || scope !== null && admitted(scope)) ? `${generation}:${partition}` : void 0;
3307
3342
  const t = (id, params, fallback) => callTranslator(translate, id, params, translateCtx) ?? untranslated(id, params, fallback);
3308
3343
  const messages = () => {
3309
3344
  if (key === void 0 || state.generation !== generation) return translateTemplates(translate, translateCtx);
@@ -3316,31 +3351,82 @@ const PLACEHOLDER = /\{(\w+)\}/g;
3316
3351
  };
3317
3352
  return {
3318
3353
  locale,
3354
+ partition,
3319
3355
  key,
3320
3356
  t,
3321
3357
  messages
3322
3358
  };
3323
3359
  }
3360
+ function translateContext(event, locale, scope) {
3361
+ return typeof scope === "string" ? {
3362
+ locale,
3363
+ scope,
3364
+ event
3365
+ } : {
3366
+ locale,
3367
+ event
3368
+ };
3369
+ }
3370
+ /**
3371
+ * The event's scope (`opts.scope`): `undefined` without one, `null` when the scope function
3372
+ * threw (logged once) — translated unscoped then, and not memoized.
3373
+ */ function readScope(event) {
3374
+ const scoping = state.scoping;
3375
+ if (!scoping || scoping.translate !== state.translate) return;
3376
+ try {
3377
+ const { get } = scoping;
3378
+ const scope = (0, _wooksjs_event_core.tryGetCurrent)() === event ? get() : (0, _wooksjs_event_core.run)(event, get);
3379
+ return typeof scope === "string" ? scope : typeof scope === "number" && Number.isFinite(scope) ? String(scope) : void 0;
3380
+ } catch (error) {
3381
+ logOnce("scopeFailureLogged", event, "The translation scope threw — translating unscoped, without memoizing (logged once)", error);
3382
+ return null;
3383
+ }
3384
+ }
3385
+ /** Whether `scope` gets memo keys: one of the first {@link MAX_SCOPES} of the generation. */ function admitted(scope) {
3386
+ if (state.admittedScopes?.generation !== state.generation) state.admittedScopes = {
3387
+ generation: state.generation,
3388
+ names: /* @__PURE__ */ new Set()
3389
+ };
3390
+ const { names } = state.admittedScopes;
3391
+ if (names.size < MAX_SCOPES) names.add(scope);
3392
+ return names.has(scope);
3393
+ }
3394
+ /** Logs a failure of the translator or the scope function once per `setTranslator`. */ function logOnce(flag, event, message, error) {
3395
+ if (!state[flag]) {
3396
+ state[flag] = true;
3397
+ (0, _wooksjs_event_core.useLogger)("i18n", event).error(message, error);
3398
+ }
3399
+ }
3324
3400
  /**
3325
3401
  * Returns a translated copy of an error body (`{ message, code?, params?, errors? }`) in the
3326
3402
  * event's locale — the body itself when there is no translation ({@link useTranslation}).
3327
3403
  *
3328
3404
  * - each entry of `errors` (and of its entries' `details`): by its `code` (unless `custom`),
3329
- * else by its message text, its English `message` being the fallback;
3330
- * - the top `message`: by `code` when the body has one; else, when it is the first entry's
3331
- * message (`path: message`), the first entry's translation; else by its text.
3405
+ * else by its message text, its English `message` being the fallback; the translator gets the
3406
+ * entry's `path`, `code` and `custom` ({@link TTranslateContext});
3407
+ * - the top `message`: by `code` when the body has one (the translator gets that `code`); else,
3408
+ * when it is the first entry's message (`path: message`), the first entry's translation
3409
+ * (`path: ` dropped with `setTranslator(t, { pathPrefix: false })`); else by its text.
3332
3410
  *
3333
3411
  * `MoostHttp` applies it to every error body when it creates the HTTP app.
3334
3412
  */ function translateErrorBody(body, ctx) {
3335
- const translation = useTranslation(ctx);
3336
- if (!translation) return body;
3337
- const { t } = translation;
3413
+ const event = ctx ?? (0, _wooksjs_event_core.current)();
3414
+ const translate = state.translate;
3415
+ const locale = (0, _wooksjs_event_core.useLocale)(event).get();
3416
+ if (!translate || locale === void 0) return body;
3417
+ const base = translateContext(event, locale, readScope(event));
3418
+ const t = (id, params, fallback, entry) => callTranslator(translate, id, params, entry ? entryContext(base, entry) : base) ?? untranslated(id, params, fallback);
3338
3419
  const source = Array.isArray(body.errors) ? body.errors : void 0;
3339
3420
  const errors = source && translateEntries(source, t);
3340
3421
  let message;
3341
- if (typeof body.code === "string" && body.code) message = t(body.code, body.params, body.message);
3342
- else if (source && errors && isEntry(source[0]) && body.message === topMessage(source[0])) message = topMessage(errors[0]);
3343
- else message = t(body.message, body.params, body.message);
3422
+ if (typeof body.code === "string" && body.code) message = callTranslator(translate, body.code, body.params, {
3423
+ ...base,
3424
+ code: body.code
3425
+ }) ?? untranslated(body.code, body.params, body.message);
3426
+ else if (source && errors && isEntry(source[0]) && body.message === topMessage(source[0])) {
3427
+ const first = errors[0];
3428
+ message = state.noPathPrefix === translate ? first.message : topMessage(first);
3429
+ } else message = t(body.message, body.params, body.message);
3344
3430
  return errors ? {
3345
3431
  ...body,
3346
3432
  message,
@@ -3361,10 +3447,7 @@ function mapTranslator(map) {
3361
3447
  const out = translate(id, params, ctx);
3362
3448
  return typeof out === "string" ? out : void 0;
3363
3449
  } catch (error) {
3364
- if (!state.failureLogged) {
3365
- state.failureLogged = true;
3366
- (0, _wooksjs_event_core.useLogger)("i18n", ctx.event).error(`The translator threw for "${id}" — using the untranslated text (logged once)`, error);
3367
- }
3450
+ logOnce("failureLogged", ctx.event, `The translator threw for "${id}" — using the untranslated text (logged once)`, error);
3368
3451
  return;
3369
3452
  }
3370
3453
  }
@@ -3381,13 +3464,22 @@ function mapTranslator(map) {
3381
3464
  function isEntry(value) {
3382
3465
  return typeof value === "object" && value !== null && typeof value.message === "string";
3383
3466
  }
3467
+ /** `base` with the entry's `path` and `code` (each only when a string) and `custom: true`. */ function entryContext(base, { path, code, custom }) {
3468
+ const hasCode = typeof code === "string" && code !== "";
3469
+ if (typeof path !== "string" && !hasCode && custom !== true) return base;
3470
+ const out = { ...base };
3471
+ if (typeof path === "string") out.path = path;
3472
+ if (hasCode) out.code = code;
3473
+ if (custom === true) out.custom = true;
3474
+ return out;
3475
+ }
3384
3476
  function translateEntries(entries, t) {
3385
3477
  return entries.map((entry) => {
3386
3478
  if (!isEntry(entry)) return entry;
3387
3479
  const id = typeof entry.code === "string" && entry.code && entry.custom !== true ? entry.code : entry.message;
3388
3480
  const out = {
3389
3481
  ...entry,
3390
- message: t(id, entry.params, entry.message)
3482
+ message: t(id, entry.params, entry.message, entry)
3391
3483
  };
3392
3484
  if (Array.isArray(entry.details)) out.details = translateEntries(entry.details, t);
3393
3485
  return out;
@@ -3460,6 +3552,7 @@ Object.defineProperty(exports, 'cached', {
3460
3552
  return _wooksjs_event_core.cached;
3461
3553
  }
3462
3554
  });
3555
+ exports.claimWooks = claimWooks;
3463
3556
  Object.defineProperty(exports, 'clearGlobalWooks', {
3464
3557
  enumerable: true,
3465
3558
  get: function () {
package/dist/index.d.ts CHANGED
@@ -1649,6 +1649,27 @@ declare function mergeSorted<T extends {
1649
1649
  priority: number;
1650
1650
  }>(...lists: (T[] | readonly T[] | undefined)[]): T[];
1651
1651
 
1652
+ /** The options of {@link claimWooks}. */
1653
+ interface TClaimWooksOptions {
1654
+ /**
1655
+ * The adapter created its Wooks app itself (none was passed in), so it routes through the
1656
+ * process-wide global Wooks. Default `false`: an app passed in — sharing it is deliberate.
1657
+ */
1658
+ created?: boolean;
1659
+ }
1660
+ /**
1661
+ * Records that `app` binds the handlers of its `adapter` (the adapter's `name`) into `wooks`
1662
+ * (the Wooks instance its routes go to: `wooksApp.getWooks()`). Adapters call it from `onInit`.
1663
+ *
1664
+ * Adapters created without a Wooks app of their own share the process-wide global Wooks, so two
1665
+ * apps in one process (typically in tests) route through one router. When another app already
1666
+ * bound handlers of the same adapter into `wooks`, and either adapter created its Wooks app
1667
+ * (`opts.created`), it warns — once per Wooks instance, with the app's logger. Apps that share
1668
+ * a Wooks app they pass in on purpose stay silent; re-initialising the same app does not warn.
1669
+ * A disposed app still counts: `dispose()` does not remove its routes.
1670
+ */
1671
+ declare function claimWooks(app: Moost, wooks: object, adapter: string, opts?: TClaimWooksOptions): void;
1672
+
1652
1673
  /**
1653
1674
  * Sets the controller context for the current event scope.
1654
1675
  * Called internally by adapters when dispatching events to handlers.
@@ -1771,12 +1792,40 @@ declare function defineInterceptor(def: TInterceptorDef, priority?: TInterceptor
1771
1792
  */
1772
1793
  declare function definePipeFn<T extends TObject = TEmpty>(fn: TPipeFn<T>, priority?: TPipePriority): TPipeFn<T>;
1773
1794
 
1774
- /** What a translator gets besides the text: the event's locale and the event itself. */
1795
+ /**
1796
+ * What a translator gets besides the text: the event's locale, its scope and the event itself —
1797
+ * plus, when an error body is translated ({@link translateErrorBody}), the entry's `path` and
1798
+ * `code`.
1799
+ */
1775
1800
  interface TTranslateContext {
1776
1801
  /** `useLocale().get()` of the event the text is translated for. */
1777
1802
  locale: string | undefined;
1803
+ /**
1804
+ * The event's translation scope ({@link TTranslatorOptions.scope}) — e.g. a tenant whose
1805
+ * wording differs. `undefined`: the shared wording.
1806
+ */
1807
+ scope?: string;
1778
1808
  /** The event context — read anything else your policy needs from it. */
1779
1809
  event: EventContext;
1810
+ /**
1811
+ * The field path (`'items.0.name'`, `''` for the root) of the error entry being
1812
+ * translated — word a message per field. Only set by {@link translateErrorBody}, for its
1813
+ * `errors` entries (and their `details`); error bodies are never memoized, while the memoized
1814
+ * {@link TTranslation.messages} never carry a path.
1815
+ */
1816
+ path?: string;
1817
+ /**
1818
+ * The code of the error entry (or error body) being translated — also when `text` is the
1819
+ * entry's own message (an entry marked `custom`, or one translated by its text). Only set by
1820
+ * {@link translateErrorBody}.
1821
+ */
1822
+ code?: string;
1823
+ /**
1824
+ * `true` when the error entry carries the app's own message (`custom: true`, e.g. from
1825
+ * `@expect.message`): `text` is that message — keep it unless you translate it as text. Only
1826
+ * set by {@link translateErrorBody}.
1827
+ */
1828
+ custom?: boolean;
1780
1829
  }
1781
1830
  /**
1782
1831
  * The app's translator. `text` is a message id: an error/message code (`WF_EXPIRED`,
@@ -1792,6 +1841,30 @@ type TTranslate = (text: string, params: Record<string, unknown> | undefined, ct
1792
1841
  * placeholders, filled with {@link formatMessage}.
1793
1842
  */
1794
1843
  type TTranslations = Record<string, Record<string, string>>;
1844
+ /** The options of {@link setTranslator}. */
1845
+ interface TTranslatorOptions {
1846
+ /** The locales the app serves, the default first. For a map: its keys by default. */
1847
+ locales?: readonly string[];
1848
+ /**
1849
+ * The event's translation scope, for wording that differs by more than the locale (per
1850
+ * tenant, brand, user group, …). Called in the event's context each time a translation is
1851
+ * taken — keep it cheap. Its result is passed to the translator ({@link TTranslateContext.scope})
1852
+ * and partitions everything memoized per translation ({@link TTranslation.key}); a finite
1853
+ * number counts as its string, and `undefined` (or any other value) is the shared, unscoped
1854
+ * wording. The translator must return the same text for the same (id, params, locale, scope).
1855
+ *
1856
+ * Derive it from a validated value (the authenticated principal's tenant), never straight from
1857
+ * request input: only the first 32 scopes seen per generation get a {@link TTranslation.key}.
1858
+ */
1859
+ scope?: () => string | number | undefined;
1860
+ /**
1861
+ * `false`: a translated error body's top `message` that repeats its first entry is just that
1862
+ * entry's text, without the `path: ` prefix ({@link translateErrorBody}) — also when the entry
1863
+ * has no translation (its English text). Bodies of events without a locale are not touched.
1864
+ * Default `true`: `path: <translated>`, like the untranslated message.
1865
+ */
1866
+ pathPrefix?: boolean;
1867
+ }
1795
1868
  /** `t(id, params?, fallback?)` — see {@link useTranslate}. */
1796
1869
  type TTranslateFn = (id: string, params?: Record<string, unknown>, fallback?: string) => string;
1797
1870
  /** The translation of one event's locale — see {@link useTranslation}. */
@@ -1799,9 +1872,17 @@ interface TTranslation {
1799
1872
  /** The event's locale. */
1800
1873
  locale: string;
1801
1874
  /**
1802
- * A memo key for anything derived from this translation: changes with every
1803
- * {@link setTranslator} call and every {@link registerMessages} call that adds or changes a
1804
- * template. `undefined` for a locale outside the translator's `locales` — don't memoize then.
1875
+ * What to index memoized results by: the locale, plus the event's scope when it has one
1876
+ * ({@link TTranslatorOptions.scope}). Keep one entry per partition, replaced when
1877
+ * {@link TTranslation.key} changes.
1878
+ */
1879
+ partition: string;
1880
+ /**
1881
+ * A memo key for anything derived from this translation: the locale, the scope (if any) and
1882
+ * the generation — changes with every {@link setTranslator} call and every
1883
+ * {@link registerMessages} call that adds or changes a template. `undefined` for a locale
1884
+ * outside the translator's `locales`, or a scope past the cap ({@link TTranslatorOptions.scope})
1885
+ * — don't memoize then.
1805
1886
  */
1806
1887
  key: string | undefined;
1807
1888
  /** {@link useTranslate}'s `t`, bound to this translation. */
@@ -1834,9 +1915,7 @@ interface TTranslation {
1834
1915
  * )
1835
1916
  * ```
1836
1917
  */
1837
- declare function setTranslator(translate: TTranslate | TTranslations | undefined, opts?: {
1838
- locales?: readonly string[];
1839
- }): void;
1918
+ declare function setTranslator(translate: TTranslate | TTranslations | undefined, opts?: TTranslatorOptions): void;
1840
1919
  /** The registered translator (a map wrapped as a function), or `undefined`. */
1841
1920
  declare function getTranslator(): TTranslate | undefined;
1842
1921
  /** The locales of the registered translator, the default first (empty without a translator). */
@@ -1882,9 +1961,11 @@ declare function useTranslation(ctx?: EventContext): TTranslation | undefined;
1882
1961
  * event's locale — the body itself when there is no translation ({@link useTranslation}).
1883
1962
  *
1884
1963
  * - each entry of `errors` (and of its entries' `details`): by its `code` (unless `custom`),
1885
- * else by its message text, its English `message` being the fallback;
1886
- * - the top `message`: by `code` when the body has one; else, when it is the first entry's
1887
- * message (`path: message`), the first entry's translation; else by its text.
1964
+ * else by its message text, its English `message` being the fallback; the translator gets the
1965
+ * entry's `path`, `code` and `custom` ({@link TTranslateContext});
1966
+ * - the top `message`: by `code` when the body has one (the translator gets that `code`); else,
1967
+ * when it is the first entry's message (`path: message`), the first entry's translation
1968
+ * (`path: ` dropped with `setTranslator(t, { pathPrefix: false })`); else by its text.
1888
1969
  *
1889
1970
  * `MoostHttp` applies it to every error body when it creates the HTTP app.
1890
1971
  */
@@ -1900,5 +1981,5 @@ declare function createLogger(opts?: Partial<TProstoLoggerOptions>): ProstoLogge
1900
1981
  /** Default colored console transport used by Moost loggers. */
1901
1982
  declare const loggerConsoleTransport: _prostojs_logger.TProstoLoggerTransportFn<any>;
1902
1983
 
1903
- export { After, ApplyDecorators, Before, Circular, Const, ConstFactory, Controller, Description, HandlerPaths, Id, ImportController, Inherit, Inject, InjectEventLogger, InjectFromScope, InjectMoost, InjectMoostLogger, InjectScopeVars, Injectable, Intercept, Interceptor, InterceptorHandler, Label, Locale, LoggerTopic, MOOST_ADAPTER_BRAND, MOOST_MODULE_IDENTITY_KEY, Moost, MoostDispose, MoostInit, OnError, Optional, Overtake, Param, Params, Pipe, Provide, Replace, Required, Resolve, Response, TInterceptorPriority, TPipePriority, Value, createLogger, defineAfterInterceptor, defineBeforeInterceptor, defineErrorInterceptor, defineInfactScope, defineInterceptor, defineMoostEventHandler, definePipeFn, detachedKind, disposeInstances, forkEventContext, formatMessage, getHandlerPaths, getInfactScopeVars, getInfactSingletonInstances, getInstanceOwnMethods, getInstanceOwnProps, getMoostInfact, getMoostMate, getNewMoostInfact, getTranslator, getTranslatorLocales, globalKey, isThenable, loggerConsoleTransport, mergeSorted, registerEventScope, registerMessages, resolvePipe, setControllerContext, setInfactLoggingOptions, setInterceptOriginal, setInterceptResult, setOvertake, setTranslator, stampModuleIdentity, stampOnce, translateErrorBody, useControllerContext, useHandlerPaths, useInterceptOriginal, useInterceptResult, useOvertake, useScopeId, useTranslate, useTranslation, withControllerContext };
1904
- 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, TRunDetachedOptions, TTranslate, TTranslateContext, TTranslation, TTranslations, TWithControllerContextOptions };
1984
+ export { After, ApplyDecorators, Before, Circular, Const, ConstFactory, Controller, Description, HandlerPaths, Id, ImportController, Inherit, Inject, InjectEventLogger, InjectFromScope, InjectMoost, InjectMoostLogger, InjectScopeVars, Injectable, Intercept, Interceptor, InterceptorHandler, Label, Locale, LoggerTopic, MOOST_ADAPTER_BRAND, MOOST_MODULE_IDENTITY_KEY, Moost, MoostDispose, MoostInit, OnError, Optional, Overtake, Param, Params, Pipe, Provide, Replace, Required, Resolve, Response, TInterceptorPriority, TPipePriority, Value, claimWooks, createLogger, defineAfterInterceptor, defineBeforeInterceptor, defineErrorInterceptor, defineInfactScope, defineInterceptor, defineMoostEventHandler, definePipeFn, detachedKind, disposeInstances, forkEventContext, formatMessage, getHandlerPaths, getInfactScopeVars, getInfactSingletonInstances, getInstanceOwnMethods, getInstanceOwnProps, getMoostInfact, getMoostMate, getNewMoostInfact, getTranslator, getTranslatorLocales, globalKey, isThenable, loggerConsoleTransport, mergeSorted, registerEventScope, registerMessages, resolvePipe, setControllerContext, setInfactLoggingOptions, setInterceptOriginal, setInterceptResult, setOvertake, setTranslator, stampModuleIdentity, stampOnce, translateErrorBody, useControllerContext, useHandlerPaths, useInterceptOriginal, useInterceptResult, useOvertake, useScopeId, useTranslate, useTranslation, withControllerContext };
1985
+ export type { TAny, TAnyFn, TClaimWooksOptions, 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, TRunDetachedOptions, TTranslate, TTranslateContext, TTranslation, TTranslations, TTranslatorOptions, 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.7.3",
50
+ version: "0.7.5",
51
51
  path: typeof __filename === "string" ? __filename : import.meta.url
52
52
  });
53
53
  }
@@ -3181,11 +3181,44 @@ function _define_property(obj, key, value) {
3181
3181
  return getHandlerPaths(moost, getController(), method ?? getMethod() ?? "", opts);
3182
3182
  }
3183
3183
 
3184
+ //#endregion
3185
+ //#region packages/moost/src/wooks-owner.ts
3186
+ /** Per Wooks instance: the first claim of each adapter (by `name`) bound into it. */ const owners = /* @__PURE__ */ new WeakMap();
3187
+ /** Wooks instances already warned about: the warning is logged once per instance. */ const warned = /* @__PURE__ */ new WeakSet();
3188
+ /**
3189
+ * Records that `app` binds the handlers of its `adapter` (the adapter's `name`) into `wooks`
3190
+ * (the Wooks instance its routes go to: `wooksApp.getWooks()`). Adapters call it from `onInit`.
3191
+ *
3192
+ * Adapters created without a Wooks app of their own share the process-wide global Wooks, so two
3193
+ * apps in one process (typically in tests) route through one router. When another app already
3194
+ * bound handlers of the same adapter into `wooks`, and either adapter created its Wooks app
3195
+ * (`opts.created`), it warns — once per Wooks instance, with the app's logger. Apps that share
3196
+ * a Wooks app they pass in on purpose stay silent; re-initialising the same app does not warn.
3197
+ * A disposed app still counts: `dispose()` does not remove its routes.
3198
+ */ function claimWooks(app, wooks, adapter, opts) {
3199
+ const created = opts?.created === true;
3200
+ let byAdapter = owners.get(wooks);
3201
+ if (!byAdapter) {
3202
+ byAdapter = /* @__PURE__ */ new Map();
3203
+ owners.set(wooks, byAdapter);
3204
+ }
3205
+ const owner = byAdapter.get(adapter);
3206
+ if (owner === void 0) byAdapter.set(adapter, {
3207
+ app,
3208
+ created
3209
+ });
3210
+ else if (owner.app !== app && (created || owner.created) && !warned.has(wooks)) {
3211
+ warned.add(wooks);
3212
+ app.getLogger().warn(`[moost] A second Moost app binds "${adapter}" handlers into the Wooks instance another app already uses (adapters created without their own Wooks app share the process-wide global one). Both apps' routes now live in one router: an event on a route both register runs the first app's handler chain, and when that throws, its error is swallowed and the next app's chain runs instead. Call clearGlobalWooks() before creating each app (e.g. in a test's beforeEach): the next adapter then gets a fresh Wooks. (Logged once per Wooks instance.)`);
3213
+ }
3214
+ }
3215
+
3184
3216
  //#endregion
3185
3217
  //#region packages/moost/src/i18n.ts
3186
3218
  var _globalThis, _STATE, _state, _state1, _state2, _state3, _state4;
3187
3219
  const STATE = Symbol.for("moost.i18n");
3188
3220
  const NO_LOCALES = Object.freeze([]);
3221
+ /** Distinct scopes given memo keys per generation: keeps every memo keyed by `key` bounded. */ const MAX_SCOPES = 32;
3189
3222
  const state = (_globalThis = globalThis)[_STATE = STATE] ?? (_globalThis[_STATE] = {});
3190
3223
  (_state = state).locales ?? (_state.locales = NO_LOCALES);
3191
3224
  (_state1 = state).generation ?? (_state1.generation = 0);
@@ -3215,9 +3248,15 @@ const state = (_globalThis = globalThis)[_STATE = STATE] ?? (_globalThis[_STATE]
3215
3248
  */ function setTranslator(translate, opts) {
3216
3249
  const isMap = typeof translate === "object";
3217
3250
  state.translate = isMap ? mapTranslator(translate) : translate;
3251
+ state.scoping = opts?.scope && {
3252
+ translate: state.translate,
3253
+ get: opts.scope
3254
+ };
3255
+ state.noPathPrefix = opts?.pathPrefix === false ? state.translate : void 0;
3218
3256
  const locales = opts?.locales ?? (isMap ? Object.keys(translate) : void 0);
3219
3257
  state.locales = translate && locales?.length ? Object.freeze([...locales]) : NO_LOCALES;
3220
3258
  state.failureLogged = false;
3259
+ state.scopeFailureLogged = false;
3221
3260
  newGeneration();
3222
3261
  }
3223
3262
  /** The registered translator (a map wrapped as a function), or `undefined`. */ function getTranslator() {
@@ -3243,7 +3282,7 @@ const state = (_globalThis = globalThis)[_STATE = STATE] ?? (_globalThis[_STATE]
3243
3282
  }
3244
3283
  if (changed) newGeneration();
3245
3284
  }
3246
- /** The only invalidation: drops everything derived from the translator and the templates. */ function newGeneration() {
3285
+ /** Drops everything derived from the translator and the templates. */ function newGeneration() {
3247
3286
  state.generation++;
3248
3287
  state.memo.clear();
3249
3288
  }
@@ -3278,10 +3317,7 @@ const PLACEHOLDER = /\{(\w+)\}/g;
3278
3317
  let translateCtx;
3279
3318
  return (id, params, fallback) => {
3280
3319
  const translate = state.translate;
3281
- return (translate ? callTranslator(translate, id, params, translateCtx ?? (translateCtx = {
3282
- locale: useLocale$1(event).get(),
3283
- event
3284
- })) : void 0) ?? untranslated(id, params, fallback);
3320
+ return (translate ? callTranslator(translate, id, params, translateCtx ?? (translateCtx = translateContext(event, useLocale$1(event).get(), readScope(event)))) : void 0) ?? untranslated(id, params, fallback);
3285
3321
  };
3286
3322
  }
3287
3323
  /** `t`'s text without a translation: `fallback` as is, else the filled template (or id). */ function untranslated(id, params, fallback) {
@@ -3297,12 +3333,11 @@ const PLACEHOLDER = /\{(\w+)\}/g;
3297
3333
  const event = ctx ?? current$1();
3298
3334
  const locale = useLocale$1(event).get();
3299
3335
  if (locale === void 0) return;
3300
- const translateCtx = {
3301
- locale,
3302
- event
3303
- };
3336
+ const scope = readScope(event);
3337
+ const translateCtx = translateContext(event, locale, scope);
3304
3338
  const generation = state.generation;
3305
- const key = state.locales.includes(locale) ? `${generation}:${locale}` : void 0;
3339
+ const partition = typeof scope === "string" ? `${locale}:${scope}` : locale;
3340
+ const key = state.locales.includes(locale) && (scope === void 0 || scope !== null && admitted(scope)) ? `${generation}:${partition}` : void 0;
3306
3341
  const t = (id, params, fallback) => callTranslator(translate, id, params, translateCtx) ?? untranslated(id, params, fallback);
3307
3342
  const messages = () => {
3308
3343
  if (key === void 0 || state.generation !== generation) return translateTemplates(translate, translateCtx);
@@ -3315,31 +3350,82 @@ const PLACEHOLDER = /\{(\w+)\}/g;
3315
3350
  };
3316
3351
  return {
3317
3352
  locale,
3353
+ partition,
3318
3354
  key,
3319
3355
  t,
3320
3356
  messages
3321
3357
  };
3322
3358
  }
3359
+ function translateContext(event, locale, scope) {
3360
+ return typeof scope === "string" ? {
3361
+ locale,
3362
+ scope,
3363
+ event
3364
+ } : {
3365
+ locale,
3366
+ event
3367
+ };
3368
+ }
3369
+ /**
3370
+ * The event's scope (`opts.scope`): `undefined` without one, `null` when the scope function
3371
+ * threw (logged once) — translated unscoped then, and not memoized.
3372
+ */ function readScope(event) {
3373
+ const scoping = state.scoping;
3374
+ if (!scoping || scoping.translate !== state.translate) return;
3375
+ try {
3376
+ const { get } = scoping;
3377
+ const scope = tryGetCurrent() === event ? get() : run$1(event, get);
3378
+ return typeof scope === "string" ? scope : typeof scope === "number" && Number.isFinite(scope) ? String(scope) : void 0;
3379
+ } catch (error) {
3380
+ logOnce("scopeFailureLogged", event, "The translation scope threw — translating unscoped, without memoizing (logged once)", error);
3381
+ return null;
3382
+ }
3383
+ }
3384
+ /** Whether `scope` gets memo keys: one of the first {@link MAX_SCOPES} of the generation. */ function admitted(scope) {
3385
+ if (state.admittedScopes?.generation !== state.generation) state.admittedScopes = {
3386
+ generation: state.generation,
3387
+ names: /* @__PURE__ */ new Set()
3388
+ };
3389
+ const { names } = state.admittedScopes;
3390
+ if (names.size < MAX_SCOPES) names.add(scope);
3391
+ return names.has(scope);
3392
+ }
3393
+ /** Logs a failure of the translator or the scope function once per `setTranslator`. */ function logOnce(flag, event, message, error) {
3394
+ if (!state[flag]) {
3395
+ state[flag] = true;
3396
+ useLogger$1("i18n", event).error(message, error);
3397
+ }
3398
+ }
3323
3399
  /**
3324
3400
  * Returns a translated copy of an error body (`{ message, code?, params?, errors? }`) in the
3325
3401
  * event's locale — the body itself when there is no translation ({@link useTranslation}).
3326
3402
  *
3327
3403
  * - each entry of `errors` (and of its entries' `details`): by its `code` (unless `custom`),
3328
- * else by its message text, its English `message` being the fallback;
3329
- * - the top `message`: by `code` when the body has one; else, when it is the first entry's
3330
- * message (`path: message`), the first entry's translation; else by its text.
3404
+ * else by its message text, its English `message` being the fallback; the translator gets the
3405
+ * entry's `path`, `code` and `custom` ({@link TTranslateContext});
3406
+ * - the top `message`: by `code` when the body has one (the translator gets that `code`); else,
3407
+ * when it is the first entry's message (`path: message`), the first entry's translation
3408
+ * (`path: ` dropped with `setTranslator(t, { pathPrefix: false })`); else by its text.
3331
3409
  *
3332
3410
  * `MoostHttp` applies it to every error body when it creates the HTTP app.
3333
3411
  */ function translateErrorBody(body, ctx) {
3334
- const translation = useTranslation(ctx);
3335
- if (!translation) return body;
3336
- const { t } = translation;
3412
+ const event = ctx ?? current$1();
3413
+ const translate = state.translate;
3414
+ const locale = useLocale$1(event).get();
3415
+ if (!translate || locale === void 0) return body;
3416
+ const base = translateContext(event, locale, readScope(event));
3417
+ const t = (id, params, fallback, entry) => callTranslator(translate, id, params, entry ? entryContext(base, entry) : base) ?? untranslated(id, params, fallback);
3337
3418
  const source = Array.isArray(body.errors) ? body.errors : void 0;
3338
3419
  const errors = source && translateEntries(source, t);
3339
3420
  let message;
3340
- if (typeof body.code === "string" && body.code) message = t(body.code, body.params, body.message);
3341
- else if (source && errors && isEntry(source[0]) && body.message === topMessage(source[0])) message = topMessage(errors[0]);
3342
- else message = t(body.message, body.params, body.message);
3421
+ if (typeof body.code === "string" && body.code) message = callTranslator(translate, body.code, body.params, {
3422
+ ...base,
3423
+ code: body.code
3424
+ }) ?? untranslated(body.code, body.params, body.message);
3425
+ else if (source && errors && isEntry(source[0]) && body.message === topMessage(source[0])) {
3426
+ const first = errors[0];
3427
+ message = state.noPathPrefix === translate ? first.message : topMessage(first);
3428
+ } else message = t(body.message, body.params, body.message);
3343
3429
  return errors ? {
3344
3430
  ...body,
3345
3431
  message,
@@ -3360,10 +3446,7 @@ function mapTranslator(map) {
3360
3446
  const out = translate(id, params, ctx);
3361
3447
  return typeof out === "string" ? out : void 0;
3362
3448
  } catch (error) {
3363
- if (!state.failureLogged) {
3364
- state.failureLogged = true;
3365
- useLogger$1("i18n", ctx.event).error(`The translator threw for "${id}" — using the untranslated text (logged once)`, error);
3366
- }
3449
+ logOnce("failureLogged", ctx.event, `The translator threw for "${id}" — using the untranslated text (logged once)`, error);
3367
3450
  return;
3368
3451
  }
3369
3452
  }
@@ -3380,13 +3463,22 @@ function mapTranslator(map) {
3380
3463
  function isEntry(value) {
3381
3464
  return typeof value === "object" && value !== null && typeof value.message === "string";
3382
3465
  }
3466
+ /** `base` with the entry's `path` and `code` (each only when a string) and `custom: true`. */ function entryContext(base, { path, code, custom }) {
3467
+ const hasCode = typeof code === "string" && code !== "";
3468
+ if (typeof path !== "string" && !hasCode && custom !== true) return base;
3469
+ const out = { ...base };
3470
+ if (typeof path === "string") out.path = path;
3471
+ if (hasCode) out.code = code;
3472
+ if (custom === true) out.custom = true;
3473
+ return out;
3474
+ }
3383
3475
  function translateEntries(entries, t) {
3384
3476
  return entries.map((entry) => {
3385
3477
  if (!isEntry(entry)) return entry;
3386
3478
  const id = typeof entry.code === "string" && entry.code && entry.custom !== true ? entry.code : entry.message;
3387
3479
  const out = {
3388
3480
  ...entry,
3389
- message: t(id, entry.params, entry.message)
3481
+ message: t(id, entry.params, entry.message, entry)
3390
3482
  };
3391
3483
  if (Array.isArray(entry.details)) out.details = translateEntries(entry.details, t);
3392
3484
  return out;
@@ -3397,4 +3489,4 @@ function topMessage(entry) {
3397
3489
  }
3398
3490
 
3399
3491
  //#endregion
3400
- export { After, ApplyDecorators, Before, Circular, Const, ConstFactory, ContextInjector, Controller, Description, HandlerPaths, Id, ImportController, Inherit, Inject, InjectEventLogger, InjectFromScope, InjectMoost, InjectMoostLogger, InjectScopeVars, Injectable, Intercept, Interceptor, InterceptorHandler, Label, Locale, LoggerTopic, MOOST_ADAPTER_BRAND, MOOST_MODULE_IDENTITY_KEY, Moost, MoostDispose, MoostInit, OnError, Optional, Overtake, Param, Params, Pipe, ProstoLogger, Provide, Replace, Required, Resolve, Response, TInterceptorPriority, TPipePriority, Value, cached, clearGlobalWooks, createEventContext, createLogger, createProvideRegistry, createReplaceRegistry, current, defineAfterInterceptor, defineBeforeInterceptor, defineErrorInterceptor, defineInfactScope, defineInterceptor, defineMoostEventHandler, definePipeFn, detachedKind, disposeInstances, eventTypeKey, forkEventContext, formatMessage, getClassKey, getConstructor, getContextInjector, getGlobalWooks, getHandlerPaths, getInfactScopeVars, getInfactSingletonInstances, getInstanceOwnMethods, getInstanceOwnProps, getMoostInfact, getMoostMate, getNewMoostInfact, getTranslator, getTranslatorLocales, globalKey, isConstructor, isThenable, key, loggerConsoleTransport, mergeSorted, registerEventScope, registerMessages, replaceContextInjector, resetContextInjector, resolvePipe, run, setControllerContext, setInfactLoggingOptions, setInterceptOriginal, setInterceptResult, setOvertake, setTranslator, stampModuleIdentity, stampOnce, translateErrorBody, useControllerContext, useHandlerPaths, useInterceptOriginal, useInterceptResult, useLocale, useLogger, useOvertake, useScopeId, useTranslate, useTranslation, withControllerContext };
3492
+ export { After, ApplyDecorators, Before, Circular, Const, ConstFactory, ContextInjector, Controller, Description, HandlerPaths, Id, ImportController, Inherit, Inject, InjectEventLogger, InjectFromScope, InjectMoost, InjectMoostLogger, InjectScopeVars, Injectable, Intercept, Interceptor, InterceptorHandler, Label, Locale, LoggerTopic, MOOST_ADAPTER_BRAND, MOOST_MODULE_IDENTITY_KEY, Moost, MoostDispose, MoostInit, OnError, Optional, Overtake, Param, Params, Pipe, ProstoLogger, Provide, Replace, Required, Resolve, Response, TInterceptorPriority, TPipePriority, Value, cached, claimWooks, clearGlobalWooks, createEventContext, createLogger, createProvideRegistry, createReplaceRegistry, current, defineAfterInterceptor, defineBeforeInterceptor, defineErrorInterceptor, defineInfactScope, defineInterceptor, defineMoostEventHandler, definePipeFn, detachedKind, disposeInstances, eventTypeKey, forkEventContext, formatMessage, getClassKey, getConstructor, getContextInjector, getGlobalWooks, getHandlerPaths, getInfactScopeVars, getInfactSingletonInstances, getInstanceOwnMethods, getInstanceOwnProps, getMoostInfact, getMoostMate, getNewMoostInfact, getTranslator, getTranslatorLocales, globalKey, isConstructor, isThenable, key, loggerConsoleTransport, mergeSorted, registerEventScope, registerMessages, replaceContextInjector, resetContextInjector, resolvePipe, run, setControllerContext, setInfactLoggingOptions, setInterceptOriginal, setInterceptResult, setOvertake, setTranslator, stampModuleIdentity, stampOnce, translateErrorBody, useControllerContext, useHandlerPaths, useInterceptOriginal, useInterceptResult, useLocale, useLogger, useOvertake, useScopeId, useTranslate, useTranslation, withControllerContext };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moost",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "moost",
5
5
  "keywords": [
6
6
  "composables",
@@ -41,15 +41,15 @@
41
41
  "@prostojs/infact": "^0.6.1",
42
42
  "@prostojs/logger": "^0.4.3",
43
43
  "@prostojs/mate": "^0.5.1",
44
- "@wooksjs/event-core": "^0.8.1",
44
+ "@wooksjs/event-core": "^0.8.2",
45
45
  "hookable": "^5.5.3",
46
- "wooks": "^0.8.1"
46
+ "wooks": "^0.8.2"
47
47
  },
48
48
  "devDependencies": {
49
- "@wooksjs/event-http": "^0.8.1",
50
- "@wooksjs/http-body": "^0.8.1",
49
+ "@wooksjs/event-http": "^0.8.2",
50
+ "@wooksjs/http-body": "^0.8.2",
51
51
  "vitest": "4.1.12",
52
- "@moostjs/event-http": "^0.7.3"
52
+ "@moostjs/event-http": "^0.7.5"
53
53
  },
54
54
  "scripts": {
55
55
  "pub": "pnpm publish --access public",