partysync
Version:
Experimental library to synchronize state between a Durable Object and client. See [design discussion](https://github.com/cloudflare/partykit/issues/147).
129 lines (128 loc) • 3.68 kB
JavaScript
import {
SyncServer
} from "../chunk-5FZDUERS.js";
import {
RPCClient
} from "../chunk-TCG3YHK4.js";
// src/agent/index.ts
import { getServerByName } from "partyserver";
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 stub = await getServerByName(
this.env[namespace],
room
);
const res = await stub.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) {
var _a;
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) => {
var _a2;
const data = JSON.parse(event.data);
if ("sync" in data && data.sync && data.channel === channel) {
const syncResponse = data;
this.state[channel] = syncResponse.payload;
(_a2 = this.ws) == null ? void 0 : _a2.removeEventListener("message", handleSyncMessage);
}
};
(_a = this.ws) == null ? void 0 : _a.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;
}
});
}
};
export {
Agent
};
//# sourceMappingURL=index.js.map