@gramio/composer 0.3.0 → 0.3.1

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
@@ -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,16 +490,31 @@ new Composer()
464
490
 
465
491
  #### `ContextOf<T>` — extract the current context type
466
492
 
467
- Extracts `TOut` from a Composer or EventComposer instance type. Used as `ContextOf<TThis>` in custom method signatures to automatically capture all accumulated derives at the call site.
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
- // From a plain Composer:
473
- type Ctx = ContextOf<Composer<{ a: number }, { a: number; b: string }>>;
474
- // Ctx = { a: number; b: string }
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
- // In a custom method — TThis is inferred from the caller instance:
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>>,
package/dist/index.cjs CHANGED
@@ -308,8 +308,14 @@ 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 scopedCtx = Object.create(ctx);
312
- await chain(scopedCtx, noopNext);
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)) delete ctx[key];
317
+ }
318
+ Object.assign(ctx, snapshot);
313
319
  return next();
314
320
  };
315
321
  nameMiddleware(mw, "group");
@@ -344,8 +350,14 @@ class Composer {
344
350
  if (localMws.length > 0) {
345
351
  const chain = compose(localMws.map((m) => m.fn));
346
352
  const isolated = async (ctx, next) => {
347
- const scopedCtx = Object.create(ctx);
348
- await chain(scopedCtx, noopNext);
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)) delete ctx[key];
359
+ }
360
+ Object.assign(ctx, snapshot);
349
361
  return next();
350
362
  };
351
363
  nameMiddleware(isolated, "extend", pluginName);
package/dist/index.js CHANGED
@@ -305,8 +305,14 @@ 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 scopedCtx = Object.create(ctx);
309
- await chain(scopedCtx, noopNext);
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)) delete ctx[key];
314
+ }
315
+ Object.assign(ctx, snapshot);
310
316
  return next();
311
317
  };
312
318
  nameMiddleware(mw, "group");
@@ -341,8 +347,14 @@ class Composer {
341
347
  if (localMws.length > 0) {
342
348
  const chain = compose(localMws.map((m) => m.fn));
343
349
  const isolated = async (ctx, next) => {
344
- const scopedCtx = Object.create(ctx);
345
- await chain(scopedCtx, noopNext);
350
+ const preKeys = new Set(Object.keys(ctx));
351
+ const snapshot = {};
352
+ for (const key of preKeys) snapshot[key] = ctx[key];
353
+ await chain(ctx, noopNext);
354
+ for (const key of Object.keys(ctx)) {
355
+ if (!preKeys.has(key)) delete ctx[key];
356
+ }
357
+ Object.assign(ctx, snapshot);
346
358
  return next();
347
359
  };
348
360
  nameMiddleware(isolated, "extend", pluginName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gramio/composer",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "General-purpose, type-safe middleware composition library for TypeScript",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",