diff --git a/index.bs b/index.bs index d5e0c49..6c4ad9d 100644 --- a/index.bs +++ b/index.bs @@ -18,406 +18,7 @@ Status: LD Title: Stateful Serverless API -## What Is Stateful Serverless? ## {#what-is-stateful-serverless} - -Unless you're deep in to Cloudflare Durable Objects or Rivet Actors, there's a good chance you've never heard of "stateful serverless." - -**Stateful serverless** allows serverless functions to maintain state across multiple invocations. They're very similar to **Web Workers (specifically `SharedWorker`) on the server** and share characteristics with the **actor model**. The most popular implementation of this today is Durable Objects. - -The adoption of stateful serverless is fairly new: Durable Objects was announced 4.5 years ago and only recently saw widespread use over the past couple years. - -### Stateless Serverless (Functions) vs Stateful Serverless (Workers) ### {#stateless-vs-stateful} - -**Stateless serverless** is what you'd normally think of when you hear **"serverless functions."** An example implementation on Cloudflare Workers would look like this: - -
-export default {
- fetch(request, env) {
- return new Response('Hello World!');
- }
-}
-
-
-On the other hand, **stateful serverless provides an infinitely running process with storage**. An example implementation on ActorCore would look like this:
-
-
-// Once created, this DurableObject will exist indefinitely
-export class Counter extends DurableObject {
- count: number;
-
- constructor(state, env) {
- super(state, env);
- this.setup();
- }
-
- async setup() {
- this.count = state.storage.get("count") || 0;
- }
-
- increment(count: number) {
- // Update state
- this.count += 1;
-
- // Persist state
- await this.ctx.storage.put("count", this.count);
-
- return this.count;
- }
-}
-
-
-### Primary Use Cases ### {#primary-use-cases}
-
-The primary use cases of stateful serverless are:
-
-* **Long-Running Processes**: Tasks that execute over extended periods or in multiple steps. For example, **AI Agents** with ongoing conversations and stateful tool calls.
-* **Stateful Services**: Applications where maintaining state across interactions is critical. For example, **Collaborative Apps** with shared editing and automatic persistence.
-* **Realtime Systems**: Applications requiring fast, in-memory state modifications or push updates to connected clients. For example, **Multiplayer Games** with game rooms and player state.
-* **Durability**: Processes that must survive crashes and restarts without data loss. For example, **Durable Execution** workflows that continue after system restarts.
-* **Horizontal Scalability**: Systems that need to scale by distributing load across many instances. For example, **Realtime Stream Processing** for stateful event handling.
-* **Local-First Architecture**: Systems that synchronize state between offline clients. For example, **Local-First Sync** between devices.
-
-Read more about stateful serverless here.
-
-### Scope Of This Proposal ### {#scope}
-
-For the purpose of this article, we'll assume **stateful serverless includes message passing & persistent storage**. This means we're not going to consider actor runtimes, such as Erlang/OTP, Akka, Orleans, Actix, and Swift Actors which do not include storage as a core component.
-
-## Why Should I Care? ## {#why-care}
-
-Even if you've never heard of stateful serverless, you're probably using a site on a daily basis that already relies on stateful serverless with technologies like Cloudflare Durable Objects:
-
-- CodePen
-- Liveblocks
-- Clerk
-- Wordware
-- Playroom
-- Tldraw
-
-Similarly, a significant portion of applications in general are powered by services with the actor pattern, which is a subset of what this standard would provide:
-
-- WhatsApp (notoriously acquired for $19B with Erlang/OTP having only 35 engineers)
-- Discord
-- LinkedIn
-- Twitter/X
-- Pinterest
-- PayPal
-- Halo
-- FoundationDB (powering Apple, Snowflake, DataDog)
-- Many, many more
-
-## Why Build A Web Standard? ## {#why-build-standard}
-
-### Customers Wary Of Vendor-Lock ### {#vendor-lock}
-
-In 2025, customers are **wary of vendor-locking themselves to cloud services**. It's common for vendor-locked providers to either shut down, turn out to be unreliable, or price gouge contracts because their customers cannot leave.
-
-Therefore, a standard allows all platforms offering stateful serverless to become a compelling offering. It's a rising tide: more competition means wider adoption and more customers.
-
-For example: though AWS is the leading cloud provider and pushes their own closed-source & non-standard software like DynamoDB & Lambda, **they still need to provide standard- and open-source-compatible software** like Redis (ElastiCache), Cassandra (Keyspace), and Postgres/MySQL (Aurora) in order to remain an attractive offering for customers concerned about vendor lock.
-
-### Incentivize More Providers ### {#incentivize-providers}
-
-**A standard would incentivize more cloud providers to enter the stateful serverless space.** Currently, only Cloudflare, Rivet, and ActorCore offer this capability, leaving a massive opportunity for other serverless cloud providers to adopt stateful serverless. With a common standard, more frameworks & developers can adopt this model.
-
-## Today's Stateful Serverless Implementations ## {#current-implementations}
-
-### Cloudflare Durable Objects: The Most Popular Option ### {#cloudflare-durable-objects}
-
-Today, Cloudflare Durable Objects are the incumbent of stateful serverless cloud providers.
-
-A Durable Object looks something like this:
-
-
-export class Counter extends DurableObject {
- constructor(state, env) {
- super(state, env);
- this.setup();
- }
-
- async setup() {
- this.count = state.storage.get("count") || 0;
- }
-
- increment(count: number) {
- this.count += 1;
- await this.ctx.storage.put("count", this.count);
- return this.count;
- }
-}
-
-
-### Cloudflare Agents Framework: Moving Beyond Durable Objects ### {#cloudflare-agents}
-
-The Cloudflare Agents framework is for AI-powered agents to operate on the Durable Objects infrastructure.
-
-As stated in their launch blog post:
-
-> Over the coming weeks, expect to see ... the ability to self-host agents on your own infrastructure.
-
-Purely theorizing: a standard for stateful serverless might provide the foundation for this.
-
-### Rivet: Open-Source Serverless Infrastructure ### {#rivet}
-
-I'm the founder of Rivet and have a vested interest in seeing stateful serverless become a standard. Rivet provides an open-source stateful serverless platform that can be easily self-hosted.
-
-We also provide a handful of features in our runtime that don't make sense to be part of a standard, since the best W3C standards build on existing web standards: Docker containers for non-JavaScript applications, HTTP, UDP, and TCP support, advanced lifecycle management, actor tagging for advanced multi-tenant applications, advanced control over actor upgrades for companies with specific use cases, fine-grained control over where your actor is running, and a developer-friendly REST API for managing Rivet Actors.
-
-We encourage developers building on Rivet to use ActorCore. However, we also provide a low-level API for defining actors:
-
-
-import type { ActorContext } from "@rivet-gg/actor";
-import * as http from "http";
-
-export default {
- async start(ctx: ActorContext) {
- let count = ctx.kv.get("count") || 0;
- const server = http.createServer((req, res) => {
- count += 1;
- await ctx.kv.put("count", count);
-
- res.writeHead(200, { "Content-Type": "text/plain" });
- res.end(`Count: ${this.count}`);
- });
- server.listen(process.env.PORT_HTTP);
-
- // Keep the actor running until explicitly destroyed
- await new Promise((resolve) => {});
- }
-};
-
-
-Unlike Durable Objects, Rivet's core API design opts to lean into Unix-like patterns such as using `SIGINT` signals, leveraging `process.exit`, and using environment variables.
-
-Check out the source code on GitHub.
-
-### ActorCore Framework: Stateful Serverless On Any Cloud ### {#actorcore}
-
-At Rivet, we also manage a framework called ActorCore that provides a Durable Object-like experience on any cloud that provides _stateless_ serverless.
-
-An ActorCore Actor definition looks something like this:
-
-
-import { actor } from "actor-core";
-
-const chatRoom = actor({
- state: { messages: [] },
- actions: {
- // receive an action call from the client
- sendMessage: (c, username, message) => {
- // save message to persistent storage
- c.state.messages.push({ username, message });
-
- // broadcast message to all clients
- c.broadcast("newMessage", username, message);
- },
- // allow client to request message history
- getMessages: (c) => c.state.messages
- }
-});
-
-
-Under the hood is where it gets interesting: ActorCore is intended to support as many platforms as possible and is **completely abstracted with drivers**. By doing so, ActorCore supports Rivet, Cloudflare Durable Objects, Bun, Node.js. Vercel, Supabase, Deno, and Lambda are coming in the near future.
-
-These drivers provide a **peek at what a standard might require**, since they're already abstracted to be platform-agnostic:
-
-**ActorDriver**: Manages actor state, lifecycle, and persistence
-
-
-```
-export interface ActorDriver {
- kvGet(actorId: string, key: KvKey): Promise;
- kvGetBatch(actorId: string, key: KvKey[]): Promise<(KvValue | undefined)[]>;
- kvPut(actorId: string, key: KvKey, value: KvValue): Promise;
- kvPutBatch(actorId: string, key: [KvKey, KvValue][]): Promise;
- kvDelete(actorId: string, key: KvKey): Promise;
- kvDeleteBatch(actorId: string, key: KvKey[]): Promise;
- setAlarm(actor: AnyActorInstance, timestamp: number): Promise;
-}
-```
-
-
-**ManagerDriver**: Handles actor discovery, routing, and scaling
-
-
-```
-export interface ManagerDriver {
- getForId(input: GetForIdInput): Promise;
- getWithTags(input: GetWithTagsInput): Promise;
- createActor(input: CreateActorInput): Promise;
-}
-
-export interface GetForIdInput {
- c?: HonoContext;
- baseUrl: string;
- actorId: string;
-}
-
-export interface GetWithTagsInput {
- c?: HonoContext;
- baseUrl: string;
- name: string;
- tags: ActorTags;
-}
-
-export interface GetActorOutput {
- c?: HonoContext;
- endpoint: string;
- name: string;
- tags: ActorTags;
-}
-
-export interface CreateActorInput {
- c?: HonoContext;
- baseUrl: string;
- name: string;
- tags: ActorTags;
- region?: string;
-}
-
-export interface CreateActorOutput {
- endpoint: string;
-}
-```
-
-
-There is also a **CoordinatedDriver** that provides support for implementing actors over peer-to-peer for platforms that don't natively support stateful serverless. This would not apply to an actor standard.
-
-Read more about building your own ActorCore drivers.
-
-Check out the source code on GitHub.
-
-### Other Actor Runtimes: The Precursor To Stateful Serverless ### {#other-runtimes}
-
-Runtimes like OTP (i.e. Erlang & Elixir & Gleam), Akka, Orleans, Actix, and Swift Actors all **lack built-in support for persisted state**. There are libraries that provide state for actors, but their models are significantly different than what we'll consider here. Additionally, we're focused on JavaScript since that's primary language for web standards.
-
-## Specification Goals ## {#specification-goals}
-
-For the purpose of this exploration, I'm going to **recommend sticking with web standards**, which are managed by the W3C organization. Doing so allows us to **build on top of existing successful web standards**, such as WinterTC and the fetch specification that are currently offered across almost all stateless serverless platforms already.
-
-The goals of this proposal are:
-
-- **Simplicity Of Implementation**: Implementing standards correctly is difficult, expensive, and highly error prone. Simpler is better.
-- **Simplicity Of Usage**: Stateful serverless is not a concept that is easy for many developers to understand. Prefer simplicity vs advanced features to keep the API as simple as possible for greater adoption.
-- **Build For Frameworks**: Hono and itty-router already proved that frameworks are almost always used with serverless runtimes. Stateful serverless is already used widely with PartyKit, ActorCore, Agents, and misc tools like TinyBase. If possible, leave functionality up to frameworks to implement.
-- **Built On Existing Web Development Paradigms**: Stateful serverless is very similar to Web Workers. Where possible refer to Web Workers to see what works well.
-- **Focus On Stateful Workloads, Not Actors**: I love building distributed systems with the actor model, but actors require extra consideration around message handling, concurrency, and backpressure.
-
-### Classes vs Functions ### {#classes-vs-functions}
-
-Durable Objects provides a class-based approach to stateful serverless (e.g. `class X extends DurableObject { ... }`). Rivet provides a functional approach (e.g. `export default { start () { ... } }`).
-
-Sticking to the WinterTC ESM-style makes sense. Additionally, it's similar to how Web Workers are already defined.
-
-### Alternative #1: Fetch-Based Communication ### {#alternative-fetch}
-
-Both Rivet & Durable Objects use WinterTC-like `fetch` handlers to accept requests on Durable Objects/Rivet Actors. An alternative spec would opt to use `fetch` handlers instead of a brand new definition. This is similar to what WinterTC is attempting to do: take existing patterns and standardize them as opposed to implementing a new standard.
-
-This standard makes an intentional decision to define a simpler, more portable API than provide compatibility with only 2 existing implementations.
-
-### Alternative #2: A CGI-Like Standard ### {#alternative-cgi}
-
-An alternative implementation is to consider something along the lines of CGI which provides a more Unix-like implementation of serverless functions & supports multiple languages. However, the industry has consolidated behind JavaScript & WebAssembly for serverless clouds because of the developer & cost efficiency. Additionally, CGI lacks many of the modern features that W3C specifications take into account for web standards. (Originally proposed by Armin Ronacher.)
-
-## Drawing Inspiration From `SharedWorker` ## {#shared-worker-inspiration}
-
-
-
-Though designed for client-side concurrency, `SharedWorker` offers a programming model that closely resembles what we're discussing for server-side actors. The `SharedWorker` API already has many of the core concepts needed for a stateful serverless standard:
-
-1. **Multi-Tenant**: `SharedWorker` can serve multiple browser tabs, similar to how Durable Objects and Rivet Actors serves multiple clients
-2. **Isolated Execution Context**: Each worker runs in its own isolated environment
-3. **Message-Based Communication**: Workers communicate via structured message passing.
-
-You can think of a `ServerlessWorker` like a `SharedWorker` that is:
-
-- In the cloud
-- Includes persistent storage
-- Runs forever
-
-Here's a comparison of how Web Workers operate in the browser:
-
-
-// main.js - Browser
-const worker = new Worker('worker.js');
-worker.postMessage({ type: 'INCREMENT', value: 5 });
-worker.onmessage = event => console.log('Counter:', event.data);
-
-// worker.js - Worker
-let counter = 0;
-self.onmessage = event => {
- const { type, value } = event.data;
- if (type === 'INCREMENT') {
- counter += value;
- self.postMessage(counter);
- }
-};
-
-
-
-// server.js - Fetch handler
-const id = env.COUNTER.idFromName('counter-1');
-const stub = env.COUNTER.get(id);
-const response = await stub.fetch('https://counter', {
- method: 'POST',
- body: JSON.stringify({ type: 'INCREMENT', value: 5 })
-});
-const counter = await response.json();
-console.log('Counter:', counter);
-
-// counter.js - Durable Object
-export class Counter {
- constructor(state) {
- this.state = state;
- this.counter = 0;
- }
-
- async fetch(request) {
- const { type, value } = await request.json();
- if (type === 'INCREMENT') {
- this.counter += value;
- await this.state.storage.put('counter', this.counter);
- return new Response(JSON.stringify(this.counter));
- }
- return new Response('Invalid request', { status: 400 });
- }
-}
-
-
-
-// client.js - Client side
-import { createClient } from 'actor-core';
-
-const client = createClient();
-const counter = await client.counter.get('counter-1');
-const result = await counter.increment(5);
-console.log('Counter:', result);
-
-// counter.js - Actor definition
-import { actor } from 'actor-core';
-
-export const counter = actor({
- state: { value: 0 },
- actions: {
- increment: (ctx, value) => {
- ctx.state.value += value;
- return ctx.state.value;
- }
- }
-});
-
-
@@ -431,43 +32,6 @@ The key differences are:
### Creating, Initializing, & Addressing `ServerlessWorker`s ### {#creating-workers}
-**Current Implementation: Rivet & ActorCore**
-
-Rivet and ActorCore provide a tagging system for organizing actors. This allows you to build more manageable multi-tenant applications with complex requirements. For example:
-
-Rivet allows you to create actors with the `actors.create` endpoint. The API accepts environment variables to configure how the actor behaves.
-
-ActorCore allows passing tags to `client.get({ /* tags */ })` to get or create an actor, but does not provide environment variables. You can also use the `client.create` method to create a fresh actor. Actors can read their own tags to configure behavior accordingly:
-
--``` -const client = createClient- -**Current Implementation: Durable Objects** - -To create a Durable Object on Cloudflare, you call the `newUniqueId` method. This will give you a unique ID for a new Durable Object. - -Cloudflare also provides a `idFromName` method to get the ID of an Durable Object from an arbitrary string. This ID is then used to resolve a Durable Object stub that you can send requests to. For example: - -("http://localhost:6420"); -const randomChannel = await client.chatRoom.get({ organization: "rivet", channel: "random" }); -``` -
-const id = env.MY_DURABLE_OBJECT.idFromName("channel:rivet:random");
-const stub = env.MY_DURABLE_OBJECT.get(id);
-await stub.fetch(...);
-
-
-Cloudflare Durable Objects is desigend to have no concept of "created" or "destroyed." The initial state can be configured by sending an RPC to the DO upon creation. This implementation of not having an input state for a `ServerlessWorker` is simpler and leaves the functionality up to the framework; instead all a `ServerlessWorker` has a unique ID that developers can send messages to.
-
-**Current Implementation: `SharedWorker`**
-
-`SharedWorker` accepts a `name` parameter, for example: `new SharedWorker("./sharedWorker.js", "chat-room-random")`. This is akin to calling `idFromName` for Durable Objects. Passing no name will give you the same worker.
-
-The `name` is accessible in the global scope of the `SharedWorker`.
-
-**Proposal**
-
Constructing a `ServerlessWorker` object will mimic the `SharedWorker` API. Passing a `name` parameter to the second constructor argument will let you address a given `ServerlessWorker`. Passing no arguments will give you the same worker.
The `name` is accessible in the global scope of the `ServerlessWorker`.
@@ -504,22 +68,6 @@ console.log(self.name);
### Terminating `ServerlessWorker`s ### {#terminating-workers}
-**Current Implementation: Rivet**
-
-Rivet provides two ways to destroy an actor: use `process.exit(0)` or call `actors.destroy` from the API.
-
-ActorCore will provide a `ctx.shutdown` method that delegates to `ActorDriver`, though is not currently implemented.
-
-**Current Implementation: Durable Objects**
-
-Cloudflare's approach to destroying Durable Objects is somewhat unique. There is no concept of "created" or "destroyed" actor. To reset an actor, you clear the storage with `deleteAll()`. While unintuitive, this leaves it up to the framework to implement.
-
-**Current Implementation: `SharedWorker`**
-
-Web Workers' `SharedWorker` is automatically destroyed when all ports are closed. This is not relevant to stateful serverless.
-
-**Proposal**
-
Add a `terminate` method on the `ServerlessWorker` itself, leave it up to the frameworks to implement destroying. For example:
@@ -532,54 +80,7 @@ export default {
### Connections & Messages ### {#connections-messages}
-**Current Implementation: Rivet**
-
-Both Rivet Actors & Durable Objects provide a way to serve requests to actors based on web standards.
-
-Rivet provides flexible networking infrastructure, including UDP, TCP, and host networking. To remain widely compatible, Rivet relies on the Deno implementation of the fetch handler with `Deno.serve`.
-
-**Current Implementation: Cloudflare**
-
-Durable Objects supports a similar fetch handler, but also provides non-standard features like RPC.
-
-Both rely on different implementations for WebSockets, though this handled gracefully by libraries like Hono's WebSocket helper.
-
-There is a WinterTC proposal for a Sockets API, though I'm going to consider this out of the scope of this article since Cloudflare's implementation is not currently supported on Durable Objects.
-
-**Current Implementation: `SharedWorker`**
-
-However, Web Worker (`SharedWorker`) opts for using `onconnect` & `port.onmessage` like this:
-
-
-
-const sharedWorker = new SharedWorker('sharedWorker.js');
-sharedWorker.port.postMessage({ action: 'greet', data: 'Hello from Tab' });
-sharedWorker.port.onmessage = function(event) {
- console.log('Received response from SharedWorker:', event.data);
-};
-sharedWorker.port.start();
-
-
-
-onconnect = (event) => {
- const port = event.ports[0];
-
- port.onmessage = (event) => {
- const { action, data } = event.data;
-
- if (action === 'greet') {
- port.postMessage({ response: `SharedWorker received: ${data}` });
- }
- };
-};
-
-
-
-**Proposal**
-
-This could go two ways: lean into the existing WinterTC for ESM-style handlers like Durable Objects and Rivet or opt for a simpler & portable interface like Web Workers.
-
-In my opinion, request/response should use ESM-style exports with the `SharedWorker` interface. This allows the simplicity of request/response & also enables full bidirectional streaming -- like a WebSocket but without the overhead.
+`ServerlessWorker` should use ESM-style exports with a `SharedWorker`-like API. This allows the simplicity of request/response & also enables full bidirectional streaming -- like a WebSocket but without the overhead.
@@ -592,7 +93,7 @@ export default {
};
-
+
export default {
connect(conn) {
const port = conn.ports[0];
@@ -611,63 +112,8 @@ export default {
-Coincidentally, this also feels very similar to socket.io whose API has proven itself as flexible & easy to understand.
-
### Storage ### {#storage}
-**Current Implementations: Rivet & Durable Objects & Deno**
-
-Both Rivet and Durable Objects support a raw KV interface to access data stored on the actor itself.
-
-While not specific to actors, Deno attempts to implement an abstract KV interface. However, this API is very opinionated to Deno Deploy and is by no means compatible across multiple clouds.
-
-**Why KV**
-
-Anything more (especially `IndexedDB`) is too complicated to expect cloud providers to implement correctly. `localStorage` has proven itself a simple & reliable key-value store that's lasted the test of time.
-
-Frameworks can implement more advanced logic on top of key-value stores, just as they've done in the browser with `localStorage`.
-
-**Async**
-
-Regardless of what API this results in, it should be asynchronous. Cloudflare has gone as far as removing async code for their SQLite interface because they state it's fast enough to not require an async context; the KV API could likely be made as simple as something like the browser's `localStorage` by making each key a synchronous get. However, platforms all implement KV differently, so this may not make sense if attempting to provide maximum compatibility. Additionally, it would be impossible for compatibility layers to implement a KV API with an external medium if that medum requires an async operation to communicate with.
-
-**Strings vs Structured Clone**
-
-`localStorage` currently only supports strings, which is what most developers are used to.
-
-However, both Rivet & Cloudflare Workers support natively storing data types that implement structured cloning which allows storing many types of data in a compact V8-compatible binary interface with high-performance serialization & deserialization.
-
-If structured cloning was a protocol defined earlier, `localStorage` would likely support it natively. Whatever storage mechanism is chosen should support structured cloning.
-
-**A Word On Complexity**
-
-W3C has a history of inventing incredibly complicated standards for storage that are both (a) hard to understand as developers and (b) difficult to implement as a platform.
-
-If you know how to use the IndexedDB API from memory, I'm impressed.
-
-Additionally, Web SQL was an alternative standard that was deprecated because of its complexity.
-
-Whatever the storage mechanism for `ServerlessWorker`s is, **keep it simple, easy to implement, and easy to understand**. Frameworks can always do the heavy lifting for you.
-
-**Concurrency & Input/Output Gates**
-
-Concurrency is difficult to implement correctly across multiple platforms.
-
-Cloudflare Durable Objects intelligently prevents common storage race conditions on Durable Objects without transactions through a mechanism called _input/output gates_ detailed here. This can be disabled with `ctx.storage.get(/* key*/, { allowConcurrency: true })`. Doing so will make the KV work like naive get/set without any extra functionality.
-
-This is a controversial opinion, but I think that this behavior should be disabled by default and not part of a specification for a few reasons:
-
-1. Durable Object users almost never read from storage at runtime. `storage.get` is called in the constructor and `storage.put` is called when changed.
-2. This behavior can be implemented at the framework level.
-3. This behavior is almost certainly going to be incorrectly implemented by vendors.
-4. Input/output gates do not work with WebSocket on Durable Objects already (please correct me if I'm wrong) and most developers aren't accounting for this. `postMessage` and `onmessage` are closer to the Durable Object WebSocket behavior than the RPC behavior.
-
-**Keeping Scope Small**
-
-For the purpose of building a broader picture of what `ServerlessWorker` might look like in practice, I'm going to assume that storage is going to be part of this specification. However, I think it makes more sense to either (a) find an existing mature spec to use for storage or (b) define a separate spec that can work in contexts outside of `ServerlessWorker`.
-
-**Proposal**
-
Provide a dead simple async structured clone-based KV API and let the platforms provide extra configs for concurrency. For example:
@@ -687,14 +133,6 @@ export default {
### Scheduling ### {#scheduling}
-**Current Implementations**
-
-A core part of `ServerlessWorker`s is to be able to run a function at an arbitrary timestamp.
-
-Cloudflare provides a simple `setAlarm` API that has a single alarm. If you already have an alarm, it will override that alarm. I like this API since it's significantly simpler than `setInterval` and `setTimeout` and lets frameworks provide more advanced APIs, such as CRONs.
-
-**Proposal**
-
Provide an API similar to Cloudflare's `setAlarm`. For example:
@@ -710,20 +148,6 @@ export default {
### Sleeping & Upgrading & Host Migrations ### {#sleeping-upgrading}
-A core advantage of `ServerlessWorker`s to something like Kubernetes jobs or other container-based stateful workloads: `ServerlessWorker`s can **automatically sleep when there are no active operations** and **wake upon either fetch or alarm**.
-
-However, `ServerlessWorker` has to address specific edge cases when there are tasks that need to run in the background before being put to sleep.
-
-**Current Implementation: Rivet**
-
-Rivet mimics Unix behavior by sending a `SIGINT` signal to the process when it plans to go to sleep. Your application is given time to clean up any work and restart gracefully when it's ready. This provides compatibility for a wide range of existing frameworks that implement the Node.js or Deno shutdown handler.
-
-**Current Implementation: Durable Objects**
-
-Durable Objects provides `waitUntil`. My understanding is this is effectively a noop since (see note). However, I like the `waitUntil` API since it provides a JavaScript-y way of forcing the actor to stay awake.
-
-**Proposal**
-
Provide an API similar to Durable Objects `waitUntil`. The provider can decide the maximum duration for this function. For example:
@@ -736,24 +160,10 @@ waitUntil(doSomethingAsync());
### Handling Backpressure ### {#backpressure}
-Backpressure is a core problem in every distributed system and should always be handled carefully by developers to handle load gracefully.
-
-**Current Implementation: Rivet**
-
-Rivet provides low-level primitives to the developer, so it's up to the developer to handle backpressure accordingly. An overloaded actor without any special backpressure handling will saturate the CPU.
-
-**Current Implementation: Durable Objects**
-
-Durable Objects throw an error with the `.overloaded` property set to `true` when making requests to a Durable Object that cannot accept more requests.
-
-**Proposal**
-
Define an error type that can be used by developers to handle backpressure gracefully.
### Security Model ### {#security-model}
-**Proposal**
-
`ServerlessWorker` should rely on the existing work in WinterTC. They are secured via these attributes:
- **Fetch Handler**: Stateful `ServerlessWorker`s are only accessible through the stateless serverless fetch handler defined in WinterTC. This creates a protected gateway where all requests are authenticated and authorized before reaching any `ServerlessWorker`.
@@ -762,385 +172,3 @@ Define an error type that can be used by developers to handle backpressure grace
The downside of relying on the fetch handler is that it may restrict who can implement `ServerlessWorker`. For example, there are many container orchestration platforms that may be able to implement the worker part of `ServerlessWorker`, but cannot implement the `fetch` handler. I don't think this is an issue, since including these technologies that don't already comply to WinterTC would make it too broad to be effective.
-### Out Of Scope ### {#out-of-scope}
-
-Items out of the scope for this spec:
-
-- **Locality**: Cloudflare & Rivet both have notions of where actors run, though the default is to run nearest where the request is coming from. This will likely end up as a platform-specific feature when constructing `ServerlessWorker`.
-- **Supervisors**: Traditional actor runtimes use "supervisors" to automatically restart crashed actors. `ServerlessWorker` cannot crash, and instead will automatically log and disregard thrown errors -- similar to service workers.
-- **SQLite Storage**: Many serious use cases of Durable Objects rely on SQLite. This is out of the scope for this web standard.
-- **Versioning**: Cloudflare and Rivet each have significantly different ways of implementing versioning. Rivet is more like AWS AMI, while Cloudflare uses a simpler but less flexible version history.
-- **Upgrades & Rolling Deploys**: Leave it up to the platform to decide what code is running. Under the hood, the Rivet implementation of this standard would automatically call `actors.upgrade`
-- **Logging**: Leave this up to the platform to decide how to ship logs.
-
-## Full Examples ## {#examples}
-
-### Rate Limiter ### {#rate-limiter-example}
-
-This example demonstrates building a rate limit.
-
-
-
-// 10 requests/minute
-const maxRequests = 10;
-const bucketDuration = 60_000;
-
-// Load state from storage
-let count = await storage.get("count") || 0;
-let resetAt = await storage.get("resetAt") || (Date.now() + bucketDuration);
-
-function saveState() {
- await storage.put("count", count);
- await storage.put("resetAt", resetAt);
-}
-
-export default {
- connect(conn) {
- const port = conn.ports[0];
-
- port.onmessage = (event) => {
- const { type, ...data } = event.data;
-
- // Check if bucket should be reset
- const now = Date.now();
- if (now >= resetAt) {
- count = 0;
- resetAt = now + bucketDuration;
- saveState();
- }
-
- switch (type) {
- // Check if within rate limit
- case "HIT":
- // Update limit
- const isAllowed = count < maxRequests;
- if (isAllowed) {
- count += 1;
- saveState();
- }
-
- conn.postMessage({ isAllowed });
-
- break;
-
- // Reset rate limit (unused, purely for example)
- case "RESET":
- count = 0;
- resetAt = now + bucketDuration;
- saveState();
- break;
- }
- };
- }
-}
-
-
-
-export default {
- async fetch(request, env) {
- const url = new URL(request.url);
-
- // Get client IP
- const forwardedFor = request.headers.get('x-forwarded-for') || '127.0.0.1';
- const clientIp = forwardedFor.split(',')[0].trim();
-
- // Create a rate limiter for this IP
- const limiterId = `rate-limit:${ip}`;
- const worker = new ServerlessWorker("./rateLimiter.js", limiterId);
-
- // Handle response
- const { promise, resolve, reject } = Promise.withResolvers();
- worker.port.onmessage = (event) => {
- const { isAllowed } = event.data;
-
- if (isAllowed) {
- // Request is allowed - continue to handle the actual request
- resolve(new Response("Request allowed"));
- } else {
- // Request is rate limited
- resolve(new Response("Rate limit exceeded", { status: 429 }));
- }
- };
-
- // Send the rate limit check
- worker.port.postMessage({ type: "HIT" });
-
- return await promise;
- }
-}
-
-
-
-### Pub/Sub Server ### {#pubsub-example}
-
-This example demonstrates a simple pub/sub system where each topic is its own serverless worker with connected clients.
-
-
-
-// A set of all connected clients
-const subscribers = new Set();
-
-export default {
- connect(event) {
- const port = event.ports[0];
- subscribers.add(port);
-
- port.onmessage = event => {
- const { type, data } = event.data;
-
- if (type === "publish") {
- // Broadcast to all subscribers
- for (const client of subscribers) {
- client.postMessage({
- type: "message",
- data
- });
- }
- }
- };
-
- // Remove on disconnect
- port.onclose = () => {
- subscribers.delete(port);
- };
- }
-}
-
-
-
-export default {
- async fetch(request) {
- const url = new URL(request.url);
- const [_, topic] = url.pathname.split('/');
-
- if (!topic) {
- return new Response("Specify a topic in the URL path");
- }
-
- // Each topic gets its own worker
- const worker = new ServerlessWorker("./topicWorker.js", `topic-${topic}`);
-
- // Handle WebSocket upgrade
- if (request.headers.get("Upgrade") === "websocket") {
- const pair = new WebSocketPair();
- const client = pair[0];
- const server = pair[1];
-
- // Connect to worker
- const port = worker.port;
-
- // Client -> Worker
- server.onmessage = event => {
- try {
- port.postMessage(JSON.parse(event.data));
- } catch (err) {
- server.send(JSON.stringify({ error: "Invalid JSON" }));
- }
- };
-
- // Worker -> Client
- port.onmessage = event => {
- server.send(JSON.stringify(event.data));
- };
-
- server.accept();
-
- return new Response(null, {
- status: 101,
- webSocket: client
- });
- }
-
- return new Response("Expected WebSocket");
- }
-}
-
-
-
-// Subscribe to a topic
-const ws = new WebSocket("wss://pubsub.example.com/news");
-
-// Handle incoming messages
-ws.onmessage = event => {
- const { type, data } = JSON.parse(event.data);
-
- if (type === "message") {
- console.log("New message:", data);
- }
-};
-
-// Publish a message to the topic
-function publish(data) {
- ws.send(JSON.stringify({
- type: "publish",
- data
- }));
-}
-
-// Example usage
-publish("Breaking news!");
-
-
-
-### Simple Chat Room ### {#chat-room-example}
-
-This example demonstrates a simple chat room implementation with message persistence.
-
-
-
-// Keep track of all connections to this chat room
-const connections = new Set();
-
-// Keep messages in memory
-let messages = await storage.get("messages") ?? [];
-
-// Broadcast a message to all connected clients
-function broadcast(message) {
- for (const conn of connections) {
- conn.postMessage(message);
- }
-}
-
-export default {
- connect(event) {
- const port = event.ports[0];
- connections.add(port);
-
- // Send message history to new client
- if (messages.length > 0) {
- port.postMessage({
- type: "history",
- messages: messages.slice(-20)
- });
- }
-
- port.onmessage = async (event) => {
- const { text, username } = event.data;
-
- // Create message object
- const message = {
- text,
- username,
- timestamp: Date.now()
- };
-
- // Store message
- messages.push(message);
-
- // Save periodically
- if (messages.length % 10 === 0) {
- await storage.put("messages", messages);
- }
-
- // Broadcast to all clients
- broadcast({
- type: "message",
- message
- });
- };
-
- // Handle client disconnection
- port.onclose = () => {
- connections.delete(port);
-
- // If this was the last connection, save messages
- if (connections.size === 0) {
- waitUntil(storage.put("messages", messages));
- }
- };
- }
-}
-
-
-
-export default {
- async fetch(request) {
- const url = new URL(request.url);
-
- // Get chat room name from URL
- const [_, roomId] = url.pathname.split('/');
-
- if (!roomId) {
- return new Response("Please specify a room ID in the URL path", { status: 400 });
- }
-
- // Get or create a worker for this room
- const worker = new ServerlessWorker("./chatRoom.js", `chat-${roomId}`);
-
- // Handle websocket upgrade
- if (request.headers.get('Upgrade') !== 'websocket') {
- return new Response('Expected websocket', { status: 426 });
- }
-
- // Create a WebSocket pair
- const pair = new WebSocketPair();
- const client = pair[0];
- const server = pair[1];
-
- // Connect client and worker
- const port = worker.port;
-
- // Client -> Worker
- server.onmessage = (event) => {
- try {
- port.postMessage(JSON.parse(event.data));
- } catch (err) {
- server.send(JSON.stringify({ error: "Invalid JSON" }));
- }
- };
-
- // Worker -> Client
- port.onmessage = (event) => {
- server.send(JSON.stringify(event.data));
- };
-
- // Handle WebSocket close
- server.onclose = () => {
- port.close();
- };
-
- server.accept();
-
- return new Response(null, {
- status: 101,
- webSocket: client
- });
- }
-}
-
-
-
-// Connect to a chat room
-const username = "user" + Math.floor(Math.random() * 1000);
-const ws = new WebSocket("wss://chat.example.com/room123");
-
-// Handle incoming messages
-ws.onmessage = event => {
- const data = JSON.parse(event.data);
-
- if (data.type === "message") {
- console.log(`${data.message.username}: ${data.message.text}`);
- } else if (data.type === "history") {
- console.log("--- Message History ---");
- data.messages.forEach(msg => {
- console.log(`${msg.username}: ${msg.text}`);
- });
- console.log("---------------------");
- }
-};
-
-// Send a chat message
-function sendMessage(text) {
- ws.send(JSON.stringify({
- text,
- username
- }));
-}
-
-// Example usage
-ws.onopen = () => {
- sendMessage("Hello everyone!");
-};
-
-
-