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