almost everything working, except maybe offline storage state (and browser reload)
This commit is contained in:
parent
100b88268b
commit
db3205f97a
|
|
@ -1,32 +1,38 @@
|
|||
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<TLRecord[]>(`gset-${roomId}`, {
|
||||
defaultValue: []
|
||||
});
|
||||
|
||||
const gset = new GSet<TLRecord>();
|
||||
|
||||
// Keep GSet instance in a ref to persist between renders
|
||||
const gsetRef = useRef<GSet<TLRecord>>();
|
||||
if (!gsetRef.current) {
|
||||
gsetRef.current = new GSet<TLRecord>();
|
||||
// Initialize G-Set with local data
|
||||
if (localSet && Array.isArray(localSet)) {
|
||||
localSet.forEach(record => gset.add(record));
|
||||
localSet.forEach(record => gsetRef.current?.add(record));
|
||||
}
|
||||
}
|
||||
|
||||
const addRecord = (record: TLRecord) => {
|
||||
gset.add(record);
|
||||
setLocalSet(Array.from(gset.values()));
|
||||
};
|
||||
const addRecord = useCallback((record: TLRecord) => {
|
||||
if (!gsetRef.current) return;
|
||||
gsetRef.current.add(record);
|
||||
setLocalSet(Array.from(gsetRef.current.values()));
|
||||
}, [setLocalSet]);
|
||||
|
||||
const merge = (remoteSet: Set<TLRecord>) => {
|
||||
remoteSet.forEach(record => gset.add(record));
|
||||
setLocalSet(Array.from(gset.values()));
|
||||
return gset.values();
|
||||
};
|
||||
const merge = useCallback((remoteSet: Set<TLRecord>) => {
|
||||
if (!gsetRef.current) return new Set<TLRecord>();
|
||||
remoteSet.forEach(record => gsetRef.current?.add(record));
|
||||
setLocalSet(Array.from(gsetRef.current.values()));
|
||||
return gsetRef.current.values();
|
||||
}, [setLocalSet]);
|
||||
|
||||
return {
|
||||
values: gset.values(),
|
||||
values: gsetRef.current.values(),
|
||||
add: addRecord,
|
||||
merge,
|
||||
localSet
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import useLocalStorageState from 'use-local-storage-state';
|
||||
import { TLRecord, createTLStore, SerializedStore } from 'tldraw';
|
||||
import { customSchema } from '../../worker/TldrawDurableObject';
|
||||
import { TLSocketRoom } from '@tldraw/sync-core';
|
||||
|
||||
export function useLocalStorageRoom(roomId: string) {
|
||||
const [records, setRecords] = useLocalStorageState<SerializedStore<TLRecord>>(`tldraw-room-${roomId}`, {
|
||||
|
|
@ -13,23 +12,8 @@ export function useLocalStorageRoom(roomId: string) {
|
|||
initialData: records,
|
||||
});
|
||||
|
||||
const socketRoom = new TLSocketRoom({
|
||||
initialSnapshot: {
|
||||
store: store.serialize(),
|
||||
schema: customSchema.serialize(),
|
||||
},
|
||||
schema: customSchema,
|
||||
onDataChange: () => {
|
||||
const serializedStore = store.serialize();
|
||||
setRecords(serializedStore);
|
||||
// Broadcast changes to other clients
|
||||
store.mergeRemoteChanges(() => Object.values(serializedStore));
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
store,
|
||||
socketRoom,
|
||||
records,
|
||||
setRecords
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,39 +1,25 @@
|
|||
import { useSync } from '@tldraw/sync'
|
||||
import { useState, useEffect } from 'react'
|
||||
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 { RecordType, BaseRecord } from '@tldraw/store'
|
||||
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 getWebSocketUrl = (baseUrl: string) => {
|
||||
// Remove any trailing slashes
|
||||
baseUrl = baseUrl.replace(/\/$/, '')
|
||||
|
||||
// Handle different protocols
|
||||
if (baseUrl.startsWith('https://')) {
|
||||
return baseUrl.replace('https://', 'wss://')
|
||||
} else if (baseUrl.startsWith('http://')) {
|
||||
return baseUrl.replace('http://', 'ws://')
|
||||
}
|
||||
return baseUrl
|
||||
}
|
||||
const initialSyncRef = useRef(false)
|
||||
const mergeInProgressRef = useRef(false)
|
||||
|
||||
const syncedStore = useSync({
|
||||
uri: import.meta.env.TLDRAW_WORKER_URL
|
||||
? `${getWebSocketUrl(import.meta.env.TLDRAW_WORKER_URL)}/connect/${roomId}`
|
||||
: `wss://jeffemmett-canvas.jeffemmett.workers.dev/connect/${roomId}`,
|
||||
uri: `${WORKER_URL.replace('https://', 'wss://')}/connect/${roomId}`,
|
||||
schema: customSchema,
|
||||
assets: multiplayerAssetStore,
|
||||
})
|
||||
|
||||
// Handle online/offline status
|
||||
useEffect(() => {
|
||||
const handleOnline = () => setIsOnline(true)
|
||||
const handleOffline = () => setIsOnline(false)
|
||||
|
|
@ -47,40 +33,60 @@ export function usePersistentBoard(roomId: string) {
|
|||
}
|
||||
}, [])
|
||||
|
||||
// Handle online/offline synchronization
|
||||
useEffect(() => {
|
||||
if (isOnline && syncedStore?.store) {
|
||||
// Sync server records to local
|
||||
const serverRecords = Object.values(syncedStore.store.allRecords())
|
||||
merge(new Set(serverRecords))
|
||||
const mergeRecords = useCallback((records: Set<TLRecord>) => {
|
||||
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))
|
||||
}
|
||||
|
||||
// Set up store change listener
|
||||
const unsubscribe = syncedStore.store.listen((event) => {
|
||||
if ('changes' in event) {
|
||||
const changedRecords = Object.values(event.changes)
|
||||
merge(new Set(changedRecords))
|
||||
// Also update local storage
|
||||
setRecords(syncedStore.store.serialize())
|
||||
if (changedRecords.length > 0) {
|
||||
mergeRecords(new Set(changedRecords))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return () => unsubscribe()
|
||||
} else if (!isOnline && localStore) {
|
||||
// When going offline, ensure we have the latest state in local storage
|
||||
} else if (!isOnline) {
|
||||
const currentRecords = Object.values(localStore.allRecords())
|
||||
merge(new Set(currentRecords))
|
||||
if (currentRecords.length > 0) {
|
||||
mergeRecords(new Set(currentRecords))
|
||||
}
|
||||
}, [isOnline, syncedStore?.store, localStore])
|
||||
}
|
||||
}, [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: (record: TLRecord) => {
|
||||
add(record)
|
||||
if (!isOnline) {
|
||||
setRecords(localStore.serialize())
|
||||
}
|
||||
},
|
||||
mergeRecords: merge
|
||||
addRecord,
|
||||
mergeRecords
|
||||
}
|
||||
}
|
||||
|
|
@ -21,12 +21,31 @@ 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) => {
|
||||
const allowedOrigins = [
|
||||
'http://localhost:5172',
|
||||
'http://192.168.1.7:5172',
|
||||
'https://jeffemmett.com'
|
||||
if (!origin) return undefined
|
||||
|
||||
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$/
|
||||
]
|
||||
return allowedOrigins.includes(origin) ? origin : undefined
|
||||
|
||||
// Check if origin matches any of our patterns
|
||||
const isAllowed = allowedPatterns.some(pattern =>
|
||||
pattern instanceof RegExp
|
||||
? pattern.test(origin)
|
||||
: pattern === origin
|
||||
)
|
||||
|
||||
return isAllowed ? origin : undefined
|
||||
},
|
||||
allowMethods: ['GET', 'POST', 'OPTIONS', 'UPGRADE'],
|
||||
allowHeaders: [
|
||||
|
|
|
|||
Loading…
Reference in New Issue