Skip to content

Commit

Permalink
implement schedule function
Browse files Browse the repository at this point in the history
  • Loading branch information
rinick committed Apr 26, 2024
1 parent 0652bc3 commit 73cfb6b
Show file tree
Hide file tree
Showing 10 changed files with 431 additions and 99 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@types/node": "^20.11.30",
"@types/qs": "^6.9.14",
"@types/react": "^18.2.69",
"@types/react-big-calendar": "^1",
"@types/react-color": "^3.0.12",
"@types/react-dom": "^18.2.22",
"@types/shelljs": "^0.8.15",
Expand Down Expand Up @@ -78,6 +79,7 @@
"rc-dock": "^3.2.19",
"rc-trigger": "^5.3.4",
"react": "^18.2.0",
"react-big-calendar": "^1.12.0",
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
"resize-observer-polyfill": "^1.5.1",
Expand Down
22 changes: 15 additions & 7 deletions src/core/block/Event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,8 @@ export class Event {
static get uid(): string {
return Event._uid.current;
}

loopId: string;
type: string;

constructor(type: string) {
constructor(public readonly type: string) {
this.type = type;
this.loopId = Event.uid;
}
Expand All @@ -39,9 +36,10 @@ export class Event {
}

export class ErrorEvent extends Event {
detail: unknown;

constructor(type: string, detail?: unknown) {
constructor(
type: string,
public readonly detail?: unknown
) {
super(type);
this.detail = detail;
}
Expand Down Expand Up @@ -77,5 +75,15 @@ export class CompleteEvent extends Event {
}
}

export class ValueUpdateEvent extends Event {
constructor(
public readonly name: string,
public readonly value: unknown,
public readonly ts?: number
) {
super('valueUpdate');
}
}

export const WAIT = new WaitEvent();
export const NO_EMIT = new NoEmit();
57 changes: 57 additions & 0 deletions src/core/functions/base/AutoUpdateFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {FunctionData, ImpureFunction} from '../../block/BlockFunction';
import {ScheduleEvent, setSchedule} from '../../util/SetSchedule';
import {Block} from '../../block/Block';
import type {BlockConfig} from '../../block/BlockProperty';

export abstract class AutoUpdateFunction extends ImpureFunction {
#schedule: ScheduleEvent;
#onTimer = (time: number) => {
this.#schedule = null;
if (this._data instanceof Block) {
this._data._queueFunction();
}
};

#autoUpdate: boolean;
#setAutoUpdate(v: boolean) {
if (v !== this.#autoUpdate && this._data instanceof Block) {
this.#autoUpdate = v;
if (!v && this.#schedule) {
this.#schedule.cancel();
this.#schedule = null;
}
return true;
}
return false;
}

addSchedule(nextCheck: number) {
if (this.#autoUpdate) {
if (nextCheck !== this.#schedule?.start) {
this.#schedule?.cancel();
this.#schedule = setSchedule(this.#onTimer, nextCheck);
}
}
}

constructor(data: FunctionData) {
super(data);
this.#autoUpdate = data instanceof Block;
}

configChanged(config: BlockConfig, val: unknown): boolean {
if (config._name === 'mode') {
return this.#setAutoUpdate(config._value == null || config._value === 'auto');
}
return false;
}

cleanup() {
this.#schedule?.cancel();
super.cleanup();
}
destroy() {
this.#schedule?.cancel();
super.destroy();
}
}
57 changes: 4 additions & 53 deletions src/core/functions/date/GenerateRange.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,12 @@
import {FunctionData, ImpureFunction, PureFunction} from '../../block/BlockFunction';
import {Functions} from '../../block/Functions';
import {DateTime} from 'luxon';
import {invalidDate} from '../../util/DateTime';
import type {BlockConfig} from '../../block/BlockProperty';
import {Block} from '../../block/Block';
import {ScheduleEvent, setSchedule} from '../../util/SetSchedule';
import {AutoUpdateFunction} from '../base/AutoUpdateFunction';

const UNIT_OPTIONS = ['year', 'month', 'day', 'hour', 'minute', 'week'] as const;
export type UNIT_TYPE = (typeof UNIT_OPTIONS)[number];

export class GenerateDateFunction extends ImpureFunction {
#schedule: ScheduleEvent;
#onTimer = (time: number) => {
this.#schedule = null;
if (this._data instanceof Block) {
this._data._queueFunction();
}
};

#autoUpdate: boolean;
#setAutoUpdate(v: boolean) {
if (v !== this.#autoUpdate && this._data instanceof Block) {
this.#autoUpdate = v;
if (!v && this.#schedule) {
this.#schedule.cancel();
this.#schedule = null;
}
return true;
}
return false;
}

constructor(data: FunctionData) {
super(data);
this.#autoUpdate = data instanceof Block;
}

configChanged(config: BlockConfig, val: unknown): boolean {
if (config._name === 'mode') {
return this.#setAutoUpdate(config._value == null || config._value === 'auto');
}
return false;
}

export class GenerateDateFunction extends AutoUpdateFunction {
run() {
const count = Number(this._data.getValue('count') ?? 1);
const unit = (this._data.getValue('unit') as UNIT_TYPE) ?? 'day';
Expand Down Expand Up @@ -77,12 +41,7 @@ export class GenerateDateFunction extends ImpureFunction {
const result = [start.plus({[unit]: dStart}), start.plus({[unit]: dEnd}).endOf(unit)];
this._data.output(result);

if (this.#autoUpdate) {
if (nextCheck !== this.#schedule?.start) {
this.#schedule?.cancel();
this.#schedule = setSchedule(this.#onTimer, nextCheck);
}
}
this.addSchedule(nextCheck);

return;
} catch (err) {
Expand All @@ -91,21 +50,13 @@ export class GenerateDateFunction extends ImpureFunction {
}
this._data.output([invalidDate, invalidDate]);
}
cleanup() {
this.#schedule?.cancel();
super.cleanup();
}
destroy() {
this.#schedule?.cancel();
super.destroy();
}
}

Functions.add(
GenerateDateFunction,
{
name: 'generate-range',
icon: 'fas:clock',
icon: 'fas:calendar',
priority: 0,
properties: [
{name: 'mode', type: 'select', options: ['previous', 'next'], init: 'previous', pinned: true},
Expand Down
136 changes: 130 additions & 6 deletions src/core/functions/date/Schedule/Schedule.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,139 @@
import {BlockFunction} from '../../../block/BlockFunction';
import {Functions} from '../../../block/Functions';
import {AutoUpdateFunction} from '../../base/AutoUpdateFunction';
import {type EventOccur, ScheduleEvent} from './ScheduleEvent';
import {BlockIO} from '../../../block/BlockProperty';
import {ValueUpdateEvent} from '../../../block/Event';

export class ScheduleFunction extends BlockFunction {
run(): unknown {
return undefined;
export class ScheduleValue {
occur: EventOccur;
constructor(
public readonly event: ScheduleEvent,
public value: unknown,
public index: number
) {}
getOccur(ts: number, timezone: string): EventOccur {
this.occur = this.event.getOccur(ts, timezone);
return this.occur;
}
shouldReplace(current: ScheduleValue) {
if (!current) {
return true;
}
if (this.event.urgency === current.event.urgency) {
if (this.occur.start === current.occur.start) {
// when everything is same, lower index is more important
return this.index < current.index;
}
return this.occur.start > current.occur.start;
}
return this.event.urgency > current.event.urgency;
}
shouldReplaceNext(current: ScheduleValue, next: ScheduleValue) {
if (current) {
// not need to check anything happens after current
if (this.occur.start > current.occur.end) {
return false;
}
// this need have higher priority, otherwise it still happens after current
if (!this.shouldReplace(current)) {
return false;
}
}
if (next) {
if (this.occur.start === next.occur.start) {
return this.shouldReplace(next);
}
// we only care about the earliest event that happens next
return this.occur.start < next.occur.start;
}
return true;
}
}

export class ScheduleFunction extends AutoUpdateFunction {
#cache = new WeakMap<object, ScheduleValue>();
#events: ScheduleValue[];

inputChanged(input: BlockIO, val: unknown): boolean {
if (input._name !== 'default') {
// re-generate events when input changes.
this.#events = null;
}
return true;
}

run() {
const eventsData = this._data.getArray('', 1, ['config', 'value']) as {config: unknown; value: unknown}[];
if (!this.#events) {
// generate events
this.#events = [];
const newCache = new WeakMap<object, ScheduleValue>();
for (let i = 0; i < eventsData.length; i++) {
let {config, value} = eventsData[i];
let sv = this.#cache.get(config as object);
if (!sv) {
let event = ScheduleEvent.fromProperty(config);
if (!event) {
continue;
}
sv = new ScheduleValue(event, value, i);
} else {
sv.value = value;
sv.index = i;
}
newCache.set(config as object, sv);
this.#events.push(sv);
}
this.#cache = newCache;
}
let timezone = this._data.getValue('timezone') as string;
if (typeof timezone !== 'string') {
timezone = null;
}
const occurs: EventOccur[] = [];
let ts = new Date().getTime();
let current: ScheduleValue;
let next: ScheduleValue;
// check the current
for (let sv of this.#events) {
const occur = sv.getOccur(ts, timezone);
if (occur.isValid()) {
if (occur.start <= ts) {
if (sv.shouldReplace(current)) {
current = sv;
}
}
}
}
// check the next
for (let sv of this.#events) {
const occur = sv.occur;
if (occur.isValid()) {
if (occur.start > ts) {
if (sv.shouldReplaceNext(current, next)) {
next = sv;
}
}
}
}
if (next) {
this.addSchedule(next.occur.start);
} else if (current) {
this.addSchedule(current.occur.end + 1);
}

let outputValue = current ? current.value : this._data.getValue('default');
this._data.output(outputValue);

if (current) {
return new ValueUpdateEvent(current.event.name, current.value, current.occur.start);
}
}
}

Functions.add(ScheduleFunction, {
name: 'schedule',
icon: 'fas:calendar',
icon: 'fas:calendar-days',
priority: 1,
properties: [
{
Expand All @@ -21,7 +145,7 @@ Functions.add(ScheduleFunction, {
{name: 'value', type: 'any', pinned: true},
],
},
{name: 'default', type: 'any'},
{name: 'default', type: 'any', pinned: true},
{name: 'timezone', type: 'string', default: ''},
{name: '#output', type: 'any', readonly: true, pinned: true},
],
Expand Down

0 comments on commit 73cfb6b

Please sign in to comment.