UNPKG

rwsdk

Version:

Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime

333 lines (332 loc) 19.7 kB
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" };