Skip to content

Commit

Permalink
feat: add an option to use native timer functions (#672)
Browse files Browse the repository at this point in the history
This allows to control the behavior of mocked timers (@sinonjs/fake-timers),
depending on the value of the "useNativeTimers" option:

- true: use native setTimeout function
- false (default): use classic timers, that may be mocked

The "installTimerFunctions" method will also be used in the
`socket.io-client` package:

```
import { installTimerFunctions } from "engine.io-client/lib/util";
```

Note: we could also have put the method in its own library, but that
sounded a bit overkill

Related: socketio/socket.io-client#1479
  • Loading branch information
vartan authored and darrachequesne committed Jul 30, 2021
1 parent ce50428 commit aa47237
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 147 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ Exposed as `eio` in the browser standalone build.
- `forceNode` (`Boolean`): Uses NodeJS implementation for websockets - even if there is a native Browser-Websocket available, which is preferred by default over the NodeJS implementation. (This is useful when using hybrid platforms like nw.js or electron) (`false`, NodeJS only)
- `localAddress` (`String`): the local IP address to connect to
- `autoUnref` (`Boolean`): whether the transport should be `unref`'d upon creation. This calls `unref` on the underlying timers and sockets so that the program is allowed to exit if they are the only timers/sockets in the event system (Node.js only)
- `useNativeTimers` (`Boolean`): Whether to always use the native timeouts. This allows the client to reconnect when the native timeout functions are overridden, such as when mock clocks are installed with [`@sinonjs/fake-timers`](https://github.com/sinonjs/fake-timers).
- **Polling-only options**
- `requestTimeout` (`Number`): Timeout for xhr-polling requests in milliseconds (`0`)
- **Websocket-only options**
Expand Down
13 changes: 8 additions & 5 deletions lib/socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const debug = require("debug")("engine.io-client:socket");
const parser = require("engine.io-parser");
const parseuri = require("parseuri");
const parseqs = require("parseqs");
const { installTimerFunctions } = require("./util");

class Socket extends Emitter {
/**
Expand Down Expand Up @@ -31,6 +32,8 @@ class Socket extends Emitter {
opts.hostname = parseuri(opts.host).host;
}

installTimerFunctions(this, opts);

this.secure =
null != opts.secure
? opts.secure
Expand Down Expand Up @@ -172,7 +175,7 @@ class Socket extends Emitter {
transport = "websocket";
} else if (0 === this.transports.length) {
// Emit error on next tick so it can be listened to
setTimeout(() => {
this.setTimeoutFn(() => {
this.emit("error", "No transports available");
}, 0);
return;
Expand Down Expand Up @@ -431,8 +434,8 @@ class Socket extends Emitter {
* @api private
*/
resetPingTimeout() {
clearTimeout(this.pingTimeoutTimer);
this.pingTimeoutTimer = setTimeout(() => {
this.clearTimeoutFn(this.pingTimeoutTimer);
this.pingTimeoutTimer = this.setTimeoutFn(() => {
this.onClose("ping timeout");
}, this.pingInterval + this.pingTimeout);
if (this.opts.autoUnref) {
Expand Down Expand Up @@ -609,8 +612,8 @@ class Socket extends Emitter {
debug('socket close with reason: "%s"', reason);

// clear timers
clearTimeout(this.pingIntervalTimer);
clearTimeout(this.pingTimeoutTimer);
this.clearTimeoutFn(this.pingIntervalTimer);
this.clearTimeoutFn(this.pingTimeoutTimer);

// stop event from firing again for transport
this.transport.removeAllListeners("close");
Expand Down
2 changes: 2 additions & 0 deletions lib/transport.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const parser = require("engine.io-parser");
const Emitter = require("component-emitter");
const { installTimerFunctions } = require("./util");
const debug = require("debug")("engine.io-client:transport");

class Transport extends Emitter {
Expand All @@ -11,6 +12,7 @@ class Transport extends Emitter {
*/
constructor(opts) {
super();
installTimerFunctions(this, opts);

this.opts = opts;
this.query = opts.query;
Expand Down
2 changes: 1 addition & 1 deletion lib/transports/polling-jsonp.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class JSONPPolling extends Polling {
"undefined" !== typeof navigator && /gecko/i.test(navigator.userAgent);

if (isUAgecko) {
setTimeout(function() {
this.setTimeoutFn(function() {
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
document.body.removeChild(iframe);
Expand Down
7 changes: 4 additions & 3 deletions lib/transports/polling-xhr.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const XMLHttpRequest = require("xmlhttprequest-ssl");
const Polling = require("./polling");
const Emitter = require("component-emitter");
const { pick } = require("../util");
const { pick, installTimerFunctions } = require("../util");
const globalThis = require("../globalThis");

const debug = require("debug")("engine.io-client:polling-xhr");
Expand Down Expand Up @@ -105,6 +105,7 @@ class Request extends Emitter {
*/
constructor(uri, opts) {
super();
installTimerFunctions(this, opts);
this.opts = opts;

this.method = opts.method || "GET";
Expand Down Expand Up @@ -187,7 +188,7 @@ class Request extends Emitter {
} else {
// make sure the `error` event handler that's user-set
// does not throw in the same tick and gets caught here
setTimeout(() => {
this.setTimeoutFn(() => {
this.onError(typeof xhr.status === "number" ? xhr.status : 0);
}, 0);
}
Expand All @@ -200,7 +201,7 @@ class Request extends Emitter {
// Need to defer since .create() is called directly from the constructor
// and thus the 'error' event can only be only bound *after* this exception
// occurs. Therefore, also, we cannot throw here at all.
setTimeout(() => {
this.setTimeoutFn(() => {
this.onError(e);
}, 0);
return;
Expand Down
2 changes: 1 addition & 1 deletion lib/transports/websocket-constructor.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const nextTick = (() => {
if (isPromiseAvailable) {
return cb => Promise.resolve().then(cb);
} else {
return cb => setTimeout(cb, 0);
return (cb, setTimeoutFn) => setTimeoutFn(cb, 0);
}
})();

Expand Down
2 changes: 1 addition & 1 deletion lib/transports/websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ class WS extends Transport {
nextTick(() => {
this.writable = true;
this.emit("drain");
});
}, this.setTimeoutFn);
}
});
}
Expand Down
16 changes: 16 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const globalThis = require("./globalThis");

module.exports.pick = (obj, ...attr) => {
return attr.reduce((acc, k) => {
if (obj.hasOwnProperty(k)) {
Expand All @@ -6,3 +8,17 @@ module.exports.pick = (obj, ...attr) => {
return acc;
}, {});
};

// Keep a reference to the real timeout functions so they can be used when overridden
const NATIVE_SET_TIMEOUT = setTimeout;
const NATIVE_CLEAR_TIMEOUT = clearTimeout;

module.exports.installTimerFunctions = (obj, opts) => {
if (opts.useNativeTimers) {
obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThis);
obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThis);
} else {
obj.setTimeoutFn = setTimeout.bind(globalThis);
obj.clearTimeoutFn = clearTimeout.bind(globalThis);
}
};
Loading

0 comments on commit aa47237

Please sign in to comment.