react-pipeline-runner 0.1.0 → 0.2.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/README.md +47 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,10 +21,11 @@ npm install react-pipeline-runner
|
|
|
21
21
|
## Basic Usage
|
|
22
22
|
|
|
23
23
|
```tsx
|
|
24
|
+
import { useMemo } from 'react'
|
|
24
25
|
import { usePipeline } from 'react-pipeline-runner'
|
|
25
26
|
|
|
26
27
|
function MyComponent() {
|
|
27
|
-
const
|
|
28
|
+
const steps = useMemo(() => [
|
|
28
29
|
async (signal) => {
|
|
29
30
|
await fetch('/api/step1', { signal })
|
|
30
31
|
},
|
|
@@ -34,7 +35,9 @@ function MyComponent() {
|
|
|
34
35
|
async () => {
|
|
35
36
|
console.log('Step 3 - no signal needed')
|
|
36
37
|
},
|
|
37
|
-
])
|
|
38
|
+
], [])
|
|
39
|
+
|
|
40
|
+
const pipeline = usePipeline(steps)
|
|
38
41
|
|
|
39
42
|
return (
|
|
40
43
|
<div>
|
|
@@ -72,12 +75,14 @@ function MyComponent() {
|
|
|
72
75
|
You can assign IDs to steps for better tracking. TypeScript will automatically infer the ID types.
|
|
73
76
|
|
|
74
77
|
```tsx
|
|
75
|
-
const
|
|
78
|
+
const steps = useMemo(() => [
|
|
76
79
|
{ id: 'fetch-user', action: async () => fetchUser() },
|
|
77
80
|
{ id: 'validate', action: () => validateData() },
|
|
78
81
|
async () => doSomethingWithoutId(),
|
|
79
82
|
{ id: 'save', action: async () => saveData() },
|
|
80
|
-
])
|
|
83
|
+
], [])
|
|
84
|
+
|
|
85
|
+
const pipeline = usePipeline(steps)
|
|
81
86
|
|
|
82
87
|
if (pipeline.state === 'running') {
|
|
83
88
|
console.log('Current step:', pipeline.current.id)
|
|
@@ -85,15 +90,14 @@ if (pipeline.state === 'running') {
|
|
|
85
90
|
}
|
|
86
91
|
```
|
|
87
92
|
|
|
88
|
-
##
|
|
93
|
+
## Autostart
|
|
89
94
|
|
|
90
95
|
Start the pipeline automatically when the component mounts:
|
|
91
96
|
|
|
92
97
|
```tsx
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
98
|
+
const steps = useMemo(() => [step1, step2, step3], [])
|
|
99
|
+
|
|
100
|
+
const pipeline = usePipeline(steps, { autostart: true })
|
|
97
101
|
```
|
|
98
102
|
|
|
99
103
|
## API
|
|
@@ -106,7 +110,7 @@ const pipeline = usePipeline(
|
|
|
106
110
|
- A function: `(signal?: AbortSignal) => Promise<unknown> | unknown`
|
|
107
111
|
- An object: `{ id: string, action: (signal?: AbortSignal) => Promise<unknown> | unknown }`
|
|
108
112
|
- `options` - Optional configuration:
|
|
109
|
-
- `
|
|
113
|
+
- `autostart?: boolean` - Start pipeline on mount (default: `false`)
|
|
110
114
|
|
|
111
115
|
#### Returns (Discriminated Union)
|
|
112
116
|
|
|
@@ -174,6 +178,39 @@ async (signal) => {
|
|
|
174
178
|
|
|
175
179
|
When `stop()` is called or the component unmounts, the signal is aborted automatically.
|
|
176
180
|
|
|
181
|
+
## Best Practice: Memoize Steps
|
|
182
|
+
|
|
183
|
+
Always wrap your steps array in `useMemo` to ensure stable references:
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
// ❌ Bad - new array on every render
|
|
187
|
+
const pipeline = usePipeline([
|
|
188
|
+
() => fetchData(),
|
|
189
|
+
() => processData(),
|
|
190
|
+
])
|
|
191
|
+
|
|
192
|
+
// ✅ Good - stable reference
|
|
193
|
+
const steps = useMemo(() => [
|
|
194
|
+
() => fetchData(),
|
|
195
|
+
() => processData(),
|
|
196
|
+
], [])
|
|
197
|
+
|
|
198
|
+
const pipeline = usePipeline(steps)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
If steps depend on props or state, include them in dependencies:
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
const steps = useMemo(() => [
|
|
205
|
+
() => fetchUser(userId),
|
|
206
|
+
() => sendNotification(),
|
|
207
|
+
], [userId])
|
|
208
|
+
|
|
209
|
+
const pipeline = usePipeline(steps)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Why?** Without `useMemo`, the `start` and `resume` methods get new references on every render, which can cause unnecessary re-renders in child components or issues with effect dependencies.
|
|
213
|
+
|
|
177
214
|
## License
|
|
178
215
|
|
|
179
216
|
ISC
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ interface PipelineStepWithId {
|
|
|
5
5
|
}
|
|
6
6
|
type PipelineStep = PipelineAction | PipelineStepWithId;
|
|
7
7
|
interface PipelineOptions {
|
|
8
|
-
|
|
8
|
+
autostart?: boolean;
|
|
9
9
|
}
|
|
10
10
|
type PipelineState = 'idle' | 'running' | 'failed' | 'completed';
|
|
11
11
|
type StepState = 'running' | 'failed';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import*as t from'react';function
|
|
1
|
+
import*as t from'react';function m(e){let[f,a]=t.useState(e),n=t.useRef(e),r=t.useCallback(u=>{n.current=u,a(u);},[]);return [f,n,r]}function b(e){return typeof e=="object"&&e!==null&&"action"in e}function A(e){return b(e)?e.action:e}function E(e){if(b(e))return e.id}function h(e,f){let[a,n,r]=m("idle"),[u,c]=t.useState(void 0),y=t.useRef(0),o=t.useRef(null),l=t.useCallback(async x=>{r("running"),o.current=new AbortController;let p=o.current.signal;for(let i=x;i<e.length;i++){if(p.aborted)return;let S=e[i],I=A(S),P=E(S);y.current=i,c({index:i,id:P,state:"running",error:void 0});try{await I(p);}catch(C){if(p.aborted)return;r("failed"),c({index:i,id:P,state:"failed",error:C});return}}p.aborted||(r("completed"),c(void 0));},[e]),d=t.useCallback(()=>n.current==="running"||n.current==="failed"?false:(l(0),true),[l]),T=t.useCallback(()=>n.current!=="running"&&n.current!=="failed"?false:(o.current?.abort(),o.current=null,r("idle"),c(void 0),true),[]),R=t.useCallback(()=>n.current!=="failed"?false:(l(y.current),true),[l]);return t.useEffect(()=>(f?.autostart&&d(),()=>{T();}),[]),t.useMemo(()=>({state:a,current:u,start:d,stop:T,resume:R}),[a,u,d,T,R])}export{h as usePipeline};//# sourceMappingURL=index.js.map
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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":"wBAOO,SAASA,CAAAA,CAAeC,CAAAA,CAAiB,CAC9C,GAAM,CAACC,CAAAA,CAAOC,CAAQ,
|
|
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?.autostart)\n start();\n\n return () => {\n stop();\n };\n }, []);\n\n //\n\n return React.useMemo(\n () => ({ state, current, start, stop, resume }),\n [state, current, start, stop, resume]\n ) as Types.PipelineResult<TIds>;\n}\n"]}
|