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 +21 -0
- package/README.md +92 -0
- package/lib/fpipe.d.ts +59 -0
- package/lib/fpipe.js +27 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +9 -0
- package/lib/pipeline.d.ts +145 -0
- package/lib/pipeline.js +208 -0
- package/package.json +119 -0
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
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 {};
|
package/lib/pipeline.js
ADDED
|
@@ -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
|
+
}
|