├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── defer.ts ├── frameScheduling.ts ├── linkedList.ts └── priorityUniqQueue.ts ├── tests └── frameScheduling.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.log 4 | .DS_Store 5 | coverage 6 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests 2 | coverage -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 8 5 | cache: 6 | yarn: true 7 | after_success: 8 | - npm run build 9 | - npm run cli -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andrey Marchenko 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 | [![Build Status](https://travis-ci.org/Tom910/frame-scheduling.svg?branch=master)](https://travis-ci.org/Tom910/frame-scheduling) 2 | [![Coverage Status](https://coveralls.io/repos/github/Tom910/frame-scheduling/badge.svg?branch=master)](https://coveralls.io/github/Tom910/frame-scheduling?branch=master) 3 | 4 | # Frame Scheduling 5 | A tiny module which allows run a non-blocking layout many tasks. 6 | 7 | * **Fast.** Contains low overhead and optimized for running lots of tasks without drop fps 8 | * **Small.** 930 B (minified and gzipped). No dependencies. It uses [Size Limit](https://github.com/ai/size-limit) to control size. 9 | * **Priority** Separate tasks into different priorities. Try to complete priority tasks as quickly as possible. 10 | * **Isomorphic.** work in browser and node js. 11 | 12 | 13 | ```js 14 | import frameScheduling, { P_IMPORTANT } from 'frame-scheduling'; 15 | 16 | frameScheduling(() => { console.log('async task') }); 17 | ``` 18 | [Demo](https://codesandbox.io/s/admiring-ride-jdoq0) 19 | 20 | Asynchronous running tasks in JavaScript based on requestAnimationFrame. Supports priority and interrupt execution every 16 milliseconds, to achieve 60fps. 21 | 22 | ## Installation 23 | 24 | ```bash 25 | # yarn 26 | yarn add frame-scheduling 27 | 28 | # npm 29 | npm install --save frame-scheduling 30 | ``` 31 | 32 | 33 | ## Priority 34 | ```js 35 | import frameScheduling, { P_IMPORTANT, P_LOW } from 'frame-scheduling'; 36 | const result = []; 37 | 38 | frameScheduling(() => { result.push('A') }, { priority: P_LOW }) 39 | frameScheduling(() => { result.push('B') }) 40 | frameScheduling(() => { result.push('C') }, { priority: P_IMPORTANT }) 41 | frameScheduling(() => { result.push('D') }, { priority: 1000 }) 42 | 43 | // after doing 44 | console.log(result) // > ['D', 'C', 'B', 'A'] 45 | ``` 46 | perform priority tasks first 47 | 48 | ### framing 49 | ```js 50 | import frameScheduling from 'frame-scheduling'; 51 | 52 | frameScheduling(() => lightFunction()) // light < 1ms exec 53 | frameScheduling(() => heavyFunction()) // heavy > 17ms exec 54 | frameScheduling(() => heavyFunction2()) // heavy > 17ms exec 55 | frameScheduling(() => lightFunction2()) // light < 1ms exec 56 | frameScheduling(() => lightFunction3()) // light < 1ms exec 57 | 58 | /* 59 | Runs in frame 60 | | lightFunction 61 | | heavyFunction 62 | | heavyFunction2 63 | | lightFunction2 64 | | lightFunction3 65 | */ 66 | ``` 67 | frame-scheduling aims to achieve 60 fps 68 | 69 | ## Options 70 | #### priority: number = 5 71 | It is possible to set the priority of the function. If the function has a low priority, then each execution skip adds +1 to the priority. Thus, low-priority tasks, when something is done. -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "moduleFileExtensions": [ 3 | "ts", 4 | "js" 5 | ], 6 | "transform": { 7 | "^.+\\.ts$": "ts-jest" 8 | }, 9 | "timers": "fake", 10 | "testMatch": [ 11 | "/tests/**/*.(ts|js)" 12 | ] 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frame-scheduling", 3 | "version": "0.7.1", 4 | "description": "Asynchronous start of functions in JS. Supports priority and interrupt execution every 16 milliseconds, to achieve 60fps.", 5 | "scripts": { 6 | "build": "rollup -c", 7 | "size": "npm run build && size-limit", 8 | "prepublishOnly": "npm run build", 9 | "test": "npm run test:unit && npm run size", 10 | "test:unit": "jest", 11 | "test:coverage": "jest --coverage", 12 | "test:watch": "jest --watch", 13 | "cli": "npm run test:coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 14 | }, 15 | "files": [ 16 | "dist", 17 | "src" 18 | ], 19 | "main": "dist/frameScheduling.js", 20 | "module": "dist/frameScheduling.esm.js", 21 | "es2015": "dist/frameScheduling.esm2015.js", 22 | "types": "dist/frameScheduling.d.ts", 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/Tom910/frame-scheduling.git" 26 | }, 27 | "keywords": [], 28 | "size-limit": [ 29 | { 30 | "path": "dist/frameScheduling.js", 31 | "limit": "930 B" 32 | } 33 | ], 34 | "author": "Andey Marchenko ", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/Tom910/frame-scheduling/issues" 38 | }, 39 | "homepage": "https://github.com/Tom910/frame-scheduling#readme", 40 | "devDependencies": { 41 | "@types/jest": "^24.0.13", 42 | "@types/node": "^12.0.4", 43 | "coveralls": "^3.0.3", 44 | "jest": "^24.8.0", 45 | "prettier": "1.17.1", 46 | "rollup": "^1.13.1", 47 | "rollup-plugin-typescript2": "^0.21.1", 48 | "size-limit": "^1.3.5", 49 | "ts-jest": "^24.0.2", 50 | "tslint": "^5.17.0", 51 | "typescript": "^3.5.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import pkg from './package.json'; 3 | 4 | const input = 'src/frameScheduling.ts'; 5 | 6 | export default [{ 7 | input, 8 | output: [{ 9 | file: pkg.main, 10 | format: 'cjs', 11 | }, 12 | { 13 | file: pkg.module, 14 | format: 'es', 15 | }, 16 | ], 17 | plugins: [ 18 | typescript({ 19 | typescript: require('typescript'), 20 | cacheRoot: './node_modules/.rts2_cache' 21 | }), 22 | ], 23 | }, { 24 | input, 25 | output: [ 26 | { 27 | file: pkg.es2015, 28 | format: 'es', 29 | }, 30 | ], 31 | plugins: [ 32 | typescript({ 33 | typescript: require('typescript'), 34 | tsconfigOverride: { compilerOptions: { "target": "es2015", } }, 35 | cacheRoot: './node_modules/.rts2_cache' 36 | }), 37 | ], 38 | }] -------------------------------------------------------------------------------- /src/defer.ts: -------------------------------------------------------------------------------- 1 | const context = typeof window !== "undefined" ? window : global; 2 | 3 | export type Defer = (f: () => void) => void; 4 | 5 | export let defer: Defer; 6 | if ("requestAnimationFrame" in context) { 7 | defer = requestAnimationFrame; 8 | } else if ("setImmediate" in context) { 9 | defer = (context as any).setImmediate; 10 | } else { 11 | defer = setTimeout; 12 | } 13 | -------------------------------------------------------------------------------- /src/frameScheduling.ts: -------------------------------------------------------------------------------- 1 | import { PriorityUniqQueue } from "./priorityUniqQueue"; 2 | import { LinkedList } from "./linkedList"; 3 | import { defer as defaultDefer, Defer } from "./defer"; 4 | 5 | const TIME_LIFE_FRAME = 16; // 16ms === 60fps 6 | 7 | export const P_LOWER = 1; 8 | export const P_LOW = 3; 9 | export const P_NORMAL = 5; 10 | export const P_HIGH = 7; 11 | export const P_IMPORTANT = 10; 12 | 13 | export const createFrameScheduling = ( 14 | defer: Defer = defaultDefer, 15 | lifeFrame: number = TIME_LIFE_FRAME 16 | ) => { 17 | const heapJobs = new PriorityUniqQueue(); 18 | let deferScheduled = false; 19 | 20 | const runDefer = () => { 21 | if (!deferScheduled) { 22 | deferScheduled = true; 23 | defer(runJobs); 24 | } 25 | }; 26 | 27 | const addJob = (callback: () => void, priority: number) => { 28 | let job = heapJobs.get(priority); 29 | 30 | if (!job) { 31 | job = new LinkedList(); 32 | heapJobs.add(priority, job); 33 | } 34 | 35 | job.push(callback); 36 | }; 37 | 38 | const runJobs = () => { 39 | const timeRun = Date.now(); 40 | 41 | while (true) { 42 | if (heapJobs.isEmpty() || Date.now() - timeRun > lifeFrame) { 43 | break; 44 | } else { 45 | const jobs = heapJobs.peek(); 46 | 47 | try { 48 | (jobs.shift())(); 49 | } catch (e) { 50 | console.error(e); // tslint:disable-line 51 | } 52 | 53 | if (jobs.isEmpty()) { 54 | heapJobs.poll(); 55 | } 56 | } 57 | } 58 | 59 | deferScheduled = false; 60 | 61 | if (!heapJobs.isEmpty()) { 62 | heapJobs.rising(); 63 | 64 | runDefer(); 65 | } 66 | }; 67 | 68 | return function scheduling( 69 | callback: () => void, 70 | { priority = P_NORMAL } = {} 71 | ) { 72 | addJob(callback, priority); 73 | 74 | runDefer(); 75 | }; 76 | }; 77 | 78 | export default createFrameScheduling(); 79 | -------------------------------------------------------------------------------- /src/linkedList.ts: -------------------------------------------------------------------------------- 1 | interface ListNode { 2 | value: () => void; 3 | next: ListNode | null; 4 | } 5 | 6 | export class LinkedList { 7 | private length: number; 8 | private head: ListNode | null; 9 | private last: ListNode | null; 10 | 11 | constructor() { 12 | this.head = null; 13 | this.last = null; 14 | this.length = 0; 15 | } 16 | 17 | public push(value: () => void) { 18 | const node: ListNode = { 19 | next: null, 20 | value 21 | }; 22 | 23 | if (this.length === 0) { 24 | this.head = node; 25 | this.last = node; 26 | } else { 27 | (this.last as ListNode).next = node; 28 | this.last = node; 29 | } 30 | 31 | this.length++; 32 | } 33 | 34 | public shift() { 35 | const currentHead = this.head as ListNode; 36 | const value = currentHead.value; 37 | 38 | this.head = currentHead.next; 39 | this.length--; 40 | 41 | return value; 42 | } 43 | 44 | public isEmpty() { 45 | return this.length === 0; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/priorityUniqQueue.ts: -------------------------------------------------------------------------------- 1 | interface QueueItem { 2 | priority: number; 3 | value: T; 4 | } 5 | 6 | const getParentIndex = (childIndex: number) => Math.floor((childIndex - 1) / 2); 7 | const getRightChildIndex = (parentIndex: number) => 2 * parentIndex + 2; 8 | const hasParent = (childIndex: number) => getParentIndex(childIndex) >= 0; 9 | const getLeftChildIndex = (parentIndex: number) => 2 * parentIndex + 1; 10 | 11 | export class PriorityUniqQueue { 12 | private heapContainer: Array>; 13 | private hashPriority: Record; 14 | 15 | constructor() { 16 | this.heapContainer = []; 17 | this.hashPriority = Object.create(null); 18 | } 19 | 20 | public peek() { 21 | return this.heapContainer[0].value; 22 | } 23 | 24 | public poll() { 25 | let item; 26 | 27 | if (this.heapContainer.length === 1) { 28 | item = this.heapContainer.pop() as QueueItem; 29 | } else { 30 | item = this.heapContainer[0]; 31 | 32 | this.heapContainer[0] = this.heapContainer.pop() as QueueItem; 33 | 34 | this.heapifyDown(); 35 | } 36 | 37 | delete this.hashPriority[item.priority]; 38 | 39 | return item.value; 40 | } 41 | 42 | public add(priority: number, value: T) { 43 | this.heapContainer.push({ priority, value }); 44 | this.heapifyUp(); 45 | this.hashPriority[priority] = value; 46 | } 47 | 48 | public isEmpty() { 49 | return !this.heapContainer.length; 50 | } 51 | 52 | public get(priority: number) { 53 | return this.hashPriority[priority]; 54 | } 55 | 56 | public rising() { 57 | const keys = Object.keys(this.hashPriority); 58 | 59 | for (let i = keys.length; i > 0; i--) { 60 | const key = keys[i - 1]; 61 | 62 | this.hashPriority[Number(key) + 1] = this.hashPriority[key]; 63 | delete this.hashPriority[key]; 64 | } 65 | 66 | for (let j = 0; j < this.heapContainer.length; j++) { 67 | this.heapContainer[j].priority += 1; 68 | } 69 | } 70 | 71 | private heapifyUp(customStartIndex?: number) { 72 | let currentIndex = customStartIndex || this.heapContainer.length - 1; 73 | 74 | while ( 75 | hasParent(currentIndex) && 76 | !this.pairIsInCorrectOrder( 77 | this.heapContainer[getParentIndex(currentIndex)], 78 | this.heapContainer[currentIndex] 79 | ) 80 | ) { 81 | this.swap(currentIndex, getParentIndex(currentIndex)); 82 | currentIndex = getParentIndex(currentIndex); 83 | } 84 | } 85 | 86 | private heapifyDown(customStartIndex?: number) { 87 | let currentIndex = customStartIndex || 0; 88 | let nextIndex = null; 89 | 90 | while (getLeftChildIndex(currentIndex) < this.heapContainer.length) { 91 | if ( 92 | getRightChildIndex(currentIndex) < this.heapContainer.length && 93 | this.pairIsInCorrectOrder( 94 | this.heapContainer[getRightChildIndex(currentIndex)], 95 | this.heapContainer[getLeftChildIndex(currentIndex)] 96 | ) 97 | ) { 98 | nextIndex = getRightChildIndex(currentIndex); 99 | } else { 100 | nextIndex = getLeftChildIndex(currentIndex); 101 | } 102 | 103 | if ( 104 | this.pairIsInCorrectOrder( 105 | this.heapContainer[currentIndex], 106 | this.heapContainer[nextIndex] 107 | ) 108 | ) { 109 | break; 110 | } 111 | 112 | this.swap(currentIndex, nextIndex); 113 | currentIndex = nextIndex; 114 | } 115 | } 116 | 117 | private pairIsInCorrectOrder( 118 | firstElement: QueueItem, 119 | secondElement: QueueItem 120 | ) { 121 | return firstElement.priority >= secondElement.priority; 122 | } 123 | 124 | private swap(indexOne: number, indexTwo: number) { 125 | const tmp = this.heapContainer[indexTwo]; 126 | this.heapContainer[indexTwo] = this.heapContainer[indexOne]; 127 | this.heapContainer[indexOne] = tmp; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/frameScheduling.ts: -------------------------------------------------------------------------------- 1 | const mockDataNow = () => { 2 | let result = 1000; 3 | let count = 0; 4 | 5 | Date.now = () => { 6 | if (count === 3) { 7 | result = 1000; 8 | count = 0; 9 | } 10 | 11 | if (count === 0) { 12 | count++; 13 | return result; 14 | } 15 | 16 | count++; 17 | return (result += 10); 18 | }; 19 | }; 20 | 21 | (global as any).requestAnimationFrame = (fn: () => {}) => setImmediate(fn); 22 | 23 | describe("frameScheduling", () => { 24 | const { default: frameScheduling, P_HIGH, P_LOW, P_NORMAL } = require("../src/frameScheduling"); 25 | const originDateNow = Date.now.bind(Date); 26 | const originConsoleError = console.error; 27 | 28 | beforeEach(() => { 29 | (setImmediate as any).mockClear(); 30 | (setTimeout as any).mockClear(); 31 | }); 32 | 33 | afterEach(() => { 34 | Date.now = originDateNow; 35 | console.error = originConsoleError; 36 | }); 37 | 38 | it("Run multi tasks in 1 frame", () => { 39 | let counter = 0; 40 | 41 | frameScheduling(() => { 42 | counter++; 43 | }); 44 | frameScheduling(() => { 45 | counter++; 46 | }); 47 | frameScheduling(() => { 48 | counter++; 49 | }); 50 | jest.runOnlyPendingTimers(); 51 | 52 | expect(setImmediate).toHaveBeenCalledTimes(1); 53 | expect(counter).toBe(3); 54 | }); 55 | 56 | it("Run scheduling with custom sync defer mode", () => { 57 | const { createFrameScheduling } = require("../src/frameScheduling"); 58 | const frameScheduling = createFrameScheduling((fn: () => void) => fn(), 10000); 59 | let counter = 0; 60 | 61 | frameScheduling(function d1() { 62 | counter++; 63 | }); 64 | frameScheduling(function d2() { 65 | counter++; 66 | }); 67 | frameScheduling(function d3() { 68 | counter++; 69 | }); 70 | 71 | expect(counter).toBe(3); 72 | }); 73 | 74 | it("Run multi tasks in multi frames", () => { 75 | let counter = 0; 76 | mockDataNow(); 77 | 78 | frameScheduling(() => { 79 | counter++; 80 | }); 81 | frameScheduling(() => { 82 | counter++; 83 | }); 84 | frameScheduling(() => { 85 | counter++; 86 | }); 87 | frameScheduling(() => { 88 | counter++; 89 | }); 90 | 91 | jest.runAllTimers(); 92 | 93 | expect(setImmediate).toHaveBeenCalledTimes(4); 94 | expect(counter).toBe(4); 95 | }); 96 | 97 | it("Simple priority", () => { 98 | const result: string[] = []; 99 | 100 | frameScheduling( 101 | () => { 102 | result.push("Vue"); 103 | }, 104 | { priority: P_LOW }, 105 | ); 106 | frameScheduling(() => { 107 | result.push("Angular"); 108 | }); 109 | frameScheduling( 110 | () => { 111 | result.push("Ember"); 112 | }, 113 | { priority: P_LOW }, 114 | ); 115 | frameScheduling( 116 | () => { 117 | result.push("React"); 118 | }, 119 | { priority: P_HIGH }, 120 | ); 121 | 122 | jest.runOnlyPendingTimers(); 123 | 124 | expect(setImmediate).toHaveBeenCalledTimes(1); 125 | expect(result).toEqual(["React", "Angular", "Vue", "Ember"]); 126 | }); 127 | 128 | it("Priority with upfiling iterations", () => { 129 | const result: string[] = []; 130 | mockDataNow(); 131 | 132 | frameScheduling(() => result.push("Bye"), { priority: 0 }); 133 | frameScheduling(() => result.push("A"), { priority: 1 }); 134 | jest.runOnlyPendingTimers(); 135 | frameScheduling(() => result.push("Al"), { priority: 2 }); 136 | jest.runOnlyPendingTimers(); 137 | 138 | frameScheduling(() => result.push("Alo"), { priority: 2 }); 139 | jest.runOnlyPendingTimers(); 140 | 141 | jest.runOnlyPendingTimers(); 142 | expect(result).toEqual(["A", "Al", "Bye", "Alo"]); 143 | }); 144 | 145 | it("Priority with many runs", () => { 146 | let result = 0; 147 | mockDataNow(); 148 | 149 | frameScheduling(() => (result *= 2), { priority: 0 }); 150 | frameScheduling(() => (result *= 3), { priority: 49 }); 151 | 152 | for (let i = 0; i < 100; i++) { 153 | frameScheduling(() => (result += 1), { priority: 90 }); 154 | jest.runOnlyPendingTimers(); 155 | } 156 | 157 | jest.runOnlyPendingTimers(); 158 | jest.runOnlyPendingTimers(); 159 | expect(result).toEqual(354); 160 | }); 161 | 162 | it("Many runs with different priority", () => { 163 | let result = 0; 164 | 165 | frameScheduling(() => (result *= 2), { priority: 0 }); 166 | frameScheduling(() => (result *= 3), { priority: 49 }); 167 | 168 | for (let i = 0; i < 100; i++) { 169 | frameScheduling(() => (result += 1), { priority: i }); 170 | } 171 | 172 | jest.runOnlyPendingTimers(); 173 | expect(result).toEqual(399); 174 | }); 175 | 176 | it("Catching errors", () => { 177 | let result = 0; 178 | 179 | console.error = jest.fn(); 180 | 181 | frameScheduling(() => (result += 2)); 182 | frameScheduling(() => { 183 | throw new Error("Error async"); 184 | }); 185 | frameScheduling(() => (result += 3)); 186 | 187 | jest.runAllTimers(); 188 | 189 | expect(result).toEqual(5); 190 | expect(setImmediate).toHaveBeenCalledTimes(1); 191 | }); 192 | 193 | it("Run different defer modes", () => { 194 | jest.resetModules(); 195 | 196 | const originWindow = (global as any).window; 197 | 198 | delete (global as any).window; 199 | (global as any).requestAnimationFrame = (fn: () => {}) => setTimeout(fn, 0); 200 | 201 | const scheduling = require("../src/frameScheduling").default; 202 | 203 | let result = 0; 204 | 205 | scheduling(() => (result += 2)); 206 | jest.runAllTimers(); 207 | 208 | delete (global as any).requestAnimationFrame; 209 | (global as any).window = originWindow; 210 | 211 | expect(result).toBe(2); 212 | expect(setTimeout).toHaveBeenCalledTimes(1); 213 | }); 214 | 215 | it("Using setTimeoutFallback", () => { 216 | jest.resetModules(); 217 | 218 | const originSetImmediate = global.setImmediate; 219 | 220 | delete global.setImmediate; 221 | 222 | const scheduling = require("../src/frameScheduling").default; 223 | 224 | let result = 0; 225 | 226 | scheduling(() => (result += 3)); 227 | jest.runAllTimers(); 228 | 229 | global.setImmediate = originSetImmediate; 230 | 231 | expect(result).toBe(3); 232 | expect(setTimeout).toHaveBeenCalledTimes(1); 233 | }); 234 | 235 | it("Using setImmediateFallback", () => { 236 | jest.resetModules(); 237 | 238 | const originRequestAnimationFrame = (global as any).requestAnimationFrame; 239 | 240 | delete (global as any).requestAnimationFrame; 241 | 242 | const scheduling = require("../src/frameScheduling").default; 243 | 244 | let result = 0; 245 | 246 | scheduling(() => (result += 3)); 247 | jest.runAllTimers(); 248 | 249 | (global as any).requestAnimationFrame = originRequestAnimationFrame; 250 | 251 | expect(result).toBe(3); 252 | expect(setImmediate).toHaveBeenCalledTimes(1); 253 | }); 254 | 255 | it("starts correctly with different priority nested calls", () => { 256 | const fn = jest.fn(); 257 | 258 | frameScheduling(() => { 259 | frameScheduling(fn, { priority: P_NORMAL }); 260 | }, { priority: P_HIGH }); 261 | jest.runOnlyPendingTimers(); 262 | jest.runOnlyPendingTimers(); 263 | 264 | expect(fn).toBeCalled(); 265 | }); 266 | }); 267 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "ES2015", 5 | "strict": true, 6 | "strictNullChecks": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "noFallthroughCasesInSwitch": true, 9 | "noImplicitAny": true, 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "esModuleInterop": true, 15 | "declaration": true, 16 | "outDir": "./dist", 17 | "lib": ["es2015", "dom"], 18 | "typeRoots": ["./node_modules/@types"] 19 | }, 20 | "include": ["src/**/*"] 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rules": { 4 | "max-classes-per-file": { "severity": "none" }, 5 | "interface-name": [true, "never-prefix"], 6 | "prefer-conditional-expression": [false] 7 | } 8 | } --------------------------------------------------------------------------------