UNPKG

partysync

Version:

Experimental library to synchronize state between a Durable Object and client. See [design discussion](https://github.com/cloudflare/partykit/issues/147).

84 lines (82 loc) 3.01 kB
import { SyncServer } from "../server/index.js"; import { getServerByName } from "partyserver"; import { RPCClient } from "partyfn"; //#region src/agent/index.ts var Agent = class extends SyncServer { ws = null; state = {}; rpc = {}; constructor(state, env) { super(state, env); this.ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS logs ( id TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()), text TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at INTEGER DEFAULT NULL )`); } onAction(_channel, action) { this.ctx.storage.sql.exec(`INSERT INTO logs (text) VALUES (${action.payload})`); return []; } async connect(namespace, room) { const res = await (await getServerByName(this.env[namespace], room)).fetch("https://dummy-example.com/", { headers: { Upgrade: "websocket" } }); if (!res.webSocket) throw new Error("Failed to connect to server"); this.ws = res.webSocket; this.ws.addEventListener("message", (event) => { const json = JSON.parse(event.data); if ("broadcast" in json && json.broadcast && json.type === "update") { const broadcast = json; if (!this.state[broadcast.channel]) { console.error("channel not synced, discarding update", broadcast.channel); return; } if (broadcast.type === "update") for (const record of broadcast.payload) { const foundIndex = this.state[broadcast.channel].findIndex((item) => item[0] === record[0]); if (foundIndex !== -1) this.state[broadcast.channel].splice(foundIndex, 1, record); else this.state[broadcast.channel].push(record); } else if (broadcast.type === "delete-all") this.state[broadcast.channel] = []; } }); } async sync(channel) { if (this.state[channel]) return; if (!this.ws) throw new Error("WebSocket not connected before sync"); this.ws.send(JSON.stringify({ sync: true, channel, from: null })); const handleSyncMessage = (event) => { const data = JSON.parse(event.data); if ("sync" in data && data.sync && data.channel === channel) { const syncResponse = data; this.state[channel] = syncResponse.payload; this.ws?.removeEventListener("message", handleSyncMessage); } }; this.ws?.addEventListener("message", handleSyncMessage); } async sendAction(channel, action) { await this.ctx.blockConcurrencyWhile(async () => { await this.sync(channel); this.rpc[channel] ||= new RPCClient(channel, this.ws); try { const result = await this.rpc[channel].call(action); for (const record of result) { const foundIndex = this.state[channel].findIndex((item) => item[0] === record[0]); if (foundIndex !== -1) this.state[channel].splice(foundIndex, 1, record); else this.state[channel].push(record); } } catch (error) { console.error("RPC call failed", error); throw error; } }); } }; //#endregion export { Agent }; //# sourceMappingURL=index.js.map