@gramio/composer 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -19,7 +19,88 @@ type MaybeArray<T> = T | readonly T[];
19
19
  /** Scope level for middleware propagation */
20
20
  type Scope = "local" | "scoped" | "global";
21
21
  /** Which method created a middleware entry */
22
- type MiddlewareType = "use" | "derive" | "decorate" | "guard" | "branch" | "route" | "fork" | "tap" | "lazy" | "group" | "extend" | "on";
22
+ type MiddlewareType = "use" | "derive" | "decorate" | "guard" | "branch" | "route" | "fork" | "tap" | "lazy" | "group" | "extend" | "on" | "macro";
23
+ /** Brand symbol for ContextCallback marker type */
24
+ declare const ContextCallbackBrand: unique symbol;
25
+ /**
26
+ * Marker type for context-aware callbacks in macro options.
27
+ * The framework replaces this with the actual handler context type at the call site.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * interface ThrottleOptions {
32
+ * limit: number;
33
+ * onLimit?: ContextCallback; // ← framework substitutes the real ctx type
34
+ * }
35
+ * ```
36
+ */
37
+ interface ContextCallback {
38
+ readonly [ContextCallbackBrand]: true;
39
+ (ctx: never): unknown;
40
+ }
41
+ /**
42
+ * Recursively replaces all `ContextCallback` occurrences in `T`
43
+ * with `(ctx: TCtx) => unknown`.
44
+ */
45
+ type WithCtx<T, TCtx> = T extends ContextCallback ? (ctx: TCtx) => unknown : T extends (...args: any[]) => any ? T : T extends object ? {
46
+ [K in keyof T]: WithCtx<T[K], TCtx>;
47
+ } : T;
48
+ /** What a macro can return when activated */
49
+ interface MacroHooks<TDerive extends object = {}> {
50
+ /** Middleware to run before the main handler */
51
+ preHandler?: Middleware<any> | Middleware<any>[];
52
+ /**
53
+ * Context enrichment — return type gets merged into the handler's context.
54
+ * Return `void` / `undefined` to stop the middleware chain (acts as a guard).
55
+ */
56
+ derive?: (ctx: any) => TDerive | void | Promise<TDerive | void>;
57
+ }
58
+ /**
59
+ * A macro definition: either a function accepting options, or a plain hooks object (boolean shorthand).
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * // Parameterized macro
64
+ * const throttle: MacroDef<{ limit: number }, {}> = (opts) => ({
65
+ * preHandler: createThrottleMiddleware(opts),
66
+ * });
67
+ *
68
+ * // Boolean shorthand
69
+ * const onlyAdmin: MacroDef<void, {}> = {
70
+ * preHandler: checkAdminMiddleware,
71
+ * };
72
+ * ```
73
+ */
74
+ type MacroDef<TOptions = void, TDerive extends object = {}> = ((opts: TOptions) => MacroHooks<TDerive>) | MacroHooks<TDerive>;
75
+ /** Registry of named macro definitions */
76
+ type MacroDefinitions = Record<string, MacroDef<any, any>>;
77
+ /** Extract the options type a macro accepts (boolean for shorthand macros) */
78
+ type MacroOptionType<M> = M extends (opts: infer O) => any ? O : boolean;
79
+ /** Extract the derive (context enrichment) type from a macro */
80
+ type MacroDeriveType<M> = M extends (opts: any) => {
81
+ derive: (...a: any) => infer R;
82
+ } ? Exclude<Awaited<R>, void> : M extends {
83
+ derive: (...a: any) => infer R;
84
+ } ? Exclude<Awaited<R>, void> : {};
85
+ /**
86
+ * Builds the `options` parameter type for handler methods.
87
+ * Includes `preHandler` plus all registered macro option types
88
+ * with ContextCallback markers replaced by `TBaseCtx`.
89
+ */
90
+ type HandlerOptions<TBaseCtx, Macros extends MacroDefinitions> = {
91
+ preHandler?: Middleware<TBaseCtx> | Middleware<TBaseCtx>[];
92
+ } & {
93
+ [K in keyof Macros]?: WithCtx<MacroOptionType<Macros[K]>, TBaseCtx>;
94
+ };
95
+ /** Helper: converts a union to an intersection */
96
+ type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
97
+ /**
98
+ * Collects all derive types from macros that are activated in `TOptions`.
99
+ * The result is intersected into the handler's context type.
100
+ */
101
+ type DeriveFromOptions<Macros extends MacroDefinitions, TOptions> = UnionToIntersection<{
102
+ [K in keyof TOptions & keyof Macros]: MacroDeriveType<Macros[K]>;
103
+ }[keyof TOptions & keyof Macros]>;
23
104
  /** Read-only projection of a middleware entry for inspect()/trace() */
24
105
  interface MiddlewareInfo {
25
106
  index: number;
@@ -51,7 +132,7 @@ interface ComposerOptions {
51
132
  declare function compose<T>(middlewares: Middleware<T>[]): ComposedMiddleware<T>;
52
133
 
53
134
  /** Route handler: single middleware, array, or Composer instance */
54
- type RouteHandler<T extends object> = Middleware<T> | Middleware<T>[] | Composer<any, any, any>;
135
+ type RouteHandler<T extends object> = Middleware<T> | Middleware<T>[] | Composer<any, any, any, any>;
55
136
  /** Route builder passed to the builder-callback overload of route() */
56
137
  interface RouteBuilder<T extends object, K extends string> {
57
138
  /** Register a route. Returns a pre-typed Composer for chaining derive/use/guard etc. */
@@ -59,7 +140,7 @@ interface RouteBuilder<T extends object, K extends string> {
59
140
  /** Fallback when router returns undefined or key has no handler */
60
141
  otherwise(...middleware: Middleware<T>[]): void;
61
142
  }
62
- declare class Composer<TIn extends object = {}, TOut extends TIn = TIn, TExposed extends object = {}> {
143
+ declare class Composer<TIn extends object = {}, TOut extends TIn = TIn, TExposed extends object = {}, TMacros extends MacroDefinitions = {}> {
63
144
  "~": {
64
145
  middlewares: ScopedMiddleware<any>[];
65
146
  onErrors: ErrorHandler<any>[];
@@ -72,13 +153,22 @@ declare class Composer<TIn extends object = {}, TOut extends TIn = TIn, TExposed
72
153
  prototype: Error;
73
154
  }>;
74
155
  tracer: TraceHandler | undefined;
156
+ macros: Record<string, MacroDef<any, any>>;
157
+ /** Phantom type accessor — never set at runtime, used by `ContextOf<T>` */
158
+ Out: TOut;
75
159
  };
76
160
  constructor(options?: ComposerOptions);
77
161
  invalidate(): void;
162
+ /** Register a single named macro */
163
+ macro<const Name extends string, TDef extends MacroDef<any, any>>(name: Name, definition: TDef): Composer<TIn, TOut, TExposed, TMacros & Record<Name, TDef>>;
164
+ /** Register multiple macros at once */
165
+ macro<const TDefs extends Record<string, MacroDef<any, any>>>(definitions: TDefs): Composer<TIn, TOut, TExposed, TMacros & TDefs>;
78
166
  decorate<D extends object>(values: D): Composer<TIn, TOut & D, TExposed>;
79
167
  decorate<D extends object>(values: D, options: {
80
168
  as: "scoped" | "global";
81
169
  }): Composer<TIn, TOut & D, TExposed & D>;
170
+ use(handler: Middleware<TOut>): this;
171
+ use<Patch extends object>(handler: Middleware<TOut & Patch>): this;
82
172
  use(...middleware: Middleware<TOut>[]): Composer<TIn, TOut, TExposed>;
83
173
  derive<D extends object>(handler: DeriveHandler<TOut, D>): Composer<TIn, TOut & D, TExposed>;
84
174
  derive<D extends object>(handler: DeriveHandler<TOut, D>, options: {
@@ -99,7 +189,7 @@ declare class Composer<TIn extends object = {}, TOut extends TIn = TIn, TExposed
99
189
  }): this;
100
190
  as(scope: "scoped" | "global"): Composer<TIn, TOut, TOut>;
101
191
  group(fn: (composer: Composer<TOut, TOut, {}>) => void): Composer<TIn, TOut, TExposed>;
102
- extend<UIn extends object, UOut extends UIn, UExposed extends object>(other: Composer<UIn, UOut, UExposed>): Composer<TIn, TOut & UExposed, TExposed>;
192
+ extend<UIn extends object, UOut extends UIn, UExposed extends object, UMacros extends MacroDefinitions = {}>(other: Composer<UIn, UOut, UExposed, UMacros>): Composer<TIn, TOut & UExposed, TExposed, TMacros & UMacros>;
103
193
  inspect(): MiddlewareInfo[];
104
194
  trace(handler: TraceHandler): this;
105
195
  compose(): ComposedMiddleware<TIn>;
@@ -136,39 +226,80 @@ declare class EventQueue<T> {
136
226
  * Array of events → discriminated union where each branch has correct derives.
137
227
  */
138
228
  type ResolveEventCtx<TOut extends object, TEventMap extends Record<string, any>, TDerives extends Record<string, object>, E extends string> = E extends any ? TOut & (E extends keyof TEventMap ? TEventMap[E] : {}) & (E extends keyof TDerives ? TDerives[E] : {}) : never;
139
- /** EventComposer interface — Composer + .on() + per-event derive tracking */
140
- interface EventComposer<TBase extends object, TEventMap extends Record<string, TBase>, TIn extends TBase = TBase, TOut extends TIn = TIn, TExposed extends object = {}, TDerives extends Record<string, object> = {}> {
141
- on<E extends keyof TEventMap & string>(event: MaybeArray<E>, handler: Middleware<ResolveEventCtx<TOut, TEventMap, TDerives, E>>): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives>;
142
- decorate<D extends object>(values: D): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed, TDerives>;
229
+ /**
230
+ * Given an event map and a Narrowing type, yields the union of event names
231
+ * whose context type contains all keys from Narrowing.
232
+ */
233
+ type CompatibleEvents<TEventMap extends Record<string, any>, Narrowing> = {
234
+ [E in keyof TEventMap & string]: keyof Narrowing & string extends keyof TEventMap[E] ? E : never;
235
+ }[keyof TEventMap & string];
236
+ /**
237
+ * Minimal structural type for constraining `this` in custom composer methods.
238
+ *
239
+ * Use as an F-bounded constraint on `TThis` so that `this.on(...)` is typed
240
+ * and returns `TThis` without requiring `as any`:
241
+ *
242
+ * @example
243
+ * ```ts
244
+ * command<TThis extends ComposerLike<TThis>>(
245
+ * this: TThis,
246
+ * name: string,
247
+ * handler: Middleware<MsgCtx & ContextOf<TThis>>,
248
+ * ): TThis {
249
+ * const inner: Middleware<MsgCtx & ContextOf<TThis>> = (ctx, next) => {
250
+ * if (ctx.text === `/${name}`) return handler(ctx, next);
251
+ * return next();
252
+ * };
253
+ * return this.on("message", inner); // typed — no cast needed
254
+ * }
255
+ * ```
256
+ */
257
+ type ComposerLike<T = unknown> = {
258
+ on(event: any, handler: any): T;
259
+ };
260
+ /** EventComposer interface — Composer + .on() + per-event derive tracking + custom methods */
261
+ interface EventComposer<TBase extends object, TEventMap extends Record<string, TBase>, TIn extends TBase = TBase, TOut extends TIn = TIn, TExposed extends object = {}, TDerives extends Record<string, object> = {}, TMethods extends Record<string, (...args: any[]) => any> = {}, TMacros extends MacroDefinitions = {}> {
262
+ on<Narrowing>(filter: (ctx: any) => ctx is Narrowing, handler: Middleware<ResolveEventCtx<TOut, TEventMap, TDerives, CompatibleEvents<TEventMap, Narrowing>> & Narrowing>): this;
263
+ on(filter: (ctx: TOut) => boolean, handler: Middleware<TOut>): this;
264
+ on<E extends keyof TEventMap & string, Narrowing>(event: MaybeArray<E>, filter: (ctx: any) => ctx is Narrowing, handler: Middleware<ResolveEventCtx<TOut, TEventMap, TDerives, E> & Narrowing>): this;
265
+ on<E extends keyof TEventMap & string>(event: MaybeArray<E>, filter: (ctx: ResolveEventCtx<TOut, TEventMap, TDerives, E>) => boolean, handler: Middleware<ResolveEventCtx<TOut, TEventMap, TDerives, E>>): this;
266
+ on<E extends keyof TEventMap & string, Patch extends object = {}>(event: MaybeArray<E>, handler: Middleware<ResolveEventCtx<TOut, TEventMap, TDerives, E> & Patch>): this;
267
+ use<Patch extends object>(handler: Middleware<TOut & Patch>): this;
268
+ use(...middleware: Middleware<TOut>[]): this;
269
+ guard<Narrowing>(predicate: (context: any) => context is Narrowing): EventComposer<TBase, TEventMap, TIn, TOut & Narrowing, TExposed, TDerives, TMethods, TMacros> & TMethods;
270
+ guard<S extends TOut>(predicate: ((context: TOut) => context is S) | ((context: TOut) => boolean | Promise<boolean>), ...middleware: Middleware<any>[]): this;
271
+ branch(predicate: ((context: TOut) => boolean | Promise<boolean>) | boolean, onTrue: Middleware<TOut>, onFalse?: Middleware<TOut>): this;
272
+ route<K extends string>(router: (context: TOut) => K | undefined | Promise<K | undefined>, cases: Partial<Record<K, (composer: Composer<TOut, TOut, {}>) => Composer<any, any, any>>>, fallback?: (composer: Composer<TOut, TOut, {}>) => Composer<any, any, any>): this;
273
+ route<K extends string>(router: (context: TOut) => K | undefined | Promise<K | undefined>, builder: (route: RouteBuilder<TOut, K>) => void): this;
274
+ route<K extends string>(router: (context: TOut) => K | undefined | Promise<K | undefined>, cases: Partial<Record<K, Middleware<TOut> | Middleware<TOut>[] | Composer<any, any, any>>>, fallback?: Middleware<TOut> | Middleware<TOut>[] | Composer<any, any, any>): this;
275
+ fork(...middleware: Middleware<TOut>[]): this;
276
+ tap(...middleware: Middleware<TOut>[]): this;
277
+ lazy(factory: LazyFactory<TOut>): this;
278
+ onError(handler: ErrorHandler<TOut>): this;
279
+ error(kind: string, errorClass: {
280
+ new (...args: any): any;
281
+ prototype: Error;
282
+ }): this;
283
+ group(fn: (composer: Composer<TOut, TOut, {}>) => void): this;
284
+ decorate<D extends object>(values: D): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed, TDerives, TMethods, TMacros> & TMethods;
143
285
  decorate<D extends object>(values: D, options: {
144
286
  as: "scoped" | "global";
145
- }): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed & D, TDerives>;
146
- use(...middleware: Middleware<TOut>[]): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives>;
147
- derive<D extends object>(handler: DeriveHandler<TOut, D>): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed, TDerives>;
287
+ }): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed & D, TDerives, TMethods, TMacros> & TMethods;
288
+ derive<D extends object>(handler: DeriveHandler<TOut, D>): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed, TDerives, TMethods, TMacros> & TMethods;
148
289
  derive<D extends object>(handler: DeriveHandler<TOut, D>, options: {
149
290
  as: "scoped" | "global";
150
- }): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed & D, TDerives>;
291
+ }): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed & D, TDerives, TMethods, TMacros> & TMethods;
151
292
  derive<E extends keyof TEventMap & string, D extends object>(event: MaybeArray<E>, handler: DeriveHandler<ResolveEventCtx<TOut, TEventMap, TDerives, E>, D>): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives & {
152
293
  [K in E]: D;
153
- }>;
154
- guard<S extends TOut>(predicate: ((context: TOut) => context is S) | ((context: TOut) => boolean | Promise<boolean>), ...middleware: Middleware<any>[]): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives>;
155
- branch(predicate: ((context: TOut) => boolean | Promise<boolean>) | boolean, onTrue: Middleware<TOut>, onFalse?: Middleware<TOut>): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives>;
156
- route<K extends string>(router: (context: TOut) => K | undefined | Promise<K | undefined>, cases: Partial<Record<K, (composer: Composer<TOut, TOut, {}>) => Composer<any, any, any>>>, fallback?: (composer: Composer<TOut, TOut, {}>) => Composer<any, any, any>): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives>;
157
- route<K extends string>(router: (context: TOut) => K | undefined | Promise<K | undefined>, builder: (route: RouteBuilder<TOut, K>) => void): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives>;
158
- route<K extends string>(router: (context: TOut) => K | undefined | Promise<K | undefined>, cases: Partial<Record<K, Middleware<TOut> | Middleware<TOut>[] | Composer<any, any, any>>>, fallback?: Middleware<TOut> | Middleware<TOut>[] | Composer<any, any, any>): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives>;
159
- fork(...middleware: Middleware<TOut>[]): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives>;
160
- tap(...middleware: Middleware<TOut>[]): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives>;
161
- lazy(factory: LazyFactory<TOut>): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives>;
162
- onError(handler: ErrorHandler<TOut>): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives>;
163
- when<UOut extends TOut>(condition: boolean, fn: (composer: Composer<TOut, TOut, {}>) => Composer<TOut, UOut, any>): EventComposer<TBase, TEventMap, TIn, TOut & Partial<Omit<UOut, keyof TOut>>, TExposed, TDerives>;
164
- error(kind: string, errorClass: {
165
- new (...args: any): any;
166
- prototype: Error;
167
- }): this;
168
- as(scope: "scoped" | "global"): EventComposer<TBase, TEventMap, TIn, TOut, TOut, TDerives>;
169
- group(fn: (composer: Composer<TOut, TOut, {}>) => void): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives>;
170
- extend<UIn extends TBase, UOut extends UIn, UExposed extends object, UDerives extends Record<string, object>>(other: EventComposer<TBase, TEventMap, UIn, UOut, UExposed, UDerives>): EventComposer<TBase, TEventMap, TIn, TOut & UExposed, TExposed, TDerives & UDerives>;
171
- extend<UIn extends object, UOut extends UIn, UExposed extends object>(other: Composer<UIn, UOut, UExposed>): EventComposer<TBase, TEventMap, TIn, TOut & UExposed, TExposed, TDerives>;
294
+ }, TMethods, TMacros> & TMethods;
295
+ when<UOut extends TOut>(condition: boolean, fn: (composer: Composer<TOut, TOut, {}>) => Composer<TOut, UOut, any>): EventComposer<TBase, TEventMap, TIn, TOut & Partial<Omit<UOut, keyof TOut>>, TExposed, TDerives, TMethods, TMacros> & TMethods;
296
+ as(scope: "scoped" | "global"): EventComposer<TBase, TEventMap, TIn, TOut, TOut, TDerives, TMethods, TMacros> & TMethods;
297
+ extend<UIn extends TBase, UOut extends UIn, UExposed extends object, UDerives extends Record<string, object>, UMacros extends MacroDefinitions = {}>(other: EventComposer<TBase, TEventMap, UIn, UOut, UExposed, UDerives, any, UMacros>): EventComposer<TBase, TEventMap, TIn, TOut & UExposed, TExposed, TDerives & UDerives, TMethods, TMacros & UMacros> & TMethods;
298
+ extend<UIn extends object, UOut extends UIn, UExposed extends object, UMacros extends MacroDefinitions = {}>(other: Composer<UIn, UOut, UExposed, UMacros>): EventComposer<TBase, TEventMap, TIn, TOut & UExposed, TExposed, TDerives, TMethods, TMacros & UMacros> & TMethods;
299
+ /** Register a single named macro */
300
+ macro<const Name extends string, TDef extends MacroDef<any, any>>(name: Name, definition: TDef): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives, TMethods, TMacros & Record<Name, TDef>> & TMethods;
301
+ /** Register multiple macros at once */
302
+ macro<const TDefs extends Record<string, MacroDef<any, any>>>(definitions: TDefs): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives, TMethods, TMacros & TDefs> & TMethods;
172
303
  inspect(): MiddlewareInfo[];
173
304
  trace(handler: TraceHandler): this;
174
305
  compose(): ComposedMiddleware<TIn>;
@@ -185,24 +316,148 @@ interface EventComposer<TBase extends object, TEventMap extends Record<string, T
185
316
  prototype: Error;
186
317
  }>;
187
318
  tracer: TraceHandler | undefined;
319
+ macros: Record<string, MacroDef<any, any>>;
188
320
  Derives: TDerives;
321
+ /** Phantom type accessor — never set at runtime, used by `ContextOf<T>` */
322
+ Out: TOut;
189
323
  };
190
324
  invalidate(): void;
191
325
  }
192
- interface EventComposerConstructor<TBase extends object, TEventMap extends Record<string, TBase>> {
193
- new <TIn extends TBase = TBase, TOut extends TIn = TIn, TExposed extends object = {}, TDerives extends Record<string, object> = {}>(options?: ComposerOptions): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives>;
326
+ interface EventComposerConstructor<TBase extends object, TEventMap extends Record<string, TBase>, TMethods extends Record<string, (...args: any[]) => any> = {}> {
327
+ new <TIn extends TBase = TBase, TOut extends TIn = TIn, TExposed extends object = {}, TDerives extends Record<string, object> = {}, TMacros extends MacroDefinitions = {}>(options?: ComposerOptions): EventComposer<TBase, TEventMap, TIn, TOut, TExposed, TDerives, TMethods, TMacros> & TMethods;
194
328
  }
329
+ /**
330
+ * Extracts the current accumulated context type (`TOut`) from an EventComposer
331
+ * or Composer instance type.
332
+ *
333
+ * Use with a `this: TThis` parameter in custom methods to get automatic
334
+ * derive type inference — the handler receives all types from previous `.derive()` calls
335
+ * without requiring explicit annotation at the call site.
336
+ *
337
+ * @example
338
+ * ```ts
339
+ * const { Composer } = createComposer({
340
+ * methods: {
341
+ * // Automatic: caller's derives are inferred into the handler
342
+ * command<TThis>(
343
+ * this: TThis,
344
+ * name: string,
345
+ * handler: Middleware<MessageCtx & ContextOf<TThis>>,
346
+ * ) {
347
+ * return (this as any).on("message", (ctx: any, next: any) => {
348
+ * if (ctx.text === `/${name}`) return handler(ctx, next);
349
+ * return next();
350
+ * }) as TThis;
351
+ * },
352
+ *
353
+ * // Manual: caller declares which derives they need via Patch generic
354
+ * hears<Patch extends object = {}>(
355
+ * trigger: string,
356
+ * handler: Middleware<MessageCtx & Patch>,
357
+ * ) {
358
+ * return this.on<"message", Patch>("message", (ctx, next) => {
359
+ * if (ctx.text === trigger) return handler(ctx, next);
360
+ * return next();
361
+ * });
362
+ * },
363
+ * },
364
+ * });
365
+ *
366
+ * // With ContextOf — no type annotation needed at call site:
367
+ * new Composer()
368
+ * .derive(() => ({ user: { id: 1 } }))
369
+ * .command("start", (ctx) => ctx.user.id); // ✅ inferred automatically
370
+ *
371
+ * // With Patch — caller declares what they need:
372
+ * new Composer()
373
+ * .derive(() => ({ user: { id: 1 } }))
374
+ * .hears<{ user: { id: number } }>("hello", (ctx) => ctx.user.id); // ✅
375
+ * ```
376
+ */
377
+ type ContextOf<T> = T extends {
378
+ "~": {
379
+ Out: infer O;
380
+ };
381
+ } ? O : never;
382
+ /**
383
+ * Helper to define custom composer methods with full TypeScript inference.
384
+ *
385
+ * TypeScript cannot infer generic method signatures when they're passed directly
386
+ * inside `createComposer({ methods: { ... } })` because `TMethods` is buried
387
+ * inside the nested return type `{ Composer: EventComposerConstructor<..., TMethods> }`.
388
+ *
389
+ * This helper has return type `TMethods` directly — which lets TypeScript preserve
390
+ * generic method signatures. Pass the result (via `typeof`) as the 3rd type argument
391
+ * to `createComposer`:
392
+ *
393
+ * @example
394
+ * ```ts
395
+ * const methods = defineComposerMethods({
396
+ * // Pattern: `this: TThis` + `ContextOf<TThis>` — zero annotation at call site
397
+ * command<TThis>(
398
+ * this: TThis,
399
+ * name: string,
400
+ * handler: Middleware<MessageCtx & ContextOf<TThis>>,
401
+ * ): TThis {
402
+ * return (this as any).on("message", (ctx: any, next: any) => {
403
+ * if (ctx.text === `/${name}`) return handler(ctx, next);
404
+ * return next();
405
+ * }) as TThis;
406
+ * },
407
+ * });
408
+ *
409
+ * const { Composer } = createComposer<Base, EventMap, typeof methods>({
410
+ * discriminator: (ctx) => ctx.updateType,
411
+ * methods,
412
+ * });
413
+ *
414
+ * // No annotation needed — derives flow in automatically:
415
+ * new Composer()
416
+ * .derive(() => ({ user: { id: 1 } }))
417
+ * .command("start", (ctx) => ctx.user.id); // ✅
418
+ * ```
419
+ */
420
+ declare function defineComposerMethods<TMethods extends Record<string, (...args: any[]) => any>>(methods: TMethods): TMethods;
421
+ /**
422
+ * Phantom type carrier for event map inference.
423
+ * Returns `undefined` at runtime — exists purely for type-level inference
424
+ * so that `TEventMap` can be inferred from the `types` config field.
425
+ *
426
+ * @example
427
+ * ```ts
428
+ * const { Composer } = createComposer({
429
+ * discriminator: (ctx: BaseCtx) => ctx.updateType,
430
+ * types: eventTypes<EventMap>(),
431
+ * methods: { hears(trigger) { return this.on("message", ...); } },
432
+ * });
433
+ * ```
434
+ */
435
+ declare function eventTypes<TEventMap extends Record<string, any>>(): TEventMap;
195
436
  /**
196
437
  * Creates a configured Composer class with type-safe .on() event discrimination.
197
438
  */
198
- declare function createComposer<TBase extends object, TEventMap extends Record<string, TBase> = {}>(config: {
439
+ declare function createComposer<TBase extends object, TEventMap extends Record<string, TBase> = {}, TMethods extends Record<string, (...args: any[]) => any> = {}>(config: {
199
440
  discriminator: (context: TBase) => string;
441
+ types?: TEventMap;
442
+ methods?: TMethods & ThisType<EventComposer<TBase, TEventMap, TBase, TBase, {}, {}, TMethods> & TMethods>;
200
443
  }): {
201
- Composer: EventComposerConstructor<TBase, TEventMap>;
444
+ Composer: EventComposerConstructor<TBase, TEventMap, TMethods>;
202
445
  compose: typeof compose;
203
446
  EventQueue: typeof EventQueue;
204
447
  };
205
448
 
449
+ /**
450
+ * Composes a handler with macro hooks and preHandlers from an options object.
451
+ *
452
+ * Execution order:
453
+ * 1. `options.preHandler` array (explicit guards — user controls order)
454
+ * 2. Per-macro in options property order:
455
+ * a. macro.preHandler (guard middleware)
456
+ * b. macro.derive (context enrichment; void return = stop chain)
457
+ * 3. Main handler
458
+ */
459
+ declare function buildFromOptions<TCtx>(macros: Record<string, MacroDef<any, any>>, options: Record<string, unknown> | undefined, handler: Middleware<TCtx>): Middleware<TCtx>;
460
+
206
461
  /** No-op next function: () => Promise.resolve() */
207
462
  declare const noopNext: Next;
208
463
  /** Pass-through middleware: calls next() immediately */
@@ -210,5 +465,5 @@ declare const skip: Middleware<any>;
210
465
  /** Terminal middleware: does NOT call next() */
211
466
  declare const stop: Middleware<any>;
212
467
 
213
- export { Composer, EventQueue, compose, createComposer, noopNext, skip, stop };
214
- export type { ComposedMiddleware, ComposerOptions, DeriveHandler, ErrorHandler, EventComposer, EventComposerConstructor, LazyFactory, MaybeArray, Middleware, MiddlewareInfo, MiddlewareType, Next, RouteBuilder, RouteHandler, Scope, TraceHandler };
468
+ export { Composer, EventQueue, buildFromOptions, compose, createComposer, defineComposerMethods, eventTypes, noopNext, skip, stop };
469
+ export type { CompatibleEvents, ComposedMiddleware, ComposerLike, ComposerOptions, ContextCallback, ContextOf, DeriveFromOptions, DeriveHandler, ErrorHandler, EventComposer, EventComposerConstructor, HandlerOptions, LazyFactory, MacroDef, MacroDefinitions, MacroDeriveType, MacroHooks, MacroOptionType, MaybeArray, Middleware, MiddlewareInfo, MiddlewareType, Next, RouteBuilder, RouteHandler, Scope, TraceHandler, WithCtx };
package/dist/index.js CHANGED
@@ -76,7 +76,10 @@ class Composer {
76
76
  name: void 0,
77
77
  seed: void 0,
78
78
  errorsDefinitions: {},
79
- tracer: void 0
79
+ tracer: void 0,
80
+ macros: {},
81
+ /** Phantom type accessor — never set at runtime, used by `ContextOf<T>` */
82
+ Out: void 0
80
83
  };
81
84
  constructor(options) {
82
85
  this["~"].name = options?.name;
@@ -85,6 +88,14 @@ class Composer {
85
88
  invalidate() {
86
89
  this["~"].compiled = null;
87
90
  }
91
+ macro(nameOrDefs, definition) {
92
+ if (typeof nameOrDefs === "string") {
93
+ this["~"].macros[nameOrDefs] = definition;
94
+ } else {
95
+ Object.assign(this["~"].macros, nameOrDefs);
96
+ }
97
+ return this;
98
+ }
88
99
  decorate(values, options) {
89
100
  const mw = (ctx, next) => {
90
101
  Object.assign(ctx, values);
@@ -96,6 +107,7 @@ class Composer {
96
107
  this.invalidate();
97
108
  return this;
98
109
  }
110
+ // biome-ignore lint/suspicious/noExplicitAny: overload implementation signature
99
111
  use(...middleware) {
100
112
  for (const fn of middleware) {
101
113
  this["~"].middlewares.push({ fn, scope: "local", type: "use", name: fn.name || void 0 });
@@ -313,6 +325,7 @@ class Composer {
313
325
  this["~"].extended.add(key);
314
326
  }
315
327
  Object.assign(this["~"].errorsDefinitions, other["~"].errorsDefinitions);
328
+ Object.assign(this["~"].macros, other["~"].macros);
316
329
  this["~"].onErrors.push(...other["~"].onErrors);
317
330
  const pluginName = other["~"].name;
318
331
  const isNew = (m) => {
@@ -472,16 +485,36 @@ class EventQueue {
472
485
  }
473
486
  }
474
487
 
488
+ function defineComposerMethods(methods) {
489
+ return methods;
490
+ }
491
+ function eventTypes() {
492
+ return void 0;
493
+ }
475
494
  function createComposer(config) {
476
495
  class EventComposerImpl extends Composer {
477
- on(event, handler) {
478
- const events = Array.isArray(event) ? event : [event];
496
+ on(eventOrFilter, filterOrHandler, handler) {
497
+ if (typeof eventOrFilter === "function") {
498
+ const filter2 = eventOrFilter;
499
+ const actualHandler2 = filterOrHandler;
500
+ const filterLabel = filter2.name || "filter";
501
+ const mw2 = (ctx, next) => {
502
+ if (filter2(ctx)) return actualHandler2(ctx, next);
503
+ return next();
504
+ };
505
+ nameMiddleware(mw2, "on", filterLabel);
506
+ this["~"].middlewares.push({ fn: mw2, scope: "local", type: "on", name: filterLabel });
507
+ this.invalidate();
508
+ return this;
509
+ }
510
+ const events = Array.isArray(eventOrFilter) ? eventOrFilter : [eventOrFilter];
479
511
  const eventLabel = events.join("|");
512
+ const actualHandler = handler ?? filterOrHandler;
513
+ const filter = handler ? filterOrHandler : void 0;
480
514
  const mw = (ctx, next) => {
481
- if (events.includes(config.discriminator(ctx))) {
482
- return handler(ctx, next);
483
- }
484
- return next();
515
+ if (!events.includes(config.discriminator(ctx))) return next();
516
+ if (filter && !filter(ctx)) return next();
517
+ return actualHandler(ctx, next);
485
518
  };
486
519
  nameMiddleware(mw, "on", eventLabel);
487
520
  this["~"].middlewares.push({ fn: mw, scope: "local", type: "on", name: eventLabel });
@@ -509,6 +542,19 @@ function createComposer(config) {
509
542
  return this;
510
543
  }
511
544
  }
545
+ if (config.methods) {
546
+ for (const [name, fn] of Object.entries(config.methods)) {
547
+ if (name in EventComposerImpl.prototype) {
548
+ throw new Error(`Custom method "${name}" conflicts with built-in method`);
549
+ }
550
+ Object.defineProperty(EventComposerImpl.prototype, name, {
551
+ value: fn,
552
+ writable: true,
553
+ configurable: true,
554
+ enumerable: false
555
+ });
556
+ }
557
+ }
512
558
  return {
513
559
  Composer: EventComposerImpl,
514
560
  compose,
@@ -516,4 +562,44 @@ function createComposer(config) {
516
562
  };
517
563
  }
518
564
 
519
- export { Composer, EventQueue, compose, createComposer, noopNext, skip, stop };
565
+ function buildFromOptions(macros, options, handler) {
566
+ if (!options) return handler;
567
+ const chain = [];
568
+ const preHandler = options.preHandler;
569
+ if (preHandler) {
570
+ if (Array.isArray(preHandler)) {
571
+ chain.push(...preHandler);
572
+ } else {
573
+ chain.push(preHandler);
574
+ }
575
+ }
576
+ for (const key of Object.keys(options)) {
577
+ if (key === "preHandler") continue;
578
+ const value = options[key];
579
+ if (value === false || value == null) continue;
580
+ const def = macros[key];
581
+ if (!def) continue;
582
+ const hooks = typeof def === "function" ? def(value === true ? void 0 : value) : def;
583
+ if (hooks.preHandler) {
584
+ if (Array.isArray(hooks.preHandler)) {
585
+ chain.push(...hooks.preHandler);
586
+ } else {
587
+ chain.push(hooks.preHandler);
588
+ }
589
+ }
590
+ if (hooks.derive) {
591
+ const deriveFn = hooks.derive;
592
+ chain.push(async (ctx, next) => {
593
+ const derived = await deriveFn(ctx);
594
+ if (derived == null) return;
595
+ Object.assign(ctx, derived);
596
+ return next();
597
+ });
598
+ }
599
+ }
600
+ chain.push(handler);
601
+ if (chain.length === 1) return chain[0];
602
+ return compose(chain);
603
+ }
604
+
605
+ export { Composer, EventQueue, buildFromOptions, compose, createComposer, defineComposerMethods, eventTypes, noopNext, skip, stop };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gramio/composer",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "General-purpose, type-safe middleware composition library for TypeScript",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",