refactor: remove Daily.co, fix IndexedDB sync for stale cache
Daily.co Cleanup: - Remove @daily-co/daily-js and @daily-co/daily-react packages - Remove DailyProvider wrapper from App.tsx - Remove ~380 lines of Daily API endpoints from worker.ts - Remove DAILY_DOMAIN from wrangler configs - Remove Daily env vars from .env.example - Video chat now uses self-hosted Jitsi (meet.jeffemmett.com) Sync Logic Fix: - Fix stale IndexedDB cache preventing server data from loading - Changed threshold from "10x more shapes" to "more shapes" - Server data now properly updates local cache on initial load - Keeps local-only records for offline work Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fb3edce5a9
commit
2030ae447d
|
|
@ -1,7 +1,6 @@
|
||||||
# Frontend (VITE) Public Variables
|
# Frontend (VITE) Public Variables
|
||||||
VITE_GOOGLE_CLIENT_ID='your_google_client_id'
|
VITE_GOOGLE_CLIENT_ID='your_google_client_id'
|
||||||
VITE_GOOGLE_MAPS_API_KEY='your_google_maps_api_key'
|
VITE_GOOGLE_MAPS_API_KEY='your_google_maps_api_key'
|
||||||
VITE_DAILY_DOMAIN='your_daily_domain'
|
|
||||||
VITE_TLDRAW_WORKER_URL='your_worker_url'
|
VITE_TLDRAW_WORKER_URL='your_worker_url'
|
||||||
|
|
||||||
# AI Configuration
|
# AI Configuration
|
||||||
|
|
@ -26,4 +25,3 @@ CLOUDFLARE_ACCOUNT_ID='your_account_id'
|
||||||
CLOUDFLARE_ZONE_ID='your_zone_id'
|
CLOUDFLARE_ZONE_ID='your_zone_id'
|
||||||
R2_BUCKET_NAME='your_bucket_name'
|
R2_BUCKET_NAME='your_bucket_name'
|
||||||
R2_PREVIEW_BUCKET_NAME='your_preview_bucket_name'
|
R2_PREVIEW_BUCKET_NAME='your_preview_bucket_name'
|
||||||
DAILY_API_KEY=your_daily_api_key_here
|
|
||||||
|
|
@ -18,8 +18,6 @@
|
||||||
"@automerge/automerge-repo-react-hooks": "^2.2.0",
|
"@automerge/automerge-repo-react-hooks": "^2.2.0",
|
||||||
"@automerge/automerge-repo-storage-indexeddb": "^2.5.0",
|
"@automerge/automerge-repo-storage-indexeddb": "^2.5.0",
|
||||||
"@chengsokdara/use-whisper": "^0.2.0",
|
"@chengsokdara/use-whisper": "^0.2.0",
|
||||||
"@daily-co/daily-js": "^0.60.0",
|
|
||||||
"@daily-co/daily-react": "^0.20.0",
|
|
||||||
"@fal-ai/client": "^1.7.2",
|
"@fal-ai/client": "^1.7.2",
|
||||||
"@mdxeditor/editor": "^3.51.0",
|
"@mdxeditor/editor": "^3.51.0",
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
|
|
@ -3411,40 +3409,6 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@daily-co/daily-js": {
|
|
||||||
"version": "0.60.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@daily-co/daily-js/-/daily-js-0.60.0.tgz",
|
|
||||||
"integrity": "sha512-4GkmOKbxZfen4DI6N1HVqj5CiWrg7r8xALgcbwb5V+Ij1h7LHODDDd78XqhzEBBQp4yNjg6U2wz+l/cVznqc4A==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.12.5",
|
|
||||||
"@sentry/browser": "^7.60.1",
|
|
||||||
"bowser": "^2.8.1",
|
|
||||||
"dequal": "^2.0.3",
|
|
||||||
"events": "^3.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@daily-co/daily-react": {
|
|
||||||
"version": "0.20.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@daily-co/daily-react/-/daily-react-0.20.0.tgz",
|
|
||||||
"integrity": "sha512-Agcp5+nvMtZfej2jzPyl8ExmXG1Kk4ULk6BHz24RvNHYVsri8eJ3NSWZgJBmqPtCuEjLjM3wdo7/e1Aew0sfoA==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"fast-deep-equal": "^3.1.3",
|
|
||||||
"lodash.throttle": "^4.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@daily-co/daily-js": ">=0.68.0 <1",
|
|
||||||
"react": ">=16.13.1",
|
|
||||||
"recoil": "^0.7.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@dimforge/rapier3d-compat": {
|
"node_modules/@dimforge/rapier3d-compat": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||||
|
|
@ -8025,132 +7989,6 @@
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry-internal/feedback": {
|
|
||||||
"version": "7.120.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.120.4.tgz",
|
|
||||||
"integrity": "sha512-eSwgvTdrh03zYYaI6UVOjI9p4VmKg6+c2+CBQfRZX++6wwnCVsNv7XF7WUIpVGBAkJ0N2oapjQmCzJKGKBRWQg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@sentry/core": "7.120.4",
|
|
||||||
"@sentry/types": "7.120.4",
|
|
||||||
"@sentry/utils": "7.120.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry-internal/replay-canvas": {
|
|
||||||
"version": "7.120.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.120.4.tgz",
|
|
||||||
"integrity": "sha512-2+W4CgUL1VzrPjArbTid4WhKh7HH21vREVilZdvffQPVwOEpgNTPAb69loQuTlhJVveh9hWTj2nE5UXLbLP+AA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@sentry/core": "7.120.4",
|
|
||||||
"@sentry/replay": "7.120.4",
|
|
||||||
"@sentry/types": "7.120.4",
|
|
||||||
"@sentry/utils": "7.120.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry-internal/tracing": {
|
|
||||||
"version": "7.120.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.4.tgz",
|
|
||||||
"integrity": "sha512-Fz5+4XCg3akeoFK+K7g+d7HqGMjmnLoY2eJlpONJmaeT9pXY7yfUyXKZMmMajdE2LxxKJgQ2YKvSCaGVamTjHw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@sentry/core": "7.120.4",
|
|
||||||
"@sentry/types": "7.120.4",
|
|
||||||
"@sentry/utils": "7.120.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry/browser": {
|
|
||||||
"version": "7.120.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.120.4.tgz",
|
|
||||||
"integrity": "sha512-ymlNtIPG6HAKzM/JXpWVGCzCNufZNADfy+O/olZuVJW5Be1DtOFyRnBvz0LeKbmxJbXb2lX/XMhuen6PXPdoQw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@sentry-internal/feedback": "7.120.4",
|
|
||||||
"@sentry-internal/replay-canvas": "7.120.4",
|
|
||||||
"@sentry-internal/tracing": "7.120.4",
|
|
||||||
"@sentry/core": "7.120.4",
|
|
||||||
"@sentry/integrations": "7.120.4",
|
|
||||||
"@sentry/replay": "7.120.4",
|
|
||||||
"@sentry/types": "7.120.4",
|
|
||||||
"@sentry/utils": "7.120.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry/core": {
|
|
||||||
"version": "7.120.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.4.tgz",
|
|
||||||
"integrity": "sha512-TXu3Q5kKiq8db9OXGkWyXUbIxMMuttB5vJ031yolOl5T/B69JRyAoKuojLBjRv1XX583gS1rSSoX8YXX7ATFGA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@sentry/types": "7.120.4",
|
|
||||||
"@sentry/utils": "7.120.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry/integrations": {
|
|
||||||
"version": "7.120.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.4.tgz",
|
|
||||||
"integrity": "sha512-kkBTLk053XlhDCg7OkBQTIMF4puqFibeRO3E3YiVc4PGLnocXMaVpOSCkMqAc1k1kZ09UgGi8DxfQhnFEjUkpA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@sentry/core": "7.120.4",
|
|
||||||
"@sentry/types": "7.120.4",
|
|
||||||
"@sentry/utils": "7.120.4",
|
|
||||||
"localforage": "^1.8.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry/replay": {
|
|
||||||
"version": "7.120.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.120.4.tgz",
|
|
||||||
"integrity": "sha512-FW8sPenNFfnO/K7sncsSTX4rIVak9j7VUiLIagJrcqZIC7d1dInFNjy8CdVJUlyz3Y3TOgIl3L3+ZpjfyMnaZg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@sentry-internal/tracing": "7.120.4",
|
|
||||||
"@sentry/core": "7.120.4",
|
|
||||||
"@sentry/types": "7.120.4",
|
|
||||||
"@sentry/utils": "7.120.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry/types": {
|
|
||||||
"version": "7.120.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.4.tgz",
|
|
||||||
"integrity": "sha512-cUq2hSSe6/qrU6oZsEP4InMI5VVdD86aypE+ENrQ6eZEVLTCYm1w6XhW1NvIu3UuWh7gZec4a9J7AFpYxki88Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry/utils": {
|
|
||||||
"version": "7.120.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.4.tgz",
|
|
||||||
"integrity": "sha512-zCKpyDIWKHwtervNK2ZlaK8mMV7gVUijAgFeJStH+CU/imcdquizV3pFLlSQYRswG+Lbyd6CT/LGRh3IbtkCFw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@sentry/types": "7.120.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sindresorhus/is": {
|
"node_modules/@sindresorhus/is": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.1.tgz",
|
||||||
|
|
@ -11917,12 +11755,6 @@
|
||||||
"base-x": "^3.0.2"
|
"base-x": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bowser": {
|
|
||||||
"version": "2.13.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz",
|
|
||||||
"integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
|
|
@ -17241,15 +17073,6 @@
|
||||||
"@types/trusted-types": "^2.0.2"
|
"@types/trusted-types": "^2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/localforage": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
|
|
||||||
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"lie": "3.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,6 @@
|
||||||
"@automerge/automerge-repo-react-hooks": "^2.2.0",
|
"@automerge/automerge-repo-react-hooks": "^2.2.0",
|
||||||
"@automerge/automerge-repo-storage-indexeddb": "^2.5.0",
|
"@automerge/automerge-repo-storage-indexeddb": "^2.5.0",
|
||||||
"@chengsokdara/use-whisper": "^0.2.0",
|
"@chengsokdara/use-whisper": "^0.2.0",
|
||||||
"@daily-co/daily-js": "^0.60.0",
|
|
||||||
"@daily-co/daily-react": "^0.20.0",
|
|
||||||
"@fal-ai/client": "^1.7.2",
|
"@fal-ai/client": "^1.7.2",
|
||||||
"@mdxeditor/editor": "^3.51.0",
|
"@mdxeditor/editor": "^3.51.0",
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
|
|
|
||||||
26
src/App.tsx
26
src/App.tsx
|
|
@ -34,11 +34,6 @@ import { Web3Provider } from './providers/Web3Provider';
|
||||||
// Import Google Data test component
|
// Import Google Data test component
|
||||||
import { GoogleDataTest } from './components/GoogleDataTest';
|
import { GoogleDataTest } from './components/GoogleDataTest';
|
||||||
|
|
||||||
// Lazy load Daily.co provider - only needed for video chat
|
|
||||||
const DailyProvider = lazy(() =>
|
|
||||||
import('@daily-co/daily-react').then(m => ({ default: m.DailyProvider }))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Loading skeleton for lazy-loaded routes
|
// Loading skeleton for lazy-loaded routes
|
||||||
const LoadingSpinner = () => (
|
const LoadingSpinner = () => (
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|
@ -69,25 +64,6 @@ const LoadingSpinner = () => (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Daily.co call object - initialized lazily when needed
|
|
||||||
let dailyCallObject: any = null;
|
|
||||||
const getDailyCallObject = async () => {
|
|
||||||
if (dailyCallObject) return dailyCallObject;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Only create call object if we're in a secure context and mediaDevices is available
|
|
||||||
if (typeof window !== 'undefined' &&
|
|
||||||
window.location.protocol === 'https:' &&
|
|
||||||
navigator.mediaDevices) {
|
|
||||||
const Daily = (await import('@daily-co/daily-js')).default;
|
|
||||||
dailyCallObject = Daily.createCallObject();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Daily.co call object initialization failed:', error);
|
|
||||||
}
|
|
||||||
return dailyCallObject;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional Auth Route component
|
* Optional Auth Route component
|
||||||
* Allows guests to browse, but provides login option
|
* Allows guests to browse, but provides login option
|
||||||
|
|
@ -151,7 +127,6 @@ const AppWithProviders = () => {
|
||||||
<FileSystemProvider>
|
<FileSystemProvider>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<Suspense fallback={<LoadingSpinner />}>
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
<DailyProvider callObject={null}>
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
{/* Display notifications */}
|
{/* Display notifications */}
|
||||||
<NotificationsDisplay />
|
<NotificationsDisplay />
|
||||||
|
|
@ -228,7 +203,6 @@ const AppWithProviders = () => {
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</DailyProvider>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</FileSystemProvider>
|
</FileSystemProvider>
|
||||||
|
|
|
||||||
|
|
@ -481,11 +481,11 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus
|
||||||
const localRecordCount = localDoc?.store ? Object.keys(localDoc.store).length : 0
|
const localRecordCount = localDoc?.store ? Object.keys(localDoc.store).length : 0
|
||||||
|
|
||||||
// Merge server data with local data
|
// Merge server data with local data
|
||||||
// Strategy:
|
// Strategy (IMPROVED):
|
||||||
// 1. If local has NO SHAPES (only ephemeral records), use server data
|
// 1. Server is the source of truth for initial page load
|
||||||
// 2. If server has SIGNIFICANTLY MORE shapes (10x), prefer server (stale local cache)
|
// 2. Always update local with server data for shape records
|
||||||
// 3. Otherwise, only add server records that don't exist locally
|
// 3. Keep local-only records (potential offline additions not yet synced)
|
||||||
// (preserve offline changes, let Automerge CRDT sync handle conflicts)
|
// 4. This ensures stale IndexedDB cache doesn't override server data
|
||||||
if (serverDoc.store && serverRecordCount > 0) {
|
if (serverDoc.store && serverRecordCount > 0) {
|
||||||
// Track if we merged any data (needed outside the change callback)
|
// Track if we merged any data (needed outside the change callback)
|
||||||
let totalMerged = 0
|
let totalMerged = 0
|
||||||
|
|
@ -500,46 +500,39 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus
|
||||||
const localShapeCount = Object.values(doc.store).filter((r: any) => r?.typeName === 'shape').length
|
const localShapeCount = Object.values(doc.store).filter((r: any) => r?.typeName === 'shape').length
|
||||||
const localIsEmpty = Object.keys(doc.store).length === 0
|
const localIsEmpty = Object.keys(doc.store).length === 0
|
||||||
|
|
||||||
// Server has significantly more shapes - local is likely stale cache
|
// IMPROVED: Server is source of truth on initial load
|
||||||
// Use 10x threshold or server has shapes but local has none
|
// Prefer server if:
|
||||||
const serverHasSignificantlyMore = (
|
// - Local is empty (first load or cleared cache)
|
||||||
localShapeCount === 0 && serverShapeCount > 0
|
// - Server has more shapes (local is likely stale/incomplete)
|
||||||
) || (
|
// - Local has shapes but server has different/more content
|
||||||
serverShapeCount > 0 && localShapeCount > 0 && serverShapeCount >= localShapeCount * 10
|
const serverHasMoreContent = serverShapeCount > localShapeCount
|
||||||
)
|
const shouldPreferServer = localIsEmpty || localShapeCount === 0 || serverHasMoreContent
|
||||||
|
|
||||||
// If local has no shapes but server does, or server has 10x more,
|
|
||||||
// replace local with server data (but keep local ephemeral records)
|
|
||||||
const shouldPreferServer = localIsEmpty || localShapeCount === 0 || serverHasSignificantlyMore
|
|
||||||
|
|
||||||
let addedFromServer = 0
|
let addedFromServer = 0
|
||||||
let skippedExisting = 0
|
let updatedFromServer = 0
|
||||||
let replacedFromServer = 0
|
let keptLocal = 0
|
||||||
|
|
||||||
Object.entries(serverDoc.store).forEach(([id, record]) => {
|
Object.entries(serverDoc.store).forEach(([id, record]) => {
|
||||||
if (shouldPreferServer) {
|
const existsLocally = !!doc.store[id]
|
||||||
// Prefer server data - bootstrap or replace stale local
|
|
||||||
if (doc.store[id]) {
|
if (!existsLocally) {
|
||||||
replacedFromServer++
|
// Record doesn't exist locally - add from server
|
||||||
} else {
|
|
||||||
addedFromServer++
|
|
||||||
}
|
|
||||||
doc.store[id] = record
|
|
||||||
} else if (!doc.store[id]) {
|
|
||||||
// Local has data but missing this record - add from server
|
|
||||||
// This handles: shapes created on another device and synced to R2
|
|
||||||
doc.store[id] = record
|
doc.store[id] = record
|
||||||
addedFromServer++
|
addedFromServer++
|
||||||
|
} else if (shouldPreferServer) {
|
||||||
|
// Record exists locally but server has more content - update with server version
|
||||||
|
// This handles stale IndexedDB cache scenarios
|
||||||
|
doc.store[id] = record
|
||||||
|
updatedFromServer++
|
||||||
} else {
|
} else {
|
||||||
// Record exists locally - preserve local version
|
// Local has equal or more content - keep local version
|
||||||
// The Automerge binary sync will handle merging conflicts via CRDT
|
// Local changes will sync to server via normal CRDT mechanism
|
||||||
// This preserves offline edits to existing shapes
|
keptLocal++
|
||||||
skippedExisting++
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
totalMerged = addedFromServer + replacedFromServer
|
totalMerged = addedFromServer + updatedFromServer
|
||||||
console.log(`🔄 Server sync: added=${addedFromServer}, replaced=${replacedFromServer}, skipped=${skippedExisting}, shouldPreferServer=${shouldPreferServer}`)
|
console.log(`🔄 Server sync: added=${addedFromServer}, updated=${updatedFromServer}, keptLocal=${keptLocal}, serverShapes=${serverShapeCount}, localShapes=${localShapeCount}, preferServer=${shouldPreferServer}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
const finalDoc = handle.doc()
|
const finalDoc = handle.doc()
|
||||||
|
|
|
||||||
382
worker/worker.ts
382
worker/worker.ts
|
|
@ -322,388 +322,6 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
.post("/daily/rooms", async (req, env) => {
|
|
||||||
// Use server-side API key - never expose to client
|
|
||||||
const apiKey = env.DAILY_API_KEY
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get the request body from the client
|
|
||||||
const body = await req.json()
|
|
||||||
|
|
||||||
const response = await fetch('https://api.daily.co/v1/rooms', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${apiKey}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
return new Response(JSON.stringify(error), {
|
|
||||||
status: response.status,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get room info by name
|
|
||||||
.get("/daily/rooms/:roomName", async (req, env) => {
|
|
||||||
// Use server-side API key - never expose to client
|
|
||||||
const apiKey = env.DAILY_API_KEY
|
|
||||||
const { roomName } = req.params
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`https://api.daily.co/v1/rooms/${roomName}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${apiKey}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
return new Response(JSON.stringify(error), {
|
|
||||||
status: response.status,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
.post("/daily/tokens", async (req, env) => {
|
|
||||||
// Use server-side API key - never expose to client
|
|
||||||
const apiKey = env.DAILY_API_KEY
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body = await req.json() as { room_name: string; properties: any };
|
|
||||||
const response = await fetch('https://api.daily.co/v1/meeting-tokens', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${apiKey}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
room_name: body.room_name,
|
|
||||||
properties: body.properties
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
return new Response(JSON.stringify(error), {
|
|
||||||
status: response.status,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add new transcription endpoints
|
|
||||||
.post("/daily/rooms/:roomName/start-transcription", async (req, env) => {
|
|
||||||
// Use server-side API key - never expose to client
|
|
||||||
const apiKey = env.DAILY_API_KEY
|
|
||||||
const { roomName } = req.params
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`https://api.daily.co/v1/rooms/${roomName}/transcription/start`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${apiKey}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
return new Response(JSON.stringify(error), {
|
|
||||||
status: response.status,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
.post("/daily/rooms/:roomName/stop-transcription", async (req, env) => {
|
|
||||||
// Use server-side API key - never expose to client
|
|
||||||
const apiKey = env.DAILY_API_KEY
|
|
||||||
const { roomName } = req.params
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`https://api.daily.co/v1/rooms/${roomName}/transcription/stop`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${apiKey}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
return new Response(JSON.stringify(error), {
|
|
||||||
status: response.status,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add endpoint to get transcript access link
|
|
||||||
.get("/daily/transcript/:transcriptId/access-link", async (req, env) => {
|
|
||||||
// Use server-side API key - never expose to client
|
|
||||||
const apiKey = env.DAILY_API_KEY
|
|
||||||
const { transcriptId } = req.params
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`https://api.daily.co/v1/transcript/${transcriptId}/access-link`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${apiKey}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
return new Response(JSON.stringify(error), {
|
|
||||||
status: response.status,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add endpoint to get transcript text
|
|
||||||
.get("/daily/transcript/:transcriptId", async (req, env) => {
|
|
||||||
// Use server-side API key - never expose to client
|
|
||||||
const apiKey = env.DAILY_API_KEY
|
|
||||||
const { transcriptId } = req.params
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`https://api.daily.co/v1/transcripts/${transcriptId}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${apiKey}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
return new Response(JSON.stringify(error), {
|
|
||||||
status: response.status,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Recording endpoints
|
|
||||||
.post("/daily/recordings/start", async (req, env) => {
|
|
||||||
// Use server-side API key - never expose to client
|
|
||||||
const apiKey = env.DAILY_API_KEY
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body = await req.json() as any;
|
|
||||||
const response = await fetch('https://api.daily.co/v1/recordings', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${apiKey}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
return new Response(JSON.stringify(error), {
|
|
||||||
status: response.status,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
.post("/daily/recordings/:recordingId/stop", async (req, env) => {
|
|
||||||
// Use server-side API key - never expose to client
|
|
||||||
const apiKey = env.DAILY_API_KEY
|
|
||||||
const { recordingId } = req.params
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`https://api.daily.co/v1/recordings/${recordingId}/stop`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${apiKey}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
return new Response(JSON.stringify(error), {
|
|
||||||
status: response.status,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Fathom API endpoints (api.fathom.ai)
|
// Fathom API endpoints (api.fathom.ai)
|
||||||
.get("/fathom/meetings", async (req) => {
|
.get("/fathom/meetings", async (req) => {
|
||||||
console.log('Fathom meetings endpoint called')
|
console.log('Fathom meetings endpoint called')
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ account_id = "0e7b3338d5278ed1b148e6456b940913"
|
||||||
|
|
||||||
[vars]
|
[vars]
|
||||||
# Development environment variables
|
# Development environment variables
|
||||||
DAILY_DOMAIN = "mycopunks.daily.co"
|
|
||||||
|
|
||||||
[dev]
|
[dev]
|
||||||
port = 5172
|
port = 5172
|
||||||
|
|
@ -52,6 +51,7 @@ crons = ["0 0 * * *"] # Run at midnight UTC every day
|
||||||
|
|
||||||
# Secrets should be set using `wrangler secret put` command for dev environment
|
# Secrets should be set using `wrangler secret put` command for dev environment
|
||||||
# DO NOT put these directly in wrangler.toml:
|
# DO NOT put these directly in wrangler.toml:
|
||||||
# - DAILY_API_KEY
|
|
||||||
# - CLOUDFLARE_API_TOKEN
|
# - CLOUDFLARE_API_TOKEN
|
||||||
|
# - FAL_API_KEY
|
||||||
|
# - RUNPOD_API_KEY
|
||||||
# etc.
|
# etc.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ account_id = "0e7b3338d5278ed1b148e6456b940913"
|
||||||
[vars]
|
[vars]
|
||||||
# Environment variables are managed in Cloudflare Dashboard
|
# Environment variables are managed in Cloudflare Dashboard
|
||||||
# Workers & Pages → jeffemmett-canvas → Settings → Variables
|
# Workers & Pages → jeffemmett-canvas → Settings → Variables
|
||||||
DAILY_DOMAIN = "mycopunks.daily.co"
|
|
||||||
|
|
||||||
# RunPod AI Service Configuration (defaults hardcoded in src/lib/clientConfig.ts)
|
# RunPod AI Service Configuration (defaults hardcoded in src/lib/clientConfig.ts)
|
||||||
# These are documented here for reference - actual values are in the client code
|
# These are documented here for reference - actual values are in the client code
|
||||||
|
|
@ -77,7 +76,7 @@ name = "jeffemmett-canvas-automerge-dev"
|
||||||
compatibility_date = "2024-07-01"
|
compatibility_date = "2024-07-01"
|
||||||
|
|
||||||
[env.dev.vars]
|
[env.dev.vars]
|
||||||
DAILY_DOMAIN = "mycopunks.daily.co"
|
# Dev environment variables
|
||||||
|
|
||||||
[env.dev.durable_objects]
|
[env.dev.durable_objects]
|
||||||
bindings = [
|
bindings = [
|
||||||
|
|
@ -106,7 +105,6 @@ 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
|
|
||||||
# - CLOUDFLARE_API_TOKEN
|
# - CLOUDFLARE_API_TOKEN
|
||||||
# - FAL_API_KEY # For fal.ai image/video generation proxy
|
# - FAL_API_KEY # For fal.ai image/video generation proxy
|
||||||
# - RUNPOD_API_KEY # For RunPod AI endpoints proxy
|
# - RUNPOD_API_KEY # For RunPod AI endpoints proxy
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue