mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 00:02:16 +00:00
108 lines
3.2 KiB
TypeScript
108 lines
3.2 KiB
TypeScript
// autobuild - decides when to build as the user types and saves. No vscode
|
|
// imports, and the clock is injectable, so tests can drive it.
|
|
//
|
|
// Changes wait for a pause in typing; a save builds at once. A build never
|
|
// overlaps another: changes during a build ask for one more, which starts
|
|
// when it finishes with the newest buffers. Slow builds stretch the pause.
|
|
|
|
export type BuildReason = 'type' | 'save' | 'command';
|
|
|
|
export interface Clock {
|
|
now(): number;
|
|
setTimeout(fn: () => void, ms: number): any;
|
|
clearTimeout(handle: any): void;
|
|
}
|
|
|
|
export const realClock: Clock = {
|
|
now: () => Date.now(),
|
|
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
clearTimeout: (h) => clearTimeout(h),
|
|
};
|
|
|
|
export interface SchedulerOptions {
|
|
/** wait this long after the last change */
|
|
debounceMs?: number;
|
|
/** builds slower than this stretch the wait to twice their time */
|
|
slowMs?: number;
|
|
clock?: Clock;
|
|
}
|
|
|
|
const PRIORITY: { [r in BuildReason]: number } = { type: 0, save: 1, command: 2 };
|
|
|
|
export class BuildScheduler {
|
|
readonly debounceMs: number;
|
|
readonly slowMs: number;
|
|
readonly clock: Clock;
|
|
/** how long the last build took */
|
|
lastBuildMs = 0;
|
|
/** when the user last changed something */
|
|
lastChange = 0;
|
|
private timer: any = null;
|
|
private building = false;
|
|
private pending: BuildReason | null = null;
|
|
|
|
/** `build` runs one build; its promise settles when the build is done. */
|
|
constructor(readonly build: (reason: BuildReason) => Promise<void>, opts: SchedulerOptions = {}) {
|
|
this.debounceMs = opts.debounceMs ?? 300;
|
|
this.slowMs = opts.slowMs ?? 1000;
|
|
this.clock = opts.clock ?? realClock;
|
|
}
|
|
|
|
/** The wait after a change: the debounce, or longer after slow builds. */
|
|
get delayMs(): number {
|
|
return this.lastBuildMs > this.slowMs ? Math.max(this.debounceMs, this.lastBuildMs * 2) : this.debounceMs;
|
|
}
|
|
|
|
get busy(): boolean {
|
|
return this.building || this.timer != null;
|
|
}
|
|
|
|
/** A file the build reads changed (typing). */
|
|
changed() {
|
|
this.lastChange = this.clock.now();
|
|
this.cancelTimer();
|
|
this.timer = this.clock.setTimeout(() => {
|
|
this.timer = null;
|
|
this.request('type');
|
|
}, this.delayMs);
|
|
}
|
|
|
|
/** A file the build reads was saved, or the user asked for a build. */
|
|
now(reason: BuildReason = 'save') {
|
|
this.cancelTimer();
|
|
return this.request(reason);
|
|
}
|
|
|
|
/** Drop a waiting build (the project went away). */
|
|
cancel() {
|
|
this.cancelTimer();
|
|
this.pending = null;
|
|
}
|
|
|
|
private cancelTimer() {
|
|
if (this.timer != null) this.clock.clearTimeout(this.timer);
|
|
this.timer = null;
|
|
}
|
|
|
|
private async request(reason: BuildReason): Promise<void> {
|
|
if (this.building) {
|
|
// one more build when this one ends, for the strongest reason asked
|
|
if (this.pending == null || PRIORITY[reason] > PRIORITY[this.pending]) this.pending = reason;
|
|
return;
|
|
}
|
|
this.building = true;
|
|
var t0 = this.clock.now();
|
|
try {
|
|
await this.build(reason);
|
|
} finally {
|
|
this.lastBuildMs = this.clock.now() - t0;
|
|
this.building = false;
|
|
}
|
|
if (this.pending != null) {
|
|
var next = this.pending;
|
|
this.pending = null;
|
|
await this.request(next);
|
|
}
|
|
}
|
|
}
|