sync error: INVALID_RECORD
Hey, I ran into an error while using tldraw:

```js
RemoteSyncError: sync error: INVALID_RECORD
    at Object.onSyncError (http://localhost:5174/node_modules/.vite/deps/@tldraw_sync.js?v=abcaf92d:2530:27)
    at http://localhost:5174/node_modules/.vite/deps/@tldraw_sync.js?v=abcaf92d:524:20
    at http://localhost:5174/node_modules/.vite/deps/@tldraw_sync.js?v=abcaf92d:863:17
    at Set.forEach (<anonymous>)
    at ClientWebSocketAdapter._handleDisconnect (http://localhost:5174/node_modules/.vite/deps/@tldraw_sync.js?v=abcaf92d:862:28)
    at ws.onclose (http://localhost:5174/node_modules/.vite/deps/@tldraw_sync.js?v=abcaf92d:888:14)
```

My browser: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36

After uploading the picture prompts this error, the server used is bun:

> package.json
```json
{
  "name": "gempoll-tldraw-server",
  "version": "1.0.0",
  "description": "",
  "main": "./dist/main.js",
  "scripts": {
    "dev": "bun --watch src/server.bun.ts",
    "dev:node": "tsx watch src/server.node.ts",
    "build": "tsc --project tsconfig.json",
    "pm2": "pm2 start ecosystem.config.js --env production",
    "pm2-restart": "pm2 restart ecosystem.config.js --env production",
    "start": "bun dist/server.bun.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@fastify/cors": "^11.0.0",
    "@fastify/websocket": "^11.0.2",
    "@tldraw/sync-core": "^3.8.1",
    "fastify": "^5.2.1",
    "itty-router": "^5.0.18",
    "pm2": "^5.4.3",
    "unfurl.js": "^6.4.0"
  },
  "devDependencies": {
    "@types/bun": "^1.2.4",
    "@types/node": "^22.13.8",
    "bun": "^1.2.4",
    "tsx": "^4.19.3",
    "typescript": "^5.8.2"
  }
}
```

> server.bun.ts
>> Other files and official website demo one to

```typescript
import { TLSocketRoom } from '@tldraw/sync-core'
import { IRequest, Router, RouterType, cors, json } from 'itty-router'
import { getRooms, makeOrLoadRoom } from './rooms'
import { unfurl } from './unfurl'
const PORT = 5959

// For this example we use Bun.serve and a basic router to handle requests
// To keep things simple we're skipping normal production concerns like rate limiting and input validation.

const { corsify, preflight } = cors({ origin: '*' })

const router: RouterType<IRequest, any, any> = Router()
  .all('*', preflight)

  // This is the main entrypoint for the multiplayer sync
  .get('/connect/:roomId', async (req) => {
    // The roomId comes from the URL pathname
    const roomId = req.params.roomId
    // The sessionId is passed from the client as a query param,
    // you need to extract it and pass it to the room.
    const sessionId = req.query.sessionId
    // Now we pass the params to the upgrade function so that
    // when the socket connects, it can be associated with the correct room
    // and session.
    server.upgrade(req, { data: { roomId, sessionId } })
    return new Response(null, { status: 101 })
  })

  // To enable unfurling of bookmarks, we add a simple endpoint that takes a URL query param
  .get('/unfurl', async (req) => {
    const url = (req.query as any).url as string
    return json(await unfurl(url))
  })

  // 获取所有rooms
  .get('/rooms', async (_req, res) => {
    return json(await getRooms())
  })

  .all('*', () => {
    new Response('Not found', { status: 404 })
  })

const server = Bun.serve<{ room?: TLSocketRoom<any, void>; sessionId: string; roomId: string }, Record<string, never>>({
  port: PORT,
  fetch(req) {
    try {
      return router.fetch(req).then(corsify)
    } catch (e) {
      console.error(e)
      return new Response('Something went wrong', {
        status: 500
      })
    }
  },
  websocket: {
    async open(socket) {
      // get the params extracted from the URL in the GET /connect/:roomId handler above
      const { sessionId, roomId } = socket.data

      // Here we make or get an existing instance of TLSocketRoom for the given roomId
      const room = await makeOrLoadRoom(roomId)

      // and finally connect the socket to the room
      room.handleSocketConnect({ sessionId, socket })
      // store the room on the socket so we can access it easily later
      socket.data.room = room
    },
    async message(ws, message) {
      try {
        const jsonMessage = JSON.parse(message as string)
        if (jsonMessage.type !== 'push') {
          console.log('message', jsonMessage)
        }
      } catch (error) {
        console.log('error:', error)
      }

      // pass the message along to the room
      ws.data.room?.handleSocketMessage(ws.data.sessionId, message)
    },
    drain(ws) {
      // If the socket was was overloaded with backpressure, let's just close it
      // and let the client reconnect and send all the data again.
      ws.close()
    },
    close(ws) {
      // let the room know the socket has closed
      ws.data.room?.handleSocketClose(ws.data.sessionId)
    }
  }
})

console.log(`Listening on localhost:${server.port}`)

```

The client uses the following：
```
import 'tldraw/tldraw.css'
import { useGempoll } from '@/store'
import { Gldraw, CustomMenuPanel, CustomStylePanel, CustomSharePanel, customShapes, customEmbeds, defaultShapeUtils } from '@/gl-editor'
import { CustomActions, PromptPanel, SideBar } from '@/components'
import { useSync } from '@tldraw/sync'
import { AssetRecordType, getHashForString, TLAssetStore, TLBookmarkAsset } from 'tldraw'
export type GlEditorProps = {
  workerUrl: string
  roomId: string
}
export const GlEditor = ({ workerUrl, roomId }: GlEditorProps) => {
  const { updateState } = useGempoll()

  useEffect(() => {
    updateState({ roomId })
  }, [roomId])

  const createGlAssetStore: TLAssetStore = useMemo(() => {
    return {
      async upload(_asset, file) {
        try {
          const formData = new FormData()
          formData.append('files', file)
          // Uploading to the minio server will return the image network link address
          const res = await api.gempoll.imageUploadOss(formData)
          return { src: res.data[0] }
        } catch (error) {
          throw new Error(error instanceof Error ? error.message : 'fail to upload')
        }
      },
      resolve(asset) {
        return asset.props.src
      }
    }
  }, [])

  const unfurlBookmarkUrl = async ({ url }: { url: string }): Promise<TLBookmarkAsset> => {
    const asset: TLBookmarkAsset = {
      id: AssetRecordType.createId(getHashForString(url)),
      typeName: 'asset',
      type: 'bookmark',
      meta: {},
      props: {
        src: url,
        description: '',
        image: '',
        favicon: '',
        title: ''
      }
    }

    try {
      const response = await fetch(`${workerUrl}/unfurl?url=${encodeURIComponent(url)}`)
      const data = await response.json()
      console.log(data)

      asset.props.description = data?.description ?? ''
      asset.props.image = data?.image ?? ''
      asset.props.favicon = data?.favicon ?? ''
      asset.props.title = data?.title ?? ''
    } catch (e) {
      console.error(e)
    }

    return asset
  }

  const store = useSync({
    uri: `${workerUrl}/connect/${roomId}`,
    assets: createGlAssetStore,
    shapeUtils: useMemo(() => [...defaultShapeUtils, ...customShapes], [])
  })

  return (
    <div className="relative flex-1 h-full overflow-hidden">
      <Gldraw
        store={store}
        inferDarkMode
        options={{
          defaultSvgPadding: 0
        }}
        shapeUtils={customShapes}
        maxAssetSize={50 * 1024 * 1024}
        components={{
          SharePanel: () => <CustomSharePanel roomId={roomId} />,
          Toolbar: () => null,
          MenuPanel: () => <CustomMenuPanel />,
          StylePanel: () => <CustomStylePanel />,
          InFrontOfTheCanvas: CustomActions
        }}
        embeds={customEmbeds}
        onMount={(editor) => {
          updateState({ editor })
          // Adaptive scaling: If the shape is selected, the shape is scaled, otherwise the entire page is scaled
          if (editor.getSelectedShapeIds().length > 0) {
            editor.zoomToSelection({ animation: { duration: editor?.options.animationMediumMs } })
          } else {
            editor.zoomToFit({ animation: { duration: editor?.options.animationMediumMs } })
          }
          if (import.meta.env.VITE_DEBUG_VIEW) {
            window.editor = editor
          }
          editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
        }}
      ></Gldraw>
      <PromptPanel />
      <SideBar />
    </div>
  )
}

```

Services running under dev environment cloned from the official repository will not report errors if connected directly：http://localhost:8989

<img width="687" alt="Image" src="https://github.com/user-attachments/assets/da82a188-4263-4a9b-a0cd-1591c900cc5a" />