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