diff --git a/.gitignore b/.gitignore index 46f7caa..efa4db0 100644 --- a/.gitignore +++ b/.gitignore @@ -180,6 +180,7 @@ dist # Environment variables .env* +.env.development !.env.example .vercel diff --git a/package copy.json b/package copy.json deleted file mode 100644 index 0c4f574..0000000 --- a/package copy.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "name": "jeffemmett", - "version": "1.0.0", - "description": "Jeff Emmett's personal website", - "type": "module", - "scripts": { - "dev": "concurrently --kill-others --names client,worker --prefix-colors blue,red \"yarn dev:client\" \"yarn dev:worker\"", - "dev:client": "vite --host --port 5173", - "dev:worker": "wrangler dev --local --port 5172 --ip 0.0.0.0", - "build": "tsc && vite build && wrangler deploy", - "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview", - "deploy": "yarn build && vercel deploy --prod" - }, - "keywords": [], - "author": "Jeff Emmett", - "license": "ISC", - "dependencies": { - "@dimforge/rapier2d": "^0.11.2", - "@tldraw/assets": "^2.0.0", - "@tldraw/tldraw": "^3.4.1", - "@tldraw/sync": "^2.4.6", - "@tldraw/sync-core": "^2.4.6", - "@tldraw/tlschema": "^2.4.6", - "@types/markdown-it": "^14.1.1", - "@vercel/analytics": "^1.2.2", - "@whereby.com/browser-sdk": "^3.9.2", - "cloudflare-workers-unfurl": "^0.0.7", - "crdts": "^0.2.0", - "gray-matter": "^4.0.3", - "itty-router": "^5.0.17", - "lodash.throttle": "^4.1.1", - "markdown-it": "^14.1.0", - "markdown-it-latex2img": "^0.0.6", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-error-boundary": "^4.1.2", - "react-router-dom": "^6.22.3", - "tldraw": "^2.4.6", - "use-local-storage-state": "^19.5.0", - "vercel": "^39.1.1" - }, - "devDependencies": { - "@biomejs/biome": "1.4.1", - "@cloudflare/types": "^6.29.1", - "@cloudflare/workers-types": "^4.20240821.1", - "@types/lodash.throttle": "^4", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^5.59.0", - "@typescript-eslint/parser": "^5.59.0", - "@vitejs/plugin-react": "^4.0.3", - "@vitejs/plugin-react-swc": "^3.6.0", - "concurrently": "^8.2.2", - "eslint": "^8.38.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.3.4", - "typescript": "^5.6.3", - "vite": "^5.3.3", - "vite-plugin-static-copy": "^1.0.6", - "vite-plugin-top-level-await": "^1.3.1", - "vite-plugin-wasm": "^3.2.2", - "wrangler": "^3.88.0" - }, - "resolutions": { - "react": "^18.2.0", - "@types/react": "^18.2.0" - } -} \ No newline at end of file diff --git a/package.json b/package.json index 4c50064..208b7b6 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,11 @@ "license": "ISC", "dependencies": { "@dimforge/rapier2d": "^0.11.2", - "@tldraw/assets": "^3.5.0", - "@tldraw/sync": "^3.4.1", - "@tldraw/sync-core": "^3.4.1", - "@tldraw/tldraw": "^3.4.1", - "@tldraw/tlschema": "^3.4.1", + "@tldraw/assets": "^3.6.0", + "@tldraw/sync": "^3.6.0", + "@tldraw/sync-core": "^3.6.0", + "@tldraw/tldraw": "^3.6.0", + "@tldraw/tlschema": "^3.6.0", "@types/markdown-it": "^14.1.1", "@vercel/analytics": "^1.2.2", "cloudflare-workers-unfurl": "^0.0.7", @@ -35,7 +35,7 @@ "react-dom": "^18.2.0", "react-error-boundary": "^4.1.2", "react-router-dom": "^6.22.3", - "tldraw": "^3.4.1", + "tldraw": "^3.6.0", "use-local-storage-state": "^19.5.0", "vercel": "^39.1.1" }, @@ -61,4 +61,4 @@ "vite-plugin-wasm": "^3.2.2", "wrangler": "^3.88.0" } -} +} \ No newline at end of file diff --git a/src/components/Board copy.tsx b/src/components/Board copy.tsx deleted file mode 100644 index b0c851d..0000000 --- a/src/components/Board copy.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useSync } from '@tldraw/sync' -import { - AssetRecordType, - getHashForString, - TLBookmarkAsset, - Tldraw, - // useLocalStorageState, -} from 'tldraw' -import { useParams } from 'react-router-dom' -import useLocalStorageState from 'use-local-storage-state' -import { ChatBoxTool } from '@/tools/ChatBoxTool' -import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil' -import { VideoChatTool } from '@/tools/VideoChatTool' -import { VideoChatShape } from '@/shapes/VideoChatShapeUtil' -import { multiplayerAssetStore } from '../client/multiplayerAssetStore' -import { customSchema } from '../../worker/TldrawDurableObject' -import { EmbedShape } from '@/shapes/EmbedShapeUtil' -import { EmbedTool } from '@/tools/EmbedTool' - -import React, { useEffect, useState } from 'react'; -import { ChatBox } from '@/shapes/ChatBoxShapeUtil'; -import { components, uiOverrides } from '@/ui-overrides' - -const WORKER_URL = `https://jeffemmett-canvas.jeffemmett.workers.dev` - -const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape] -const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools - -export function Board() { - const { slug } = useParams<{ slug: string }>(); // Ensure this is inside the Board component - const roomId = slug || 'default-room'; // Declare roomId here - - const store = useSync({ - uri: `${WORKER_URL}/connect/${roomId}`, - assets: multiplayerAssetStore, - shapeUtils: shapeUtils, - schema: customSchema, - }); - - const [isChatBoxVisible, setChatBoxVisible] = useState(false); - const [userName, setUserName] = useState(''); - const [isVideoChatVisible, setVideoChatVisible] = useState(false); // Added state for video chat visibility - - const handleNameChange = (event: React.ChangeEvent) => { - setUserName(event.target.value); - }; - - const [persistedStore, setPersistedStore] = useLocalStorageState('board-store', { defaultValue: store } - ) - - useEffect(() => { - setPersistedStore(store); - }, [store]); - - return ( -
- { - editor.registerExternalAssetHandler('url', unfurlBookmarkUrl) - editor.setCurrentTool('hand') - }} - /> - {isChatBoxVisible && ( -
- - -
- )} - {isVideoChatVisible && ( // Render the button to join video chat - - )} -
- ) -} - -// How does our server handle bookmark unfurling? -async function unfurlBookmarkUrl({ url }: { url: string }): Promise { - const asset: TLBookmarkAsset = { - id: AssetRecordType.createId(getHashForString(url)), - typeName: 'asset', - type: 'bookmark', - meta: {}, - props: { - src: url, - description: '', - image: '', - favicon: '', - title: '', - }, - } - - try { - const response = await fetch(`${WORKER_URL}/unfurl?url=${encodeURIComponent(url)}`) - const data = await response.json() as { description: string, image: string, favicon: string, title: string } - - asset.props.description = data?.description ?? '' - asset.props.image = data?.image ?? '' - asset.props.favicon = data?.favicon ?? '' - asset.props.title = data?.title ?? '' - } catch (e) { - console.error(e) - } - - return asset -} diff --git a/src/components/Board.tsx b/src/components/Board.tsx index b584ae8..3dea2ef 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -1,4 +1,5 @@ import { useSync } from '@tldraw/sync' +import { useMemo } from 'react' import { AssetRecordType, getHashForString, @@ -10,7 +11,6 @@ import { TLUiEventSource, } from 'tldraw' import { useParams } from 'react-router-dom' -import useLocalStorageState from 'use-local-storage-state' import { ChatBoxTool } from '@/tools/ChatBoxTool' import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil' import { VideoChatTool } from '@/tools/VideoChatTool' @@ -19,6 +19,7 @@ import { multiplayerAssetStore } from '../client/multiplayerAssetStore' import { customSchema } from '../../worker/TldrawDurableObject' import { EmbedShape } from '@/shapes/EmbedShapeUtil' import { EmbedTool } from '@/tools/EmbedTool' +import { defaultShapeUtils, defaultBindingUtils } from 'tldraw' import React, { useState, useEffect, useCallback } from 'react'; import { ChatBox } from '@/shapes/ChatBoxShapeUtil'; @@ -26,22 +27,25 @@ import { components, uiOverrides } from '@/ui-overrides' import { useCameraControls } from '@/hooks/useCameraControls' import { zoomToSelection } from '../ui-overrides' -//const WORKER_URL = `https://jeffemmett-canvas.jeffemmett.workers.dev` +// Default to production URL if env var isn't available export const WORKER_URL = 'https://jeffemmett-canvas.jeffemmett.workers.dev'; const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape] const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools -// Add these imports -import { useGSetState } from '@/hooks/useGSetState'; -import { useLocalStorageRoom } from '@/hooks/useLocalStorageRoom'; -import { usePersistentBoard } from '@/hooks/usePersistentBoard'; - export function Board() { const { slug } = useParams<{ slug: string }>(); const roomId = slug || 'default-room'; - const store = usePersistentBoard(roomId); + + const storeConfig = useMemo(() => ({ + uri: `${WORKER_URL}/connect/${roomId}`, + assets: multiplayerAssetStore, + shapeUtils: [...shapeUtils, ...defaultShapeUtils], + bindingUtils: [...defaultBindingUtils], + }), [roomId]); + + const store = useSync(storeConfig); const [editor, setEditor] = useState(null) const { zoomToFrame, copyFrameLink, copyLocationLink, revertCamera } = useCameraControls(editor) diff --git a/src/hooks/useGSetState.ts b/src/hooks/useGSetState.ts deleted file mode 100644 index e825fa1..0000000 --- a/src/hooks/useGSetState.ts +++ /dev/null @@ -1,40 +0,0 @@ -import useLocalStorageState from 'use-local-storage-state'; -import GSet from 'crdts/src/G-Set'; -import { TLRecord } from 'tldraw'; -import { useRef, useCallback } from 'react'; - -export function useGSetState(roomId: string) { - const [localSet, setLocalSet] = useLocalStorageState(`gset-${roomId}`, { - defaultValue: [] - }); - - // Keep GSet instance in a ref to persist between renders - const gsetRef = useRef>(); - if (!gsetRef.current) { - gsetRef.current = new GSet(); - // Initialize G-Set with local data - if (localSet && Array.isArray(localSet)) { - localSet.forEach(record => gsetRef.current?.add(record)); - } - } - - const addRecord = useCallback((record: TLRecord) => { - if (!gsetRef.current) return; - gsetRef.current.add(record); - setLocalSet(Array.from(gsetRef.current.values())); - }, [setLocalSet]); - - const merge = useCallback((remoteSet: Set) => { - if (!gsetRef.current) return new Set(); - remoteSet.forEach(record => gsetRef.current?.add(record)); - setLocalSet(Array.from(gsetRef.current.values())); - return gsetRef.current.values(); - }, [setLocalSet]); - - return { - values: gsetRef.current.values(), - add: addRecord, - merge, - localSet - }; -} \ No newline at end of file diff --git a/src/hooks/useLocalStorageRoom.ts b/src/hooks/useLocalStorageRoom.ts deleted file mode 100644 index 946fef5..0000000 --- a/src/hooks/useLocalStorageRoom.ts +++ /dev/null @@ -1,20 +0,0 @@ -import useLocalStorageState from 'use-local-storage-state'; -import { TLRecord, createTLStore, SerializedStore } from 'tldraw'; -import { customSchema } from '../../worker/TldrawDurableObject'; - -export function useLocalStorageRoom(roomId: string) { - const [records, setRecords] = useLocalStorageState>(`tldraw-room-${roomId}`, { - defaultValue: createTLStore({ schema: customSchema }).serialize() - }); - - const store = createTLStore({ - schema: customSchema, - initialData: records, - }); - - return { - store, - records, - setRecords - }; -} \ No newline at end of file diff --git a/src/hooks/usePersistentBoard.ts b/src/hooks/usePersistentBoard.ts deleted file mode 100644 index 128bf51..0000000 --- a/src/hooks/usePersistentBoard.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { useSync } from '@tldraw/sync' -import { useState, useEffect, useCallback, useRef } from 'react' -import { customSchema } from '../../worker/TldrawDurableObject' -import { multiplayerAssetStore } from '../client/multiplayerAssetStore' -import { useGSetState } from './useGSetState' -import { useLocalStorageRoom } from './useLocalStorageRoom' -import { TLRecord } from 'tldraw' -import { WORKER_URL } from '../components/Board' - -export function usePersistentBoard(roomId: string) { - const [isOnline, setIsOnline] = useState(navigator.onLine) - const { store: localStore, records, setRecords } = useLocalStorageRoom(roomId) - const { values, add, merge } = useGSetState(roomId) - const initialSyncRef = useRef(false) - const mergeInProgressRef = useRef(false) - - const syncedStore = useSync({ - uri: `${WORKER_URL.replace('https://', 'wss://')}/connect/${roomId}`, - schema: customSchema, - assets: multiplayerAssetStore, - }) - - useEffect(() => { - const handleOnline = () => setIsOnline(true) - const handleOffline = () => setIsOnline(false) - - window.addEventListener('online', handleOnline) - window.addEventListener('offline', handleOffline) - - return () => { - window.removeEventListener('online', handleOnline) - window.removeEventListener('offline', handleOffline) - } - }, []) - - const mergeRecords = useCallback((records: Set) => { - if (mergeInProgressRef.current || records.size === 0) return - - try { - mergeInProgressRef.current = true - merge(records) - if (!isOnline && localStore) { - setRecords(localStore.serialize()) - } - } finally { - mergeInProgressRef.current = false - } - }, [isOnline, localStore, merge, setRecords]) - - useEffect(() => { - if (!syncedStore?.store || !localStore) return - - if (isOnline && !initialSyncRef.current) { - initialSyncRef.current = true - const serverRecords = Object.values(syncedStore.store.allRecords()) - if (serverRecords.length > 0) { - mergeRecords(new Set(serverRecords)) - } - - const unsubscribe = syncedStore.store.listen((event) => { - if ('changes' in event) { - const changedRecords = Object.values(event.changes) - if (changedRecords.length > 0) { - mergeRecords(new Set(changedRecords)) - } - } - }) - - return () => unsubscribe() - } else if (!isOnline) { - const currentRecords = Object.values(localStore.allRecords()) - if (currentRecords.length > 0) { - mergeRecords(new Set(currentRecords)) - } - } - }, [isOnline, syncedStore?.store, localStore, mergeRecords]) - - const addRecord = useCallback((record: TLRecord) => { - if (!record) return - add(record) - if (!isOnline && localStore) { - setRecords(localStore.serialize()) - } - }, [add, isOnline, localStore, setRecords]) - - return { - store: isOnline ? syncedStore?.store : localStore, - isOnline, - addRecord, - mergeRecords - } -} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..f90643f 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,9 @@ /// + +interface ImportMetaEnv { + readonly VITE_TLDRAW_WORKER_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/vite.config.ts b/vite.config.ts index 3927bd7..48b47b5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,5 @@ import { markdownPlugin } from './build/markdownPlugin'; -import { defineConfig } from 'vite' +import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' import wasm from "vite-plugin-wasm"; import topLevelAwait from "vite-plugin-top-level-await"; @@ -35,4 +35,7 @@ export default defineConfig({ '@': '/src', }, }, -}) + define: { + 'import.meta.env.VITE_WORKER_URL': JSON.stringify(process.env.VITE_WORKER_URL) + } +}); diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts index 5e4a496..66c725a 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -5,7 +5,7 @@ import { TLRecord, TLShape, createTLSchema, - // defaultBindingSchemas, + defaultBindingSchemas, defaultShapeSchemas, } from '@tldraw/tlschema' import { AutoRouter, IRequest, error } from 'itty-router' @@ -20,11 +20,20 @@ import GSet from 'crdts/src/G-Set' export const customSchema = createTLSchema({ shapes: { ...defaultShapeSchemas, - ChatBox: ChatBoxShape, - VideoChat: VideoChatShape, - Embed: EmbedShape + ChatBox: { + props: ChatBoxShape.props, + migrations: ChatBoxShape.migrations, + }, + VideoChat: { + props: VideoChatShape.props, + migrations: VideoChatShape.migrations, + }, + Embed: { + props: EmbedShape.props, + migrations: EmbedShape.migrations, + }, }, - // bindings: { ...defaultBindingSchemas }, + bindings: defaultBindingSchemas, }) // each whiteboard room is hosted in a DurableObject: @@ -67,41 +76,105 @@ export class TldrawDurableObject { } return this.handleConnect(request) }) - .get('/room/:roomId', async () => { + .get('/room/:roomId', async (request) => { const room = await this.getRoom() const snapshot = room.getCurrentSnapshot() - return new Response(JSON.stringify(snapshot.documents)) + return new Response(JSON.stringify(snapshot.documents), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': request.headers.get('Origin') || '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Max-Age': '86400', + } + }) }) .post('/room/:roomId', async (request) => { const records = await request.json() as TLRecord[] const mergedRecords = await this.mergeCrdtState(records) - return new Response(JSON.stringify(Array.from(mergedRecords))) + + return new Response(JSON.stringify(Array.from(mergedRecords)), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': request.headers.get('Origin') || '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Max-Age': '86400', + } + }) }) // `fetch` is the entry point for all requests to the Durable Object fetch(request: Request): Response | Promise { - return this.router.fetch(request) + try { + return this.router.fetch(request) + } catch (err) { + console.error('Error in DO fetch:', err); + return new Response(JSON.stringify({ + error: 'Internal Server Error', + message: (err as Error).message + }), { + status: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, UPGRADE', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, Upgrade, Connection', + 'Access-Control-Max-Age': '86400', + 'Access-Control-Allow-Credentials': 'true' + } + }); + } } // what happens when someone tries to connect to this room? async handleConnect(request: IRequest): Promise { - // extract query params from request - const sessionId = request.query.sessionId as string - if (!sessionId) return error(400, 'Missing sessionId') + if (!this.roomId) { + return new Response('Room not initialized', { status: 400 }); + } - // Create the websocket pair for the client - const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair() - // @ts-ignore - serverWebSocket.accept() + const sessionId = request.query.sessionId as string; + if (!sessionId) { + return new Response('Missing sessionId', { status: 400 }); + } - // load the room, or retrieve it if it's already loaded - const room = await this.getRoom() + const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair(); - // connect the client to the room - room.handleSocketConnect({ sessionId, socket: serverWebSocket }) + try { + serverWebSocket.accept(); + const room = await this.getRoom(); - // return the websocket connection to the client - return new Response(null, { status: 101, webSocket: clientWebSocket }) + // Handle socket connection with proper error boundaries + room.handleSocketConnect({ + sessionId, + socket: { + send: serverWebSocket.send.bind(serverWebSocket), + close: serverWebSocket.close.bind(serverWebSocket), + addEventListener: serverWebSocket.addEventListener.bind(serverWebSocket), + removeEventListener: serverWebSocket.removeEventListener.bind(serverWebSocket), + readyState: serverWebSocket.readyState, + } + }); + + return new Response(null, { + status: 101, + webSocket: clientWebSocket, + headers: { + 'Access-Control-Allow-Origin': request.headers.get('Origin') || '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, UPGRADE', + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Allow-Credentials': 'true', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade' + } + }); + } catch (error) { + console.error('WebSocket connection error:', error); + serverWebSocket.close(1011, 'Failed to initialize connection'); + return new Response('Failed to establish WebSocket connection', { + status: 500 + }); + } } getRoom() { diff --git a/worker/worker.ts b/worker/worker.ts index 50c4da9..6c0d900 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -21,31 +21,27 @@ const securityHeaders = { // we're hosting the worker separately to the client. you should restrict this to your own domain. const { preflight, corsify } = cors({ origin: (origin) => { - if (!origin) return undefined + const allowedOrigins = [ + 'https://jeffemmett.com', + 'https://www.jeffemmett.com', + 'https://jeffemmett-canvas.jeffemmett.workers.dev', + 'https://jeffemmett.com/board/*', + ]; - const allowedPatterns = [ - // Localhost with any port - /^http:\/\/localhost:\d+$/, - // 127.0.0.1 with any port - /^http:\/\/127\.0\.0\.1:\d+$/, - // 192.168.*.* with any port - /^http:\/\/192\.168\.\d+\.\d+:\d+$/, - // 169.254.*.* with any port - /^http:\/\/169\.254\.\d+\.\d+:\d+$/, - // 10.*.*.* with any port - /^http:\/\/10\.\d+\.\d+\.\d+:\d+$/, - // Production domain - /^https:\/\/jeffemmett\.com$/ - ] + // Always allow if no origin (like from a local file) + if (!origin) return '*'; - // Check if origin matches any of our patterns - const isAllowed = allowedPatterns.some(pattern => - pattern instanceof RegExp - ? pattern.test(origin) - : pattern === origin - ) + // Check exact matches + if (allowedOrigins.includes(origin)) { + return origin; + } - return isAllowed ? origin : undefined + // For development - check if it's a localhost or local IP + if (origin.match(/^http:\/\/(localhost|127\.0\.0\.192\.168\.|169\.254\.|10\.)/)) { + return origin; + } + + return undefined; }, allowMethods: ['GET', 'POST', 'OPTIONS', 'UPGRADE'], allowHeaders: [ @@ -56,10 +52,10 @@ const { preflight, corsify } = cors({ 'Sec-WebSocket-Key', 'Sec-WebSocket-Version', 'Sec-WebSocket-Extensions', - 'Sec-WebSocket-Protocol', - ...Object.keys(securityHeaders) + 'Sec-WebSocket-Protocol' ], maxAge: 86400, + credentials: true }) const router = AutoRouter({ before: [preflight], @@ -81,7 +77,11 @@ const router = AutoRouter({ .get('/connect/:roomId', (request, env) => { const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId) const room = env.TLDRAW_DURABLE_OBJECT.get(id) - return room.fetch(request.url, { headers: request.headers, body: request.body }) + return room.fetch(request.url, { + headers: request.headers, + body: request.body, + method: request.method + }) }) // assets can be uploaded to the bucket under /uploads: @@ -93,11 +93,14 @@ const router = AutoRouter({ // bookmarks need to extract metadata from pasted URLs: .get('/unfurl', handleUnfurlRequest) - .get('/room/:roomId', async (request, env) => { + .get('/room/:roomId', (request, env) => { const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId) const room = env.TLDRAW_DURABLE_OBJECT.get(id) - const response = await room.fetch(request.url) - return response + return room.fetch(request.url, { + headers: request.headers, + body: request.body, + method: request.method + }) }) .post('/room/:roomId', async (request, env) => {