y-durableobjects
Version:
[](https://gyazo.com/e94637740dbb11fc5107b0cd0850326d)
360 lines (348 loc) • 10.4 kB
JavaScript
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