UNPKG

partysync

Version:

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

221 lines (218 loc) 6.49 kB
import { RPCClient } from "../chunk-TCG3YHK4.js"; // src/react/index.tsx import { startTransition, useEffect, useMemo, useOptimistic, useState } from "react"; // src/client/persist.ts import * as idb from "idb"; var Persist = class { constructor(channel) { this.channel = channel; } dbName = "partysync"; version = 1; async getDb() { const channel = this.channel; return idb.openDB(`${this.dbName}-${channel}`, this.version, { upgrade(db) { if (!db.objectStoreNames.contains(channel)) { db.createObjectStore(channel, { keyPath: "key" }); } } }); } async getAll() { const db = await this.getDb(); const records = await db.getAll(this.channel); const values = []; for (const record of records) { values.push(record.value); } values.sort( (a, b) => new Date(a.at(-3)).getTime() - new Date(b.at(-3)).getTime() ); return values; } async set(records) { const db = await this.getDb(); const tx = db.transaction(this.channel, "readwrite"); const store = tx.objectStore(this.channel); for (const record of records) { await store.put({ key: record[0], value: record }); } await tx.done; } // async delete(id: string): Promise<void> { // const db = await this.getDb(); // await db.delete(this.channel, id); // } async deleteDeletedRecordsBefore(date) { const db = await this.getDb(); const records = await db.getAll(this.channel); const tx = db.transaction(this.channel, "readwrite"); const store = tx.objectStore(this.channel); const cutoff = date.getTime(); for (const record of records) { if (record.value.at(-1) !== null && new Date(record.value.at(-1)).getTime() < cutoff) { await store.delete(record.key); } } await tx.done; } // async deleteAll(): Promise<void> { // const db = await this.getDb(); // await db.clear(this.channel); // } }; // src/react/index.tsx function useRPC(key, socket) { const [rpc] = useState( () => new RPCClient(key, socket) ); useEffect(() => { return () => { rpc.destroy(); }; }, [rpc]); return rpc; } function useSync(key, socket, optimisticReducer = (currentState) => currentState) { const persist = useMemo(() => new Persist(key), [key]); const [value, setValue] = useState([]); const rpc = useRPC(key, socket); useEffect(() => { const controller = new AbortController(); persist.getAll().then((records) => { setValue(records.filter((r) => r.at(-1) === null)); let lastRecordTime = null; for (const record of records) { const recordDeletedAt = record[record.length - 1]; const recordUpdatedAt = record[record.length - 2]; if (recordDeletedAt && (!lastRecordTime || recordDeletedAt > lastRecordTime)) { lastRecordTime = recordDeletedAt; } if (recordUpdatedAt && (!lastRecordTime || recordUpdatedAt > lastRecordTime)) { lastRecordTime = recordUpdatedAt; } } socket.send( JSON.stringify({ channel: key, sync: true, from: lastRecordTime }) ); }); socket.addEventListener( "message", async (event) => { const message = JSON.parse(event.data); if (message.channel === key && message.sync === true) { setValue((value2) => { const updatedRecords = [...value2]; for (const record of message.payload) { const index = updatedRecords.findIndex((r) => r[0] === record[0]); if (index !== -1) { updatedRecords.splice(index, 1, record); } else { updatedRecords.push(record); } } persist.set(message.payload); return updatedRecords; }); } }, { signal: controller.signal } ); return () => { controller.abort(); }; }, [socket, key, persist]); useEffect(() => { function handleMessage(event) { const message = JSON.parse(event.data); if (message.broadcast === true && message.channel === key) { if (message.type === "update") { setValue((records) => { const updates = message.payload; const updatedRecords = [...records]; for (const update of updates) { const index = updatedRecords.findIndex((r) => r[0] === update[0]); if (update.at(-1) === null) { if (index !== -1) { updatedRecords.splice(index, 1, update); } else { updatedRecords.push(update); } } else { if (index !== -1) { updatedRecords.splice(index, 1); } else { } } } persist.set(updates); return updatedRecords; }); } else if (message.type === "delete-all") { setValue([]); } } } socket.addEventListener("message", handleMessage); return () => { socket.removeEventListener("message", handleMessage); }; }, [socket, key, persist]); const [optimisticValue, setOptimisticValue] = useOptimistic(value, (currentState, action) => { return optimisticReducer(currentState, action); }); return [ optimisticValue, (action) => { startTransition(async () => { setOptimisticValue(action); const result = await rpc.call(action); if (result.length === 0) { return; } startTransition(() => { setValue((value2) => { const newValue = [...value2]; for (const record of result) { const index = newValue.findIndex((item) => item[0] === record[0]); if (index !== -1) { if (record.at(-1) !== null) { newValue.splice(index, 1); } else { newValue.splice(index, 1, record); } } else if (index === -1) { if (record.at(-1) === null) { newValue.push(record); } else { } } } persist.set(result); return newValue; }); }); }); } ]; } export { useSync }; //# sourceMappingURL=index.js.map