@gramio/composer 0.1.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/LICENSE +21 -0
- package/README.md +335 -0
- package/dist/index.cjs +520 -0
- package/dist/index.d.cts +214 -0
- package/dist/index.d.ts +214 -0
- package/dist/index.js +511 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Kravets
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# @gramio/composer
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.org/package/@gramio/composer)
|
|
4
|
+
[](https://www.npmjs.org/package/@gramio/composer)
|
|
5
|
+
[](https://jsr.io/@gramio/composer)
|
|
6
|
+
[](https://jsr.io/@gramio/composer)
|
|
7
|
+
|
|
8
|
+
General-purpose, type-safe middleware composition library for TypeScript.
|
|
9
|
+
Zero dependencies. Cross-runtime (Bun / Node.js / Deno).
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Koa-style onion middleware composition
|
|
14
|
+
- Type-safe context accumulation via `derive()`
|
|
15
|
+
- Scope isolation (local / scoped / global) like Elysia
|
|
16
|
+
- Plugin deduplication by name + seed
|
|
17
|
+
- Abstract event system via factory pattern (`.on()`)
|
|
18
|
+
- Concurrent event queue with graceful shutdown
|
|
19
|
+
- Branching, routing, forking, lazy middleware
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# npm
|
|
25
|
+
npm install @gramio/composer
|
|
26
|
+
|
|
27
|
+
# bun
|
|
28
|
+
bun add @gramio/composer
|
|
29
|
+
|
|
30
|
+
# deno
|
|
31
|
+
deno add jsr:@gramio/composer
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { Composer } from "@gramio/composer";
|
|
38
|
+
|
|
39
|
+
const app = new Composer<{ request: Request }>()
|
|
40
|
+
.use(async (ctx, next) => {
|
|
41
|
+
console.log("before");
|
|
42
|
+
await next();
|
|
43
|
+
console.log("after");
|
|
44
|
+
})
|
|
45
|
+
.derive((ctx) => ({
|
|
46
|
+
url: new URL(ctx.request.url),
|
|
47
|
+
}))
|
|
48
|
+
.use((ctx, next) => {
|
|
49
|
+
console.log(ctx.url.pathname); // typed!
|
|
50
|
+
return next();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await app.run({ request: new Request("https://example.com/hello") });
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
### `compose(middlewares)`
|
|
59
|
+
|
|
60
|
+
Standalone Koa-style onion composition. Takes an array of middleware, returns a single composed middleware.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { compose } from "@gramio/composer";
|
|
64
|
+
|
|
65
|
+
const handler = compose([
|
|
66
|
+
async (ctx, next) => { console.log(1); await next(); console.log(4); },
|
|
67
|
+
async (ctx, next) => { console.log(2); await next(); console.log(3); },
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
await handler({});
|
|
71
|
+
// 1 → 2 → 3 → 4
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `Composer`
|
|
75
|
+
|
|
76
|
+
The core class. Registers middleware with type-safe context accumulation.
|
|
77
|
+
|
|
78
|
+
#### `use(...middleware)`
|
|
79
|
+
|
|
80
|
+
Register raw middleware functions.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
app.use((ctx, next) => {
|
|
84
|
+
// do something
|
|
85
|
+
return next();
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### `derive(handler, options?)`
|
|
90
|
+
|
|
91
|
+
Compute and assign additional context properties. Subsequent middleware sees the new types.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
app
|
|
95
|
+
.derive((ctx) => ({ user: getUser(ctx) }))
|
|
96
|
+
.use((ctx, next) => {
|
|
97
|
+
ctx.user; // typed!
|
|
98
|
+
return next();
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
With scope propagation:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const plugin = new Composer({ name: "auth" })
|
|
106
|
+
.derive((ctx) => ({ user: getUser(ctx) }), { as: "scoped" });
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### `guard(predicate, ...middleware)`
|
|
110
|
+
|
|
111
|
+
Two modes depending on whether handlers are provided:
|
|
112
|
+
|
|
113
|
+
**With handlers** — run middleware as side-effects when true, always continue the chain:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
app.guard(
|
|
117
|
+
(ctx): ctx is WithText => "text" in ctx,
|
|
118
|
+
(ctx, next) => { /* ctx.text is typed */ return next(); }
|
|
119
|
+
);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Without handlers** — gate the chain: if false, stop this composer's remaining middleware:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// Only admin can reach subsequent middleware
|
|
126
|
+
app
|
|
127
|
+
.guard((ctx) => ctx.role === "admin")
|
|
128
|
+
.use(adminOnlyHandler); // skipped if not admin
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
When used inside an `extend()`-ed plugin, the guard stops the plugin's chain but the parent continues:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const adminPlugin = new Composer()
|
|
135
|
+
.guard((ctx) => ctx.isAdmin)
|
|
136
|
+
.use(adminDashboard); // skipped if not admin
|
|
137
|
+
|
|
138
|
+
app
|
|
139
|
+
.extend(adminPlugin) // guard inside, isolated
|
|
140
|
+
.use(alwaysRuns); // parent continues regardless
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### `branch(predicate, onTrue, onFalse?)`
|
|
144
|
+
|
|
145
|
+
If/else branching. Static boolean optimization at registration time.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
app.branch(
|
|
149
|
+
(ctx) => ctx.isAdmin,
|
|
150
|
+
adminHandler,
|
|
151
|
+
userHandler
|
|
152
|
+
);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### `route(router, cases, fallback?)`
|
|
156
|
+
|
|
157
|
+
Multi-way dispatch (like a switch).
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
app.route(
|
|
161
|
+
(ctx) => ctx.type,
|
|
162
|
+
{
|
|
163
|
+
message: handleMessage,
|
|
164
|
+
callback: handleCallback,
|
|
165
|
+
},
|
|
166
|
+
handleFallback
|
|
167
|
+
);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### `fork(...middleware)`
|
|
171
|
+
|
|
172
|
+
Fire-and-forget parallel execution. Doesn't block the main chain.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
app.fork(analyticsMiddleware);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### `tap(...middleware)`
|
|
179
|
+
|
|
180
|
+
Run middleware but always continue the chain (cannot stop it).
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
app.tap(loggingMiddleware);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### `lazy(factory)`
|
|
187
|
+
|
|
188
|
+
Dynamic middleware selection. Factory is called on every invocation (not cached).
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
app.lazy((ctx) => ctx.premium ? premiumHandler : freeHandler);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### `onError(handler)`
|
|
195
|
+
|
|
196
|
+
Error boundary for all subsequent middleware.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
app.onError((ctx, error) => {
|
|
200
|
+
console.error(error);
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### `group(fn)`
|
|
205
|
+
|
|
206
|
+
Isolated sub-chain. Derives inside the group don't leak to the parent.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
app.group((g) => {
|
|
210
|
+
g.derive(() => ({ internal: true }))
|
|
211
|
+
.use((ctx, next) => {
|
|
212
|
+
ctx.internal; // available here
|
|
213
|
+
return next();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
// ctx.internal is NOT available here
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### `extend(other)`
|
|
220
|
+
|
|
221
|
+
Merge another composer. Scope-aware and dedup-aware.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const auth = new Composer({ name: "auth" })
|
|
225
|
+
.derive(() => ({ user: "alice" }))
|
|
226
|
+
.as("scoped");
|
|
227
|
+
|
|
228
|
+
app.extend(auth);
|
|
229
|
+
// app now sees ctx.user
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### `as(scope)`
|
|
233
|
+
|
|
234
|
+
Promote all middleware to `"scoped"` (one level) or `"global"` (all levels).
|
|
235
|
+
|
|
236
|
+
#### `compose()` / `run(context)`
|
|
237
|
+
|
|
238
|
+
Compile to a single middleware or run directly.
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const handler = app.compose();
|
|
242
|
+
await handler(ctx);
|
|
243
|
+
|
|
244
|
+
// or
|
|
245
|
+
await app.run(ctx);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Scope System
|
|
249
|
+
|
|
250
|
+
When `parent.extend(child)`:
|
|
251
|
+
|
|
252
|
+
| Child scope | Effect in parent |
|
|
253
|
+
|---|---|
|
|
254
|
+
| `local` (default) | Isolated via `Object.create()` — derives don't leak |
|
|
255
|
+
| `scoped` | Merged into parent, stops there (one level) |
|
|
256
|
+
| `global` | Merged into parent and propagates to all ancestors |
|
|
257
|
+
|
|
258
|
+
### Plugin Deduplication
|
|
259
|
+
|
|
260
|
+
Composers with a `name` are deduplicated. Same name + seed = skipped on second extend.
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
const auth = new Composer({ name: "auth" });
|
|
264
|
+
app.extend(auth); // applied
|
|
265
|
+
app.extend(auth); // skipped
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Different seed = different plugin:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
const limit100 = new Composer({ name: "rate-limit", seed: { max: 100 } });
|
|
272
|
+
const limit200 = new Composer({ name: "rate-limit", seed: { max: 200 } });
|
|
273
|
+
app.extend(limit100); // applied
|
|
274
|
+
app.extend(limit200); // applied (different seed)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### `createComposer(config)` — Event System
|
|
278
|
+
|
|
279
|
+
Factory that creates a Composer class with `.on()` event discrimination.
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import { createComposer } from "@gramio/composer";
|
|
283
|
+
|
|
284
|
+
interface BaseCtx { updateType: string }
|
|
285
|
+
interface MessageCtx extends BaseCtx { text?: string }
|
|
286
|
+
interface CallbackCtx extends BaseCtx { data?: string }
|
|
287
|
+
|
|
288
|
+
const { Composer, EventQueue } = createComposer<BaseCtx, {
|
|
289
|
+
message: MessageCtx;
|
|
290
|
+
callback_query: CallbackCtx;
|
|
291
|
+
}>({
|
|
292
|
+
discriminator: (ctx) => ctx.updateType,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const app = new Composer()
|
|
296
|
+
.derive(() => ({ timestamp: Date.now() }))
|
|
297
|
+
.on("message", (ctx, next) => {
|
|
298
|
+
ctx.text; // string | undefined
|
|
299
|
+
ctx.timestamp; // number
|
|
300
|
+
return next();
|
|
301
|
+
})
|
|
302
|
+
.on("callback_query", (ctx, next) => {
|
|
303
|
+
ctx.data; // string | undefined
|
|
304
|
+
return next();
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### `EventQueue`
|
|
309
|
+
|
|
310
|
+
Concurrent event queue with graceful shutdown.
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
import { EventQueue } from "@gramio/composer";
|
|
314
|
+
|
|
315
|
+
const queue = new EventQueue<RawEvent>(async (event) => {
|
|
316
|
+
const ctx = createContext(event);
|
|
317
|
+
return app.run(ctx);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
queue.add(event);
|
|
321
|
+
queue.addBatch(events);
|
|
322
|
+
|
|
323
|
+
// Graceful shutdown (waits up to 5s for pending handlers)
|
|
324
|
+
await queue.stop(5000);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Utilities
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import { noopNext, skip, stop } from "@gramio/composer";
|
|
331
|
+
|
|
332
|
+
noopNext; // () => Promise.resolve()
|
|
333
|
+
skip; // middleware that calls next()
|
|
334
|
+
stop; // middleware that does NOT call next()
|
|
335
|
+
```
|