rwsdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
333 lines (332 loc) • 19.7 kB
JavaScript
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _SyncedStateServer_instances, _a, _SyncedStateServer_keyHandler, _SyncedStateServer_roomHandler, _SyncedStateServer_setStateHandler, _SyncedStateServer_getStateHandler, _SyncedStateServer_subscribeHandler, _SyncedStateServer_unsubscribeHandler, _SyncedStateServer_identityExtractor, _SyncedStateServer_namespace, _SyncedStateServer_durableObjectName, _SyncedStateServer_stub, _SyncedStateServer_handleClientMessage, _SyncedStateServer_stateStore, _SyncedStateServer_stateStoreLoaded, _SyncedStateServer_loadStateStore, _SyncedStateServer_stateStorageKey, _SyncedStateServer_getState, _SyncedStateServer_setState, _SyncedStateServer_subscribe, _SyncedStateServer_unsubscribe, _SyncedStateServer_broadcastUpdate, _SyncedStateServer_getAttachment, _SyncedStateServer_getIdentity, _SyncedStateServer_getSubscriptionsFromAttachment, _SyncedStateServer_setSubscriptionsInAttachment, _SyncedStateServer_resolveStorageKey, _SyncedStateServer_getStubForHandlers, _SyncedStateServer_send, _SyncedStateServer_sendError;
import { DurableObject } from "cloudflare:workers";
import { getIdentityFromUrl, } from "./identity.mjs";
import { unpackClientMessage, packMessage, } from "./protocol.mjs";
/**
* Durable Object that keeps shared state for multiple clients and notifies
* subscribers, using the Cloudflare Hibernation WebSocket API so idle
* connections do not keep the object active.
*
* The implementation copies the lifecycle pattern from the older
* RealtimeDurableObject but replaces its RSC/action protocol with a small
* JSON state-sync protocol.
*
* Keys arrive as user-facing values. When a key handler is registered, the DO
* transforms them internally using the identity captured at upgrade time by
* the worker. This lets the worker hand off the WebSocket and exit instead of
* staying alive as a proxy.
*/
export class SyncedStateServer extends DurableObject {
static registerKeyHandler(handler) {
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_keyHandler);
}
static getKeyHandler() {
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_keyHandler);
}
static registerRoomHandler(handler) {
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_roomHandler);
}
static getRoomHandler() {
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_roomHandler);
}
static registerIdentityExtractor(extractor) {
__classPrivateFieldSet(_a, _a, extractor, "f", _SyncedStateServer_identityExtractor);
}
static getIdentityExtractor() {
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_identityExtractor);
}
static registerNamespace(namespace, durableObjectName) {
__classPrivateFieldSet(_a, _a, namespace, "f", _SyncedStateServer_namespace);
if (durableObjectName) {
__classPrivateFieldSet(_a, _a, durableObjectName, "f", _SyncedStateServer_durableObjectName);
}
}
static getNamespace() {
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_namespace);
}
static getDurableObjectName() {
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_durableObjectName);
}
static registerSetStateHandler(handler) {
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_setStateHandler);
}
static registerGetStateHandler(handler) {
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_getStateHandler);
}
static registerSubscribeHandler(handler) {
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_subscribeHandler);
}
static registerUnsubscribeHandler(handler) {
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_unsubscribeHandler);
}
static getSubscribeHandler() {
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_subscribeHandler);
}
static getUnsubscribeHandler() {
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_unsubscribeHandler);
}
constructor(state, env) {
super(state, env);
_SyncedStateServer_instances.add(this);
_SyncedStateServer_stub.set(this, null);
// ---------------------------------------------------------------------------
// State storage
// ---------------------------------------------------------------------------
// In-memory cache backed by Durable Object storage. Hibernation can evict
// the DO, so every write is persisted and the cache is warmed on first read.
_SyncedStateServer_stateStore.set(this, new Map());
_SyncedStateServer_stateStoreLoaded.set(this, false);
this.state = state;
this.env = env;
this.storage = state.storage;
}
setStub(stub) {
__classPrivateFieldSet(this, _SyncedStateServer_stub, stub, "f");
}
async fetch(request) {
const url = new URL(request.url);
const clientId = url.searchParams.get("clientId") ?? crypto.randomUUID();
const identity = getIdentityFromUrl(url);
const { 0: client, 1: server } = new WebSocketPair();
const attachment = {
clientId,
identity,
subscriptions: [],
};
server.serializeAttachment(attachment);
this.state.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client });
}
async webSocketMessage(ws, data) {
if (typeof data !== "string") {
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_sendError).call(this, ws, "Expected text WebSocket message");
return;
}
let message;
try {
message = unpackClientMessage(data);
}
catch (error) {
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_sendError).call(this, ws, error instanceof Error ? error.message : "Invalid protocol message");
return;
}
// Once a request id is parsed, the client is waiting for a matching
// response. Every dispatch path must resolve or reject that request id.
try {
await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_handleClientMessage).call(this, ws, message);
}
catch (error) {
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_sendError).call(this, ws, error instanceof Error ? error.message : String(error), message.id);
}
}
async webSocketClose(_ws) {
// context(justinvdm, 29 Jun 2026): Subscriptions are persisted on the
// WebSocket attachment. When the runtime removes the socket, the
// attachment disappears with it, so no persistent cleanup is required.
}
// Public RPC surface exposed to handler callbacks and other Workers RPC callers.
async getState(key) {
return __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getState).call(this, key, undefined);
}
async setState(value, key) {
await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_setState).call(this, key, value, undefined);
}
}
_a = SyncedStateServer, _SyncedStateServer_stub = new WeakMap(), _SyncedStateServer_stateStore = new WeakMap(), _SyncedStateServer_stateStoreLoaded = new WeakMap(), _SyncedStateServer_instances = new WeakSet(), _SyncedStateServer_handleClientMessage = async function _SyncedStateServer_handleClientMessage(ws, message) {
const identity = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getIdentity).call(this, ws);
const storageKey = await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_resolveStorageKey).call(this, message.key, identity);
switch (message.kind) {
case "getState": {
const value = await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getState).call(this, storageKey, identity);
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_send).call(this, ws, {
kind: "getState",
key: message.key,
value,
id: message.id,
});
break;
}
case "setState": {
await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_setState).call(this, storageKey, message.value, identity);
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_send).call(this, ws, {
kind: "setState",
key: message.key,
id: message.id,
});
break;
}
case "subscribe": {
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_subscribe).call(this, ws, storageKey, message.key);
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_send).call(this, ws, {
kind: "subscribe",
key: message.key,
id: message.id,
});
break;
}
case "unsubscribe": {
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_unsubscribe).call(this, ws, storageKey, message.key);
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_send).call(this, ws, {
kind: "unsubscribe",
key: message.key,
id: message.id,
});
break;
}
default: {
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_sendError).call(this, ws, "Unknown message kind", message.id);
}
}
}, _SyncedStateServer_loadStateStore = async function _SyncedStateServer_loadStateStore() {
if (__classPrivateFieldGet(this, _SyncedStateServer_stateStoreLoaded, "f")) {
return;
}
const entries = await this.storage.list({
prefix: "state:",
});
for (const [storageKey, value] of entries) {
const key = storageKey.slice("state:".length);
__classPrivateFieldGet(this, _SyncedStateServer_stateStore, "f").set(key, value);
}
__classPrivateFieldSet(this, _SyncedStateServer_stateStoreLoaded, true, "f");
}, _SyncedStateServer_stateStorageKey = function _SyncedStateServer_stateStorageKey(key) {
return `state:${key}`;
}, _SyncedStateServer_getState = async function _SyncedStateServer_getState(key, identity) {
await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_loadStateStore).call(this);
const value = __classPrivateFieldGet(this, _SyncedStateServer_stateStore, "f").get(key);
if (__classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_getStateHandler)) {
const stub = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getStubForHandlers).call(this);
if (stub) {
__classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_getStateHandler).call(_a, key, value, identity, stub);
}
}
return value;
}, _SyncedStateServer_setState = async function _SyncedStateServer_setState(key, value, identity) {
await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_loadStateStore).call(this);
__classPrivateFieldGet(this, _SyncedStateServer_stateStore, "f").set(key, value);
await this.storage.put(__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_stateStorageKey).call(this, key), value);
if (__classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_setStateHandler)) {
const stub = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getStubForHandlers).call(this);
if (stub) {
__classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_setStateHandler).call(_a, key, value, identity, stub);
}
}
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_broadcastUpdate).call(this, key, value);
}, _SyncedStateServer_subscribe = function _SyncedStateServer_subscribe(ws, storageKey, userKey) {
// Defensive deduplication: a stateful client may send subscribe more than
// once for the same key (e.g. across reconnects). Keep only one entry per
// (socket, userKey) pair in the attachment.
const subs = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getSubscriptionsFromAttachment).call(this, ws);
if (!subs.some((s) => s.userKey === userKey && s.storageKey === storageKey)) {
subs.push({ userKey, storageKey });
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_setSubscriptionsInAttachment).call(this, ws, subs);
}
const identity = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getIdentity).call(this, ws);
const subscribeHandler = __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_subscribeHandler);
if (subscribeHandler) {
const stub = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getStubForHandlers).call(this);
if (stub) {
subscribeHandler(storageKey, identity, stub);
}
}
}, _SyncedStateServer_unsubscribe = function _SyncedStateServer_unsubscribe(ws, storageKey, userKey) {
const subs = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getSubscriptionsFromAttachment).call(this, ws).filter((s) => !(s.userKey === userKey && s.storageKey === storageKey));
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_setSubscriptionsInAttachment).call(this, ws, subs);
const identity = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getIdentity).call(this, ws);
const unsubscribeHandler = __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_unsubscribeHandler);
if (unsubscribeHandler) {
const stub = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getStubForHandlers).call(this);
if (stub) {
unsubscribeHandler(storageKey, identity, stub);
}
}
}, _SyncedStateServer_broadcastUpdate = function _SyncedStateServer_broadcastUpdate(storageKey, value) {
// Cloudflare's live socket list is the source of truth for currently
// connected sockets. Each socket's attachment holds the user-facing keys it
// subscribed to, so we send each socket its original key even after the DO
// has hibernated.
for (const ws of this.ctx.getWebSockets()) {
const subs = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getSubscriptionsFromAttachment).call(this, ws);
for (const sub of subs) {
if (sub.storageKey !== storageKey)
continue;
const message = {
kind: "update",
key: sub.userKey,
value,
};
try {
ws.send(packMessage(message));
}
catch {
// Socket is already closed; it will be cleaned up via webSocketClose.
}
}
}
}, _SyncedStateServer_getAttachment = function _SyncedStateServer_getAttachment(ws) {
const raw = ws.deserializeAttachment();
if (raw &&
typeof raw === "object" &&
"subscriptions" in raw &&
Array.isArray(raw.subscriptions)) {
return raw;
}
return { clientId: "", identity: undefined, subscriptions: [] };
}, _SyncedStateServer_getIdentity = function _SyncedStateServer_getIdentity(ws) {
return __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getAttachment).call(this, ws).identity;
}, _SyncedStateServer_getSubscriptionsFromAttachment = function _SyncedStateServer_getSubscriptionsFromAttachment(ws) {
return __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getAttachment).call(this, ws).subscriptions;
}, _SyncedStateServer_setSubscriptionsInAttachment = function _SyncedStateServer_setSubscriptionsInAttachment(ws, subscriptions) {
const attachment = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getAttachment).call(this, ws);
attachment.subscriptions = subscriptions;
ws.serializeAttachment(attachment);
}, _SyncedStateServer_resolveStorageKey =
// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------
async function _SyncedStateServer_resolveStorageKey(key, identity) {
const keyHandler = __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_keyHandler);
if (keyHandler) {
const stub = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getStubForHandlers).call(this);
return await keyHandler(key, identity, stub ?? {});
}
return key;
}, _SyncedStateServer_getStubForHandlers = function _SyncedStateServer_getStubForHandlers() {
if (__classPrivateFieldGet(this, _SyncedStateServer_stub, "f")) {
return __classPrivateFieldGet(this, _SyncedStateServer_stub, "f");
}
const namespace = __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_namespace);
if (namespace) {
return namespace.get(this.ctx.id);
}
return null;
}, _SyncedStateServer_send = function _SyncedStateServer_send(ws, message) {
try {
ws.send(packMessage(message));
}
catch {
// Ignore send failures on closed sockets.
}
}, _SyncedStateServer_sendError = function _SyncedStateServer_sendError(ws, message, id) {
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_send).call(this, ws, { kind: "error", message, id });
};
_SyncedStateServer_keyHandler = { value: null };
_SyncedStateServer_roomHandler = { value: null };
_SyncedStateServer_setStateHandler = { value: null };
_SyncedStateServer_getStateHandler = { value: null };
_SyncedStateServer_subscribeHandler = { value: null };
_SyncedStateServer_unsubscribeHandler = { value: null };
_SyncedStateServer_identityExtractor = { value: null };
_SyncedStateServer_namespace = { value: null };
_SyncedStateServer_durableObjectName = { value: "syncedStateHibernation" };