UNPKG

y-durableobjects

Version:

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

1 lines 18.1 kB
{"version":3,"sources":["/home/runner/work/y-durableobjects/y-durableobjects/dist/index.cjs","../src/index.ts","../src/yjs/index.ts","../src/yjs/remote/ws-shared-doc.ts","../src/yjs/message-type/index.ts","../src/yjs/client/setup.ts","../src/yjs/hono/index.ts","../src/yjs/storage/index.ts","../src/yjs/storage/storage-key/index.ts"],"names":["encodeAwarenessUpdate"],"mappings":"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","file":"/home/runner/work/y-durableobjects/y-durableobjects/dist/index.cjs","sourcesContent":[null,"import { Hono } from \"hono\";\nimport { hc } from \"hono/client\";\n\nimport { upgrade } from \"./middleware\";\n\nimport type { YDurableObjectsAppType } from \"./yjs\";\nimport type { Env } from \"hono\";\n\nconst app = new Hono();\n\ntype Selector<E extends Env> = (c: E[\"Bindings\"]) => DurableObjectNamespace;\n\nexport const yRoute = <E extends Env>(selector: Selector<E>) => {\n const route = app.get(\"/:id\", upgrade(), async (c) => {\n const obj = selector(c.env as E[\"Bindings\"]);\n const stub = obj.get(obj.idFromName(c.req.param(\"id\")));\n\n // get websocket connection\n const url = new URL(\"/\", c.req.url);\n const client = hc<YDurableObjectsAppType>(url.toString(), {\n fetch: stub.fetch.bind(stub),\n });\n const res = await client.rooms[\":roomId\"].$get(\n { param: { roomId: c.req.param(\"id\") } },\n { init: { headers: c.req.raw.headers } },\n );\n\n return new Response(null, {\n webSocket: res.webSocket,\n status: res.status,\n statusText: res.statusText,\n });\n });\n\n return route;\n};\n\nexport { YDurableObjects, type YDurableObjectsAppType } from \"./yjs\";\nexport type YRoute = ReturnType<typeof yRoute>;\nexport type { YTransactionStorage } from \"./yjs/storage\";\nexport { type RemoteDoc, WSSharedDoc } from \"./yjs/remote\";\n","import { DurableObject } from \"cloudflare:workers\";\nimport { removeAwarenessStates } from \"y-protocols/awareness\";\nimport { applyUpdate, encodeStateAsUpdate } from \"yjs\";\n\nimport { WSSharedDoc } from \"../yjs/remote\";\n\nimport { setupWSConnection } from \"./client/setup\";\nimport { createApp } from \"./hono\";\nimport { YTransactionStorageImpl } from \"./storage\";\n\nimport type { AwarenessChanges } from \"../yjs/remote\";\nimport type { Env } from \"hono\";\n\nexport type WebSocketAttachment = {\n roomId: string;\n connectedAt: Date;\n};\n\nexport type YDurableObjectsAppType = ReturnType<typeof createApp>;\n\nexport class YDurableObjects<T extends Env> extends DurableObject<\n T[\"Bindings\"]\n> {\n protected app = createApp({\n createRoom: this.createRoom.bind(this),\n });\n protected doc = new WSSharedDoc();\n protected storage = new YTransactionStorageImpl({\n get: (key) => this.state.storage.get(key),\n list: (options) => this.state.storage.list(options),\n put: (key, value) => this.state.storage.put(key, value),\n delete: async (key) =>\n this.state.storage.delete(Array.isArray(key) ? key : [key]),\n transaction: (closure) => this.state.storage.transaction(closure),\n });\n protected sessions = new Map<WebSocket, () => void>();\n private awarenessClients = new Set<number>();\n\n constructor(\n public state: DurableObjectState,\n public env: T[\"Bindings\"],\n ) {\n super(state, env);\n\n void this.state.blockConcurrencyWhile(this.onStart.bind(this));\n }\n\n protected async onStart(): Promise<void> {\n const doc = await this.storage.getYDoc();\n applyUpdate(this.doc, encodeStateAsUpdate(doc));\n\n for (const ws of this.state.getWebSockets()) {\n this.registerWebSocket(ws);\n }\n\n this.doc.on(\"update\", async (update) => {\n await this.storage.storeUpdate(update);\n });\n this.doc.awareness.on(\n \"update\",\n async ({ added, removed, updated }: AwarenessChanges) => {\n for (const client of [...added, ...updated]) {\n this.awarenessClients.add(client);\n }\n for (const client of removed) {\n this.awarenessClients.delete(client);\n }\n },\n );\n }\n\n protected createRoom(roomId: string) {\n const pair = new WebSocketPair();\n const client = pair[0];\n const server = pair[1];\n server.serializeAttachment({\n roomId,\n connectedAt: new Date(),\n } satisfies WebSocketAttachment);\n\n this.state.acceptWebSocket(server);\n this.registerWebSocket(server);\n\n return client;\n }\n\n fetch(request: Request): Response | Promise<Response> {\n return this.app.request(request, undefined, this.env);\n }\n\n async updateYDoc(update: Uint8Array): Promise<void> {\n this.doc.update(update);\n await this.cleanup();\n }\n async getYDoc(): Promise<Uint8Array> {\n return encodeStateAsUpdate(this.doc);\n }\n\n async webSocketMessage(\n ws: WebSocket,\n message: string | ArrayBuffer,\n ): Promise<void> {\n if (!(message instanceof ArrayBuffer)) return;\n\n const update = new Uint8Array(message);\n await this.updateYDoc(update);\n }\n\n async webSocketError(ws: WebSocket): Promise<void> {\n await this.unregisterWebSocket(ws);\n await this.cleanup();\n }\n\n async webSocketClose(ws: WebSocket): Promise<void> {\n await this.unregisterWebSocket(ws);\n await this.cleanup();\n }\n\n protected registerWebSocket(ws: WebSocket) {\n setupWSConnection(ws, this.doc);\n const s = this.doc.notify((message) => {\n ws.send(message);\n });\n this.sessions.set(ws, s);\n }\n\n protected async unregisterWebSocket(ws: WebSocket) {\n try {\n const dispose = this.sessions.get(ws);\n dispose?.();\n this.sessions.delete(ws);\n const clientIds = this.awarenessClients;\n\n removeAwarenessStates(this.doc.awareness, Array.from(clientIds), null);\n } catch (e) {\n // eslint-disable-next-line no-console\n console.error(e);\n }\n }\n\n protected async cleanup() {\n if (this.sessions.size < 1) {\n await this.storage.commit();\n }\n }\n}\n","import { createDecoder, readVarUint, readVarUint8Array } from \"lib0/decoding\";\nimport {\n createEncoder,\n length,\n toUint8Array,\n writeVarUint,\n writeVarUint8Array,\n} from \"lib0/encoding\";\nimport {\n applyAwarenessUpdate,\n Awareness,\n encodeAwarenessUpdate,\n} from \"y-protocols/awareness\";\nimport { readSyncMessage, writeUpdate } from \"y-protocols/sync\";\nimport { Doc } from \"yjs\";\n\nimport { createTypedEncoder, messageType } from \"../message-type\";\n\nimport type { AwarenessChanges, RemoteDoc } from \".\";\n\ntype Listener<T> = (message: T) => void;\ntype Unsubscribe = () => void;\ninterface Notification<T> extends RemoteDoc {\n notify(cb: Listener<T>): Unsubscribe;\n}\n\nexport class WSSharedDoc extends Doc implements Notification<Uint8Array> {\n private listeners = new Set<Listener<Uint8Array>>();\n readonly awareness = new Awareness(this);\n\n constructor(gc = true) {\n super({ gc });\n this.awareness.setLocalState(null);\n\n // カーソルなどの付加情報の更新通知\n this.awareness.on(\"update\", (changes: AwarenessChanges) => {\n this.awarenessChangeHandler(changes);\n });\n // yDoc の更新通知\n this.on(\"update\", (update: Uint8Array) => {\n this.syncMessageHandler(update);\n });\n }\n\n update(message: Uint8Array) {\n const encoder = createEncoder();\n const decoder = createDecoder(message);\n const type = readVarUint(decoder);\n\n switch (type) {\n case messageType.sync: {\n writeVarUint(encoder, messageType.sync);\n readSyncMessage(decoder, encoder, this, null);\n\n // changed remote doc\n if (length(encoder) > 1) {\n this._notify(toUint8Array(encoder));\n }\n break;\n }\n case messageType.awareness: {\n applyAwarenessUpdate(this.awareness, readVarUint8Array(decoder), null);\n break;\n }\n }\n }\n\n notify(listener: Listener<Uint8Array>) {\n this.listeners.add(listener);\n\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private syncMessageHandler(update: Uint8Array) {\n const encoder = createTypedEncoder(\"sync\");\n writeUpdate(encoder, update);\n\n this._notify(toUint8Array(encoder));\n }\n private awarenessChangeHandler({\n added,\n updated,\n removed,\n }: AwarenessChanges) {\n const changed = [...added, ...updated, ...removed];\n const encoder = createTypedEncoder(\"awareness\");\n const update = encodeAwarenessUpdate(\n this.awareness,\n changed,\n this.awareness.states,\n );\n writeVarUint8Array(encoder, update);\n\n this._notify(toUint8Array(encoder));\n }\n\n private _notify(message: Uint8Array) {\n for (const subscriber of this.listeners) {\n subscriber(message);\n }\n }\n}\n","import { createEncoder, writeVarUint } from \"lib0/encoding\";\n\nexport const messageType = {\n sync: 0,\n awareness: 1,\n};\n\nexport const isMessageType = (\n type: string,\n): type is keyof typeof messageType => {\n return Object.keys(messageType).includes(type);\n};\n\nexport const createTypedEncoder = (type: keyof typeof messageType) => {\n if (!isMessageType(type)) {\n throw new Error(`Unsupported message type: ${type}`);\n }\n\n const encoder = createEncoder();\n writeVarUint(encoder, messageType[type]);\n\n return encoder;\n};\n","import { toUint8Array, writeVarUint8Array } from \"lib0/encoding\";\nimport { encodeAwarenessUpdate } from \"y-protocols/awareness\";\nimport { writeSyncStep1 } from \"y-protocols/sync\";\n\nimport { createTypedEncoder } from \"../message-type\";\n\nimport type { RemoteDoc } from \"../remote\";\n\nexport const setupWSConnection = (ws: WebSocket, doc: RemoteDoc) => {\n {\n const encoder = createTypedEncoder(\"sync\");\n writeSyncStep1(encoder, doc);\n ws.send(toUint8Array(encoder));\n }\n\n {\n const states = doc.awareness.getStates();\n if (states.size > 0) {\n const encoder = createTypedEncoder(\"awareness\");\n const update = encodeAwarenessUpdate(\n doc.awareness,\n Array.from(states.keys()),\n );\n writeVarUint8Array(encoder, update);\n\n ws.send(toUint8Array(encoder));\n }\n }\n};\n","import { Hono } from \"hono\";\n\ntype Service = {\n createRoom(roomId: string): Promise<WebSocket> | WebSocket;\n};\n\nexport const createApp = (service: Service) => {\n const app = new Hono();\n\n return app.get(\"/rooms/:roomId\", async (c) => {\n const roomId = c.req.param(\"roomId\");\n const client = await service.createRoom(roomId);\n\n return new Response(null, {\n webSocket: client,\n status: 101,\n statusText: \"Switching Protocols\",\n });\n });\n};\n","import { Doc, applyUpdate, encodeStateAsUpdate } from \"yjs\";\n\nimport { storageKey } from \"./storage-key\";\n\nimport type { TransactionStorage } from \"./type\";\n\nexport interface YTransactionStorage {\n getYDoc(): Promise<Doc>;\n storeUpdate(update: Uint8Array): Promise<void>;\n commit(): Promise<void>;\n}\n\ntype Options = {\n /**\n * @description default is 10KB\n * @default 10 * 1024 * 1\n */\n maxBytes?: number;\n /**\n * @description default is 500 snapshot\n * @default 500\n */\n maxUpdates?: number;\n};\n\nexport class YTransactionStorageImpl implements YTransactionStorage {\n private readonly MAX_BYTES: number;\n private readonly MAX_UPDATES: number;\n\n constructor(\n private readonly storage: TransactionStorage,\n options?: Options,\n ) {\n this.MAX_BYTES = options?.maxBytes ?? 10 * 1024;\n if (this.MAX_BYTES > 128 * 1024) {\n // https://developers.cloudflare.com/durable-objects/platform/limits/\n throw new Error(\"maxBytes must be less than 128KB\");\n }\n\n this.MAX_UPDATES = options?.maxUpdates ?? 500;\n }\n\n async getYDoc(): Promise<Doc> {\n const snapshot = await this.storage.get<Uint8Array>(\n storageKey({ type: \"state\", name: \"doc\" }),\n );\n const data = await this.storage.list<Uint8Array>({\n prefix: storageKey({ type: \"update\" }),\n });\n\n const updates: Uint8Array[] = Array.from(data.values());\n const doc = new Doc();\n\n doc.transact(() => {\n if (snapshot) {\n applyUpdate(doc, snapshot);\n }\n for (const update of updates) {\n applyUpdate(doc, update);\n }\n });\n\n return doc;\n }\n\n storeUpdate(update: Uint8Array): Promise<void> {\n return this.storage.transaction(async (tx) => {\n const bytes =\n (await tx.get<number>(storageKey({ type: \"state\", name: \"bytes\" }))) ??\n 0;\n const count =\n (await tx.get<number>(storageKey({ type: \"state\", name: \"count\" }))) ??\n 0;\n\n const updateBytes = bytes + update.byteLength;\n const updateCount = count + 1;\n\n if (updateBytes > this.MAX_BYTES || updateCount > this.MAX_UPDATES) {\n const doc = await this.getYDoc();\n applyUpdate(doc, update);\n\n await this._commit(doc, tx);\n } else {\n await tx.put(storageKey({ type: \"state\", name: \"bytes\" }), updateBytes);\n await tx.put(storageKey({ type: \"state\", name: \"count\" }), updateCount);\n await tx.put(storageKey({ type: \"update\", name: updateCount }), update);\n }\n });\n }\n\n private async _commit(doc: Doc, tx: Omit<TransactionStorage, \"transaction\">) {\n const data = await tx.list<Uint8Array>({\n prefix: storageKey({ type: \"update\" }),\n });\n\n for (const update of data.values()) {\n applyUpdate(doc, update);\n }\n\n const update = encodeStateAsUpdate(doc);\n\n await tx.delete(Array.from(data.keys()));\n await tx.put(storageKey({ type: \"state\", name: \"bytes\" }), 0);\n await tx.put(storageKey({ type: \"state\", name: \"count\" }), 0);\n await tx.put(storageKey({ type: \"state\", name: \"doc\" }), update);\n }\n\n async commit(): Promise<void> {\n const doc = await this.getYDoc();\n\n return this.storage.transaction(async (tx) => {\n await this._commit(doc, tx);\n });\n }\n}\n","export type Key =\n | {\n type: \"update\";\n name?: number;\n }\n | {\n type: \"state\";\n name: \"bytes\" | \"doc\" | \"count\";\n };\n\nexport const storageKey = (key: Key) => {\n return `ydoc:${key.type}:${key.name ?? \"\"}`;\n};\n"]}