snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
3
templates/socketio-server-example/.gitignore
vendored
Normal file
3
templates/socketio-server-example/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.rooms/
|
||||
.assets/
|
||||
.yarn
|
||||
33
templates/socketio-server-example/README.md
Normal file
33
templates/socketio-server-example/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# tldraw sync, custom transport example
|
||||
|
||||
This is a simple example of a backend for [tldraw sync](https://tldraw.dev/docs/sync) hosted on Node, with a custom socket.io transport instead of our default websocket-based one.
|
||||
|
||||
Run `yarn dev` in this folder to start the server + client.
|
||||
|
||||
Room data is automatically persisted to SQLite databases in the `.rooms` directory using `SQLiteSyncStorage`.
|
||||
|
||||
For a production-ready example specific to Cloudflare, see /templates/sync-cloudflare.
|
||||
|
||||
## License
|
||||
|
||||
This project is provided under the MIT license found [here](https://github.com/tldraw/tldraw/blob/main/apps/simple-server-example/LICENSE.md). The tldraw SDK is provided under the [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md).
|
||||
|
||||
## Trademarks
|
||||
|
||||
Copyright (c) 2024-present tldraw Inc. The tldraw name and logo are trademarks of tldraw. Please see our [trademark guidelines](https://github.com/tldraw/tldraw/blob/main/TRADEMARKS.md) for info on acceptable usage.
|
||||
|
||||
## Distributions
|
||||
|
||||
You can find tldraw on npm [here](https://www.npmjs.com/package/@tldraw/tldraw?activeTab=versions).
|
||||
|
||||
## Contribution
|
||||
|
||||
Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
|
||||
|
||||
## Community
|
||||
|
||||
Have questions, comments or feedback? [Join our discord](https://discord.tldraw.com/?utm_source=github&utm_medium=readme&utm_campaign=sociallink). For the latest news and release notes, visit [tldraw.dev](https://tldraw.dev).
|
||||
|
||||
## Contact
|
||||
|
||||
Find us on Twitter/X at [@tldraw](https://twitter.com/tldraw).
|
||||
47
templates/socketio-server-example/package.json
Normal file
47
templates/socketio-server-example/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@tldraw/socketio-server-example",
|
||||
"description": "tldraw infinite canvas SDK (example socket.io server).",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "tldraw GB Ltd.",
|
||||
"email": "hello@tldraw.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "./src/server/server.ts",
|
||||
"scripts": {
|
||||
"dev": "concurrently -n server,client -c red,blue \"yarn dev-server\" \"yarn dev-client\"",
|
||||
"dev-server": "yarn run -T tsx watch ./src/server/server.ts",
|
||||
"dev-client": "vite dev --host",
|
||||
"test-ci": "echo 'No tests yet'",
|
||||
"test": "yarn run -T vitest run --passWithNoTests",
|
||||
"test-coverage": "lazy inherit",
|
||||
"lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/ws": "^8.18.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tldraw/sync": "workspace:*",
|
||||
"@tldraw/sync-core": "workspace:*",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"better-sqlite3": "^12.10.0",
|
||||
"express": "^5.1.0",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-router-dom": "^6.28.2",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tldraw": "workspace:*",
|
||||
"unfurl.js": "^6.4.0",
|
||||
"vite": "^8.0.16"
|
||||
}
|
||||
}
|
||||
176
templates/socketio-server-example/src/client/App.tsx
Normal file
176
templates/socketio-server-example/src/client/App.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import { TLPersistentClientSocket, TLSocketStatusChangeEvent, useSync } from '@tldraw/sync'
|
||||
import { useCallback } from 'react'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
import {
|
||||
AssetRecordType,
|
||||
getHashForString,
|
||||
TLAssetStore,
|
||||
TLBookmarkAsset,
|
||||
Tldraw,
|
||||
TLRecord,
|
||||
uniqueId,
|
||||
} from 'tldraw'
|
||||
|
||||
const WORKER_URL = `http://localhost:5858`
|
||||
|
||||
// In this example, the room ID is hard-coded. You can set this however you like though.
|
||||
const roomId = 'test-room'
|
||||
|
||||
function App() {
|
||||
// Create a store connected to multiplayer.
|
||||
const store = useSync({
|
||||
// We need a connection to the server:
|
||||
connect: useCallback((query) => {
|
||||
const socket = io(WORKER_URL, {
|
||||
query: { ...query, roomId },
|
||||
})
|
||||
return socketIoToTldrawSocket(socket)
|
||||
}, []),
|
||||
// ...and how to handle static assets like images & videos
|
||||
assets: multiplayerAssets,
|
||||
})
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0 }}>
|
||||
<Tldraw
|
||||
// we can pass the connected store into the Tldraw component which will handle
|
||||
// loading states & enable multiplayer UX like cursors & a presence menu
|
||||
store={store}
|
||||
onMount={(editor) => {
|
||||
// @ts-expect-error
|
||||
window.editor = editor
|
||||
// when the editor is ready, we need to register out bookmark unfurling service
|
||||
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to convert Socket.IO to TLPersistentClientSocket
|
||||
function socketIoToTldrawSocket(ioSocket: Socket): TLPersistentClientSocket<TLRecord> {
|
||||
const statusChangeListeners = new Set<(event: TLSocketStatusChangeEvent) => void>()
|
||||
const tldrawSocket: TLPersistentClientSocket<TLRecord> = {
|
||||
connectionStatus: 'offline',
|
||||
|
||||
sendMessage: (message) => {
|
||||
console.log('📤 Sending:', message)
|
||||
ioSocket.emit('tldraw-message', JSON.stringify(message))
|
||||
},
|
||||
|
||||
onReceiveMessage: (callback) => {
|
||||
// Listen for tldraw sync protocol messages
|
||||
const handler = (message: any) => {
|
||||
console.log('📥 Received:', message)
|
||||
callback(message)
|
||||
}
|
||||
ioSocket.on('tldraw-message', handler)
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
ioSocket.off('tldraw-message', handler)
|
||||
}
|
||||
},
|
||||
|
||||
onStatusChange: (callback) => {
|
||||
statusChangeListeners.add(callback)
|
||||
return () => {
|
||||
statusChangeListeners.delete(callback)
|
||||
}
|
||||
},
|
||||
|
||||
restart: () => {
|
||||
console.log('🔄 Restarting Socket.IO connection...')
|
||||
ioSocket.disconnect()
|
||||
ioSocket.connect()
|
||||
},
|
||||
|
||||
close: () => {
|
||||
ioSocket.off('connect', connectHandler)
|
||||
ioSocket.off('disconnect', disconnectHandler)
|
||||
ioSocket.off('connect_error', errorHandler)
|
||||
clearTimeout(initialStatusTimeout)
|
||||
ioSocket.disconnect()
|
||||
},
|
||||
}
|
||||
|
||||
// Map Socket.IO events to TLPersistentClientSocket status
|
||||
const setStatus = (event: TLSocketStatusChangeEvent) => {
|
||||
tldrawSocket.connectionStatus = event.status
|
||||
statusChangeListeners.forEach((cb) => cb(event))
|
||||
}
|
||||
|
||||
const connectHandler = () => setStatus({ status: 'online' })
|
||||
const disconnectHandler = () => setStatus({ status: 'offline' })
|
||||
const errorHandler = (error: any) =>
|
||||
setStatus({ status: 'error', reason: error.message || 'Connection error' })
|
||||
|
||||
ioSocket.on('connect', connectHandler)
|
||||
ioSocket.on('disconnect', disconnectHandler)
|
||||
ioSocket.on('connect_error', errorHandler)
|
||||
|
||||
// Set initial status
|
||||
const initialStatusTimeout = setTimeout(() => {
|
||||
if (ioSocket.connected) connectHandler()
|
||||
}, 0)
|
||||
|
||||
return tldrawSocket
|
||||
}
|
||||
|
||||
// How does our server handle assets like images and videos?
|
||||
const multiplayerAssets: TLAssetStore = {
|
||||
// to upload an asset, we prefix it with a unique id, POST it to our worker, and return the URL
|
||||
async upload(_asset, file) {
|
||||
const objectName = `${uniqueId()}-${file.name}`
|
||||
const url = `${WORKER_URL}/uploads/${encodeURIComponent(objectName)}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload asset: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return { src: url }
|
||||
},
|
||||
// to retrieve an asset, we can just use the same URL. you could customize this to add extra
|
||||
// auth, or to serve optimized versions / sizes of the asset.
|
||||
resolve(asset) {
|
||||
return asset.props.src
|
||||
},
|
||||
}
|
||||
|
||||
// How does our server handle bookmark unfurling?
|
||||
async function unfurlBookmarkUrl({ 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(`${WORKER_URL}/unfurl?url=${encodeURIComponent(url)}`)
|
||||
const data = await response.json()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export default App
|
||||
7
templates/socketio-server-example/src/client/index.css
Normal file
7
templates/socketio-server-example/src/client/index.css
Normal file
@@ -0,0 +1,7 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;700&display=swap');
|
||||
@import url('tldraw/tldraw.css');
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
13
templates/socketio-server-example/src/client/index.html
Normal file
13
templates/socketio-server-example/src/client/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>tldraw node/bun server example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
<noscript>You need to enable JavaScript to run tldraw. ✌️</noscript>
|
||||
</body>
|
||||
</html>
|
||||
10
templates/socketio-server-example/src/client/main.tsx
Normal file
10
templates/socketio-server-example/src/client/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
1
templates/socketio-server-example/src/client/vite-env.d.ts
vendored
Normal file
1
templates/socketio-server-example/src/client/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
15
templates/socketio-server-example/src/server/assets.ts
Normal file
15
templates/socketio-server-example/src/server/assets.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { mkdir, readFile, writeFile } from 'fs/promises'
|
||||
import { join, resolve } from 'path'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
// We are just using the filesystem to store assets
|
||||
const DIR = resolve('./.assets')
|
||||
|
||||
export async function storeAsset(id: string, stream: Readable) {
|
||||
await mkdir(DIR, { recursive: true })
|
||||
await writeFile(join(DIR, id), stream)
|
||||
}
|
||||
|
||||
export async function loadAsset(id: string) {
|
||||
return await readFile(join(DIR, id))
|
||||
}
|
||||
48
templates/socketio-server-example/src/server/rooms.ts
Normal file
48
templates/socketio-server-example/src/server/rooms.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { mkdirSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { NodeSqliteWrapper, SQLiteSyncStorage, TLSocketRoom } from '@tldraw/sync-core'
|
||||
import Database from 'better-sqlite3'
|
||||
|
||||
// For this example we're saving data to a SQLite database on the local filesystem
|
||||
const DIR = './.rooms'
|
||||
mkdirSync(DIR, { recursive: true })
|
||||
|
||||
// Sanitize roomId to prevent path traversal attacks
|
||||
function sanitizeRoomId(roomId: string): string {
|
||||
return roomId.replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||
}
|
||||
|
||||
// We'll keep an in-memory map of active rooms
|
||||
const rooms = new Map<string, TLSocketRoom<any, void>>()
|
||||
|
||||
export function makeOrLoadRoom(roomId: string): TLSocketRoom<any, void> {
|
||||
roomId = sanitizeRoomId(roomId)
|
||||
|
||||
const existing = rooms.get(roomId)
|
||||
if (existing && !existing.isClosed()) {
|
||||
return existing
|
||||
}
|
||||
|
||||
console.log('loading room', roomId)
|
||||
|
||||
// Open the database - file is created if it doesn't exist
|
||||
const db = new Database(join(DIR, `${roomId}.db`))
|
||||
const sql = new NodeSqliteWrapper(db)
|
||||
const storage = new SQLiteSyncStorage({ sql })
|
||||
|
||||
const room = new TLSocketRoom({
|
||||
storage,
|
||||
onSessionRemoved(room, args) {
|
||||
console.log('client disconnected', args.sessionId, roomId)
|
||||
if (args.numSessionsRemaining === 0) {
|
||||
console.log('closing room', roomId)
|
||||
room.close()
|
||||
db.close()
|
||||
rooms.delete(roomId)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
rooms.set(roomId, room)
|
||||
return room
|
||||
}
|
||||
139
templates/socketio-server-example/src/server/server.ts
Normal file
139
templates/socketio-server-example/src/server/server.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { createServer } from 'http'
|
||||
import { WebSocketMinimal } from '@tldraw/sync-core'
|
||||
import express from 'express'
|
||||
import { Server } from 'socket.io'
|
||||
import { loadAsset, storeAsset } from './assets'
|
||||
import { makeOrLoadRoom } from './rooms'
|
||||
import { unfurl } from './unfurl'
|
||||
|
||||
const PORT = 5858
|
||||
|
||||
// For this example we use Socket.IO with Express
|
||||
// To keep things simple we're skipping normal production concerns like rate limiting and input validation.
|
||||
// Create Express app and HTTP server for Socket.IO
|
||||
const app = express()
|
||||
const server = createServer(app)
|
||||
|
||||
// Enable CORS and parse JSON
|
||||
app.use(express.json())
|
||||
app.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*')
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
|
||||
next()
|
||||
})
|
||||
|
||||
export interface ServerToClientMessage {
|
||||
'tldraw-message': (message: string) => void
|
||||
}
|
||||
|
||||
export interface ClientToServerMessage {
|
||||
'tldraw-message': (message: string) => void
|
||||
}
|
||||
|
||||
// Create Socket.IO server
|
||||
const io = new Server<ClientToServerMessage, ServerToClientMessage>(server, {
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST'],
|
||||
},
|
||||
})
|
||||
|
||||
// This is the main entrypoint for the multiplayer sync
|
||||
io.on('connection', async (socket) => {
|
||||
console.log('Socket.IO client connected:', socket.id)
|
||||
|
||||
// The roomId and sessionId are passed from the client as query params,
|
||||
// you need to extract them and pass them to the room.
|
||||
const sessionId = socket.handshake.query.sessionId as string
|
||||
const roomId = socket.handshake.query.roomId as string
|
||||
|
||||
if (!sessionId || !roomId) {
|
||||
console.log('Missing required parameters, disconnecting:', socket.id)
|
||||
socket.disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Connecting to room:', roomId, 'with session:', sessionId)
|
||||
|
||||
try {
|
||||
// Here we make or get an existing instance of TLSocketRoom for the given roomId
|
||||
const room = makeOrLoadRoom(roomId)
|
||||
|
||||
// Create a socket adapter for TLSocketRoom
|
||||
const socketAdapter: WebSocketMinimal = {
|
||||
send: (message) => {
|
||||
socket.emit('tldraw-message', JSON.parse(message))
|
||||
},
|
||||
close: () => {
|
||||
socket.disconnect()
|
||||
},
|
||||
get readyState() {
|
||||
return socket.connected ? 1 : 3 // 1 = OPEN, 3 = CLOSED
|
||||
},
|
||||
}
|
||||
|
||||
// and finally connect the socket to the room
|
||||
room.handleSocketConnect({ sessionId, socket: socketAdapter })
|
||||
|
||||
// Handle tldraw sync messages
|
||||
socket.on('tldraw-message', (message) => {
|
||||
// Ensure message is a string - Socket.IO might send it as an object or buffer
|
||||
room.handleSocketMessage(sessionId, message)
|
||||
})
|
||||
|
||||
// Handle disconnect
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Socket.IO client disconnected:', socket.id)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error setting up room connection:', error)
|
||||
socket.disconnect()
|
||||
}
|
||||
})
|
||||
|
||||
// To enable blob storage for assets, we add simple endpoints supporting PUT and GET requests
|
||||
app.put('/uploads/:id', express.raw({ type: '*/*', limit: '50mb' }), async (req, res) => {
|
||||
try {
|
||||
const id = req.params.id
|
||||
await storeAsset(id, req.body)
|
||||
res.json({ ok: true })
|
||||
} catch (error) {
|
||||
console.error('Asset upload error:', error)
|
||||
res.status(500).json({ error: 'Upload failed' })
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/uploads/:id', async (req, res) => {
|
||||
try {
|
||||
const id = req.params.id
|
||||
const data = await loadAsset(id)
|
||||
// Prevent XSS from user-uploaded SVGs
|
||||
res.header('Content-Security-Policy', "default-src 'none'")
|
||||
res.header('X-Content-Type-Options', 'nosniff')
|
||||
res.send(data)
|
||||
} catch (error) {
|
||||
console.error('Asset download error:', error)
|
||||
res.status(404).json({ error: 'Asset not found' })
|
||||
}
|
||||
})
|
||||
|
||||
// To enable unfurling of bookmarks, we add a simple endpoint that takes a URL query param
|
||||
app.get('/unfurl', async (req, res) => {
|
||||
try {
|
||||
const url = req.query.url as string
|
||||
if (!url) {
|
||||
return res.status(400).json({ error: 'URL parameter required' })
|
||||
}
|
||||
const result = await unfurl(url)
|
||||
return res.json(result)
|
||||
} catch (error) {
|
||||
console.error('Unfurl error:', error)
|
||||
return res.status(500).json({ error: 'Unfurl failed' })
|
||||
}
|
||||
})
|
||||
|
||||
// Start server
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Server started on port ${PORT}`)
|
||||
})
|
||||
14
templates/socketio-server-example/src/server/unfurl.ts
Normal file
14
templates/socketio-server-example/src/server/unfurl.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import _unfurl from 'unfurl.js'
|
||||
|
||||
export async function unfurl(url: string) {
|
||||
const { title, description, open_graph, twitter_card, favicon } = await _unfurl.unfurl(url)
|
||||
|
||||
const image = open_graph?.images?.[0]?.url || twitter_card?.images?.[0]?.url
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
favicon,
|
||||
}
|
||||
}
|
||||
11
templates/socketio-server-example/tsconfig.json
Normal file
11
templates/socketio-server-example/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../internal/config/tsconfig.base.json",
|
||||
"include": ["src", "scripts", "vite.config.mts"],
|
||||
"exclude": ["node_modules", "dist", ".tsbuild*"],
|
||||
"compilerOptions": { "noEmit": true, "emitDeclarationOnly": false, "types": ["node"] },
|
||||
"references": [
|
||||
{ "path": "../../packages/sync" },
|
||||
{ "path": "../../packages/sync-core" },
|
||||
{ "path": "../../packages/tldraw" }
|
||||
]
|
||||
}
|
||||
15
templates/socketio-server-example/vite.config.mts
Normal file
15
templates/socketio-server-example/vite.config.mts
Normal file
@@ -0,0 +1,15 @@
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig(() => ({
|
||||
plugins: [react()],
|
||||
root: path.join(__dirname, 'src/client'),
|
||||
publicDir: path.join(__dirname, 'public'),
|
||||
server: {
|
||||
port: 5757,
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['@tldraw/assets'],
|
||||
},
|
||||
}))
|
||||
Reference in New Issue
Block a user