UNPKG

y-durableobjects

Version:

[![Yjs on Cloudflare Workers with Durable Objects Demo Movie](https://i.gyazo.com/e94637740dbb11fc5107b0cd0850326d.gif)](https://gyazo.com/e94637740dbb11fc5107b0cd0850326d)

360 lines (348 loc) 10.4 kB
import { upgrade } from "./chunk-44JZXIF7.js"; // src/index.ts import { Hono as Hono2 } from "hono"; import { hc } from "hono/client"; // src/yjs/index.ts import { DurableObject } from "cloudflare:workers"; import { removeAwarenessStates } from "y-protocols/awareness"; import { applyUpdate as applyUpdate2, encodeStateAsUpdate as encodeStateAsUpdate2 } from "yjs"; // src/yjs/remote/ws-shared-doc.ts import { createDecoder, readVarUint, readVarUint8Array } from "lib0/decoding"; import { createEncoder as createEncoder2, length, toUint8Array, writeVarUint as writeVarUint2, writeVarUint8Array } from "lib0/encoding"; import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate } from "y-protocols/awareness"; import { readSyncMessage, writeUpdate } from "y-protocols/sync"; import { Doc } from "yjs"; // src/yjs/message-type/index.ts import { createEncoder, writeVarUint } from "lib0/encoding"; var messageType = { sync: 0, awareness: 1 }; var isMessageType = (type) => { return Object.keys(messageType).includes(type); }; var createTypedEncoder = (type) => { if (!isMessageType(type)) { throw new Error(`Unsupported message type: ${type}`); } const encoder = createEncoder(); writeVarUint(encoder, messageType[type]); return encoder; }; // src/yjs/remote/ws-shared-doc.ts var WSSharedDoc = class extends Doc { listeners = /* @__PURE__ */ new Set(); awareness = new Awareness(this); constructor(gc = true) { super({ gc }); this.awareness.setLocalState(null); this.awareness.on("update", (changes) => { this.awarenessChangeHandler(changes); }); this.on("update", (update) => { this.syncMessageHandler(update); }); } update(message) { const encoder = createEncoder2(); const decoder = createDecoder(message); const type = readVarUint(decoder); switch (type) { case messageType.sync: { writeVarUint2(encoder, messageType.sync); readSyncMessage(decoder, encoder, this, null); if (length(encoder) > 1) { this._notify(toUint8Array(encoder)); } break; } case messageType.awareness: { applyAwarenessUpdate(this.awareness, readVarUint8Array(decoder), null); break; } } } notify(listener) { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; } syncMessageHandler(update) { const encoder = createTypedEncoder("sync"); writeUpdate(encoder, update); this._notify(toUint8Array(encoder)); } awarenessChangeHandler({ added, updated, removed }) { const changed = [...added, ...updated, ...removed]; const encoder = createTypedEncoder("awareness"); const update = encodeAwarenessUpdate( this.awareness, changed, this.awareness.states ); writeVarUint8Array(encoder, update); this._notify(toUint8Array(encoder)); } _notify(message) { for (const subscriber of this.listeners) { subscriber(message); } } }; // src/yjs/client/setup.ts import { toUint8Array as toUint8Array2, writeVarUint8Array as writeVarUint8Array2 } from "lib0/encoding"; import { encodeAwarenessUpdate as encodeAwarenessUpdate2 } from "y-protocols/awareness"; import { writeSyncStep1 } from "y-protocols/sync"; var setupWSConnection = (ws, doc) => { { const encoder = createTypedEncoder("sync"); writeSyncStep1(encoder, doc); ws.send(toUint8Array2(encoder)); } { const states = doc.awareness.getStates(); if (states.size > 0) { const encoder = createTypedEncoder("awareness"); const update = encodeAwarenessUpdate2( doc.awareness, Array.from(states.keys()) ); writeVarUint8Array2(encoder, update); ws.send(toUint8Array2(encoder)); } } }; // src/yjs/hono/index.ts import { Hono } from "hono"; var createApp = (service) => { const app2 = new Hono(); return app2.get("/rooms/:roomId", async (c) => { const roomId = c.req.param("roomId"); const client = await service.createRoom(roomId); return new Response(null, { webSocket: client, status: 101, statusText: "Switching Protocols" }); }); }; // src/yjs/storage/index.ts import { Doc as Doc2, applyUpdate, encodeStateAsUpdate } from "yjs"; // src/yjs/storage/storage-key/index.ts var storageKey = (key) => { return `ydoc:${key.type}:${key.name ?? ""}`; }; // src/yjs/storage/index.ts var YTransactionStorageImpl = class { constructor(storage, options) { this.storage = storage; this.MAX_BYTES = options?.maxBytes ?? 10 * 1024; if (this.MAX_BYTES > 128 * 1024) { throw new Error("maxBytes must be less than 128KB"); } this.MAX_UPDATES = options?.maxUpdates ?? 500; } MAX_BYTES; MAX_UPDATES; async getYDoc() { const snapshot = await this.storage.get( storageKey({ type: "state", name: "doc" }) ); const data = await this.storage.list({ prefix: storageKey({ type: "update" }) }); const updates = Array.from(data.values()); const doc = new Doc2(); doc.transact(() => { if (snapshot) { applyUpdate(doc, snapshot); } for (const update of updates) { applyUpdate(doc, update); } }); return doc; } storeUpdate(update) { return this.storage.transaction(async (tx) => { const bytes = await tx.get(storageKey({ type: "state", name: "bytes" })) ?? 0; const count = await tx.get(storageKey({ type: "state", name: "count" })) ?? 0; const updateBytes = bytes + update.byteLength; const updateCount = count + 1; if (updateBytes > this.MAX_BYTES || updateCount > this.MAX_UPDATES) { const doc = await this.getYDoc(); applyUpdate(doc, update); await this._commit(doc, tx); } else { await tx.put(storageKey({ type: "state", name: "bytes" }), updateBytes); await tx.put(storageKey({ type: "state", name: "count" }), updateCount); await tx.put(storageKey({ type: "update", name: updateCount }), update); } }); } async _commit(doc, tx) { const data = await tx.list({ prefix: storageKey({ type: "update" }) }); for (const update2 of data.values()) { applyUpdate(doc, update2); } const update = encodeStateAsUpdate(doc); await tx.delete(Array.from(data.keys())); await tx.put(storageKey({ type: "state", name: "bytes" }), 0); await tx.put(storageKey({ type: "state", name: "count" }), 0); await tx.put(storageKey({ type: "state", name: "doc" }), update); } async commit() { const doc = await this.getYDoc(); return this.storage.transaction(async (tx) => { await this._commit(doc, tx); }); } }; // src/yjs/index.ts var YDurableObjects = class extends DurableObject { constructor(state, env) { super(state, env); this.state = state; this.env = env; void this.state.blockConcurrencyWhile(this.onStart.bind(this)); } app = createApp({ createRoom: this.createRoom.bind(this) }); doc = new WSSharedDoc(); storage = new YTransactionStorageImpl({ get: (key) => this.state.storage.get(key), list: (options) => this.state.storage.list(options), put: (key, value) => this.state.storage.put(key, value), delete: async (key) => this.state.storage.delete(Array.isArray(key) ? key : [key]), transaction: (closure) => this.state.storage.transaction(closure) }); sessions = /* @__PURE__ */ new Map(); awarenessClients = /* @__PURE__ */ new Set(); async onStart() { const doc = await this.storage.getYDoc(); applyUpdate2(this.doc, encodeStateAsUpdate2(doc)); for (const ws of this.state.getWebSockets()) { this.registerWebSocket(ws); } this.doc.on("update", async (update) => { await this.storage.storeUpdate(update); }); this.doc.awareness.on( "update", async ({ added, removed, updated }) => { for (const client of [...added, ...updated]) { this.awarenessClients.add(client); } for (const client of removed) { this.awarenessClients.delete(client); } } ); } createRoom(roomId) { const pair = new WebSocketPair(); const client = pair[0]; const server = pair[1]; server.serializeAttachment({ roomId, connectedAt: /* @__PURE__ */ new Date() }); this.state.acceptWebSocket(server); this.registerWebSocket(server); return client; } fetch(request) { return this.app.request(request, void 0, this.env); } async updateYDoc(update) { this.doc.update(update); await this.cleanup(); } async getYDoc() { return encodeStateAsUpdate2(this.doc); } async webSocketMessage(ws, message) { if (!(message instanceof ArrayBuffer)) return; const update = new Uint8Array(message); await this.updateYDoc(update); } async webSocketError(ws) { await this.unregisterWebSocket(ws); await this.cleanup(); } async webSocketClose(ws) { await this.unregisterWebSocket(ws); await this.cleanup(); } registerWebSocket(ws) { setupWSConnection(ws, this.doc); const s = this.doc.notify((message) => { ws.send(message); }); this.sessions.set(ws, s); } async unregisterWebSocket(ws) { try { const dispose = this.sessions.get(ws); dispose?.(); this.sessions.delete(ws); const clientIds = this.awarenessClients; removeAwarenessStates(this.doc.awareness, Array.from(clientIds), null); } catch (e) { console.error(e); } } async cleanup() { if (this.sessions.size < 1) { await this.storage.commit(); } } }; // src/index.ts var app = new Hono2(); var yRoute = (selector) => { const route = app.get("/:id", upgrade(), async (c) => { const obj = selector(c.env); const stub = obj.get(obj.idFromName(c.req.param("id"))); const url = new URL("/", c.req.url); const client = hc(url.toString(), { fetch: stub.fetch.bind(stub) }); const res = await client.rooms[":roomId"].$get( { param: { roomId: c.req.param("id") } }, { init: { headers: c.req.raw.headers } } ); return new Response(null, { webSocket: res.webSocket, status: res.status, statusText: res.statusText }); }); return route; }; export { WSSharedDoc, YDurableObjects, yRoute }; //# sourceMappingURL=index.js.map