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 useLocalStorageState from 'use-local-storage-state';
|
||||||
import GSet from 'crdts/src/G-Set';
|
import GSet from 'crdts/src/G-Set';
|
||||||
import { TLRecord } from 'tldraw';
|
import { TLRecord } from 'tldraw';
|
||||||
|
import { useRef, useCallback } from 'react';
|
||||||
|
|
||||||
export function useGSetState(roomId: string) {
|
export function useGSetState(roomId: string) {
|
||||||
const [localSet, setLocalSet] = useLocalStorageState<TLRecord[]>(`gset-${roomId}`, {
|
const [localSet, setLocalSet] = useLocalStorageState<TLRecord[]>(`gset-${roomId}`, {
|
||||||
defaultValue: []
|
defaultValue: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const gset = new GSet<TLRecord>();
|
// Keep GSet instance in a ref to persist between renders
|
||||||
|
const gsetRef = useRef<GSet<TLRecord>>();
|
||||||
// Initialize G-Set with local data
|
if (!gsetRef.current) {
|
||||||
if (localSet && Array.isArray(localSet)) {
|
gsetRef.current = new GSet<TLRecord>();
|
||||||
localSet.forEach(record => gset.add(record));
|
// Initialize G-Set with local data
|
||||||
|
if (localSet && Array.isArray(localSet)) {
|
||||||
|
localSet.forEach(record => gsetRef.current?.add(record));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addRecord = (record: TLRecord) => {
|
const addRecord = useCallback((record: TLRecord) => {
|
||||||
gset.add(record);
|
if (!gsetRef.current) return;
|
||||||
setLocalSet(Array.from(gset.values()));
|
gsetRef.current.add(record);
|
||||||
};
|
setLocalSet(Array.from(gsetRef.current.values()));
|
||||||
|
}, [setLocalSet]);
|
||||||
|
|
||||||
const merge = (remoteSet: Set<TLRecord>) => {
|
const merge = useCallback((remoteSet: Set<TLRecord>) => {
|
||||||
remoteSet.forEach(record => gset.add(record));
|
if (!gsetRef.current) return new Set<TLRecord>();
|
||||||
setLocalSet(Array.from(gset.values()));
|
remoteSet.forEach(record => gsetRef.current?.add(record));
|
||||||
return gset.values();
|
setLocalSet(Array.from(gsetRef.current.values()));
|
||||||
};
|
return gsetRef.current.values();
|
||||||
|
}, [setLocalSet]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
values: gset.values(),
|
values: gsetRef.current.values(),
|
||||||
add: addRecord,
|
add: addRecord,
|
||||||
merge,
|
merge,
|
||||||
localSet
|
localSet
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import useLocalStorageState from 'use-local-storage-state';
|
import useLocalStorageState from 'use-local-storage-state';
|
||||||
import { TLRecord, createTLStore, SerializedStore } from 'tldraw';
|
import { TLRecord, createTLStore, SerializedStore } from 'tldraw';
|
||||||
import { customSchema } from '../../worker/TldrawDurableObject';
|
import { customSchema } from '../../worker/TldrawDurableObject';
|
||||||
import { TLSocketRoom } from '@tldraw/sync-core';
|
|
||||||
|
|
||||||
export function useLocalStorageRoom(roomId: string) {
|
export function useLocalStorageRoom(roomId: string) {
|
||||||
const [records, setRecords] = useLocalStorageState<SerializedStore<TLRecord>>(`tldraw-room-${roomId}`, {
|
const [records, setRecords] = useLocalStorageState<SerializedStore<TLRecord>>(`tldraw-room-${roomId}`, {
|
||||||
|
|
@ -13,23 +12,8 @@ export function useLocalStorageRoom(roomId: string) {
|
||||||
initialData: records,
|
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 {
|
return {
|
||||||
store,
|
store,
|
||||||
socketRoom,
|
|
||||||
records,
|
records,
|
||||||
setRecords
|
setRecords
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,25 @@
|
||||||
import { useSync } from '@tldraw/sync'
|
import { useSync } from '@tldraw/sync'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { customSchema } from '../../worker/TldrawDurableObject'
|
import { customSchema } from '../../worker/TldrawDurableObject'
|
||||||
import { multiplayerAssetStore } from '../client/multiplayerAssetStore'
|
import { multiplayerAssetStore } from '../client/multiplayerAssetStore'
|
||||||
import { useGSetState } from './useGSetState'
|
import { useGSetState } from './useGSetState'
|
||||||
import { useLocalStorageRoom } from './useLocalStorageRoom'
|
import { useLocalStorageRoom } from './useLocalStorageRoom'
|
||||||
import { RecordType, BaseRecord } from '@tldraw/store'
|
|
||||||
import { TLRecord } from 'tldraw'
|
import { TLRecord } from 'tldraw'
|
||||||
|
import { WORKER_URL } from '../components/Board'
|
||||||
|
|
||||||
export function usePersistentBoard(roomId: string) {
|
export function usePersistentBoard(roomId: string) {
|
||||||
const [isOnline, setIsOnline] = useState(navigator.onLine)
|
const [isOnline, setIsOnline] = useState(navigator.onLine)
|
||||||
const { store: localStore, records, setRecords } = useLocalStorageRoom(roomId)
|
const { store: localStore, records, setRecords } = useLocalStorageRoom(roomId)
|
||||||
const { values, add, merge } = useGSetState(roomId)
|
const { values, add, merge } = useGSetState(roomId)
|
||||||
|
const initialSyncRef = useRef(false)
|
||||||
const getWebSocketUrl = (baseUrl: string) => {
|
const mergeInProgressRef = useRef(false)
|
||||||
// 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 syncedStore = useSync({
|
const syncedStore = useSync({
|
||||||
uri: import.meta.env.TLDRAW_WORKER_URL
|
uri: `${WORKER_URL.replace('https://', 'wss://')}/connect/${roomId}`,
|
||||||
? `${getWebSocketUrl(import.meta.env.TLDRAW_WORKER_URL)}/connect/${roomId}`
|
|
||||||
: `wss://jeffemmett-canvas.jeffemmett.workers.dev/connect/${roomId}`,
|
|
||||||
schema: customSchema,
|
schema: customSchema,
|
||||||
assets: multiplayerAssetStore,
|
assets: multiplayerAssetStore,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle online/offline status
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleOnline = () => setIsOnline(true)
|
const handleOnline = () => setIsOnline(true)
|
||||||
const handleOffline = () => setIsOnline(false)
|
const handleOffline = () => setIsOnline(false)
|
||||||
|
|
@ -47,40 +33,60 @@ export function usePersistentBoard(roomId: string) {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Handle online/offline synchronization
|
const mergeRecords = useCallback((records: Set<TLRecord>) => {
|
||||||
useEffect(() => {
|
if (mergeInProgressRef.current || records.size === 0) return
|
||||||
if (isOnline && syncedStore?.store) {
|
|
||||||
// Sync server records to local
|
try {
|
||||||
const serverRecords = Object.values(syncedStore.store.allRecords())
|
mergeInProgressRef.current = true
|
||||||
merge(new Set(serverRecords))
|
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) => {
|
const unsubscribe = syncedStore.store.listen((event) => {
|
||||||
if ('changes' in event) {
|
if ('changes' in event) {
|
||||||
const changedRecords = Object.values(event.changes)
|
const changedRecords = Object.values(event.changes)
|
||||||
merge(new Set(changedRecords))
|
if (changedRecords.length > 0) {
|
||||||
// Also update local storage
|
mergeRecords(new Set(changedRecords))
|
||||||
setRecords(syncedStore.store.serialize())
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => unsubscribe()
|
return () => unsubscribe()
|
||||||
} else if (!isOnline && localStore) {
|
} else if (!isOnline) {
|
||||||
// When going offline, ensure we have the latest state in local storage
|
|
||||||
const currentRecords = Object.values(localStore.allRecords())
|
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 {
|
return {
|
||||||
store: isOnline ? syncedStore?.store : localStore,
|
store: isOnline ? syncedStore?.store : localStore,
|
||||||
isOnline,
|
isOnline,
|
||||||
addRecord: (record: TLRecord) => {
|
addRecord,
|
||||||
add(record)
|
mergeRecords
|
||||||
if (!isOnline) {
|
|
||||||
setRecords(localStore.serialize())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mergeRecords: merge
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -21,12 +21,31 @@ const securityHeaders = {
|
||||||
// we're hosting the worker separately to the client. you should restrict this to your own domain.
|
// we're hosting the worker separately to the client. you should restrict this to your own domain.
|
||||||
const { preflight, corsify } = cors({
|
const { preflight, corsify } = cors({
|
||||||
origin: (origin) => {
|
origin: (origin) => {
|
||||||
const allowedOrigins = [
|
if (!origin) return undefined
|
||||||
'http://localhost:5172',
|
|
||||||
'http://192.168.1.7:5172',
|
const allowedPatterns = [
|
||||||
'https://jeffemmett.com'
|
// 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'],
|
allowMethods: ['GET', 'POST', 'OPTIONS', 'UPGRADE'],
|
||||||
allowHeaders: [
|
allowHeaders: [
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue