typed-pipeline 2.0.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) 2023 Ryan Sonshine
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,92 @@
1
+ # typed-pipeline
2
+
3
+ Type-safe, composable async pipelines for TypeScript.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install typed-pipeline
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { Pipeline } from 'typed-pipeline'
15
+
16
+ // Basic chaining
17
+ const result = await new Pipeline<number>()
18
+ .pipe((n) => n * 2)
19
+ .pipe((n) => `value: ${n}`)
20
+ .run(5)
21
+ // => "value: 10"
22
+
23
+ // Async steps
24
+ const result2 = await new Pipeline<string>()
25
+ .pipe(async (s) => s.trim())
26
+ .pipe(async (s) => s.toUpperCase())
27
+ .run(' hello ')
28
+ // => "HELLO"
29
+ ```
30
+
31
+ ## API
32
+
33
+ ### `.pipe(step)`
34
+ Add a transformation step. Input type flows from previous step's output.
35
+
36
+ ```ts
37
+ .pipe((input: TOutput) => TNext)
38
+ ```
39
+
40
+ ### `.parallel(...steps)`
41
+ Run multiple steps concurrently, returns a tuple of results.
42
+
43
+ ```ts
44
+ const pipeline = new Pipeline<number>()
45
+ .parallel(
46
+ (n) => n + 1,
47
+ (n) => n * 2,
48
+ async (n) => n ** 2,
49
+ )
50
+
51
+ await pipeline.run(3) // => [4, 6, 9]
52
+ ```
53
+
54
+ ### `.bypass(step)` / `.tap(step)`
55
+ Run a side-effect step without changing the value (logging, caching, etc.).
56
+
57
+ ```ts
58
+ new Pipeline<string>()
59
+ .tap((s) => console.log('input:', s))
60
+ .pipe((s) => s.toUpperCase())
61
+ .tap((s) => console.log('output:', s))
62
+ .run('hello')
63
+ ```
64
+
65
+ ### `.saveAs(key)`
66
+ Save the current value under a named key, accessible after `run()`.
67
+
68
+ ```ts
69
+ const pipeline = new Pipeline<number>()
70
+ .pipe((n) => n * 2).saveAs('doubled')
71
+ .pipe((n) => n + 1)
72
+
73
+ await pipeline.run(5)
74
+ pipeline.getResult('doubled') // => 10
75
+ ```
76
+
77
+ ### `.getResult(key)`
78
+ Retrieve a value previously saved with `.saveAs()`.
79
+
80
+ ### `.waited()`
81
+ Flatten a nested `Promise<Promise<T>>` to `Promise<T>`.
82
+
83
+ ### `.run(input)`
84
+ Execute the pipeline with the given input.
85
+
86
+ ```ts
87
+ const output = await pipeline.run(input)
88
+ ```
89
+
90
+ ## License
91
+
92
+ MIT
package/lib/fpipe.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * fpipe — functional pipe with $$-injection, TS 5.x full type-gymnastics edition
3
+ *
4
+ * Features used:
5
+ * - `infer X extends Constraint` (TS 4.8+)
6
+ * - Recursive conditional types (TS 4.1+)
7
+ * - Const type parameters (TS 5.0+)
8
+ * - NoInfer<T> (TS 5.4+)
9
+ * - Variadic tuple types (TS 4.0+)
10
+ * - Labeled tuple elements (TS 4.0+)
11
+ * - Template literal types (TS 4.1+) — used for error messages
12
+ */
13
+ type Awaited_<T> = T extends Promise<infer U> ? Awaited_<U> : T;
14
+ type MaybePromise<T> = T | Promise<T>;
15
+ /**
16
+ * Brand that marks "this parameter receives the previous step's output".
17
+ * The extra `__fpipe_prev__` brand key makes it nominally distinct from plain T.
18
+ */
19
+ export type Prev<T> = T & {
20
+ readonly __fpipe_prev__: unique symbol;
21
+ };
22
+ /** A plain unary step: (currentValue: TIn) => TOut */
23
+ type PlainStep<TIn, TOut> = (value: TIn) => MaybePromise<TOut>;
24
+ /**
25
+ * A $$-aware step: ($$: Prev<TIn>, ...defaults) => TOut
26
+ * Extra params MUST have defaults so the step is callable as unary at runtime.
27
+ */
28
+ type PrevStep<TIn, TOut> = ($$: Prev<TIn>, ...rest: DefaultsOnly) => MaybePromise<TOut>;
29
+ /** Ensures extra params all have question marks (i.e. are optional / have defaults). */
30
+ type DefaultsOnly = [] | [first?: unknown, ...rest: (unknown | undefined)[]];
31
+ type AnyStep<TIn = any, TOut = any> = PlainStep<TIn, TOut> | PrevStep<TIn, TOut>;
32
+ /**
33
+ * Extract the output type of a step, resolving Promise.
34
+ * Works for both PlainStep and PrevStep (first arg may be Prev<T>).
35
+ */
36
+ type StepOutput<S> = S extends (arg: any, ...rest: any[]) => MaybePromise<infer O> ? Awaited_<O> : never;
37
+ /**
38
+ * Extract the *input* type a step expects.
39
+ * For PrevStep, the first param is Prev<TIn> — unwrap the brand.
40
+ */
41
+ type StepInput<S> = S extends PlainStep<infer TIn, any> ? TIn : S extends ($$: Prev<infer TIn>, ...rest: any[]) => any ? TIn : never;
42
+ /**
43
+ * Thread a tuple of steps and build the output type at each position.
44
+ *
45
+ * ThreadPipeline<[S1, S2, S3], Seed> =
46
+ * [StepOutput<S1>, StepOutput<S2>, StepOutput<S3>]
47
+ * where each step's input must match the previous step's output.
48
+ *
49
+ * Uses TS5's `infer X extends Constraint` for the head/tail split.
50
+ */
51
+ type ThreadPipeline<Steps extends readonly AnyStep[], Seed> = Steps extends readonly [] ? Seed : Steps extends readonly [
52
+ infer Head extends AnyStep,
53
+ ...infer Tail extends readonly AnyStep[]
54
+ ] ? ThreadPipeline<Tail, StepOutput<Head>> : never;
55
+ /** Final output type of a full pipeline */
56
+ type PipelineOutput<Steps extends readonly AnyStep[], Seed> = ThreadPipeline<Steps, Seed>;
57
+ export declare function fpipe<const Steps extends readonly AnyStep[], Seed extends StepInput<Steps[0]>>(...steps: Steps): (input: NoInfer<Seed>) => Promise<PipelineOutput<Steps, Seed>>;
58
+ export type InferOutput<P extends (...args: any[]) => Promise<any>> = P extends (...args: any[]) => Promise<infer O> ? O : never;
59
+ export {};
package/lib/fpipe.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * fpipe — functional pipe with $$-injection, TS 5.x full type-gymnastics edition
4
+ *
5
+ * Features used:
6
+ * - `infer X extends Constraint` (TS 4.8+)
7
+ * - Recursive conditional types (TS 4.1+)
8
+ * - Const type parameters (TS 5.0+)
9
+ * - NoInfer<T> (TS 5.4+)
10
+ * - Variadic tuple types (TS 4.0+)
11
+ * - Labeled tuple elements (TS 4.0+)
12
+ * - Template literal types (TS 4.1+) — used for error messages
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.fpipe = fpipe;
16
+ // ─── Implementation ────────────────────────────────────────────────────────────
17
+ function fpipe(...steps) {
18
+ return async (input) => {
19
+ let current = input;
20
+ for (const step of steps) {
21
+ // $$-aware: fn.length >= 2 → first param is Prev<T>
22
+ // We call with (current) — extra params use their defaults
23
+ current = await step(current);
24
+ }
25
+ return current;
26
+ };
27
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare const myPackage: (taco?: string) => string;
2
+ export { Pipeline } from './pipeline';
3
+ export { fpipe, type Prev } from './fpipe';
package/lib/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fpipe = exports.Pipeline = exports.myPackage = void 0;
4
+ const myPackage = (taco = '') => `${taco} from my package`;
5
+ exports.myPackage = myPackage;
6
+ var pipeline_1 = require("./pipeline");
7
+ Object.defineProperty(exports, "Pipeline", { enumerable: true, get: function () { return pipeline_1.Pipeline; } });
8
+ var fpipe_1 = require("./fpipe");
9
+ Object.defineProperty(exports, "fpipe", { enumerable: true, get: function () { return fpipe_1.fpipe; } });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Pipeline<TInput, TOutput, TSaved>
3
+ *
4
+ * Features:
5
+ * - .pipe(fn) — type flows, fn param auto-inferred (no annotation needed)
6
+ * - .pipe($$ step) — first param is Prev<T> ($$-aware), extra params need defaults
7
+ * - .pipe((v, $) => ...) — second param is $ accessor: $['key'] gives saved value with type
8
+ * - .parallel() — concurrent steps, returns typed tuple
9
+ * - .bypass()/.tap() — side effects, value passes through
10
+ * - .saveAs(key) — snapshot current value by name, retrieve after run()
11
+ * - .inject(key, fn) — inject one saved value alongside current
12
+ * - .withSaved(fn) — inject all saved values alongside current
13
+ * - .waited() — flatten nested Promise
14
+ * - .run(input) — execute with external seed
15
+ */
16
+ import { type Prev } from './fpipe';
17
+ type MaybePromise<T> = T | Promise<T>;
18
+ /** Unary step: current value in, next value out */
19
+ type PlainStep<TIn, TOut> = (input: TIn) => MaybePromise<TOut>;
20
+ /**
21
+ * $$-aware step: first param is Prev<TIn>, extra params must have defaults.
22
+ * The "$$" name is just convention — any name works.
23
+ */
24
+ type PrevStep<TIn, TOut> = ($$: Prev<TIn>, ...rest: DefaultsOnly) => MaybePromise<TOut>;
25
+ type DefaultsOnly = any[];
26
+ type SavedResults = Record<string, unknown>;
27
+ /**
28
+ * Saved-aware step: exactly 2 required params — current value and $ (typed saved accessor).
29
+ * fn.length === 2 at runtime → distinct from PlainStep (1) and PrevStep (1 + defaults).
30
+ *
31
+ * @example
32
+ * .pipe((n, $) => n + $['doubled'])
33
+ * .pipe((s, $) => ({ s, original: $['original'], d: $['doubled'] }))
34
+ */
35
+ type SavedStep<TIn, TOut, TSaved extends SavedResults> = (current: TIn, $: TSaved) => MaybePromise<TOut>;
36
+ type AnyStep<TIn = any, TOut = any> = PlainStep<TIn, TOut> | PrevStep<TIn, TOut> | SavedStep<TIn, TOut, any>;
37
+ type ParallelResults<TSteps extends readonly PlainStep<any, any>[]> = {
38
+ [K in keyof TSteps]: Awaited<ReturnType<TSteps[K]>>;
39
+ };
40
+ declare class Multicast<T> {
41
+ private callbacks;
42
+ emit(value: T): void;
43
+ subscribe(cb: (value: T) => void): void;
44
+ }
45
+ declare class Job<TInput, TOutput> {
46
+ private readonly action;
47
+ private readonly savedResults;
48
+ readonly after: Multicast<TOutput>;
49
+ constructor(action: AnyStep<TInput, TOutput>, savedResults?: Map<string, unknown>);
50
+ run(input: TInput): Promise<TOutput>;
51
+ }
52
+ export declare class Pipeline<TInput, TOutput = TInput, TSaved extends SavedResults = Record<never, never>> {
53
+ private readonly jobs;
54
+ private readonly results;
55
+ private readonly extraResults;
56
+ constructor(jobs?: Array<Job<any, any>>, results?: Map<string, unknown>, extraResults?: Map<string, unknown>[]);
57
+ /**
58
+ * Add a plain step. Parameter type is automatically inferred — no annotation needed.
59
+ *
60
+ * @example
61
+ * new Pipeline<number>()
62
+ * .pipe(n => n * 2) // n inferred as number ✓
63
+ * .pipe(n => `val: ${n}`) // n inferred as number ✓
64
+ *
65
+ * Or with $$-aware (first param is previous output, extra params need defaults):
66
+ * .pipe(($$ , bonus = 3) => $$ + bonus) // $$ inferred as number ✓
67
+ */
68
+ pipe<TNext>(step: SavedStep<TOutput, TNext, TSaved>): Pipeline<TInput, Awaited<TNext>, TSaved>;
69
+ pipe<TNext>(step: PrevStep<TOutput, TNext>): Pipeline<TInput, Awaited<TNext>, TSaved>;
70
+ pipe<TNext>(step: PlainStep<TOutput, TNext>): Pipeline<TInput, Awaited<TNext>, TSaved>;
71
+ /**
72
+ * Run multiple steps concurrently. Returns a typed tuple.
73
+ *
74
+ * @example
75
+ * .parallel(
76
+ * n => n + 1,
77
+ * n => n * 2,
78
+ * async n => n ** 2,
79
+ * )
80
+ * // → [number, number, number]
81
+ */
82
+ parallel<TSteps extends readonly PlainStep<TOutput, any>[]>(...steps: TSteps): Pipeline<TInput, ParallelResults<TSteps>, TSaved>;
83
+ /**
84
+ * Run a side-effect without changing the value (logging, caching, etc.).
85
+ *
86
+ * @example
87
+ * .tap(n => console.log('current:', n))
88
+ */
89
+ bypass(step: (value: TOutput) => MaybePromise<unknown>): Pipeline<TInput, TOutput, TSaved>;
90
+ tap: (step: (value: TOutput) => MaybePromise<unknown>) => Pipeline<TInput, TOutput, TSaved>;
91
+ /**
92
+ * Snapshot the current value under a key. Retrieve after run() with .getResult(key).
93
+ *
94
+ * @example
95
+ * .pipe(n => n * 2).saveAs('doubled')
96
+ * // pipeline.getResult('doubled') === 10 after run(5)
97
+ */
98
+ saveAs<TKey extends string>(key: TKey): Pipeline<TInput, TOutput, TSaved & Record<TKey, TOutput>>;
99
+ getResult<TKey extends keyof TSaved>(key: TKey): TSaved[TKey] | undefined;
100
+ /** Flatten a nested Promise<Promise<T>> to Promise<T>. */
101
+ waited(): Pipeline<TInput, Awaited<TOutput>, TSaved>;
102
+ /**
103
+ * Inject a previously saved value alongside the current value.
104
+ * Both current and saved value are fully typed.
105
+ *
106
+ * @example
107
+ * new Pipeline<number>()
108
+ * .pipe(n => n * 2).saveAs('doubled')
109
+ * .pipe(n => n + 100)
110
+ * .inject('doubled', (current, doubled) => current - doubled)
111
+ * // current: number, doubled: number (typed from saveAs)
112
+ */
113
+ inject<TKey extends keyof TSaved, TNext>(key: TKey, fn: (current: TOutput, saved: TSaved[TKey]) => MaybePromise<TNext>): Pipeline<TInput, Awaited<TNext>, TSaved>;
114
+ /**
115
+ * Access all saved values alongside the current value.
116
+ * The `saved` object is typed with all previously saved keys.
117
+ *
118
+ * @example
119
+ * new Pipeline<number>()
120
+ * .pipe(n => n * 2).saveAs('doubled')
121
+ * .pipe(n => n + 1).saveAs('incremented')
122
+ * .withSaved((n, saved) => n + saved.doubled + saved.incremented)
123
+ * // saved.doubled: number, saved.incremented: number — fully typed
124
+ */
125
+ withSaved<TNext>(fn: (current: TOutput, saved: TSaved) => MaybePromise<TNext>): Pipeline<TInput, Awaited<TNext>, TSaved>;
126
+ /**
127
+ * Compose two pipelines. The output type of `this` must be assignable to
128
+ * the input type of `other`.
129
+ *
130
+ * @example
131
+ * const parse = new Pipeline<string>().pipe(s => parseInt(s))
132
+ * const double = new Pipeline<number>().pipe(n => n * 2)
133
+ * const combined = parse.concat(double)
134
+ * await combined.run('5') // 10
135
+ */
136
+ concat<TNext, TOtherSaved extends SavedResults>(other: Pipeline<TOutput, TNext, TOtherSaved>): Pipeline<TInput, TNext, TSaved & TOtherSaved>;
137
+ /**
138
+ * Execute the pipeline with the given input.
139
+ *
140
+ * @example
141
+ * await pipeline.run(5)
142
+ */
143
+ run(input: TInput): Promise<TOutput>;
144
+ }
145
+ export {};
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ /**
3
+ * Pipeline<TInput, TOutput, TSaved>
4
+ *
5
+ * Features:
6
+ * - .pipe(fn) — type flows, fn param auto-inferred (no annotation needed)
7
+ * - .pipe($$ step) — first param is Prev<T> ($$-aware), extra params need defaults
8
+ * - .pipe((v, $) => ...) — second param is $ accessor: $['key'] gives saved value with type
9
+ * - .parallel() — concurrent steps, returns typed tuple
10
+ * - .bypass()/.tap() — side effects, value passes through
11
+ * - .saveAs(key) — snapshot current value by name, retrieve after run()
12
+ * - .inject(key, fn) — inject one saved value alongside current
13
+ * - .withSaved(fn) — inject all saved values alongside current
14
+ * - .waited() — flatten nested Promise
15
+ * - .run(input) — execute with external seed
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.Pipeline = void 0;
19
+ // ─── Multicast (for saveAs subscriptions) ────────────────────────────────────
20
+ class Multicast {
21
+ callbacks = [];
22
+ emit(value) {
23
+ for (const cb of this.callbacks)
24
+ cb(value);
25
+ }
26
+ subscribe(cb) {
27
+ this.callbacks.push(cb);
28
+ }
29
+ }
30
+ // ─── Job ──────────────────────────────────────────────────────────────────────
31
+ class Job {
32
+ action;
33
+ savedResults;
34
+ after = new Multicast();
35
+ constructor(action, savedResults = new Map()) {
36
+ this.action = action;
37
+ this.savedResults = savedResults;
38
+ }
39
+ async run(input) {
40
+ let result;
41
+ const len = this.action.length;
42
+ if (len === 2) {
43
+ // SavedStep: (current, $) — exactly 2 required params
44
+ const $ = Object.fromEntries(this.savedResults);
45
+ result = await this.action(input, $);
46
+ }
47
+ else if (len >= 2) {
48
+ // PrevStep with extra defaults beyond 2 (unusual edge case)
49
+ result = await this.action(input);
50
+ }
51
+ else if (len === 1) {
52
+ // PlainStep OR PrevStep(fn.length===1 because extras have defaults)
53
+ // Both called with single arg — correct for both
54
+ result = await this.action(input);
55
+ }
56
+ else {
57
+ // len === 0: no-arg step (e.g. () => value)
58
+ result = await this.action();
59
+ }
60
+ this.after.emit(result);
61
+ return result;
62
+ }
63
+ }
64
+ // ─── Pipeline ─────────────────────────────────────────────────────────────────
65
+ class Pipeline {
66
+ jobs;
67
+ results;
68
+ extraResults;
69
+ constructor(jobs = [], results = new Map(), extraResults = []) {
70
+ this.jobs = jobs;
71
+ this.results = results;
72
+ this.extraResults = extraResults;
73
+ }
74
+ // Implementation
75
+ pipe(step) {
76
+ return new Pipeline([...this.jobs, new Job(step, this.results)], this.results, this.extraResults);
77
+ }
78
+ // ── .parallel() ──────────────────────────────────────────────────────────
79
+ /**
80
+ * Run multiple steps concurrently. Returns a typed tuple.
81
+ *
82
+ * @example
83
+ * .parallel(
84
+ * n => n + 1,
85
+ * n => n * 2,
86
+ * async n => n ** 2,
87
+ * )
88
+ * // → [number, number, number]
89
+ */
90
+ parallel(...steps) {
91
+ return this.pipe(async (value) => {
92
+ const results = await Promise.all(steps.map(s => s(value)));
93
+ return results;
94
+ });
95
+ }
96
+ // ── .bypass() / .tap() ───────────────────────────────────────────────────
97
+ /**
98
+ * Run a side-effect without changing the value (logging, caching, etc.).
99
+ *
100
+ * @example
101
+ * .tap(n => console.log('current:', n))
102
+ */
103
+ bypass(step) {
104
+ return this.pipe(async (value) => {
105
+ await step(value);
106
+ return value;
107
+ });
108
+ }
109
+ tap = this.bypass.bind(this);
110
+ // ── .saveAs() ────────────────────────────────────────────────────────────
111
+ /**
112
+ * Snapshot the current value under a key. Retrieve after run() with .getResult(key).
113
+ *
114
+ * @example
115
+ * .pipe(n => n * 2).saveAs('doubled')
116
+ * // pipeline.getResult('doubled') === 10 after run(5)
117
+ */
118
+ saveAs(key) {
119
+ const latestJob = this.jobs[this.jobs.length - 1];
120
+ if (latestJob == null)
121
+ throw new Error('saveAs() requires at least one step before it');
122
+ latestJob.after.subscribe(value => { this.results.set(key, value); });
123
+ return this;
124
+ }
125
+ getResult(key) {
126
+ const k = key;
127
+ if (this.results.has(k))
128
+ return this.results.get(k);
129
+ for (const m of this.extraResults) {
130
+ if (m.has(k))
131
+ return m.get(k);
132
+ }
133
+ return undefined;
134
+ }
135
+ // ── .waited() ────────────────────────────────────────────────────────────
136
+ /** Flatten a nested Promise<Promise<T>> to Promise<T>. */
137
+ waited() {
138
+ return this.pipe(async (value) => await value);
139
+ }
140
+ // ── .inject() ────────────────────────────────────────────────────────────
141
+ /**
142
+ * Inject a previously saved value alongside the current value.
143
+ * Both current and saved value are fully typed.
144
+ *
145
+ * @example
146
+ * new Pipeline<number>()
147
+ * .pipe(n => n * 2).saveAs('doubled')
148
+ * .pipe(n => n + 100)
149
+ * .inject('doubled', (current, doubled) => current - doubled)
150
+ * // current: number, doubled: number (typed from saveAs)
151
+ */
152
+ inject(key, fn) {
153
+ return this.pipe(async (current) => {
154
+ const saved = this.results.get(key);
155
+ return fn(current, saved);
156
+ });
157
+ }
158
+ // ── .withSaved() ─────────────────────────────────────────────────────────
159
+ /**
160
+ * Access all saved values alongside the current value.
161
+ * The `saved` object is typed with all previously saved keys.
162
+ *
163
+ * @example
164
+ * new Pipeline<number>()
165
+ * .pipe(n => n * 2).saveAs('doubled')
166
+ * .pipe(n => n + 1).saveAs('incremented')
167
+ * .withSaved((n, saved) => n + saved.doubled + saved.incremented)
168
+ * // saved.doubled: number, saved.incremented: number — fully typed
169
+ */
170
+ withSaved(fn) {
171
+ return this.pipe(async (current) => {
172
+ const saved = Object.fromEntries(this.results);
173
+ return fn(current, saved);
174
+ });
175
+ }
176
+ // ── .concat() ────────────────────────────────────────────────────────────
177
+ /**
178
+ * Compose two pipelines. The output type of `this` must be assignable to
179
+ * the input type of `other`.
180
+ *
181
+ * @example
182
+ * const parse = new Pipeline<string>().pipe(s => parseInt(s))
183
+ * const double = new Pipeline<number>().pipe(n => n * 2)
184
+ * const combined = parse.concat(double)
185
+ * await combined.run('5') // 10
186
+ */
187
+ concat(other) {
188
+ // Each pipeline keeps its own results map (saveAs subscriptions are already wired).
189
+ // The concat'd pipeline holds references to both maps and checks them in getResult.
190
+ const o = other;
191
+ return new Pipeline([...this.jobs, ...o.jobs], this.results, [...this.extraResults, o.results, ...o.extraResults]);
192
+ }
193
+ // ── .run() ───────────────────────────────────────────────────────────────
194
+ /**
195
+ * Execute the pipeline with the given input.
196
+ *
197
+ * @example
198
+ * await pipeline.run(5)
199
+ */
200
+ async run(input) {
201
+ let current = input;
202
+ for (const job of this.jobs) {
203
+ current = await job.run(current);
204
+ }
205
+ return current;
206
+ }
207
+ }
208
+ exports.Pipeline = Pipeline;
package/package.json ADDED
@@ -0,0 +1,119 @@
1
+ {
2
+ "name": "typed-pipeline",
3
+ "version": "2.0.0",
4
+ "description": "Type-safe composable async pipelines. Parameters auto-inferred, $$-aware steps, parallel/saveAs/concat.",
5
+ "main": "./lib/index.js",
6
+ "types": "./lib/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./lib/index.js",
10
+ "import": "./lib/index.js",
11
+ "types": "./lib/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "lib/**/*"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc --project tsconfig.build.json",
19
+ "clean": "rm -rf ./lib/",
20
+ "cm": "cz",
21
+ "lint": "eslint ./src/ --fix",
22
+ "semantic-release": "semantic-release",
23
+ "test:watch": "jest --watch",
24
+ "test": "jest --coverage",
25
+ "typecheck": "tsc --noEmit",
26
+ "prepublishOnly": "npm run clean && npm run build && npm test"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/bkmashiro/typed-pipeline.git"
31
+ },
32
+ "license": "MIT",
33
+ "author": {
34
+ "name": "baka_mashiro",
35
+ "email": "bkmashiro@users.noreply.github.com",
36
+ "url": "https://github.com/bkmashiro"
37
+ },
38
+ "engines": {
39
+ "node": ">=16.0"
40
+ },
41
+ "keywords": [
42
+ "pipeline",
43
+ "pipe",
44
+ "compose",
45
+ "async",
46
+ "typescript",
47
+ "type-safe",
48
+ "functional",
49
+ "flow",
50
+ "chain"
51
+ ],
52
+ "bugs": {
53
+ "url": "https://github.com/bkmashiro/typed-pipeline/issues"
54
+ },
55
+ "homepage": "https://github.com/bkmashiro/typed-pipeline#readme",
56
+ "devDependencies": {
57
+ "@ryansonshine/commitizen": "^4.2.8",
58
+ "@ryansonshine/cz-conventional-changelog": "^3.3.4",
59
+ "@types/jest": "^27.5.2",
60
+ "@types/node": "^12.20.11",
61
+ "@typescript-eslint/eslint-plugin": "^4.22.0",
62
+ "@typescript-eslint/parser": "^4.22.0",
63
+ "conventional-changelog-conventionalcommits": "^5.0.0",
64
+ "eslint": "^7.25.0",
65
+ "eslint-config-prettier": "^8.3.0",
66
+ "eslint-plugin-node": "^11.1.0",
67
+ "eslint-plugin-prettier": "^3.4.0",
68
+ "jest": "^27.2.0",
69
+ "lint-staged": "^13.2.1",
70
+ "prettier": "^2.2.1",
71
+ "semantic-release": "^21.0.1",
72
+ "ts-jest": "^29.4.6",
73
+ "ts-node": "^10.2.1",
74
+ "typescript": "^5.9.3"
75
+ },
76
+ "config": {
77
+ "commitizen": {
78
+ "path": "./node_modules/@ryansonshine/cz-conventional-changelog"
79
+ }
80
+ },
81
+ "lint-staged": {
82
+ "*.ts": "eslint --cache --cache-location .eslintcache --fix"
83
+ },
84
+ "release": {
85
+ "branches": [
86
+ "main"
87
+ ],
88
+ "plugins": [
89
+ [
90
+ "@semantic-release/commit-analyzer",
91
+ {
92
+ "preset": "conventionalcommits",
93
+ "releaseRules": [
94
+ {
95
+ "type": "build",
96
+ "scope": "deps",
97
+ "release": "patch"
98
+ }
99
+ ]
100
+ }
101
+ ],
102
+ [
103
+ "@semantic-release/release-notes-generator",
104
+ {
105
+ "preset": "conventionalcommits",
106
+ "presetConfig": {
107
+ "types": [
108
+ { "type": "feat", "section": "Features" },
109
+ { "type": "fix", "section": "Bug Fixes" },
110
+ { "type": "build", "section": "Dependencies and Other Build Updates", "hidden": false }
111
+ ]
112
+ }
113
+ }
114
+ ],
115
+ "@semantic-release/npm",
116
+ "@semantic-release/github"
117
+ ]
118
+ }
119
+ }