react-flow-z 1.0.2 → 1.0.4
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 +79 -20
- package/build/flow-system/Flow.d.ts +3 -2
- package/build/index.cjs.js +1 -1
- package/build/index.esm.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ It focuses on **how async logic runs**, not:
|
|
|
20
20
|
## Why react-flow-z
|
|
21
21
|
|
|
22
22
|
- Typed async execution pipeline
|
|
23
|
-
-
|
|
23
|
+
- Declarative flow composition
|
|
24
24
|
- Abort & cancellation via `AbortController`
|
|
25
25
|
- Async orchestration operators: `debounce` · `retry` · `timeout` · `switchMap` ·` parallel`...
|
|
26
26
|
- Control flow: `filter` · `take` · `conditional execution`...
|
|
@@ -45,11 +45,16 @@ import { Flow } from "react-flow-z"
|
|
|
45
45
|
|
|
46
46
|
new Flow()
|
|
47
47
|
.debounce(300)
|
|
48
|
-
.switchMap(q =>
|
|
49
|
-
|
|
48
|
+
.switchMap(async q => {
|
|
49
|
+
const res = await fetch(`/search?q=${q}`)
|
|
50
|
+
return res.json()
|
|
51
|
+
})
|
|
52
|
+
.tap(console.log)
|
|
50
53
|
.run("hello")
|
|
51
54
|
```
|
|
52
55
|
|
|
56
|
+
> Avoid mutating a shared Flow instance inside React render loops.
|
|
57
|
+
|
|
53
58
|
---
|
|
54
59
|
|
|
55
60
|
## Cancellation
|
|
@@ -104,21 +109,25 @@ useFlow(
|
|
|
104
109
|
|
|
105
110
|
##### searchFlow.ts
|
|
106
111
|
```ts
|
|
107
|
-
import { Flow } from "react-flow-z"
|
|
112
|
+
import { Flow } from "react-flow-z";
|
|
113
|
+
|
|
114
|
+
export type Post = {
|
|
115
|
+
id: number;
|
|
116
|
+
title: string;
|
|
117
|
+
};
|
|
108
118
|
|
|
109
|
-
export const searchFlow = new Flow()
|
|
110
|
-
.onStart(() => console.log("loading..."))
|
|
119
|
+
export const searchFlow = new Flow<string, Post[]>()
|
|
111
120
|
.debounce(300)
|
|
112
|
-
.filter(
|
|
113
|
-
.switchMap(async
|
|
121
|
+
.filter(q => q.trim().length > 0)
|
|
122
|
+
.switchMap(async q => {
|
|
114
123
|
const res = await fetch(
|
|
115
124
|
`https://jsonplaceholder.typicode.com/posts?q=${q}`
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (!res.ok) throw new Error("network error");
|
|
128
|
+
|
|
129
|
+
return res.json();
|
|
130
|
+
});
|
|
122
131
|
|
|
123
132
|
// searchFlow.run("r")
|
|
124
133
|
// searchFlow.run("re")
|
|
@@ -127,16 +136,43 @@ export const searchFlow = new Flow()
|
|
|
127
136
|
|
|
128
137
|
##### React usage
|
|
129
138
|
```ts
|
|
139
|
+
import { useEffect, useState } from "react";
|
|
140
|
+
import { searchFlow, Post } from "./searchFlow";
|
|
141
|
+
import "./styles.css";
|
|
142
|
+
|
|
130
143
|
function SearchExample() {
|
|
131
|
-
const [q, setQ] = useState("")
|
|
132
|
-
const [posts, setPosts] = useState<
|
|
144
|
+
const [q, setQ] = useState("");
|
|
145
|
+
const [posts, setPosts] = useState<Post[]>([]);
|
|
146
|
+
const [loading, setLoading] = useState(false);
|
|
147
|
+
const [error, setError] = useState<string | null>(null);
|
|
133
148
|
|
|
134
149
|
useEffect(() => {
|
|
150
|
+
if (!q) {
|
|
151
|
+
setPosts([]);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setLoading(true);
|
|
156
|
+
setError(null);
|
|
157
|
+
|
|
135
158
|
searchFlow
|
|
136
|
-
.tap(setPosts)
|
|
137
|
-
.catch(() => [])
|
|
138
159
|
.run(q)
|
|
139
|
-
|
|
160
|
+
.then(result => {
|
|
161
|
+
setPosts(Array.isArray(result) ? result : []);
|
|
162
|
+
})
|
|
163
|
+
.catch(err => {
|
|
164
|
+
console.error(err);
|
|
165
|
+
setError("Something went wrong");
|
|
166
|
+
setPosts([]);
|
|
167
|
+
})
|
|
168
|
+
.finally(() => {
|
|
169
|
+
setLoading(false);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return () => {
|
|
173
|
+
searchFlow.cancel();
|
|
174
|
+
};
|
|
175
|
+
}, [q]);
|
|
140
176
|
|
|
141
177
|
return (
|
|
142
178
|
<>
|
|
@@ -145,13 +181,26 @@ function SearchExample() {
|
|
|
145
181
|
onChange={e => setQ(e.target.value)}
|
|
146
182
|
placeholder="Search posts..."
|
|
147
183
|
/>
|
|
184
|
+
|
|
185
|
+
{loading && <p>Loading...</p>}
|
|
186
|
+
{error && <p style={{ color: "red" }}>{error}</p>}
|
|
187
|
+
|
|
148
188
|
<ul>
|
|
149
189
|
{posts.map(p => (
|
|
150
190
|
<li key={p.id}>{p.title}</li>
|
|
151
191
|
))}
|
|
152
192
|
</ul>
|
|
153
193
|
</>
|
|
154
|
-
)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export default function App() {
|
|
198
|
+
return (
|
|
199
|
+
<div className="App">
|
|
200
|
+
<h2>react-flow-z + React</h2>
|
|
201
|
+
<SearchExample />
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
155
204
|
}
|
|
156
205
|
|
|
157
206
|
```
|
|
@@ -218,6 +267,16 @@ If you need **explicit async execution with cancel / debounce / queue** → reac
|
|
|
218
267
|
|
|
219
268
|
---
|
|
220
269
|
|
|
270
|
+
## Flow lifecycle notes
|
|
271
|
+
|
|
272
|
+
- A `Flow` instance is stateful
|
|
273
|
+
- Operators like `tap`, `catch`, `onDone` mutate the instance
|
|
274
|
+
- Prefer:
|
|
275
|
+
- configuring the flow once
|
|
276
|
+
- handling React state outside the flow
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
221
280
|
## License
|
|
222
281
|
|
|
223
282
|
MIT
|
|
@@ -19,19 +19,20 @@ export type TypedFlow<I, O, Context> = Flow<Context> & {
|
|
|
19
19
|
backoff?: "linear" | "exponential";
|
|
20
20
|
}): TypedFlow<I, O, Context>;
|
|
21
21
|
poll(ms: number, options?: {
|
|
22
|
-
until?: (v:
|
|
22
|
+
until?: (v: O) => boolean;
|
|
23
23
|
max?: number;
|
|
24
24
|
}): TypedFlow<I, O, Context>;
|
|
25
25
|
timeout(ms: number): TypedFlow<I, O, Context>;
|
|
26
26
|
catch(fn: (e: any, context: Context) => O | Promise<O>): TypedFlow<I, O, Context>;
|
|
27
27
|
take(n: number): TypedFlow<I, O, Context>;
|
|
28
|
+
finally(fn: () => void): TypedFlow<I, O, Context>;
|
|
28
29
|
};
|
|
29
30
|
export declare class Flow<Context = {}> {
|
|
30
31
|
private readonly ctx;
|
|
31
32
|
static SKIP: symbol;
|
|
32
33
|
private ops;
|
|
33
34
|
private paused?;
|
|
34
|
-
private resume
|
|
35
|
+
private resume?;
|
|
35
36
|
private controller;
|
|
36
37
|
private cancelHandlers;
|
|
37
38
|
private onStartHandlers;
|
package/build/index.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var t=require("react");const r=(t,r)=>new Promise((e,s)=>{const n=setTimeout(e,t);r.addEventListener("abort",()=>{clearTimeout(n),s("aborted")})});class
|
|
1
|
+
"use strict";var t=require("react");const r=(t,r)=>new Promise((e,s)=>{const n=setTimeout(e,t);r.addEventListener("abort",()=>{clearTimeout(n),s("aborted")})}),e=Symbol("FLOW_HANDLED");class s{constructor(t={}){this.ctx=t,this.ops=[],this.controller=null,this.cancelHandlers=[],this.onStartHandlers=[],this.onDoneHandlers=[],this.onErrorHandlers=[],this.finallyHandlers=[]}static from(t,r){const e=new s(r);return e.initialInput=t,e}async run(t){const r=void 0!==t?t:this.initialInput;if(void 0===r)throw Error("Flow.run: missing input");this.cancel(),this.controller=new AbortController,this.onStartHandlers.forEach(t=>t());let n=r,i=!1;try{for(const t of this.ops){if(this.controller.signal.aborted)return;await this.waitIfPaused(),n=await t(n,this.ctx,this.controller.signal)}return this.onDoneHandlers.forEach(t=>t()),n}catch(t){if(t===s.SKIP)return void(i=!0);throw(null==t?void 0:t[e])||this.onErrorHandlers.forEach(r=>r(t)),t}finally{i||this.finallyHandlers.forEach(t=>t())}}onStart(t){return this.onStartHandlers.push(t),this}onDone(t){return this.onDoneHandlers.push(t),this}onError(t){return this.onErrorHandlers.push(t),this}onCancel(t){return this.cancelHandlers.push(t),this}cancel(){var t;null===(t=this.controller)||void 0===t||t.abort(),this.controller=null,this.resume&&(this.resume(),this.resume=void 0,this.paused=void 0),this.cancelHandlers.forEach(t=>t())}pause(){return this.paused||(this.paused=new Promise(t=>{this.resume=t})),this}resumeFlow(){return this.resume&&(this.resume(),this.resume=void 0,this.paused=void 0),this}async waitIfPaused(){this.paused&&await this.paused}context(){return this.ctx}step(t){return this.ops.push(t),this}tap(t){return this.step((r,e)=>(t(r,e),r))}filter(t){return this.step(r=>{if(!t(r))throw s.SKIP;return r})}debounce(t){return this.step(async(e,s,n)=>(await r(t,n),e))}leading(t){let r=!1;return this.step(async e=>{if(r)throw s.SKIP;return r=!0,setTimeout(()=>r=!1,t),e})}throttle(t){return this.leading(t)}take(t){let r=0;return this.step(e=>{if(++r>t)throw s.SKIP;return e})}map(t){return this.step((r,e)=>t(r,e))}switchMap(t){let r=null;return this.step(async(e,s,n)=>(null==r||r.abort(),r=new AbortController,n.addEventListener("abort",()=>null==r?void 0:r.abort()),t(e,s,r.signal)))}exhaustMap(t){let r=!1;return this.step(async(e,n,i)=>{if(r)throw s.SKIP;r=!0;try{return await t(e,n,i)}finally{r=!1}})}distinct(t=Object.is){let r,e=!1;return this.step(n=>{if(e&&t(r,n))throw s.SKIP;return e=!0,r=n,n})}retry(t){const e=this.ops.pop();if(!e)return this;const s="number"==typeof t?{times:t}:t,{times:n,delay:i=0,backoff:o}=s;return this.ops.push(async(t,s,a)=>{let l=0;for(;;)try{return await e(t,s,a)}catch(t){if(l++,l>n)throw t;if(i>0){const t="exponential"===o?i*Math.pow(2,l-1):"linear"===o?i*l:i;await r(t,a)}}}),this}poll(t,e){const s=this.ops.pop();if(!s)return this;const{until:n,max:i}=e||{};return this.ops.push(async(e,o,a)=>{let l,u=0;for(;;){if(l=await s(e,o,a),null==n?void 0:n(l))return l;if(i&&++u>=i)throw Error("poll max reached");await r(t,a)}}),this}timeout(t){const e=this.ops.pop();if(!e)return this;return this.ops.push((s,n,i)=>Promise.race([e(s,n,i),r(t,i).then(()=>{throw Error("timeout")})])),this}catch(t){const r=this.ops.pop();if(!r)return this;return this.ops.push(async(s,n,i)=>{try{return await r(s,n,i)}catch(r){return r[e]=!0,t(r,n)}}),this}finally(t){return this.finallyHandlers.push(t),this}}s.SKIP=Symbol("FLOW_SKIP"),exports.Flow=s,exports.sleep=r,exports.useFlow=function(r,e,n){const i=t.useRef(null),o=t.useRef(null!=n?n:{});return o.current=null!=n?n:o.current,t.useEffect(()=>{var t;null===(t=i.current)||void 0===t||t.cancel();const n=s.from(r,o.current),a=e(n);return i.current=a,a.run(),()=>a.cancel()},[r,e]),{cancel:t.useCallback(()=>{var t;null===(t=i.current)||void 0===t||t.cancel()},[]),pause:t.useCallback(()=>{var t;null===(t=i.current)||void 0===t||t.pause()},[]),resume:t.useCallback(()=>{var t;null===(t=i.current)||void 0===t||t.resumeFlow()},[]),flow:i.current}};
|
package/build/index.esm.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{useRef as t,useEffect as r,useCallback as
|
|
1
|
+
import{useRef as t,useEffect as r,useCallback as s}from"react";const e=(t,r)=>new Promise((s,e)=>{const n=setTimeout(s,t);r.addEventListener("abort",()=>{clearTimeout(n),e("aborted")})}),n=Symbol("FLOW_HANDLED");class i{constructor(t={}){this.ctx=t,this.ops=[],this.controller=null,this.cancelHandlers=[],this.onStartHandlers=[],this.onDoneHandlers=[],this.onErrorHandlers=[],this.finallyHandlers=[]}static from(t,r){const s=new i(r);return s.initialInput=t,s}async run(t){const r=void 0!==t?t:this.initialInput;if(void 0===r)throw Error("Flow.run: missing input");this.cancel(),this.controller=new AbortController,this.onStartHandlers.forEach(t=>t());let s=r,e=!1;try{for(const t of this.ops){if(this.controller.signal.aborted)return;await this.waitIfPaused(),s=await t(s,this.ctx,this.controller.signal)}return this.onDoneHandlers.forEach(t=>t()),s}catch(t){if(t===i.SKIP)return void(e=!0);throw(null==t?void 0:t[n])||this.onErrorHandlers.forEach(r=>r(t)),t}finally{e||this.finallyHandlers.forEach(t=>t())}}onStart(t){return this.onStartHandlers.push(t),this}onDone(t){return this.onDoneHandlers.push(t),this}onError(t){return this.onErrorHandlers.push(t),this}onCancel(t){return this.cancelHandlers.push(t),this}cancel(){var t;null===(t=this.controller)||void 0===t||t.abort(),this.controller=null,this.resume&&(this.resume(),this.resume=void 0,this.paused=void 0),this.cancelHandlers.forEach(t=>t())}pause(){return this.paused||(this.paused=new Promise(t=>{this.resume=t})),this}resumeFlow(){return this.resume&&(this.resume(),this.resume=void 0,this.paused=void 0),this}async waitIfPaused(){this.paused&&await this.paused}context(){return this.ctx}step(t){return this.ops.push(t),this}tap(t){return this.step((r,s)=>(t(r,s),r))}filter(t){return this.step(r=>{if(!t(r))throw i.SKIP;return r})}debounce(t){return this.step(async(r,s,n)=>(await e(t,n),r))}leading(t){let r=!1;return this.step(async s=>{if(r)throw i.SKIP;return r=!0,setTimeout(()=>r=!1,t),s})}throttle(t){return this.leading(t)}take(t){let r=0;return this.step(s=>{if(++r>t)throw i.SKIP;return s})}map(t){return this.step((r,s)=>t(r,s))}switchMap(t){let r=null;return this.step(async(s,e,n)=>(null==r||r.abort(),r=new AbortController,n.addEventListener("abort",()=>null==r?void 0:r.abort()),t(s,e,r.signal)))}exhaustMap(t){let r=!1;return this.step(async(s,e,n)=>{if(r)throw i.SKIP;r=!0;try{return await t(s,e,n)}finally{r=!1}})}distinct(t=Object.is){let r,s=!1;return this.step(e=>{if(s&&t(r,e))throw i.SKIP;return s=!0,r=e,e})}retry(t){const r=this.ops.pop();if(!r)return this;const s="number"==typeof t?{times:t}:t,{times:n,delay:i=0,backoff:o}=s;return this.ops.push(async(t,s,a)=>{let l=0;for(;;)try{return await r(t,s,a)}catch(t){if(l++,l>n)throw t;if(i>0){const t="exponential"===o?i*Math.pow(2,l-1):"linear"===o?i*l:i;await e(t,a)}}}),this}poll(t,r){const s=this.ops.pop();if(!s)return this;const{until:n,max:i}=r||{};return this.ops.push(async(r,o,a)=>{let l,u=0;for(;;){if(l=await s(r,o,a),null==n?void 0:n(l))return l;if(i&&++u>=i)throw Error("poll max reached");await e(t,a)}}),this}timeout(t){const r=this.ops.pop();if(!r)return this;return this.ops.push((s,n,i)=>Promise.race([r(s,n,i),e(t,i).then(()=>{throw Error("timeout")})])),this}catch(t){const r=this.ops.pop();if(!r)return this;return this.ops.push(async(s,e,i)=>{try{return await r(s,e,i)}catch(r){return r[n]=!0,t(r,e)}}),this}finally(t){return this.finallyHandlers.push(t),this}}function o(e,n,o){const a=t(null),l=t(null!=o?o:{});l.current=null!=o?o:l.current,r(()=>{var t;null===(t=a.current)||void 0===t||t.cancel();const r=i.from(e,l.current),s=n(r);return a.current=s,s.run(),()=>s.cancel()},[e,n]);return{cancel:s(()=>{var t;null===(t=a.current)||void 0===t||t.cancel()},[]),pause:s(()=>{var t;null===(t=a.current)||void 0===t||t.pause()},[]),resume:s(()=>{var t;null===(t=a.current)||void 0===t||t.resumeFlow()},[]),flow:a.current}}i.SKIP=Symbol("FLOW_SKIP");export{i as Flow,e as sleep,o as useFlow};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-flow-z",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A lightweight async flow runtime for orchestrating side effects with explicit control over cancellation, debounce, throttling, and execution order.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Delpi.Kye",
|