Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.

Commit 9395a21

Browse files
committed
Add new methods to Schedule API
1 parent 026ce04 commit 9395a21

File tree

2 files changed

+223
-11
lines changed

2 files changed

+223
-11
lines changed

docs/concepts/schedule.mdx

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Parameters:
2121
- `fn` (string): The name of the action to be executed.
2222
- `...args` (unknown[]): Additional arguments to pass to the function.
2323

24+
Returns:
25+
- `Promise<string>`: A unique identifier for the scheduled event.
26+
2427
### `c.schedule.at(timestamp, fn, ...args)`
2528

2629
Schedules a function to be executed at a specific timestamp. This function persists across actor restarts, upgrades, or crashes.
@@ -31,6 +34,41 @@ Parameters:
3134
- `fn` (string): The name of the action to be executed.
3235
- `...args` (unknown[]): Additional arguments to pass to the function.
3336

37+
Returns:
38+
- `Promise<string>`: A unique identifier for the scheduled event.
39+
40+
### `c.schedule.list()`
41+
42+
Lists all scheduled events for the actor.
43+
44+
Returns:
45+
- `Promise<Alarm[]>`: An array of scheduled alarms, where each alarm has the following properties:
46+
- `id` (string): The unique identifier of the alarm
47+
- `createdAt` (number): The timestamp when the alarm was created
48+
- `triggersAt` (number): The timestamp when the alarm will trigger
49+
- `fn` (string): The name of the action to be executed
50+
- `args` (unknown[]): The arguments to pass to the function
51+
52+
### `c.schedule.get(alarmId)`
53+
54+
Gets details about a specific scheduled event.
55+
56+
Parameters:
57+
- `alarmId` (string): The unique identifier of the alarm to retrieve.
58+
59+
Returns:
60+
- `Promise<Alarm | undefined>`: The alarm details if found, undefined otherwise.
61+
62+
### `c.schedule.cancel(alarmId)`
63+
64+
Cancels a scheduled event.
65+
66+
Parameters:
67+
- `alarmId` (string): The unique identifier of the alarm to cancel.
68+
69+
Returns:
70+
- `Promise<void>`
71+
3472
## Scheduling Private Actions
3573

3674
Currently, scheduling can only trigger public actions. If the scheduled action is private, it needs to be secured with something like a token.
@@ -46,7 +84,7 @@ const reminderService = actor({
4684
},
4785

4886
actions: {
49-
setReminder: (c, userId, message, delayMs) => {
87+
setReminder: async (c, userId, message, delayMs) => {
5088
const reminderId = crypto.randomUUID();
5189

5290
// Store the reminder in state
@@ -57,7 +95,89 @@ const reminderService = actor({
5795
};
5896

5997
// Schedule the sendReminder action to run after the delay
60-
c.after(delayMs, "sendReminder", reminderId);
98+
// Store the alarmId for potential cancellation
99+
const alarmId = await c.schedule.after(delayMs, "sendReminder", reminderId);
100+
101+
return { reminderId, alarmId };
102+
},
103+
104+
cancelReminder: async (c, reminderId) => {
105+
const reminder = c.state.reminders[reminderId];
106+
if (!reminder) return { success: false };
107+
108+
// Cancel the scheduled reminder
109+
await c.schedule.cancel(reminder.alarmId);
110+
111+
// Clean up the reminder
112+
delete c.state.reminders[reminderId];
113+
114+
return { success: true };
115+
},
116+
117+
sendReminder: (c, reminderId) => {
118+
const reminder = c.state.reminders[reminderId];
119+
if (!reminder) return;
120+
121+
// Find the user's connection if they're online
122+
const userConn = c.conns.find(
123+
conn => conn.state.userId === reminder.userId
124+
);
125+
126+
if (userConn) {
127+
// Send the reminder to the user
128+
userConn.send("reminder", {
129+
message: reminder.message,
130+
scheduledAt: reminder.scheduledFor
131+
});
132+
} else {
133+
// If user is offline, store reminder for later delivery
134+
// ...
135+
}
136+
137+
// Clean up the processed reminder
138+
delete c.state.reminders[reminderId];
139+
}
140+
}
141+
});
142+
```
143+
144+
## Testing Schedules
145+
146+
```typescript
147+
import { actor } from "actor-core";
148+
149+
const reminderService = actor({
150+
state: {
151+
reminders: {}
152+
},
153+
154+
actions: {
155+
setReminder: async (c, userId, message, delayMs) => {
156+
const reminderId = crypto.randomUUID();
157+
158+
// Store the reminder in state
159+
c.state.reminders[reminderId] = {
160+
userId,
161+
message,
162+
scheduledFor: Date.now() + delayMs
163+
};
164+
165+
// Schedule the sendReminder action to run after the delay
166+
// Store the alarmId for potential cancellation
167+
const alarmId = await c.schedule.after(delayMs, "sendReminder", reminderId);
168+
169+
return { reminderId, alarmId };
170+
},
171+
172+
cancelReminder: async (c, reminderId) => {
173+
const reminder = c.state.reminders[reminderId];
174+
if (!reminder) return { success: false };
175+
176+
// Cancel the scheduled reminder
177+
await c.schedule.cancel(reminder.alarmId);
178+
179+
// Clean up the reminder
180+
delete c.state.reminders[reminderId];
61181

62182
return { reminderId };
63183
},

packages/actor-core/src/actor/schedule.ts

Lines changed: 101 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ interface ScheduleIndexEvent {
1616

1717
interface ScheduleEvent {
1818
timestamp: number;
19+
createdAt: number;
20+
fn: string;
21+
args: unknown[];
22+
}
23+
24+
export interface Alarm {
25+
id: string;
26+
createdAt: number;
27+
triggersAt: number;
1928
fn: string;
2029
args: unknown[];
2130
}
@@ -29,27 +38,109 @@ export class Schedule {
2938
this.#driver = driver;
3039
}
3140

32-
async after(duration: number, fn: string, ...args: unknown[]) {
33-
await this.#scheduleEvent(Date.now() + duration, fn, args);
41+
async after(duration: number, fn: string, ...args: unknown[]): Promise<string> {
42+
return this.#scheduleEvent(Date.now() + duration, fn, args);
43+
}
44+
45+
async at(timestamp: number, fn: string, ...args: unknown[]): Promise<string> {
46+
return this.#scheduleEvent(timestamp, fn, args);
47+
}
48+
49+
async get(alarmId: string): Promise<Alarm | null> {
50+
const event = await this.#driver.kvGet(
51+
this.#actor.id,
52+
KEYS.SCHEDULE.event(alarmId)
53+
) as ScheduleEvent | undefined;
54+
55+
if (!event) return null;
56+
57+
return {
58+
id: alarmId,
59+
createdAt: event.createdAt,
60+
triggersAt: event.timestamp,
61+
fn: event.fn,
62+
args: event.args
63+
};
64+
}
65+
66+
async list(): Promise<readonly Alarm[]> {
67+
const schedule: ScheduleState = ((await this.#driver.kvGet(
68+
this.#actor.id,
69+
KEYS.SCHEDULE.SCHEDULE
70+
)) as ScheduleState) ?? { events: [] };
71+
72+
const alarms: Alarm[] = [];
73+
for (const event of schedule.events) {
74+
const scheduleEvent = await this.#driver.kvGet(
75+
this.#actor.id,
76+
KEYS.SCHEDULE.event(event.eventId)
77+
) as ScheduleEvent;
78+
79+
if (scheduleEvent) {
80+
alarms.push(Object.freeze({
81+
id: event.eventId,
82+
createdAt: scheduleEvent.createdAt,
83+
triggersAt: scheduleEvent.timestamp,
84+
fn: scheduleEvent.fn,
85+
args: scheduleEvent.args
86+
}));
87+
}
88+
}
89+
90+
return Object.freeze(alarms);
3491
}
3592

36-
async at(timestamp: number, fn: string, ...args: unknown[]) {
37-
await this.#scheduleEvent(timestamp, fn, args);
93+
async cancel(alarmId: string): Promise<void> {
94+
// Get the schedule index
95+
const schedule: ScheduleState = ((await this.#driver.kvGet(
96+
this.#actor.id,
97+
KEYS.SCHEDULE.SCHEDULE
98+
)) as ScheduleState) ?? { events: [] };
99+
100+
// Find and remove the event from the index
101+
const eventIndex = schedule.events.findIndex(x => x.eventId === alarmId);
102+
if (eventIndex === -1) return;
103+
104+
const [removedEvent] = schedule.events.splice(eventIndex, 1);
105+
106+
// Delete the event data
107+
await this.#driver.kvDelete(
108+
this.#actor.id,
109+
KEYS.SCHEDULE.event(alarmId)
110+
);
111+
112+
// Update the schedule index
113+
await this.#driver.kvPut(
114+
this.#actor.id,
115+
KEYS.SCHEDULE.SCHEDULE,
116+
schedule
117+
);
118+
119+
// If we removed the first event (next to execute), update the alarm
120+
if (eventIndex === 0) {
121+
if (schedule.events.length > 0) {
122+
// Set alarm to next event
123+
await this.#driver.setAlarm(this.#actor, schedule.events[0].timestamp);
124+
} else {
125+
// No more events, delete the alarm
126+
await this.#driver.deleteAlarm(this.#actor);
127+
}
128+
}
38129
}
39130

40131
async #scheduleEvent(
41132
timestamp: number,
42133
fn: string,
43134
args: unknown[],
44-
): Promise<void> {
135+
): Promise<string> {
45136
// Save event
46137
const eventId = crypto.randomUUID();
47138
await this.#driver.kvPut(
48139
this.#actor.id,
49-
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
50-
KEYS.SCHEDULE.event(eventId) as any,
140+
KEYS.SCHEDULE.event(eventId),
51141
{
52142
timestamp,
143+
createdAt: Date.now(),
53144
fn,
54145
args,
55146
},
@@ -59,8 +150,7 @@ export class Schedule {
59150
// Read index
60151
const schedule: ScheduleState = ((await this.#driver.kvGet(
61152
this.#actor.id,
62-
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
63-
KEYS.SCHEDULE.SCHEDULE as any,
153+
KEYS.SCHEDULE.SCHEDULE,
64154
)) as ScheduleState) ?? {
65155
events: [],
66156
};
@@ -85,6 +175,8 @@ export class Schedule {
85175
if (insertIndex === 0 || schedule.events.length === 1) {
86176
await this.#driver.setAlarm(this.#actor, newEvent.timestamp);
87177
}
178+
179+
return eventId;
88180
}
89181

90182
async __onAlarm() {

0 commit comments

Comments
 (0)