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
JavaScript
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