Skip to content

Commit 1ec1ddc

Browse files
committed
Make reusable polling scheduler API
1 parent 00835e8 commit 1ec1ddc

File tree

2 files changed

+86
-32
lines changed

2 files changed

+86
-32
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { moduleLogger } from "#common/logger/index.ts";
2+
import { dateToHMSString } from "#common/time.ts";
3+
4+
const logger = moduleLogger();
5+
6+
export interface PollingSchedulerOptions<T> {
7+
discriminator: string;
8+
pollRate: number;
9+
10+
poll: (startInclusive: Date, endExclusive: Date) => Promise<T[]>;
11+
run: (task: T) => Promise<void>;
12+
13+
getTimestamp: (task: T) => Date;
14+
debugFormat: (task: T) => string;
15+
}
16+
17+
export interface PollingSchedulerHandle<T> {
18+
track: (task: T) => void;
19+
}
20+
21+
interface State<T> {
22+
options: PollingSchedulerOptions<T>;
23+
nextStartTimestamp: Date;
24+
}
25+
26+
/**
27+
* Start polling for tasks.
28+
* @return A handle to the scheduler, after the first poll is done
29+
*/
30+
export async function startPollingScheduler<T>(options: PollingSchedulerOptions<T>): Promise<PollingSchedulerHandle<T>> {
31+
const state: State<T> = {
32+
options,
33+
nextStartTimestamp: new Date(0),
34+
};
35+
36+
await poll(state);
37+
setInterval(() => poll(state), state.options.pollRate).unref();
38+
39+
return {
40+
track: task => {
41+
if (state.options.getTimestamp(task) >= state.nextStartTimestamp)
42+
return;
43+
44+
setRunTimeout(state, task);
45+
}
46+
};
47+
}
48+
49+
async function poll<E>(state: State<E>): Promise<void> {
50+
const end = new Date(Date.now() + state.options.pollRate);
51+
52+
logger.debug?.(
53+
end.getTime()
54+
? `Setting initial timeouts for #${state.options.discriminator} tasks`
55+
: `Setting timeouts for #${state.options.discriminator} tasks `
56+
+ ` from ${dateToHMSString(state.nextStartTimestamp)}`
57+
+ ` to ${dateToHMSString(end)}`
58+
);
59+
60+
const tasks = await state.options.poll(state.nextStartTimestamp, end);
61+
state.nextStartTimestamp = end;
62+
63+
for (const task of tasks) {
64+
setRunTimeout(state, task);
65+
}
66+
}
67+
68+
async function setRunTimeout<T>(state: State<T>, task: T) {
69+
const delay = Math.max(0, state.options.getTimestamp(task).getTime() - Date.now());
70+
setTimeout(() => state.options.run(task), delay).unref();
71+
72+
logger.debug?.(`Setting up timeout for #${state.options.discriminator} task ${state.options.debugFormat(task)} with delay ${delay}`);
73+
}

backend/src/plugin/reminders/scheduler.ts

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { debugFormatChannel } from "#common/discord/debugFormat.ts";
33
import { isTextableChannel, isThreadChannelType } from "#common/discord/general.ts";
44
import { canWriteInChannel } from "#common/discord/permissions.ts";
55
import { moduleLogger } from "#common/logger/index.ts";
6-
import { dateToHMSString, dateToUnixSecs } from "#common/time.ts";
6+
import { startPollingScheduler, type PollingSchedulerHandle } from "#common/pollingScheduler.ts";
7+
import { dateToHMSString, dateToUnixSecs, SECOND } from "#common/time.ts";
78
import { onBotInit } from "#discord/extensionPoints.ts";
89
import { bot } from "#discord/index.ts";
910
import { icons } from "#plugin/core/public/icons.ts";
@@ -13,45 +14,25 @@ import { DiscordRESTError, MessageFlags, Permissions, type AnyTextableChannel }
1314

1415
const logger = moduleLogger();
1516

16-
const TIMEOUT_POLL_RATE = 60 * 1000;
17-
18-
let nextExpiryStartTime = new Date(0);
17+
let scheduler: PollingSchedulerHandle<Reminder> | null = null;
1918

2019
export default [onBotInit(beginPollingReminders)];
2120

2221
async function beginPollingReminders(): Promise<void> {
23-
await poll();
24-
setInterval(poll, TIMEOUT_POLL_RATE).unref();
25-
}
26-
27-
export function trackNewReminder(reminder: Reminder): void {
28-
if (reminder.firesAt.getTime() >= nextExpiryStartTime.getTime())
29-
return;
30-
31-
setTimeout(() => fire(reminder), Math.max(0, reminder.firesAt.getTime() - Date.now())).unref();
32-
}
33-
34-
async function poll(): Promise<void> {
35-
const end = new Date(Date.now() + TIMEOUT_POLL_RATE);
36-
37-
logger.debug?.(
38-
nextExpiryStartTime.getTime() === 0
39-
? "Setting initial timeouts for missed and upcoming reminders until " + dateToHMSString(end)
40-
: "Setting timeouts for reminders from " + dateToHMSString(nextExpiryStartTime) + " to " + dateToHMSString(end)
41-
);
22+
scheduler = await startPollingScheduler({
23+
discriminator: "reminders",
24+
pollRate: 60 * SECOND,
4225

43-
const reminders = await getRemindersByFiresAt(nextExpiryStartTime, end);
44-
nextExpiryStartTime = end;
26+
poll: (start, end) => getRemindersByFiresAt(start, end),
27+
run: fire,
4528

46-
for (const reminder of reminders)
47-
setFireTimeout(reminder);
29+
getTimestamp: task => task.firesAt,
30+
debugFormat: debugFormatReminder
31+
});
4832
}
4933

50-
function setFireTimeout(reminder: Reminder): void {
51-
const delay = Math.max(0, reminder.firesAt.getTime() - Date.now());
52-
setTimeout(() => fire(reminder), delay);
53-
54-
logger.debug?.(`Setting up timeout for reminder ${debugFormatReminder(reminder)} with delay ${delay}`);
34+
export function trackNewReminder(reminder: Reminder): void {
35+
scheduler?.track(reminder);
5536
}
5637

5738
async function fire(reminder: Reminder): Promise<void> {

0 commit comments

Comments
 (0)