diff --git a/.github/media/icons/bolt-regular.svg b/.github/media/icons/bolt-regular.svg new file mode 100644 index 000000000..4c2dfcf8f --- /dev/null +++ b/.github/media/icons/bolt-regular.svg @@ -0,0 +1 @@ + diff --git a/.github/media/icons/cloud-regular.svg b/.github/media/icons/cloud-regular.svg new file mode 100644 index 000000000..b4ced7e1f --- /dev/null +++ b/.github/media/icons/cloud-regular.svg @@ -0,0 +1 @@ + diff --git a/.github/media/icons/database-regular.svg b/.github/media/icons/database-regular.svg new file mode 100644 index 000000000..24532d94c --- /dev/null +++ b/.github/media/icons/database-regular.svg @@ -0,0 +1 @@ + diff --git a/.github/media/icons/globe-regular.svg b/.github/media/icons/globe-regular.svg new file mode 100644 index 000000000..5ec55387c --- /dev/null +++ b/.github/media/icons/globe-regular.svg @@ -0,0 +1 @@ + diff --git a/.github/media/icons/microchip-regular.svg b/.github/media/icons/microchip-regular.svg new file mode 100644 index 000000000..6588f197e --- /dev/null +++ b/.github/media/icons/microchip-regular.svg @@ -0,0 +1 @@ + diff --git a/.github/media/icons/tower-broadcast-regular.svg b/.github/media/icons/tower-broadcast-regular.svg new file mode 100644 index 000000000..bf5863619 --- /dev/null +++ b/.github/media/icons/tower-broadcast-regular.svg @@ -0,0 +1 @@ + diff --git a/.github/media/studio-video-demo5.png b/.github/media/studio-video-demo5.png new file mode 100644 index 000000000..56ad44d93 Binary files /dev/null and b/.github/media/studio-video-demo5.png differ diff --git a/TESTING.md b/CONTRIBUTING.md similarity index 96% rename from TESTING.md rename to CONTRIBUTING.md index d69da5965..b16bad821 100644 --- a/TESTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,6 @@ -# Testing +# Contributing + +## Testing For now, only testing the outcome of running the CLI is supported. Before running tests, make sure that you have installed and started Docker. @@ -25,4 +27,4 @@ $ ./scripts/e2e-publish.ts 5. ```bash $ yarn workspace @actor-core/cli run test ``` - - Runs the tests for the CLI, which includes running the CLI (inside Docker) with different arguments and checking the output. \ No newline at end of file + - Runs the tests for the CLI, which includes running the CLI (inside Docker) with different arguments and checking the output. diff --git a/README.md b/README.md index c4d606bd0..cd2222019 100644 --- a/README.md +++ b/README.md @@ -1,138 +1,268 @@ -

+

- + ActorCore -

+

Stateful Serverless That Runs Anywhere

+

+ The easiest way to build stateful, AI agent, collaborative, or local-first applications.
+ Deploy to Rivet, Cloudflare, Bun, Node.js, and more. +

+ +

+ Documentation β€’ + Discord β€’ + X β€’ + Bluesky +

+
-

Stateful, Scalable, Realtime Backend Framework

-

-

-

- - GitHub Discussions - Discord - Rivet Twitter - Rivet Bluesky - License Apache-2.0 -

+## Quickstart -## Intro +**Start By Framework** -The modern way to build multiplayer, realtime, or AI agent backends. +- React  [React](https://actorcore.org/frameworks/react) +- Node.js  [Node.js & Bun](https://actorcore.org/clients/javascript) +- Rust  [Rust](https://actorcore.org/clients/rust) -Runs on [Rivet](https://actorcore.org/platforms/rivet), [Cloudflare Workers](https://actorcore.org/platforms/cloudflare-workers), [Bun](https://actorcore.org/platforms/bun), and [Node.js](https://actorcore.org/platforms/nodejs). Integrates with [Hono](https://actorcore.org/integrations/hono) and [Redis](https://actorcore.org/drivers/redis). +**Start With Studio** -### Architecture +Rivet  [Open Studio](https://studio.rivet.gg) -- πŸ’Ύ **Persistent, In-Memory State**: Fast in-memory access with built-in durability β€” no external databases or caches needed. -- ⚑ **Ultra-Fast State Updates**: Real-time state updates with ultra-low latency, powered by co-locating compute and data. -- πŸ”‹ **Batteries Included**: Integrated support for state, actions, events, scheduling, and multiplayer β€” no extra boilerplate code needed. -- πŸ–₯️ **Serverless & Scalable**: Effortless scaling, scale-to-zero, and easy deployments on any serverless runtime. +**Start With Template** -### Features +```bash +npx create-actor@latest +``` -- πŸ’Ύ [**State**](https://actorcore.org/concepts/state): Fast in-memory access with built-in durability. -- πŸ’» [**Actions**](https://actorcore.org/concepts/actions): Callable functions for seamless client-server communication. -- πŸ“‘ [**Events**](https://actorcore.org/concepts/events): Real-time event handling and broadcasting. -- ⏰ [**Scheduling**](https://actorcore.org/concepts/schedule): Timed tasks and operations management. -- 🌐 [**Connections & Multiplayer**](https://actorcore.org/concepts/connections): Manage connections and multiplayer interactions. -- 🏷️ [**Metadata**](https://actorcore.org/concepts/metadata): Store and manage additional data attributes. +## What is Stateful Serverless? -### Everything you need to build realtime, stateful backends +
-ActorCore provides a solid foundation with the features you'd expect for modern apps. +**  Long-Lived, Stateful Compute** -| Feature | ActorCore | Durable Objects | Socket.io | Redis | AWS Lambda | -| --------------- | --------- | --------------- | --------- | ----- | ---------- | -| In-Memory State | βœ“ | βœ“ | βœ“ | βœ“ | | -| Persisted State | βœ“ | βœ“ | | | | -| Actions (RPC) | βœ“ | βœ“ | βœ“ | | βœ“ | -| Events (Pub/Sub)| βœ“ | - | βœ“ | βœ“ | | -| Scheduling | βœ“ | - | | | - | -| Edge Computing | βœ“ † | βœ“ | | | βœ“ | -| No Vendor Lock | βœ“ | | βœ“ | βœ“ | | +Each unit of compute is like a tiny server that remembers things between requests – no need to reload data or worry about timeouts. Like AWS Lambda, but with memory and no timeouts. -_\- = requires significant boilerplate code or external service_ +
-_† = on supported platforms_ +  **Durable State Without a Database** -## Quickstart +Your code's state is saved automaticallyβ€”no database, ORM, or config needed. Just use regular JavaScript objects or SQLite (available in April). -Run this command: +
-``` -npx create-actor@latest -``` +  **Blazing-Fast Reads & Writes** -## Supported Platforms +State is stored on the same machine as your compute, so reads and writes are ultra-fast. No database round trips, no latency spikes. -- [**Rivet**](https://actorcore.org/platforms/rivet) -- [**Cloudflare Workers**](https://actorcore.org/platforms/cloudflare-workers) -- [**Bun**](https://actorcore.org/platforms/bun) -- [**Node.js**](https://actorcore.org/platforms/nodejs) +
-## Overview +  **Realtime, Made Simple** -**Create Actor** +Update state and broadcast changes in realtime. No external pub/sub systems, no polling – just built-in low-latency events. -```typescript -import { actor, setup } from "actor-core"; +
-const chatRoom = actor({ - state: { messages: [] }, - actions: { - // receive an action call from the client - sendMessage: (c, username: string, message: string) => { - // save message to persistent storage - c.state.messages.push({ username, message }); +  **Store Data Near Your Users** - // broadcast message to all clients - c.broadcast("newMessage", username, message); - }, - // allow client to request message history - getMessages: (c) => c.state.messages - }, -}); +Your state lives close to your users on the edge – not in a faraway data center – so every interaction feels instant. -export const app = setup({ - actors: { chatRoom }, - cors: { origin: "http://localhost:8080" } -}); +
-export type App = typeof app; -``` +  **Serverless & Scalable** -**Connect to Actor** +No servers to manage. Your code runs on-demand and scales automatically with usage. -```typescript -import { createClient } from "actor-core/client"; -import type { App } from "../src/index"; +
-const client = createClient(/* manager endpoint */); +*[Have more questions? Jump to our FAQ β†’](#frequently-asked-questions)* -// connect to chat room -const chatRoom = await client.chatRoom.get({ channel: "random" }); +## Examples -// listen for new messages -chatRoom.on("newMessage", (username: string, message: string) => - console.log(`Message from ${username}: ${message}`), -); +Browse snippets for how to use ActorCore with different use cases. -// send message to room -await chatRoom.sendMessage("william", "All the world's a stage."); -``` +| Example | Actor (JavaScript) | Actor (SQLite) | Frontend (React) | +|---------|------------|--------|-------| +| Chat Room | [actor.ts](/examples/snippets/chat-room/actor-json.ts) | [actor.ts](/examples/snippets/chat-room/actor-sqlite.ts) | [App.tsx](/examples/snippets/chat-room/App.tsx) | +| AI Agent | [actor.ts](/examples/snippets/ai-agent/actor-json.ts) | [actor.ts](/examples/snippets/ai-agent/actor-sqlite.ts) | [App.tsx](/examples/snippets/ai-agent/App.tsx) | +| Local-First Sync | [actor.ts](/examples/snippets/sync/actor-json.ts) | [actor.ts](/examples/snippets/sync/actor-sqlite.ts) | [App.tsx](/examples/snippets/sync/App.tsx) | +| Per-Tenant SaaS | [actor.ts](/examples/snippets/tenant/actor-json.ts) | [actor.ts](/examples/snippets/tenant/actor-sqlite.ts) | [App.tsx](/examples/snippets/tenant/App.tsx) | +| Per-User Databases | [actor.ts](/examples/snippets/database/actor-json.ts) | [actor.ts](/examples/snippets/database/actor-sqlite.ts) | [App.tsx](/examples/snippets/database/App.tsx) | +| Yjs CRDT | [actor.ts](/examples/snippets/crdt/actor-json.ts) | [actor.ts](/examples/snippets/crdt/actor-sqlite.ts) | [App.tsx](/examples/snippets/crdt/App.tsx) | +| Collaborative Document | [actor.ts](/examples/snippets/document/actor-json.ts) | [actor.ts](/examples/snippets/document/actor-sqlite.ts) | [App.tsx](/examples/snippets/document/App.tsx) | +| Stream Processing | [actor.ts](/examples/snippets/stream/actor-json.ts) | [actor.ts](/examples/snippets/stream/actor-sqlite.ts) | [App.tsx](/examples/snippets/stream/App.tsx) | +| Multiplayer Game | [actor.ts](/examples/snippets/game/actor-json.ts) | [actor.ts](/examples/snippets/game/actor-sqlite.ts) | [App.tsx](/examples/snippets/game/App.tsx) | +| Rate Limiter | [actor.ts](/examples/snippets/rate/actor-json.ts) | [actor.ts](/examples/snippets/rate/actor-sqlite.ts) | [App.tsx](/examples/snippets/rate/App.tsx) | + +_SQLite will be available in late April. We’re working on publishing full examples related to these snippets. If you find an error, please create an issue._ + +## Runs On Your Stack + +Deploy ActorCore anywhere - from serverless platforms to your own infrastructure. Don't see the runtime you want? [Add your own](http://localhost:3000/drivers/build). + +### All-In-One +- Rivet  [Rivet](/platforms/rivet) +- Cloudflare Workers  [Cloudflare Workers](/platforms/cloudflare-workers) + +### Compute +- Vercel  [Vercel](https://github.com/rivet-gg/actor-core/issues/897) *(On The Roadmap)* +- AWS Lambda  [AWS Lambda](https://github.com/rivet-gg/actor-core/issues/898) *(On The Roadmap)* +- Supabase  [Supabase](https://github.com/rivet-gg/actor-core/issues/905) *(Help Wanted)* +- Bun  [Bun](/platforms/bun) +- Node.js  [Node.js](/platforms/nodejs) + +### Storage +- Redis  [Redis](/drivers/redis) +- Postgres  [Postgres](https://github.com/rivet-gg/actor-core/issues/899) *(Help Wanted)* +- File System  [File System](/drivers/file-system) +- Memory  [Memory](/drivers/memory) + +## Works With Your Tools + +Seamlessly integrate ActorCore with your favorite frameworks, languages, and tools. Don't see what you need? [Request an integration](https://github.com/rivet-gg/actor-core/issues/new). + +### Frameworks +- React  [React](/frameworks/react) +- Next.js  [Next.js](https://github.com/rivet-gg/actor-core/issues/904) *(Help Wanted)* +- Vue  [Vue](https://github.com/rivet-gg/actor-core/issues/903) *(Help Wanted)* + +### Clients +- JavaScript  [JavaScript](/clients/javascript) +- TypeScript  [TypeScript](/clients/javascript) +- Python  [Python](https://github.com/rivet-gg/actor-core/issues/900) *(Available In April)* +- Rust  [Rust](/clients/rust) + +### Integrations +- Hono  [Hono](/integrations/hono) +- Resend  [Resend](/integrations/resend) +- Better Auth  [Better Auth](https://github.com/rivet-gg/actor-core/issues/906) *(On The Roadmap)* +- AI SDK  [AI SDK](https://github.com/rivet-gg/actor-core/issues/907) *(On The Roadmap)* + +### Local-First Sync +- LiveStore  [LiveStore](https://github.com/rivet-gg/actor-core/issues/908) *(Available In May)* +- ZeroSync  [ZeroSync](https://github.com/rivet-gg/actor-core/issues/909) *(Help Wanted)* +- TinyBase  [TinyBase](https://github.com/rivet-gg/actor-core/issues/910) *(Help Wanted)* +- Yjs  [Yjs](https://github.com/rivet-gg/actor-core/issues/911) *(Help Wanted)* + +## Local Development with the Studio + +

Like Postman, but for all of your stateful serverless needs.

+ +

Visit The Studio β†’

+ + + +## Join the Community + +Help make ActorCore the universal way to build & scale stateful serverless applications. -## Community & Support +- [Discord](https://rivet.gg/discord) +- [X](https://x.com/ActorCore_org) +- [Bluesky](https://bsky.app/profile/rivet.gg) +- [Discussions](https://github.com/rivet-gg/actor-core/discussions) +- [Issues](https://github.com/rivet-gg/actor-core/issues) -- Join our [**Discord**](https://rivet.gg/discord) -- Follow us on [**X**](https://x.com/rivet_gg) -- Follow us on [**Bluesky**](https://bsky.app/profile/rivet.gg) -- File bug reports in [**GitHub Issues**](https://github.com/rivet-gg/actor-core/issues) -- Post questions & ideas in [**GitHub Discussions**](https://github.com/rivet-gg/actor-core/discussions) +## Frequently Asked Questions + +
+How is ActorCore different than Rivet Actors? + +ActorCore is a framework written in TypeScript that provides high-level functionality. Rivet is an open-source serverless platform written in Rust with features tailored for stateful serverless. + +You can think of it as ActorCore is to Rivet as Next.js is to Vercel. + +While Rivet is the primary maintainer of ActorCore, we intend for this to be community driven. +
+ +
+How does stateful serverless compare to the traditional actor model? + +Stateful serverless is very similar to actors: it's essentially actors with persistence, and usually doesn't have as rigid constraints on message handling. This makes it more flexible while maintaining the core benefits of the actor model. +
+ +
+How do stateful and stateless serverless work together? + +Stateless serverless works well when you have an external resource that maintains state. Stateful serverless, on the other hand, is almost like a mini-database. + +Sometimes it makes sense to use stateless serverless to make requests to multiple stateful serverless instances, orchestrating complex operations across multiple state boundaries. +
+ +
+How does ActorCore achieve huge performance gains? + +By storing state in memory and flushing to a persistence layer, we can serve requests instantly instead of waiting for a round trip to the database. There are additional optimizations that can be made around your state to tune the durability of it. + +Additionally, data is stored near your users at the edge, ensuring round-trip times of less than 50ms when they request it. This edge-first approach eliminates the latency typically associated with centralized databases. +
+ +
+Isn't well-designed software supposed to separate compute and storage? + +Some software makes sense to separate – e.g., for data lakes or highly relational data. But at the end of the day, data has to be partitioned somewhere at some point. + +Usually "faster" databases like Cassandra, DynamoDB, or Vitess make consistency tradeoffs to get better performance. Stateful serverless forces you to think about how your data is sharded for better performance, better scalability, and less consistency footguns. +
+ +
+What is stateful serverless not good for? + +OLAP, data lakes, graph databases, and highly relational data are currently not ideal use cases for stateful serverless, though it will get better at handling these use cases over time. +
+ +
+Can this create a single bottleneck? + +Yes, but only as much as storing data in a single database row does. We're working on building out read replicas to allow you to perform read-only actions on actors. +
+ +
+Stateless serverless is standardized under WinterTC. Is there any intention to standardize stateful serverless? + +Things are cooking! Check out our [blog post](https://rivet.gg/blog/2025-03-23-what-would-a-w3c-standard-look-like-for-stateful-serverless-) about what a W3C standard for stateful serverless might look like and [the awesome people who are collaborating on this](https://x.com/threepointone/status/1903579571028390038). +
+ +Have more questions? Join our [Discord](https://discord.gg/rivet) or go to [GitHub Discussions](https://github.com/rivet-gg/actor-core/discussions). + +## Roadmap For 2025 + +We ship fast, so we want to share what you can expect to see before the end of the year. +Help shape our roadmap by [creating issues](https://github.com/rivet-gg/actor-core/issues) and [joining our Discord](https://rivet.gg/discord). + +- [ ] SQLite Support +- [ ] Oodles Of Examples +- [ ] SQLite in Studio +- [ ] Local-First Extensions +- [ ] Auth Extensions +- [ ] Workflows +- [ ] Queues +- [ ] MCP +- [ ] Actor-Actor Actions +- [ ] Cancellable Schedules +- [ ] Cron Jobs +- [ ] Drizzle Support +- [ ] Prisma v7 Support +- [ ] Read Replicas +- [ ] Middleware +- [ ] Schema Validation +- [ ] Vite Integration +- [ ] OpenTelemetry +- [X] Studio +- [X] File system driver +- [X] React client +- [X] Rust client +- [X] Resend Integration +- [X] Vitest Integration +- [X] Non-serialized state +- [X] `create-actor` +- [X] `actor-core dev` command +- [X] Hono Integration ## License Apache 2.0 + +_Scale without drama – only with ActorCore._ + diff --git a/docs/images/clients/react.svg b/docs/images/clients/react.svg new file mode 100644 index 000000000..001b82e80 --- /dev/null +++ b/docs/images/clients/react.svg @@ -0,0 +1 @@ + diff --git a/docs/images/clients/vue.svg b/docs/images/clients/vue.svg new file mode 100644 index 000000000..1c509d9d8 --- /dev/null +++ b/docs/images/clients/vue.svg @@ -0,0 +1 @@ + diff --git a/docs/images/platforms/file-system.svg b/docs/images/platforms/file-system.svg new file mode 100644 index 000000000..ce0539fc8 --- /dev/null +++ b/docs/images/platforms/file-system.svg @@ -0,0 +1 @@ + diff --git a/docs/images/platforms/memory.svg b/docs/images/platforms/memory.svg new file mode 100644 index 000000000..484a48220 --- /dev/null +++ b/docs/images/platforms/memory.svg @@ -0,0 +1 @@ + diff --git a/docs/snippets/landing-tech.mdx b/docs/snippets/landing-tech.mdx index fdd524af5..5bff0fc53 100644 --- a/docs/snippets/landing-tech.mdx +++ b/docs/snippets/landing-tech.mdx @@ -30,9 +30,7 @@ On The Roadmap -
- -
+ AWS Lambda AWS Lambda On The Roadmap
@@ -65,15 +63,11 @@ Help Wanted -
- -
+ File System File System
-
- -
+ Memory Memory
@@ -95,9 +89,7 @@

Frameworks

-
- -
+ React React
@@ -106,9 +98,7 @@ Help Wanted -
- -
+ Vue Vue Help Wanted
@@ -132,9 +122,7 @@ Available In April -
- -
+ Rust Rust
diff --git a/examples/snippets/README.md b/examples/snippets/README.md new file mode 100644 index 000000000..c2a3ae120 --- /dev/null +++ b/examples/snippets/README.md @@ -0,0 +1,11 @@ +# ActorCore Snippets + +This directory contains the full source code for examples shown in the documentation. + +These snippets are not intended to be complete examples. + +Each example has these files in a single folder: +- `actor-json.ts` - Server implementation with JavaScript state +- `actor-sqlite.ts` - Server implementation with SQLite state +- `App.tsx` - React client implementation + diff --git a/examples/snippets/ai-agent/App.tsx b/examples/snippets/ai-agent/App.tsx new file mode 100644 index 000000000..93fc5106e --- /dev/null +++ b/examples/snippets/ai-agent/App.tsx @@ -0,0 +1,85 @@ +import { createClient } from "actor-core/client"; +import { createReactActorCore } from "@actor-core/react"; +import { useState, useEffect } from "react"; +import type { App } from "../actors/app"; +import type { Message } from "./actor"; + +const client = createClient("http://localhost:6420"); +const { useActor, useActorEvent } = createReactActorCore(client); + +export function AIAssistant() { + const [{ actor }] = useActor("aiAgent", { tags: { conversationId: "default" } }); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + // Load initial messages + useEffect(() => { + if (actor) { + actor.getMessages().then(setMessages); + } + }, [actor]); + + // Listen for real-time messages + useActorEvent({ actor, event: "messageReceived" }, (message) => { + setMessages(prev => [...prev, message as Message]); + setIsLoading(false); + }); + + const handleSendMessage = async () => { + if (actor && input.trim()) { + setIsLoading(true); + + // Add user message to UI immediately + const userMessage = { role: "user", content: input } as Message; + setMessages(prev => [...prev, userMessage]); + + // Send to actor (AI response will come through the event) + await actor.sendMessage(input); + setInput(""); + } + }; + + return ( +
+
+ {messages.length === 0 ? ( +
+ Ask the AI assistant a question to get started +
+ ) : ( + messages.map((msg, i) => ( +
+
+ {msg.role === "user" ? "πŸ‘€" : "πŸ€–"} +
+
{msg.content}
+
+ )) + )} + {isLoading && ( +
+
πŸ€–
+
Thinking...
+
+ )} +
+ +
+ setInput(e.target.value)} + onKeyPress={e => e.key === "Enter" && handleSendMessage()} + placeholder="Ask the AI assistant..." + disabled={isLoading} + /> + +
+
+ ); +} \ No newline at end of file diff --git a/examples/snippets/ai-agent/actor-json.ts b/examples/snippets/ai-agent/actor-json.ts new file mode 100644 index 000000000..b7ce7d606 --- /dev/null +++ b/examples/snippets/ai-agent/actor-json.ts @@ -0,0 +1,65 @@ +import { actor } from "actor-core"; +import { generateText, tool } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { getWeather } from "./my-utils"; + +export type Message = { role: "user" | "assistant"; content: string; timestamp: number; } + +const aiAgent = actor({ + // State is automatically persisted + state: { + messages: [] as Message[] + }, + + actions: { + // Get conversation history + getMessages: (c) => c.state.messages, + + // Send a message to the AI and get a response + sendMessage: async (c, userMessage: string) => { + // Add user message to conversation + const userMsg: Message = { + role: "user", + content: userMessage, + timestamp: Date.now() + }; + c.state.messages.push(userMsg); + + // Generate AI response using Vercel AI SDK with tools + const { text } = await generateText({ + model: openai("o3-mini"), + prompt: userMessage, + messages: c.state.messages, + tools: { + weather: tool({ + description: 'Get the weather in a location', + parameters: { + location: { + type: 'string', + description: 'The location to get the weather for', + }, + }, + execute: async ({ location }) => { + return await getWeather(location); + }, + }), + }, + }); + + // Add AI response to conversation + const assistantMsg: Message = { + role: "assistant", + content: text, + timestamp: Date.now() + }; + c.state.messages.push(assistantMsg); + + // Broadcast the new message to all connected clients + c.broadcast("messageReceived", assistantMsg); + + return assistantMsg; + }, + } +}); + +export default aiAgent; \ No newline at end of file diff --git a/examples/snippets/ai-agent/actor-sqlite.ts b/examples/snippets/ai-agent/actor-sqlite.ts new file mode 100644 index 000000000..b61b19eb1 --- /dev/null +++ b/examples/snippets/ai-agent/actor-sqlite.ts @@ -0,0 +1,86 @@ +import { actor } from "actor-core"; +import { drizzle } from "@actor-core/drizzle"; +import { generateText, tool } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { getWeather } from "./my-utils"; +import { messages } from "./schema"; + +export type Message = { role: "user" | "assistant"; content: string; timestamp: number; } + +const aiAgent = actor({ + sql: drizzle(), + + actions: { + // Get conversation history + getMessages: async (c) => { + const result = await c.db + .select() + .from(messages) + .orderBy(messages.timestamp.asc()); + + return result; + }, + + // Send a message to the AI and get a response + sendMessage: async (c, userMessage: string) => { + const now = Date.now(); + + // Add user message to conversation + const userMsg = { + conversationId: c.actorId, // Use the actor instance ID + role: "user", + content: userMessage, + }; + + // Store user message + await c.db + .insert(messages) + .values(userMsg); + + // Get all messages + const allMessages = await c.db + .select() + .from(messages) + .orderBy(messages.timestamp.asc()); + + // Generate AI response using Vercel AI SDK with tools + const { text } = await generateText({ + model: openai("o3-mini"), + prompt: userMessage, + messages: allMessages, + tools: { + weather: tool({ + description: 'Get the weather in a location', + parameters: { + location: { + type: 'string', + description: 'The location to get the weather for', + }, + }, + execute: async ({ location }) => { + return await getWeather(location); + }, + }), + }, + }); + + // Add AI response to conversation + const assistantMsg = { + role: "assistant", + content: text, + }; + + // Store assistant message + await c.db + .insert(messages) + .values(assistantMsg); + + // Broadcast the new message to all connected clients + c.broadcast("messageReceived", assistantMsg); + + return assistantMsg; + }, + } +}); + +export default aiAgent; \ No newline at end of file diff --git a/examples/snippets/chat-room/App.tsx b/examples/snippets/chat-room/App.tsx new file mode 100644 index 000000000..6b7e631f2 --- /dev/null +++ b/examples/snippets/chat-room/App.tsx @@ -0,0 +1,71 @@ +import { createClient } from "actor-core/client"; +import { createReactActorCore } from "@actor-core/react"; +import { useState, useEffect } from "react"; +import type { App } from "../actors/app"; +import type { Message } from "./actor"; + +const client = createClient("http://localhost:6420"); +const { useActor, useActorEvent } = createReactActorCore(client); + +export function ChatRoom({ roomId = "general" }) { + // Connect to specific chat room using tags + const [{ actor }] = useActor("chatRoom", { + tags: { roomId } + }); + + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + + // Load initial state + useEffect(() => { + if (actor) { + // Load chat history + actor.getHistory().then(setMessages); + } + }, [actor]); + + // Listen for real-time updates from the server + useActorEvent({ actor, event: "newMessage" }, (message) => { + setMessages(prev => [...prev, message]); + }); + + const sendMessage = () => { + if (actor && input.trim()) { + actor.sendMessage("User", input); + setInput(""); + } + }; + + return ( +
+
+

Chat Room: {roomId}

+
+ +
+ {messages.length === 0 ? ( +
No messages yet. Start the conversation!
+ ) : ( + messages.map((msg, i) => ( +
+ {msg.sender}: {msg.text} + + {new Date(msg.timestamp).toLocaleTimeString()} + +
+ )) + )} +
+ +
+ setInput(e.target.value)} + onKeyPress={e => e.key === "Enter" && sendMessage()} + placeholder="Type a message..." + /> + +
+
+ ); +} \ No newline at end of file diff --git a/examples/snippets/chat-room/actor-json.ts b/examples/snippets/chat-room/actor-json.ts new file mode 100644 index 000000000..60f486c6a --- /dev/null +++ b/examples/snippets/chat-room/actor-json.ts @@ -0,0 +1,31 @@ +import { actor } from "actor-core"; + +export type Message = { sender: string; text: string; timestamp: number; } + +const chatRoom = actor({ + // State is automatically persisted + state: { + messages: [] as Message[] + }, + + // Initialize the room + createState: () => ({ + messages: [] + }), + + actions: { + sendMessage: (c, sender: string, text: string) => { + const message = { sender, text, timestamp: Date.now() }; + + // Any changes to state are automatically saved + c.state.messages.push(message); + + // Broadcast events trigger real-time updates in connected clients + c.broadcast("newMessage", message); + }, + + getHistory: (c) => c.state.messages + } +}); + +export default chatRoom; \ No newline at end of file diff --git a/examples/snippets/chat-room/actor-sqlite.ts b/examples/snippets/chat-room/actor-sqlite.ts new file mode 100644 index 000000000..fe244d3cd --- /dev/null +++ b/examples/snippets/chat-room/actor-sqlite.ts @@ -0,0 +1,40 @@ +import { actor } from "actor-core"; +import { drizzle } from "@actor-core/drizzle"; +import { messages } from "./schema"; + +export type Message = { sender: string; text: string; timestamp: number; } + +const chatRoom = actor({ + sql: drizzle(), + + actions: { + sendMessage: async (c, sender: string, text: string) => { + const message = { + sender, + text, + timestamp: Date.now(), + }; + + // Insert the message into SQLite + await c.db.insert(messages).values(message); + + // Broadcast to all connected clients + c.broadcast("newMessage", message); + + // Return the created message (matches JS memory version) + return message; + }, + + getHistory: async (c) => { + // Query all messages ordered by timestamp + const result = await c.db + .select() + .from(messages) + .orderBy(messages.timestamp); + + return result as Message[]; + } + } +}); + +export default chatRoom; \ No newline at end of file diff --git a/examples/snippets/crdt/App.tsx b/examples/snippets/crdt/App.tsx new file mode 100644 index 000000000..1692fb23f --- /dev/null +++ b/examples/snippets/crdt/App.tsx @@ -0,0 +1,178 @@ +import { createClient } from "actor-core/client"; +import { createReactActorCore } from "@actor-core/react"; +import { useState, useEffect, useRef } from "react"; +import * as Y from 'yjs'; +import { applyUpdate, encodeStateAsUpdate } from 'yjs'; +import type { App } from "../actors/app"; + +const client = createClient("http://localhost:6420"); +const { useActor, useActorEvent } = createReactActorCore(client); + +export function YjsEditor({ documentId = "shared-doc" }) { + // Connect to specific document using tags + const { actor } = useActor("yjsDocument", { + tags: { documentId } + }); + + // Document state + const [isLoading, setIsLoading] = useState(true); + const [text, setText] = useState(""); + + // Local Yjs document + const yDocRef = useRef(null); + // Flag to prevent infinite update loops + const updatingFromServer = useRef(false); + const updatingFromLocal = useRef(false); + // Track if we've initialized observation + const observationInitialized = useRef(false); + + // Initialize local Yjs document and connect + useEffect(() => { + // Create Yjs document + const yDoc = new Y.Doc(); + yDocRef.current = yDoc; + setIsLoading(false); + + return () => { + // Clean up Yjs document + yDoc.destroy(); + }; + }, [actor]); + + // Set up text observation + useEffect(() => { + const yDoc = yDocRef.current; + if (!yDoc || observationInitialized.current) return; + + // Get the Yjs Text type from the document + const yText = yDoc.getText('content'); + + // Observe changes to the text + yText.observe(() => { + // Only update UI if change wasn't from server + if (!updatingFromServer.current) { + // Update React state + setText(yText.toString()); + + if (actor && !updatingFromLocal.current) { + // Set flag to prevent loops + updatingFromLocal.current = true; + + // Convert update to base64 and send to server + const update = encodeStateAsUpdate(yDoc); + const base64 = bufferToBase64(update); + actor.applyUpdate(base64).finally(() => { + updatingFromLocal.current = false; + }); + } + } + }); + + observationInitialized.current = true; + }, [actor]); + + // Handle initial state from server + useActorEvent({ actor, event: "initialState" }, ({ update }) => { + const yDoc = yDocRef.current; + if (!yDoc) return; + + // Set flag to prevent update loops + updatingFromServer.current = true; + + try { + // Convert base64 to binary and apply to document + const binary = atob(update); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + + // Apply update to Yjs document + applyUpdate(yDoc, bytes); + + // Update React state + const yText = yDoc.getText('content'); + setText(yText.toString()); + } catch (error) { + console.error("Error applying initial update:", error); + } finally { + updatingFromServer.current = false; + } + }); + + // Handle updates from other clients + useActorEvent({ actor, event: "update" }, ({ update }) => { + const yDoc = yDocRef.current; + if (!yDoc) return; + + // Set flag to prevent update loops + updatingFromServer.current = true; + + try { + // Convert base64 to binary and apply to document + const binary = atob(update); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + + // Apply update to Yjs document + applyUpdate(yDoc, bytes); + + // Update React state + const yText = yDoc.getText('content'); + setText(yText.toString()); + } catch (error) { + console.error("Error applying update:", error); + } finally { + updatingFromServer.current = false; + } + }); + + // Handle text changes from user + const handleTextChange = (e: React.ChangeEvent) => { + if (!yDocRef.current) return; + + const newText = e.target.value; + const yText = yDocRef.current.getText('content'); + + // Only update if text actually changed + if (newText !== yText.toString()) { + // Set flag to avoid loops + updatingFromLocal.current = true; + + // Update Yjs document (this will trigger observe callback) + yDocRef.current.transact(() => { + yText.delete(0, yText.length); + yText.insert(0, newText); + }); + + updatingFromLocal.current = false; + } + }; + + if (isLoading) { + return
Loading collaborative document...
; + } + + return ( +
+

Collaborative Document: {documentId}

+