@stackra/pipeline 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stackra L.L.C
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,312 @@
1
+ # @stackra/pipeline
2
+
3
+ Laravel-style middleware pipeline with a fluent API, DI-container integration, and named preset registration via `PipelineHub`. Framework-agnostic, zero server dependency, works in every JavaScript runtime.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @stackra/pipeline @stackra/container @stackra/contracts reflect-metadata
9
+ ```
10
+
11
+ Peer requirements: `@stackra/container`, `@stackra/contracts`, `reflect-metadata`.
12
+
13
+ ## Quick start
14
+
15
+ ```typescript
16
+ import 'reflect-metadata';
17
+ import { ApplicationFactory, Module } from '@stackra/container';
18
+ import { PipelineModule, Pipeline } from '@stackra/pipeline';
19
+
20
+ @Module({
21
+ imports: [PipelineModule.forRoot()],
22
+ })
23
+ class AppModule {}
24
+
25
+ const app = await ApplicationFactory.create(AppModule);
26
+
27
+ const result = new Pipeline<number>(app)
28
+ .send(1)
29
+ .through([(n, next) => next(n + 1), (n, next) => next(n * 10)])
30
+ .thenReturn();
31
+
32
+ console.log(result); // 20
33
+ ```
34
+
35
+ ## Public API
36
+
37
+ ### `Pipeline<TPassable, TReturn>`
38
+
39
+ The fluent pipeline class. Composes a chain of pipes that transform a passable value.
40
+
41
+ ```typescript
42
+ new Pipeline(container)
43
+ .send(input) // set the passable
44
+ .through(pipes) // replace all pipes
45
+ .pipe(extra) // append additional pipes
46
+ .via('process') // override handler method name (default: 'handle')
47
+ .finally((v) => log(v)) // callback after execution
48
+ .then((v) => destination) // execute with destination
49
+ // — or —
50
+ .thenReturn(); // execute and return the passable as-is
51
+ ```
52
+
53
+ ### `PipelineHub`
54
+
55
+ Named pipeline preset registry — reusable pipeline configurations invoked by name.
56
+
57
+ ```typescript
58
+ import { PipelineHub } from '@stackra/pipeline';
59
+
60
+ @Injectable()
61
+ class OrderService {
62
+ constructor(private hub: PipelineHub) {}
63
+
64
+ onModuleInit() {
65
+ this.hub.defaults((pipeline, passable) =>
66
+ pipeline.send(passable).through([Sanitize]).thenReturn()
67
+ );
68
+
69
+ this.hub.pipeline('order-validation', (pipeline, order) =>
70
+ pipeline.send(order).through([ValidateStock, ValidatePayment, ValidateAddress]).thenReturn()
71
+ );
72
+ }
73
+
74
+ validate(order: Order) {
75
+ return this.hub.pipe(order, 'order-validation');
76
+ }
77
+ }
78
+ ```
79
+
80
+ Hub runtime API:
81
+
82
+ ```typescript
83
+ hub.defaults(cb); // set the default pipeline definition
84
+ hub.pipeline(name, cb); // register a named pipeline
85
+ hub.pipe(passable, name?); // execute a named or default pipeline
86
+ hub.has(name); // boolean — does this name exist?
87
+ ```
88
+
89
+ ### `PipelineModule`
90
+
91
+ DI module exposing `Pipeline`, `PipelineHub`, and `PIPELINE_FACTORY`. Import once at the root:
92
+
93
+ ```typescript
94
+ @Module({
95
+ imports: [PipelineModule.forRoot()],
96
+ })
97
+ class AppModule {}
98
+ ```
99
+
100
+ ### `PIPELINE_FACTORY`
101
+
102
+ DI token for a factory that produces fresh `Pipeline` instances backed by the container.
103
+
104
+ ```typescript
105
+ import { Inject, Injectable } from '@stackra/container';
106
+ import { PIPELINE_FACTORY, PipelineFactory } from '@stackra/pipeline';
107
+
108
+ @Injectable()
109
+ class RequestHandler {
110
+ constructor(@Inject(PIPELINE_FACTORY) private makePipeline: PipelineFactory) {}
111
+
112
+ handle(request: Request) {
113
+ return this.makePipeline<Request, Response>()
114
+ .send(request)
115
+ .through([Auth, Logging, Handler])
116
+ .thenReturn();
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### `PipelineError`
122
+
123
+ Thrown when a pipe cannot be resolved or executed. Carries a `code` for programmatic dispatch:
124
+
125
+ | Code | Meaning |
126
+ | ------------------------ | ------------------------------------------------------------ |
127
+ | `INVALID_PIPE_TYPE` | Pipe was not a function, string, object, or tuple |
128
+ | `INVALID_PIPE_ENTRY` | Tuple's first element was of an unsupported type |
129
+ | `NO_CONTAINER` | String pipe used but no container provided to Pipeline |
130
+ | `PIPE_RESOLUTION_FAILED` | Container threw while resolving a string pipe |
131
+ | `INVALID_RESOLVED_PIPE` | Container returned a non-object for a string pipe |
132
+ | `METHOD_NOT_FOUND` | Pipe object is missing the configured handler method |
133
+ | `PIPE_EXECUTION_FAILED` | A pipe threw during execution (`cause` carries the original) |
134
+ | `INVALID_PIPELINE_NAME` | Empty name passed to `hub.pipeline(name, cb)` |
135
+ | `PIPELINE_NOT_FOUND` | `hub.pipe(v, name)` called with an unregistered name |
136
+ | `NO_DEFAULT_PIPELINE` | `hub.pipe(v)` called with no name and no default registered |
137
+
138
+ ## Pipe forms
139
+
140
+ A pipe can be any of four shapes:
141
+
142
+ ### Closure
143
+
144
+ Simplest form — a function taking `(passable, next)`:
145
+
146
+ ```typescript
147
+ const AddOne = (n: number, next: (n: number) => number) => next(n + 1);
148
+
149
+ new Pipeline<number>().send(1).through([AddOne]).thenReturn(); // 2
150
+ ```
151
+
152
+ ### String (DI-resolved)
153
+
154
+ Resolved from the container by name/token. The resolved instance must have a handler method (default `handle`):
155
+
156
+ ```typescript
157
+ @Injectable()
158
+ class LoggingPipe {
159
+ handle(request: Request, next: (r: Request) => Response) {
160
+ console.log(request.url);
161
+ return next(request);
162
+ }
163
+ }
164
+
165
+ // register `LoggingPipe` under the string 'logging' via a module provider
166
+ // ...
167
+ new Pipeline<Request, Response>(container)
168
+ .send(request)
169
+ .through(['logging'])
170
+ .then((r) => processRequest(r));
171
+ ```
172
+
173
+ ### Object
174
+
175
+ Instance with a handler method — bypasses container resolution:
176
+
177
+ ```typescript
178
+ new Pipeline<Request, Response>()
179
+ .send(request)
180
+ .through([new LoggingPipe()])
181
+ .then((r) => processRequest(r));
182
+ ```
183
+
184
+ ### Tuple (parameterized)
185
+
186
+ `[pipe, ...params]` — extra params flow to the handler after `(passable, next)`:
187
+
188
+ ```typescript
189
+ @Injectable()
190
+ class RateLimitPipe {
191
+ handle(req: Request, next: (r: Request) => Response, limit: number, window: string) {
192
+ // ...
193
+ return next(req);
194
+ }
195
+ }
196
+
197
+ new Pipeline<Request, Response>(container)
198
+ .send(request)
199
+ .through([['rate-limit', 100, '1m']])
200
+ .then((r) => processRequest(r));
201
+ ```
202
+
203
+ ## Container integration
204
+
205
+ Every string pipe is resolved through the `@stackra/container` DI system. The `Pipeline` constructor takes an optional `IApplication`:
206
+
207
+ - **With container** — string pipes work, `Pipeline` is itself `@Injectable()` so it can be injected anywhere.
208
+ - **Without container** — only function and object pipes are supported (string pipes throw `NO_CONTAINER`).
209
+
210
+ When injecting `Pipeline` into your services, always inject a **factory** (`PIPELINE_FACTORY`) rather than a singleton — pipelines are single-use, and reusing the same instance across requests will leak state.
211
+
212
+ ## `via()` — custom method names
213
+
214
+ By default the pipeline calls `handle(passable, next)` on pipe objects. Use `.via(method)` to invoke a different method:
215
+
216
+ ```typescript
217
+ class ProcessorPipe {
218
+ process(data: Data, next: (d: Data) => Data) {
219
+ return next(transform(data));
220
+ }
221
+ }
222
+
223
+ new Pipeline<Data>().send(data).via('process').through([new ProcessorPipe()]).thenReturn();
224
+ ```
225
+
226
+ ## `finally()` — post-execution callback
227
+
228
+ The `finally` callback fires after `then()` / `thenReturn()` completes, receiving the (potentially mutated) passable:
229
+
230
+ ```typescript
231
+ new Pipeline<Request, Response>()
232
+ .send(request)
233
+ .through([Auth, Logging])
234
+ .finally((req) => telemetry.record(req))
235
+ .then((r) => handleRequest(r));
236
+ ```
237
+
238
+ ## `PipelineHub` presets
239
+
240
+ Use the Hub when the same pipeline definition is called from multiple sites. Register once, invoke by name — the passable is provided at call time.
241
+
242
+ ```typescript
243
+ @Injectable()
244
+ class BootstrapService {
245
+ constructor(private hub: PipelineHub) {}
246
+
247
+ onModuleInit() {
248
+ // Preset A — inbound HTTP
249
+ this.hub.pipeline('http-inbound', (pipeline, req) =>
250
+ pipeline.send(req).through([Auth, RateLimit, Logger]).thenReturn()
251
+ );
252
+
253
+ // Preset B — outbound API call
254
+ this.hub.pipeline('http-outbound', (pipeline, req) =>
255
+ pipeline.send(req).through([AddAuthHeader, Retry, Telemetry]).thenReturn()
256
+ );
257
+ }
258
+ }
259
+
260
+ // elsewhere
261
+ const finalReq = hub.pipe(request, 'http-inbound');
262
+ ```
263
+
264
+ ## Types
265
+
266
+ ```typescript
267
+ type PipeClosure<TPassable, TResult> = (
268
+ passable: TPassable,
269
+ next: (passable: TPassable) => TResult
270
+ ) => TResult;
271
+
272
+ type PipeTuple = [PipeEntry, ...unknown[]];
273
+
274
+ type PipeEntry = string | object | PipeClosure<unknown, unknown>;
275
+
276
+ type PipeType = PipeClosure<unknown, unknown> | string | object | PipeTuple;
277
+
278
+ type PipelineDefinition = <T>(pipeline: Pipeline, passable: T) => unknown;
279
+
280
+ type PipelineFactory = <TPassable = unknown, TReturn = TPassable>() => Pipeline<TPassable, TReturn>;
281
+ ```
282
+
283
+ ## Error handling
284
+
285
+ Every pipe execution is wrapped in a try/catch. If a pipe throws:
286
+
287
+ - **Non-`PipelineError`** — wrapped in a new `PipelineError` with code `PIPE_EXECUTION_FAILED` and the original attached as `cause`.
288
+ - **Already a `PipelineError`** — re-thrown unchanged.
289
+
290
+ This preserves the original stack trace via the `cause` chain while giving downstream code a stable error surface.
291
+
292
+ ```typescript
293
+ import { PipelineError } from '@stackra/pipeline';
294
+
295
+ try {
296
+ const result = pipeline.thenReturn();
297
+ } catch (err) {
298
+ if (err instanceof PipelineError) {
299
+ console.error(err.code, err.message, err.cause);
300
+ }
301
+ }
302
+ ```
303
+
304
+ ## Integration with `@stackra/ssr/middleware`
305
+
306
+ `@stackra/pipeline` is the runtime that powers HTTP and UI middleware in `@stackra/ssr`. Any `MiddlewareDefinition` can be adapted to a `PipeType` via `toPipe(mw, container)` and dropped straight into `.through([...])`.
307
+
308
+ See the [SSR middleware docs](../ssr/README.md#middleware) for the full integration story.
309
+
310
+ ## License
311
+
312
+ MIT © Stackra L.L.C