首次提交

This commit is contained in:
2026-07-13 12:06:16 +08:00
commit e3c810b1a6
1690 changed files with 228324 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
name: Tests
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x, 20.x]
fail-fast: false
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: npm install
- name: Lint
if: matrix.node-version == '20.x'
run: |
npx standard
- name: Test
run: |
npm test
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Matteo Collina
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
+61
View File
@@ -0,0 +1,61 @@
# retimer  [![Build Status](https://travis-ci.org/mcollina/retimer.png)](https://travis-ci.org/mcollina/retimer)
reschedulable setTimeout for your node needs. This library is built for
building a keep alive functionality across a large numbers of
clients/sockets.
Rescheduling a 10000 functions 20 times with an interval of 50ms (see
`bench.js`), with 100 repetitions:
* `benchSetTimeout*100: 40.295s`
* `benchRetimer*100: 36.122s`
## Install
```
npm install retimer --save
```
## Example
```js
var retimer = require('retimer')
var timer = retimer(function () {
throw new Error('this should never get called!')
}, 20)
setTimeout(function () {
timer.reschedule(50)
setTimeout(function () {
timer.clear()
}, 10)
}, 10)
```
## API
### retimer(callback, timeout, [...args])
Exactly like your beloved `setTimeout`.
Returns a `Retimer object`
### timer.reschedule(timeout)
Reschedule the timer.
Retimer will not gove any performance benefit if the specified timeout comes __before__ the original timeout.
### timer.clear()
Clear the timer, like your beloved `clearTimeout`.
## How it works
Timers are stored in a Linked List in node.js, if you create a lot of
timers this Linked List becomes massive which makes __removing a timer an expensive operation__.
Retimer let the old timer run at its time, and schedule a new one accordingly, when the new one is __after__ the original timeout.
There is no performance gain when the new timeout is before the original one as retimer will just __remove the previous timer__.
## License
MIT
+67
View File
@@ -0,0 +1,67 @@
'use strict'
/* eslint-disable no-var */
const bench = require('fastbench')
const retimer = require('./')
const max = 10000
function benchSetTimeout (done) {
const timers = new Array(max)
let completed = 0
let toReschedule = 20
schedule()
function complete () {
if (++completed === max) {
done()
}
}
function schedule () {
for (var i = 0; i < max; i++) {
if (timers[i]) {
clearTimeout(timers[i])
}
timers[i] = setTimeout(complete, 50)
}
if (--toReschedule > 0) {
setTimeout(schedule, 10)
}
}
}
function benchRetimer (done) {
const timers = new Array(max)
let completed = 0
let toReschedule = 20
schedule()
function complete () {
if (++completed === max) {
done()
}
}
function schedule () {
for (var i = 0; i < max; i++) {
if (timers[i]) {
timers[i].reschedule(50)
} else {
timers[i] = retimer(complete, 50)
}
}
if (--toReschedule > 0) {
setTimeout(schedule, 10)
}
}
}
const run = bench([
benchSetTimeout,
benchRetimer
], 100)
run(run)
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Christoph Guttandin
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
@@ -0,0 +1,51 @@
# fast-unique-numbers
**A module to create a set of unique numbers as fast as possible.**
[![version](https://img.shields.io/npm/v/fast-unique-numbers.svg?style=flat-square)](https://www.npmjs.com/package/fast-unique-numbers)
This module is meant to create unique numbers within a given [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) or [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set). To achieve that as fast as possible the resulting set of numbers will only contain integers. Additionally only small integers will be used for as long as possible. Small integers can be stored more efficiently by JavaScript engines like [SpiderMonkey](https://spidermonkey.dev/) or [V8](https://v8.dev).
To verify the expected perfomance benefit an expectation test is used to make sure small integers do actually perform better in Chromium based browsers, Firefox and when using Node.js.
## Usage
This module is available on [npm](https://www.npmjs.com/package/fast-unique-numbers) and can be
installed by running the following command:
```shell
npm install fast-unique-numbers
```
This module exports two functions.
### addUniqueNumber()
This function takes a `Set` of numbers as argument and appends a new unique number to it. It also returns that number.
```js
import { addUniqueNumber } from 'fast-unique-numbers';
const set = new Set([1, 4, 8]);
const uniqueNumber = addUniqueNumber(set);
console.log(uniqueNumber); // 3
console.log(set); // Set(4) { 1, 4, 8, 3 }
```
### generateUniqueNumber()
This function can be used to generate a unique number which is not yet present in the given `Set` or is no key in the given `Map`. The resulting number gets not appended. It only gets returned.
```js
import { generateUniqueNumber } from 'fast-unique-numbers';
const map = new Map([
[1, 'something'],
[4, 'something else']
]);
const uniqueNumber = generateUniqueNumber(map);
console.log(uniqueNumber); // 2
```
@@ -0,0 +1,94 @@
{
"author": "Christoph Guttandin",
"browser": "build/es5/bundle.js",
"bugs": {
"url": "https://github.com/chrisguttandin/fast-unique-numbers/issues"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"dependencies": {
"@babel/runtime": "^7.23.8",
"tslib": "^2.6.2"
},
"description": "A module to create a set of unique numbers as fast as possible.",
"devDependencies": {
"@babel/cli": "^7.23.4",
"@babel/core": "^7.23.7",
"@babel/plugin-external-helpers": "^7.23.3",
"@babel/plugin-transform-runtime": "^7.23.7",
"@babel/preset-env": "^7.23.8",
"@babel/register": "^7.23.7",
"@commitlint/cli": "^17.8.0",
"@commitlint/config-angular": "^17.8.0",
"@rollup/plugin-babel": "^6.0.4",
"chai": "^4.3.10",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.56.0",
"eslint-config-holy-grail": "^57.2.27",
"grunt": "^1.6.1",
"grunt-cli": "^1.4.3",
"grunt-sh": "^0.2.1",
"husky": "^8.0.3",
"karma": "^6.4.2",
"karma-browserstack-launcher": "^1.6.0",
"karma-chrome-launcher": "^3.2.0",
"karma-firefox-launcher": "^2.1.2",
"karma-mocha": "^2.0.1",
"karma-sinon-chai": "^2.0.2",
"karma-webkit-launcher": "^2.4.0",
"karma-webpack": "^5.0.0",
"lint-staged": "^15.2.0",
"load-grunt-config": "^4.0.1",
"mocha": "^10.2.0",
"prettier": "^3.2.2",
"rimraf": "^5.0.5",
"rollup": "^4.9.5",
"sinon": "^17.0.1",
"sinon-chai": "^3.7.0",
"tinybench": "^2.6.0",
"ts-loader": "^9.5.1",
"tsconfig-holy-grail": "^14.0.8",
"tslint": "^6.1.3",
"tslint-config-holy-grail": "^55.0.5",
"typescript": "^5.3.3",
"webpack": "^5.89.0"
},
"engines": {
"node": ">=16.1.0"
},
"files": [
"build/es2019/",
"build/es5/",
"build/node/",
"src/"
],
"homepage": "https://github.com/chrisguttandin/fast-unique-numbers",
"keywords": [
"performance",
"speed"
],
"license": "MIT",
"main": "build/node/module.js",
"module": "build/es2019/module.js",
"name": "fast-unique-numbers",
"repository": {
"type": "git",
"url": "https://github.com/chrisguttandin/fast-unique-numbers.git"
},
"scripts": {
"build": "rimraf build/* && tsc --project src/tsconfig.json && rollup --config config/rollup/bundle.mjs && babel ./build/es2019 --config-file ./config/babel/build.json --out-dir ./build/node",
"lint": "npm run lint:config && npm run lint:src && npm run lint:test",
"lint:config": "eslint --config config/eslint/config.json --ext .js --report-unused-disable-directives config/",
"lint:src": "tslint --config config/tslint/src.json --project src/tsconfig.json src/*.ts src/**/*.ts",
"lint:test": "eslint --config config/eslint/test.json --ext .js --report-unused-disable-directives test/",
"prepare": "husky install",
"prepublishOnly": "npm run build",
"test": "grunt lint && grunt test"
},
"types": "build/es2019/module.d.ts",
"version": "8.0.13"
}
@@ -0,0 +1,11 @@
import { TAddUniqueNumberFactory } from '../types';
export const createAddUniqueNumber: TAddUniqueNumberFactory = (generateUniqueNumber) => {
return (set) => {
const number = generateUniqueNumber(set);
set.add(number);
return number;
};
};
@@ -0,0 +1,9 @@
import { TCacheFactory } from '../types';
export const createCache: TCacheFactory = (lastNumberWeakMap) => {
return (collection, nextNumber) => {
lastNumberWeakMap.set(collection, nextNumber);
return nextNumber;
};
};
@@ -0,0 +1,55 @@
import { TGenerateUniqueNumberFactory } from '../types';
/*
* The value of the constant Number.MAX_SAFE_INTEGER equals (2 ** 53 - 1) but it
* is fairly new.
*/
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER === undefined ? 9007199254740991 : Number.MAX_SAFE_INTEGER;
const TWO_TO_THE_POWER_OF_TWENTY_NINE = 536870912;
const TWO_TO_THE_POWER_OF_THIRTY = TWO_TO_THE_POWER_OF_TWENTY_NINE * 2;
export const createGenerateUniqueNumber: TGenerateUniqueNumberFactory = (cache, lastNumberWeakMap) => {
return (collection) => {
const lastNumber = lastNumberWeakMap.get(collection);
/*
* Let's try the cheapest algorithm first. It might fail to produce a new
* number, but it is so cheap that it is okay to take the risk. Just
* increase the last number by one or reset it to 0 if we reached the upper
* bound of SMIs (which stands for small integers). When the last number is
* unknown it is assumed that the collection contains zero based consecutive
* numbers.
*/
let nextNumber = lastNumber === undefined ? collection.size : lastNumber < TWO_TO_THE_POWER_OF_THIRTY ? lastNumber + 1 : 0;
if (!collection.has(nextNumber)) {
return cache(collection, nextNumber);
}
/*
* If there are less than half of 2 ** 30 numbers stored in the collection,
* the chance to generate a new random number in the range from 0 to 2 ** 30
* is at least 50%. It's benifitial to use only SMIs because they perform
* much better in any environment based on V8.
*/
if (collection.size < TWO_TO_THE_POWER_OF_TWENTY_NINE) {
while (collection.has(nextNumber)) {
nextNumber = Math.floor(Math.random() * TWO_TO_THE_POWER_OF_THIRTY);
}
return cache(collection, nextNumber);
}
// Quickly check if there is a theoretical chance to generate a new number.
if (collection.size > MAX_SAFE_INTEGER) {
throw new Error('Congratulations, you created a collection of unique numbers which uses all available integers!');
}
// Otherwise use the full scale of safely usable integers.
while (collection.has(nextNumber)) {
nextNumber = Math.floor(Math.random() * MAX_SAFE_INTEGER);
}
return cache(collection, nextNumber);
};
};
@@ -0,0 +1,17 @@
import { createAddUniqueNumber } from './factories/add-unique-number';
import { createCache } from './factories/cache';
import { createGenerateUniqueNumber } from './factories/generate-unique-number';
/*
* @todo Explicitly referencing the barrel file seems to be necessary when enabling the
* isolatedModules compiler option.
*/
export * from './types/index';
const LAST_NUMBER_WEAK_MAP = new WeakMap<Map<number, any> | Set<number>, number>();
const cache = createCache(LAST_NUMBER_WEAK_MAP);
const generateUniqueNumber = createGenerateUniqueNumber(cache, LAST_NUMBER_WEAK_MAP);
const addUniqueNumber = createAddUniqueNumber(generateUniqueNumber);
export { addUniqueNumber, generateUniqueNumber };
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"isolatedModules": true
},
"extends": "tsconfig-holy-grail/src/tsconfig-universal"
}
@@ -0,0 +1,4 @@
import { TAddUniqueNumberFunction } from './add-unique-number-function';
import { TGenerateUniqueNumberFunction } from './generate-unique-number-function';
export type TAddUniqueNumberFactory = (generateUniqueNumber: TGenerateUniqueNumberFunction) => TAddUniqueNumberFunction;
@@ -0,0 +1 @@
export type TAddUniqueNumberFunction = (set: Set<number>) => number;
@@ -0,0 +1,3 @@
import { TCacheFunction } from './cache-function';
export type TCacheFactory = (lastNumberWeakMap: WeakMap<Map<number, any> | Set<number>, number>) => TCacheFunction;
@@ -0,0 +1 @@
export type TCacheFunction = (collection: Map<number, any> | Set<number>, nextNumber: number) => number;
@@ -0,0 +1,7 @@
import { TCacheFunction } from './cache-function';
import { TGenerateUniqueNumberFunction } from './generate-unique-number-function';
export type TGenerateUniqueNumberFactory = (
cache: TCacheFunction,
lastNumberWeakMap: WeakMap<Map<number, any> | Set<number>, number>
) => TGenerateUniqueNumberFunction;
@@ -0,0 +1 @@
export type TGenerateUniqueNumberFunction = (collection: Map<number, any> | Set<number>) => number;
@@ -0,0 +1,6 @@
export * from './add-unique-number-factory';
export * from './add-unique-number-function';
export * from './cache-factory';
export * from './cache-function';
export * from './generate-unique-number-factory';
export * from './generate-unique-number-function';
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Christoph Guttandin
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
@@ -0,0 +1,5 @@
# worker-timers-broker
**The broker which is used by the worker-timers package.**
[![version](https://img.shields.io/npm/v/worker-timers-broker.svg?style=flat-square)](https://www.npmjs.com/package/worker-timers-broker)
@@ -0,0 +1,84 @@
{
"author": "Christoph Guttandin",
"bugs": {
"url": "https://github.com/chrisguttandin/worker-timers-broker/issues"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"dependencies": {
"@babel/runtime": "^7.24.5",
"fast-unique-numbers": "^8.0.13",
"tslib": "^2.6.2",
"worker-timers-worker": "^7.0.71"
},
"description": "The broker which is used by the worker-timers package.",
"devDependencies": {
"@babel/core": "^7.24.5",
"@babel/plugin-external-helpers": "^7.24.1",
"@babel/plugin-transform-runtime": "^7.24.3",
"@babel/preset-env": "^7.24.5",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-angular": "^19.3.0",
"@rollup/plugin-babel": "^6.0.4",
"chai": "^4.3.10",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.57.0",
"eslint-config-holy-grail": "^59.0.8",
"grunt": "^1.6.1",
"grunt-cli": "^1.4.3",
"grunt-sh": "^0.2.1",
"husky": "^8.0.3",
"karma": "^6.4.3",
"karma-chrome-launcher": "^3.2.0",
"karma-firefox-launcher": "^2.1.3",
"karma-mocha": "^2.0.1",
"karma-sauce-launcher": "^4.3.6",
"karma-sinon-chai": "^2.0.2",
"karma-webkit-launcher": "^2.4.0",
"karma-webpack": "^5.0.1",
"lint-staged": "^15.2.2",
"load-grunt-config": "^4.0.1",
"mocha": "^10.4.0",
"prettier": "^3.2.5",
"rimraf": "^5.0.5",
"rollup": "^4.17.2",
"sinon": "^17.0.1",
"sinon-chai": "^3.7.0",
"ts-loader": "^9.5.1",
"tsconfig-holy-grail": "^15.0.1",
"tslint": "^6.1.3",
"tslint-config-holy-grail": "^56.0.1",
"typescript": "^5.4.5",
"webpack": "^5.91.0"
},
"files": [
"build/es2019/",
"build/es5/",
"src/"
],
"homepage": "https://github.com/chrisguttandin/worker-timers-broker",
"license": "MIT",
"main": "build/es5/bundle.js",
"module": "build/es2019/module.js",
"name": "worker-timers-broker",
"repository": {
"type": "git",
"url": "https://github.com/chrisguttandin/worker-timers-broker.git"
},
"scripts": {
"build": "rimraf build/* && tsc --project src/tsconfig.json && rollup --config config/rollup/bundle.mjs",
"lint": "npm run lint:config && npm run lint:src && npm run lint:test",
"lint:config": "eslint --config config/eslint/config.json --ext .js --report-unused-disable-directives config/",
"lint:src": "tslint --config config/tslint/src.json --project src/tsconfig.json src/*.ts src/**/*.ts",
"lint:test": "eslint --config config/eslint/test.json --ext .js --report-unused-disable-directives test/",
"prepare": "husky install",
"prepublishOnly": "npm run build",
"test": "grunt lint && grunt test"
},
"types": "build/es2019/module.d.ts",
"version": "6.1.8"
}
@@ -0,0 +1,5 @@
import { ICallNotification, TWorkerMessage } from 'worker-timers-worker';
export const isCallNotification = (message: TWorkerMessage): message is ICallNotification => {
return (<ICallNotification>message).method !== undefined && (<ICallNotification>message).method === 'call';
};
@@ -0,0 +1,5 @@
import { IClearResponse, TWorkerMessage } from 'worker-timers-worker';
export const isClearResponse = (message: TWorkerMessage): message is IClearResponse => {
return (<IClearResponse>message).error === null && typeof message.id === 'number';
};
@@ -0,0 +1,173 @@
import { generateUniqueNumber } from 'fast-unique-numbers';
import { IClearRequest, ISetNotification, IWorkerEvent, TTimerType } from 'worker-timers-worker';
import { isCallNotification } from './guards/call-notification';
import { isClearResponse } from './guards/clear-response';
export const load = (url: string) => {
// Prefilling the Maps with a function indexed by zero is necessary to be compliant with the specification.
const scheduledIntervalFunctions: Map<number, number | Function> = new Map([[0, () => {}]]); // tslint:disable-line no-empty
const scheduledTimeoutFunctions: Map<number, number | Function> = new Map([[0, () => {}]]); // tslint:disable-line no-empty
const unrespondedRequests: Map<number, { timerId: number; timerType: TTimerType }> = new Map();
const worker = new Worker(url);
worker.addEventListener('message', ({ data }: IWorkerEvent) => {
if (isCallNotification(data)) {
const {
params: { timerId, timerType }
} = data;
if (timerType === 'interval') {
const idOrFunc = scheduledIntervalFunctions.get(timerId);
if (typeof idOrFunc === 'number') {
const timerIdAndTimerType = unrespondedRequests.get(idOrFunc);
if (
timerIdAndTimerType === undefined ||
timerIdAndTimerType.timerId !== timerId ||
timerIdAndTimerType.timerType !== timerType
) {
throw new Error('The timer is in an undefined state.');
}
} else if (typeof idOrFunc !== 'undefined') {
idOrFunc();
} else {
throw new Error('The timer is in an undefined state.');
}
} else if (timerType === 'timeout') {
const idOrFunc = scheduledTimeoutFunctions.get(timerId);
if (typeof idOrFunc === 'number') {
const timerIdAndTimerType = unrespondedRequests.get(idOrFunc);
if (
timerIdAndTimerType === undefined ||
timerIdAndTimerType.timerId !== timerId ||
timerIdAndTimerType.timerType !== timerType
) {
throw new Error('The timer is in an undefined state.');
}
} else if (typeof idOrFunc !== 'undefined') {
idOrFunc();
// A timeout can be savely deleted because it is only called once.
scheduledTimeoutFunctions.delete(timerId);
} else {
throw new Error('The timer is in an undefined state.');
}
}
} else if (isClearResponse(data)) {
const { id } = data;
const timerIdAndTimerType = unrespondedRequests.get(id);
if (timerIdAndTimerType === undefined) {
throw new Error('The timer is in an undefined state.');
}
const { timerId, timerType } = timerIdAndTimerType;
unrespondedRequests.delete(id);
if (timerType === 'interval') {
scheduledIntervalFunctions.delete(timerId);
} else {
scheduledTimeoutFunctions.delete(timerId);
}
} else {
const {
error: { message }
} = data;
throw new Error(message);
}
});
const clearInterval = (timerId: number) => {
const id = generateUniqueNumber(unrespondedRequests);
unrespondedRequests.set(id, { timerId, timerType: 'interval' });
scheduledIntervalFunctions.set(timerId, id);
worker.postMessage(<IClearRequest>{
id,
method: 'clear',
params: { timerId, timerType: 'interval' }
});
};
const clearTimeout = (timerId: number) => {
const id = generateUniqueNumber(unrespondedRequests);
unrespondedRequests.set(id, { timerId, timerType: 'timeout' });
scheduledTimeoutFunctions.set(timerId, id);
worker.postMessage(<IClearRequest>{
id,
method: 'clear',
params: { timerId, timerType: 'timeout' }
});
};
const setInterval = (func: Function, delay = 0) => {
const timerId = generateUniqueNumber(scheduledIntervalFunctions);
scheduledIntervalFunctions.set(timerId, () => {
func();
// Doublecheck if the interval should still be rescheduled because it could have been cleared inside of func().
if (typeof scheduledIntervalFunctions.get(timerId) === 'function') {
worker.postMessage(<ISetNotification>{
id: null,
method: 'set',
params: {
delay,
now: performance.now(),
timerId,
timerType: 'interval'
}
});
}
});
worker.postMessage(<ISetNotification>{
id: null,
method: 'set',
params: {
delay,
now: performance.now(),
timerId,
timerType: 'interval'
}
});
return timerId;
};
const setTimeout = (func: Function, delay = 0) => {
const timerId = generateUniqueNumber(scheduledTimeoutFunctions);
scheduledTimeoutFunctions.set(timerId, func);
worker.postMessage(<ISetNotification>{
id: null,
method: 'set',
params: {
delay,
now: performance.now(),
timerId,
timerType: 'timeout'
}
});
return timerId;
};
return {
clearInterval,
clearTimeout,
setInterval,
setTimeout
};
};
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"isolatedModules": true
},
"extends": "tsconfig-holy-grail/src/tsconfig-browser"
}
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Christoph Guttandin
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
@@ -0,0 +1,5 @@
# worker-timers-worker
**The worker which is used by the worker-timers package.**
[![version](https://img.shields.io/npm/v/worker-timers-worker.svg?style=flat-square)](https://www.npmjs.com/package/worker-timers-worker)
@@ -0,0 +1,91 @@
{
"author": "Christoph Guttandin",
"bugs": {
"url": "https://github.com/chrisguttandin/worker-timers-worker/issues"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"contributors": [
{
"email": "vac872089248@gmail.com",
"name": "Knissing"
}
],
"dependencies": {
"@babel/runtime": "^7.24.5",
"tslib": "^2.6.2"
},
"description": "The worker which is used by the worker-timers package.",
"devDependencies": {
"@babel/core": "^7.24.5",
"@babel/plugin-external-helpers": "^7.24.1",
"@babel/plugin-transform-runtime": "^7.24.3",
"@babel/preset-env": "^7.24.5",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-angular": "^19.3.0",
"@rollup/plugin-babel": "^6.0.4",
"chai": "^4.3.10",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.57.0",
"eslint-config-holy-grail": "^59.0.8",
"grunt": "^1.6.1",
"grunt-cli": "^1.4.3",
"grunt-sh": "^0.2.1",
"husky": "^8.0.3",
"karma": "^6.4.3",
"karma-browserstack-launcher": "^1.6.0",
"karma-chrome-launcher": "^3.2.0",
"karma-cli": "^2.0.0",
"karma-firefox-launcher": "^2.1.3",
"karma-mocha": "^2.0.1",
"karma-mocha-webworker": "^1.3.0",
"karma-sinon-chai": "^2.0.2",
"karma-webkit-launcher": "^2.4.0",
"karma-webpack": "^5.0.1",
"lint-staged": "^15.2.2",
"load-grunt-config": "^4.0.1",
"memory-fs": "^0.5.0",
"mocha": "^10.4.0",
"prettier": "^3.2.5",
"rimraf": "^5.0.5",
"rollup": "^4.17.2",
"sinon": "^17.0.1",
"sinon-chai": "^3.7.0",
"ts-loader": "^9.5.1",
"tsconfig-holy-grail": "^15.0.1",
"tslint": "^6.1.3",
"tslint-config-holy-grail": "^56.0.1",
"typescript": "^5.4.5",
"webpack": "^5.91.0"
},
"files": [
"build/es2019/",
"build/es5/",
"src/"
],
"homepage": "https://github.com/chrisguttandin/worker-timers-worker",
"license": "MIT",
"main": "build/es5/bundle.js",
"module": "build/es2019/module.js",
"name": "worker-timers-worker",
"repository": {
"type": "git",
"url": "https://github.com/chrisguttandin/worker-timers-worker.git"
},
"scripts": {
"build": "rimraf build/* && tsc --project src/tsconfig.json && rollup --config config/rollup/bundle.mjs",
"lint": "npm run lint:config && npm run lint:src && npm run lint:test",
"lint:config": "eslint --config config/eslint/config.json --ext .js --report-unused-disable-directives config/",
"lint:src": "tslint --config config/tslint/src.json --project src/tsconfig.json src/*.ts src/**/*.ts",
"lint:test": "eslint --config config/eslint/test.json --ext .js --report-unused-disable-directives test/",
"prepare": "husky install",
"prepublishOnly": "npm run build",
"test": "grunt lint && grunt test"
},
"types": "build/es2019/module.d.ts",
"version": "7.0.71"
}
@@ -0,0 +1,68 @@
import { ICallNotification } from '../interfaces';
const scheduledIntervalIdentifiers: Map<number, number> = new Map();
const scheduledTimeoutIdentifiers: Map<number, number> = new Map();
export const clearScheduledInterval = (timerId: number) => {
const identifier = scheduledIntervalIdentifiers.get(timerId);
if (identifier !== undefined) {
clearTimeout(identifier);
scheduledIntervalIdentifiers.delete(timerId);
} else {
throw new Error(`There is no interval scheduled with the given id "${timerId}".`);
}
};
export const clearScheduledTimeout = (timerId: number) => {
const identifier = scheduledTimeoutIdentifiers.get(timerId);
if (identifier !== undefined) {
clearTimeout(identifier);
scheduledTimeoutIdentifiers.delete(timerId);
} else {
throw new Error(`There is no timeout scheduled with the given id "${timerId}".`);
}
};
const computeDelayAndExpectedCallbackTime = (delay: number, nowInMainThread: number) => {
let now: number;
let remainingDelay: number;
const nowInWorker = performance.now();
const elapsed = Math.max(0, nowInWorker - nowInMainThread);
now = nowInWorker;
remainingDelay = delay - elapsed;
const expected = now + remainingDelay;
return { expected, remainingDelay };
};
const setTimeoutCallback = (identifiers: Map<number, number>, timerId: number, expected: number, timerType: string) => {
const now = performance.now();
if (now > expected) {
postMessage(<ICallNotification>{ id: null, method: 'call', params: { timerId, timerType } });
} else {
identifiers.set(timerId, setTimeout(setTimeoutCallback, expected - now, identifiers, timerId, expected, timerType));
}
};
export const scheduleInterval = (delay: number, timerId: number, nowInMainThread: number) => {
const { expected, remainingDelay } = computeDelayAndExpectedCallbackTime(delay, nowInMainThread);
scheduledIntervalIdentifiers.set(
timerId,
setTimeout(setTimeoutCallback, remainingDelay, scheduledIntervalIdentifiers, timerId, expected, 'interval')
);
};
export const scheduleTimeout = (delay: number, timerId: number, nowInMainThread: number) => {
const { expected, remainingDelay } = computeDelayAndExpectedCallbackTime(delay, nowInMainThread);
scheduledTimeoutIdentifiers.set(
timerId,
setTimeout(setTimeoutCallback, remainingDelay, scheduledTimeoutIdentifiers, timerId, expected, 'timeout')
);
};
@@ -0,0 +1,5 @@
import { TBrokerMessage } from '../types';
export interface IBrokerEvent extends Event {
data: TBrokerMessage;
}
@@ -0,0 +1,13 @@
import { TTimerType } from '../types';
export interface ICallNotification {
id: null;
method: 'call';
params: {
timerId: number;
timerType: TTimerType;
};
}
@@ -0,0 +1,13 @@
import { TTimerType } from '../types';
export interface IClearRequest {
id: number;
method: 'clear';
params: {
timerId: number;
timerType: TTimerType;
};
}
@@ -0,0 +1,5 @@
export interface IClearResponse {
error: null;
id: number;
}
@@ -0,0 +1,9 @@
export interface IErrorNotification {
error: {
message: string;
};
id: null;
result: null;
}
@@ -0,0 +1,9 @@
export interface IErrorResponse {
error: {
message: string;
};
id: number;
result: null;
}
@@ -0,0 +1,8 @@
export * from './broker-event';
export * from './call-notification';
export * from './clear-request';
export * from './clear-response';
export * from './error-notification';
export * from './error-response';
export * from './set-notification';
export * from './worker-event';
@@ -0,0 +1,17 @@
import { TTimerType } from '../types';
export interface ISetNotification {
id: null;
method: 'set';
params: {
delay: number;
now: number;
timerId: number;
timerType: TTimerType;
};
}
@@ -0,0 +1,5 @@
import { TWorkerMessage } from '../types';
export interface IWorkerEvent extends Event {
data: TWorkerMessage;
}
@@ -0,0 +1,54 @@
import { clearScheduledInterval, clearScheduledTimeout, scheduleInterval, scheduleTimeout } from './helpers/timer';
import { IBrokerEvent, IClearResponse, IErrorNotification, IErrorResponse } from './interfaces';
/*
* @todo Explicitly referencing the barrel file seems to be necessary when enabling the
* isolatedModules compiler option.
*/
export * from './interfaces/index';
export * from './types/index';
addEventListener('message', ({ data }: IBrokerEvent) => {
try {
if (data.method === 'clear') {
const {
id,
params: { timerId, timerType }
} = data;
if (timerType === 'interval') {
clearScheduledInterval(timerId);
postMessage(<IClearResponse>{ error: null, id });
} else if (timerType === 'timeout') {
clearScheduledTimeout(timerId);
postMessage(<IClearResponse>{ error: null, id });
} else {
throw new Error(`The given type "${timerType}" is not supported`);
}
} else if (data.method === 'set') {
const {
params: { delay, now, timerId, timerType }
} = data;
if (timerType === 'interval') {
scheduleInterval(delay, timerId, now);
} else if (timerType === 'timeout') {
scheduleTimeout(delay, timerId, now);
} else {
throw new Error(`The given type "${timerType}" is not supported`);
}
} else {
throw new Error(`The given method "${(<any>data).method}" is not supported`);
}
} catch (err) {
postMessage(<IErrorNotification | IErrorResponse>{
error: {
message: err.message
},
id: data.id,
result: null
});
}
});
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"isolatedModules": true
},
"extends": "tsconfig-holy-grail/src/tsconfig-web-worker"
}
@@ -0,0 +1,3 @@
import { IClearRequest, ISetNotification } from '../interfaces';
export type TBrokerMessage = IClearRequest | ISetNotification;
@@ -0,0 +1,3 @@
export * from './broker-message';
export * from './timer-type';
export * from './worker-message';
@@ -0,0 +1 @@
export type TTimerType = 'interval' | 'timeout';
@@ -0,0 +1,3 @@
import { ICallNotification, IClearResponse, IErrorNotification, IErrorResponse } from '../interfaces';
export type TWorkerMessage = ICallNotification | IClearResponse | IErrorNotification | IErrorResponse;
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Christoph Guttandin
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
+79
View File
@@ -0,0 +1,79 @@
![logo](https://repository-images.githubusercontent.com/24792198/dd93c980-323f-11ea-8a14-a0299de4847a)
# worker-timers
**A replacement for setInterval() and setTimeout() which works in unfocused windows.**
[![version](https://img.shields.io/npm/v/worker-timers.svg?style=flat-square)](https://www.npmjs.com/package/worker-timers)
## Motivation
For scripts that rely on [WindowTimers](http://www.w3.org/TR/html5/webappapis.html#timers) like `setInterval()` or `setTimeout()` things get confusing when the site which the script is running on loses focus. Chrome, Firefox and maybe others throttle the frequency at which they invoke those timers to a maximum of once per second in such a situation. However this is only true for the main thread and does not affect the behavior of [Web Workers](http://www.w3.org/TR/workers/). Therefore it is possible to avoid the throttling by using a worker to do the actual scheduling. This is exactly what `worker-timers` does.
## Getting Started
`worker-timers` is available as a package on [npm](https://www.npmjs.org/package/worker-timers). Run the following command to install it:
```shell
npm install worker-timers
```
You can then import the exported functions in your code like this:
```js
import { clearInterval, clearTimeout, setInterval, setTimeout } from 'worker-timers';
```
The usage is exactly the same (despite of the [error handling](#error-handling) and the
[differentiation between intervals and timeouts](#differentiation-between-intervals-and-timeouts))
as with the corresponding functions on the global scope.
```js
var intervalId = setInterval(() => {
// do something many times
}, 100);
clearInterval(intervalId);
var timeoutId = setTimeout(() => {
// do something once
}, 100);
clearTimeout(timeoutId);
```
## Error Handling
The native WindowTimers are very forgiving. Calling `clearInterval()` or `clearTimeout()` without a value or with an id which doesn't exist will get ignored. In contrast to that `worker-timers` will throw an error when doing so.
```js
// This will return undefined.
window.clearTimeout('not-a-timeout-id');
// This will throw an error.
clearTimeout('not-a-timeout-id');
```
## Differentiation between Intervals and Timeouts
Another difference between `worker-timers` and WindowTimers is that this package maintains two separate lists to store the ids of intervals and timeouts internally. WindowTimers do only have one list which allows intervals to be cancelled by calling `clearTimeout()` and the other way round. This is not possible with `worker-timers`. As mentioned above `worker-timers` will throw an error when provided with an unknown id.
```js
const periodicWork = () => {};
// This will stop the interval.
const windowId = window.setInterval(periodicWork, 100);
window.clearTimeout(windowId);
// This will throw an error.
const workerId = setInterval(periodicWork, 100);
clearTimeout(workerId);
```
## Server-Side Rendering
This package is intended to be used in the browser and requires the browser to have [support for Web Workers](https://caniuse.com/#feat=webworkers). It does not contain any fallback which would allow it to run in another environment like Node.js which doesn't know about Web Workers. This is to prevent this package from silently failing in an unsupported browser. But it also means that it needs to be replaced when used in a web project which also supports server-side rendering. The replacement should be straightforward, at least in theory, because each function has the exact same signature as its corresponding builtin function. But the configuration of a real-life project can be tricky. For a concrete example, please have a look at the [worker-timers-ssr-example](https://github.com/newyork-anthonyng/worker-timers-ssr-example) provided by [@newyork-anthonyng](https://github.com/newyork-anthonyng). It shows the usage inside of a server-side rendered React app.
## Angular (& Zone.js)
If `worker-timers` is used inside of an Angular app and Zone.js (which is the default) is used to detect changes, the behavior of `worker-timers` can be confusing. Angular is using Zone.js which is patching the native `setInterval()` and `setTimeout()` functions to get notified about the invocation of their callback functions. But Angular (more specifically Zone.js) is not aware of `worker-timers` and doesn't get notified about any callback invocations. Therefore Angular needs to be notified manually about state changes that occur inside of a callback function which was scheduled with the help of `worker-timers`.
@@ -0,0 +1,106 @@
{
"author": "Christoph Guttandin",
"bugs": {
"url": "https://github.com/chrisguttandin/worker-timers/issues"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"contributors": [
{
"email": "a-anng@expedia.com",
"name": "Anthony Ng"
}
],
"dependencies": {
"@babel/runtime": "^7.24.5",
"tslib": "^2.6.2",
"worker-timers-broker": "^6.1.8",
"worker-timers-worker": "^7.0.71"
},
"description": "A replacement for setInterval() and setTimeout() which works in unfocused windows.",
"devDependencies": {
"@babel/cli": "^7.24.5",
"@babel/core": "^7.24.5",
"@babel/plugin-external-helpers": "^7.24.1",
"@babel/plugin-transform-runtime": "^7.24.3",
"@babel/preset-env": "^7.24.5",
"@babel/register": "^7.23.7",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-angular": "^19.3.0",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-replace": "^5.0.5",
"babel-loader": "^9.1.3",
"chai": "^4.3.10",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.57.0",
"eslint-config-holy-grail": "^59.0.8",
"grunt": "^1.6.1",
"grunt-cli": "^1.4.3",
"grunt-sh": "^0.2.1",
"husky": "^8.0.3",
"karma": "^6.4.3",
"karma-chrome-launcher": "^3.2.0",
"karma-firefox-launcher": "^2.1.3",
"karma-mocha": "^2.0.1",
"karma-sauce-launcher": "^4.3.6",
"karma-sinon-chai": "^2.0.2",
"karma-webkit-launcher": "^2.4.0",
"karma-webpack": "^5.0.1",
"lint-staged": "^15.2.2",
"load-grunt-config": "^4.0.1",
"memfs": "^4.9.2",
"mocha": "^10.4.0",
"prettier": "^3.2.5",
"rimraf": "^5.0.5",
"rollup": "^4.17.2",
"sinon": "^17.0.1",
"sinon-chai": "^3.7.0",
"terser-webpack-plugin": "^5.3.10",
"ts-loader": "^9.5.1",
"tsconfig-holy-grail": "^15.0.1",
"tslint": "^6.1.3",
"tslint-config-holy-grail": "^56.0.1",
"typescript": "^5.4.5",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
},
"files": [
"build/es2019/",
"build/es5/",
"src/"
],
"homepage": "https://github.com/chrisguttandin/worker-timers",
"keywords": [
"Web Workers",
"WindowTimers",
"clearInterval",
"clearTimeout",
"interval",
"setInterval",
"setTimeout"
],
"license": "MIT",
"main": "build/es5/bundle.js",
"module": "build/es2019/module.js",
"name": "worker-timers",
"repository": {
"type": "git",
"url": "https://github.com/chrisguttandin/worker-timers.git"
},
"scripts": {
"build": "rimraf build/* && webpack --config config/webpack/worker-es2019.js && tsc --project src/tsconfig.json && rollup --config config/rollup/bundle.mjs && babel ./build/es2019 --config-file ./config/babel/build.json --out-dir ./build/node",
"lint": "npm run lint:config && npm run lint:src && npm run lint:test",
"lint:config": "eslint --config config/eslint/config.json --ext .js --report-unused-disable-directives config/",
"lint:src": "tslint --config config/tslint/src.json --project src/tsconfig.json src/*.ts src/**/*.ts",
"lint:test": "eslint --config config/eslint/test.json --ext .js --report-unused-disable-directives test/",
"prepare": "husky install",
"prepublishOnly": "npm run build",
"test": "grunt lint && grunt test"
},
"types": "build/es2019/module.d.ts",
"version": "7.1.8"
}
@@ -0,0 +1,19 @@
export const createLoadOrReturnBroker = <Broker>(loadBroker: (url: string) => Broker, worker: string) => {
let broker: null | Broker = null;
return () => {
if (broker !== null) {
return broker;
}
const blob = new Blob([worker], { type: 'application/javascript; charset=utf-8' });
const url = URL.createObjectURL(blob);
broker = loadBroker(url);
// Bug #1: Edge up until v18 didn't like the URL to be revoked directly.
setTimeout(() => URL.revokeObjectURL(url));
return broker;
};
};
@@ -0,0 +1,13 @@
import { load } from 'worker-timers-broker';
import { createLoadOrReturnBroker } from './factories/load-or-return-broker';
import { worker } from './worker/worker';
const loadOrReturnBroker = createLoadOrReturnBroker(load, worker);
export const clearInterval: ReturnType<typeof load>['clearInterval'] = (timerId) => loadOrReturnBroker().clearInterval(timerId);
export const clearTimeout: ReturnType<typeof load>['clearTimeout'] = (timerId) => loadOrReturnBroker().clearTimeout(timerId);
export const setInterval: ReturnType<typeof load>['setInterval'] = (...args) => loadOrReturnBroker().setInterval(...args);
export const setTimeout: ReturnType<typeof load>['setTimeout'] = (...args) => loadOrReturnBroker().setTimeout(...args);
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"isolatedModules": true
},
"extends": "tsconfig-holy-grail/src/tsconfig-browser"
}
@@ -0,0 +1,2 @@
// This is the minified and stringified code of the worker-timers-worker package.
export const worker = `(()=>{"use strict";const e=new Map,t=new Map,r=(e,t)=>{let r,o;const i=performance.now();r=i,o=e-Math.max(0,i-t);return{expected:r+o,remainingDelay:o}},o=(e,t,r,i)=>{const s=performance.now();s>r?postMessage({id:null,method:"call",params:{timerId:t,timerType:i}}):e.set(t,setTimeout(o,r-s,e,t,r,i))};addEventListener("message",(i=>{let{data:s}=i;try{if("clear"===s.method){const{id:r,params:{timerId:o,timerType:i}}=s;if("interval"===i)(t=>{const r=e.get(t);if(void 0===r)throw new Error('There is no interval scheduled with the given id "'.concat(t,'".'));clearTimeout(r),e.delete(t)})(o),postMessage({error:null,id:r});else{if("timeout"!==i)throw new Error('The given type "'.concat(i,'" is not supported'));(e=>{const r=t.get(e);if(void 0===r)throw new Error('There is no timeout scheduled with the given id "'.concat(e,'".'));clearTimeout(r),t.delete(e)})(o),postMessage({error:null,id:r})}}else{if("set"!==s.method)throw new Error('The given method "'.concat(s.method,'" is not supported'));{const{params:{delay:i,now:n,timerId:a,timerType:d}}=s;if("interval"===d)((t,i,s)=>{const{expected:n,remainingDelay:a}=r(t,s);e.set(i,setTimeout(o,a,e,i,n,"interval"))})(i,a,n);else{if("timeout"!==d)throw new Error('The given type "'.concat(d,'" is not supported'));((e,i,s)=>{const{expected:n,remainingDelay:a}=r(e,s);t.set(i,setTimeout(o,a,t,i,n,"timeout"))})(i,a,n)}}}}catch(e){postMessage({error:{message:e.message},id:s.id,result:null})}}))})();`; // tslint:disable-line:max-line-length
+48
View File
@@ -0,0 +1,48 @@
{
"name": "retimer",
"version": "4.0.0",
"description": "Reschedulable Timer for your node needs",
"main": "retimer.js",
"types": "types.d.ts",
"scripts": {
"lint": "standard",
"test": "npm run test:ci && npm run test:typescript",
"test:ci": "tape test.js | tap-dot",
"test:typescript": "tsd"
},
"pre-commit": [
"lint",
"test"
],
"repository": {
"type": "git",
"url": "git+https://github.com/mcollina/retimer.git"
},
"keywords": [
"schedulable",
"reschedulable",
"timer",
"setTimeout"
],
"author": "Matteo Collina <hello@matteocollina.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/mcollina/retimer/issues"
},
"homepage": "https://github.com/mcollina/retimer#readme",
"devDependencies": {
"fastbench": "^1.0.1",
"pre-commit": "^1.2.2",
"standard": "^17.1.0",
"tap-dot": "^2.0.0",
"tape": "^5.6.6",
"tsd": "^0.28.1"
},
"browser": {
"./time.js": "./time-browser.js",
"./timers.js": "./timers-browser.js"
},
"dependencies": {
"worker-timers": "^7.0.75"
}
}
+82
View File
@@ -0,0 +1,82 @@
'use strict'
const getTime = require('./time')
const { clearTimeout, setTimeout } = require('./timers')
class Retimer {
constructor (callback, timeout, args) {
const that = this
this._started = getTime()
this._rescheduled = 0
this._scheduled = timeout
this._args = args
this._triggered = false
this._timerWrapper = () => {
if (that._rescheduled > 0) {
that._scheduled = that._rescheduled - (getTime() - that._started)
that._schedule(that._scheduled)
} else {
that._triggered = true
callback.apply(null, that._args)
}
}
this._timer = setTimeout(this._timerWrapper, timeout)
}
reschedule (timeout) {
if (!timeout) {
timeout = this._scheduled
}
const now = getTime()
if ((now + timeout) - (this._started + this._scheduled) < 0) {
clearTimeout(this._timer)
this._schedule(timeout)
} else if (!this._triggered) {
this._started = now
this._rescheduled = timeout
} else {
this._schedule(timeout)
}
}
_schedule (timeout) {
this._triggered = false
this._started = getTime()
this._rescheduled = 0
this._scheduled = timeout
this._timer = setTimeout(this._timerWrapper, timeout)
}
clear () {
clearTimeout(this._timer)
}
}
function retimer () {
if (typeof arguments[0] !== 'function') {
throw new Error('callback needed')
}
if (typeof arguments[1] !== 'number') {
throw new Error('timeout needed')
}
let args
if (arguments.length > 0) {
args = new Array(arguments.length - 2)
/* eslint-disable no-var */
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i + 2]
}
}
return new Retimer(arguments[0], arguments[1], args)
}
module.exports = retimer
module.exports.Retimer = Retimer
+117
View File
@@ -0,0 +1,117 @@
'use strict'
const test = require('tape')
const retimer = require('./')
test('schedule a callback', function (t) {
t.plan(1)
const start = Date.now()
retimer(function () {
t.ok(Date.now() - start >= 50, 'it was deferred ok!')
}, 50)
})
test('reschedule a callback', function (t) {
t.plan(1)
const start = Date.now()
const timer = retimer(function () {
t.ok(Date.now() - start >= 70, 'it was deferred ok!')
}, 50)
setTimeout(function () {
timer.reschedule(50)
}, 20)
})
test('reschedule multiple times', function (t) {
t.plan(1)
const start = Date.now()
const timer = retimer(function () {
t.ok(Date.now() - start >= 90, 'it was deferred ok!')
}, 50)
setTimeout(function () {
timer.reschedule(50)
setTimeout(function () {
timer.reschedule(50)
}, 20)
}, 20)
})
test('clear a timer', function (t) {
t.plan(1)
const timer = retimer(function () {
t.fail('the timer should never get called')
}, 20)
timer.clear()
setTimeout(function () {
t.pass('nothing happened')
}, 50)
})
test('clear a timer after a reschedule', function (t) {
t.plan(1)
const timer = retimer(function () {
t.fail('the timer should never get called')
}, 20)
setTimeout(function () {
timer.reschedule(50)
setTimeout(function () {
timer.clear()
}, 10)
}, 10)
setTimeout(function () {
t.pass('nothing happened')
}, 50)
})
test('can be rescheduled early', function (t) {
t.plan(1)
const start = Date.now()
const timer = retimer(function () {
t.ok(Date.now() - start <= 500, 'it was rescheduled!')
}, 500)
setTimeout(function () {
timer.reschedule(10)
}, 20)
})
test('can be rescheduled even if the timeout has already triggered', function (t) {
t.plan(2)
const start = Date.now()
let count = 0
const timer = retimer(function () {
count++
if (count === 1) {
t.ok(Date.now() - start >= 20, 'it was triggered!')
timer.reschedule(20)
} else {
t.ok(Date.now() - start >= 40, 'it was rescheduled!')
}
}, 20)
})
test('pass arguments to the callback', function (t) {
t.plan(1)
retimer(function (arg) {
t.equal(arg, 42, 'argument matches')
}, 50, 42)
})
+5
View File
@@ -0,0 +1,5 @@
'use strict'
module.exports = function getTime () {
return Date.now()
}
+6
View File
@@ -0,0 +1,6 @@
'use strict'
module.exports = function getTime () {
const t = process.hrtime()
return Math.floor(t[0] * 1000 + t[1] / 1000000)
}
+8
View File
@@ -0,0 +1,8 @@
const { clearInterval, clearTimeout, setInterval, setTimeout } = require('worker-timers')
module.exports = {
clearInterval,
clearTimeout,
setInterval,
setTimeout
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
clearInterval,
clearTimeout,
setInterval,
setTimeout
}
+19
View File
@@ -0,0 +1,19 @@
type TimerCallback = (...args: any[]) => void;
export class Retimer {
constructor(callback: TimerCallback, timeout: number, args?: any[]);
reschedule(timeout?: number): void;
clear(): void;
}
export default function retimer(
callback: TimerCallback,
timeout: number,
...args: any[]
): Retimer;
declare module 'retimer' {
export = retimer;
}
+11
View File
@@ -0,0 +1,11 @@
import retimer, { Retimer } from "./types"
import { expectType } from 'tsd'
expectType<Retimer>(retimer(() => {}, 1000))
expectType<Retimer>(retimer(() => {}, 1000, 42))
// use new Retimer()
expectType<Retimer>(new Retimer(() => {}, 1000))
expectType<Retimer>(new Retimer(() => {}, 1000, [42]))