@gramio/composer 0.1.0 → 0.2.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/README.md CHANGED
@@ -305,6 +305,52 @@ const app = new Composer()
305
305
  });
306
306
  ```
307
307
 
308
+ #### `types` + `eventTypes()` — phantom type inference
309
+
310
+ TypeScript cannot partially infer type arguments, so when you need both `TEventMap` and `TMethods` inferred together, use the `types` phantom field with the `eventTypes()` helper instead of explicit type parameters:
311
+
312
+ ```ts
313
+ import { createComposer, eventTypes } from "@gramio/composer";
314
+
315
+ // eventTypes<T>() returns undefined at runtime — purely for inference
316
+ const { Composer } = createComposer({
317
+ discriminator: (ctx: BaseCtx) => ctx.updateType,
318
+ types: eventTypes<{ message: MessageCtx; callback_query: CallbackCtx }>(),
319
+ });
320
+ // TBase inferred from discriminator, TEventMap inferred from types
321
+ ```
322
+
323
+ #### `methods` — custom prototype methods
324
+
325
+ Inject framework-specific DX sugar directly onto the Composer prototype via the `methods` config option. Method bodies receive `this` typed as the full `EventComposer`, giving access to `.on()`, `.use()`, `.derive()`, etc.
326
+
327
+ ```ts
328
+ const { Composer } = createComposer({
329
+ discriminator: (ctx: BaseCtx) => ctx.updateType,
330
+ types: eventTypes<{ message: MessageCtx }>(),
331
+ methods: {
332
+ hears(trigger: RegExp | string, handler: (ctx: MessageCtx) => unknown) {
333
+ return this.on("message", (ctx, next) => {
334
+ const text = ctx.text;
335
+ if (
336
+ (typeof trigger === "string" && text === trigger) ||
337
+ (trigger instanceof RegExp && text && trigger.test(text))
338
+ ) {
339
+ return handler(ctx);
340
+ }
341
+ return next();
342
+ });
343
+ },
344
+ },
345
+ });
346
+
347
+ const bot = new Composer();
348
+ bot.hears(/hello/, handler); // custom method
349
+ bot.on("message", h).hears(/hi/, h2); // chaining works — TMethods preserved
350
+ ```
351
+
352
+ Custom methods are preserved through **all** method chains (`on`, `use`, `derive`, `extend`, etc.). A runtime conflict check throws if a method name collides with a built-in (e.g. `on`, `use`, `derive`).
353
+
308
354
  ### `EventQueue`
309
355
 
310
356
  Concurrent event queue with graceful shutdown.
package/dist/index.cjs CHANGED
@@ -311,15 +311,23 @@ class Composer {
311
311
  if (this["~"].extended.has(key)) return this;
312
312
  this["~"].extended.add(key);
313
313
  }
314
+ const alreadyExtended = new Set(this["~"].extended);
314
315
  for (const key of other["~"].extended) {
315
316
  this["~"].extended.add(key);
316
317
  }
317
318
  Object.assign(this["~"].errorsDefinitions, other["~"].errorsDefinitions);
318
319
  this["~"].onErrors.push(...other["~"].onErrors);
319
320
  const pluginName = other["~"].name;
320
- const localMws = other["~"].middlewares.filter((m) => m.scope === "local");
321
- const scopedMws = other["~"].middlewares.filter((m) => m.scope === "scoped");
322
- const globalMws = other["~"].middlewares.filter((m) => m.scope === "global");
321
+ const isNew = (m) => {
322
+ if (!m.plugin) return true;
323
+ for (const key of alreadyExtended) {
324
+ if (key.startsWith(m.plugin + ":")) return false;
325
+ }
326
+ return true;
327
+ };
328
+ const localMws = other["~"].middlewares.filter((m) => m.scope === "local" && isNew(m));
329
+ const scopedMws = other["~"].middlewares.filter((m) => m.scope === "scoped" && isNew(m));
330
+ const globalMws = other["~"].middlewares.filter((m) => m.scope === "global" && isNew(m));
323
331
  if (localMws.length > 0) {
324
332
  const chain = compose(localMws.map((m) => m.fn));
325
333
  const isolated = async (ctx, next) => {
@@ -467,6 +475,9 @@ class EventQueue {
467
475
  }
468
476
  }
469
477
 
478
+ function eventTypes() {
479
+ return void 0;
480
+ }
470
481
  function createComposer(config) {
471
482
  class EventComposerImpl extends Composer {
472
483
  on(event, handler) {
@@ -504,6 +515,19 @@ function createComposer(config) {
504
515
  return this;
505
516
  }
506
517
  }
518
+ if (config.methods) {
519
+ for (const [name, fn] of Object.entries(config.methods)) {
520
+ if (name in EventComposerImpl.prototype) {
521
+ throw new Error(`Custom method "${name}" conflicts with built-in method`);
522
+ }
523
+ Object.defineProperty(EventComposerImpl.prototype, name, {
524
+ value: fn,
525
+ writable: true,
526
+ configurable: true,
527
+ enumerable: false
528
+ });
529
+ }
530
+ }
507
531
  return {
508
532
  Composer: EventComposerImpl,
509
533
  compose,
@@ -515,6 +539,7 @@ exports.Composer = Composer;
515
539
  exports.EventQueue = EventQueue;
516
540
  exports.compose = compose;
517
541
  exports.createComposer = createComposer;
542
+ exports.eventTypes = eventTypes;
518
543
  exports.noopNext = noopNext;
519
544
  exports.skip = skip;
520
545
  exports.stop = stop;
package/dist/index.d.cts CHANGED
@@ -15,7 +15,7 @@ type DeriveHandler<T, D> = (context: T) => D | Promise<D>;
15
15
  /** Lazy middleware factory — called per invocation */
16
16
  type LazyFactory<T> = (context: T) => Middleware<T> | Promise<Middleware<T>>;
17
17
  /** Single value or array */
18
- type MaybeArray<T> = T | T[];
18
+ 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 */
@@ -136,39 +136,39 @@ declare class EventQueue<T> {
136
136
  * Array of events → discriminated union where each branch has correct derives.
137
137
  */
138
138
  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>;
139
+ /** EventComposer interface — Composer + .on() + per-event derive tracking + custom methods */
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> = {}, TMethods extends Record<string, (...args: any[]) => any> = {}> {
141
+ on<E extends keyof TEventMap & string>(event: MaybeArray<E>, handler: Middleware<ResolveEventCtx<TOut, TEventMap, TDerives, E>>): this;
142
+ use(...middleware: Middleware<TOut>[]): this;
143
+ guard<S extends TOut>(predicate: ((context: TOut) => context is S) | ((context: TOut) => boolean | Promise<boolean>), ...middleware: Middleware<any>[]): this;
144
+ branch(predicate: ((context: TOut) => boolean | Promise<boolean>) | boolean, onTrue: Middleware<TOut>, onFalse?: Middleware<TOut>): this;
145
+ 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;
146
+ route<K extends string>(router: (context: TOut) => K | undefined | Promise<K | undefined>, builder: (route: RouteBuilder<TOut, K>) => void): this;
147
+ 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;
148
+ fork(...middleware: Middleware<TOut>[]): this;
149
+ tap(...middleware: Middleware<TOut>[]): this;
150
+ lazy(factory: LazyFactory<TOut>): this;
151
+ onError(handler: ErrorHandler<TOut>): this;
152
+ error(kind: string, errorClass: {
153
+ new (...args: any): any;
154
+ prototype: Error;
155
+ }): this;
156
+ group(fn: (composer: Composer<TOut, TOut, {}>) => void): this;
157
+ decorate<D extends object>(values: D): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed, TDerives, TMethods> & TMethods;
143
158
  decorate<D extends object>(values: D, options: {
144
159
  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>;
160
+ }): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed & D, TDerives, TMethods> & TMethods;
161
+ derive<D extends object>(handler: DeriveHandler<TOut, D>): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed, TDerives, TMethods> & TMethods;
148
162
  derive<D extends object>(handler: DeriveHandler<TOut, D>, options: {
149
163
  as: "scoped" | "global";
150
- }): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed & D, TDerives>;
164
+ }): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed & D, TDerives, TMethods> & TMethods;
151
165
  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
166
  [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>;
167
+ }, TMethods> & TMethods;
168
+ 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> & TMethods;
169
+ as(scope: "scoped" | "global"): EventComposer<TBase, TEventMap, TIn, TOut, TOut, TDerives, TMethods> & TMethods;
170
+ extend<UIn extends TBase, UOut extends UIn, UExposed extends object, UDerives extends Record<string, object>>(other: EventComposer<TBase, TEventMap, UIn, UOut, UExposed, UDerives, any>): EventComposer<TBase, TEventMap, TIn, TOut & UExposed, TExposed, TDerives & UDerives, TMethods> & TMethods;
171
+ extend<UIn extends object, UOut extends UIn, UExposed extends object>(other: Composer<UIn, UOut, UExposed>): EventComposer<TBase, TEventMap, TIn, TOut & UExposed, TExposed, TDerives, TMethods> & TMethods;
172
172
  inspect(): MiddlewareInfo[];
173
173
  trace(handler: TraceHandler): this;
174
174
  compose(): ComposedMiddleware<TIn>;
@@ -189,16 +189,33 @@ interface EventComposer<TBase extends object, TEventMap extends Record<string, T
189
189
  };
190
190
  invalidate(): void;
191
191
  }
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>;
192
+ interface EventComposerConstructor<TBase extends object, TEventMap extends Record<string, TBase>, TMethods extends Record<string, (...args: any[]) => any> = {}> {
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, TMethods> & TMethods;
194
194
  }
195
+ /**
196
+ * Phantom type carrier for event map inference.
197
+ * Returns `undefined` at runtime — exists purely for type-level inference
198
+ * so that `TEventMap` can be inferred from the `types` config field.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * const { Composer } = createComposer({
203
+ * discriminator: (ctx: BaseCtx) => ctx.updateType,
204
+ * types: eventTypes<EventMap>(),
205
+ * methods: { hears(trigger) { return this.on("message", ...); } },
206
+ * });
207
+ * ```
208
+ */
209
+ declare function eventTypes<TEventMap extends Record<string, any>>(): TEventMap;
195
210
  /**
196
211
  * Creates a configured Composer class with type-safe .on() event discrimination.
197
212
  */
198
- declare function createComposer<TBase extends object, TEventMap extends Record<string, TBase> = {}>(config: {
213
+ declare function createComposer<TBase extends object, TEventMap extends Record<string, TBase> = {}, TMethods extends Record<string, (...args: any[]) => any> = {}>(config: {
199
214
  discriminator: (context: TBase) => string;
215
+ types?: TEventMap;
216
+ methods?: TMethods & ThisType<EventComposer<TBase, TEventMap, TBase, TBase, {}, {}, TMethods> & TMethods>;
200
217
  }): {
201
- Composer: EventComposerConstructor<TBase, TEventMap>;
218
+ Composer: EventComposerConstructor<TBase, TEventMap, TMethods>;
202
219
  compose: typeof compose;
203
220
  EventQueue: typeof EventQueue;
204
221
  };
@@ -210,5 +227,5 @@ declare const skip: Middleware<any>;
210
227
  /** Terminal middleware: does NOT call next() */
211
228
  declare const stop: Middleware<any>;
212
229
 
213
- export { Composer, EventQueue, compose, createComposer, noopNext, skip, stop };
230
+ export { Composer, EventQueue, compose, createComposer, eventTypes, noopNext, skip, stop };
214
231
  export type { ComposedMiddleware, ComposerOptions, DeriveHandler, ErrorHandler, EventComposer, EventComposerConstructor, LazyFactory, MaybeArray, Middleware, MiddlewareInfo, MiddlewareType, Next, RouteBuilder, RouteHandler, Scope, TraceHandler };
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ type DeriveHandler<T, D> = (context: T) => D | Promise<D>;
15
15
  /** Lazy middleware factory — called per invocation */
16
16
  type LazyFactory<T> = (context: T) => Middleware<T> | Promise<Middleware<T>>;
17
17
  /** Single value or array */
18
- type MaybeArray<T> = T | T[];
18
+ 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 */
@@ -136,39 +136,39 @@ declare class EventQueue<T> {
136
136
  * Array of events → discriminated union where each branch has correct derives.
137
137
  */
138
138
  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>;
139
+ /** EventComposer interface — Composer + .on() + per-event derive tracking + custom methods */
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> = {}, TMethods extends Record<string, (...args: any[]) => any> = {}> {
141
+ on<E extends keyof TEventMap & string>(event: MaybeArray<E>, handler: Middleware<ResolveEventCtx<TOut, TEventMap, TDerives, E>>): this;
142
+ use(...middleware: Middleware<TOut>[]): this;
143
+ guard<S extends TOut>(predicate: ((context: TOut) => context is S) | ((context: TOut) => boolean | Promise<boolean>), ...middleware: Middleware<any>[]): this;
144
+ branch(predicate: ((context: TOut) => boolean | Promise<boolean>) | boolean, onTrue: Middleware<TOut>, onFalse?: Middleware<TOut>): this;
145
+ 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;
146
+ route<K extends string>(router: (context: TOut) => K | undefined | Promise<K | undefined>, builder: (route: RouteBuilder<TOut, K>) => void): this;
147
+ 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;
148
+ fork(...middleware: Middleware<TOut>[]): this;
149
+ tap(...middleware: Middleware<TOut>[]): this;
150
+ lazy(factory: LazyFactory<TOut>): this;
151
+ onError(handler: ErrorHandler<TOut>): this;
152
+ error(kind: string, errorClass: {
153
+ new (...args: any): any;
154
+ prototype: Error;
155
+ }): this;
156
+ group(fn: (composer: Composer<TOut, TOut, {}>) => void): this;
157
+ decorate<D extends object>(values: D): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed, TDerives, TMethods> & TMethods;
143
158
  decorate<D extends object>(values: D, options: {
144
159
  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>;
160
+ }): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed & D, TDerives, TMethods> & TMethods;
161
+ derive<D extends object>(handler: DeriveHandler<TOut, D>): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed, TDerives, TMethods> & TMethods;
148
162
  derive<D extends object>(handler: DeriveHandler<TOut, D>, options: {
149
163
  as: "scoped" | "global";
150
- }): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed & D, TDerives>;
164
+ }): EventComposer<TBase, TEventMap, TIn, TOut & D, TExposed & D, TDerives, TMethods> & TMethods;
151
165
  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
166
  [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>;
167
+ }, TMethods> & TMethods;
168
+ 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> & TMethods;
169
+ as(scope: "scoped" | "global"): EventComposer<TBase, TEventMap, TIn, TOut, TOut, TDerives, TMethods> & TMethods;
170
+ extend<UIn extends TBase, UOut extends UIn, UExposed extends object, UDerives extends Record<string, object>>(other: EventComposer<TBase, TEventMap, UIn, UOut, UExposed, UDerives, any>): EventComposer<TBase, TEventMap, TIn, TOut & UExposed, TExposed, TDerives & UDerives, TMethods> & TMethods;
171
+ extend<UIn extends object, UOut extends UIn, UExposed extends object>(other: Composer<UIn, UOut, UExposed>): EventComposer<TBase, TEventMap, TIn, TOut & UExposed, TExposed, TDerives, TMethods> & TMethods;
172
172
  inspect(): MiddlewareInfo[];
173
173
  trace(handler: TraceHandler): this;
174
174
  compose(): ComposedMiddleware<TIn>;
@@ -189,16 +189,33 @@ interface EventComposer<TBase extends object, TEventMap extends Record<string, T
189
189
  };
190
190
  invalidate(): void;
191
191
  }
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>;
192
+ interface EventComposerConstructor<TBase extends object, TEventMap extends Record<string, TBase>, TMethods extends Record<string, (...args: any[]) => any> = {}> {
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, TMethods> & TMethods;
194
194
  }
195
+ /**
196
+ * Phantom type carrier for event map inference.
197
+ * Returns `undefined` at runtime — exists purely for type-level inference
198
+ * so that `TEventMap` can be inferred from the `types` config field.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * const { Composer } = createComposer({
203
+ * discriminator: (ctx: BaseCtx) => ctx.updateType,
204
+ * types: eventTypes<EventMap>(),
205
+ * methods: { hears(trigger) { return this.on("message", ...); } },
206
+ * });
207
+ * ```
208
+ */
209
+ declare function eventTypes<TEventMap extends Record<string, any>>(): TEventMap;
195
210
  /**
196
211
  * Creates a configured Composer class with type-safe .on() event discrimination.
197
212
  */
198
- declare function createComposer<TBase extends object, TEventMap extends Record<string, TBase> = {}>(config: {
213
+ declare function createComposer<TBase extends object, TEventMap extends Record<string, TBase> = {}, TMethods extends Record<string, (...args: any[]) => any> = {}>(config: {
199
214
  discriminator: (context: TBase) => string;
215
+ types?: TEventMap;
216
+ methods?: TMethods & ThisType<EventComposer<TBase, TEventMap, TBase, TBase, {}, {}, TMethods> & TMethods>;
200
217
  }): {
201
- Composer: EventComposerConstructor<TBase, TEventMap>;
218
+ Composer: EventComposerConstructor<TBase, TEventMap, TMethods>;
202
219
  compose: typeof compose;
203
220
  EventQueue: typeof EventQueue;
204
221
  };
@@ -210,5 +227,5 @@ declare const skip: Middleware<any>;
210
227
  /** Terminal middleware: does NOT call next() */
211
228
  declare const stop: Middleware<any>;
212
229
 
213
- export { Composer, EventQueue, compose, createComposer, noopNext, skip, stop };
230
+ export { Composer, EventQueue, compose, createComposer, eventTypes, noopNext, skip, stop };
214
231
  export type { ComposedMiddleware, ComposerOptions, DeriveHandler, ErrorHandler, EventComposer, EventComposerConstructor, LazyFactory, MaybeArray, Middleware, MiddlewareInfo, MiddlewareType, Next, RouteBuilder, RouteHandler, Scope, TraceHandler };
package/dist/index.js CHANGED
@@ -308,15 +308,23 @@ class Composer {
308
308
  if (this["~"].extended.has(key)) return this;
309
309
  this["~"].extended.add(key);
310
310
  }
311
+ const alreadyExtended = new Set(this["~"].extended);
311
312
  for (const key of other["~"].extended) {
312
313
  this["~"].extended.add(key);
313
314
  }
314
315
  Object.assign(this["~"].errorsDefinitions, other["~"].errorsDefinitions);
315
316
  this["~"].onErrors.push(...other["~"].onErrors);
316
317
  const pluginName = other["~"].name;
317
- const localMws = other["~"].middlewares.filter((m) => m.scope === "local");
318
- const scopedMws = other["~"].middlewares.filter((m) => m.scope === "scoped");
319
- const globalMws = other["~"].middlewares.filter((m) => m.scope === "global");
318
+ const isNew = (m) => {
319
+ if (!m.plugin) return true;
320
+ for (const key of alreadyExtended) {
321
+ if (key.startsWith(m.plugin + ":")) return false;
322
+ }
323
+ return true;
324
+ };
325
+ const localMws = other["~"].middlewares.filter((m) => m.scope === "local" && isNew(m));
326
+ const scopedMws = other["~"].middlewares.filter((m) => m.scope === "scoped" && isNew(m));
327
+ const globalMws = other["~"].middlewares.filter((m) => m.scope === "global" && isNew(m));
320
328
  if (localMws.length > 0) {
321
329
  const chain = compose(localMws.map((m) => m.fn));
322
330
  const isolated = async (ctx, next) => {
@@ -464,6 +472,9 @@ class EventQueue {
464
472
  }
465
473
  }
466
474
 
475
+ function eventTypes() {
476
+ return void 0;
477
+ }
467
478
  function createComposer(config) {
468
479
  class EventComposerImpl extends Composer {
469
480
  on(event, handler) {
@@ -501,6 +512,19 @@ function createComposer(config) {
501
512
  return this;
502
513
  }
503
514
  }
515
+ if (config.methods) {
516
+ for (const [name, fn] of Object.entries(config.methods)) {
517
+ if (name in EventComposerImpl.prototype) {
518
+ throw new Error(`Custom method "${name}" conflicts with built-in method`);
519
+ }
520
+ Object.defineProperty(EventComposerImpl.prototype, name, {
521
+ value: fn,
522
+ writable: true,
523
+ configurable: true,
524
+ enumerable: false
525
+ });
526
+ }
527
+ }
504
528
  return {
505
529
  Composer: EventComposerImpl,
506
530
  compose,
@@ -508,4 +532,4 @@ function createComposer(config) {
508
532
  };
509
533
  }
510
534
 
511
- export { Composer, EventQueue, compose, createComposer, noopNext, skip, stop };
535
+ export { Composer, EventQueue, compose, createComposer, eventTypes, noopNext, skip, stop };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gramio/composer",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",