Skip to content

Commit

Permalink
Add support for timers/promises module from nodejs (#495)
Browse files Browse the repository at this point in the history
* Add support for timers/promises
  • Loading branch information
WhiteAutumn authored Aug 15, 2024
1 parent 9dcb325 commit 78e7795
Show file tree
Hide file tree
Showing 3 changed files with 717 additions and 4 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ clock instance, not the browser's internals.

Calling `install` with no arguments achieves this. You can call `uninstall`
later to restore things as they were again.
Note that in NodeJS also the [timers](https://nodejs.org/api/timers.html) module will receive fake timers when using global scope.
Note that in NodeJS the [timers](https://nodejs.org/api/timers.html) and [timers/promises](https://nodejs.org/api/timers.html#timers-promises-api) modules will also receive fake timers when using global scope.

```js
// In the browser distribution, a global `FakeTimers` is already available
Expand Down Expand Up @@ -148,7 +148,7 @@ The `loopLimit` argument sets the maximum number of timers that will be run when
### `var clock = FakeTimers.install([config])`

Installs FakeTimers using the specified config (otherwise with epoch `0` on the global scope).
Note that in NodeJS also the [timers](https://nodejs.org/api/timers.html) module will receive fake timers when using global scope.
Note that in NodeJS the [timers](https://nodejs.org/api/timers.html) and [timers/promises](https://nodejs.org/api/timers.html#timers-promises-api) modules will also receive fake timers when using global scope.
The following configuration options are available

| Parameter | Type | Default | Description |
Expand Down
221 changes: 220 additions & 1 deletion src/fake-timers-src.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
"use strict";

const globalObject = require("@sinonjs/commons").global;
let timersModule;
let timersModule, timersPromisesModule;
if (typeof require === "function" && typeof module === "object") {
try {
timersModule = require("timers");
} catch (e) {
// ignored
}
try {
timersPromisesModule = require("timers/promises");
} catch (e) {
// ignored
}
}

/**
Expand Down Expand Up @@ -94,6 +99,7 @@ if (typeof require === "function" && typeof module === "object") {
* @property {Function[]} methods - the methods that are faked
* @property {boolean} [shouldClearNativeTimers] inherited from config
* @property {{methodName:string, original:any}[] | undefined} timersModuleMethods
* @property {{methodName:string, original:any}[] | undefined} timersPromisesModuleMethods
*/
/* eslint-enable jsdoc/require-property-description */

Expand Down Expand Up @@ -954,6 +960,16 @@ function withGlobal(_global) {
timersModule[entry.methodName] = entry.original;
}
}
if (clock.timersPromisesModuleMethods !== undefined) {
for (
let j = 0;
j < clock.timersPromisesModuleMethods.length;
j++
) {
const entry = clock.timersPromisesModuleMethods[j];
timersPromisesModule[entry.methodName] = entry.original;
}
}
}

if (config.shouldAdvanceTime === true) {
Expand Down Expand Up @@ -1834,6 +1850,9 @@ function withGlobal(_global) {
if (_global === globalObject && timersModule) {
clock.timersModuleMethods = [];
}
if (_global === globalObject && timersPromisesModule) {
clock.timersPromisesModuleMethods = [];
}
for (i = 0, l = clock.methods.length; i < l; i++) {
const nameOfMethodToReplace = clock.methods[i];

Expand Down Expand Up @@ -1872,6 +1891,206 @@ function withGlobal(_global) {
timersModule[nameOfMethodToReplace] =
_global[nameOfMethodToReplace];
}
if (clock.timersPromisesModuleMethods !== undefined) {
if (nameOfMethodToReplace === "setTimeout") {
clock.timersPromisesModuleMethods.push({
methodName: "setTimeout",
original: timersPromisesModule.setTimeout,
});

timersPromisesModule.setTimeout = (
delay,
value,
options = {},
) =>
new Promise((resolve, reject) => {
const abort = () => {
options.signal.removeEventListener(
"abort",
abort,
);
// This is safe, there is no code path that leads to this function
// being invoked before handle has been assigned.
// eslint-disable-next-line no-use-before-define
clock.clearTimeout(handle);
reject(options.signal.reason);
};

const handle = clock.setTimeout(() => {
options.signal?.removeEventListener(
"abort",
abort,
);

resolve(value);
}, delay);

if (options.signal?.aborted) {
abort();
} else {
options.signal?.addEventListener(
"abort",
abort,
);
}
});
} else if (nameOfMethodToReplace === "setImmediate") {
clock.timersPromisesModuleMethods.push({
methodName: "setImmediate",
original: timersPromisesModule.setImmediate,
});

timersPromisesModule.setImmediate = (value, options = {}) =>
new Promise((resolve, reject) => {
const abort = () => {
options.signal.removeEventListener(
"abort",
abort,
);
// This is safe, there is no code path that leads to this function
// being invoked before handle has been assigned.
// eslint-disable-next-line no-use-before-define
clock.clearImmediate(handle);
reject(options.signal.reason);
};

const handle = clock.setImmediate(() => {
options.signal?.removeEventListener(
"abort",
abort,
);

resolve(value);
});

if (options.signal?.aborted) {
abort();
} else {
options.signal?.addEventListener(
"abort",
abort,
);
}
});
} else if (nameOfMethodToReplace === "setInterval") {
clock.timersPromisesModuleMethods.push({
methodName: "setInterval",
original: timersPromisesModule.setInterval,
});

timersPromisesModule.setInterval = (
delay,
value,
options = {},
) => ({
[Symbol.asyncIterator]: () => {
const createResolvable = () => {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
promise.resolve = resolve;
promise.reject = reject;
return promise;
};

let done = false;
let hasThrown = false;
let returnCall;
let nextAvailable = 0;
const nextQueue = [];

const handle = clock.setInterval(() => {
if (nextQueue.length > 0) {
nextQueue.shift().resolve();
} else {
nextAvailable++;
}
}, delay);

const abort = () => {
options.signal.removeEventListener(
"abort",
abort,
);
clock.clearInterval(handle);
done = true;
for (const resolvable of nextQueue) {
resolvable.resolve();
}
};

if (options.signal?.aborted) {
done = true;
} else {
options.signal?.addEventListener(
"abort",
abort,
);
}

return {
next: async () => {
if (options.signal?.aborted && !hasThrown) {
hasThrown = true;
throw options.signal.reason;
}

if (done) {
return { done: true, value: undefined };
}

if (nextAvailable > 0) {
nextAvailable--;
return { done: false, value: value };
}

const resolvable = createResolvable();
nextQueue.push(resolvable);

await resolvable;

if (returnCall && nextQueue.length === 0) {
returnCall.resolve();
}

if (options.signal?.aborted && !hasThrown) {
hasThrown = true;
throw options.signal.reason;
}

if (done) {
return { done: true, value: undefined };
}

return { done: false, value: value };
},
return: async () => {
if (done) {
return { done: true, value: undefined };
}

if (nextQueue.length > 0) {
returnCall = createResolvable();
await returnCall;
}

clock.clearInterval(handle);
done = true;

options.signal?.removeEventListener(
"abort",
abort,
);

return { done: true, value: undefined };
},
};
},
});
}
}
}

return clock;
Expand Down
Loading

0 comments on commit 78e7795

Please sign in to comment.