├── README.md
├── example_customclock.html
├── example_simple_spoof.html
├── example_slow2x.html
└── js
├── MTC.js
└── MTCWorker.js
/README.md:
--------------------------------------------------------------------------------
1 | # MockTheClock
2 |
3 | MockTheClock (MTC) is a JavaScript library for spoofing time in browser. (Yes, this is for browser, not Node, there is a better way to do this in Node)
4 |
5 | While libraries like this existed before, they didn't support performance.now() or Workers, but since I needed this to slow down some 3D stuff, and those 3D apps were using these features, I had to write my own library.
6 |
7 | What this can do\*:
8 | * Spoof Date(), Date.now(), new Date(), performance.now()
9 | * Influence intervals, timeouts
10 | * Do these things inside (nested) workers!
11 |
12 | What this can't do:
13 | * Guarantee that the application cannot figure out the clock is spoofed and bypass this
14 | * Spoof meta refresh, synchronous timeout XHR's, CSS transition/animation timing events etc
15 | * Spoof time for pages inside iframes
16 | * Pretty much everything else :)
17 |
18 |
19 | _\* well, the software is provided as is (orly? can it be provided as it isn't?), and no warranty and blah blah blah... IOW: the author thinks it can do these things_
20 |
21 | ## Installation
22 |
23 | Add `MTC.js` and `MTCWorker.js` to your project (they need to be placed in the same folder) and include the `MTC.js` file.
24 | ```html
25 |
26 | ```
27 |
28 | **Note** that this file must be the first JS file included, because otherwise other files may be able to grab references to original JS timing functions, and in that case it's not possible to override their functionality.
29 |
30 | Another thing you need to do as soon as possible is to choose which functions or Worker URL's you want to remain unaffected:
31 | ```javascript
32 | MTC.excludeTimers = [sendHeartbeat, logMemoryUsage];
33 | ```
34 | It does **not** mean that calls to ```Date``` will not be spoofed inside these functions, but it means that if you set timers that call these functions (via `setTimeout` or `setInterval`), those timers will execute like MTC wasn't active.
35 |
36 | If you want some Workers to be unaffected by MTC, you can use `excludeWorkers`:
37 | ```javascript
38 | MTC.excludeWorkers = ['js/sendHeartbeat.js', 'js/logMemoryUsage.js'];
39 | ```
40 | But unlike timers - calls to `Date` will not be spoofed here, because MTC code won't even be injected into these workers.
41 |
42 |
43 | ## Usage
44 |
45 | ### Turn it on
46 | MTC is off by default, however it still takes care of timers itself. It does it every 10ms, so if you schedule a 5ms timer, it will be executed twice every 10ms.... (brilliant, right?)
47 |
48 | To enable MTC, use `toggleSpoofing`:
49 | ```javascript
50 | MTC.toggleSpoofing(true);
51 | ```
52 |
53 | And from now on... **the clock is stopped**, and timers are not executed (except for `setTimeout(x, 0)` which has nothing to do with actual timers).
54 |
55 | Functions like `Date.now` will now return the same value until you **manually** forward the clock or stop spoofing time!
56 |
57 | ### Control the clock
58 |
59 | If you want to change the clock, use the `setTime` function:
60 | ```javascript
61 | MTC.setTime(Date.now() + 500); //forward the clock by 500ms
62 | ```
63 |
64 | If some timers have been scheduled, `setTime` will execute them just like 500ms had passed.
65 |
66 | So for instance if `Date.now()` returns `X` and you schedule an interval:
67 | ```javascript
68 | setInterval(someFunction, 1000)
69 | ```
70 |
71 | And then do:
72 | ```javascript
73 | MTC.setTime(Date.now() + 10000)
74 | ```
75 | `someFunction` will be executed 10 times, and if that function tries to read the clock, the first call will get `X + 1000`, the second will get `X + 2000` and so on, just like we'd called `setTime` 10 times.
76 |
77 |
78 | ### Original time functions
79 |
80 | If we didn't have access to the original timing functions in JS, we wouldn't be able to slow down timers or speed them up, since no timer would be executed until `setTime` was called.
81 |
82 | So for that purpose, the original functions are stored inside `MTC.real` object:
83 | ```javascript
84 | MTC.real.setTimeout
85 | MTC.real.clearTimeout
86 | MTC.real.setInterval
87 | MTC.real.clearInterval
88 | MTC.real.Date
89 | MTC.real.dateNow //Date.now()
90 | MTC.real.performanceNow //performance.now()
91 | MTC.real.Worker
92 | ```
93 |
94 | So for instance, if you wanted to slow timers down 2x, you'd do this:
95 | ```javascript
96 | var lastCallTime;
97 | MTC.real.setInterval(function() {
98 | var delta = lastCallTime ? MTC.real.dateNow() - lastCallTime : 0;
99 | lastCallTime = MTC.real.dateNow();
100 | MTC.setTime(Date.now() + delta/2);
101 | }, 10);
102 | ````
103 |
104 | ### Just spoofing the date
105 |
106 | If you don't want to change timers' attitude, and just want to use this library to spoof the date, then you still need to take care of the clock, that is, you need to update it, that's how time actually works :)
107 |
108 | But, this is easy:
109 | ```javascript
110 | MTC.real.setInterval(function() {
111 | MTC.setTime(MTC.real.dateNow()-delta);
112 | }, 10);
113 | ```
114 |
115 | This is indeed stupid, but unfortunately, JS timing functions provide no other way to change clock, it needs to be... stupid.
116 |
117 |
--------------------------------------------------------------------------------
/example_customclock.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 | Open JS console for fun :)
PS. make sure you don't run this file from a local filesystem, Workers can't be loaded from a local filesystem
11 |
12 |
20 |
34 |
--------------------------------------------------------------------------------
/example_simple_spoof.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 | Open JS console for fun :)
11 |
12 |
13 |
41 |
--------------------------------------------------------------------------------
/example_slow2x.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 | Open JS console for fun :)
11 |
12 |
13 |
44 |
--------------------------------------------------------------------------------
/js/MTC.js:
--------------------------------------------------------------------------------
1 | if (typeof MTC === 'undefined') {
2 |
3 | (typeof window !== 'undefined' ? window : this).MTC = {};
4 | MTC.jspath = '';
5 | if (typeof document === 'object' && document && document.currentScript) {
6 | MTC.jspath = document.currentScript.src.substring(0, document.currentScript.src.lastIndexOf('/')+1)
7 | }
8 | MTC.real = {
9 | Date: Date,
10 | dateNow: Date.now,
11 | performanceNow: this.performance && performance.now.bind(performance) || Date.now,
12 | setTimeout: setTimeout.bind(this),
13 | clearTimeout: clearTimeout.bind(this),
14 | setInterval: setInterval.bind(this),
15 | clearInterval: clearInterval.bind(this),
16 | Worker: this.Worker && Worker.bind(this)
17 | };
18 | MTC.excludeTimers = [];
19 | MTC.excludeWorkers = [];
20 | MTC.installTimeHook = function() {
21 | var self = this,
22 | spoofOn = false;
23 |
24 | var timeStart = 0,
25 | nowStart, currentTime = 0,
26 | realTimeInterval = null;
27 | var timers = [],
28 | tfirst = 0,
29 | ctimers = [],
30 | workers = [];
31 |
32 | function getTime() {
33 | return spoofOn ? currentTime : self.real.dateNow();
34 | }
35 |
36 | var fakeDate = function() {
37 | var d = new self.real.Date();
38 | if (spoofOn) d.setTime(currentTime);
39 |
40 | if (!(this instanceof fakeDate))
41 | return d.toString();
42 |
43 | return d; //yeah, legal, it's an object
44 | };
45 | Date = fakeDate;
46 | Date.prototype = self.real.Date.prototype; //should fake instanceOf
47 | Date.now = getTime;
48 | performance.now = function() {
49 | return spoofOn ? nowStart + getTime() - timeStart : self.real.performanceNow.call(performance);
50 | };
51 |
52 | var addTimer = function(func, delta, repeat) {
53 | delta = Math.abs(delta);
54 | while (timers[tfirst]) tfirst++;
55 | ctimers.push(tfirst);
56 |
57 | if (!delta || self.excludeTimers && self.excludeTimers.indexOf(func) !== -1)
58 | timers[tfirst] = [self.real[delta ? 'setInterval' : 'setTimeout'](func, delta), Infinity, repeat ? delta : 0];
59 | else
60 | timers[tfirst] = [func, getTime() + delta, repeat ? delta : 0];
61 |
62 | return tfirst;
63 | };
64 | setTimeout = function(func, delta) {
65 | return addTimer(func, delta);
66 | };
67 | setInterval = function(func, delta) {
68 | return addTimer(func, delta, true);
69 | };
70 |
71 | clearInterval = clearTimeout = function(id) {
72 | if (id === null || !timers[id]) return;
73 |
74 | if (timers[id][1] === Infinity)
75 | self.real[timers[id][2] ? 'clearInterval' : 'clearTimeout'](timers[id][0]);
76 | timers[id] = null;
77 | tfirst = Math.min(tfirst, id);
78 | ctimers.splice(ctimers.indexOf(id), 1);
79 | };
80 |
81 | //who doesn't support nested workers? IE? FF? Nope, it's actually chrome.
82 | if (typeof Worker !== 'undefined') {
83 | Worker = function(url) {
84 | if (self.excludeWorkers.indexOf(url) !== -1) return new self.real.Worker(url);
85 |
86 | var worker = new self.real.Worker(MTC.jspath+'MTCWorker.js');
87 | worker.postMessage({
88 | MTCInit: true,
89 | spoofOn: spoofOn,
90 | url: url,
91 | currentTime: getTime()
92 | });
93 | workers.push(worker);
94 | return worker;
95 | };
96 | //we'll assign the prototype, however we'll overwrite instance things
97 | Worker.prototype = self.real.Worker.prototype;
98 | }
99 |
100 | var changingTime, desiredTime = 0;
101 |
102 | function setTime(newTime) {
103 | if (changingTime) {
104 | desiredTime = Math.max(newTime, desiredTime);
105 | return;
106 | }
107 |
108 | changingTime = true;
109 | desiredTime = newTime;
110 | var minimalTime = 0,
111 | t;
112 | while (minimalTime < desiredTime) {
113 | minimalTime = desiredTime;
114 |
115 | for (t = 0; t < ctimers.length; t++)
116 | if (timers[ctimers[t]][1] < desiredTime)
117 | minimalTime = Math.min(timers[ctimers[t]][1], minimalTime);
118 |
119 | currentTime = minimalTime;
120 | for (t = 0; t < ctimers.length; t++)
121 | if (timers[ctimers[t]][1] === currentTime) {
122 | if (typeof timers[ctimers[t]][0] === 'string')
123 | (1, eval)(timers[ctimers[t]][0]);
124 | else if (typeof timers[ctimers[t]][0] === 'function')
125 | timers[ctimers[t]][0]();
126 |
127 | if (timers[ctimers[t]][2])
128 | timers[ctimers[t]][1] = currentTime + timers[ctimers[t]][2];
129 | else
130 | clearTimeout(ctimers[t--]);
131 | }
132 | }
133 |
134 | for (var t = 0; t < workers.length; t++)
135 | workers[t].postMessage({
136 | MTCcmd: 'setTime',
137 | param: currentTime
138 | });
139 |
140 | changingTime = false;
141 | }
142 | this.setTime = setTime;
143 |
144 | function syncTime() {
145 | setTime(self.real.dateNow());
146 | }
147 |
148 | function toggleSpoofing(on) {
149 | if (on) {
150 | currentTime = timeStart = getTime();
151 | nowStart = performance.now();
152 | spoofOn = true;
153 |
154 | self.real.clearInterval(realTimeInterval);
155 | realTimeInterval = null;
156 | } else {
157 | spoofOn = false;
158 | if (realTimeInterval === null)
159 | realTimeInterval = self.real.setInterval(syncTime, 10);
160 | }
161 |
162 | for (var t = 0; t < workers.length; t++)
163 | workers[t].postMessage({
164 | MTCcmd: 'toggleSpoofing',
165 | param: on
166 | });
167 |
168 | return currentTime;
169 | };
170 | this.toggleSpoofing = toggleSpoofing;
171 |
172 | toggleSpoofing(spoofOn);
173 | };
174 | MTC.installTimeHook();
175 | }
--------------------------------------------------------------------------------
/js/MTCWorker.js:
--------------------------------------------------------------------------------
1 | importScripts('MTC.js')
2 |
3 | addEventListener('message', function(e) {
4 | if (typeof e.data === 'object' && e.data !== null && e.data.MTCInit) {
5 | MTC.toggleSpoofing(e.data.spoofOn);
6 | MTC.setTime(e.data.currentTime);
7 | importScripts(e.data.url);
8 | }
9 |
10 | if (typeof e.data === 'object' && e.data !== null && e.data.MTCcmd)
11 | MTC[e.data.MTCcmd](e.data.param);
12 |
13 | if (typeof e.data === 'object' && e.data !== null && (e.data.MTCcmd || e.data.MTCInit))
14 | e.stopImmediatePropagation();
15 | }, true);
--------------------------------------------------------------------------------