diff --git a/src/utils/Idler.tsx b/src/utils/Idler.tsx index f17f4239f..21a994cf5 100644 --- a/src/utils/Idler.tsx +++ b/src/utils/Idler.tsx @@ -8,16 +8,21 @@ */ import {CancelledPromiseError} from './errors'; +import {sleep} from './promiseTimeout'; -export class Idler { - lastIdle: number; - interval: number; - kill: boolean; +export interface BaseIdler { + shouldIdle(): boolean; + idle(): Promise; + cancel(): void; +} - constructor() { - this.lastIdle = 0; - this.interval = 3; - this.kill = false; +export class Idler implements BaseIdler { + lastIdle = performance.now(); + interval = 16; + kill = false; + + shouldIdle(): boolean { + return this.kill || performance.now() - this.lastIdle > this.interval; } idle(): Promise { @@ -36,3 +41,65 @@ export class Idler { this.kill = true; } } + +// This smills like we should be using generators :) +export class TestIdler implements BaseIdler { + resolver?: () => void; + kill = false; + autoRun = false; + hasProgressed = false; + + shouldIdle() { + if (this.kill) { + return true; + } + if (this.autoRun) { + return false; + } + // In turn we signal idle is needed and that it isn't + this.hasProgressed = !this.hasProgressed; + return !this.hasProgressed; + } + + async idle() { + if (this.kill) { + throw new CancelledPromiseError('Idler got killed'); + } + if (this.autoRun) { + return undefined; + } + if (this.resolver) { + throw new Error('Already idling'); + } + return new Promise(resolve => { + this.resolver = () => { + this.resolver = undefined; + // this.hasProgressed = false; + resolve(); + }; + }); + } + + cancel() { + this.kill = true; + this.run(); + } + + async next() { + if (!this.resolver) { + throw new Error('Not yet idled'); + } + + this.resolver(); + // make sure waiting promise runs first + await sleep(10); + } + + /** + * Automatically progresses through all idle calls + */ + run() { + this.resolver?.(); + this.autoRun = true; + } +} diff --git a/src/utils/__tests__/Idler.node.js b/src/utils/__tests__/Idler.node.js index 1eceee8b6..7ff29978c 100644 --- a/src/utils/__tests__/Idler.node.js +++ b/src/utils/__tests__/Idler.node.js @@ -7,7 +7,8 @@ * @format */ -import {Idler} from '../Idler.tsx'; +import {Idler, TestIdler} from '../Idler.tsx'; +import {sleep} from '../promiseTimeout.tsx'; test('Idler should interrupt', async () => { const idler = new Idler(); @@ -15,7 +16,9 @@ test('Idler should interrupt', async () => { try { for (; i < 500; i++) { if (i == 100) { + expect(idler.shouldIdle()).toBe(false); idler.cancel(); + expect(idler.shouldIdle()).toBe(true); } await idler.idle(); } @@ -24,3 +27,47 @@ test('Idler should interrupt', async () => { expect(i).toEqual(100); } }); + +test('Idler should want to idle', async () => { + const idler = new Idler(); + expect(idler.shouldIdle()).toBe(false); + await sleep(10); + expect(idler.shouldIdle()).toBe(false); + await sleep(200); + expect(idler.shouldIdle()).toBe(true); + await idler.idle(); + expect(idler.shouldIdle()).toBe(false); +}); + +test('TestIdler can be controlled', async () => { + const idler = new TestIdler(); + + expect(idler.shouldIdle()).toBe(false); + expect(idler.shouldIdle()).toBe(true); + let resolved = false; + idler.idle().then(() => { + resolved = true; + }); + expect(resolved).toBe(false); + await idler.next(); + expect(resolved).toBe(true); + + expect(idler.shouldIdle()).toBe(false); + expect(idler.shouldIdle()).toBe(true); + idler.idle(); + await idler.next(); + + idler.cancel(); + expect(idler.shouldIdle()).toBe(true); + + let threw = false; + const p = idler.idle().catch(e => { + threw = true; + expect(e).toMatchInlineSnapshot( + `[CancelledPromiseError: Idler got killed]`, + ); + }); + + await p; + expect(threw).toBe(true); +});