-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into dependabot/npm_and_yarn/word-wrap-1.2.4
- Loading branch information
Showing
8 changed files
with
407 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import { expect } from 'chai'; | ||
import { Deferred } from './deferred'; | ||
import { BusyStatus, BusyStatusTracker } from './BusyStatusTracker'; | ||
|
||
describe('BusyStatusTracker', () => { | ||
let tracker: BusyStatusTracker; | ||
|
||
let latestStatus: BusyStatus; | ||
|
||
beforeEach(() => { | ||
latestStatus = BusyStatus.idle; | ||
tracker = new BusyStatusTracker(); | ||
tracker.on('change', (value) => { | ||
latestStatus = value; | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
tracker?.destroy(); | ||
}); | ||
|
||
it('tracks a single run', () => { | ||
expect(latestStatus).to.eql(BusyStatus.idle); | ||
tracker.run(() => { | ||
expect(tracker.status).to.eql(BusyStatus.busy); | ||
}); | ||
expect(latestStatus).to.eql(BusyStatus.idle); | ||
}); | ||
|
||
it('tracks a single async flow', async () => { | ||
const deferred = new Deferred(); | ||
const finishedPromise = tracker.run(() => { | ||
return deferred.promise; | ||
}); | ||
expect(latestStatus).to.eql(BusyStatus.busy); | ||
|
||
deferred.resolve(); | ||
await finishedPromise; | ||
|
||
expect(latestStatus).to.eql(BusyStatus.idle); | ||
}); | ||
|
||
it('independently tracks multiple runs for same program', () => { | ||
tracker.run(() => { | ||
expect(latestStatus).to.eql(BusyStatus.busy); | ||
}); | ||
tracker.run(() => { | ||
expect(latestStatus).to.eql(BusyStatus.busy); | ||
}); | ||
expect(latestStatus).to.eql(BusyStatus.idle); | ||
}); | ||
|
||
it('tracks as `busy` one of the runs is still pending', async () => { | ||
const deferred = new Deferred(); | ||
tracker.run(() => { | ||
expect(latestStatus).to.eql(BusyStatus.busy); | ||
}); | ||
const finishedPromise = tracker.run(() => { | ||
expect(latestStatus).to.eql(BusyStatus.busy); | ||
return deferred.promise; | ||
}); | ||
expect(latestStatus).to.eql(BusyStatus.busy); | ||
|
||
deferred.resolve(); | ||
await finishedPromise; | ||
|
||
expect(latestStatus).to.eql(BusyStatus.idle); | ||
}); | ||
|
||
it('handles error during synchronous flow', () => { | ||
try { | ||
tracker.run(() => { | ||
throw new Error('Crash'); | ||
}); | ||
} catch { } | ||
expect(latestStatus).to.eql(BusyStatus.idle); | ||
}); | ||
|
||
it('handles error during async flow', async () => { | ||
try { | ||
await tracker.run(() => { | ||
return Promise.reject(new Error('Crash')); | ||
}); | ||
} catch { } | ||
expect(latestStatus).to.eql(BusyStatus.idle); | ||
}); | ||
|
||
it('only finalizes on the first call to finalize', () => { | ||
try { | ||
tracker.run((finalize) => { | ||
expect(latestStatus).to.eql(BusyStatus.busy); | ||
finalize(); | ||
expect(latestStatus).to.eql(BusyStatus.idle); | ||
finalize(); | ||
expect(latestStatus).to.eql(BusyStatus.idle); | ||
}); | ||
} catch { } | ||
expect(latestStatus).to.eql(BusyStatus.idle); | ||
}); | ||
|
||
it('supports multiple simultaneous projects', async () => { | ||
//run the projects out of order | ||
const deferred2 = new Deferred(); | ||
const run1Promise = tracker.run(() => { | ||
expect(latestStatus).to.eql(BusyStatus.busy); | ||
return deferred2.promise; | ||
}); | ||
|
||
const deferred1 = new Deferred(); | ||
const run2Promise = tracker.run(() => { | ||
expect(latestStatus).to.eql(BusyStatus.busy); | ||
return deferred1.promise; | ||
}); | ||
|
||
expect(latestStatus).to.eql(BusyStatus.busy); | ||
|
||
|
||
deferred1.resolve(); | ||
await run2Promise; | ||
expect(latestStatus).to.eql(BusyStatus.busy); | ||
|
||
deferred2.resolve(); | ||
await run1Promise; | ||
expect(latestStatus).to.eql(BusyStatus.idle); | ||
}); | ||
|
||
it('supports unsubscribing from events', () => { | ||
const changes = []; //contains every busy/idle status change | ||
const disconnect = tracker.on('change', (status) => changes.push(status)); | ||
|
||
expect(changes.length).to.eql(0); | ||
|
||
tracker.run(() => { }); | ||
expect(changes.length).to.eql(2); | ||
|
||
tracker.run(() => { }); | ||
expect(changes.length).to.eql(4); | ||
|
||
disconnect(); | ||
|
||
tracker.run(() => { }); | ||
expect(changes.length).to.eql(4); | ||
}); | ||
|
||
it('getStatus returns proper value', () => { | ||
expect(tracker.status).to.eql(BusyStatus.idle); | ||
tracker.run(() => { | ||
expect(tracker.status).to.eql(BusyStatus.busy); | ||
}); | ||
expect(tracker.status).to.eql(BusyStatus.idle); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { EventEmitter } from 'eventemitter3'; | ||
|
||
/** | ||
* Tracks the busy/idle status of various sync or async tasks | ||
* Reports the overall status to the client | ||
*/ | ||
export class BusyStatusTracker { | ||
/** | ||
* @readonly | ||
*/ | ||
public activeRuns = new Set<{ | ||
label?: string; | ||
startTime?: Date; | ||
}>(); | ||
|
||
/** | ||
* Start a new piece of work | ||
*/ | ||
public run<T, R = T | Promise<T>>(callback: (finalize?: FinalizeBuildStatusRun) => R, label?: string): R { | ||
const run = { | ||
label: label, | ||
startTime: new Date() | ||
}; | ||
this.activeRuns.add(run); | ||
|
||
if (this.activeRuns.size === 1) { | ||
this.emit('change', BusyStatus.busy); | ||
} | ||
|
||
let isFinalized = false; | ||
const finalizeRun = () => { | ||
if (isFinalized === false) { | ||
isFinalized = true; | ||
this.activeRuns.delete(run); | ||
if (this.activeRuns.size <= 0) { | ||
this.emit('change', BusyStatus.idle); | ||
} | ||
} | ||
}; | ||
|
||
let result: R | PromiseLike<R>; | ||
//call the callback function | ||
try { | ||
result = callback(finalizeRun); | ||
//if the result is a promise, don't finalize until it completes | ||
if (typeof (result as any)?.then === 'function') { | ||
return Promise.resolve(result).finally(finalizeRun).then(() => result) as any; | ||
} else { | ||
finalizeRun(); | ||
return result; | ||
} | ||
} catch (e) { | ||
finalizeRun(); | ||
throw e; | ||
} | ||
} | ||
|
||
private emitter = new EventEmitter<string, BusyStatus>(); | ||
|
||
public on(eventName: 'change', handler: (status: BusyStatus) => void) { | ||
this.emitter.on(eventName, handler); | ||
return () => { | ||
this.emitter.off(eventName, handler); | ||
}; | ||
} | ||
|
||
private emit(eventName: 'change', value: BusyStatus) { | ||
this.emitter.emit(eventName, value); | ||
} | ||
|
||
public destroy() { | ||
this.emitter.removeAllListeners(); | ||
} | ||
|
||
/** | ||
* The current status of the busy tracker. | ||
* @readonly | ||
*/ | ||
public get status() { | ||
return this.activeRuns.size === 0 ? BusyStatus.idle : BusyStatus.busy; | ||
} | ||
} | ||
|
||
export type FinalizeBuildStatusRun = (status?: BusyStatus) => void; | ||
|
||
export enum BusyStatus { | ||
busy = 'busy', | ||
idle = 'idle' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.