@gramio/composer 0.3.0 → 0.3.3
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 +103 -5
- package/dist/index.cjs +22 -4
- package/dist/index.d.cts +35 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.js +22 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -282,6 +282,32 @@ app.extend(limit100); // applied
|
|
|
282
282
|
app.extend(limit200); // applied (different seed)
|
|
283
283
|
```
|
|
284
284
|
|
|
285
|
+
> [!WARNING]
|
|
286
|
+
> **Dedup removes middleware at registration time — not at runtime per-request.**
|
|
287
|
+
>
|
|
288
|
+
> If a shared plugin (e.g. `withUser`) is extended only inside sub-composers, its
|
|
289
|
+
> `derive` runs inside each sub-composer's isolation group. When dedup removes the
|
|
290
|
+
> derive from the second sub-composer, `ctx.user` set in the first group is **not
|
|
291
|
+
> visible** in the second — TypeScript types are correct, runtime value is `undefined`.
|
|
292
|
+
>
|
|
293
|
+
> **Fix:** extend the shared composer at the level where its data must be available,
|
|
294
|
+
> and let sub-composers extend it only for type safety (dedup prevents double execution).
|
|
295
|
+
>
|
|
296
|
+
> ```ts
|
|
297
|
+
> // ✅ correct — withUser runs once on the real ctx, both routers see ctx.user
|
|
298
|
+
> app
|
|
299
|
+
> .extend(withUser) // derive on real ctx
|
|
300
|
+
> .extend(adminRouter) // withUser inside → deduped (skipped)
|
|
301
|
+
> .extend(chatRouter); // withUser inside → deduped (skipped)
|
|
302
|
+
>
|
|
303
|
+
> // ⚠️ risky — works only if routers are mutually exclusive (one handles per update)
|
|
304
|
+
> app
|
|
305
|
+
> .extend(adminRouter) // withUser runs in isolation group
|
|
306
|
+
> .extend(chatRouter); // withUser deduped → chatHandlers can't see ctx.user
|
|
307
|
+
> ```
|
|
308
|
+
>
|
|
309
|
+
> See [`docs/layered-composers.md`](./docs/layered-composers.md) for the full breakdown.
|
|
310
|
+
|
|
285
311
|
### `createComposer(config)` — Event System
|
|
286
312
|
|
|
287
313
|
Factory that creates a Composer class with `.on()` event discrimination.
|
|
@@ -464,22 +490,94 @@ new Composer()
|
|
|
464
490
|
|
|
465
491
|
#### `ContextOf<T>` — extract the current context type
|
|
466
492
|
|
|
467
|
-
Extracts `TOut`
|
|
493
|
+
Extracts `TOut` (the fully accumulated context type after all `.derive()` and `.decorate()` calls) from a Composer or EventComposer instance type.
|
|
494
|
+
|
|
495
|
+
**Naming a plugin's context type for reuse:**
|
|
468
496
|
|
|
469
497
|
```ts
|
|
470
498
|
import type { ContextOf } from "@gramio/composer";
|
|
471
499
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
500
|
+
const withUser = new Composer()
|
|
501
|
+
.derive(async (ctx) => ({
|
|
502
|
+
user: await db.getUser(ctx.userId),
|
|
503
|
+
}));
|
|
504
|
+
|
|
505
|
+
// Extract the enriched context — no manual conditional type needed
|
|
506
|
+
export type WithUser = ContextOf<typeof withUser>;
|
|
507
|
+
// WithUser = { userId: string } & { user: User }
|
|
508
|
+
|
|
509
|
+
// Use it in standalone functions, other plugins, or type assertions:
|
|
510
|
+
function requireAdmin(ctx: WithUser) {
|
|
511
|
+
if (!ctx.user.isAdmin) throw new Error("Forbidden");
|
|
512
|
+
}
|
|
513
|
+
```
|
|
475
514
|
|
|
476
|
-
|
|
515
|
+
**In a custom method signature** — `ContextOf<TThis>` captures all derives accumulated at the call site:
|
|
516
|
+
|
|
517
|
+
```ts
|
|
477
518
|
command<TThis extends ComposerLike<TThis>>(
|
|
478
519
|
this: TThis,
|
|
479
520
|
handler: Middleware<ContextOf<TThis>>,
|
|
480
521
|
): TThis
|
|
481
522
|
```
|
|
482
523
|
|
|
524
|
+
#### `EventContextOf<T, E>` — extract context for a specific event (global + per-event derives)
|
|
525
|
+
|
|
526
|
+
Like `ContextOf<T>`, but also includes per-event derives registered via `derive(event, handler)`.
|
|
527
|
+
|
|
528
|
+
| | Includes |
|
|
529
|
+
|---|---|
|
|
530
|
+
| `ContextOf<TThis>` | `TOut` — global derives only |
|
|
531
|
+
| `EventContextOf<TThis, "message">` | `TOut & TDerives["message"]` — global **and** per-event derives |
|
|
532
|
+
|
|
533
|
+
**Why it matters:** when a custom method always routes to a specific event (e.g. `command` → `"message"`), its handler should see per-event derives too. With `ContextOf` alone, a `derive("message", ...)` plugin's types are invisible inside the handler even though the value is there at runtime.
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
import { createComposer, defineComposerMethods, eventTypes } from "@gramio/composer";
|
|
537
|
+
import type { ComposerLike, EventContextOf, Middleware } from "@gramio/composer";
|
|
538
|
+
|
|
539
|
+
const methods = defineComposerMethods({
|
|
540
|
+
command<TThis extends ComposerLike<TThis>>(
|
|
541
|
+
this: TThis,
|
|
542
|
+
name: string,
|
|
543
|
+
// ↓ EventContextOf instead of ContextOf
|
|
544
|
+
handler: Middleware<MessageCtx & EventContextOf<TThis, "message">>,
|
|
545
|
+
): TThis {
|
|
546
|
+
return this.on("message", (ctx: any, next: Next) => {
|
|
547
|
+
if (ctx.text === `/${name}`) return handler(ctx, next);
|
|
548
|
+
return next();
|
|
549
|
+
});
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const { Composer } = createComposer<BaseCtx, { message: MessageCtx }, typeof methods>({
|
|
554
|
+
discriminator: (ctx) => ctx.updateType,
|
|
555
|
+
methods,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// Per-event plugin that adds `t` only for message events:
|
|
559
|
+
const i18nPlugin = new Composer({ name: "i18n" })
|
|
560
|
+
.derive("message", (ctx) => ({
|
|
561
|
+
t: i18n.buildT(ctx.from?.languageCode ?? "en"),
|
|
562
|
+
}))
|
|
563
|
+
.as("scoped");
|
|
564
|
+
|
|
565
|
+
new Composer()
|
|
566
|
+
.extend(i18nPlugin)
|
|
567
|
+
.command("start", (ctx, next) => {
|
|
568
|
+
ctx.t("Hello"); // ✅ typed — EventContextOf sees TDerives["message"]
|
|
569
|
+
ctx.text; // ✅ string | undefined — from MessageCtx
|
|
570
|
+
return next();
|
|
571
|
+
})
|
|
572
|
+
.on("message", (ctx, next) => {
|
|
573
|
+
ctx.t("Hi"); // ✅ also works here via ResolveEventCtx
|
|
574
|
+
return next();
|
|
575
|
+
});
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
> [!NOTE]
|
|
579
|
+
> If the derive is registered globally (`.derive(() => ...)` without an event name), both `ContextOf` and `EventContextOf` will see it. Per-event derives (`derive("message", ...)`) are only visible through `EventContextOf` in custom method signatures, or directly inside `.on("message", ...)` handlers.
|
|
580
|
+
|
|
483
581
|
#### `ComposerLike<T>` — minimal structural type for `this` constraints
|
|
484
582
|
|
|
485
583
|
A minimal interface `{ on(event: any, handler: any): T }` used as an F-bounded constraint on `TThis`. Makes `this.on(...)` fully typed and return `TThis` without casts.
|
package/dist/index.cjs
CHANGED
|
@@ -308,8 +308,17 @@ class Composer {
|
|
|
308
308
|
fn(group);
|
|
309
309
|
const chain = compose(group["~"].middlewares.map((m) => m.fn));
|
|
310
310
|
const mw = async (ctx, next) => {
|
|
311
|
-
const
|
|
312
|
-
|
|
311
|
+
const preKeys = new Set(Object.keys(ctx));
|
|
312
|
+
const snapshot = {};
|
|
313
|
+
for (const key of preKeys) snapshot[key] = ctx[key];
|
|
314
|
+
await chain(ctx, noopNext);
|
|
315
|
+
for (const key of Object.keys(ctx)) {
|
|
316
|
+
if (!preKeys.has(key)) {
|
|
317
|
+
const desc = Object.getOwnPropertyDescriptor(ctx, key);
|
|
318
|
+
if (desc?.configurable) delete ctx[key];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
Object.assign(ctx, snapshot);
|
|
313
322
|
return next();
|
|
314
323
|
};
|
|
315
324
|
nameMiddleware(mw, "group");
|
|
@@ -344,8 +353,17 @@ class Composer {
|
|
|
344
353
|
if (localMws.length > 0) {
|
|
345
354
|
const chain = compose(localMws.map((m) => m.fn));
|
|
346
355
|
const isolated = async (ctx, next) => {
|
|
347
|
-
const
|
|
348
|
-
|
|
356
|
+
const preKeys = new Set(Object.keys(ctx));
|
|
357
|
+
const snapshot = {};
|
|
358
|
+
for (const key of preKeys) snapshot[key] = ctx[key];
|
|
359
|
+
await chain(ctx, noopNext);
|
|
360
|
+
for (const key of Object.keys(ctx)) {
|
|
361
|
+
if (!preKeys.has(key)) {
|
|
362
|
+
const desc = Object.getOwnPropertyDescriptor(ctx, key);
|
|
363
|
+
if (desc?.configurable) delete ctx[key];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
Object.assign(ctx, snapshot);
|
|
349
367
|
return next();
|
|
350
368
|
};
|
|
351
369
|
nameMiddleware(isolated, "extend", pluginName);
|
package/dist/index.d.cts
CHANGED
|
@@ -379,6 +379,40 @@ type ContextOf<T> = T extends {
|
|
|
379
379
|
Out: infer O;
|
|
380
380
|
};
|
|
381
381
|
} ? O : never;
|
|
382
|
+
/**
|
|
383
|
+
* Extracts the context type for a specific event from an EventComposer instance,
|
|
384
|
+
* combining global `TOut` (like `ContextOf`) **and** per-event `TDerives[E]`.
|
|
385
|
+
*
|
|
386
|
+
* Use this in custom event-specific methods (e.g. `command`, `hears`) instead of
|
|
387
|
+
* `ContextOf<T>` so that handlers registered via `derive(event, handler)` are
|
|
388
|
+
* visible in the handler's type:
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* ```ts
|
|
392
|
+
* const methods = defineComposerMethods({
|
|
393
|
+
* command<TThis extends ComposerLike<TThis>>(
|
|
394
|
+
* this: TThis,
|
|
395
|
+
* name: string,
|
|
396
|
+
* handler: Middleware<MsgCtx & EventContextOf<TThis, "message">>,
|
|
397
|
+
* ): TThis {
|
|
398
|
+
* return this.on("message", (ctx, next) => {
|
|
399
|
+
* if (ctx.text === `/${name}`) return handler(ctx, next);
|
|
400
|
+
* return next();
|
|
401
|
+
* });
|
|
402
|
+
* },
|
|
403
|
+
* });
|
|
404
|
+
*
|
|
405
|
+
* new Composer()
|
|
406
|
+
* .derive("message", () => ({ t: (s: string) => s }))
|
|
407
|
+
* .command("start", (ctx) => ctx.t("Hi!")); // ✅ t is visible
|
|
408
|
+
* ```
|
|
409
|
+
*/
|
|
410
|
+
type EventContextOf<T, E extends string> = T extends {
|
|
411
|
+
"~": {
|
|
412
|
+
Out: infer O extends object;
|
|
413
|
+
Derives: infer D extends Record<string, object>;
|
|
414
|
+
};
|
|
415
|
+
} ? O & (E extends keyof D ? D[E] : {}) : never;
|
|
382
416
|
/**
|
|
383
417
|
* Helper to define custom composer methods with full TypeScript inference.
|
|
384
418
|
*
|
|
@@ -466,4 +500,4 @@ declare const skip: Middleware<any>;
|
|
|
466
500
|
declare const stop: Middleware<any>;
|
|
467
501
|
|
|
468
502
|
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 };
|
|
503
|
+
export type { CompatibleEvents, ComposedMiddleware, ComposerLike, ComposerOptions, ContextCallback, ContextOf, DeriveFromOptions, DeriveHandler, ErrorHandler, EventComposer, EventComposerConstructor, EventContextOf, HandlerOptions, LazyFactory, MacroDef, MacroDefinitions, MacroDeriveType, MacroHooks, MacroOptionType, MaybeArray, Middleware, MiddlewareInfo, MiddlewareType, Next, RouteBuilder, RouteHandler, Scope, TraceHandler, WithCtx };
|
package/dist/index.d.ts
CHANGED
|
@@ -379,6 +379,40 @@ type ContextOf<T> = T extends {
|
|
|
379
379
|
Out: infer O;
|
|
380
380
|
};
|
|
381
381
|
} ? O : never;
|
|
382
|
+
/**
|
|
383
|
+
* Extracts the context type for a specific event from an EventComposer instance,
|
|
384
|
+
* combining global `TOut` (like `ContextOf`) **and** per-event `TDerives[E]`.
|
|
385
|
+
*
|
|
386
|
+
* Use this in custom event-specific methods (e.g. `command`, `hears`) instead of
|
|
387
|
+
* `ContextOf<T>` so that handlers registered via `derive(event, handler)` are
|
|
388
|
+
* visible in the handler's type:
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* ```ts
|
|
392
|
+
* const methods = defineComposerMethods({
|
|
393
|
+
* command<TThis extends ComposerLike<TThis>>(
|
|
394
|
+
* this: TThis,
|
|
395
|
+
* name: string,
|
|
396
|
+
* handler: Middleware<MsgCtx & EventContextOf<TThis, "message">>,
|
|
397
|
+
* ): TThis {
|
|
398
|
+
* return this.on("message", (ctx, next) => {
|
|
399
|
+
* if (ctx.text === `/${name}`) return handler(ctx, next);
|
|
400
|
+
* return next();
|
|
401
|
+
* });
|
|
402
|
+
* },
|
|
403
|
+
* });
|
|
404
|
+
*
|
|
405
|
+
* new Composer()
|
|
406
|
+
* .derive("message", () => ({ t: (s: string) => s }))
|
|
407
|
+
* .command("start", (ctx) => ctx.t("Hi!")); // ✅ t is visible
|
|
408
|
+
* ```
|
|
409
|
+
*/
|
|
410
|
+
type EventContextOf<T, E extends string> = T extends {
|
|
411
|
+
"~": {
|
|
412
|
+
Out: infer O extends object;
|
|
413
|
+
Derives: infer D extends Record<string, object>;
|
|
414
|
+
};
|
|
415
|
+
} ? O & (E extends keyof D ? D[E] : {}) : never;
|
|
382
416
|
/**
|
|
383
417
|
* Helper to define custom composer methods with full TypeScript inference.
|
|
384
418
|
*
|
|
@@ -466,4 +500,4 @@ declare const skip: Middleware<any>;
|
|
|
466
500
|
declare const stop: Middleware<any>;
|
|
467
501
|
|
|
468
502
|
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 };
|
|
503
|
+
export type { CompatibleEvents, ComposedMiddleware, ComposerLike, ComposerOptions, ContextCallback, ContextOf, DeriveFromOptions, DeriveHandler, ErrorHandler, EventComposer, EventComposerConstructor, EventContextOf, HandlerOptions, LazyFactory, MacroDef, MacroDefinitions, MacroDeriveType, MacroHooks, MacroOptionType, MaybeArray, Middleware, MiddlewareInfo, MiddlewareType, Next, RouteBuilder, RouteHandler, Scope, TraceHandler, WithCtx };
|
package/dist/index.js
CHANGED
|
@@ -305,8 +305,17 @@ class Composer {
|
|
|
305
305
|
fn(group);
|
|
306
306
|
const chain = compose(group["~"].middlewares.map((m) => m.fn));
|
|
307
307
|
const mw = async (ctx, next) => {
|
|
308
|
-
const
|
|
309
|
-
|
|
308
|
+
const preKeys = new Set(Object.keys(ctx));
|
|
309
|
+
const snapshot = {};
|
|
310
|
+
for (const key of preKeys) snapshot[key] = ctx[key];
|
|
311
|
+
await chain(ctx, noopNext);
|
|
312
|
+
for (const key of Object.keys(ctx)) {
|
|
313
|
+
if (!preKeys.has(key)) {
|
|
314
|
+
const desc = Object.getOwnPropertyDescriptor(ctx, key);
|
|
315
|
+
if (desc?.configurable) delete ctx[key];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
Object.assign(ctx, snapshot);
|
|
310
319
|
return next();
|
|
311
320
|
};
|
|
312
321
|
nameMiddleware(mw, "group");
|
|
@@ -341,8 +350,17 @@ class Composer {
|
|
|
341
350
|
if (localMws.length > 0) {
|
|
342
351
|
const chain = compose(localMws.map((m) => m.fn));
|
|
343
352
|
const isolated = async (ctx, next) => {
|
|
344
|
-
const
|
|
345
|
-
|
|
353
|
+
const preKeys = new Set(Object.keys(ctx));
|
|
354
|
+
const snapshot = {};
|
|
355
|
+
for (const key of preKeys) snapshot[key] = ctx[key];
|
|
356
|
+
await chain(ctx, noopNext);
|
|
357
|
+
for (const key of Object.keys(ctx)) {
|
|
358
|
+
if (!preKeys.has(key)) {
|
|
359
|
+
const desc = Object.getOwnPropertyDescriptor(ctx, key);
|
|
360
|
+
if (desc?.configurable) delete ctx[key];
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
Object.assign(ctx, snapshot);
|
|
346
364
|
return next();
|
|
347
365
|
};
|
|
348
366
|
nameMiddleware(isolated, "extend", pluginName);
|