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; +}