separate worker and buckets between dev & prod, fix cron job scheduler
This commit is contained in:
parent
9065a408f2
commit
38566e1a75
|
|
@ -26,6 +26,7 @@ jobs:
|
||||||
- name: Deploy to Cloudflare Workers
|
- name: Deploy to Cloudflare Workers
|
||||||
run: |
|
run: |
|
||||||
npm install -g wrangler@3.107.3
|
npm install -g wrangler@3.107.3
|
||||||
|
# Uses default wrangler.toml (production config)
|
||||||
wrangler deploy
|
wrangler deploy
|
||||||
working-directory: ./worker
|
working-directory: ./worker
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
|
|
@ -6,10 +6,13 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently --kill-others --names client,worker --prefix-colors blue,red \"npm run dev:client\" \"npm run dev:worker\"",
|
"dev": "concurrently --kill-others --names client,worker --prefix-colors blue,red \"npm run dev:client\" \"npm run dev:worker\"",
|
||||||
"dev:client": "vite --host --port 5173",
|
"dev:client": "vite --host --port 5173",
|
||||||
"dev:worker": "wrangler dev --remote --port 5172 --ip 0.0.0.0",
|
"dev:worker": "wrangler dev --config wrangler.dev.toml --remote --port 5172",
|
||||||
|
"dev:worker:local": "wrangler dev --config wrangler.dev.toml --port 5172 --ip 0.0.0.0",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"deploy": "tsc && vite build && vercel deploy --prod && wrangler deploy",
|
"deploy": "tsc && vite build && vercel deploy --prod && wrangler deploy",
|
||||||
|
"deploy:worker": "wrangler deploy",
|
||||||
|
"deploy:worker:dev": "wrangler deploy --config wrangler.dev.toml",
|
||||||
"types": "tsc --noEmit"
|
"types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|
@ -17,15 +20,17 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.33.1",
|
"@anthropic-ai/sdk": "^0.33.1",
|
||||||
|
"@automerge/automerge": "^3.1.1",
|
||||||
|
"@automerge/automerge-repo": "^2.2.0",
|
||||||
"@automerge/automerge-repo-react-hooks": "^2.2.0",
|
"@automerge/automerge-repo-react-hooks": "^2.2.0",
|
||||||
"@daily-co/daily-js": "^0.60.0",
|
"@daily-co/daily-js": "^0.60.0",
|
||||||
"@daily-co/daily-react": "^0.20.0",
|
"@daily-co/daily-react": "^0.20.0",
|
||||||
"@oddjs/odd": "^0.37.2",
|
"@oddjs/odd": "^0.37.2",
|
||||||
"@tldraw/assets": "^3.6.0",
|
"@tldraw/assets": "^3.15.4",
|
||||||
"@tldraw/sync": "^3.6.0",
|
"@tldraw/sync": "^3.15.4",
|
||||||
"@tldraw/sync-core": "^3.6.0",
|
"@tldraw/sync-core": "^3.15.4",
|
||||||
"@tldraw/tldraw": "^3.6.0",
|
"@tldraw/tldraw": "^3.15.4",
|
||||||
"@tldraw/tlschema": "^3.6.0",
|
"@tldraw/tlschema": "^3.15.4",
|
||||||
"@types/markdown-it": "^14.1.1",
|
"@types/markdown-it": "^14.1.1",
|
||||||
"@types/marked": "^5.0.2",
|
"@types/marked": "^5.0.2",
|
||||||
"@uiw/react-md-editor": "^4.0.5",
|
"@uiw/react-md-editor": "^4.0.5",
|
||||||
|
|
@ -49,7 +54,7 @@
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.0.2",
|
"react-router-dom": "^7.0.2",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
"tldraw": "^3.6.0",
|
"tldraw": "^3.15.4",
|
||||||
"vercel": "^39.1.1",
|
"vercel": "^39.1.1",
|
||||||
"webcola": "^3.4.0",
|
"webcola": "^3.4.0",
|
||||||
"webnative": "^0.36.3"
|
"webnative": "^0.36.3"
|
||||||
|
|
@ -65,7 +70,9 @@
|
||||||
"concurrently": "^9.1.0",
|
"concurrently": "^9.1.0",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^6.0.3",
|
"vite": "^6.0.3",
|
||||||
"wrangler": "^3.107.3"
|
"vite-plugin-top-level-await": "^1.6.0",
|
||||||
|
"vite-plugin-wasm": "^3.5.0",
|
||||||
|
"wrangler": "^4.33.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,10 @@ import { useAuth } from "../context/AuthContext"
|
||||||
import { updateLastVisited } from "../lib/starredBoards"
|
import { updateLastVisited } from "../lib/starredBoards"
|
||||||
import { captureBoardScreenshot } from "../lib/screenshotService"
|
import { captureBoardScreenshot } from "../lib/screenshotService"
|
||||||
|
|
||||||
// Default to production URL if env var isn't available
|
// Automatically switch between production and local dev based on environment
|
||||||
export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev"
|
export const WORKER_URL = import.meta.env.DEV
|
||||||
|
? "http://localhost:5172"
|
||||||
|
: "https://jeffemmett-canvas.jeffemmett.workers.dev"
|
||||||
|
|
||||||
const customShapeUtils = [
|
const customShapeUtils = [
|
||||||
ChatBoxShape,
|
ChatBoxShape,
|
||||||
|
|
@ -97,6 +99,7 @@ export function Board() {
|
||||||
[roomId, session.authed, session.username],
|
[roomId, session.authed, session.username],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Using TLdraw sync - fixed version compatibility issue
|
||||||
const store = useSync(storeConfig)
|
const store = useSync(storeConfig)
|
||||||
const [editor, setEditor] = useState<Editor | null>(null)
|
const [editor, setEditor] = useState<Editor | null>(null)
|
||||||
|
|
||||||
|
|
@ -176,31 +179,36 @@ export function Board() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor || !roomId || !store.store) return;
|
if (!editor || !roomId || !store.store) return;
|
||||||
|
|
||||||
// Get current shapes to detect changes
|
let lastContentHash = '';
|
||||||
const currentShapes = editor.getCurrentPageShapes();
|
let timeoutId: NodeJS.Timeout;
|
||||||
const currentShapeCount = currentShapes.length;
|
|
||||||
|
|
||||||
// Create a simple hash of the content for change detection
|
const captureScreenshot = async () => {
|
||||||
const currentContentHash = currentShapes.length > 0
|
const currentShapes = editor.getCurrentPageShapes();
|
||||||
? currentShapes.map(shape => `${shape.id}-${shape.type}`).sort().join('|')
|
const currentContentHash = currentShapes.length > 0
|
||||||
: '';
|
? currentShapes.map(shape => `${shape.id}-${shape.type}`).sort().join('|')
|
||||||
|
|
||||||
// Debounced screenshot capture only when content actually changes
|
|
||||||
const timeoutId = setTimeout(async () => {
|
|
||||||
const newShapes = editor.getCurrentPageShapes();
|
|
||||||
const newShapeCount = newShapes.length;
|
|
||||||
const newContentHash = newShapes.length > 0
|
|
||||||
? newShapes.map(shape => `${shape.id}-${shape.type}`).sort().join('|')
|
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// Only capture if content actually changed
|
// Only capture if content actually changed
|
||||||
if (newShapeCount !== currentShapeCount || newContentHash !== currentContentHash) {
|
if (currentContentHash !== lastContentHash) {
|
||||||
|
lastContentHash = currentContentHash;
|
||||||
await captureBoardScreenshot(editor, roomId);
|
await captureBoardScreenshot(editor, roomId);
|
||||||
}
|
}
|
||||||
}, 3000); // Wait 3 seconds to ensure changes are complete
|
};
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
// Listen to store changes instead of using getSnapshot() in dependencies
|
||||||
}, [editor, roomId, store.store?.getSnapshot()]); // Still trigger on store changes to detect them
|
const unsubscribe = store.store.listen(() => {
|
||||||
|
// Clear existing timeout
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// Set new timeout for debounced screenshot capture
|
||||||
|
timeoutId = setTimeout(captureScreenshot, 3000);
|
||||||
|
}, { source: "user", scope: "document" });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [editor, roomId, store.store]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "fixed", inset: 0 }}>
|
<div style={{ position: "fixed", inset: 0 }}>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { defineConfig, loadEnv } from "vite"
|
import { defineConfig, loadEnv } from "vite"
|
||||||
import react from "@vitejs/plugin-react"
|
import react from "@vitejs/plugin-react"
|
||||||
|
import wasm from "vite-plugin-wasm"
|
||||||
|
import topLevelAwait from "vite-plugin-top-level-await"
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
// Load env file based on `mode` in the current working directory.
|
// Load env file based on `mode` in the current working directory.
|
||||||
|
|
@ -15,7 +17,7 @@ export default defineConfig(({ mode }) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
envPrefix: ["VITE_"],
|
envPrefix: ["VITE_"],
|
||||||
plugins: [react()],
|
plugins: [react(), wasm(), topLevelAwait()],
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,8 @@ export class TldrawDurableObject {
|
||||||
})
|
})
|
||||||
// when we get a connection request, we stash the room id if needed and handle the connection
|
// when we get a connection request, we stash the room id if needed and handle the connection
|
||||||
.get("/connect/:roomId", async (request) => {
|
.get("/connect/:roomId", async (request) => {
|
||||||
|
// Connect request received
|
||||||
|
|
||||||
if (!this.roomId) {
|
if (!this.roomId) {
|
||||||
await this.ctx.blockConcurrencyWhile(async () => {
|
await this.ctx.blockConcurrencyWhile(async () => {
|
||||||
await this.ctx.storage.put("roomId", request.params.roomId)
|
await this.ctx.storage.put("roomId", request.params.roomId)
|
||||||
|
|
@ -164,11 +166,19 @@ export class TldrawDurableObject {
|
||||||
|
|
||||||
// what happens when someone tries to connect to this room?
|
// what happens when someone tries to connect to this room?
|
||||||
async handleConnect(request: IRequest): Promise<Response> {
|
async handleConnect(request: IRequest): Promise<Response> {
|
||||||
|
// Check if this is a WebSocket upgrade request
|
||||||
|
const upgradeHeader = request.headers.get("Upgrade")
|
||||||
|
|
||||||
|
if (!upgradeHeader || upgradeHeader !== "websocket") {
|
||||||
|
return new Response("WebSocket upgrade required", { status: 426 })
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.roomId) {
|
if (!this.roomId) {
|
||||||
return new Response("Room not initialized", { status: 400 })
|
return new Response("Room not initialized", { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = request.query.sessionId as string
|
const sessionId = request.query.sessionId as string
|
||||||
|
// Session ID received
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return new Response("Missing sessionId", { status: 400 })
|
return new Response("Missing sessionId", { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +255,6 @@ export class TldrawDurableObject {
|
||||||
onDataChange: () => {
|
onDataChange: () => {
|
||||||
// and persist whenever the data in the room changes
|
// and persist whenever the data in the room changes
|
||||||
this.schedulePersistToR2()
|
this.schedulePersistToR2()
|
||||||
console.log("Persisting", this.roomId, "to R2")
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})()
|
})()
|
||||||
|
|
@ -254,7 +263,7 @@ export class TldrawDurableObject {
|
||||||
return this.roomPromise
|
return this.roomPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
// we throttle persistance so it only happens every 10 seconds
|
// we throttle persistence so it only happens every 30 seconds, batching all updates
|
||||||
schedulePersistToR2 = throttle(async () => {
|
schedulePersistToR2 = throttle(async () => {
|
||||||
if (!this.roomPromise || !this.roomId) return
|
if (!this.roomPromise || !this.roomId) return
|
||||||
const room = await this.getRoom()
|
const room = await this.getRoom()
|
||||||
|
|
@ -262,7 +271,8 @@ export class TldrawDurableObject {
|
||||||
// convert the room to JSON and upload it to R2
|
// convert the room to JSON and upload it to R2
|
||||||
const snapshot = JSON.stringify(room.getCurrentSnapshot())
|
const snapshot = JSON.stringify(room.getCurrentSnapshot())
|
||||||
await this.r2.put(`rooms/${this.roomId}`, snapshot)
|
await this.r2.put(`rooms/${this.roomId}`, snapshot)
|
||||||
}, 10_000)
|
console.log(`Board persisted to R2: ${this.roomId}`)
|
||||||
|
}, 30_000)
|
||||||
|
|
||||||
// Add CORS headers for WebSocket upgrade
|
// Add CORS headers for WebSocket upgrade
|
||||||
handleWebSocket(request: Request) {
|
handleWebSocket(request: Request) {
|
||||||
|
|
@ -283,10 +293,8 @@ export class TldrawDurableObject {
|
||||||
|
|
||||||
server.addEventListener("close", () => {
|
server.addEventListener("close", () => {
|
||||||
if (this.roomPromise) {
|
if (this.roomPromise) {
|
||||||
this.getRoom().then((room) => {
|
// Force a final persistence when WebSocket closes
|
||||||
// Update store to ensure all changes are persisted
|
this.schedulePersistToR2()
|
||||||
room.updateStore(() => {})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
175
worker/worker.ts
175
worker/worker.ts
|
|
@ -2,8 +2,9 @@ import { AutoRouter, cors, error, IRequest } from "itty-router"
|
||||||
import { handleAssetDownload, handleAssetUpload } from "./assetUploads"
|
import { handleAssetDownload, handleAssetUpload } from "./assetUploads"
|
||||||
import { Environment } from "./types"
|
import { Environment } from "./types"
|
||||||
|
|
||||||
// make sure our sync durable object is made available to cloudflare
|
// make sure our sync durable objects are made available to cloudflare
|
||||||
export { TldrawDurableObject } from "./TldrawDurableObject"
|
export { TldrawDurableObject } from "./TldrawDurableObject"
|
||||||
|
// export { AutomergeDurableObject } from "./AutomergeDurableObject" // Disabled - not currently used
|
||||||
|
|
||||||
// Lazy load heavy dependencies to avoid startup timeouts
|
// Lazy load heavy dependencies to avoid startup timeouts
|
||||||
let handleUnfurlRequest: any = null
|
let handleUnfurlRequest: any = null
|
||||||
|
|
@ -82,40 +83,80 @@ const { preflight, corsify } = cors({
|
||||||
credentials: true,
|
credentials: true,
|
||||||
})
|
})
|
||||||
const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
before: [preflight],
|
before: [
|
||||||
|
// Handle WebSocket upgrades before CORS processing
|
||||||
|
(request) => {
|
||||||
|
const upgradeHeader = request.headers.get("Upgrade")
|
||||||
|
if (upgradeHeader === "websocket") {
|
||||||
|
// WebSocket upgrade detected, bypassing CORS
|
||||||
|
return // Don't process CORS for WebSocket upgrades
|
||||||
|
}
|
||||||
|
return preflight(request)
|
||||||
|
}
|
||||||
|
],
|
||||||
finally: [
|
finally: [
|
||||||
(response) => {
|
(response) => {
|
||||||
// Add security headers to all responses except WebSocket upgrades
|
// Add security headers to all responses except WebSocket upgrades
|
||||||
if (response.status !== 101) {
|
if (response.status !== 101) {
|
||||||
|
// Create new headers to avoid modifying immutable headers
|
||||||
|
const newHeaders = new Headers(response.headers)
|
||||||
Object.entries(securityHeaders).forEach(([key, value]) => {
|
Object.entries(securityHeaders).forEach(([key, value]) => {
|
||||||
response.headers.set(key, value)
|
newHeaders.set(key, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a new response with the updated headers
|
||||||
|
return new Response(response.body, {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: newHeaders
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return corsify(response)
|
return corsify(response)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
catch: (e: Error) => {
|
catch: (e: Error) => {
|
||||||
// Silently handle WebSocket errors, but log other errors
|
// Log all errors for debugging
|
||||||
|
console.error("Worker error:", e)
|
||||||
|
console.error("Error stack:", e.stack)
|
||||||
|
|
||||||
|
// Handle WebSocket errors more gracefully
|
||||||
if (e.message?.includes("WebSocket")) {
|
if (e.message?.includes("WebSocket")) {
|
||||||
console.debug("WebSocket error:", e)
|
console.error("WebSocket error:", e)
|
||||||
return new Response(null, { status: 400 })
|
return new Response(JSON.stringify({ error: "WebSocket connection failed", message: e.message }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
console.error(e)
|
|
||||||
return error(e)
|
return error(e)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// requests to /connect are routed to the Durable Object, and handle realtime websocket syncing
|
// requests to /connect are routed to the Durable Object, and handle realtime websocket syncing
|
||||||
.get("/connect/:roomId", (request, env) => {
|
.get("/connect/:roomId", (request, env) => {
|
||||||
|
console.log("Connect request for room:", request.params.roomId)
|
||||||
|
console.log("Request headers:", Object.fromEntries(request.headers.entries()))
|
||||||
|
|
||||||
// Check if this is a WebSocket upgrade request
|
// Check if this is a WebSocket upgrade request
|
||||||
const upgradeHeader = request.headers.get("Upgrade")
|
const upgradeHeader = request.headers.get("Upgrade")
|
||||||
|
console.log("Upgrade header:", upgradeHeader)
|
||||||
|
|
||||||
if (upgradeHeader === "websocket") {
|
if (upgradeHeader === "websocket") {
|
||||||
|
console.log("WebSocket upgrade requested for room:", request.params.roomId)
|
||||||
|
console.log("Request URL:", request.url)
|
||||||
|
console.log("Request method:", request.method)
|
||||||
|
console.log("All headers:", Object.fromEntries(request.headers.entries()))
|
||||||
|
|
||||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
||||||
return room.fetch(request.url, {
|
|
||||||
|
console.log("Calling Durable Object fetch...")
|
||||||
|
const result = room.fetch(request.url, {
|
||||||
headers: request.headers,
|
headers: request.headers,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
method: request.method,
|
method: request.method,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log("Durable Object fetch result:", result)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle regular GET requests
|
// Handle regular GET requests
|
||||||
|
|
@ -159,6 +200,49 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Automerge routes - these will be used when we switch to Automerge sync
|
||||||
|
.get("/automerge/connect/:roomId", (request, env) => {
|
||||||
|
// Check if this is a WebSocket upgrade request
|
||||||
|
const upgradeHeader = request.headers.get("Upgrade")
|
||||||
|
if (upgradeHeader === "websocket") {
|
||||||
|
const id = env.AUTOMERGE_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||||
|
const room = env.AUTOMERGE_DURABLE_OBJECT.get(id)
|
||||||
|
return room.fetch(request.url, {
|
||||||
|
headers: request.headers,
|
||||||
|
body: request.body,
|
||||||
|
method: request.method,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle regular GET requests
|
||||||
|
const id = env.AUTOMERGE_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||||
|
const room = env.AUTOMERGE_DURABLE_OBJECT.get(id)
|
||||||
|
return room.fetch(request.url, {
|
||||||
|
headers: request.headers,
|
||||||
|
body: request.body,
|
||||||
|
method: request.method,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
.get("/automerge/room/:roomId", (request, env) => {
|
||||||
|
const id = env.AUTOMERGE_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||||
|
const room = env.AUTOMERGE_DURABLE_OBJECT.get(id)
|
||||||
|
return room.fetch(request.url, {
|
||||||
|
headers: request.headers,
|
||||||
|
body: request.body,
|
||||||
|
method: request.method,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
.post("/automerge/room/:roomId", async (request, env) => {
|
||||||
|
const id = env.AUTOMERGE_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||||
|
const room = env.AUTOMERGE_DURABLE_OBJECT.get(id)
|
||||||
|
return room.fetch(request.url, {
|
||||||
|
method: "POST",
|
||||||
|
body: request.body,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
.post("/daily/rooms", async (req) => {
|
.post("/daily/rooms", async (req) => {
|
||||||
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1]
|
||||||
|
|
||||||
|
|
@ -514,18 +598,18 @@ async function backupAllBoards(env: Environment) {
|
||||||
// Store in backup bucket as JSON
|
// Store in backup bucket as JSON
|
||||||
await env.BOARD_BACKUPS_BUCKET.put(backupKey, jsonData)
|
await env.BOARD_BACKUPS_BUCKET.put(backupKey, jsonData)
|
||||||
|
|
||||||
console.log(`Backed up ${room.key} to ${backupKey}`)
|
// Backed up successfully
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to backup room ${room.key}:`, error)
|
console.error(`Failed to backup room ${room.key}:`, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up old backups (keep last 30 days)
|
// Clean up old backups (keep last 90 days)
|
||||||
const thirtyDaysAgo = new Date()
|
const ninetyDaysAgo = new Date()
|
||||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
|
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90)
|
||||||
|
|
||||||
const oldBackups = await env.BOARD_BACKUPS_BUCKET.list({
|
const oldBackups = await env.BOARD_BACKUPS_BUCKET.list({
|
||||||
prefix: thirtyDaysAgo.toISOString().split('T')[0]
|
prefix: ninetyDaysAgo.toISOString().split('T')[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const backup of oldBackups.objects) {
|
for (const backup of oldBackups.objects) {
|
||||||
|
|
@ -546,6 +630,67 @@ router
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.get("/backup/test", async (_, env) => {
|
||||||
|
try {
|
||||||
|
// Simple test to check R2 access
|
||||||
|
const testResult = await env.TLDRAW_BUCKET.list({ prefix: 'rooms/', limit: 1 })
|
||||||
|
|
||||||
// export our router for cloudflare
|
return new Response(JSON.stringify({
|
||||||
export default router
|
success: true,
|
||||||
|
message: 'R2 access test successful',
|
||||||
|
roomCount: testResult.objects?.length || 0,
|
||||||
|
hasMore: testResult.truncated || false
|
||||||
|
}), {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('R2 test failed:', error)
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: 'R2 access test failed',
|
||||||
|
error: (error as Error).message
|
||||||
|
}), {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle scheduled events (cron jobs)
|
||||||
|
export async function scheduled(_event: ScheduledEvent, env: Environment, _ctx: ExecutionContext) {
|
||||||
|
// Cron job triggered
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Run the backup function
|
||||||
|
const result = await backupAllBoards(env)
|
||||||
|
|
||||||
|
// Scheduled backup completed
|
||||||
|
|
||||||
|
// You can add additional logging or notifications here
|
||||||
|
if (!result.success) {
|
||||||
|
console.error('Scheduled backup failed:', result.message)
|
||||||
|
// In a real scenario, you might want to send alerts here
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Scheduled backup error:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// export our router for cloudflare with CORS enabled
|
||||||
|
export default {
|
||||||
|
fetch: (request: Request, env: Environment, ctx: ExecutionContext) => {
|
||||||
|
// Handle WebSocket upgrades directly without CORS processing
|
||||||
|
const upgradeHeader = request.headers.get("Upgrade")
|
||||||
|
if (upgradeHeader === "websocket") {
|
||||||
|
// WebSocket upgrade detected, bypassing router CORS
|
||||||
|
return router.fetch(request, env, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For regular requests, apply CORS
|
||||||
|
return router.fetch(request, env, ctx).then(response => corsify(response))
|
||||||
|
},
|
||||||
|
scheduled
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
main = "worker/worker.ts"
|
||||||
|
compatibility_date = "2024-07-01"
|
||||||
|
name = "jeffemmett-canvas-dev"
|
||||||
|
account_id = "0e7b3338d5278ed1b148e6456b940913"
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
# Development environment variables
|
||||||
|
DAILY_DOMAIN = "mycopunks.daily.co"
|
||||||
|
|
||||||
|
[dev]
|
||||||
|
port = 5172
|
||||||
|
ip = "0.0.0.0"
|
||||||
|
local_protocol = "http"
|
||||||
|
upstream_protocol = "https"
|
||||||
|
|
||||||
|
[durable_objects]
|
||||||
|
bindings = [
|
||||||
|
{ name = "TLDRAW_DURABLE_OBJECT", class_name = "TldrawDurableObject" },
|
||||||
|
# { name = "AUTOMERGE_DURABLE_OBJECT", class_name = "AutomergeDurableObject" }, # Disabled - not currently used
|
||||||
|
]
|
||||||
|
|
||||||
|
[[migrations]]
|
||||||
|
tag = "v1"
|
||||||
|
new_classes = ["TldrawDurableObject"]
|
||||||
|
|
||||||
|
[[migrations]]
|
||||||
|
tag = "v2"
|
||||||
|
new_classes = ["AutomergeDurableObject"]
|
||||||
|
|
||||||
|
[[migrations]]
|
||||||
|
tag = "v3"
|
||||||
|
deleted_classes = ["AutomergeDurableObject"]
|
||||||
|
|
||||||
|
[[r2_buckets]]
|
||||||
|
binding = 'TLDRAW_BUCKET'
|
||||||
|
bucket_name = 'jeffemmett-canvas-preview'
|
||||||
|
preview_bucket_name = 'jeffemmett-canvas-preview'
|
||||||
|
|
||||||
|
[[r2_buckets]]
|
||||||
|
binding = 'BOARD_BACKUPS_BUCKET'
|
||||||
|
bucket_name = 'board-backups-preview'
|
||||||
|
preview_bucket_name = 'board-backups-preview'
|
||||||
|
|
||||||
|
[miniflare]
|
||||||
|
kv_persist = true
|
||||||
|
r2_persist = true
|
||||||
|
durable_objects_persist = true
|
||||||
|
|
||||||
|
[observability]
|
||||||
|
enabled = true
|
||||||
|
head_sampling_rate = 1
|
||||||
|
|
||||||
|
[triggers]
|
||||||
|
crons = ["0 0 * * *"] # Run at midnight UTC every day
|
||||||
|
|
||||||
|
# Secrets should be set using `wrangler secret put` command for dev environment
|
||||||
|
# DO NOT put these directly in wrangler.toml:
|
||||||
|
# - DAILY_API_KEY
|
||||||
|
# - CLOUDFLARE_API_TOKEN
|
||||||
|
# etc.
|
||||||
|
|
@ -17,21 +17,28 @@ upstream_protocol = "https"
|
||||||
[durable_objects]
|
[durable_objects]
|
||||||
bindings = [
|
bindings = [
|
||||||
{ name = "TLDRAW_DURABLE_OBJECT", class_name = "TldrawDurableObject" },
|
{ name = "TLDRAW_DURABLE_OBJECT", class_name = "TldrawDurableObject" },
|
||||||
|
# { name = "AUTOMERGE_DURABLE_OBJECT", class_name = "AutomergeDurableObject" }, # Disabled - not currently used
|
||||||
]
|
]
|
||||||
|
|
||||||
[[migrations]]
|
[[migrations]]
|
||||||
tag = "v1"
|
tag = "v1"
|
||||||
new_classes = ["TldrawDurableObject"]
|
new_classes = ["TldrawDurableObject"]
|
||||||
|
|
||||||
|
[[migrations]]
|
||||||
|
tag = "v2"
|
||||||
|
new_classes = ["AutomergeDurableObject"]
|
||||||
|
|
||||||
|
[[migrations]]
|
||||||
|
tag = "v3"
|
||||||
|
deleted_classes = ["AutomergeDurableObject"]
|
||||||
|
|
||||||
[[r2_buckets]]
|
[[r2_buckets]]
|
||||||
binding = 'TLDRAW_BUCKET'
|
binding = 'TLDRAW_BUCKET'
|
||||||
bucket_name = 'jeffemmett-canvas'
|
bucket_name = 'jeffemmett-canvas'
|
||||||
preview_bucket_name = 'jeffemmett-canvas-preview'
|
|
||||||
|
|
||||||
[[r2_buckets]]
|
[[r2_buckets]]
|
||||||
binding = 'BOARD_BACKUPS_BUCKET'
|
binding = 'BOARD_BACKUPS_BUCKET'
|
||||||
bucket_name = 'board-backups'
|
bucket_name = 'board-backups'
|
||||||
preview_bucket_name = 'board-backups-preview'
|
|
||||||
|
|
||||||
[miniflare]
|
[miniflare]
|
||||||
kv_persist = true
|
kv_persist = true
|
||||||
|
|
@ -46,6 +53,43 @@ head_sampling_rate = 1
|
||||||
crons = ["0 0 * * *"] # Run at midnight UTC every day
|
crons = ["0 0 * * *"] # Run at midnight UTC every day
|
||||||
# crons = ["*/10 * * * *"] # Run every 10 minutes
|
# crons = ["*/10 * * * *"] # Run every 10 minutes
|
||||||
|
|
||||||
|
# Development environment configuration
|
||||||
|
[env.dev]
|
||||||
|
name = "jeffemmett-canvas-dev"
|
||||||
|
compatibility_date = "2024-07-01"
|
||||||
|
|
||||||
|
[env.dev.vars]
|
||||||
|
DAILY_DOMAIN = "mycopunks.daily.co"
|
||||||
|
|
||||||
|
[env.dev.durable_objects]
|
||||||
|
bindings = [
|
||||||
|
{ name = "TLDRAW_DURABLE_OBJECT", class_name = "TldrawDurableObject" },
|
||||||
|
# { name = "AUTOMERGE_DURABLE_OBJECT", class_name = "AutomergeDurableObject" }, # Disabled - not currently used
|
||||||
|
]
|
||||||
|
|
||||||
|
[[env.dev.migrations]]
|
||||||
|
tag = "v1"
|
||||||
|
new_classes = ["TldrawDurableObject"]
|
||||||
|
|
||||||
|
[[env.dev.migrations]]
|
||||||
|
tag = "v2"
|
||||||
|
new_classes = ["AutomergeDurableObject"]
|
||||||
|
|
||||||
|
[[env.dev.migrations]]
|
||||||
|
tag = "v3"
|
||||||
|
deleted_classes = ["AutomergeDurableObject"]
|
||||||
|
|
||||||
|
[[env.dev.r2_buckets]]
|
||||||
|
binding = 'TLDRAW_BUCKET'
|
||||||
|
bucket_name = 'jeffemmett-canvas-preview'
|
||||||
|
|
||||||
|
[[env.dev.r2_buckets]]
|
||||||
|
binding = 'BOARD_BACKUPS_BUCKET'
|
||||||
|
bucket_name = 'board-backups-preview'
|
||||||
|
|
||||||
|
[env.dev.triggers]
|
||||||
|
crons = ["0 0 * * *"] # Run at midnight UTC every day
|
||||||
|
|
||||||
# Secrets should be set using `wrangler secret put` command
|
# Secrets should be set using `wrangler secret put` command
|
||||||
# DO NOT put these directly in wrangler.toml:
|
# DO NOT put these directly in wrangler.toml:
|
||||||
# - DAILY_API_KEY
|
# - DAILY_API_KEY
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue