react-pipeline-runner 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,7 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 tonylus
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # react-pipeline-runner
2
+
3
+ A lightweight React hook for running async actions sequentially with abort support, error handling, and resume capability.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install react-pipeline-runner
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - Sequential execution of sync/async actions
14
+ - AbortController support for cancellation
15
+ - Resume from failed step
16
+ - TypeScript-first with intelligent ID inference
17
+ - Discriminated union for type-safe state handling
18
+ - Automatic cleanup on unmount
19
+ - Zero dependencies (except React 18+)
20
+
21
+ ## Basic Usage
22
+
23
+ ```tsx
24
+ import { usePipeline } from 'react-pipeline-runner'
25
+
26
+ function MyComponent() {
27
+ const pipeline = usePipeline([
28
+ async (signal) => {
29
+ await fetch('/api/step1', { signal })
30
+ },
31
+ async (signal) => {
32
+ await fetch('/api/step2', { signal })
33
+ },
34
+ async () => {
35
+ console.log('Step 3 - no signal needed')
36
+ },
37
+ ])
38
+
39
+ return (
40
+ <div>
41
+ <p>State: {pipeline.state}</p>
42
+
43
+ {pipeline.state === 'idle' && (
44
+ <button onClick={pipeline.start}>Start</button>
45
+ )}
46
+
47
+ {pipeline.state === 'running' && (
48
+ <>
49
+ <p>Running step {pipeline.current.index + 1}...</p>
50
+ <button onClick={pipeline.stop}>Cancel</button>
51
+ </>
52
+ )}
53
+
54
+ {pipeline.state === 'failed' && (
55
+ <>
56
+ <p>Error at step {pipeline.current.index + 1}: {String(pipeline.current.error)}</p>
57
+ <button onClick={pipeline.resume}>Retry</button>
58
+ <button onClick={pipeline.stop}>Abandon</button>
59
+ </>
60
+ )}
61
+
62
+ {pipeline.state === 'completed' && (
63
+ <p>Done!</p>
64
+ )}
65
+ </div>
66
+ )
67
+ }
68
+ ```
69
+
70
+ ## Steps with IDs
71
+
72
+ You can assign IDs to steps for better tracking. TypeScript will automatically infer the ID types.
73
+
74
+ ```tsx
75
+ const pipeline = usePipeline([
76
+ { id: 'fetch-user', action: async () => fetchUser() },
77
+ { id: 'validate', action: () => validateData() },
78
+ async () => doSomethingWithoutId(),
79
+ { id: 'save', action: async () => saveData() },
80
+ ])
81
+
82
+ if (pipeline.state === 'running') {
83
+ console.log('Current step:', pipeline.current.id)
84
+ // TypeScript knows: id is 'fetch-user' | 'validate' | 'save' | undefined
85
+ }
86
+ ```
87
+
88
+ ## Auto-run
89
+
90
+ Start the pipeline automatically when the component mounts:
91
+
92
+ ```tsx
93
+ const pipeline = usePipeline(
94
+ [step1, step2, step3],
95
+ { autorun: true }
96
+ )
97
+ ```
98
+
99
+ ## API
100
+
101
+ ### `usePipeline(steps, options?)`
102
+
103
+ #### Parameters
104
+
105
+ - `steps` - Array of actions. Each action can be:
106
+ - A function: `(signal?: AbortSignal) => Promise<unknown> | unknown`
107
+ - An object: `{ id: string, action: (signal?: AbortSignal) => Promise<unknown> | unknown }`
108
+ - `options` - Optional configuration:
109
+ - `autorun?: boolean` - Start pipeline on mount (default: `false`)
110
+
111
+ #### Returns (Discriminated Union)
112
+
113
+ The hook returns a discriminated union based on `state`:
114
+
115
+ | State | `current` | Description |
116
+ |-------|-----------|-------------|
117
+ | `'idle'` | `undefined` | Not started or stopped |
118
+ | `'running'` | `CurrentStatus` | Executing steps |
119
+ | `'failed'` | `CurrentStatus` | Stopped on error |
120
+ | `'completed'` | `undefined` | All steps done |
121
+
122
+ **Methods:**
123
+
124
+ - `start()` - Start from beginning. Returns `true` if started, `false` if running or failed.
125
+ - `stop()` - Cancel and reset to idle. Returns `true` if was running or failed, `false` otherwise.
126
+ - `resume()` - Retry failed step. Returns `true` if was failed, `false` otherwise.
127
+
128
+ **State transitions:**
129
+
130
+ | State | `start()` | `stop()` | `resume()` |
131
+ |-------|-----------|----------|------------|
132
+ | `idle` | ✅ starts | ❌ false | ❌ false |
133
+ | `running` | ❌ false | ✅ → idle | ❌ false |
134
+ | `failed` | ❌ false | ✅ → idle | ✅ retries |
135
+ | `completed` | ✅ restarts | ❌ false | ❌ false |
136
+
137
+ **CurrentStatus:**
138
+
139
+ ```ts
140
+ {
141
+ index: number // Step index (0-based)
142
+ id: string | undefined // Step ID if provided
143
+ state: 'running' | 'failed'
144
+ error: unknown | undefined // Error if failed
145
+ }
146
+ ```
147
+
148
+ ## Type Safety
149
+
150
+ Thanks to discriminated unions, TypeScript narrows the `current` type based on `state`:
151
+
152
+ ```tsx
153
+ if (pipeline.state === 'failed') {
154
+ // TypeScript knows current is defined!
155
+ console.log(pipeline.current.error) // ✅ No need for && pipeline.current
156
+ }
157
+
158
+ if (pipeline.state === 'idle') {
159
+ pipeline.current.index // ❌ Compile error - current is undefined
160
+ }
161
+ ```
162
+
163
+ ## AbortController Support
164
+
165
+ Each action receives an optional `AbortSignal`. Use it to make your actions cancellable:
166
+
167
+ ```tsx
168
+ async (signal) => {
169
+ // Fetch automatically aborts when signal fires
170
+ const response = await fetch('/api/data', { signal })
171
+ return response.json()
172
+ }
173
+ ```
174
+
175
+ When `stop()` is called or the component unmounts, the signal is aborted automatically.
176
+
177
+ ## License
178
+
179
+ ISC
@@ -0,0 +1,50 @@
1
+ type PipelineAction = (signal?: AbortSignal) => Promise<unknown> | unknown;
2
+ interface PipelineStepWithId {
3
+ id: string;
4
+ action: PipelineAction;
5
+ }
6
+ type PipelineStep = PipelineAction | PipelineStepWithId;
7
+ interface PipelineOptions {
8
+ autorun?: boolean;
9
+ }
10
+ type PipelineState = 'idle' | 'running' | 'failed' | 'completed';
11
+ type StepState = 'running' | 'failed';
12
+ interface CurrentStatus<TIds extends string | undefined> {
13
+ index: number;
14
+ id: TIds;
15
+ state: StepState;
16
+ error: unknown | undefined;
17
+ }
18
+ interface PipelineMethods {
19
+ start: () => boolean;
20
+ stop: () => boolean;
21
+ resume: () => boolean;
22
+ }
23
+ type PipelineIdle = PipelineMethods & {
24
+ state: 'idle';
25
+ current: undefined;
26
+ };
27
+ type PipelineRunning<TIds extends string | undefined> = PipelineMethods & {
28
+ state: 'running';
29
+ current: CurrentStatus<TIds>;
30
+ };
31
+ type PipelineFailed<TIds extends string | undefined> = PipelineMethods & {
32
+ state: 'failed';
33
+ current: CurrentStatus<TIds>;
34
+ };
35
+ type PipelineCompleted = PipelineMethods & {
36
+ state: 'completed';
37
+ current: undefined;
38
+ };
39
+ type PipelineResult<TIds extends string | undefined> = PipelineIdle | PipelineRunning<TIds> | PipelineFailed<TIds> | PipelineCompleted;
40
+ type ExtractStepId<T> = T extends {
41
+ id: infer Id extends string;
42
+ } ? Id : undefined;
43
+ type ExtractAllIds<T extends readonly PipelineStep[]> = ExtractStepId<T[number]>;
44
+
45
+ /**
46
+ * Runs a list of actions sequentially with support for abort, resume, and error handling.
47
+ */
48
+ declare function usePipeline<const T extends readonly PipelineStep[]>(steps: T, options?: PipelineOptions): PipelineResult<ExtractAllIds<T>>;
49
+
50
+ export { type CurrentStatus, type PipelineAction, type PipelineOptions, type PipelineResult, type PipelineState, type PipelineStep, type PipelineStepWithId, type StepState, usePipeline };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import*as t from'react';function R(e){let[p,f]=t.useState(e),n=t.useRef(e),r=t.useCallback(a=>{n.current=a,f(a);},[]);return [p,n,r]}function P(e){return typeof e=="object"&&e!==null&&"action"in e}function A(e){return P(e)?e.action:e}function E(e){if(P(e))return e.id}function h(e,p){let[f,n,r]=R("idle"),[a,c]=t.useState(void 0),d=t.useRef(0),u=t.useRef(null),o=t.useCallback(async x=>{r("running"),u.current=new AbortController;let l=u.current.signal;for(let i=x;i<e.length;i++){if(l.aborted)return;let y=e[i],I=A(y),S=E(y);d.current=i,c({index:i,id:S,state:"running",error:void 0});try{await I(l);}catch(C){if(l.aborted)return;r("failed"),c({index:i,id:S,state:"failed",error:C});return}}l.aborted||(r("completed"),c(void 0));},[e]),T=t.useCallback(()=>n.current==="running"||n.current==="failed"?false:(o(0),true),[o]),b=t.useCallback(()=>n.current!=="running"&&n.current!=="failed"?false:(u.current?.abort(),u.current=null,r("idle"),c(void 0),true),[]),m=t.useCallback(()=>n.current!=="failed"?false:(o(d.current),true),[o]);return t.useEffect(()=>(p?.autorun&&T(),()=>{u.current?.abort();}),[]),{state:f,current:a,start:T,stop:b,resume:m}}export{h as usePipeline};//# sourceMappingURL=index.js.map
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils.ts","../src/usePipeline.ts"],"names":["useStateRef","initialValue","state","setState","s","ref","setValue","value","isStepWithId","step","getStepAction","getStepId","usePipeline","steps","options","stateRef","current","setCurrent","currentIndexRef","abortControllerRef","runFromIndex","startIndex","signal","action","id","error","start","stop","resume"],"mappings":"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","file":"index.js","sourcesContent":["import * as React from 'react'\n\n//\n\n/**\n * Combines useState and useRef - state for React reactivity, ref for immediate access in async callbacks.\n */\nexport function useStateRef<T>(initialValue: T) {\n const [state, setState] = React.useState(initialValue);\n const ref = React.useRef(initialValue);\n\n const setValue = React.useCallback((value: T) => {\n ref.current = value;\n setState(value);\n }, []);\n\n return [state, ref, setValue] as const;\n}\n","import * as React from 'react'\n\nimport type * as Types from './types'\nimport * as Utils from './utils'\n\n//\n\nfunction isStepWithId(step: Types.PipelineStep): step is Types.PipelineStepWithId {\n return typeof step === 'object' && step !== null && 'action' in step;\n}\n\nfunction getStepAction(step: Types.PipelineStep): Types.PipelineAction {\n if (isStepWithId(step))\n return step.action;\n\n return step;\n}\n\nfunction getStepId<T extends Types.PipelineStep>(step: T): Types.ExtractStepId<T> {\n if (isStepWithId(step))\n return step.id as Types.ExtractStepId<T>;\n\n return undefined as Types.ExtractStepId<T>;\n}\n\n//\n\n/**\n * Runs a list of actions sequentially with support for abort, resume, and error handling.\n */\nexport function usePipeline<const T extends readonly Types.PipelineStep[]>(\n steps: T,\n options?: Types.PipelineOptions\n): Types.PipelineResult<Types.ExtractAllIds<T>> {\n type TIds = Types.ExtractAllIds<T>\n\n const [state, stateRef, setState] = Utils.useStateRef<Types.PipelineState>('idle');\n const [current, setCurrent] = React.useState<Types.CurrentStatus<TIds> | undefined>(undefined);\n\n const currentIndexRef = React.useRef<number>(0);\n const abortControllerRef = React.useRef<AbortController | null>(null);\n\n //\n\n const runFromIndex = React.useCallback(async (startIndex: number) => {\n setState('running');\n\n abortControllerRef.current = new AbortController();\n const signal = abortControllerRef.current.signal;\n\n for (let i = startIndex; i < steps.length; i++) {\n if (signal.aborted)\n return;\n\n const step = steps[i];\n const action = getStepAction(step);\n const id = getStepId(step) as TIds;\n\n currentIndexRef.current = i;\n setCurrent({ index: i, id, state: 'running', error: undefined });\n\n try {\n await action(signal);\n }\n catch (error) {\n if (signal.aborted)\n return;\n\n setState('failed');\n setCurrent({ index: i, id, state: 'failed', error });\n return;\n }\n }\n\n if (signal.aborted)\n return;\n\n setState('completed');\n setCurrent(undefined);\n }, [steps]);\n\n //\n\n const start = React.useCallback(() => {\n if (stateRef.current === 'running' || stateRef.current === 'failed')\n return false;\n\n runFromIndex(0);\n return true;\n }, [runFromIndex]);\n\n const stop = React.useCallback(() => {\n if (stateRef.current !== 'running' && stateRef.current !== 'failed')\n return false;\n\n abortControllerRef.current?.abort();\n abortControllerRef.current = null;\n\n setState('idle');\n setCurrent(undefined);\n\n return true;\n }, []);\n\n const resume = React.useCallback(() => {\n if (stateRef.current !== 'failed')\n return false;\n\n runFromIndex(currentIndexRef.current);\n return true;\n }, [runFromIndex]);\n\n //\n\n React.useEffect(() => {\n if (options?.autorun)\n start();\n\n return () => {\n abortControllerRef.current?.abort();\n };\n }, []);\n\n //\n\n return { state, current, start, stop, resume } as Types.PipelineResult<TIds>;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "react-pipeline-runner",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight React hook for running async actions sequentially with abort support and resume capability",
5
+ "keywords": [
6
+ "react",
7
+ "hook",
8
+ "pipeline",
9
+ "async",
10
+ "sequential",
11
+ "queue",
12
+ "runner"
13
+ ],
14
+ "author": "tonylus",
15
+ "license": "ISC",
16
+ "repository": "https://github.com/TonylusMark1/react-pipeline-runner",
17
+ "type": "module",
18
+ "main": "./dist/index.js",
19
+ "module": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsup",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "peerDependencies": {
37
+ "react": "^18.0.0 || ^19.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@testing-library/react": "^16.3.2",
41
+ "@types/node": "^25.3.2",
42
+ "@types/react": "^19.0.0",
43
+ "happy-dom": "^20.8.4",
44
+ "react": "^19.2.4",
45
+ "tsup": "^8.5.1",
46
+ "typescript": "^5.5.3",
47
+ "vitest": "^4.1.0"
48
+ }
49
+ }