From 692d24875c892f6fff2e558ad4ef858f332d4748 Mon Sep 17 00:00:00 2001 From: Kris Baumgartner Date: Wed, 29 May 2024 22:21:47 -0700 Subject: [PATCH 1/4] Add Scheduler class --- .../scheduler/src/class/scheduler.test.ts | 180 ++++++++++++++++++ packages/scheduler/src/class/scheduler.ts | 78 ++++++++ packages/scheduler/src/class/types.ts | 8 + .../src/class/utils/create-options-fns.ts | 43 +++++ 4 files changed, 309 insertions(+) create mode 100644 packages/scheduler/src/class/scheduler.test.ts create mode 100644 packages/scheduler/src/class/scheduler.ts create mode 100644 packages/scheduler/src/class/types.ts create mode 100644 packages/scheduler/src/class/utils/create-options-fns.ts diff --git a/packages/scheduler/src/class/scheduler.test.ts b/packages/scheduler/src/class/scheduler.test.ts new file mode 100644 index 0000000..373ecba --- /dev/null +++ b/packages/scheduler/src/class/scheduler.test.ts @@ -0,0 +1,180 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { Scheduler } from './scheduler'; + +describe('Scheduler Class', () => { + let order: string[] = []; + + const aFn = vi.fn(() => { + order.push('A'); + }); + + const bFn = vi.fn(() => { + order.push('B'); + }); + + const cFn = vi.fn(() => { + order.push('C'); + }); + + const dFn = vi.fn(() => { + order.push('D'); + }); + + const eFn = vi.fn(() => { + order.push('E'); + }); + + const fFn = vi.fn(() => { + order.push('F'); + }); + + beforeEach(() => { + order = []; + + aFn.mockClear(); + bFn.mockClear(); + cFn.mockClear(); + dFn.mockClear(); + eFn.mockClear(); + fFn.mockClear(); + }); + + test('scheduler with a single runnable', () => { + const schedule = new Scheduler(); + + schedule.add(aFn, { id: 'A' }); + + schedule.run({}); + + expect(aFn).toBeCalledTimes(1); + + expect(order).toEqual(['A']); + }); + + test('schedule a runnable with before', () => { + const schedule = new Scheduler(); + + schedule.add(aFn, { id: 'A' }); + schedule.add(bFn, { id: 'B', before: 'A' }); + + schedule.run({}); + + expect(aFn).toBeCalledTimes(1); + expect(bFn).toBeCalledTimes(1); + + expect(order).toEqual(['B', 'A']); + }); + + test('schedule a runnable with after', () => { + const schedule = new Scheduler(); + + schedule.add(aFn, { id: 'A' }); + schedule.add(bFn, { id: 'B', after: 'A' }); + + schedule.run({}); + + expect(aFn).toBeCalledTimes(1); + expect(bFn).toBeCalledTimes(1); + + expect(order).toEqual(['A', 'B']); + }); + + test('schedule a runnable after multiple runnables', () => { + const schedule = new Scheduler(); + + schedule.add(aFn, { id: 'A' }); + schedule.add(bFn, { id: 'B' }); + schedule.add(cFn, { id: 'C', after: ['A', 'B'] }); + schedule.add(dFn, { id: 'D', after: 'C' }); + + schedule.run({}); + + expect(aFn).toBeCalledTimes(1); + expect(bFn).toBeCalledTimes(1); + expect(cFn).toBeCalledTimes(1); + expect(dFn).toBeCalledTimes(1); + + expect(order).toEqual(['A', 'B', 'C', 'D']); + }); + + test('schedule a runnable with tag', () => { + const group1 = Symbol(); + const schedule = new Scheduler(); + + schedule.add(aFn, { id: 'A', tag: group1 }); + schedule.add(bFn, { id: 'B', after: 'A', tag: group1 }); + + schedule.run({}); + + expect(aFn).toBeCalledTimes(1); + expect(bFn).toBeCalledTimes(1); + + expect(order).toEqual(['A', 'B']); + }); + + test('schedule a runnable before and after a tag', () => { + const group1 = Symbol(); + const schedule = new Scheduler(); + + schedule.add(aFn, { id: 'A', tag: group1 }); + schedule.add(bFn, { id: 'B', after: 'A', tag: group1 }); + schedule.add(cFn, { id: 'C', before: group1 }); + schedule.add(dFn, { id: 'D', after: group1 }); + + schedule.run({}); + + expect(aFn).toBeCalledTimes(1); + expect(bFn).toBeCalledTimes(1); + expect(cFn).toBeCalledTimes(1); + expect(dFn).toBeCalledTimes(1); + + expect(order).toEqual(['C', 'A', 'B', 'D']); + }); + + test('schedule a runnable into an existing tag', () => { + const group1 = Symbol(); + const schedule = new Scheduler(); + + schedule.add(aFn, { id: 'A', tag: group1 }); + schedule.add(bFn, { id: 'B', after: 'A', tag: group1 }); + + schedule.add(cFn, { id: 'C', before: group1 }); + schedule.add(dFn, { id: 'D', after: group1 }); + + schedule.add(eFn, { id: 'E', tag: group1 }); + + schedule.run({}); + + expect(aFn).toBeCalledTimes(1); + expect(bFn).toBeCalledTimes(1); + expect(cFn).toBeCalledTimes(1); + expect(dFn).toBeCalledTimes(1); + expect(eFn).toBeCalledTimes(1); + + expect(order).toEqual(['C', 'A', 'E', 'B', 'D']); + }); + + test('schedule a tag before or after another tag', () => { + const group1 = Symbol(); + const group2 = Symbol(); + const group3 = Symbol(); + + const schedule = new Scheduler(); + + schedule.createTag(group1); + schedule.createTag(group2, { before: group1 }); + schedule.createTag(group3, { after: group1 }); + + schedule.add(aFn, { tag: group1, id: 'A' }); + schedule.add(bFn, { tag: group2, id: 'B' }); + schedule.add(cFn, { tag: group3, id: 'C' }); + + schedule.run({}); + + expect(aFn).toBeCalledTimes(1); + expect(bFn).toBeCalledTimes(1); + expect(cFn).toBeCalledTimes(1); + + expect(order).toEqual(['B', 'A', 'C']); + }); +}); diff --git a/packages/scheduler/src/class/scheduler.ts b/packages/scheduler/src/class/scheduler.ts new file mode 100644 index 0000000..5ec4f7a --- /dev/null +++ b/packages/scheduler/src/class/scheduler.ts @@ -0,0 +1,78 @@ +import { DirectedGraph } from '../directed-graph'; +import { + add, + build, + createTag, + debug, + remove, + removeTag, + run, +} from '../scheduler'; +import { Runnable, Tag } from '../scheduler-types'; +import { OptionsObject } from './types'; +import { createOptionsFns } from './utils/create-options-fns'; + +export class Scheduler { + dag: DirectedGraph>; + tags: Map>; + symbols: Map>; + + constructor() { + this.dag = new DirectedGraph>(); + this.tags = new Map>(); + this.symbols = new Map>(); + } + + add(runnable: Runnable, options?: OptionsObject): Scheduler { + // If there are tags, check if they exist, otherwise create them. + if (options?.tag) { + if (Array.isArray(options.tag)) { + options.tag.forEach((tag) => { + if (!this.tags.has(tag)) { + createTag(this, tag); + } + }); + } else { + if (!this.tags.has(options.tag)) { + createTag(this, options.tag); + } + } + } + + const optionsFns = createOptionsFns(options); + add(this, runnable, ...optionsFns); + + return this; + } + + run(context: Scheduler.Context): Scheduler { + run(this, context); + return this; + } + + createTag(id: symbol | string, options?: OptionsObject): Scheduler { + const optionsFns = createOptionsFns(options); + createTag(this, id, ...optionsFns); + return this; + } + + removeTag(id: symbol | string): Scheduler { + removeTag(this, id); + return this; + } + + build(): Scheduler { + build(this); + return this; + } + + remove(runnable: Runnable): Scheduler { + remove(this, runnable); + return this; + } + + debug(): Scheduler { + debug(this); + return this; + } +} diff --git a/packages/scheduler/src/class/types.ts b/packages/scheduler/src/class/types.ts new file mode 100644 index 0000000..f352223 --- /dev/null +++ b/packages/scheduler/src/class/types.ts @@ -0,0 +1,8 @@ +import { Runnable } from '../scheduler-types'; + +export type OptionsObject = { + id?: symbol | string; + before?: symbol | string | Runnable | (symbol | string | Runnable)[]; + after?: symbol | string | Runnable | (symbol | string | Runnable)[]; + tag?: symbol | string | (symbol | string)[]; +}; diff --git a/packages/scheduler/src/class/utils/create-options-fns.ts b/packages/scheduler/src/class/utils/create-options-fns.ts new file mode 100644 index 0000000..a6c81cd --- /dev/null +++ b/packages/scheduler/src/class/utils/create-options-fns.ts @@ -0,0 +1,43 @@ +import { + after as afterFn, + before as beforeFn, + id as idFn, + tag as tagFn, +} from '../../scheduler'; +import { OptionsFn } from '../../scheduler-types'; +import { OptionsObject } from '../types'; + +export function createOptionsFns(options: OptionsObject | undefined) { + const optionsFns: OptionsFn[] = []; + + if (options?.id) { + optionsFns.push(idFn(options.id)); + } + + if (options?.before) { + if (Array.isArray(options.before)) { + optionsFns.push( + ...options.before.map((before) => beforeFn(before)) + ); + } else { + optionsFns.push(beforeFn(options.before)); + } + } + + if (options?.after) { + if (Array.isArray(options.after)) { + optionsFns.push(...options.after.map((after) => afterFn(after))); + } else { + optionsFns.push(afterFn(options.after)); + } + } + + if (options?.tag) { + if (Array.isArray(options.tag)) { + optionsFns.push(...options.tag.map((tag) => tagFn(tag))); + } else { + optionsFns.push(tagFn(options.tag)); + } + } + return optionsFns; +} From 5ad27f9a409431d7aea3f9c5183a2af3a053010a Mon Sep 17 00:00:00 2001 From: Kris Baumgartner Date: Thu, 30 May 2024 15:38:59 -0700 Subject: [PATCH 2/4] Remove auto-tag creation --- packages/scheduler/src/class/scheduler.test.ts | 6 ++++++ packages/scheduler/src/class/scheduler.ts | 15 --------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/packages/scheduler/src/class/scheduler.test.ts b/packages/scheduler/src/class/scheduler.test.ts index 373ecba..7cdd30b 100644 --- a/packages/scheduler/src/class/scheduler.test.ts +++ b/packages/scheduler/src/class/scheduler.test.ts @@ -101,6 +101,8 @@ describe('Scheduler Class', () => { const group1 = Symbol(); const schedule = new Scheduler(); + schedule.createTag(group1); + schedule.add(aFn, { id: 'A', tag: group1 }); schedule.add(bFn, { id: 'B', after: 'A', tag: group1 }); @@ -116,6 +118,8 @@ describe('Scheduler Class', () => { const group1 = Symbol(); const schedule = new Scheduler(); + schedule.createTag(group1); + schedule.add(aFn, { id: 'A', tag: group1 }); schedule.add(bFn, { id: 'B', after: 'A', tag: group1 }); schedule.add(cFn, { id: 'C', before: group1 }); @@ -135,6 +139,8 @@ describe('Scheduler Class', () => { const group1 = Symbol(); const schedule = new Scheduler(); + schedule.createTag(group1); + schedule.add(aFn, { id: 'A', tag: group1 }); schedule.add(bFn, { id: 'B', after: 'A', tag: group1 }); diff --git a/packages/scheduler/src/class/scheduler.ts b/packages/scheduler/src/class/scheduler.ts index 5ec4f7a..bfbadf0 100644 --- a/packages/scheduler/src/class/scheduler.ts +++ b/packages/scheduler/src/class/scheduler.ts @@ -24,21 +24,6 @@ export class Scheduler { } add(runnable: Runnable, options?: OptionsObject): Scheduler { - // If there are tags, check if they exist, otherwise create them. - if (options?.tag) { - if (Array.isArray(options.tag)) { - options.tag.forEach((tag) => { - if (!this.tags.has(tag)) { - createTag(this, tag); - } - }); - } else { - if (!this.tags.has(options.tag)) { - createTag(this, options.tag); - } - } - } - const optionsFns = createOptionsFns(options); add(this, runnable, ...optionsFns); From 7ab7ed77b385b6cf85b542045ed88374af3575e3 Mon Sep 17 00:00:00 2001 From: Kris Baumgartner Date: Fri, 31 May 2024 19:01:44 -0700 Subject: [PATCH 3/4] Add missing methods to class --- packages/scheduler/src/class/scheduler.ts | 30 ++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/scheduler/src/class/scheduler.ts b/packages/scheduler/src/class/scheduler.ts index bfbadf0..44d0cdd 100644 --- a/packages/scheduler/src/class/scheduler.ts +++ b/packages/scheduler/src/class/scheduler.ts @@ -35,17 +35,6 @@ export class Scheduler { return this; } - createTag(id: symbol | string, options?: OptionsObject): Scheduler { - const optionsFns = createOptionsFns(options); - createTag(this, id, ...optionsFns); - return this; - } - - removeTag(id: symbol | string): Scheduler { - removeTag(this, id); - return this; - } - build(): Scheduler { build(this); return this; @@ -60,4 +49,23 @@ export class Scheduler { debug(this); return this; } + + createTag(id: symbol | string, options?: OptionsObject): Scheduler { + const optionsFns = createOptionsFns(options); + createTag(this, id, ...optionsFns); + return this; + } + + removeTag(id: symbol | string): Scheduler { + removeTag(this, id); + return this; + } + + hasTag(id: symbol | string): boolean { + return this.tags.has(id); + } + + getRunnable(id: symbol | string): Runnable | undefined { + return this.symbols.get(id); + } } From a272e6420f3ff8c3dfdf506b9a7e7680258dc29a Mon Sep 17 00:00:00 2001 From: Kris Baumgartner Date: Fri, 31 May 2024 19:29:36 -0700 Subject: [PATCH 4/4] Fix type generics --- packages/scheduler/src/class/scheduler.ts | 36 ++++++------ .../src/class/utils/create-options-fns.ts | 6 +- packages/scheduler/src/scheduler.ts | 58 +++++++++++++------ 3 files changed, 62 insertions(+), 38 deletions(-) diff --git a/packages/scheduler/src/class/scheduler.ts b/packages/scheduler/src/class/scheduler.ts index 44d0cdd..f355c7b 100644 --- a/packages/scheduler/src/class/scheduler.ts +++ b/packages/scheduler/src/class/scheduler.ts @@ -8,55 +8,55 @@ import { removeTag, run, } from '../scheduler'; -import { Runnable, Tag } from '../scheduler-types'; -import { OptionsObject } from './types'; +import type { Runnable, Tag } from '../scheduler-types'; +import type { OptionsObject } from './types'; import { createOptionsFns } from './utils/create-options-fns'; -export class Scheduler { - dag: DirectedGraph>; +export class Scheduler { + dag: DirectedGraph>; tags: Map>; - symbols: Map>; + symbols: Map>; constructor() { - this.dag = new DirectedGraph>(); - this.tags = new Map>(); - this.symbols = new Map>(); + this.dag = new DirectedGraph>(); + this.tags = new Map>(); + this.symbols = new Map>(); } - add(runnable: Runnable, options?: OptionsObject): Scheduler { - const optionsFns = createOptionsFns(options); + add(runnable: Runnable, options?: OptionsObject): Scheduler { + const optionsFns = createOptionsFns(options); add(this, runnable, ...optionsFns); return this; } - run(context: Scheduler.Context): Scheduler { + run(context: T): Scheduler { run(this, context); return this; } - build(): Scheduler { + build(): Scheduler { build(this); return this; } - remove(runnable: Runnable): Scheduler { + remove(runnable: Runnable): Scheduler { remove(this, runnable); return this; } - debug(): Scheduler { + debug(): Scheduler { debug(this); return this; } - createTag(id: symbol | string, options?: OptionsObject): Scheduler { - const optionsFns = createOptionsFns(options); + createTag(id: symbol | string, options?: OptionsObject): Scheduler { + const optionsFns = createOptionsFns(options); createTag(this, id, ...optionsFns); return this; } - removeTag(id: symbol | string): Scheduler { + removeTag(id: symbol | string): Scheduler { removeTag(this, id); return this; } @@ -65,7 +65,7 @@ export class Scheduler { return this.tags.has(id); } - getRunnable(id: symbol | string): Runnable | undefined { + getRunnable(id: symbol | string): Runnable | undefined { return this.symbols.get(id); } } diff --git a/packages/scheduler/src/class/utils/create-options-fns.ts b/packages/scheduler/src/class/utils/create-options-fns.ts index a6c81cd..557bbc4 100644 --- a/packages/scheduler/src/class/utils/create-options-fns.ts +++ b/packages/scheduler/src/class/utils/create-options-fns.ts @@ -7,7 +7,9 @@ import { import { OptionsFn } from '../../scheduler-types'; import { OptionsObject } from '../types'; -export function createOptionsFns(options: OptionsObject | undefined) { +export function createOptionsFns< + T extends Scheduler.Context = Scheduler.Context +>(options: OptionsObject | undefined): OptionsFn[] { const optionsFns: OptionsFn[] = []; if (options?.id) { @@ -39,5 +41,5 @@ export function createOptionsFns(options: OptionsObject | undefined) { optionsFns.push(tagFn(options.tag)); } } - return optionsFns; + return optionsFns as OptionsFn[]; } diff --git a/packages/scheduler/src/scheduler.ts b/packages/scheduler/src/scheduler.ts index b721fb1..bbcc6d9 100644 --- a/packages/scheduler/src/scheduler.ts +++ b/packages/scheduler/src/scheduler.ts @@ -181,7 +181,10 @@ export function create< * @param {Schedule} schedule - The schedule containing the runnables to execute. * @param {Context} context - The context to be passed to each runnable. */ -export function run(schedule: Schedule, context: Scheduler.Context) { +export function run( + schedule: Schedule, + context: T +) { for (let i = 0; i < schedule.dag.sorted.length; i++) { const runnable = schedule.dag.sorted[i]; runnable(context); @@ -195,7 +198,10 @@ export function run(schedule: Schedule, context: Scheduler.Context) { * @param {symbol | string} id - The ID of the tag to remove. * @return {void} This function does not return anything. */ -export function removeTag(schedule: Schedule, id: symbol | string) { +export function removeTag( + schedule: Schedule, + id: symbol | string +) { const tag = schedule.tags.get(id); if (!tag) { @@ -215,7 +221,10 @@ export function removeTag(schedule: Schedule, id: symbol | string) { * @param {symbol | string} id - The ID of the tag to check. * @return {boolean} Returns true if the tag exists, false otherwise. */ -export function hasTag(schedule: Schedule, id: symbol | string) { +export function hasTag( + schedule: Schedule, + id: symbol | string +) { return schedule.tags.has(id); } @@ -228,12 +237,12 @@ export function hasTag(schedule: Schedule, id: symbol | string) { * @param {...OptionsFn[]} options - Additional options to customize the tag. * @return {Tag} The newly created tag. */ -export function createTag( - schedule: Schedule, +export function createTag( + schedule: Schedule, id: symbol | string, - ...options: OptionsFn[] + ...options: OptionsFn[] ): Tag { - if (hasTag(schedule, id)) { + if (hasTag(schedule, id)) { throw new Error(`Tag with id ${String(id)} already exists`); } @@ -256,7 +265,7 @@ export function createTag( const tag = { id, before, after }; - const optionParams: Options = { + const optionParams: Options = { dag: schedule.dag, tag, schedule, @@ -281,10 +290,10 @@ export function createTag( * @throws {Error} If the runnable already exists in the schedule. * @return {void} */ -export function add( - schedule: Schedule, - runnable: Runnable, - ...options: OptionsFn[] +export function add( + schedule: Schedule, + runnable: Runnable, + ...options: OptionsFn[] ) { if (schedule.dag.exists(runnable)) { throw new Error('Runnable already exists in schedule'); @@ -293,7 +302,7 @@ export function add( // add the runnable to the graph schedule.dag.addVertex(runnable, {}); - const optionParams: Options = { + const optionParams: Options = { dag: schedule.dag, runnable, schedule, @@ -314,7 +323,9 @@ export function add( * @param {Schedule} schedule - The schedule to be built. * @return {void} This function does not return anything. */ -export function build(schedule: Schedule) { +export function build( + schedule: Schedule +) { schedule.dag.topSort(); } @@ -325,7 +336,10 @@ export function build(schedule: Schedule) { * @param {Runnable} runnable - The runnable to remove from the schedule. * @return {void} This function does not return anything. */ -export function remove(schedule: Schedule, runnable: Runnable) { +export function remove( + schedule: Schedule, + runnable: Runnable +) { schedule.dag.removeVertex(runnable); } @@ -336,7 +350,10 @@ export function remove(schedule: Schedule, runnable: Runnable) { * @param {symbol | string} id - The ID of the runnable to retrieve. * @return {Runnable | undefined} The retrieved runnable or undefined if not found. */ -export function getRunnable(schedule: Schedule, id: symbol | string) { +export function getRunnable( + schedule: Schedule, + id: symbol | string +) { return schedule.symbols.get(id); } @@ -347,7 +364,10 @@ export function getRunnable(schedule: Schedule, id: symbol | string) { * @param {symbol | string} id - The ID of the tag to retrieve. * @return {Tag | undefined} The retrieved tag or undefined if not found. */ -export function getTag(schedule: Schedule, id: symbol | string) { +export function getTag( + schedule: Schedule, + id: symbol | string +) { return schedule.tags.get(id); } @@ -357,6 +377,8 @@ export function getTag(schedule: Schedule, id: symbol | string) { * @param {Schedule} schedule - The schedule containing the DAG to visualize. * @return {void} This function does not return anything. */ -export function debug(schedule: Schedule) { +export function debug( + schedule: Schedule +) { schedule.dag.asciiVisualize(); }