UNPKG

partysync

Version:

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

155 lines (152 loc) 5.04 kB
import { startTransition, useEffect, useMemo, useOptimistic, useState } from "react"; import { RPCClient } from "partyfn"; import * as idb from "idb"; //#region src/client/persist.ts var Persist = class { dbName = "partysync"; version = 1; constructor(channel) { this.channel = channel; } 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 records = await (await this.getDb()).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 tx = (await this.getDb()).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 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; } }; //#endregion //#region 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((value) => { const updatedRecords = [...value]; 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); } 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((value) => { const newValue = [...value]; 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); } } persist.set(result); return newValue; }); }); }); }]; } //#endregion export { useSync }; //# sourceMappingURL=index.js.map