-
-
-
+
+
);
}
+
+ // ... rest of your utility methods ...
}
diff --git a/src/test/EmbedTest.tsx b/src/test/EmbedTest.tsx
new file mode 100644
index 0000000..f49833f
--- /dev/null
+++ b/src/test/EmbedTest.tsx
@@ -0,0 +1,51 @@
+import { useState } from 'react';
+
+export const EmbedTest = () => {
+ const [docUrl, setDocUrl] = useState('');
+ const [embedUrl, setEmbedUrl] = useState('');
+ const [error, setError] = useState('');
+
+ const handleEmbed = () => {
+ const docId = docUrl.match(/\/d\/([a-zA-Z0-9-_]+)/)?.[1];
+ if (!docId) {
+ setError('Invalid Google Docs URL');
+ return;
+ }
+
+ const newEmbedUrl = `https://docs.google.com/document/d/${docId}/preview`;
+ setEmbedUrl(newEmbedUrl);
+ setError('');
+ };
+
+ return (
+
+
Embed Test
+
+
+ setDocUrl(e.target.value)}
+ placeholder="Paste Google Doc URL"
+ style={{ width: '100%', padding: '8px' }}
+ />
+
+
+
+ {error &&
{error}
}
+
+ {embedUrl && (
+
+
Embedded Document:
+
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/src/test/EnvCheck.tsx b/src/test/EnvCheck.tsx
new file mode 100644
index 0000000..8749b9a
--- /dev/null
+++ b/src/test/EnvCheck.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+
+export default function EnvCheck() {
+ console.log('Client ID:', process.env.VITE_GOOGLE_CLIENT_ID);
+ console.log('API Key:', process.env.VITE_GOOGLE_API_KEY);
+ console.log(process.env)
+ return
Check console for environment variables
;
+}
\ No newline at end of file
diff --git a/src/types/google.accounts.d.ts b/src/types/google.accounts.d.ts
new file mode 100644
index 0000000..3392b6f
--- /dev/null
+++ b/src/types/google.accounts.d.ts
@@ -0,0 +1,52 @@
+declare namespace google {
+ namespace accounts {
+ namespace oauth2 {
+ interface TokenClient {
+ callback: (response: { access_token?: string }) => void;
+ client_id: string;
+ scope: string;
+ }
+
+ function initTokenClient(config: {
+ client_id: string;
+ scope: string;
+ callback: (response: { access_token?: string }) => void;
+ error_callback?: (error: any) => void;
+ }): {
+ requestAccessToken(options?: { prompt?: string }): void;
+ };
+ }
+ }
+
+ namespace picker {
+ class PickerBuilder {
+ addView(view: any): PickerBuilder;
+ setOAuthToken(token: string): PickerBuilder;
+ setDeveloperKey(key: string): PickerBuilder;
+ setCallback(callback: (data: PickerResponse) => void): PickerBuilder;
+ build(): Picker;
+ }
+
+ interface PickerResponse {
+ action: string;
+ docs: Array<{
+ id: string;
+ name: string;
+ url: string;
+ mimeType: string;
+ }>;
+ }
+
+ class Picker {
+ setVisible(visible: boolean): void;
+ }
+
+ const ViewId: {
+ DOCS: string;
+ DOCUMENTS: string;
+ PRESENTATIONS: string;
+ SPREADSHEETS: string;
+ FOLDERS: string;
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/utils/websocket-manager.ts b/src/utils/websocket-manager.ts
new file mode 100644
index 0000000..40c5ac4
--- /dev/null
+++ b/src/utils/websocket-manager.ts
@@ -0,0 +1,53 @@
+import { io, Socket } from 'socket.io-client';
+
+export const wsConfig = {
+ reconnection: true,
+ reconnectionAttempts: 5,
+ reconnectionDelay: 1000,
+ timeout: 10000
+};
+
+class WebSocketManager {
+ private socket: Socket | null = null;
+ private retryCount = 0;
+
+ connect(url: string) {
+ this.socket = io(url, wsConfig);
+
+ this.socket.on('connect', () => {
+ console.log('WebSocket connected');
+ this.retryCount = 0;
+ });
+
+ this.socket.on('error', (error) => {
+ console.error('WebSocket error:', error);
+ this.handleRetry();
+ });
+
+ this.socket.on('disconnect', () => {
+ console.log('WebSocket disconnected');
+ this.handleRetry();
+ });
+ }
+
+ private handleRetry() {
+ if (this.retryCount < wsConfig.reconnectionAttempts) {
+ this.retryCount++;
+ setTimeout(() => {
+ console.log(`Attempting reconnection ${this.retryCount}/${wsConfig.reconnectionAttempts}`);
+ this.socket?.connect();
+ }, wsConfig.reconnectionDelay);
+ }
+ }
+
+ // Add methods to send/receive messages
+ send(event: string, data: any) {
+ this.socket?.emit(event, data);
+ }
+
+ subscribe(event: string, callback: (data: any) => void) {
+ this.socket?.on(event, callback);
+ }
+}
+
+export const webSocketManager = new WebSocketManager();
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 2be2aaa..be04702 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,16 +1,23 @@
{
"compilerOptions": {
- "baseUrl": "./src",
+ "baseUrl": ".",
"paths": {
- "@/*": ["*"],
- "src/*": ["./src/*"],
+ "@/*": [
+ "./src/*"
+ ],
+ "src/*": [
+ "./src/*"
+ ]
},
"target": "ES2020",
"useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "lib": [
+ "ES2020",
+ "DOM",
+ "DOM.Iterable"
+ ],
"module": "ESNext",
"skipLibCheck": true,
-
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
@@ -18,14 +25,20 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
-
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
- "include": ["src", "worker", "src/client"],
-
- "references": [{ "path": "./tsconfig.node.json" }]
-}
+ "include": [
+ "src",
+ "worker",
+ "src/client"
+ ],
+ "references": [
+ {
+ "path": "./tsconfig.node.json"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
index c10cd5c..4477aa9 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,37 +1,49 @@
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";
import { viteStaticCopy } from 'vite-plugin-static-copy';
+import path from 'path'
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, process.cwd(), '');
-export default defineConfig({
- define: {
- 'process.env.TLDRAW_WORKER_URL': JSON.stringify('https://jeffemmett-canvas.jeffemmett.workers.dev')
- },
- plugins: [
- react(),
- wasm(),
- topLevelAwait(),
- markdownPlugin,
- viteStaticCopy({
- targets: [
- {
- src: 'src/posts/',
- dest: '.'
- }
- ]
- })
- ],
- build: {
- sourcemap: true,
- },
- base: '/',
- publicDir: 'src/public',
- resolve: {
- alias: {
- '@': '/src',
+ return {
+ define: {
+ 'process.env.TLDRAW_WORKER_URL': JSON.stringify('https://jeffemmett-canvas.jeffemmett.workers.dev'),
+ 'process.env.VITE_GOOGLE_CLIENT_ID': JSON.stringify(env.VITE_GOOGLE_CLIENT_ID),
+ 'process.env.VITE_GOOGLE_API_KEY': JSON.stringify(env.VITE_GOOGLE_API_KEY)
},
- },
-})
+ plugins: [
+ react(),
+ wasm(),
+ topLevelAwait(),
+ markdownPlugin,
+ viteStaticCopy({
+ targets: [
+ {
+ src: 'src/posts/',
+ dest: '.'
+ }
+ ]
+ })
+ ],
+ build: {
+ sourcemap: true,
+ },
+ base: '/',
+ publicDir: 'src/public',
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src')
+ },
+ },
+ server: {
+ headers: {
+ 'Cross-Origin-Opener-Policy': 'same-origin-allow-popups',
+ 'Cross-Origin-Embedder-Policy': 'require-corp'
+ }
+ },
+ }
+});
diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts
index 6d99202..2a03d17 100644
--- a/worker/TldrawDurableObject.ts
+++ b/worker/TldrawDurableObject.ts
@@ -11,9 +11,9 @@ import {
import { AutoRouter, IRequest, error } from 'itty-router'
import throttle from 'lodash.throttle'
import { Environment } from './types'
-import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil'
-import { VideoChatShape } from '@/shapes/VideoChatShapeUtil'
-import { EmbedShape } from '@/shapes/EmbedShapeUtil'
+import { ChatBoxShape } from '../src/shapes/ChatBoxShapeUtil'
+import { VideoChatShape } from '../src/shapes/VideoChatShapeUtil'
+import { EmbedShape } from '../src/shapes/EmbedShapeUtil'
// add custom shapes and bindings here if needed:
export const customSchema = createTLSchema({
@@ -74,23 +74,25 @@ export class TldrawDurableObject {
// 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')
- // Create the websocket pair for the client
- const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair()
- // @ts-ignore
- serverWebSocket.accept()
+ const webSocketPair = new WebSocketPair()
+ const [client, server] = Object.values(webSocketPair)
+
+ server.accept()
- // load the room, or retrieve it if it's already loaded
const room = await this.getRoom()
+ room.handleSocketConnect({ sessionId, socket: server })
- // connect the client to the room
- room.handleSocketConnect({ sessionId, socket: serverWebSocket })
-
- // return the websocket connection to the client
- return new Response(null, { status: 101, webSocket: clientWebSocket })
+ return new Response(null, {
+ status: 101,
+ webSocket: client,
+ headers: {
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade'
+ }
+ })
}
getRoom() {
diff --git a/worker/worker.ts b/worker/worker.ts
index b288744..7dd4241 100644
--- a/worker/worker.ts
+++ b/worker/worker.ts
@@ -8,20 +8,53 @@ export { TldrawDurableObject } from './TldrawDurableObject'
// we use itty-router (https://itty.dev/) to handle routing. in this example we turn on CORS because
// we're hosting the worker separately to the client. you should restrict this to your own domain.
-const { preflight, corsify } = cors({ origin: '*' })
+const { preflight, corsify } = cors({
+ origin: '*',
+ allowMethods: 'GET, POST, PUT, DELETE, OPTIONS',
+ allowHeaders: {
+ 'Access-Control-Allow-Headers': '*',
+ 'Cross-Origin-Opener-Policy': 'same-origin',
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': '*',
+ 'Sec-WebSocket-Version': '*',
+ 'Sec-WebSocket-Protocol': '*'
+ }
+})
+
+const addSecurityHeaders = (response: Response): Response => {
+ const headers = new Headers(response.headers)
+ headers.set('Cross-Origin-Opener-Policy', 'same-origin')
+ headers.set('Cross-Origin-Embedder-Policy', 'require-corp')
+ return new Response(response.body, {
+ status: response.status,
+ headers
+ })
+}
+
const router = AutoRouter({
before: [preflight],
- finally: [corsify],
+ finally: [(response) => corsify(addSecurityHeaders(response))],
catch: (e) => {
console.error(e)
return error(e)
},
})
// requests to /connect are routed to the Durable Object, and handle realtime websocket syncing
- .get('/connect/:roomId', (request, env) => {
+ .get('/connect/:roomId', async (request, env) => {
+ const upgradeHeader = request.headers.get('Upgrade')
+ if (!upgradeHeader || upgradeHeader.toLowerCase() !== 'websocket') {
+ return new Response('Expected Upgrade: websocket', { status: 426 })
+ }
+
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:
diff --git a/wrangler.toml b/wrangler.toml
index 0d5d04e..8302d5e 100644
--- a/wrangler.toml
+++ b/wrangler.toml
@@ -31,4 +31,8 @@ logpush = true
# wrangler.toml (wrangler v3.79.0^)
[observability]
enabled = true
-head_sampling_rate = 1
\ No newline at end of file
+head_sampling_rate = 1
+
+[build]
+command = "yarn build"
+watch_dir = "src"
\ No newline at end of file