Skip to content

Commit 1704d2e

Browse files
flossypurseclaude
andcommitted
Improve schedule() API and fix bugs
This commit enhances the existing schedule() method and fixes critical bugs: Bug fixes: - Fixed promise ID pattern to include function name (was missing) - Fixed resonate:invoke tag to use function name instead of opts.target - Pattern is now: `{scheduleId}.{{.timestamp}}.{functionName}` Enhancements: - Added schedule() method to ResonateFunc interface - Registered functions can now call .schedule() directly - Comprehensive documentation with examples - Example script demonstrating all usage patterns Examples: // On Resonate class await resonate.schedule("daily", "0 9 * * *", myFunc, arg1); // On registered function const func = resonate.register("myFunc", async (ctx, x) => x); await func.schedule("daily", "0 9 * * *", arg1); // With options await resonate .options({ timeout: 3600000, tags: { env: "prod" } }) .schedule("sync", "*/30 * * * *", syncFunc); The implementation now matches the Python SDK's pattern and provides a consistent developer experience across both SDKs. Related: - TypeScript issue: #435 - Python SDK PR: resonatehq/resonate-sdk-py#397 - Python SDK issue: resonatehq/resonate-sdk-py#328 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 66473a2 commit 1704d2e

2 files changed

Lines changed: 137 additions & 4 deletions

File tree

example_schedule.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Example demonstrating the high-level schedule API.
3+
*/
4+
5+
import { type Context, Resonate } from "./src/resonate";
6+
7+
// Create a local Resonate instance for testing
8+
const resonate = Resonate.local();
9+
10+
// Example 1: Schedule using resonate.schedule()
11+
console.log("Example 1: Schedule using resonate.schedule()");
12+
const generateReport = resonate.register("generateReport", async (ctx: Context, userId: number, reportType: string) => {
13+
return `Generated ${reportType} report for user ${userId}`;
14+
});
15+
16+
const schedule1 = await resonate.schedule(
17+
"daily_report_schedule",
18+
"0 9 * * *", // Every day at 9am
19+
generateReport,
20+
123,
21+
"daily",
22+
);
23+
console.log(`Created schedule: daily_report_schedule`);
24+
console.log();
25+
26+
// Example 2: Schedule using function.schedule()
27+
console.log("Example 2: Schedule using function.schedule()");
28+
const schedule2 = await generateReport.schedule(
29+
"weekly_report_schedule",
30+
"0 9 * * 1", // Every Monday at 9am
31+
456,
32+
"weekly",
33+
);
34+
console.log(`Created schedule: weekly_report_schedule`);
35+
console.log();
36+
37+
// Example 3: Schedule with options (timeout and tags)
38+
console.log("Example 3: Schedule with custom options");
39+
const schedule3 = await resonate
40+
.options({
41+
timeout: 3600000,
42+
tags: { env: "production", priority: "high" },
43+
})
44+
.schedule(
45+
"priority_report_schedule",
46+
"*/30 * * * *", // Every 30 minutes
47+
generateReport,
48+
789,
49+
"realtime",
50+
);
51+
console.log(`Created schedule: priority_report_schedule`);
52+
console.log();
53+
54+
// Example 4: Schedule by name
55+
console.log("Example 4: Schedule using function name");
56+
const schedule4 = await resonate.schedule(
57+
"analytics_report_schedule",
58+
"0 0 * * *", // Every day at midnight
59+
"generateReport",
60+
999,
61+
"analytics",
62+
);
63+
console.log(`Created schedule: analytics_report_schedule`);
64+
console.log();
65+
66+
// Cleanup
67+
console.log("Cleaning up schedules...");
68+
await schedule1.delete();
69+
await schedule2.delete();
70+
await schedule3.delete();
71+
await schedule4.delete();
72+
console.log("Done!");

src/resonate.ts

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface ResonateFunc<F extends Func> {
3737
rpc: (id: string, ...args: ParamsWithOptions<F>) => Promise<Return<F>>;
3838
beginRun: (id: string, ...args: ParamsWithOptions<F>) => Promise<ResonateHandle<Return<F>>>;
3939
beginRpc: (id: string, ...args: ParamsWithOptions<F>) => Promise<ResonateHandle<Return<F>>>;
40+
schedule: (scheduleId: string, cron: string, ...args: ParamsWithOptions<F>) => Promise<ResonateSchedule>;
4041
options: (opts?: Partial<Options>) => Options;
4142
}
4243

@@ -441,6 +442,8 @@ export class Resonate {
441442
this.beginRun(id, func, ...this.getArgsAndOpts(args, version)),
442443
beginRpc: (id: string, ...args: ParamsWithOptions<F>): Promise<ResonateHandle<Return<F>>> =>
443444
this.beginRpc(id, func, ...this.getArgsAndOpts(args, version)),
445+
schedule: (scheduleId: string, cron: string, ...args: ParamsWithOptions<F>): Promise<ResonateSchedule> =>
446+
this.schedule(scheduleId, cron, func, ...this.getArgsAndOpts(args, version)),
444447
options: this.options,
445448
};
446449
}
@@ -720,6 +723,58 @@ export class Resonate {
720723
}
721724
}
722725

726+
/**
727+
* Creates a schedule to execute a registered function periodically.
728+
*
729+
* This method creates a schedule that will automatically invoke the specified
730+
* function according to the provided cron expression. Each scheduled execution
731+
* creates a unique promise with an ID based on the schedule name, timestamp,
732+
* and function name, preventing duplicate executions and enabling automatic
733+
* recovery from failures.
734+
*
735+
* The function will be executed durably through Resonate's promise system,
736+
* ensuring reliability and fault tolerance across scheduled executions.
737+
*
738+
* @param name - Unique identifier for the schedule. Used to update or retrieve
739+
* the schedule later.
740+
* @param cron - Cron expression defining when to execute the function
741+
* (e.g., "0 9 * * *" for daily at 9am, "* /5 * * * *" for every 5 minutes).
742+
* @param funcOrName - The function to execute on schedule, either as a callable
743+
* or the registered name of the function.
744+
* @param args - Positional arguments to pass to the function on each execution.
745+
*
746+
* @returns A {@link ResonateSchedule} object that can be used to manage the schedule.
747+
*
748+
* @example
749+
* Basic scheduling:
750+
* ```ts
751+
* const report = resonate.register("generateReport", async (ctx, userId: number) => {
752+
* return `Generated report for user ${userId}`;
753+
* });
754+
*
755+
* // Schedule to run every day at 9am
756+
* const schedule = await resonate.schedule("daily_report", "0 9 * * *", report, 123);
757+
* ```
758+
*
759+
* @example
760+
* Scheduling with options:
761+
* ```ts
762+
* const schedule = await resonate
763+
* .options({ timeout: 3600000, tags: { env: "production" } })
764+
* .schedule("priority_sync", "* /30 * * * *", syncData, "api");
765+
* ```
766+
*
767+
* @example
768+
* Function-level scheduling:
769+
* ```ts
770+
* const cleanup = resonate.register("cleanup", async (ctx, days: number) => {
771+
* // cleanup logic
772+
* });
773+
*
774+
* // Schedule using the function instance
775+
* await cleanup.schedule("nightly_cleanup", "0 2 * * *", 30);
776+
* ```
777+
*/
723778
public async schedule<F extends Func>(
724779
name: string,
725780
cron: string,
@@ -741,19 +796,25 @@ export class Resonate {
741796
throw exceptions.REGISTRY_FUNCTION_NOT_REGISTERED(funcOrName.name, opts.version);
742797
}
743798

799+
const funcName = registered ? registered.name : (funcOrName as string);
800+
const funcVersion = registered ? registered.version : opts.version || 1;
801+
744802
// TODO: move this into the handler?
745803
const { headers, data } = this.encryptor.encrypt(
746804
this.encoder.encode({
747-
func: registered ? registered.name : (funcOrName as string),
805+
func: funcName,
748806
args: args,
749-
version: registered ? registered.version : opts.version || 1,
807+
version: funcVersion,
750808
}),
751809
);
752810

753-
await this.schedules.create(name, cron, `${this.idPrefix}{{.id}}.{{.timestamp}}`, opts.timeout, {
811+
// Create promise ID pattern that includes the schedule name and function name
812+
const promiseIdPattern = `${this.idPrefix}${name}.{{.timestamp}}.${funcName}`;
813+
814+
await this.schedules.create(name, cron, promiseIdPattern, opts.timeout, {
754815
promiseHeaders: headers,
755816
promiseData: data,
756-
promiseTags: { ...opts.tags, "resonate:invoke": opts.target },
817+
promiseTags: { ...opts.tags, "resonate:invoke": funcName },
757818
});
758819

759820
return {

0 commit comments

Comments
 (0)