| 14 | } |
| 15 | // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging |
| 16 | export class SimulatedClock implements SimulatedClock { |
| 17 | private timeouts: Map<number, SimulatedTimeout> = new Map(); |
| 18 | private _now: number = 0; |
| 19 | private _id: number = 0; |
| 20 | private _flushing = false; |
| 21 | private _flushingInvalidated = false; |
| 22 | |
| 23 | public now() { |
| 24 | return this._now; |
| 25 | } |
| 26 | private getId() { |
| 27 | return this._id++; |
| 28 | } |
| 29 | public setTimeout(fn: (...args: any[]) => void, timeout: number) { |
| 30 | this._flushingInvalidated = this._flushing; |
| 31 | const id = this.getId(); |
| 32 | this.timeouts.set(id, { |
| 33 | start: this.now(), |
| 34 | timeout, |
| 35 | fn |
| 36 | }); |
| 37 | return id; |
| 38 | } |
| 39 | public clearTimeout(id: number) { |
| 40 | this._flushingInvalidated = this._flushing; |
| 41 | this.timeouts.delete(id); |
| 42 | } |
| 43 | public set(time: number) { |
| 44 | if (this._now > time) { |
| 45 | throw new Error('Unable to travel back in time'); |
| 46 | } |
| 47 | |
| 48 | this._now = time; |
| 49 | this.flushTimeouts(); |
| 50 | } |
| 51 | private flushTimeouts() { |
| 52 | if (this._flushing) { |
| 53 | this._flushingInvalidated = true; |
| 54 | return; |
| 55 | } |
| 56 | this._flushing = true; |
| 57 | |
| 58 | const sorted = [...this.timeouts].sort( |
| 59 | ([_idA, timeoutA], [_idB, timeoutB]) => { |
| 60 | const endA = timeoutA.start + timeoutA.timeout; |
| 61 | const endB = timeoutB.start + timeoutB.timeout; |
| 62 | return endB > endA ? -1 : 1; |
| 63 | } |
| 64 | ); |
| 65 | |
| 66 | for (const [id, timeout] of sorted) { |
| 67 | if (this._flushingInvalidated) { |
| 68 | this._flushingInvalidated = false; |
| 69 | this._flushing = false; |
| 70 | this.flushTimeouts(); |
| 71 | return; |
| 72 | } |
| 73 | if (this.now() - timeout.start >= timeout.timeout) { |
nothing calls this directly
no outgoing calls
no test coverage detected
searching dependent graphs…