put sync stuff in bemo worker (#4060)

this PR puts sync stuff in the bemo worker, and sets up a temporary
dev-only page in dotcom for testing bemo stuff


### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [x] `feature`
- [ ] `api`
- [ ] `other`

### Test plan

1. Create a shape...
2.

- [ ] Unit tests
- [ ] End to end tests

### Release notes

- Fixed a bug with...
This commit is contained in:
David Sheldrick 2024-07-03 15:10:54 +01:00 committed by GitHub
parent 8906bd8ffa
commit c1fe8ec99a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
83 changed files with 571 additions and 120 deletions

View file

@ -0,0 +1,144 @@
import {
ClientWebSocketAdapter,
TLCloseEventCode,
TLIncompatibilityReason,
TLPersistentClientSocketStatus,
TLRemoteSyncError,
TLSyncClient,
schema,
} from '@tldraw/sync'
import { useEffect, useState } from 'react'
import {
Signal,
TAB_ID,
TLRecord,
TLStore,
TLStoreSnapshot,
TLStoreWithStatus,
TLUserPreferences,
computed,
createPresenceStateDerivation,
defaultUserPreferences,
getUserPreferences,
useTLStore,
useValue,
} from 'tldraw'
const MULTIPLAYER_EVENT_NAME = 'multiplayer.client'
/** @public */
export type RemoteTLStoreWithStatus = Exclude<
TLStoreWithStatus,
{ status: 'synced-local' } | { status: 'not-synced' }
>
/** @public */
export function useRemoteSyncClient(opts: UseSyncClientConfig): RemoteTLStoreWithStatus {
const [state, setState] = useState<{
readyClient?: TLSyncClient<TLRecord, TLStore>
error?: Error
} | null>(null)
const { uri, roomId = 'default', userPreferences: prefs } = opts
const store = useTLStore({ schema })
const error: NonNullable<typeof state>['error'] = state?.error ?? undefined
const track = opts.trackAnalyticsEvent
useEffect(() => {
if (error) return
const userPreferences = computed<{ id: string; color: string; name: string }>(
'userPreferences',
() => {
const user = prefs?.get() ?? getUserPreferences()
return {
id: user.id,
color: user.color ?? defaultUserPreferences.color,
name: user.name ?? defaultUserPreferences.name,
}
}
)
const socket = new ClientWebSocketAdapter(async () => {
// set sessionKey as a query param on the uri
const withParams = new URL(uri)
withParams.searchParams.set('sessionKey', TAB_ID)
withParams.searchParams.set('storeId', store.id)
return withParams.toString()
})
socket.onStatusChange((val: TLPersistentClientSocketStatus, closeCode?: number) => {
if (val === 'error' && closeCode === TLCloseEventCode.NOT_FOUND) {
track?.(MULTIPLAYER_EVENT_NAME, { name: 'room-not-found', roomId })
setState({ error: new TLRemoteSyncError(TLIncompatibilityReason.RoomNotFound) })
client.close()
socket.close()
return
}
})
let didCancel = false
const client = new TLSyncClient({
store,
socket,
didCancel: () => didCancel,
onLoad(client) {
track?.(MULTIPLAYER_EVENT_NAME, { name: 'load', roomId })
setState({ readyClient: client })
},
onLoadError(err) {
track?.(MULTIPLAYER_EVENT_NAME, { name: 'load-error', roomId })
console.error(err)
setState({ error: err })
},
onSyncError(reason) {
track?.(MULTIPLAYER_EVENT_NAME, { name: 'sync-error', roomId, reason })
setState({ error: new TLRemoteSyncError(reason) })
},
onAfterConnect() {
// if the server crashes and loses all data it can return an empty document
// when it comes back up. This is a safety check to make sure that if something like
// that happens, it won't render the app broken and require a restart. The user will
// most likely lose all their changes though since they'll have been working with pages
// that won't exist. There's certainly something we can do to make this better.
// but the likelihood of this happening is very low and maybe not worth caring about beyond this.
store.ensureStoreIsUsable()
},
presence: createPresenceStateDerivation(userPreferences)(store),
})
return () => {
didCancel = true
client.close()
socket.close()
}
}, [prefs, roomId, store, uri, error, track])
return useValue<RemoteTLStoreWithStatus>(
'remote synced store',
() => {
if (!state) return { status: 'loading' }
if (state.error) return { status: 'error', error: state.error }
if (!state.readyClient) return { status: 'loading' }
const connectionStatus = state.readyClient.socket.connectionStatus
return {
status: 'synced-remote',
connectionStatus: connectionStatus === 'error' ? 'offline' : connectionStatus,
store: state.readyClient.store,
}
},
[state]
)
}
/** @public */
export interface UseSyncClientConfig {
uri: string
roomId?: string
userPreferences?: Signal<TLUserPreferences>
snapshotForNewRoomRef?: { current: null | TLStoreSnapshot }
/* @internal */
trackAnalyticsEvent?(name: string, data: { [key: string]: any }): void
}