├── .editorconfig
├── .gitignore
├── .travis.yml
├── builder.js
├── example
└── index.html
├── license
├── package.json
├── readme.md
├── src
└── index.js
└── test
└── index.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_size = 2
6 | indent_style = tab
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.{json,yml,md}]
13 | indent_style = space
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | *.lock
4 | *.log
5 | dist
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 8
4 |
--------------------------------------------------------------------------------
/builder.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const mkdir = require('mkdirplz');
3 | const pretty = require('pretty-bytes');
4 | const { minify } = require('terser');
5 | const sizer = require('gzip-size');
6 | const pkg = require('./package');
7 |
8 | let data = fs.readFileSync('src/index.js', 'utf8');
9 |
10 | mkdir('dist').then(() => {
11 | // Copy as is for ESM
12 | fs.writeFileSync(pkg.module, data);
13 |
14 | // Mutate exports for CJS
15 | data = data.replace(/export default /, 'module.exports =');
16 | fs.writeFileSync(pkg.main, data);
17 |
18 | // Minify & print gzip-size
19 | let { code } = minify(data, { toplevel:true });
20 | console.log(`> gzip size: ${pretty(sizer.sync(code))}`);
21 |
22 | // Write UMD bundle
23 | let name = pkg['umd:name'] || pkg.name;
24 | let UMD = `!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.${name}=t()}(this,function(){`;
25 | UMD += code.replace(/module.exports=/, 'return ') + '});';
26 | fs.writeFileSync(pkg.unpkg, UMD);
27 | });
28 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Workshy
5 |
6 |
7 |
8 |
9 |
10 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Terkel Gjervig (terkel.com)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "workshy",
3 | "version": "1.0.4",
4 | "repository": "terkelg/workshy",
5 | "description": "A small (354B) lazy function scheduler for a butter smooth main thread",
6 | "unpkg": "dist/workshy.min.js",
7 | "module": "dist/workshy.mjs",
8 | "main": "dist/workshy.js",
9 | "license": "MIT",
10 | "author": {
11 | "name": "Terkel Gjervig",
12 | "email": "terkel@terkel.com",
13 | "url": "https://terkel.com"
14 | },
15 | "engines": {
16 | "node": ">=6"
17 | },
18 | "scripts": {
19 | "build": "node builder",
20 | "pretest": "npm run build",
21 | "prepublishOnly": "npm run build",
22 | "test": "tape test/*.js | tap-spec"
23 | },
24 | "files": [
25 | "dist"
26 | ],
27 | "keywords": [
28 | "scheduler",
29 | "lazy",
30 | "idle",
31 | "thread",
32 | "worker",
33 | "workshy",
34 | "throttle"
35 | ],
36 | "devDependencies": {
37 | "gzip-size": "^5.1.1",
38 | "mkdirplz": "1.0.1",
39 | "pretty-bytes": "^5.3.0",
40 | "tap-spec": "^5.0.0",
41 | "tape": "^4.13.2",
42 | "terser": "^4.6.6"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # workshy [](https://travis-ci.org/terkelg/workshy) [](https://npmjs.com/package/workshy)
2 |
3 | > A small (376B) lazy function scheduler for a butter smooth main thread
4 |
5 | Workshy is a `throttle` utility that **rate limit**, **queue**, and **distribute** function executions over time to prevent the main thread from becoming unresponsive.
6 |
7 | Unlike a standard throttle function, and to ensure non-blocking rendering and responsive UIs, `workshy` break up functions into smaller chunks executed over time if necessary.
8 |
9 | This module is available in three formats:
10 |
11 | * **ES Module**: `dist/workshy.mjs`
12 | * **CommonJS**: `dist/workshy.js`
13 | * **UMD**: `dist/workshy.min.js`
14 |
15 |
16 | ## Install
17 |
18 | ```
19 | $ npm install --save workshy
20 | ```
21 |
22 | The script can also be directly included from [unpkg.com](https://unpkg.com/workshy):
23 | ```html
24 |
25 | ```
26 |
27 |
28 | ## Usage
29 |
30 | ```js
31 | import workshy from 'workshy';
32 |
33 | // dummy function doing heavy work
34 | const greet = () => 'hello world';
35 |
36 | // queue and call function
37 | workshy(greet)();
38 | // => 'hello world'
39 |
40 | // tasks are only called once, but
41 | // multiple calls increases priority
42 | const a = workshy(x => console.log(`A: ${x}`));
43 | const b = workshy(x => console.log(`B: ${x}`));
44 | b(1);
45 | a(1);
46 | a(2);
47 | // => A: 2
48 | // => B: 1
49 |
50 | // manually define priority
51 | const func = workshy(greet, {priority: 2});
52 |
53 | // force it to be called immediately
54 | const func = workshy(greet, {force: true});
55 |
56 | // workshy distribute the work over time to
57 | // make sure the main thread runs butter smooth
58 | for (let i = 0; i < 5000; i++) {
59 | workshy(greet)(); // => this won't block UI
60 | }
61 |
62 | ```
63 |
64 |
65 | ## API
66 |
67 | ### workshy(task, [options])
68 | Returns: `function`
69 |
70 | #### task
71 | Type: `function`
72 |
73 | Accepts any function a returns a `function` (a function that wraps your original function). Call returned function to queue task.
74 |
75 | The returned `function` will execute your function with the latest arguments provided to it as soon as possible based on queue length and prioroty.
76 |
77 | > **Important:** Task are only called _once_.
Calling the same task multiple times increases its priority.
78 |
79 | #### options.priority
80 | Type: `Number`
81 | Default: `0`
82 |
83 | Tasks are sorted by priority. Functions with high priority are called first.
84 |
85 | > **Important:** Priority also increase if a task is called multiple times.
86 |
87 | ```js
88 | workshy(() => console.log('Hello World'), {force: false, priority: 2});
89 | //=> 'Hello World'
90 | ```
91 |
92 | #### options.force
93 | Type: `Boolean`
94 | Default: `false`
95 |
96 | ```js
97 | workshy(() => console.log('Hello World'), {force: false, priority: 2});
98 | //=> 'Hello World'
99 | ```
100 |
101 |
102 | ## Inspiration
103 |
104 | This is inspired by the talk [The Virtue of Laziness: Leveraging Incrementality for Faster Web UI](https://youtu.be/ypPRdtjGooc?t=510)
105 |
106 |
107 | ## License
108 |
109 | MIT © [Terkel Gjervig](https://terkel.com)
110 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const onIdle = window.requestIdleCallback || function (handler) {
2 | const start = Date.now();
3 | return setTimeout(() => handler({ didTimeout: false, timeRemaining: () => Math.max(0, 50 - (Date.now() - start)) }), 1);
4 | };
5 |
6 | function workshy() {
7 | const tasks = [];
8 | let running = false;
9 |
10 | const sort = () => tasks.sort((a, b) => b._priority - a._priority);
11 | const start = () => (onIdle(process, { timeout: 50 }), running = true);
12 |
13 | function process(deadline) {
14 | while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
15 | const fn = tasks.shift();
16 | fn._scheduled = false;
17 | fn(...fn._args);
18 | }
19 | tasks.length > 0 ? start() : running = false;
20 | }
21 |
22 | function run(fn) {
23 | if (!fn._scheduled) {
24 | tasks.push(fn);
25 | fn._scheduled = true;
26 | fn._priority = 0;
27 | } else {
28 | fn._priority++;
29 | sort();
30 | }
31 |
32 | if (tasks.length && !running) start();
33 | }
34 |
35 | return function(fn, {force = false, priority = 0} = {}) {
36 | return (...args) => {
37 | fn._args = args;
38 | if (force) {
39 | fn._scheduled = false;
40 | fn(...args)
41 | } else if (!fn._scheduled) {
42 | tasks.push(fn);
43 | fn._scheduled = true;
44 | fn._priority = priority;
45 | priority !== 0 && sort();
46 | } else {
47 | fn._priority++;
48 | sort()
49 | }
50 |
51 | if (tasks.length && !running) start();
52 | }
53 | }
54 | }
55 |
56 | export default workshy();
57 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | global.window = { requestIdleCallback: false }
2 |
3 | const test = require('tape');
4 | const fn = require('../dist/workshy');
5 |
6 | test('workshy', t => {
7 | t.is(typeof fn, 'function', 'exports a function');
8 | t.is(typeof fn(), 'function', '~> returns function output');
9 | t.end();
10 | });
11 |
--------------------------------------------------------------------------------