├── .gitignore ├── .npmignore ├── .nvmrc ├── .size-limit ├── .travis.yml ├── LICENSE ├── README.md ├── __tests__ └── index.tsx ├── example ├── app.tsx ├── assets │ └── .gitkeep ├── index.html ├── index.tsx ├── styled.ts └── utils.ts ├── package.json ├── src ├── FlattenPriorityGroup.tsx ├── Promised.tsx ├── Queue.tsx ├── Scheduler.tsx ├── Types.tsx ├── index.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /dist/ 3 | /coverage/ 4 | .DS_Store 5 | .idea 6 | npm-debug.log 7 | yarn-error.log 8 | *.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn.lock 4 | circle.yml 5 | stories/ 6 | _tests/ 7 | src/ 8 | assets/ 9 | __tests__ 10 | example 11 | 12 | .DS_Store 13 | .size-limit 14 | .babelrc 15 | .eslintrc 16 | .npmignore 17 | .gitignore 18 | .storybook/ 19 | 20 | 21 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.5.0 -------------------------------------------------------------------------------- /.size-limit: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | path: "dist/index.js", 4 | limit: "7.5 KB" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | cache: yarn 5 | script: 6 | - yarn 7 | - yarn test:ci && codecov 8 | notifications: 9 | email: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Anton Korzunov 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

React ⏳ Queue

3 |
4 | A declarative scheduler 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | 22 | To schedule events one-after another. To play _lazy_ animations in order, correlated with their position on the page. 23 | 24 | # API 25 | ## Scheduler 26 | - `Scheduler` - task scheduler. Collect tasks and execute them with `stepDelay` between in the `priority` order. 27 | - `stepDelay` - delay between two events 28 | - `[reverse]` - reverses the queue 29 | - `[source]` - priority calculation function 30 | - `[withSideEffect]` - indicates that Scheduler has side effects, and enabled auto update(re-render) on task execution. __Affects performance__. 31 | - `[observe]` - cache buster property. Scheduler sorts queue only on element change, in case of using `source` you might need "inform" 32 | - `[noInitialDelay]` - remove delay from the first task. 33 | - `[disabled]` - disables ticking 34 | it to resort queue. 35 | 36 | 37 | ```js 38 | import {Scheduler} from 'react-queue'; 39 | 40 | 41 | {channel => .... } 42 | 43 | 44 | // use source to create priority based on element position on the page 45 | ref.getBoundingClientRect().top} /> 46 | ``` 47 | `channel` also provides `channel.reset()` function, to clear all `executed` bits, and start everything from the scratch. 48 | 49 | ## Queue 50 | - `Queue` - queued event. It just got executed, nothing more. "When", in "when order" - that is the question. 51 | - `channel` - channel acquired from Scheduler 52 | - `callback` - callback to execute. In case if callback will return a number, or a promise resolving to number, it would be used to _shift_ delay to the next step. 53 | - `priority` - pririty in queue, where 0-s should be executed before 1-s. 54 | - [`shift`] - sub priority change. `shift={-1}` will swap this task with previous sibling. 55 | - [`disabled`] - holds queue execution (sets priority to Infitity). 56 | next tick will be moved by {number}ms. In case of just Promise - next tick will wait to for promise to be resolved. 57 | - [`children`] - any DOM node, Queue will pass as `ref` into scheduler's `source` 58 | 59 | ```js 60 | import {Scheduler, Queue} from 'react-queue'; 61 | 62 | 63 | {channel => 64 | 65 | 66 | // this one will report `ref` to the scheduler 67 | 68 |
42
69 |
70 | 71 | this.setState({x: 1})}> 72 |
1 {x == 1 && "selected!!"}
73 |
74 | 75 | this.setState({x: 2})}> 76 |
2 {x == 2 && "selected!!"}
77 |
78 | 79 | this.setState({x: 3})}> 80 |
3 {x == 3 && "selected!!"}
81 |
82 | } 83 |
84 | ``` 85 | 86 | ## FlattenPriorityGroup 87 | - `FlattenPriorityGroup` - "flattens" all priority changes inside. Could help manage nested tasks. 88 | - `channel` - channel acquired from Scheduler 89 | - [`children`] - render function 90 | - [`priority`] - task priority. Would be set for all nested tasks. 91 | - [`shift`] - sub priority change. `shift={-1}` will swap this task with previous sibling. 92 | - [`disabled`] - holds queue execution (sets priority to Infitity). 93 | 94 | In the next example executing order would be - 2, 1, 4, 3. 95 | ```js 96 | 97 | {channel => ( 98 | 99 | 100 | { pchannel => [ 101 | 1, 102 | 2 103 | ]} 104 | 105 | 106 | { pchannel => [ 107 | 3, 108 | 4 109 | ]} 110 | 111 | 112 | )} 113 | 114 | ``` 115 | 116 | 117 | ## Promised 118 | - `Promised` - promised event. Once it started it should all `done` when it's done. This is a more complex form of queue, with much stronger feedback. 119 | - `channel` - channel acquired from Scheduler 120 | - [`children`] - render function 121 | - [`autoexecuted`] - auto "done" the promised. boolean or number. If number - would be used to shift next step. 122 | - [`priority`] - task priority. Lower goes first 123 | - [`shift`] - sub priority change. `shift={-1}` will swap this task with previous sibling. 124 | - [`disabled`] - holds queue execution (sets priority to Infitity). 125 | ```js 126 | import {Scheduler, Promised} from 'react-queue'; 127 | import {Trigger} from 'recondition'; 128 | 129 | 130 | {channel => 131 | 132 | {({executed, active, done, forwardRed}) => ( 133 |
134 | {executed && "task is done"} 135 | {active && "task is running"} 136 | // don't call anything in render 137 | done(42/* make next step by 42ms later*/)}/> 138 |
139 | ) 140 |
141 | } 142 |
143 | 144 | // this code has the same behavior 145 | 146 | {channel => 147 | 148 | {({executed, active, done, forwardRed}) => ( 149 |
150 | {executed && "task is done"} 151 | {active && "task is running"} 152 |
153 | ) 154 |
155 | } 156 |
157 | ``` 158 | 159 | For example - animation - it will execute one `Promised` after another, and triggering waterfall animation. 160 | ```js 161 | import {Scheduler, Promised} from 'react-queue'; 162 | import {Trigger} from 'recondition'; 163 | 164 | 165 | {channel => 166 | 167 | {({executed, active, fired}) => (
Line1
)} 168 |
169 | 170 | 171 | {({executed, active, fired}) => (
Line2
)} 172 |
173 | 174 | 175 | {({executed, active, fired}) => (
Line3
)} 176 |
177 | 178 | 179 | {({executed, active, fired}) => (
Line4
)} 180 |
181 | } 182 |
183 | ``` 184 | 185 | ## Examples 186 | [react-remock + react-queue](https://codesandbox.io/s/q89q2jm8qw) - simple and complex example - "jquery like" image lazy loading with queued execution. 187 | [react-visibility-sensor + react-queue](https://codesandbox.io/s/6xvr42y6xr) - animate element appearance based on visibility check. 188 | 189 | # Licence 190 | MIT 191 | 192 | 193 | -------------------------------------------------------------------------------- /__tests__/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {mount} from 'enzyme'; 3 | import {Promised, Queue, Scheduler} from "../src"; 4 | 5 | describe('Queue', () => { 6 | it('Ordered Queue', (done) => { 7 | const set: number[] = []; 8 | mount( 9 | { 10 | expect(set).toEqual([1, 2, 3]); 11 | done(); 12 | }}> 13 | {channel => ( 14 |
15 | set.push(1)}/> 16 | set.push(2)}/> 17 | set.push(3)}/> 18 |
19 | )} 20 |
21 | ); 22 | }); 23 | 24 | it('Reverse Ordered Queue', (done) => { 25 | const set: number[] = []; 26 | mount( 27 | { 28 | expect(set).toEqual([3, 2, 1]); 29 | done(); 30 | }}> 31 | {channel => ( 32 |
33 | set.push(1)}/> 34 | set.push(2)}/> 35 | set.push(3)}/> 36 |
37 | )} 38 |
39 | ); 40 | }); 41 | 42 | it('Nested Ordered Queue', (done) => { 43 | const set: number[] = []; 44 | const Render: React.SFC<{ children: () => React.ReactNode }> = ({children}) =>
{children()}
45 | const Q = ({channel}:{channel:any}) => set.push(2)}/> 46 | const QQ = ({channel}:{channel:any}) => ( 47 | 48 | { () => } 49 | 50 | ); 51 | mount( 52 | { 53 | expect(set).toEqual([1, 2, 3]); 54 | done(); 55 | }}> 56 | {channel => ( 57 |
58 |
59 |
60 | set.push(1)}/> 61 |
62 |
63 | 64 | {() => 65 | 66 | } 67 | 68 | set.push(3)}/> 69 |
70 | )} 71 |
72 | ); 73 | }); 74 | 75 | it('P-Ordered Queue', (done) => { 76 | const set: number[] = []; 77 | mount( 78 | { 79 | expect(set).toEqual([1, 3, 2]); 80 | done(); 81 | }}> 82 | {channel => ( 83 |
84 | set.push(1)}/> 85 | set.push(2)}/> 86 | set.push(3)}/> 87 |
88 | )} 89 |
90 | ); 91 | }); 92 | 93 | it('Dynamic Ordered Queue', (done) => { 94 | const set: number[] = []; 95 | mount( 96 | { 100 | expect(set).toEqual([1, 2, 3, 4]); 101 | done(); 102 | }}> 103 | {channel => ( 104 |
105 | set.push(1)}/> 106 | set.push(2)}/> 107 | set.push(3)}/> 108 | set.push(4)}/> 109 |
110 | )} 111 |
112 | ); 113 | }); 114 | 115 | it('No sideEffect Dynamic Ordered Queue', (done) => { 116 | const set: number[] = []; 117 | mount( 118 | { 121 | expect(set).toEqual([1, 4, 3, 2]); 122 | done(); 123 | }}> 124 | {channel => ( 125 |
126 | set.push(1)}/> 127 | set.push(2)}/> 128 | set.push(3)}/> 129 | set.push(4)}/> 130 |
131 | )} 132 |
133 | ); 134 | }); 135 | 136 | it('N-Ordered Queue', (done) => { 137 | const set: number[] = []; 138 | mount( 139 | { 140 | expect(set).toEqual([1, 2, 3]); 141 | done(); 142 | }}> 143 | {channel => ( 144 |
145 | set.push(1)}/> 146 | set.push(2)}/> 147 | set.push(3)}/> 148 |
149 | )} 150 |
151 | ); 152 | }); 153 | }); 154 | 155 | describe('Promised', () => { 156 | it('Ordered Promise', (done) => { 157 | const set: number[] = []; 158 | mount( 159 | { 160 | expect(set).toEqual([1, 2, 3]); 161 | done(); 162 | }}> 163 | {channel => ( 164 |
165 | {({active}) => active && set.push(1)} 166 | {({active}) => active && set.push(2)} 167 | {({active}) => active && set.push(3)} 168 |
169 | )} 170 |
171 | ); 172 | }); 173 | 174 | it('Side-effect Executed Promise', (done) => { 175 | const set: number[] = []; 176 | mount( 177 | { 181 | expect(set).toEqual([1, 1, 2, 1, 2, 3, 1, 2, 3]); 182 | done(); 183 | }}> 184 | {channel => ( 185 |
186 | {({executed}) => executed && set.push(1)} 187 | {({executed}) => executed && set.push(2)} 188 | {({executed}) => executed && set.push(3)} 189 |
190 | )} 191 |
192 | ); 193 | }); 194 | 195 | it('Executed Promise', (done) => { 196 | const set: number[] = []; 197 | mount( 198 | { 199 | expect(set).toEqual([1, 2, 3]); 200 | done(); 201 | }}> 202 | {channel => ( 203 |
204 | {({executed}) => executed && set.push(1)} 205 | {({executed}) => executed && set.push(2)} 206 | {({executed}) => executed && set.push(3)} 207 |
208 | )} 209 |
210 | ); 211 | }); 212 | 213 | it('Pri-Promise', (done) => { 214 | const set: number[] = []; 215 | mount( 216 | { 220 | expect(set).toEqual([1, 3, 2]); 221 | done(); 222 | }}> 223 | {channel => ( 224 |
225 | {({active}) => active && set.push(1)} 226 | {({active}) => active && set.push(2)} 228 | {({active}) => active && set.push(3)} 230 |
231 | )} 232 |
233 | ); 234 | }); 235 | 236 | it('Ordered Mixed Promise', (done) => { 237 | const set: number[] = []; 238 | mount( 239 | { 240 | expect(set).toEqual([1, 1, 2, 2, 3, 3]); 241 | done(); 242 | }}> 243 | {channel => ( 244 |
245 | {({fired}) => fired && set.push(1)} 246 | {({fired}) => fired && set.push(2)} 247 | {({fired}) => fired && set.push(3)} 248 |
249 | )} 250 |
251 | ); 252 | }); 253 | 254 | it('sideEffect Ordered Mixed Promise', (done) => { 255 | const set: number[] = []; 256 | mount( 257 | { 261 | expect(set).toEqual([1, 1, 1, 2, 2, 1, 2, 3, 3, 1, 2, 3]); 262 | done(); 263 | }}> 264 | {channel => ( 265 |
266 | {({fired}) => fired && set.push(1)} 267 | {({fired}) => fired && set.push(2)} 268 | {({fired}) => fired && set.push(3)} 269 |
270 | )} 271 |
272 | ); 273 | }); 274 | 275 | it('Call Promise', (done) => { 276 | const set: number[] = []; 277 | mount( 278 | { 279 | expect(set).toEqual([1, 2, 3]); 280 | done(); 281 | }}> 282 | {channel => ( 283 |
284 | {({active, done}) => active && done() && set.push(1)} 285 | {({active, done}) => active && done() && set.push(2)} 286 | {({active, done}) => active && done() && set.push(3)} 287 |
288 | )} 289 |
290 | ); 291 | }); 292 | 293 | it('Call nested Promise', (done) => { 294 | const set: number[] = []; 295 | const Render: React.SFC<{ children: () => React.ReactNode }> = ({children}) =>
{children()}
296 | const Q = ({channel}:{channel:any}) => {({active, done}) => active && done() && set.push(2)} 297 | const QQ = ({channel}:{channel:any}) => ( 298 | 299 | { () => } 300 | 301 | ); 302 | mount( 303 | { 304 | expect(set).toEqual([1, 2, 3]); 305 | done(); 306 | }}> 307 | {channel => ( 308 |
309 | {({active, done}) => active && done() && set.push(1)} 310 | 311 | {({active, done}) => active && done() && set.push(3)} 312 |
313 | )} 314 |
315 | ); 316 | }); 317 | }); 318 | 319 | describe("Observer Q", () => { 320 | it('div', (done) => { 321 | const set: number[] = []; 322 | mount( 323 | { 326 | expect(set).toEqual([2, 1, 3]); 327 | done(); 328 | }} 329 | source={({ref}) => +(ref!.getAttribute('data-p') || 0)} 330 | > 331 | {channel => ( 332 |
333 | set.push(1)}> 334 |
335 |
336 | 337 | 338 | set.push(3)}> 339 |
340 |
341 | {({active, forwardRef}) => { 342 | active && set.push(2) 343 | return
344 | }} 345 | 346 |
347 | )} 348 | 349 | ); 350 | }) 351 | }) 352 | 353 | -------------------------------------------------------------------------------- /example/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Component} from 'react'; 3 | import {AppWrapper} from './styled'; 4 | import {Scheduler} from "../src/Scheduler"; 5 | import {FlattenPriorityGroup, Promised, Queue} from "../src"; 6 | 7 | export interface AppState { 8 | x: number 9 | } 10 | 11 | const AppN0 = (function() { 12 | const Render: React.SFC<{ children: () => React.ReactNode }> = ({children}) =>
{children()}
13 | 14 | return class AppN0 extends Component <{}, AppState> { 15 | state: AppState = { 16 | x: 0 17 | } 18 | 19 | render() { 20 | const {x} = this.state; 21 | return ( 22 | 23 | --{this.state.x}-- 24 | 25 | {channel => ( 26 |
27 | {() => 28 | {() => 29 | {({active}) => { 30 | active && x !== 1 && this.setState({x: 1}); 31 | return "1" 32 | }} 33 | } 34 | } 35 | { ({active}) => { active && x!==2 && console.log('P1') && this.setState({x:2}); return "2"}} 36 | { ({active}) => { active && x!==3 && console.log('P2')&& this.setState({x:3})&& console.log('P2');; return "3"}} 37 | { ({active}) => { active && x!==4 && console.log('P3')&& this.setState({x:4})&& console.log('P3');; return "4"}} 38 | { ({active}) => { active && console.log('P0'); return "3"}} 39 | 40 | { 41 | pchannel => ( 42 | 43 | { ({active}) => { active && console.log('P11'); return "1"}} 44 | { ({active}) => { active && console.log('P12'); return "2"}} 45 | { ({active}) => { active && console.log('P13'); return "3"}} 46 | { ({active}) => { active && console.log('P00'); return "0"}} 47 | 48 | )} 49 | 50 |
51 | )} 52 |
53 | Example! 54 |
55 | ) 56 | } 57 | } 58 | })(); 59 | 60 | class App0 extends Component <{}, AppState> { 61 | state: AppState = { 62 | x: 0 63 | } 64 | 65 | render() { 66 | return ( 67 | 68 | --{this.state.x}-- 69 | 70 | {channel => ( 71 |
72 | this.setState({x: 1})}/> 73 | this.setState({x: 2})}/> 74 | this.setState({x: 1})}/> 75 |
76 | )} 77 |
78 | Example! 79 |
80 | ) 81 | } 82 | } 83 | 84 | class App1 extends Component <{}, AppState> { 85 | state: AppState = { 86 | x: 0 87 | } 88 | 89 | render() { 90 | return ( 91 | 92 | --{this.state.x}-- 93 | 94 | {channel => ( 95 |
96 | this.setState({x: 1})}/> 97 | this.setState({x: 2})}/> 98 | this.setState({x: 3})}/> 99 |
100 | )} 101 |
102 | Example! 103 |
104 | ) 105 | } 106 | } 107 | 108 | class App1_1 extends Component <{}, AppState> { 109 | state: AppState = { 110 | x: 0 111 | } 112 | 113 | render() { 114 | const {x} = this.state; 115 | return ( 116 | 117 | --{this.state.x}-- 118 | 119 | {channel => ( 120 |
121 | {x == 0 && this.setState({x: 1})}/>} 122 | {x == 1 && this.setState({x: 2})}/>} 123 | {x == 2 && this.setState({x: 3})}/>} 124 |
125 | )} 126 |
127 | Example! 128 |
129 | ) 130 | } 131 | } 132 | 133 | class App2 extends Component <{}, AppState> { 134 | state: AppState = { 135 | x: 0 136 | } 137 | 138 | render() { 139 | return ( 140 | 141 | --{this.state.x}-- 142 | 143 | {channel => ( 144 |
145 | this.setState({x: 1})}/> 146 | this.setState({x: 2})}/> 147 | this.setState({x: 3})}/> 148 |
149 | )} 150 |
151 | Example! 152 |
153 | ) 154 | } 155 | } 156 | 157 | class App2_1 extends Component <{}, AppState> { 158 | state: AppState = { 159 | x: 0 160 | } 161 | 162 | render() { 163 | const {x} = this.state; 164 | return ( 165 | 166 | --{this.state.x}-- 167 | 168 | {channel => ( 169 |
170 | this.setState({x: 1})}/> 171 | this.setState({x: 2})}/> 172 | { 173 | this.setState({x: 3}); 174 | return -900 175 | }}/> 176 | 177 | this.setState({x: 0}, channel.reset)}/> 178 |
179 | )} 180 |
181 | Example! 182 |
183 | ) 184 | } 185 | } 186 | 187 | class App3 extends Component <{}, AppState> { 188 | state: AppState = { 189 | x: 0 190 | } 191 | 192 | render() { 193 | const {x} = this.state; 194 | return ( 195 | 196 | --{this.state.x}-- 197 | ref ? ref.getBoundingClientRect().top : 1000}> 198 | {channel => ( 199 |
200 | this.setState({x: 1})}> 201 |
1 {x == 1 && "selected!!"}
202 |
203 | 204 | this.setState({x: 2})}> 205 |
2 {x == 2 && "selected!!"}
206 |
207 | 208 | this.setState({x: 3})}> 209 |
3 {x == 3 && "selected!!"}
210 |
211 | 212 | 213 | this.setState({x: 0}, channel.reset)}/> 214 |
215 | )} 216 |
217 | Example! 218 |
219 | ) 220 | } 221 | } 222 | 223 | class App4 extends Component <{}, AppState> { 224 | state: AppState = { 225 | x: 0 226 | }; 227 | 228 | render() { 229 | 230 | return ( 231 | 232 | 233 | 234 | {channel => ( 235 |
236 | 237 | 238 | {({active, executed, done}) => ( 239 |
240 | {executed && "task done"} 241 | {active && "task is running"} 242 | 243 |
244 | )} 245 |
246 | 247 | {({active, executed, done}) => ( 248 |
249 | {executed && "task done"} 250 | {active && "task is running"} 251 | 252 |
253 | )} 254 |
255 | 256 | {({active, executed, done}) => ( 257 |
258 | {executed && "task done"} 259 | {active && "task is running"} 260 | 261 |
262 | )} 263 |
264 | 265 |
266 | )} 267 |
268 | Example! 269 |
270 | ) 271 | } 272 | } 273 | 274 | const App = () => ( 275 |
276 | 277 | {/**/} 278 | {/**/} 279 | 280 | {/**/} 281 | {/**/} 282 | {/**/} 283 | {/**/} 284 | {/**/} 285 | 286 |
287 | ) 288 | 289 | export default App; -------------------------------------------------------------------------------- /example/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theKashey/react-queue/375494846d62bfcca8cb963cf60f05e7f738fc23/example/assets/.gitkeep -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | 5 | ReactDOM.render(, document.getElementById('app')); 6 | -------------------------------------------------------------------------------- /example/styled.ts: -------------------------------------------------------------------------------- 1 | import styled, {injectGlobal} from 'styled-components'; 2 | 3 | injectGlobal` 4 | body { 5 | font-family: Helvetica; 6 | background-color: #D8D1F5; 7 | } 8 | 9 | * { 10 | box-sizing: content-box; 11 | } 12 | 13 | .github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}} 14 | 15 | .github-corner svg { 16 | fill:#64CEAA; color:#fff; position: absolute; top: 0; border: 0; right: 0; 17 | } 18 | `; 19 | 20 | export const AppWrapper = styled.div` 21 | 22 | `; -------------------------------------------------------------------------------- /example/utils.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'react'; 2 | 3 | export class ToolboxApp extends Component { 4 | onCheckboxChange = (propName: any) => () => { 5 | const currentValue = (this.state as any)[propName]; 6 | this.setState({ [propName]: !currentValue } as any); 7 | } 8 | 9 | onFieldTextChange = (propName: any) => (e: any) => { 10 | const value = e.target.value; 11 | 12 | (this as any).setState({ 13 | [propName]: value 14 | }); 15 | } 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-queue", 3 | "version": "0.6.1", 4 | "description": "Declarative task scheduler", 5 | "sideEffects": false, 6 | "scripts": { 7 | "test": "ts-react-toolbox test", 8 | "bootstrap": "ts-react-toolbox init", 9 | "dev": "ts-react-toolbox dev", 10 | "test:ci": "ts-react-toolbox test --runInBand --coverage", 11 | "build": "ts-react-toolbox build", 12 | "prepublish": "ts-react-toolbox build", 13 | "release": "ts-react-toolbox release", 14 | "lint": "ts-react-toolbox lint", 15 | "static": "ts-react-toolbox publish", 16 | "format": "ts-react-toolbox format", 17 | "analyze": "ts-react-toolbox analyze" 18 | }, 19 | "repository": "https://github.com/theKashey/react-queue/", 20 | "keywords": [ 21 | "queue", 22 | "react", 23 | "task", 24 | "scheduler" 25 | ], 26 | "author": "theKashey ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/theKashey/react-queue/issues" 30 | }, 31 | "devDependencies": { 32 | "codecov": "^3.0.4", 33 | "ts-react-toolbox": "^0.1.21" 34 | }, 35 | "engines": { 36 | "node": ">=8.5.0" 37 | }, 38 | "peerDependencies": { 39 | "react": "^16.3.0" 40 | }, 41 | "types": "dist/es5/index.d.ts", 42 | "main": "dist/es5/index.js", 43 | "files": [ 44 | "dist" 45 | ], 46 | "jsnext:main": "dist/es2015/index.js", 47 | "module": "dist/es2015/index.js" 48 | } 49 | -------------------------------------------------------------------------------- /src/FlattenPriorityGroup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {CB, IChannel, IGroupProps, IPriority} from "./Types"; 3 | import {diff} from "./utils"; 4 | 5 | export class FlattenPriorityGroup extends React.Component { 6 | 7 | private queue: any[] = []; 8 | 9 | componentDidUpdate(oldProps: IGroupProps) { 10 | if (oldProps.disabled && !this.props.disabled) { 11 | this.update(); 12 | } 13 | } 14 | 15 | add = (cb: CB, {priority = 0xFFFFFF, shift = 0}: IPriority, ref: any) => { 16 | const q: any = { 17 | cb, 18 | ref, 19 | priority, 20 | shift, 21 | index: this.queue.length, 22 | }; 23 | q.sortIndex = q.index + q.shift; 24 | this.queue.push(q); 25 | this.update(); 26 | return q; 27 | }; 28 | 29 | remove = (q: any) => { 30 | const index = this.queue.indexOf(q); 31 | if (index > 0) { 32 | this.queue.splice(index, 1); 33 | this.props.channel.remove(q.q); 34 | } 35 | }; 36 | 37 | replace = (q: any, cb: CB, {priority = 0xFFFFFF, shift = 0}: IPriority, ref: any) => { 38 | if (q) { 39 | const changedPriority = q.priority !== priority || q.shift !== q.shift; 40 | const changedRef = q.ref !== ref; 41 | q.cb = cb; 42 | q.priority = priority; 43 | q.shift = shift; 44 | q.sortIndex = q.index + q.shift; 45 | q.ref = ref; 46 | if (changedPriority || changedRef) { 47 | q.update = true; 48 | this.update(); 49 | } 50 | } 51 | return q; 52 | }; 53 | 54 | reset = () => { 55 | this.props.channel.reset(); 56 | }; 57 | 58 | schedule = (ref: any, cb: any) => { 59 | this.props.channel.schedule(ref, cb); 60 | }; 61 | 62 | channel: IChannel = { 63 | add: this.add, 64 | remove: this.remove, 65 | replace: this.replace, 66 | reset: this.reset, 67 | schedule: this.schedule, 68 | }; 69 | 70 | update() { 71 | if (this.props.disabled) { 72 | return; 73 | } 74 | const {channel, shift = 0, priority: blockPriority} = this.props; 75 | channel.schedule(this, () => { 76 | this.sortQ(); 77 | this.queue.forEach((q, index) => { 78 | if (q.index0 !== index || q.update || !q.q) { 79 | const {cb, index0, ref, priority: qPriority} = q; 80 | const priority = qPriority < Infinity ? blockPriority : qPriority; 81 | q.update = false; 82 | if (q.q) { 83 | channel.replace(q.q, cb, {priority, shift: index0 - index + shift}, ref); 84 | } else { 85 | q.q = channel.add(q.cb, {priority, shift: shift}, ref); 86 | q.index0 = index; 87 | } 88 | } 89 | }); 90 | }); 91 | } 92 | 93 | sortQ() { 94 | if (this.props.source) { 95 | this.queue.forEach(q => q.sortOrder = q.priority < Infinity ? this.props.source!(q) : q.priority); 96 | } else { 97 | this.queue.forEach(q => q.sortOrder = q.priority); 98 | } 99 | if (this.props.reverse) { 100 | this.queue.sort((a, b) => diff(b, a)) 101 | } else { 102 | this.queue.sort((a, b) => diff(a, b)) 103 | } 104 | } 105 | 106 | render() { 107 | return this.props.children(this.channel); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Promised.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {IPromisedProps} from "./Types"; 3 | 4 | interface Deffered { 5 | p: Promise; 6 | resolve: (a: any) => any; 7 | reject: (a: any) => any; 8 | } 9 | 10 | interface PromisedState { 11 | active: boolean, 12 | executed: boolean, 13 | }; 14 | 15 | const deffered = () => { 16 | const d: Deffered = {} as any; 17 | d.p = new Promise((resolve, reject) => { 18 | d.resolve = resolve; 19 | d.reject = reject; 20 | }); 21 | return d; 22 | }; 23 | 24 | export class Promised extends React.Component { 25 | state = { 26 | active: false, 27 | executed: false 28 | }; 29 | 30 | private q: any; 31 | private ref: any; 32 | 33 | private promise = deffered(); 34 | 35 | componentDidMount() { 36 | const {channel, priority = 0xFFFFFF, shift = 0, disabled} = this.props; 37 | if (!channel) { 38 | throw new Error('Queue: please provide a channel props'); 39 | } 40 | this.q = channel.add( 41 | this.callback, { 42 | priority: disabled ? Infinity : (priority || 0), 43 | shift: shift 44 | }, 45 | this.ref); 46 | } 47 | 48 | componentWillUnmount() { 49 | const {channel} = this.props; 50 | channel.remove(this.q); 51 | } 52 | 53 | componentDidUpdate() { 54 | const {channel, priority = 0xFFFFFF, shift = 0, disabled} = this.props; 55 | channel.replace( 56 | this.q, 57 | this.callback, { 58 | priority: disabled ? Infinity : (priority || 0), 59 | shift: shift 60 | }, 61 | this.ref 62 | ); 63 | } 64 | 65 | callback = () => { 66 | this.setState({ 67 | active: true 68 | }); 69 | const {autoexecuted} = this.props; 70 | if (autoexecuted) { 71 | this.fulfill(autoexecuted); 72 | } 73 | return this.promise.p; 74 | }; 75 | 76 | forwardRef = (ref: any) => { 77 | this.ref = ref; 78 | this.componentDidUpdate(); 79 | }; 80 | 81 | fulfill = (a: any) => { 82 | if (this.state.active) { 83 | this.setState({ 84 | executed: true, 85 | active: false, 86 | }); 87 | this.promise.resolve(a); 88 | } else if (this.state.executed) { 89 | console.error('react-queue: trying to finish finished Promised') 90 | return false; 91 | } else { 92 | console.error('react-queue: trying to finish unstarted Promised') 93 | return false; 94 | } 95 | return true; 96 | }; 97 | 98 | render() { 99 | const {active, executed} = this.state; 100 | return this.props.children({ 101 | active, 102 | executed, 103 | fired: active || executed, 104 | done: this.fulfill, 105 | forwardRef: this.forwardRef 106 | }) || null 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Queue.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {IQueueProps} from "./Types"; 3 | 4 | export class Queue extends React.Component { 5 | private q: any; 6 | private ref: any; 7 | 8 | componentDidMount() { 9 | const {channel, callback, priority = 0xFFFFFF, shift = 0, disabled} = this.props; 10 | if (!channel) { 11 | throw new Error('Queue: please provide a channel props'); 12 | } 13 | this.q = channel.add( 14 | callback, { 15 | priority: disabled ? Infinity : (priority || 0), 16 | shift: shift 17 | }, this.ref); 18 | } 19 | 20 | componentWillUnmount() { 21 | const {channel} = this.props; 22 | channel.remove(this.q); 23 | } 24 | 25 | componentDidUpdate() { 26 | const {channel, callback, priority = 0xFFFFFF, shift = 0, disabled} = this.props; 27 | channel.replace( 28 | this.q, 29 | callback, { 30 | priority: disabled ? Infinity : (priority || 0), 31 | shift: shift 32 | }, 33 | this.ref 34 | ); 35 | } 36 | 37 | setRef = (ref: any) => this.ref = ref; 38 | 39 | render() { 40 | return this.props.children 41 | ? React.cloneElement(React.Children.only(this.props.children), {ref: this.setRef}) 42 | : null; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Scheduler.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {CB, IChannel, IPriority, ISchedulerProps, Q} from "./Types"; 3 | import {diff} from "./utils"; 4 | 5 | 6 | export class Scheduler extends React.Component { 11 | 12 | private queue: Q[] = []; 13 | private timeout: number = 0; 14 | private sortOnQ: boolean = false; 15 | private scheduledCallbacks: Map = new Map(); 16 | 17 | state = { 18 | recalculate: 0, 19 | queue: this.queue, 20 | current: {ref: null as any} 21 | }; 22 | 23 | componentWillUnmount() { 24 | window.clearTimeout(this.timeout); 25 | } 26 | 27 | componentDidUpdate(props: ISchedulerProps) { 28 | if ( 29 | this.props.observe !== props.observe || 30 | this.props.reverse !== props.reverse || 31 | this.props.disabled !== props.disabled 32 | ) { 33 | this.update(); 34 | } 35 | } 36 | 37 | update() { 38 | this.sortOnQ = true; 39 | this.scheduleQ( 40 | this.props.noInitialDelay && this.noExecutedTask() 41 | ? 0 42 | : this.props.stepDelay 43 | ); 44 | } 45 | 46 | scheduleQ(when: number) { 47 | if (!this.timeout && !this.props.disabled) { 48 | this.timeout = window.setTimeout(() => { 49 | this.timeout = 0; 50 | const cbs = this.scheduledCallbacks; 51 | this.scheduledCallbacks = new Map(); 52 | cbs.forEach(cb => cb()); 53 | 54 | if (this.sortOnQ) { 55 | this.sortOnQ = false; 56 | this.sortQ(); 57 | } 58 | this.executeQ(); 59 | }, when) 60 | } 61 | } 62 | 63 | add = (cb: CB, {priority = 0xFFFFFF, shift = 0}: IPriority, ref: any) => { 64 | const q: Q = { 65 | cb, 66 | priority, 67 | shift, 68 | index: this.queue.length, 69 | sortIndex: 0, 70 | sortOrder: priority, 71 | ref, 72 | executed: false 73 | }; 74 | q.sortIndex = q.index + q.shift; 75 | this.queue.push(q); 76 | this.update(); 77 | return q; 78 | }; 79 | 80 | remove = (q: any) => { 81 | const index = this.queue.indexOf(q); 82 | if (index > 0) { 83 | this.queue.splice(index, 1); 84 | } 85 | }; 86 | 87 | replace = (q: any, cb: CB, {priority = 0, shift = 0}: IPriority, ref: any) => { 88 | if (q) { 89 | const changedPriority = q.priority !== priority || q.shift !== shift; 90 | const changedRef = q.ref !== ref; 91 | q.cb = cb; 92 | q.priority = priority; 93 | q.shift = shift; 94 | q.sortIndex = q.index + q.shift; 95 | q.ref = ref; 96 | if (changedPriority || changedRef) { 97 | this.update(); 98 | } 99 | } 100 | return q; 101 | }; 102 | 103 | reset = () => { 104 | this.queue.forEach(q => q.executed = false); 105 | this.update(); 106 | }; 107 | 108 | schedule = (ref: any, cb: CB) => { 109 | this.scheduledCallbacks.set(ref, cb); 110 | this.update(); 111 | }; 112 | 113 | channel: IChannel = { 114 | add: this.add, 115 | remove: this.remove, 116 | replace: this.replace, 117 | reset: this.reset, 118 | schedule: this.schedule 119 | }; 120 | 121 | sortQ() { 122 | if (this.props.source) { 123 | this.queue.forEach(q => q.sortOrder = q.priority < Infinity ? this.props.source!(q) : q.priority); 124 | } else { 125 | this.queue.forEach(q => q.sortOrder = q.priority); 126 | } 127 | if (this.props.reverse) { 128 | this.queue.sort((a, b) => diff(b, a)) 129 | } else { 130 | this.queue.sort((a, b) => diff(a, b)) 131 | } 132 | } 133 | 134 | nextQ() { 135 | const left = this.queue.filter( 136 | ({executed, priority, sortOrder}) => !executed && sortOrder < Infinity && priority < Infinity 137 | ); 138 | return left[0] 139 | } 140 | 141 | executeQ() { 142 | const q = this.nextQ(); 143 | this.state.current.ref = { ...(q as any) }; 144 | if (q) { 145 | q.executed = true; 146 | Promise.resolve(q.cb()) 147 | .then(result => { 148 | const dt = (typeof result === "number" ? result : 0) || 0; 149 | const when = Math.max(0, this.props.stepDelay + dt); 150 | this.scheduleQ(when); 151 | if (this.props.withSideEffect) { 152 | this.setState({ 153 | recalculate: 1 154 | }) 155 | } 156 | }) 157 | } else { 158 | this.props.onEmptyQueue && this.props.onEmptyQueue() 159 | } 160 | } 161 | 162 | noExecutedTask() { 163 | return !this.queue.find(x => x.executed); 164 | } 165 | 166 | render() { 167 | return this.props.children(this.channel); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Types.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export type CB = () => Promise | any; 4 | 5 | export interface IPriority { 6 | priority?: number; 7 | shift?: number, 8 | } 9 | 10 | export type IChannel = { 11 | add: (cb: CB, priority: IPriority, ref: any) => any; 12 | remove: (cb: CB) => void; 13 | replace: (q: any, cb: CB, priority: IPriority, ref: any) => any; 14 | reset: () => any; 15 | schedule: (ref: any, cb: () => any) => any; 16 | } 17 | 18 | export type IPromisedCallback = { 19 | executed: boolean; 20 | active: boolean; 21 | fired: boolean; 22 | done: (timeShift?: number) => boolean; 23 | forwardRef: React.Ref; 24 | }; 25 | 26 | export interface IQueueProps extends IPriority { 27 | channel: IChannel; 28 | callback: CB; 29 | children?: React.ReactElement; 30 | disabled?: boolean; 31 | } 32 | 33 | export interface IPromisedProps extends IPriority { 34 | channel: IChannel; 35 | disabled?: boolean; 36 | autoexecuted?: boolean | number; 37 | 38 | children: (props: IPromisedCallback) => React.ReactNode; 39 | } 40 | 41 | export interface Q { 42 | cb: CB; 43 | index: number; 44 | priority: number; 45 | shift: number; 46 | sortIndex: number; 47 | sortOrder: number; 48 | ref?: HTMLElement; 49 | executed: boolean; 50 | } 51 | 52 | export interface IGroupProps extends IPriority{ 53 | channel: IChannel; 54 | disabled?: boolean; 55 | source?: (q: Q) => number; 56 | reverse?: boolean; 57 | children: (channel: IChannel) => React.ReactNode; 58 | } 59 | 60 | export interface ISchedulerProps { 61 | children: (channel: IChannel) => React.ReactNode; 62 | stepDelay: number; 63 | reverse?: boolean; 64 | observe?: string | number | boolean; 65 | withSideEffect?: boolean; 66 | source?: (q: Q) => number; 67 | onEmptyQueue?: () => any; 68 | noInitialDelay?: boolean; 69 | disabled?: boolean; 70 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {Scheduler} from "./Scheduler"; 2 | import {Queue} from "./Queue"; 3 | import {Promised} from "./Promised"; 4 | import {FlattenPriorityGroup} from "./FlattenPriorityGroup"; 5 | 6 | export { 7 | Scheduler, 8 | 9 | FlattenPriorityGroup, 10 | 11 | Queue, 12 | Promised 13 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import {Q} from "./Types"; 2 | 3 | export const diff = (a: Q, b: Q): number => { 4 | return ( 5 | (a.sortOrder - b.sortOrder) || 6 | (a.sortIndex - b.sortIndex) || 7 | (a.index - b.index) 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "importHelpers": true, 4 | "strict": true, 5 | "strictNullChecks": true, 6 | "strictFunctionTypes": true, 7 | "noImplicitThis": true, 8 | "alwaysStrict": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": true, 14 | "removeComments": true, 15 | "target": "es5", 16 | "lib": [ 17 | "dom", 18 | "es5", 19 | "es6", 20 | "scripthost", 21 | "es2015", 22 | "es2015.collection", 23 | "es2015.symbol", 24 | "es2015.iterable", 25 | "es2015.promise", 26 | "es2016" 27 | ], 28 | "jsx": "react" 29 | }, 30 | } --------------------------------------------------------------------------------