Skip to content

Commit

Permalink
Merge pull request #4 from pmndrs/feat/scheduler-class
Browse files Browse the repository at this point in the history
Add `Scheduler` class
  • Loading branch information
akdjr authored Jun 1, 2024
2 parents 955f609 + a272e64 commit 794b5b0
Show file tree
Hide file tree
Showing 5 changed files with 350 additions and 18 deletions.
186 changes: 186 additions & 0 deletions packages/scheduler/src/class/scheduler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
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.createTag(group1);

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.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 });
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.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 });
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']);
});
});
71 changes: 71 additions & 0 deletions packages/scheduler/src/class/scheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { DirectedGraph } from '../directed-graph';
import {
add,
build,
createTag,
debug,
remove,
removeTag,
run,
} from '../scheduler';
import type { Runnable, Tag } from '../scheduler-types';
import type { OptionsObject } from './types';
import { createOptionsFns } from './utils/create-options-fns';

export class Scheduler<T extends Scheduler.Context = Scheduler.Context> {
dag: DirectedGraph<Runnable<T>>;
tags: Map<symbol | string, Tag<any>>;
symbols: Map<symbol | string, Runnable<T>>;

constructor() {
this.dag = new DirectedGraph<Runnable<T>>();
this.tags = new Map<symbol | string, Tag<T>>();
this.symbols = new Map<symbol | string, Runnable<T>>();
}

add(runnable: Runnable<T>, options?: OptionsObject): Scheduler<T> {
const optionsFns = createOptionsFns<T>(options);
add(this, runnable, ...optionsFns);

return this;
}

run(context: T): Scheduler<T> {
run(this, context);
return this;
}

build(): Scheduler<T> {
build(this);
return this;
}

remove(runnable: Runnable): Scheduler<T> {
remove(this, runnable);
return this;
}

debug(): Scheduler<T> {
debug(this);
return this;
}

createTag(id: symbol | string, options?: OptionsObject): Scheduler<T> {
const optionsFns = createOptionsFns<T>(options);
createTag(this, id, ...optionsFns);
return this;
}

removeTag(id: symbol | string): Scheduler<T> {
removeTag(this, id);
return this;
}

hasTag(id: symbol | string): boolean {
return this.tags.has(id);
}

getRunnable(id: symbol | string): Runnable<T> | undefined {
return this.symbols.get(id);
}
}
8 changes: 8 additions & 0 deletions packages/scheduler/src/class/types.ts
Original file line number Diff line number Diff line change
@@ -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)[];
};
45 changes: 45 additions & 0 deletions packages/scheduler/src/class/utils/create-options-fns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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<
T extends Scheduler.Context = Scheduler.Context
>(options: OptionsObject | undefined): OptionsFn<T>[] {
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 as OptionsFn<T>[];
}
Loading

0 comments on commit 794b5b0

Please sign in to comment.