react-flow-z 1.0.0-z

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Delpi.Kye
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,223 @@
1
+ # 🌊 react-flow-z
2
+
3
+ [![NPM](https://img.shields.io/npm/v/react-flow-z.svg)](https://www.npmjs.com/package/react-flow-z) ![Downloads](https://img.shields.io/npm/dt/react-flow-z.svg)
4
+
5
+ <a href="https://codesandbox.io/p/sandbox/vkl64l" target="_blank">LIVE EXAMPLE</a>
6
+
7
+ ---
8
+
9
+ `react-flow-z` is a **small, framework-agnostic async flow runtime**.
10
+
11
+ It focuses on **how async logic runs**, not:
12
+ - how state is stored
13
+ - how UI renders
14
+ - or how effects are magically managed
15
+
16
+ > This library is about **orchestration**, not reactivity.
17
+
18
+ ---
19
+
20
+ ## Why react-flow-z
21
+
22
+ - Typed async execution pipeline
23
+ - Immutable flow composition
24
+ - Abort & cancellation via `AbortController`
25
+ - Async orchestration operators: `debounce` · `retry` · `timeout` · `switchMap` ·` parallel`
26
+ - Control flow: `filter` · `take` · `conditional execution`
27
+ - Pause / resume execution
28
+ - Framework-agnostic core
29
+ - Optional React hook (`useFlow`)
30
+
31
+ ---
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install react-flow-z
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Basic Usage
42
+
43
+ ```ts
44
+ import { Flow } from "react-flow-z"
45
+
46
+ new Flow()
47
+ .debounce(300)
48
+ .switchMap(q => fetch(`/search?q=${q}`))
49
+ .tap(res => console.log(res))
50
+ .run("hello")
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Cancellation
56
+
57
+ ```ts
58
+ const flow = new Flow()
59
+ .step(async (v, _, signal) => {
60
+ await sleep(1000, signal)
61
+ return v
62
+ })
63
+
64
+ flow.run(1)
65
+ flow.cancel()
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Pause / Resume
71
+
72
+ ```ts
73
+ flow.pause()
74
+
75
+ setTimeout(() => {
76
+ flow.resumeFlow()
77
+ }, 1000)
78
+ ```
79
+
80
+ ---
81
+
82
+ ## React Integration
83
+
84
+ ```ts
85
+ import { useFlow } from "react-flow-z"
86
+
87
+ useFlow(
88
+ keyword,
89
+ flow =>
90
+ flow
91
+ .debounce(300)
92
+ .switchMap(search)
93
+ .tap(setResult)
94
+ .catch(() => []),
95
+ {}
96
+ )
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Search posts (official example)
102
+
103
+ #### ✅ Pattern 1: Flow instance (recommended)
104
+
105
+ ##### searchFlow.ts
106
+ ```ts
107
+ import { Flow } from "react-flow-z"
108
+
109
+ export const searchFlow = new Flow()
110
+ .onStart(() => console.log("loading..."))
111
+ .debounce(300)
112
+ .filter((q: string) => q.length > 0)
113
+ .switchMap(async (q: string) => {
114
+ const res = await fetch(
115
+ `https://jsonplaceholder.typicode.com/posts?q=${q}`
116
+ )
117
+ if (!res.ok) throw new Error("network error")
118
+ return res.json()
119
+ })
120
+ .onDone(() => console.log("done"))
121
+ .onError(err => console.error(err))
122
+
123
+ // searchFlow.run("r")
124
+ // searchFlow.run("re")
125
+ // searchFlow.run("react")
126
+ ```
127
+
128
+ ##### React usage
129
+ ```ts
130
+ function SearchExample() {
131
+ const [q, setQ] = useState("")
132
+ const [posts, setPosts] = useState<any[]>([])
133
+
134
+ useEffect(() => {
135
+ searchFlow
136
+ .tap(setPosts)
137
+ .catch(() => [])
138
+ .run(q)
139
+ }, [q])
140
+
141
+ return (
142
+ <>
143
+ <input
144
+ value={q}
145
+ onChange={e => setQ(e.target.value)}
146
+ placeholder="Search posts..."
147
+ />
148
+ <ul>
149
+ {posts.map(p => (
150
+ <li key={p.id}>{p.title}</li>
151
+ ))}
152
+ </ul>
153
+ </>
154
+ )
155
+ }
156
+
157
+ ```
158
+
159
+ ---
160
+
161
+ #### ✅ Pattern 2: One-off Flow execution
162
+
163
+ ##### Submit form – prevent double submit (leading)
164
+
165
+ ```ts
166
+ import { Flow } from "react-flow-z"
167
+
168
+ export function runSearch(q: string) {
169
+ return new Flow()
170
+ .debounce(300)
171
+ .filter(Boolean)
172
+ .switchMap(async query => {
173
+ const res = await fetch(
174
+ `https://jsonplaceholder.typicode.com/posts?q=${query}`
175
+ )
176
+ if (!res.ok) throw new Error("network error")
177
+ return res.json()
178
+ })
179
+ .run(q)
180
+ }
181
+
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Philosophy
187
+
188
+ - Explicit over implicit
189
+ - Async/await over streams
190
+ - No global state
191
+ - No magic scheduling
192
+ - You own execution
193
+
194
+ ---
195
+
196
+ ## Compare high-level
197
+
198
+ | Point | react-flow-z | RxJS | Redux-Saga | XState | React Query |
199
+ | ---------------------- | ------------ | ----- | ---------- | ------- | ----------- |
200
+ | Async orchestration | ✅ | ✅ | ✅ | 🟡 | ❌ |
201
+ | Debounce / cancel | ✅ | ✅ | 🟡 | 🟡 | ❌ |
202
+ | Execution-first design | ✅ | ❌ | 🟡 | ❌ | ❌ |
203
+ | Framework-agnostic | ✅ | ✅ | ❌ | 🟡 | ❌ |
204
+ | Learning curve | ⭐ easy | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐ |
205
+
206
+ ---
207
+
208
+ ### What react-flow-z is NOT
209
+
210
+ - ❌ Not a state manager
211
+ - ❌ Not a reactive signal system
212
+ - ❌ Not a data cache like React Query
213
+ - ❌ Not a stream library like RxJS
214
+
215
+ If you only need data fetching → use React Query
216
+ If you need event streams → use RxJS
217
+ If you need **explicit async execution with cancel / debounce / queue** → react-flow-z
218
+
219
+ ---
220
+
221
+ ## License
222
+
223
+ MIT
@@ -0,0 +1,55 @@
1
+ export type Step<I, O, Context> = (input: I, context: Context, signal: AbortSignal) => O | Promise<O>;
2
+ export type Condition<T> = (v: T) => boolean;
3
+ export type CancelHandler = () => void;
4
+ export type TypedFlow<I, O, Context> = Flow<Context> & {
5
+ run(input?: I): Promise<O | undefined>;
6
+ step<N>(fn: Step<O, N, Context>): TypedFlow<I, N, Context>;
7
+ tap(fn: (v: O, context: Context) => void): TypedFlow<I, O, Context>;
8
+ filter(cond: Condition<O>): TypedFlow<I, O, Context>;
9
+ debounce(ms: number): TypedFlow<I, O, Context>;
10
+ throttle(ms: number): TypedFlow<I, O, Context>;
11
+ leading(ms: number): TypedFlow<I, O, Context>;
12
+ switchMap<N>(fn: Step<O, N, Context>): TypedFlow<I, N, Context>;
13
+ exhaustMap<N>(fn: Step<O, N, Context>): TypedFlow<I, N, Context>;
14
+ retry(times: number): TypedFlow<I, O, Context>;
15
+ timeout(ms: number): TypedFlow<I, O, Context>;
16
+ catch(fn: (e: any, context: Context) => O | Promise<O>): TypedFlow<I, O, Context>;
17
+ take(n: number): TypedFlow<I, O, Context>;
18
+ };
19
+ export declare class Flow<Context = {}> {
20
+ private readonly ctx;
21
+ static SKIP: symbol;
22
+ private ops;
23
+ private paused?;
24
+ private resume;
25
+ private controller;
26
+ private cancelHandlers;
27
+ private onStartHandlers;
28
+ private onDoneHandlers;
29
+ private onErrorHandlers;
30
+ private initialInput?;
31
+ constructor(ctx?: Context);
32
+ static from<I, Context = {}>(input: I, context?: Context): TypedFlow<I, I, Context>;
33
+ run(input?: unknown): Promise<any>;
34
+ onStart(fn: () => void): this;
35
+ onDone(fn: () => void): this;
36
+ onError(fn: (e: any) => void): this;
37
+ onCancel(fn: CancelHandler): this;
38
+ cancel(): void;
39
+ pause(): this;
40
+ resumeFlow(): this;
41
+ private waitIfPaused;
42
+ context(): Context;
43
+ step(fn: Step<any, any, Context>): any;
44
+ tap(fn: (v: any, context: Context) => void): any;
45
+ filter(cond: Condition<any>): any;
46
+ debounce(ms: number): any;
47
+ leading(ms: number): any;
48
+ throttle(ms: number): any;
49
+ take(n: number): any;
50
+ switchMap(fn: Step<any, any, Context>): any;
51
+ exhaustMap(fn: Step<any, any, Context>): any;
52
+ retry(times: number): this;
53
+ timeout(ms: number): this;
54
+ catch(fn: (e: any, context: Context) => any): this;
55
+ }
@@ -0,0 +1,3 @@
1
+ export type Step<I, O, Context> = (input: I, context: Context, signal: AbortSignal) => O | Promise<O>;
2
+ export type Condition<T> = (v: T) => boolean;
3
+ export type CancelHandler = () => void;
@@ -0,0 +1,7 @@
1
+ import { TypedFlow } from "./Flow";
2
+ export declare function useFlow<I, O = I, Context = {}>(source: I, builder: (f: TypedFlow<I, I, Context>) => TypedFlow<I, O, Context>, context?: Context): {
3
+ cancel: () => void;
4
+ pause: () => void;
5
+ resume: () => void;
6
+ flow: TypedFlow<I, O, Context> | null;
7
+ };
@@ -0,0 +1 @@
1
+ export declare const sleep: (ms: number, signal: AbortSignal) => Promise<void>;
@@ -0,0 +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 e{constructor(t={}){this.ctx=t,this.ops=[],this.controller=null,this.cancelHandlers=[],this.onStartHandlers=[],this.onDoneHandlers=[],this.onErrorHandlers=[]}static from(t,r){const s=new e(r);return s.initialInput=t,s}async run(t){var r;const s=void 0!==t?t:this.initialInput;if(void 0===s)throw Error("Flow.run: missing input");null===(r=this.controller)||void 0===r||r.abort(),this.controller=new AbortController,this.onStartHandlers.forEach(t=>t());let n=s;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===e.SKIP)return;throw this.onErrorHandlers.forEach(r=>r(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.cancelHandlers.forEach(t=>t())}pause(){return this.paused||(this.paused=new Promise(t=>this.resume=t)),this}resumeFlow(){return this.paused&&(this.resume(),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 e.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 s=>{if(r)throw e.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 e.SKIP;return s})}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(s,n,i)=>{if(r)throw e.SKIP;r=!0;try{return await t(s,n,i)}finally{r=!1}})}retry(t){const r=this.ops.pop();if(!r)return this;return this.ops.push(async(e,s,n)=>{let i=0;for(;;)try{return await r(e,s,n)}catch(r){if(++i>t)throw r}}),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(e,s,n)=>{try{return await r(e,s,n)}catch(r){return t(r,s)}}),this}}e.SKIP=Symbol("FLOW_SKIP"),exports.Flow=e,exports.sleep=r,exports.useFlow=function(r,s,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=e.from(r,o.current),a=s(n);return i.current=a,a.run(),()=>a.cancel()},[r,s]),{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}};
@@ -0,0 +1,4 @@
1
+ export { Flow } from "./flow-system/Flow";
2
+ export { useFlow } from "./flow-system/useFlow";
3
+ export type { Step, Condition, CancelHandler } from "./flow-system/types";
4
+ export { sleep } from "./flow-system/utils";
@@ -0,0 +1 @@
1
+ import{useRef as t,useEffect as r,useCallback as n}from"react";const e=(t,r)=>new Promise((n,e)=>{const s=setTimeout(n,t);r.addEventListener("abort",()=>{clearTimeout(s),e("aborted")})});class s{constructor(t={}){this.ctx=t,this.ops=[],this.controller=null,this.cancelHandlers=[],this.onStartHandlers=[],this.onDoneHandlers=[],this.onErrorHandlers=[]}static from(t,r){const n=new s(r);return n.initialInput=t,n}async run(t){var r;const n=void 0!==t?t:this.initialInput;if(void 0===n)throw Error("Flow.run: missing input");null===(r=this.controller)||void 0===r||r.abort(),this.controller=new AbortController,this.onStartHandlers.forEach(t=>t());let e=n;try{for(const t of this.ops){if(this.controller.signal.aborted)return;await this.waitIfPaused(),e=await t(e,this.ctx,this.controller.signal)}return this.onDoneHandlers.forEach(t=>t()),e}catch(t){if(t===s.SKIP)return;throw this.onErrorHandlers.forEach(r=>r(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.cancelHandlers.forEach(t=>t())}pause(){return this.paused||(this.paused=new Promise(t=>this.resume=t)),this}resumeFlow(){return this.paused&&(this.resume(),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,n)=>(t(r,n),r))}filter(t){return this.step(r=>{if(!t(r))throw s.SKIP;return r})}debounce(t){return this.step(async(r,n,s)=>(await e(t,s),r))}leading(t){let r=!1;return this.step(async n=>{if(r)throw s.SKIP;return r=!0,setTimeout(()=>r=!1,t),n})}throttle(t){return this.leading(t)}take(t){let r=0;return this.step(n=>{if(++r>t)throw s.SKIP;return n})}switchMap(t){let r=null;return this.step(async(n,e,s)=>(null==r||r.abort(),r=new AbortController,s.addEventListener("abort",()=>null==r?void 0:r.abort()),t(n,e,r.signal)))}exhaustMap(t){let r=!1;return this.step(async(n,e,i)=>{if(r)throw s.SKIP;r=!0;try{return await t(n,e,i)}finally{r=!1}})}retry(t){const r=this.ops.pop();if(!r)return this;return this.ops.push(async(n,e,s)=>{let i=0;for(;;)try{return await r(n,e,s)}catch(r){if(++i>t)throw r}}),this}timeout(t){const r=this.ops.pop();if(!r)return this;return this.ops.push((n,s,i)=>Promise.race([r(n,s,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(n,e,s)=>{try{return await r(n,e,s)}catch(r){return t(r,e)}}),this}}function i(e,i,o){const a=t(null),u=t(null!=o?o:{});u.current=null!=o?o:u.current,r(()=>{var t;null===(t=a.current)||void 0===t||t.cancel();const r=s.from(e,u.current),n=i(r);return a.current=n,n.run(),()=>n.cancel()},[e,i]);return{cancel:n(()=>{var t;null===(t=a.current)||void 0===t||t.cancel()},[]),pause:n(()=>{var t;null===(t=a.current)||void 0===t||t.pause()},[]),resume:n(()=>{var t;null===(t=a.current)||void 0===t||t.resumeFlow()},[]),flow:a.current}}s.SKIP=Symbol("FLOW_SKIP");export{s as Flow,e as sleep,i as useFlow};
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "react-flow-z",
3
+ "version": "1.0.0-z",
4
+ "description": "A lightweight async flow runtime for orchestrating side effects with explicit control over cancellation, debounce, throttling, and execution order.",
5
+ "license": "MIT",
6
+ "author": "Delpi.Kye",
7
+ "sideEffects": false,
8
+ "type": "module",
9
+ "main": "build/index.cjs.js",
10
+ "module": "build/index.esm.js",
11
+ "types": "build/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./build/index.d.ts",
15
+ "import": "./build/index.esm.js",
16
+ "require": "./build/index.cjs.js"
17
+ }
18
+ },
19
+ "files": ["build"],
20
+ "scripts": {
21
+ "clean": "rimraf build",
22
+ "dev": "rollup -c -w",
23
+ "build": "rollup -c",
24
+ "cb": "npm run clean && npm run build",
25
+ "lint": "tsc --noEmit",
26
+ "prepublishOnly": "npm run cb"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/delpikye-v/react-flow.git"
31
+ },
32
+ "homepage": "https://github.com/delpikye-v/react-flow#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/delpikye-v/react-flow/issues"
35
+ },
36
+ "keywords": [
37
+ "async-flow",
38
+ "async-runtime",
39
+ "side-effects",
40
+ "effect-orchestration",
41
+ "cancellation",
42
+ "debounce",
43
+ "throttle",
44
+ "takeLatest",
45
+ "queue",
46
+ "abort-controller",
47
+ "flow-control",
48
+ "headless",
49
+ "framework-agnostic",
50
+ "typescript"
51
+ ],
52
+ "peerDependencies": {
53
+ "react": ">=16.8"
54
+ },
55
+ "devDependencies": {
56
+ "react": "^18.2.0",
57
+ "react-dom": "^18.2.0",
58
+ "@types/react": "^18.2.0",
59
+
60
+ "@rollup/plugin-commonjs": "^25.0.7",
61
+ "@rollup/plugin-node-resolve": "^15.2.3",
62
+ "@rollup/plugin-terser": "^0.4.4",
63
+ "rimraf": "^5.0.5",
64
+ "rollup": "^4.12.0",
65
+ "rollup-plugin-peer-deps-external": "^2.2.4",
66
+ "rollup-plugin-typescript2": "^0.36.0",
67
+ "tslib": "^2.6.2",
68
+ "typescript": "^5.3.3"
69
+ },
70
+ "engines": {
71
+ "node": ">=16.0.0"
72
+ }
73
+ }