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 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 pipeline = usePipeline([
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 pipeline = usePipeline([
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
- ## Auto-run
93
+ ## Autostart
89
94
 
90
95
  Start the pipeline automatically when the component mounts:
91
96
 
92
97
  ```tsx
93
- const pipeline = usePipeline(
94
- [step1, step2, step3],
95
- { autorun: true }
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
- - `autorun?: boolean` - Start pipeline on mount (default: `false`)
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
- autorun?: boolean;
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 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
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":"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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"]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-pipeline-runner",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Lightweight React hook for running async actions sequentially with abort support and resume capability",
5
5
  "keywords": [
6
6
  "react",