chronokinesis 7.0.1 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -17
- package/dist/chronokinesis.cjs +101 -4
- package/dist/index.cjs +101 -4
- package/index.js +101 -4
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Mock time and date for traveling and freezing. Inspired and borrowed from [timek
|
|
|
7
7
|
<!-- toc -->
|
|
8
8
|
|
|
9
9
|
- [Introduction](#introduction)
|
|
10
|
+
- [Examples](#examples)
|
|
10
11
|
- [API Reference](#api-reference)
|
|
11
12
|
- [`freeze([...args])`](#freezeargs)
|
|
12
13
|
- [`travel([...args])`](#travelargs)
|
|
@@ -15,21 +16,26 @@ Mock time and date for traveling and freezing. Inspired and borrowed from [timek
|
|
|
15
16
|
- [`isKeepingTime()`](#iskeepingtime)
|
|
16
17
|
- [`timezone(timeZone[, ...args])`](#timezonetimezone-args)
|
|
17
18
|
- [`new TimeZoneTraveller(timeZone)`](#new-timezonetravellertimezone)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
- [`timezone.freeze([...args])`](#timezonefreezeargs)
|
|
20
|
+
- [`timezone.travel([...args])`](#timezonetravelargs)
|
|
21
|
+
- [`timezone.reset()`](#timezonereset)
|
|
22
|
+
- [`timezone.defrost()`](#timezonedefrost)
|
|
22
23
|
- [Distributions](#distributions)
|
|
23
24
|
- [Nodejs require](#nodejs-require)
|
|
24
25
|
- [Browser (UMD)](#browser-umd)
|
|
26
|
+
- [High-resolution clocks](#high-resolution-clocks)
|
|
27
|
+
- [chronokinesis vs `node:test` mock timers](#chronokinesis-vs-nodetest-mock-timers)
|
|
28
|
+
- [Caveats when mocking high-resolution clocks](#caveats-when-mocking-high-resolution-clocks)
|
|
25
29
|
- [Acknowledgements](#acknowledgements)
|
|
26
30
|
|
|
27
31
|
<!-- tocstop -->
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
## Introduction
|
|
30
34
|
|
|
31
35
|
Mock `Date` and `Date.now` in order to help you test time-dependent code. Provides `travel`, `freeze`, and timezone functionality for your Node.js tests.
|
|
32
36
|
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
33
39
|
```javascript
|
|
34
40
|
import * as ck from 'chronokinesis';
|
|
35
41
|
|
|
@@ -87,9 +93,9 @@ setTimeout(() => {
|
|
|
87
93
|
}, 2000);
|
|
88
94
|
```
|
|
89
95
|
|
|
90
|
-
|
|
96
|
+
## API Reference
|
|
91
97
|
|
|
92
|
-
|
|
98
|
+
### `freeze([...args])`
|
|
93
99
|
|
|
94
100
|
Freeze point in time. Calls can be made with the same arguments as the `Date` constructor.
|
|
95
101
|
|
|
@@ -108,7 +114,7 @@ setTimeout(() => {
|
|
|
108
114
|
}, 2000);
|
|
109
115
|
```
|
|
110
116
|
|
|
111
|
-
|
|
117
|
+
### `travel([...args])`
|
|
112
118
|
|
|
113
119
|
Time travel to another era. Calls can be made with the same arguments as the `Date` constructor
|
|
114
120
|
|
|
@@ -145,7 +151,7 @@ setTimeout(function () {
|
|
|
145
151
|
}, 1500);
|
|
146
152
|
```
|
|
147
153
|
|
|
148
|
-
|
|
154
|
+
### `defrost()`
|
|
149
155
|
|
|
150
156
|
Defrost a frozen point in time. Used in combination with travelling will start ticking the clock.
|
|
151
157
|
|
|
@@ -168,7 +174,7 @@ setTimeout(() => {
|
|
|
168
174
|
}, 2000);
|
|
169
175
|
```
|
|
170
176
|
|
|
171
|
-
|
|
177
|
+
### `reset()`
|
|
172
178
|
|
|
173
179
|
Resets Date to current glory.
|
|
174
180
|
|
|
@@ -184,7 +190,7 @@ ck.reset();
|
|
|
184
190
|
console.log(new Date());
|
|
185
191
|
```
|
|
186
192
|
|
|
187
|
-
|
|
193
|
+
### `isKeepingTime()`
|
|
188
194
|
|
|
189
195
|
Utility function to see if we still travel or freeze time.
|
|
190
196
|
|
|
@@ -198,7 +204,7 @@ console.log(ck.isKeepingTime() ? 'Is' : 'Not', 'keeping time');
|
|
|
198
204
|
ck.reset();
|
|
199
205
|
```
|
|
200
206
|
|
|
201
|
-
|
|
207
|
+
### `timezone(timeZone[, ...args])`
|
|
202
208
|
|
|
203
209
|
Travel to time zone.
|
|
204
210
|
|
|
@@ -221,7 +227,7 @@ tz.freeze();
|
|
|
221
227
|
ck.reset();
|
|
222
228
|
```
|
|
223
229
|
|
|
224
|
-
|
|
230
|
+
### `new TimeZoneTraveller(timeZone)`
|
|
225
231
|
|
|
226
232
|
Time zone traveller api.
|
|
227
233
|
|
|
@@ -251,20 +257,63 @@ Same as [#reset](#reset)
|
|
|
251
257
|
|
|
252
258
|
Same as [#defrost](#defrost)
|
|
253
259
|
|
|
254
|
-
|
|
260
|
+
## Distributions
|
|
255
261
|
|
|
256
262
|
The module is prepared for browser and nodejs.
|
|
257
263
|
|
|
258
|
-
|
|
264
|
+
### Nodejs require
|
|
259
265
|
|
|
260
266
|
```js
|
|
261
267
|
const ck = require('chronokinesis');
|
|
262
268
|
```
|
|
263
269
|
|
|
264
|
-
|
|
270
|
+
### Browser (UMD)
|
|
265
271
|
|
|
266
272
|
Use `dist/chronokinesis.cjs`. Sets global property `chronokinesis`.
|
|
267
273
|
|
|
268
|
-
|
|
274
|
+
## High-resolution clocks
|
|
275
|
+
|
|
276
|
+
`process.hrtime`, `process.hrtime.bigint()` and `performance.now()` are mocked alongside `Date` while timekeeping is active. The fake clocks stay on the native monotonic scale — each `freeze(T)` / `travel(T)` call shifts them by `(T − previousMockedMs)`, so readings before and after the mock are directly comparable. `freeze()` additionally locks the value so repeated reads return the same result. `reset()` clears the shift and restores the native functions by identity.
|
|
277
|
+
|
|
278
|
+
Because the fake and native axes are aligned, a pre-mock baseline can be diffed meaningfully against a reading taken after travelling forward:
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
import * as ck from 'chronokinesis';
|
|
282
|
+
|
|
283
|
+
const baseHr = process.hrtime.bigint();
|
|
284
|
+
const basePerf = performance.now();
|
|
285
|
+
const baseMs = Date.now();
|
|
286
|
+
|
|
287
|
+
ck.freeze(baseMs);
|
|
288
|
+
ck.travel(baseMs + 1000);
|
|
289
|
+
|
|
290
|
+
console.log(Number(process.hrtime.bigint() - baseHr) / 1e6); // ≈ 1000 (ms)
|
|
291
|
+
console.log(performance.now() - basePerf); // ≈ 1000 (ms)
|
|
292
|
+
|
|
293
|
+
ck.reset();
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## chronokinesis vs `node:test` mock timers
|
|
297
|
+
|
|
298
|
+
chronokinesis mocks **clocks only** — `Date`, `process.hrtime`, `performance.now`. Timers (`setTimeout`, `setInterval`, `setImmediate`) still run on the real wall clock; a `setTimeout(fn, 10)` under `freeze()` still fires ~10ms later in real time.
|
|
299
|
+
|
|
300
|
+
Node's built-in `node:test` provides `mock.timers` which is different: it mocks both the clock **and** the timer queue, and `mock.timers.tick(ms)` synchronously advances time and fires any timers scheduled within that window — no real waiting required.
|
|
301
|
+
|
|
302
|
+
Rule of thumb:
|
|
303
|
+
|
|
304
|
+
- Use **chronokinesis** to pin the wall-clock/timezone to a specific moment while letting real timers run — e.g. you want `new Date()` to return 1980-01-01 but your `setTimeout(..., 10)` should still take 10ms.
|
|
305
|
+
- Use **`node:test` `mock.timers`** to fast-forward deterministically through timer-based logic without waiting real time.
|
|
306
|
+
- Don't enable both simultaneously; they both own `Date`.
|
|
307
|
+
|
|
308
|
+
## Caveats when mocking high-resolution clocks
|
|
309
|
+
|
|
310
|
+
Swapping `process.hrtime` and `performance.now` is generally safe — `fetch`, `AbortSignal.timeout`, `setTimeout`, `setInterval`, and other libuv-backed APIs are unaffected (they use `uv_hrtime`, which chronokinesis does not touch). A few things are worth knowing:
|
|
311
|
+
|
|
312
|
+
- **`performance.mark` + `performance.measure` across a mock boundary are meaningless.** A mark captured before `freeze()`/`travel()` and a measure taken after produces a fabricated duration — the hrtime axis shifted by the mock delta in between.
|
|
313
|
+
- **Long-lived resources with hrtime-based idle or keep-alive timers** (database pools, HTTP keep-alive, gRPC deadlines) may misbehave if they were acquired before the mock. Prefer to mock before creating such resources and reset after tearing them down.
|
|
314
|
+
- **Tracing / metrics / structured logging** (pino, OpenTelemetry, APM agents) will emit bogus timestamps under mock. Fine for unit tests; don't assert on telemetry timing in mocked phases.
|
|
315
|
+
- **Monotonicity holds within a single `freeze()` or `travel()` phase**, but not across them — `reset()` snaps back to native, and `travel(past)` can move values backwards relative to earlier readings.
|
|
316
|
+
|
|
317
|
+
## Acknowledgements
|
|
269
318
|
|
|
270
319
|
chronokinesis initial code is inspired and borrowed from [timekeeper](https://github.com/vesln/timekeeper)
|
package/dist/chronokinesis.cjs
CHANGED
|
@@ -15,10 +15,28 @@
|
|
|
15
15
|
const NativeDate = Date[kNativeDate] || Date;
|
|
16
16
|
const nativeGetTimezoneOffset = NativeDate.prototype.getTimezoneOffset;
|
|
17
17
|
|
|
18
|
+
/* c8 ignore start -- environment-capability guards; false branches only hit in browser (isomorphic test) */
|
|
19
|
+
const nativeProcess = typeof process !== 'undefined' ? process : null;
|
|
20
|
+
const nativeHrtime = nativeProcess && typeof nativeProcess.hrtime === 'function' ? nativeProcess.hrtime : null;
|
|
21
|
+
const nativeHrtimeBigint = nativeHrtime && typeof nativeHrtime.bigint === 'function' ? nativeHrtime.bigint : null;
|
|
22
|
+
const nativePerformance = typeof performance !== 'undefined' ? performance : null;
|
|
23
|
+
const nativePerformanceNow = nativePerformance && typeof nativePerformance.now === 'function' ? nativePerformance.now : null;
|
|
24
|
+
|
|
25
|
+
// Anchor native perf.now against native hrtime at module load so that later reads
|
|
26
|
+
// can reconstruct real native perf.now even when process.hrtime has been swapped
|
|
27
|
+
// (Node <22 wires performance.now to process.hrtime internally).
|
|
28
|
+
const perfAnchorMs = nativePerformanceNow ? nativePerformanceNow.call(nativePerformance) : 0;
|
|
29
|
+
const hrtimeAnchorNs = nativeHrtimeBigint ? nativeHrtimeBigint.call(nativeProcess) : 0n;
|
|
30
|
+
/* c8 ignore stop */
|
|
31
|
+
|
|
18
32
|
let freezedAt = null;
|
|
19
33
|
let traveledTo = null;
|
|
20
34
|
let started = null;
|
|
21
35
|
let iana = null;
|
|
36
|
+
let hrtimeOffset = 0n;
|
|
37
|
+
let performanceNowOffset = 0;
|
|
38
|
+
let freezedHrtimeNs = null;
|
|
39
|
+
let freezedPerformanceNow = null;
|
|
22
40
|
|
|
23
41
|
function FakeDate(...args) {
|
|
24
42
|
const length = args.length;
|
|
@@ -57,16 +75,21 @@
|
|
|
57
75
|
|
|
58
76
|
function freeze(...args) {
|
|
59
77
|
useFakeDate();
|
|
60
|
-
|
|
61
|
-
if (isNaN(
|
|
78
|
+
const target = instantiate(Date, args);
|
|
79
|
+
if (isNaN(target)) {
|
|
62
80
|
reset();
|
|
63
81
|
throw new TypeError('Chronokinesis cannot freeze to invalid date, check your arguments. Chronokinesis is reset');
|
|
64
82
|
}
|
|
83
|
+
const prevMockedMs = currentMockedMs();
|
|
84
|
+
freezedAt = target;
|
|
85
|
+
shiftAndLockFakeClocks(target.getTime() - prevMockedMs);
|
|
65
86
|
return freezedAt;
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
function defrost() {
|
|
69
90
|
freezedAt = null;
|
|
91
|
+
freezedHrtimeNs = null;
|
|
92
|
+
freezedPerformanceNow = null;
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
function travel(...args) {
|
|
@@ -77,13 +100,21 @@
|
|
|
77
100
|
throw new TypeError('Chronokinesis cannot travel to invalid date, check your arguments. Chronokinesis is reset');
|
|
78
101
|
}
|
|
79
102
|
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
const prevMockedMs = currentMockedMs();
|
|
104
|
+
const targetMs = travelToDate.getTime();
|
|
105
|
+
const deltaMs = targetMs - prevMockedMs;
|
|
106
|
+
if (nativeHrtimeBigint) hrtimeOffset += BigInt(deltaMs) * 1_000_000n;
|
|
107
|
+
if (nativePerformanceNow) performanceNowOffset += deltaMs;
|
|
82
108
|
|
|
83
109
|
if (freezedAt) {
|
|
84
110
|
freezedAt = travelToDate;
|
|
111
|
+
if (nativeHrtimeBigint) freezedHrtimeNs = nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
|
|
112
|
+
if (nativePerformanceNow) freezedPerformanceNow = nativePerformanceNowFromHrtime() + performanceNowOffset;
|
|
85
113
|
}
|
|
86
114
|
|
|
115
|
+
traveledTo = targetMs;
|
|
116
|
+
started = NativeDate.now();
|
|
117
|
+
|
|
87
118
|
return travelToDate;
|
|
88
119
|
}
|
|
89
120
|
|
|
@@ -93,6 +124,10 @@
|
|
|
93
124
|
started = null;
|
|
94
125
|
traveledTo = null;
|
|
95
126
|
iana = null;
|
|
127
|
+
hrtimeOffset = 0n;
|
|
128
|
+
performanceNowOffset = 0;
|
|
129
|
+
freezedHrtimeNs = null;
|
|
130
|
+
freezedPerformanceNow = null;
|
|
96
131
|
}
|
|
97
132
|
|
|
98
133
|
function isKeepingTime() {
|
|
@@ -151,16 +186,78 @@
|
|
|
151
186
|
|
|
152
187
|
function useFakeDate() {
|
|
153
188
|
Date = FakeDate;
|
|
189
|
+
if (nativeHrtime) nativeProcess.hrtime = fakeHrtime;
|
|
190
|
+
if (nativePerformanceNow) {
|
|
191
|
+
Object.defineProperty(nativePerformance, 'now', { value: fakePerformanceNow, configurable: true, writable: true });
|
|
192
|
+
}
|
|
154
193
|
}
|
|
155
194
|
|
|
156
195
|
function useNativeDate() {
|
|
157
196
|
Date = FakeDate[kNativeDate];
|
|
197
|
+
if (nativeHrtime) nativeProcess.hrtime = nativeHrtime;
|
|
198
|
+
if (nativePerformanceNow) {
|
|
199
|
+
Object.defineProperty(nativePerformance, 'now', { value: nativePerformanceNow, configurable: true, writable: true });
|
|
200
|
+
}
|
|
158
201
|
}
|
|
159
202
|
|
|
160
203
|
function time() {
|
|
161
204
|
return traveledTo + (NativeDate.now() - started);
|
|
162
205
|
}
|
|
163
206
|
|
|
207
|
+
function currentMockedMs() {
|
|
208
|
+
if (freezedAt) return freezedAt.getTime();
|
|
209
|
+
if (traveledTo !== null) return time();
|
|
210
|
+
return NativeDate.now();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function nativePerformanceNowFromHrtime() {
|
|
214
|
+
if (nativeHrtimeBigint) {
|
|
215
|
+
const elapsedNs = nativeHrtimeBigint.call(nativeProcess) - hrtimeAnchorNs;
|
|
216
|
+
return perfAnchorMs + Number(elapsedNs) / 1_000_000;
|
|
217
|
+
}
|
|
218
|
+
/* c8 ignore next -- browser fallback; covered by isomorphic test, not by Node c8 */
|
|
219
|
+
return nativePerformanceNow.call(nativePerformance);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function shiftAndLockFakeClocks(deltaMs) {
|
|
223
|
+
if (nativeHrtimeBigint) {
|
|
224
|
+
hrtimeOffset += BigInt(deltaMs) * 1_000_000n;
|
|
225
|
+
freezedHrtimeNs = nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
|
|
226
|
+
}
|
|
227
|
+
if (nativePerformanceNow) {
|
|
228
|
+
performanceNowOffset += deltaMs;
|
|
229
|
+
freezedPerformanceNow = nativePerformanceNowFromHrtime() + performanceNowOffset;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function fakeHrtimeNs() {
|
|
234
|
+
if (freezedHrtimeNs !== null) return freezedHrtimeNs;
|
|
235
|
+
return nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function fakeHrtime(prev) {
|
|
239
|
+
const total = fakeHrtimeNs();
|
|
240
|
+
const sec = Number(total / 1_000_000_000n);
|
|
241
|
+
const ns = Number(total % 1_000_000_000n);
|
|
242
|
+
if (!prev) return [sec, ns];
|
|
243
|
+
let ds = sec - prev[0];
|
|
244
|
+
let dn = ns - prev[1];
|
|
245
|
+
if (dn < 0) {
|
|
246
|
+
ds -= 1;
|
|
247
|
+
dn += 1_000_000_000;
|
|
248
|
+
}
|
|
249
|
+
return [ds, dn];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fakeHrtime.bigint = function fakeHrtimeBigint() {
|
|
253
|
+
return fakeHrtimeNs();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
function fakePerformanceNow() {
|
|
257
|
+
if (freezedPerformanceNow !== null) return freezedPerformanceNow;
|
|
258
|
+
return nativePerformanceNowFromHrtime() + performanceNowOffset;
|
|
259
|
+
}
|
|
260
|
+
|
|
164
261
|
function instantiate(type, args) {
|
|
165
262
|
const ctorArgs = args.slice();
|
|
166
263
|
ctorArgs.unshift(null);
|
package/dist/index.cjs
CHANGED
|
@@ -11,10 +11,28 @@ const kNativeDate = Symbol.for('chronokinesis native date');
|
|
|
11
11
|
const NativeDate = Date[kNativeDate] || Date;
|
|
12
12
|
const nativeGetTimezoneOffset = NativeDate.prototype.getTimezoneOffset;
|
|
13
13
|
|
|
14
|
+
/* c8 ignore start -- environment-capability guards; false branches only hit in browser (isomorphic test) */
|
|
15
|
+
const nativeProcess = typeof process !== 'undefined' ? process : null;
|
|
16
|
+
const nativeHrtime = nativeProcess && typeof nativeProcess.hrtime === 'function' ? nativeProcess.hrtime : null;
|
|
17
|
+
const nativeHrtimeBigint = nativeHrtime && typeof nativeHrtime.bigint === 'function' ? nativeHrtime.bigint : null;
|
|
18
|
+
const nativePerformance = typeof performance !== 'undefined' ? performance : null;
|
|
19
|
+
const nativePerformanceNow = nativePerformance && typeof nativePerformance.now === 'function' ? nativePerformance.now : null;
|
|
20
|
+
|
|
21
|
+
// Anchor native perf.now against native hrtime at module load so that later reads
|
|
22
|
+
// can reconstruct real native perf.now even when process.hrtime has been swapped
|
|
23
|
+
// (Node <22 wires performance.now to process.hrtime internally).
|
|
24
|
+
const perfAnchorMs = nativePerformanceNow ? nativePerformanceNow.call(nativePerformance) : 0;
|
|
25
|
+
const hrtimeAnchorNs = nativeHrtimeBigint ? nativeHrtimeBigint.call(nativeProcess) : 0n;
|
|
26
|
+
/* c8 ignore stop */
|
|
27
|
+
|
|
14
28
|
let freezedAt = null;
|
|
15
29
|
let traveledTo = null;
|
|
16
30
|
let started = null;
|
|
17
31
|
let iana = null;
|
|
32
|
+
let hrtimeOffset = 0n;
|
|
33
|
+
let performanceNowOffset = 0;
|
|
34
|
+
let freezedHrtimeNs = null;
|
|
35
|
+
let freezedPerformanceNow = null;
|
|
18
36
|
|
|
19
37
|
function FakeDate(...args) {
|
|
20
38
|
const length = args.length;
|
|
@@ -53,16 +71,21 @@ FakeDate.now = function fakeNow() {
|
|
|
53
71
|
|
|
54
72
|
function freeze(...args) {
|
|
55
73
|
useFakeDate();
|
|
56
|
-
|
|
57
|
-
if (isNaN(
|
|
74
|
+
const target = instantiate(Date, args);
|
|
75
|
+
if (isNaN(target)) {
|
|
58
76
|
reset();
|
|
59
77
|
throw new TypeError('Chronokinesis cannot freeze to invalid date, check your arguments. Chronokinesis is reset');
|
|
60
78
|
}
|
|
79
|
+
const prevMockedMs = currentMockedMs();
|
|
80
|
+
freezedAt = target;
|
|
81
|
+
shiftAndLockFakeClocks(target.getTime() - prevMockedMs);
|
|
61
82
|
return freezedAt;
|
|
62
83
|
}
|
|
63
84
|
|
|
64
85
|
function defrost() {
|
|
65
86
|
freezedAt = null;
|
|
87
|
+
freezedHrtimeNs = null;
|
|
88
|
+
freezedPerformanceNow = null;
|
|
66
89
|
}
|
|
67
90
|
|
|
68
91
|
function travel(...args) {
|
|
@@ -73,13 +96,21 @@ function travel(...args) {
|
|
|
73
96
|
throw new TypeError('Chronokinesis cannot travel to invalid date, check your arguments. Chronokinesis is reset');
|
|
74
97
|
}
|
|
75
98
|
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
const prevMockedMs = currentMockedMs();
|
|
100
|
+
const targetMs = travelToDate.getTime();
|
|
101
|
+
const deltaMs = targetMs - prevMockedMs;
|
|
102
|
+
if (nativeHrtimeBigint) hrtimeOffset += BigInt(deltaMs) * 1_000_000n;
|
|
103
|
+
if (nativePerformanceNow) performanceNowOffset += deltaMs;
|
|
78
104
|
|
|
79
105
|
if (freezedAt) {
|
|
80
106
|
freezedAt = travelToDate;
|
|
107
|
+
if (nativeHrtimeBigint) freezedHrtimeNs = nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
|
|
108
|
+
if (nativePerformanceNow) freezedPerformanceNow = nativePerformanceNowFromHrtime() + performanceNowOffset;
|
|
81
109
|
}
|
|
82
110
|
|
|
111
|
+
traveledTo = targetMs;
|
|
112
|
+
started = NativeDate.now();
|
|
113
|
+
|
|
83
114
|
return travelToDate;
|
|
84
115
|
}
|
|
85
116
|
|
|
@@ -89,6 +120,10 @@ function reset() {
|
|
|
89
120
|
started = null;
|
|
90
121
|
traveledTo = null;
|
|
91
122
|
iana = null;
|
|
123
|
+
hrtimeOffset = 0n;
|
|
124
|
+
performanceNowOffset = 0;
|
|
125
|
+
freezedHrtimeNs = null;
|
|
126
|
+
freezedPerformanceNow = null;
|
|
92
127
|
}
|
|
93
128
|
|
|
94
129
|
function isKeepingTime() {
|
|
@@ -147,16 +182,78 @@ function timezone(timeZone, ...args) {
|
|
|
147
182
|
|
|
148
183
|
function useFakeDate() {
|
|
149
184
|
Date = FakeDate;
|
|
185
|
+
if (nativeHrtime) nativeProcess.hrtime = fakeHrtime;
|
|
186
|
+
if (nativePerformanceNow) {
|
|
187
|
+
Object.defineProperty(nativePerformance, 'now', { value: fakePerformanceNow, configurable: true, writable: true });
|
|
188
|
+
}
|
|
150
189
|
}
|
|
151
190
|
|
|
152
191
|
function useNativeDate() {
|
|
153
192
|
Date = FakeDate[kNativeDate];
|
|
193
|
+
if (nativeHrtime) nativeProcess.hrtime = nativeHrtime;
|
|
194
|
+
if (nativePerformanceNow) {
|
|
195
|
+
Object.defineProperty(nativePerformance, 'now', { value: nativePerformanceNow, configurable: true, writable: true });
|
|
196
|
+
}
|
|
154
197
|
}
|
|
155
198
|
|
|
156
199
|
function time() {
|
|
157
200
|
return traveledTo + (NativeDate.now() - started);
|
|
158
201
|
}
|
|
159
202
|
|
|
203
|
+
function currentMockedMs() {
|
|
204
|
+
if (freezedAt) return freezedAt.getTime();
|
|
205
|
+
if (traveledTo !== null) return time();
|
|
206
|
+
return NativeDate.now();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function nativePerformanceNowFromHrtime() {
|
|
210
|
+
if (nativeHrtimeBigint) {
|
|
211
|
+
const elapsedNs = nativeHrtimeBigint.call(nativeProcess) - hrtimeAnchorNs;
|
|
212
|
+
return perfAnchorMs + Number(elapsedNs) / 1_000_000;
|
|
213
|
+
}
|
|
214
|
+
/* c8 ignore next -- browser fallback; covered by isomorphic test, not by Node c8 */
|
|
215
|
+
return nativePerformanceNow.call(nativePerformance);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function shiftAndLockFakeClocks(deltaMs) {
|
|
219
|
+
if (nativeHrtimeBigint) {
|
|
220
|
+
hrtimeOffset += BigInt(deltaMs) * 1_000_000n;
|
|
221
|
+
freezedHrtimeNs = nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
|
|
222
|
+
}
|
|
223
|
+
if (nativePerformanceNow) {
|
|
224
|
+
performanceNowOffset += deltaMs;
|
|
225
|
+
freezedPerformanceNow = nativePerformanceNowFromHrtime() + performanceNowOffset;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function fakeHrtimeNs() {
|
|
230
|
+
if (freezedHrtimeNs !== null) return freezedHrtimeNs;
|
|
231
|
+
return nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function fakeHrtime(prev) {
|
|
235
|
+
const total = fakeHrtimeNs();
|
|
236
|
+
const sec = Number(total / 1_000_000_000n);
|
|
237
|
+
const ns = Number(total % 1_000_000_000n);
|
|
238
|
+
if (!prev) return [sec, ns];
|
|
239
|
+
let ds = sec - prev[0];
|
|
240
|
+
let dn = ns - prev[1];
|
|
241
|
+
if (dn < 0) {
|
|
242
|
+
ds -= 1;
|
|
243
|
+
dn += 1_000_000_000;
|
|
244
|
+
}
|
|
245
|
+
return [ds, dn];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fakeHrtime.bigint = function fakeHrtimeBigint() {
|
|
249
|
+
return fakeHrtimeNs();
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
function fakePerformanceNow() {
|
|
253
|
+
if (freezedPerformanceNow !== null) return freezedPerformanceNow;
|
|
254
|
+
return nativePerformanceNowFromHrtime() + performanceNowOffset;
|
|
255
|
+
}
|
|
256
|
+
|
|
160
257
|
function instantiate(type, args) {
|
|
161
258
|
const ctorArgs = args.slice();
|
|
162
259
|
ctorArgs.unshift(null);
|
package/index.js
CHANGED
|
@@ -9,10 +9,28 @@ const kNativeDate = Symbol.for('chronokinesis native date');
|
|
|
9
9
|
const NativeDate = Date[kNativeDate] || Date;
|
|
10
10
|
const nativeGetTimezoneOffset = NativeDate.prototype.getTimezoneOffset;
|
|
11
11
|
|
|
12
|
+
/* c8 ignore start -- environment-capability guards; false branches only hit in browser (isomorphic test) */
|
|
13
|
+
const nativeProcess = typeof process !== 'undefined' ? process : null;
|
|
14
|
+
const nativeHrtime = nativeProcess && typeof nativeProcess.hrtime === 'function' ? nativeProcess.hrtime : null;
|
|
15
|
+
const nativeHrtimeBigint = nativeHrtime && typeof nativeHrtime.bigint === 'function' ? nativeHrtime.bigint : null;
|
|
16
|
+
const nativePerformance = typeof performance !== 'undefined' ? performance : null;
|
|
17
|
+
const nativePerformanceNow = nativePerformance && typeof nativePerformance.now === 'function' ? nativePerformance.now : null;
|
|
18
|
+
|
|
19
|
+
// Anchor native perf.now against native hrtime at module load so that later reads
|
|
20
|
+
// can reconstruct real native perf.now even when process.hrtime has been swapped
|
|
21
|
+
// (Node <22 wires performance.now to process.hrtime internally).
|
|
22
|
+
const perfAnchorMs = nativePerformanceNow ? nativePerformanceNow.call(nativePerformance) : 0;
|
|
23
|
+
const hrtimeAnchorNs = nativeHrtimeBigint ? nativeHrtimeBigint.call(nativeProcess) : 0n;
|
|
24
|
+
/* c8 ignore stop */
|
|
25
|
+
|
|
12
26
|
let freezedAt = null;
|
|
13
27
|
let traveledTo = null;
|
|
14
28
|
let started = null;
|
|
15
29
|
let iana = null;
|
|
30
|
+
let hrtimeOffset = 0n;
|
|
31
|
+
let performanceNowOffset = 0;
|
|
32
|
+
let freezedHrtimeNs = null;
|
|
33
|
+
let freezedPerformanceNow = null;
|
|
16
34
|
|
|
17
35
|
export function FakeDate(...args) {
|
|
18
36
|
const length = args.length;
|
|
@@ -51,16 +69,21 @@ FakeDate.now = function fakeNow() {
|
|
|
51
69
|
|
|
52
70
|
export function freeze(...args) {
|
|
53
71
|
useFakeDate();
|
|
54
|
-
|
|
55
|
-
if (isNaN(
|
|
72
|
+
const target = instantiate(Date, args);
|
|
73
|
+
if (isNaN(target)) {
|
|
56
74
|
reset();
|
|
57
75
|
throw new TypeError('Chronokinesis cannot freeze to invalid date, check your arguments. Chronokinesis is reset');
|
|
58
76
|
}
|
|
77
|
+
const prevMockedMs = currentMockedMs();
|
|
78
|
+
freezedAt = target;
|
|
79
|
+
shiftAndLockFakeClocks(target.getTime() - prevMockedMs);
|
|
59
80
|
return freezedAt;
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
export function defrost() {
|
|
63
84
|
freezedAt = null;
|
|
85
|
+
freezedHrtimeNs = null;
|
|
86
|
+
freezedPerformanceNow = null;
|
|
64
87
|
}
|
|
65
88
|
|
|
66
89
|
export function travel(...args) {
|
|
@@ -71,13 +94,21 @@ export function travel(...args) {
|
|
|
71
94
|
throw new TypeError('Chronokinesis cannot travel to invalid date, check your arguments. Chronokinesis is reset');
|
|
72
95
|
}
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
const prevMockedMs = currentMockedMs();
|
|
98
|
+
const targetMs = travelToDate.getTime();
|
|
99
|
+
const deltaMs = targetMs - prevMockedMs;
|
|
100
|
+
if (nativeHrtimeBigint) hrtimeOffset += BigInt(deltaMs) * 1_000_000n;
|
|
101
|
+
if (nativePerformanceNow) performanceNowOffset += deltaMs;
|
|
76
102
|
|
|
77
103
|
if (freezedAt) {
|
|
78
104
|
freezedAt = travelToDate;
|
|
105
|
+
if (nativeHrtimeBigint) freezedHrtimeNs = nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
|
|
106
|
+
if (nativePerformanceNow) freezedPerformanceNow = nativePerformanceNowFromHrtime() + performanceNowOffset;
|
|
79
107
|
}
|
|
80
108
|
|
|
109
|
+
traveledTo = targetMs;
|
|
110
|
+
started = NativeDate.now();
|
|
111
|
+
|
|
81
112
|
return travelToDate;
|
|
82
113
|
}
|
|
83
114
|
|
|
@@ -87,6 +118,10 @@ export function reset() {
|
|
|
87
118
|
started = null;
|
|
88
119
|
traveledTo = null;
|
|
89
120
|
iana = null;
|
|
121
|
+
hrtimeOffset = 0n;
|
|
122
|
+
performanceNowOffset = 0;
|
|
123
|
+
freezedHrtimeNs = null;
|
|
124
|
+
freezedPerformanceNow = null;
|
|
90
125
|
}
|
|
91
126
|
|
|
92
127
|
export function isKeepingTime() {
|
|
@@ -145,16 +180,78 @@ export function timezone(timeZone, ...args) {
|
|
|
145
180
|
|
|
146
181
|
function useFakeDate() {
|
|
147
182
|
Date = FakeDate;
|
|
183
|
+
if (nativeHrtime) nativeProcess.hrtime = fakeHrtime;
|
|
184
|
+
if (nativePerformanceNow) {
|
|
185
|
+
Object.defineProperty(nativePerformance, 'now', { value: fakePerformanceNow, configurable: true, writable: true });
|
|
186
|
+
}
|
|
148
187
|
}
|
|
149
188
|
|
|
150
189
|
function useNativeDate() {
|
|
151
190
|
Date = FakeDate[kNativeDate];
|
|
191
|
+
if (nativeHrtime) nativeProcess.hrtime = nativeHrtime;
|
|
192
|
+
if (nativePerformanceNow) {
|
|
193
|
+
Object.defineProperty(nativePerformance, 'now', { value: nativePerformanceNow, configurable: true, writable: true });
|
|
194
|
+
}
|
|
152
195
|
}
|
|
153
196
|
|
|
154
197
|
function time() {
|
|
155
198
|
return traveledTo + (NativeDate.now() - started);
|
|
156
199
|
}
|
|
157
200
|
|
|
201
|
+
function currentMockedMs() {
|
|
202
|
+
if (freezedAt) return freezedAt.getTime();
|
|
203
|
+
if (traveledTo !== null) return time();
|
|
204
|
+
return NativeDate.now();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function nativePerformanceNowFromHrtime() {
|
|
208
|
+
if (nativeHrtimeBigint) {
|
|
209
|
+
const elapsedNs = nativeHrtimeBigint.call(nativeProcess) - hrtimeAnchorNs;
|
|
210
|
+
return perfAnchorMs + Number(elapsedNs) / 1_000_000;
|
|
211
|
+
}
|
|
212
|
+
/* c8 ignore next -- browser fallback; covered by isomorphic test, not by Node c8 */
|
|
213
|
+
return nativePerformanceNow.call(nativePerformance);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function shiftAndLockFakeClocks(deltaMs) {
|
|
217
|
+
if (nativeHrtimeBigint) {
|
|
218
|
+
hrtimeOffset += BigInt(deltaMs) * 1_000_000n;
|
|
219
|
+
freezedHrtimeNs = nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
|
|
220
|
+
}
|
|
221
|
+
if (nativePerformanceNow) {
|
|
222
|
+
performanceNowOffset += deltaMs;
|
|
223
|
+
freezedPerformanceNow = nativePerformanceNowFromHrtime() + performanceNowOffset;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function fakeHrtimeNs() {
|
|
228
|
+
if (freezedHrtimeNs !== null) return freezedHrtimeNs;
|
|
229
|
+
return nativeHrtimeBigint.call(nativeProcess) + hrtimeOffset;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function fakeHrtime(prev) {
|
|
233
|
+
const total = fakeHrtimeNs();
|
|
234
|
+
const sec = Number(total / 1_000_000_000n);
|
|
235
|
+
const ns = Number(total % 1_000_000_000n);
|
|
236
|
+
if (!prev) return [sec, ns];
|
|
237
|
+
let ds = sec - prev[0];
|
|
238
|
+
let dn = ns - prev[1];
|
|
239
|
+
if (dn < 0) {
|
|
240
|
+
ds -= 1;
|
|
241
|
+
dn += 1_000_000_000;
|
|
242
|
+
}
|
|
243
|
+
return [ds, dn];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fakeHrtime.bigint = function fakeHrtimeBigint() {
|
|
247
|
+
return fakeHrtimeNs();
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
function fakePerformanceNow() {
|
|
251
|
+
if (freezedPerformanceNow !== null) return freezedPerformanceNow;
|
|
252
|
+
return nativePerformanceNowFromHrtime() + performanceNowOffset;
|
|
253
|
+
}
|
|
254
|
+
|
|
158
255
|
function instantiate(type, args) {
|
|
159
256
|
const ctorArgs = args.slice();
|
|
160
257
|
ctorArgs.unshift(null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chronokinesis",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "Module for testing time-dependent code",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Pål Edman",
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"keywords": [
|
|
45
45
|
"date",
|
|
46
46
|
"time",
|
|
47
|
+
"hrtime",
|
|
48
|
+
"performance",
|
|
47
49
|
"timezone",
|
|
48
50
|
"fake",
|
|
49
51
|
"mock",
|
|
@@ -58,6 +60,7 @@
|
|
|
58
60
|
"c8": "^10.1.2",
|
|
59
61
|
"chai": "^6.2.0",
|
|
60
62
|
"eslint": "^9.3.0",
|
|
63
|
+
"happy-dom": "^17.6.1",
|
|
61
64
|
"lodash.clonedeep": "^4.5.0",
|
|
62
65
|
"luxon": "^3.7.1",
|
|
63
66
|
"markdown-toc": "^1.2.0",
|
|
@@ -65,6 +68,6 @@
|
|
|
65
68
|
"moment": "^2.30.1",
|
|
66
69
|
"prettier": "^3.2.5",
|
|
67
70
|
"rollup": "^4.12.1",
|
|
68
|
-
"texample": "^0.0
|
|
71
|
+
"texample": "^0.1.0"
|
|
69
72
|
}
|
|
70
73
|
}
|