Skip to content

Commit 06288ff

Browse files
committed
docs: resend integration (#893)
1 parent 240dd60 commit 06288ff

File tree

11 files changed

+916
-30
lines changed

11 files changed

+916
-30
lines changed

docs/docs.json

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,24 +114,14 @@
114114
]
115115
},
116116
{
117-
<<<<<<< HEAD
118117
"tab": "Integrations",
119-
=======
120-
"tab": "Examples",
121-
"pages": [
122-
"examples/Live-Cursors",
123-
"examples/chat"
124-
]
125-
},
126-
{
127-
"tab": "Changelog",
128-
>>>>>>> f2a76c1 (updated docs for examples)
129118
"pages": [
130119
"integrations/overview",
131120
{
132121
"group": "Integrations",
133122
"pages": [
134-
"integrations/hono"
123+
"integrations/hono",
124+
"integrations/resend"
135125
]
136126
},
137127
{

docs/integrations/overview.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ sidebarTitle: Overview
77

88
<CardGroup>
99
<Card title="Hono" href="/integrations/hono" />
10+
<Card title="Resend" href="/integrations/resend" />
1011
</CardGroup>
1112

1213
## Drivers

docs/integrations/resend.mdx

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
---
2+
title: Resend
3+
---
4+
5+
[Resend](https://resend.com) is an email API service that works seamlessly with ActorCore for handling emails and notifications.
6+
7+
## Example
8+
9+
See how ActorCore and Resend can power engagement with daily streak notifications.
10+
11+
<Card icon="fire" title="Streaks" href="https://github.com/rivet-gg/actor-core/tree/main/examples/resend-streaks">View on GitHub</Card>
12+
13+
## Quickstart
14+
15+
<Steps>
16+
<Step title="Install Resend SDK">
17+
```bash
18+
npm install resend
19+
```
20+
</Step>
21+
22+
<Step title="Create an actor with Resend">
23+
```typescript actors.ts
24+
import { actor, setup } from "actor-core";
25+
import { Resend } from "resend";
26+
27+
const resend = new Resend(process.env.RESEND_API_KEY);
28+
29+
const user = actor({
30+
state: {
31+
email: null as string | null,
32+
},
33+
34+
actions: {
35+
// Example: Somehow acquire the user's email
36+
register: async (c, email: string) => {
37+
c.state.email = email;
38+
},
39+
40+
// Example: Send an email
41+
sendExampleEmail: async (c) => {
42+
if (!c.state.email) throw new Error("No email registered");
43+
44+
await resend.emails.send({
45+
from: "updates@yourdomain.com",
46+
to: c.state.email,
47+
subject: "Hello, world!",
48+
html: "<p>Lorem ipsum</p>",
49+
});
50+
},
51+
},
52+
});
53+
54+
export const app = setup({ actors: { user } });
55+
export type App = typeof app;
56+
```
57+
</Step>
58+
59+
<Step title="Call your actor">
60+
```typescript client.ts
61+
import { createClient } from "actor-core";
62+
import { App } from "./actors/app.ts";
63+
64+
const client = createClient<App>("http://localhost:8787");
65+
const userActor = await client.user.get({ tags: { user: "user123" } });
66+
67+
await userActor.register("user@example.com");
68+
await userActor.sendExampleEmail();
69+
```
70+
</Step>
71+
</Steps>
72+
73+
## Use Cases
74+
75+
### Scheduling Emails
76+
77+
ActorCore's scheduling capabilities with Resend make it easy to send emails at specific times:
78+
79+
<CodeGroup>
80+
```typescript actors.ts
81+
const emailScheduler = actor({
82+
state: {
83+
email: null as string | null,
84+
},
85+
86+
actions: {
87+
scheduleEmail: async (c, email: string, delayMs: number = 86400000) => {
88+
c.state.email = email;
89+
await c.schedule.at(Date.now() + delayMs, "sendEmail");
90+
},
91+
92+
sendEmail: async (c) => {
93+
if (!c.state.email) return;
94+
95+
await resend.emails.send({
96+
from: "updates@yourdomain.com",
97+
to: c.state.email,
98+
subject: "Your scheduled message",
99+
html: "<p>This email was scheduled earlier!</p>",
100+
});
101+
},
102+
},
103+
});
104+
```
105+
106+
```typescript client.ts
107+
const client = createClient<App>({ url: "http://localhost:3000" });
108+
const scheduler = await client.emailScheduler.get({ id: "user123" });
109+
await scheduler.scheduleEmail("user@example.com", 60000); // 1 minute
110+
```
111+
</CodeGroup>
112+
113+
### Daily Reminders
114+
115+
Send daily reminders to users based on their activity:
116+
117+
<CodeGroup>
118+
```typescript actors.ts
119+
const reminder = actor({
120+
state: {
121+
email: null as string | null,
122+
lastActive: null as number | null,
123+
},
124+
125+
actions: {
126+
trackActivity: async (c, email: string) => {
127+
c.state.email = email;
128+
c.state.lastActive = Date.now();
129+
130+
// Schedule check for tomorrow
131+
await c.schedule.at(Date.now() + 24 * 60 * 60 * 1000, "checkActivity");
132+
},
133+
134+
checkActivity: async (c) => {
135+
if (!c.state.email) return;
136+
137+
// If inactive for 24+ hours, send reminder
138+
if (Date.now() - (c.state.lastActive || 0) >= 24 * 60 * 60 * 1000) {
139+
await resend.emails.send({
140+
from: "reminders@yourdomain.com",
141+
to: c.state.email,
142+
subject: "We miss you!",
143+
html: "<p>Don't forget to check in today.</p>",
144+
});
145+
}
146+
147+
// Reschedule for tomorrow
148+
await c.schedule.at(Date.now() + 24 * 60 * 60 * 1000, "checkActivity");
149+
},
150+
},
151+
});
152+
```
153+
154+
```typescript client.ts
155+
const client = createClient<App>({ url: "http://localhost:3000" });
156+
const userReminder = await client.reminder.get({ id: "user123" });
157+
await userReminder.trackActivity("user@example.com");
158+
```
159+
</CodeGroup>
160+
161+
### Alerting Systems
162+
163+
Monitor your systems and send alerts when issues are detected:
164+
165+
<CodeGroup>
166+
```typescript actors.ts
167+
const monitor = actor({
168+
state: {
169+
alertEmail: null as string | null,
170+
isHealthy: true,
171+
},
172+
173+
actions: {
174+
configure: async (c, email: string) => {
175+
c.state.alertEmail = email;
176+
await c.schedule.at(Date.now() + 60000, "checkHealth");
177+
},
178+
179+
checkHealth: async (c) => {
180+
// Simple mock health check
181+
const wasHealthy = c.state.isHealthy;
182+
c.state.isHealthy = await mockHealthCheck();
183+
184+
// Alert on status change to unhealthy
185+
if (wasHealthy && !c.state.isHealthy && c.state.alertEmail) {
186+
await resend.emails.send({
187+
from: "alerts@yourdomain.com",
188+
to: c.state.alertEmail,
189+
subject: "⚠️ System Alert",
190+
html: "<p>The system is experiencing issues.</p>",
191+
});
192+
}
193+
194+
// Reschedule next check
195+
await c.schedule.at(Date.now() + 60000, "checkHealth");
196+
},
197+
},
198+
});
199+
200+
// Mock function
201+
async function mockHealthCheck() {
202+
return Math.random() > 0.1; // 90% chance of being healthy
203+
}
204+
```
205+
206+
```typescript client.ts
207+
const client = createClient<App>({ url: "http://localhost:3000" });
208+
const systemMonitor = await client.monitor.get({ id: "api-service" });
209+
await systemMonitor.configure("admin@example.com");
210+
```
211+
</CodeGroup>
212+
213+
## Testing
214+
215+
When testing actors that use Resend, you should mock the Resend API to avoid sending real emails during tests. ActorCore's testing utilities combined with Vitest make this straightforward:
216+
217+
```typescript
218+
import { test, expect, vi, beforeEach } from "vitest";
219+
import { setupTest } from "actor-core/test";
220+
import { app } from "../actors/app";
221+
222+
// Create mock for send method
223+
const mockSendEmail = vi.fn().mockResolvedValue({ success: true });
224+
225+
beforeEach(() => {
226+
process.env.RESEND_API_KEY = "test_mock_api_key_12345";
227+
228+
vi.mock("resend", () => {
229+
return {
230+
Resend: vi.fn().mockImplementation(() => {
231+
return {
232+
emails: {
233+
send: mockSendEmail
234+
}
235+
};
236+
})
237+
};
238+
});
239+
240+
mockSendEmail.mockClear();
241+
});
242+
243+
test("email is sent when action is called", async (t) => {
244+
const { client } = await setupTest(t, app);
245+
const actor = await client.user.get();
246+
247+
// Call the action that should send an email
248+
await actor.someActionThatSendsEmail("user@example.com");
249+
250+
// Verify the email was sent with the right parameters
251+
expect(mockSendEmail).toHaveBeenCalledWith(
252+
expect.objectContaining({
253+
to: "user@example.com",
254+
subject: "Expected Subject",
255+
}),
256+
);
257+
});
258+
```
259+
260+
Using `vi.advanceTimersByTimeAsync()` is particularly useful for testing scheduled emails:
261+
262+
```typescript
263+
// Fast forward time to test scheduled emails
264+
await vi.advanceTimersByTimeAsync(24 * 60 * 60 * 1000); // Advance 24 hours
265+
266+
// Test that the scheduled email was sent
267+
expect(mockSendEmail).toHaveBeenCalledTimes(2);
268+
```
269+

examples/chat-room/scripts/cli.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
import { createClient, type Encoding } from "actor-core/client";
1+
import { createClient } from "actor-core/client";
22
import type { App } from "../actors/app";
33
import prompts from "prompts";
44

55
async function main() {
6-
const { encoding, username, room } = await initPrompt();
6+
const { username, room } = await initPrompt();
77

88
// Create type-aware client
9-
const client = createClient<App>("http://localhost:6420", {
10-
encoding,
11-
});
9+
const client = createClient<App>("http://localhost:6420");
1210

1311
// connect to chat room - now accessed via property
1412
// can still pass parameters like room
1513
const chatRoom = await client.chatRoom.get({
14+
tags: { room },
1615
params: { room },
1716
});
1817

@@ -47,20 +46,10 @@ async function main() {
4746
}
4847

4948
async function initPrompt(): Promise<{
50-
encoding: Encoding;
5149
room: string;
5250
username: string;
5351
}> {
5452
return await prompts([
55-
{
56-
type: "select",
57-
name: "encoding",
58-
message: "Encoding",
59-
choices: [
60-
{ title: "CBOR", value: "cbor" },
61-
{ title: "JSON", value: "json" },
62-
],
63-
},
6453
{
6554
type: "text",
6655
name: "username",

examples/chat-room/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
/* Specify how TypeScript looks up a file from a given module specifier. */
1515
"moduleResolution": "bundler",
1616
/* Specify type package names to be included without being referenced in a source file. */
17-
"types": ["@cloudflare/workers-types"],
17+
"types": ["node"],
1818
/* Enable importing .json files */
1919
"resolveJsonModule": true,
2020

examples/resend-streaks/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.actorcore
2+
node_modules

0 commit comments

Comments
 (0)