Allow clients to gracefully handle rejection (#3673)
This PR fixes the issue where sync clients would get into a reconnect loop after being rejected by the sync server. - Close the socket when in the error state (see useRemoteSyncClient) - Show a 'plx refresh the page' screen that doesn't have a sad face on it. <img width="665" alt="image" src="https://github.com/tldraw/tldraw/assets/1242537/96025fa3-cc20-4f53-8f58-74e473e16702"> - If older clients who can't handle rejection well need to be rejected (e.g. due to a store migration being added) then we send them to a special purgatory where the canvas goes blank and it shows the offline indicator but the websocket connection stays open and it won't try to reconnect. ### Change Type - [x] `dotcom` — Changes the tldraw.com web app - [x] `bugfix` — Bug fix ### Test Plan 1. Gonna manually test this one by doing sneaky deploys to a test PR
This commit is contained in:
parent
e999316691
commit
f2827f6409
15 changed files with 143 additions and 68 deletions
|
@ -1,28 +1,39 @@
|
||||||
|
import { ReactNode } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { isInIframe } from '../../utils/iFrame'
|
import { isInIframe } from '../../utils/iFrame'
|
||||||
|
|
||||||
export function ErrorPage({
|
const GoBackLink = () => {
|
||||||
icon,
|
|
||||||
messages,
|
|
||||||
}: {
|
|
||||||
icon?: boolean
|
|
||||||
messages: { header: string; para1: string; para2?: string }
|
|
||||||
}) {
|
|
||||||
const inIframe = isInIframe()
|
const inIframe = isInIframe()
|
||||||
|
return (
|
||||||
|
<Link to={'/'} target={inIframe ? '_blank' : '_self'}>
|
||||||
|
{inIframe ? 'Open tldraw.' : 'Back to tldraw.'}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sadFaceIcon = (
|
||||||
|
<img width={36} height={36} src="/404-Sad-tldraw.svg" loading="lazy" role="presentation" />
|
||||||
|
)
|
||||||
|
|
||||||
|
export function ErrorPage({
|
||||||
|
messages,
|
||||||
|
icon = sadFaceIcon,
|
||||||
|
cta = <GoBackLink />,
|
||||||
|
}: {
|
||||||
|
icon?: ReactNode
|
||||||
|
messages: { header: string; para1: string; para2?: string }
|
||||||
|
cta?: ReactNode
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="error-page">
|
<div className="error-page">
|
||||||
<div className="error-page__container">
|
<div className="error-page__container">
|
||||||
{icon && (
|
{icon}
|
||||||
<img width={36} height={36} alt={'Not found'} src="/404-Sad-tldraw.svg" loading="lazy" />
|
|
||||||
)}
|
|
||||||
<div className="error-page__content">
|
<div className="error-page__content">
|
||||||
<h1>{messages.header}</h1>
|
<h1>{messages.header}</h1>
|
||||||
<p>{messages.para1}</p>
|
<p>{messages.para1}</p>
|
||||||
{messages.para2 && <p>{messages.para2}</p>}
|
{messages.para2 && <p>{messages.para2}</p>}
|
||||||
</div>
|
</div>
|
||||||
<Link to={'/'} target={inIframe ? '_blank' : '_self'}>
|
{cta}
|
||||||
{inIframe ? 'Open tldraw.' : 'Back to tldraw.'}
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,17 +1,33 @@
|
||||||
import { TLIncompatibilityReason } from '@tldraw/tlsync'
|
import { TLIncompatibilityReason } from '@tldraw/tlsync'
|
||||||
import { exhaustiveSwitchError } from 'tldraw'
|
import { exhaustiveSwitchError } from 'tldraw'
|
||||||
|
import 'tldraw/tldraw.css'
|
||||||
import { RemoteSyncError } from '../utils/remote-sync/remote-sync'
|
import { RemoteSyncError } from '../utils/remote-sync/remote-sync'
|
||||||
import { ErrorPage } from './ErrorPage/ErrorPage'
|
import { ErrorPage } from './ErrorPage/ErrorPage'
|
||||||
|
|
||||||
export function StoreErrorScreen({ error }: { error: Error }) {
|
export function StoreErrorScreen({ error }: { error: Error }) {
|
||||||
let header = 'Could not connect to server.'
|
let header = 'Could not connect to server.'
|
||||||
let message = ''
|
let message = ''
|
||||||
|
|
||||||
if (error instanceof RemoteSyncError) {
|
if (error instanceof RemoteSyncError) {
|
||||||
switch (error.reason) {
|
switch (error.reason) {
|
||||||
case TLIncompatibilityReason.ClientTooOld: {
|
case TLIncompatibilityReason.ClientTooOld: {
|
||||||
message = 'This client is out of date. Please refresh the page.'
|
return (
|
||||||
break
|
<ErrorPage
|
||||||
|
icon={
|
||||||
|
<img
|
||||||
|
width={36}
|
||||||
|
height={36}
|
||||||
|
src="/tldraw-white-on-black.svg"
|
||||||
|
loading="lazy"
|
||||||
|
role="presentation"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
messages={{
|
||||||
|
header: 'Refresh the page',
|
||||||
|
para1: 'You need to update to the latest version of tldraw to continue.',
|
||||||
|
}}
|
||||||
|
cta={<button onClick={() => window.location.reload()}>Refresh</button>}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case TLIncompatibilityReason.ServerTooOld: {
|
case TLIncompatibilityReason.ServerTooOld: {
|
||||||
message =
|
message =
|
||||||
|
@ -38,5 +54,5 @@ export function StoreErrorScreen({ error }: { error: Error }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ErrorPage icon messages={{ header, para1: message }} />
|
return <ErrorPage messages={{ header, para1: message }} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,11 @@ export function useRemoteSyncClient(opts: UseSyncClientConfig): RemoteTLStoreWit
|
||||||
|
|
||||||
const store = useTLStore({ schema })
|
const store = useTLStore({ schema })
|
||||||
|
|
||||||
|
const error: NonNullable<typeof state>['error'] = state?.error ?? undefined
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (error) return
|
||||||
|
|
||||||
const userPreferences = computed<{ id: string; color: string; name: string }>(
|
const userPreferences = computed<{ id: string; color: string; name: string }>(
|
||||||
'userPreferences',
|
'userPreferences',
|
||||||
() => {
|
() => {
|
||||||
|
@ -107,7 +111,7 @@ export function useRemoteSyncClient(opts: UseSyncClientConfig): RemoteTLStoreWit
|
||||||
client.close()
|
client.close()
|
||||||
socket.close()
|
socket.close()
|
||||||
}
|
}
|
||||||
}, [prefs, roomId, store, uri])
|
}, [prefs, roomId, store, uri, error])
|
||||||
|
|
||||||
return useValue<RemoteTLStoreWithStatus>(
|
return useValue<RemoteTLStoreWithStatus>(
|
||||||
'remote synced store',
|
'remote synced store',
|
||||||
|
|
|
@ -28,7 +28,6 @@ export function Component() {
|
||||||
if (!result || !result.timestamp)
|
if (!result || !result.timestamp)
|
||||||
return (
|
return (
|
||||||
<ErrorPage
|
<ErrorPage
|
||||||
icon
|
|
||||||
messages={{
|
messages={{
|
||||||
header: 'Page not found',
|
header: 'Page not found',
|
||||||
para1: 'The page you are looking does not exist or has been moved.',
|
para1: 'The page you are looking does not exist or has been moved.',
|
||||||
|
|
|
@ -25,7 +25,6 @@ export function Component() {
|
||||||
if (!data)
|
if (!data)
|
||||||
return (
|
return (
|
||||||
<ErrorPage
|
<ErrorPage
|
||||||
icon
|
|
||||||
messages={{
|
messages={{
|
||||||
header: 'Page not found',
|
header: 'Page not found',
|
||||||
para1: 'The page you are looking does not exist or has been moved.',
|
para1: 'The page you are looking does not exist or has been moved.',
|
||||||
|
|
|
@ -29,7 +29,6 @@ export function Component() {
|
||||||
if (!data)
|
if (!data)
|
||||||
return (
|
return (
|
||||||
<ErrorPage
|
<ErrorPage
|
||||||
icon
|
|
||||||
messages={{
|
messages={{
|
||||||
header: 'Page not found',
|
header: 'Page not found',
|
||||||
para1: 'The page you are looking does not exist or has been moved.',
|
para1: 'The page you are looking does not exist or has been moved.',
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { ErrorPage } from '../components/ErrorPage/ErrorPage'
|
||||||
export function Component() {
|
export function Component() {
|
||||||
return (
|
return (
|
||||||
<ErrorPage
|
<ErrorPage
|
||||||
icon
|
|
||||||
messages={{
|
messages={{
|
||||||
header: 'Page not found',
|
header: 'Page not found',
|
||||||
para1: 'The page you are looking does not exist or has been moved.',
|
para1: 'The page you are looking does not exist or has been moved.',
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { TLSocketClientSentEvent, TLSYNC_PROTOCOL_VERSION } from '@tldraw/tlsync'
|
import { TLSocketClientSentEvent, getTlsyncProtocolVersion } from '@tldraw/tlsync'
|
||||||
import { TLRecord } from 'tldraw'
|
import { TLRecord } from 'tldraw'
|
||||||
import { ClientWebSocketAdapter, INACTIVE_MIN_DELAY } from './ClientWebSocketAdapter'
|
import { ClientWebSocketAdapter, INACTIVE_MIN_DELAY } from './ClientWebSocketAdapter'
|
||||||
// NOTE: there is a hack in apps/dotcom/jestResolver.js to make this import work
|
// NOTE: there is a hack in apps/dotcom/jestResolver.js to make this import work
|
||||||
|
@ -141,7 +141,7 @@ describe(ClientWebSocketAdapter, () => {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
schema: { schemaVersion: 1, storeVersion: 0, recordVersions: {} },
|
schema: { schemaVersion: 1, storeVersion: 0, recordVersions: {} },
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
lastServerClock: 0,
|
lastServerClock: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ export {
|
||||||
} from './lib/diff'
|
} from './lib/diff'
|
||||||
export {
|
export {
|
||||||
TLIncompatibilityReason,
|
TLIncompatibilityReason,
|
||||||
TLSYNC_PROTOCOL_VERSION,
|
getTlsyncProtocolVersion,
|
||||||
type TLConnectRequest,
|
type TLConnectRequest,
|
||||||
type TLPingRequest,
|
type TLPingRequest,
|
||||||
type TLPushRequest,
|
type TLPushRequest,
|
||||||
|
|
|
@ -33,7 +33,6 @@ export type RoomSession<R extends UnknownRecord> =
|
||||||
state: typeof RoomSessionState.Connected
|
state: typeof RoomSessionState.Connected
|
||||||
sessionKey: string
|
sessionKey: string
|
||||||
presenceId: string
|
presenceId: string
|
||||||
isV4Client: boolean
|
|
||||||
socket: TLRoomSocket<R>
|
socket: TLRoomSocket<R>
|
||||||
serializedSchema: SerializedSchema
|
serializedSchema: SerializedSchema
|
||||||
lastInteractionTime: number
|
lastInteractionTime: number
|
||||||
|
|
|
@ -15,10 +15,10 @@ import { interval } from './interval'
|
||||||
import {
|
import {
|
||||||
TLIncompatibilityReason,
|
TLIncompatibilityReason,
|
||||||
TLPushRequest,
|
TLPushRequest,
|
||||||
TLSYNC_PROTOCOL_VERSION,
|
|
||||||
TLSocketClientSentEvent,
|
TLSocketClientSentEvent,
|
||||||
TLSocketServerSentDataEvent,
|
TLSocketServerSentDataEvent,
|
||||||
TLSocketServerSentEvent,
|
TLSocketServerSentEvent,
|
||||||
|
getTlsyncProtocolVersion,
|
||||||
} from './protocol'
|
} from './protocol'
|
||||||
import './requestAnimationFrame.polyfill'
|
import './requestAnimationFrame.polyfill'
|
||||||
|
|
||||||
|
@ -272,7 +272,7 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
connectRequestId: this.latestConnectRequestId,
|
connectRequestId: this.latestConnectRequestId,
|
||||||
schema: this.store.schema.serialize(),
|
schema: this.store.schema.serialize(),
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
lastServerClock: this.lastServerClock,
|
lastServerClock: this.lastServerClock,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,10 +41,10 @@ import {
|
||||||
import { interval } from './interval'
|
import { interval } from './interval'
|
||||||
import {
|
import {
|
||||||
TLIncompatibilityReason,
|
TLIncompatibilityReason,
|
||||||
TLSYNC_PROTOCOL_VERSION,
|
|
||||||
TLSocketClientSentEvent,
|
TLSocketClientSentEvent,
|
||||||
TLSocketServerSentDataEvent,
|
TLSocketServerSentDataEvent,
|
||||||
TLSocketServerSentEvent,
|
TLSocketServerSentEvent,
|
||||||
|
getTlsyncProtocolVersion,
|
||||||
} from './protocol'
|
} from './protocol'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -420,9 +420,7 @@ export class TLSyncRoom<R extends UnknownRecord> {
|
||||||
} else {
|
} else {
|
||||||
if (session.debounceTimer === null) {
|
if (session.debounceTimer === null) {
|
||||||
// this is the first message since the last flush, don't delay it
|
// this is the first message since the last flush, don't delay it
|
||||||
session.socket.sendMessage(
|
session.socket.sendMessage({ type: 'data', data: [message] })
|
||||||
session.isV4Client ? message : { type: 'data', data: [message] }
|
|
||||||
)
|
|
||||||
|
|
||||||
session.debounceTimer = setTimeout(
|
session.debounceTimer = setTimeout(
|
||||||
() => this._flushDataMessages(sessionKey),
|
() => this._flushDataMessages(sessionKey),
|
||||||
|
@ -449,14 +447,7 @@ export class TLSyncRoom<R extends UnknownRecord> {
|
||||||
session.debounceTimer = null
|
session.debounceTimer = null
|
||||||
|
|
||||||
if (session.outstandingDataMessages.length > 0) {
|
if (session.outstandingDataMessages.length > 0) {
|
||||||
if (session.isV4Client) {
|
|
||||||
// v4 clients don't support the "data" message, so we need to send each message separately
|
|
||||||
for (const message of session.outstandingDataMessages) {
|
|
||||||
session.socket.sendMessage(message)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
session.socket.sendMessage({ type: 'data', data: session.outstandingDataMessages })
|
session.socket.sendMessage({ type: 'data', data: session.outstandingDataMessages })
|
||||||
}
|
|
||||||
session.outstandingDataMessages.length = 0
|
session.outstandingDataMessages.length = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -678,14 +669,15 @@ export class TLSyncRoom<R extends UnknownRecord> {
|
||||||
// if the protocol versions don't match, disconnect the client
|
// if the protocol versions don't match, disconnect the client
|
||||||
// we will eventually want to try to make our protocol backwards compatible to some degree
|
// we will eventually want to try to make our protocol backwards compatible to some degree
|
||||||
// and have a MIN_PROTOCOL_VERSION constant that the TLSyncRoom implements support for
|
// and have a MIN_PROTOCOL_VERSION constant that the TLSyncRoom implements support for
|
||||||
const isV4Client = message.protocolVersion === 4 && TLSYNC_PROTOCOL_VERSION === 5
|
let theirProtocolVersion = message.protocolVersion
|
||||||
if (
|
// 5 is the same as 6
|
||||||
message.protocolVersion == null ||
|
if (theirProtocolVersion === 5) {
|
||||||
(message.protocolVersion < TLSYNC_PROTOCOL_VERSION && !isV4Client)
|
theirProtocolVersion = 6
|
||||||
) {
|
}
|
||||||
|
if (theirProtocolVersion == null || theirProtocolVersion < getTlsyncProtocolVersion()) {
|
||||||
this.rejectSession(session, TLIncompatibilityReason.ClientTooOld)
|
this.rejectSession(session, TLIncompatibilityReason.ClientTooOld)
|
||||||
return
|
return
|
||||||
} else if (message.protocolVersion > TLSYNC_PROTOCOL_VERSION) {
|
} else if (theirProtocolVersion > getTlsyncProtocolVersion()) {
|
||||||
this.rejectSession(session, TLIncompatibilityReason.ServerTooOld)
|
this.rejectSession(session, TLIncompatibilityReason.ServerTooOld)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -711,7 +703,6 @@ export class TLSyncRoom<R extends UnknownRecord> {
|
||||||
state: RoomSessionState.Connected,
|
state: RoomSessionState.Connected,
|
||||||
sessionKey: session.sessionKey,
|
sessionKey: session.sessionKey,
|
||||||
presenceId: session.presenceId,
|
presenceId: session.presenceId,
|
||||||
isV4Client,
|
|
||||||
socket: session.socket,
|
socket: session.socket,
|
||||||
serializedSchema: sessionSchema,
|
serializedSchema: sessionSchema,
|
||||||
lastInteractionTime: Date.now(),
|
lastInteractionTime: Date.now(),
|
||||||
|
@ -751,7 +742,7 @@ export class TLSyncRoom<R extends UnknownRecord> {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
connectRequestId: message.connectRequestId,
|
connectRequestId: message.connectRequestId,
|
||||||
hydrationType: 'wipe_all',
|
hydrationType: 'wipe_all',
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
schema: this.schema.serialize(),
|
schema: this.schema.serialize(),
|
||||||
serverClock: this.clock,
|
serverClock: this.clock,
|
||||||
diff: migrated.value,
|
diff: migrated.value,
|
||||||
|
@ -797,7 +788,7 @@ export class TLSyncRoom<R extends UnknownRecord> {
|
||||||
connectRequestId: message.connectRequestId,
|
connectRequestId: message.connectRequestId,
|
||||||
hydrationType: 'wipe_presence',
|
hydrationType: 'wipe_presence',
|
||||||
schema: this.schema.serialize(),
|
schema: this.schema.serialize(),
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
serverClock: this.clock,
|
serverClock: this.clock,
|
||||||
diff: migrated.value,
|
diff: migrated.value,
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,7 +2,11 @@ import { SerializedSchema, UnknownRecord } from '@tldraw/store'
|
||||||
import { NetworkDiff, ObjectDiff, RecordOpType } from './diff'
|
import { NetworkDiff, ObjectDiff, RecordOpType } from './diff'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const TLSYNC_PROTOCOL_VERSION = 5
|
const TLSYNC_PROTOCOL_VERSION = 6
|
||||||
|
|
||||||
|
export function getTlsyncProtocolVersion() {
|
||||||
|
return TLSYNC_PROTOCOL_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const TLIncompatibilityReason = {
|
export const TLIncompatibilityReason = {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { DBLoadResult, TLServer } from '../lib/TLServer'
|
||||||
import { RoomSnapshot } from '../lib/TLSyncRoom'
|
import { RoomSnapshot } from '../lib/TLSyncRoom'
|
||||||
import { chunk } from '../lib/chunk'
|
import { chunk } from '../lib/chunk'
|
||||||
import { RecordOpType } from '../lib/diff'
|
import { RecordOpType } from '../lib/diff'
|
||||||
import { TLSYNC_PROTOCOL_VERSION, TLSocketClientSentEvent } from '../lib/protocol'
|
import { TLSocketClientSentEvent, getTlsyncProtocolVersion } from '../lib/protocol'
|
||||||
import { RoomState } from '../lib/server-types'
|
import { RoomState } from '../lib/server-types'
|
||||||
|
|
||||||
// Because we are using jsdom in this package, jest tries to load the 'browser' version of the ws library
|
// Because we are using jsdom in this package, jest tries to load the 'browser' version of the ws library
|
||||||
|
@ -145,7 +145,7 @@ describe('TLServer', () => {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
lastServerClock: 0,
|
lastServerClock: 0,
|
||||||
connectRequestId: 'test-connect-request-id',
|
connectRequestId: 'test-connect-request-id',
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
schema,
|
schema,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,12 +15,24 @@ import { RoomSnapshot, TLRoomSocket } from '../lib/TLSyncRoom'
|
||||||
import { RecordOpType, ValueOpType } from '../lib/diff'
|
import { RecordOpType, ValueOpType } from '../lib/diff'
|
||||||
import {
|
import {
|
||||||
TLIncompatibilityReason,
|
TLIncompatibilityReason,
|
||||||
TLSYNC_PROTOCOL_VERSION,
|
|
||||||
TLSocketServerSentEvent,
|
TLSocketServerSentEvent,
|
||||||
|
getTlsyncProtocolVersion,
|
||||||
} from '../lib/protocol'
|
} from '../lib/protocol'
|
||||||
import { TestServer } from './TestServer'
|
import { TestServer } from './TestServer'
|
||||||
import { TestSocketPair } from './TestSocketPair'
|
import { TestSocketPair } from './TestSocketPair'
|
||||||
|
|
||||||
|
const actualProtocol = jest.requireActual('../lib/protocol')
|
||||||
|
|
||||||
|
jest.mock('../lib/protocol', () => {
|
||||||
|
const actual = jest.requireActual('../lib/protocol')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
getTlsyncProtocolVersion: jest.fn(actual.getTlsyncProtocolVersion),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockGetTlsyncProtocolVersion = getTlsyncProtocolVersion as jest.Mock
|
||||||
|
|
||||||
function mockSocket<R extends UnknownRecord>(): TLRoomSocket<R> {
|
function mockSocket<R extends UnknownRecord>(): TLRoomSocket<R> {
|
||||||
return {
|
return {
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
|
@ -328,7 +340,7 @@ test('clients will receive updates from a snapshot migration upon connection', (
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
lastServerClock: snapshot.clock,
|
lastServerClock: snapshot.clock,
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
schema: schemaV3.serialize(),
|
schema: schemaV3.serialize(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -352,7 +364,7 @@ test('out-of-date clients will receive incompatibility errors', () => {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
lastServerClock: 0,
|
lastServerClock: 0,
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
schema: schemaV2.serialize(),
|
schema: schemaV2.serialize(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -363,6 +375,9 @@ test('out-of-date clients will receive incompatibility errors', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('clients using an out-of-date protocol will receive compatibility errors', () => {
|
test('clients using an out-of-date protocol will receive compatibility errors', () => {
|
||||||
|
const actualVersion = getTlsyncProtocolVersion()
|
||||||
|
mockGetTlsyncProtocolVersion.mockReturnValue(actualVersion + 1)
|
||||||
|
try {
|
||||||
const v2server = new TestServer(schemaV2)
|
const v2server = new TestServer(schemaV2)
|
||||||
|
|
||||||
const id = 'test_upgrade_v3'
|
const id = 'test_upgrade_v3'
|
||||||
|
@ -373,7 +388,7 @@ test('clients using an out-of-date protocol will receive compatibility errors',
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
lastServerClock: 0,
|
lastServerClock: 0,
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION - 2,
|
protocolVersion: actualVersion,
|
||||||
schema: schemaV2.serialize(),
|
schema: schemaV2.serialize(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -381,6 +396,45 @@ test('clients using an out-of-date protocol will receive compatibility errors',
|
||||||
type: 'incompatibility_error',
|
type: 'incompatibility_error',
|
||||||
reason: TLIncompatibilityReason.ClientTooOld,
|
reason: TLIncompatibilityReason.ClientTooOld,
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
mockGetTlsyncProtocolVersion.mockReset()
|
||||||
|
mockGetTlsyncProtocolVersion.mockImplementation(actualProtocol.getTlsyncProtocolVersion)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// this can be deleted when the protocol gets to v7
|
||||||
|
test('v5 special case should allow connections', () => {
|
||||||
|
const actualVersion = getTlsyncProtocolVersion()
|
||||||
|
if (actualVersion > 6) return
|
||||||
|
|
||||||
|
const v2server = new TestServer(schemaV2)
|
||||||
|
|
||||||
|
const id = 'test_upgrade_v3'
|
||||||
|
const socket = mockSocket()
|
||||||
|
|
||||||
|
v2server.room.handleNewSession(id, socket)
|
||||||
|
v2server.room.handleMessage(id, {
|
||||||
|
type: 'connect',
|
||||||
|
connectRequestId: 'test',
|
||||||
|
lastServerClock: 0,
|
||||||
|
protocolVersion: 5,
|
||||||
|
schema: schemaV2.serialize(),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(socket.sendMessage).toHaveBeenCalledWith({
|
||||||
|
connectRequestId: 'test',
|
||||||
|
diff: {},
|
||||||
|
hydrationType: 'wipe_all',
|
||||||
|
protocolVersion: 6,
|
||||||
|
schema: {
|
||||||
|
schemaVersion: 2,
|
||||||
|
sequences: {
|
||||||
|
'com.tldraw.user': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serverClock: 1,
|
||||||
|
type: 'connect',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('clients using a too-new protocol will receive compatibility errors', () => {
|
test('clients using a too-new protocol will receive compatibility errors', () => {
|
||||||
|
@ -394,7 +448,7 @@ test('clients using a too-new protocol will receive compatibility errors', () =>
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
lastServerClock: 0,
|
lastServerClock: 0,
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION + 1,
|
protocolVersion: getTlsyncProtocolVersion() + 1,
|
||||||
schema: schemaV2.serialize(),
|
schema: schemaV2.serialize(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -436,7 +490,7 @@ test('when the client is too new it cannot connect', () => {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
lastServerClock: 10,
|
lastServerClock: 10,
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
schema: schemaV2.serialize(),
|
schema: schemaV2.serialize(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -496,7 +550,7 @@ describe('when the client is too old', () => {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
lastServerClock: 10,
|
lastServerClock: 10,
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
schema: schemaV1.serialize(),
|
schema: schemaV1.serialize(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -505,7 +559,7 @@ describe('when the client is too old', () => {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
lastServerClock: 10,
|
lastServerClock: 10,
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
schema: schemaV2.serialize(),
|
schema: schemaV2.serialize(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -514,7 +568,7 @@ describe('when the client is too old', () => {
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
hydrationType: 'wipe_presence',
|
hydrationType: 'wipe_presence',
|
||||||
diff: {},
|
diff: {},
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
schema: schemaV2.serialize(),
|
schema: schemaV2.serialize(),
|
||||||
serverClock: 10,
|
serverClock: 10,
|
||||||
} satisfies TLSocketServerSentEvent<RV2>)
|
} satisfies TLSocketServerSentEvent<RV2>)
|
||||||
|
@ -524,7 +578,7 @@ describe('when the client is too old', () => {
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
hydrationType: 'wipe_presence',
|
hydrationType: 'wipe_presence',
|
||||||
diff: {},
|
diff: {},
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
schema: schemaV2.serialize(),
|
schema: schemaV2.serialize(),
|
||||||
serverClock: 10,
|
serverClock: 10,
|
||||||
} satisfies TLSocketServerSentEvent<RV2>)
|
} satisfies TLSocketServerSentEvent<RV2>)
|
||||||
|
@ -643,7 +697,7 @@ describe('when the client is the same version', () => {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
lastServerClock: 10,
|
lastServerClock: 10,
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
schema: JSON.parse(JSON.stringify(schemaV2.serialize())),
|
schema: JSON.parse(JSON.stringify(schemaV2.serialize())),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -652,7 +706,7 @@ describe('when the client is the same version', () => {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
lastServerClock: 10,
|
lastServerClock: 10,
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
schema: JSON.parse(JSON.stringify(schemaV2.serialize())),
|
schema: JSON.parse(JSON.stringify(schemaV2.serialize())),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -661,7 +715,7 @@ describe('when the client is the same version', () => {
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
hydrationType: 'wipe_presence',
|
hydrationType: 'wipe_presence',
|
||||||
diff: {},
|
diff: {},
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
schema: schemaV2.serialize(),
|
schema: schemaV2.serialize(),
|
||||||
serverClock: 10,
|
serverClock: 10,
|
||||||
} satisfies TLSocketServerSentEvent<RV2>)
|
} satisfies TLSocketServerSentEvent<RV2>)
|
||||||
|
@ -671,7 +725,7 @@ describe('when the client is the same version', () => {
|
||||||
connectRequestId: 'test',
|
connectRequestId: 'test',
|
||||||
hydrationType: 'wipe_presence',
|
hydrationType: 'wipe_presence',
|
||||||
diff: {},
|
diff: {},
|
||||||
protocolVersion: TLSYNC_PROTOCOL_VERSION,
|
protocolVersion: getTlsyncProtocolVersion(),
|
||||||
schema: schemaV2.serialize(),
|
schema: schemaV2.serialize(),
|
||||||
serverClock: 10,
|
serverClock: 10,
|
||||||
} satisfies TLSocketServerSentEvent<RV2>)
|
} satisfies TLSocketServerSentEvent<RV2>)
|
||||||
|
|
Loading…
Reference in a new issue