diff --git a/Makefile b/Makefile index d0c9de5..554b413 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,13 @@ deploy: ssh -o StrictHostKeyChecking=no -p 32022 -i ansible/.ssh_key ubuntu@o1.gempir.com "sudo chown gempbot:gempbot /home/gempbot/gempbot" ssh -o StrictHostKeyChecking=no -p 32022 -i ansible/.ssh_key ubuntu@o1.gempir.com "sudo systemctl restart gempbot-migrate && sudo systemctl start gempbot" +deploy_tldraw_server: + (cd tldraw-server && bun install && bun build --compile --target=bun-linux-arm64 ./server.bun.ts --outfile tldraw-server) + ssh -o StrictHostKeyChecking=no -p 32022 -i ansible/.ssh_key ubuntu@o1.gempir.com "sudo systemctl stop gempbot-tldraw" + rsync -avz -e "ssh -o StrictHostKeyChecking=no -p 32022 -i ansible/.ssh_key" ./tldraw-server/tldraw-server ubuntu@o1.gempir.com:/home/gempbot/tldraw-server/tldraw-server + ssh -o StrictHostKeyChecking=no -p 32022 -i ansible/.ssh_key ubuntu@o1.gempir.com "sudo chown gempbot:gempbot /home/gempbot/tldraw-server/" + ssh -o StrictHostKeyChecking=no -p 32022 -i ansible/.ssh_key ubuntu@o1.gempir.com "sudo systemctl start gempbot-tldraw" + ansible: cd ansible && ansible-vault decrypt ssh_key.vault --output=.ssh_key chmod 600 ansible/.ssh_key @@ -46,6 +53,3 @@ run_docker: tunnel: npx localtunnel --port 3010 --subdomain gempbot - -ollama: - ssh -N -R 11434:127.0.0.1:11434 o1 \ No newline at end of file diff --git a/ansible/group_vars/main/vars.yml b/ansible/group_vars/main/vars.yml index a5cf1f8..4dfbb55 100644 --- a/ansible/group_vars/main/vars.yml +++ b/ansible/group_vars/main/vars.yml @@ -3,6 +3,7 @@ web_host: bot.gempir.com cookie_domain: gempir.com api_host: bot-api.gempir.com yjs_host: bot-yjs.gempir.com +tldraw_host: bot-tldraw.gempir.com grafana_host: grafana.o.gempir.com prometheus_host: prometheus.o.gempir.com diff --git a/ansible/roles/caddy/templates/Caddyfile.j2 b/ansible/roles/caddy/templates/Caddyfile.j2 index 91f4aa0..0ae5b7a 100644 --- a/ansible/roles/caddy/templates/Caddyfile.j2 +++ b/ansible/roles/caddy/templates/Caddyfile.j2 @@ -6,6 +6,10 @@ reverse_proxy 127.0.0.1:8070 } +{{ tldraw_host }} { + reverse_proxy 127.0.0.1:5858 +} + {{ grafana_host }} { reverse_proxy 127.0.0.1:3000 } diff --git a/ansible/roles/gempbot/tasks/main.yml b/ansible/roles/gempbot/tasks/main.yml index a3cc31a..2c24b7d 100644 --- a/ansible/roles/gempbot/tasks/main.yml +++ b/ansible/roles/gempbot/tasks/main.yml @@ -16,6 +16,15 @@ recurse: yes mode: 0770 +- name: Ensure tldraw-server folder + file: + path: /home/gempbot/tldraw-server + state: directory + owner: gempbot + group: gempbot + recurse: yes + mode: 0770 + - name: Setup env file template: src: templates/env.j2 @@ -29,6 +38,11 @@ src: templates/service.j2 dest: /etc/systemd/system/gempbot.service +- name: Install Tldraw Service + template: + src: templates/service-tldraw.j2 + dest: /etc/systemd/system/gempbot-tldraw.service + - name: Install Yjs Service template: src: templates/service-yjs.j2 @@ -51,6 +65,12 @@ name: gempbot enabled: true +- name: ensure tldraw service is enabled + systemd: + daemon_reload: true + name: gempbot-tldraw + enabled: true + - name: ensure yjs service is enabled systemd: daemon_reload: true diff --git a/ansible/roles/gempbot/templates/service-tldraw.j2 b/ansible/roles/gempbot/templates/service-tldraw.j2 new file mode 100644 index 0000000..545aa36 --- /dev/null +++ b/ansible/roles/gempbot/templates/service-tldraw.j2 @@ -0,0 +1,14 @@ +[Unit] +Description=gempbot-tldraw +After=network.target +StartLimitBurst=10 + +[Service] +Restart=always +RestartSec=5 +ExecStart=/home/gempbot/tldraw-server/tldraw-server +WorkingDirectory=/home/gempbot/tldraw-server +Environment=NODE_ENV=production + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/internal/server/api.go b/internal/server/api.go index 8184e8a..c1e0718 100644 --- a/internal/server/api.go +++ b/internal/server/api.go @@ -11,7 +11,6 @@ import ( "github.com/gempir/gempbot/internal/store" "github.com/gempir/gempbot/internal/user" "github.com/gempir/gempbot/internal/ws" - "github.com/gempir/gempbot/internal/ysweet" ) type Api struct { @@ -25,10 +24,9 @@ type Api struct { channelPointManager *channelpoint.ChannelPointManager sevenTvClient emoteservice.ApiClient wsHandler *ws.WsHandler - tokenFactory *ysweet.Factory } -func NewApi(cfg *config.Config, db *store.Database, helixClient helixclient.Client, userAdmin *user.UserAdmin, authClient *auth.Auth, emoteChief *emotechief.EmoteChief, eventsubManager *eventsubmanager.EventsubManager, channelPointManager *channelpoint.ChannelPointManager, sevenTvClient emoteservice.ApiClient, wsHandler *ws.WsHandler, tokenFactory *ysweet.Factory) *Api { +func NewApi(cfg *config.Config, db *store.Database, helixClient helixclient.Client, userAdmin *user.UserAdmin, authClient *auth.Auth, emoteChief *emotechief.EmoteChief, eventsubManager *eventsubmanager.EventsubManager, channelPointManager *channelpoint.ChannelPointManager, sevenTvClient emoteservice.ApiClient, wsHandler *ws.WsHandler) *Api { return &Api{ db: db, cfg: cfg, @@ -40,6 +38,5 @@ func NewApi(cfg *config.Config, db *store.Database, helixClient helixclient.Clie channelPointManager: channelPointManager, sevenTvClient: sevenTvClient, wsHandler: wsHandler, - tokenFactory: tokenFactory, } } diff --git a/internal/server/overlay.go b/internal/server/overlay.go index ddad764..4fec09d 100644 --- a/internal/server/overlay.go +++ b/internal/server/overlay.go @@ -7,28 +7,19 @@ import ( "github.com/gempir/gempbot/internal/api" "github.com/gempir/gempbot/internal/dto" "github.com/gempir/gempbot/internal/store" - "github.com/gempir/gempbot/internal/ysweet" "github.com/google/uuid" "github.com/teris-io/shortid" ) type OverlayResponse struct { - Overlay store.Overlay `json:"overlay"` - Auth ysweet.TokenResponse `json:"auth"` + Overlay store.Overlay `json:"overlay"` } func (a *Api) OverlayHandler(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { if r.URL.Query().Get("roomId") != "" { overlay := a.db.GetOverlayByRoomId(r.URL.Query().Get("roomId")) - - token, err := a.tokenFactory.CreateToken(overlay.RoomID) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - api.WriteJson(w, OverlayResponse{overlay, token}, http.StatusOK) + api.WriteJson(w, OverlayResponse{overlay}, http.StatusOK) return } } @@ -50,13 +41,8 @@ func (a *Api) OverlayHandler(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { if r.URL.Query().Get("id") != "" { overlay := a.db.GetOverlay(r.URL.Query().Get("id"), userID) - token, err := a.tokenFactory.CreateToken(overlay.RoomID) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - api.WriteJson(w, OverlayResponse{overlay, token}, http.StatusOK) + api.WriteJson(w, OverlayResponse{overlay}, http.StatusOK) return } diff --git a/internal/ysweet/factory.go b/internal/ysweet/factory.go deleted file mode 100644 index c117736..0000000 --- a/internal/ysweet/factory.go +++ /dev/null @@ -1,67 +0,0 @@ -package ysweet - -import ( - "context" - "strings" - - "github.com/carlmjohnson/requests" - "github.com/gempir/gempbot/internal/config" -) - -type Factory struct { - ysweetUrl string - ssl bool - bearerToken string -} - -func NewFactory(cfg *config.Config) *Factory { - return &Factory{ - ysweetUrl: cfg.YsweetUrl, - ssl: strings.HasPrefix(cfg.WebBaseUrl, "https"), - bearerToken: cfg.YsweetToken, - } -} - -type TokenResponse struct { - Url string `json:"url"` - DocId string `json:"docId"` - Token string `json:"token"` -} - -type DocResponse struct { - DocID string `json:"docId"` -} - -func (f *Factory) CreateToken(docID string) (TokenResponse, error) { - var docResponse DocResponse - err := requests. - URL(f.ysweetUrl). - Post(). - BodyJSON(map[string]string{"docId": docID}). - Bearer(f.bearerToken). - Pathf("/doc/new"). - ToJSON(&docResponse). - Fetch(context.Background()) - if err != nil { - return TokenResponse{}, err - } - - var tokenResponse TokenResponse - err = requests. - URL(f.ysweetUrl). - Post(). - Pathf("/doc/%s/auth", docResponse.DocID). - BodyJSON(map[string]string{}). - Bearer(f.bearerToken). - ToJSON(&tokenResponse). - Fetch(context.Background()) - if err != nil { - return TokenResponse{}, err - } - - if f.ssl { - tokenResponse.Url = strings.Replace(tokenResponse.Url, "ws://", "wss://", 1) - } - - return tokenResponse, nil -} diff --git a/internal/ysweet/ysweet.http b/internal/ysweet/ysweet.http deleted file mode 100644 index a48acb0..0000000 --- a/internal/ysweet/ysweet.http +++ /dev/null @@ -1,15 +0,0 @@ - -POST http://127.0.0.1:8070/doc/new -Content-Type: application/json -Authorization: Bearer AAAgT07jyfGLORBpu8Oj0XmtJmbT7KAeWBRZ3Y75d1T4u1A - -{ - "docId": "newDoc123" -} - -### -POST http://127.0.0.1:8070/doc/newDoc123/auth -Content-Type: application/json -Authorization: Bearer AAAgT07jyfGLORBpu8Oj0XmtJmbT7KAeWBRZ3Y75d1T4u1A - -{} \ No newline at end of file diff --git a/main.go b/main.go index 7acf204..f6b92b3 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,6 @@ import ( "github.com/gempir/gempbot/internal/store" "github.com/gempir/gempbot/internal/user" "github.com/gempir/gempbot/internal/ws" - "github.com/gempir/gempbot/internal/ysweet" "github.com/rs/cors" ) @@ -36,7 +35,6 @@ func main() { userAdmin := user.NewUserAdmin(cfg, db, helixClient) authClient := auth.NewAuth(cfg, db, helixClient) - tokenFactory := ysweet.NewFactory(cfg) seventvClient := emoteservice.NewSevenTvClient(db) @@ -45,7 +43,7 @@ func main() { wsHandler := ws.NewWsHandler(authClient) eventsubManager := eventsubmanager.NewEventsubManager(cfg, helixClient, db, emoteChief) - apiHandlers := server.NewApi(cfg, db, helixClient, userAdmin, authClient, emoteChief, eventsubManager, channelPointManager, seventvClient, wsHandler, tokenFactory) + apiHandlers := server.NewApi(cfg, db, helixClient, userAdmin, authClient, emoteChief, eventsubManager, channelPointManager, seventvClient, wsHandler) mux := http.NewServeMux() diff --git a/tldraw-server/.gitignore b/tldraw-server/.gitignore new file mode 100644 index 0000000..8204107 --- /dev/null +++ b/tldraw-server/.gitignore @@ -0,0 +1,6 @@ +.rooms/ +.assets/ +.yarn + +node_modules/ +tldraw-server diff --git a/tldraw-server/README.md b/tldraw-server/README.md new file mode 100644 index 0000000..1db083e --- /dev/null +++ b/tldraw-server/README.md @@ -0,0 +1,31 @@ +# tldraw sync, simple Node/Bun server example + +This is a simple example of a backend for [tldraw sync](https://tldraw.dev/docs/sync) with a Node or Bun server. + +Run `yarn dev-node` or `yarn dev-bun` in this folder to start the server + client. + +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 + +Please see our [contributing guide](https://github.com/tldraw/tldraw/blob/main/CONTRIBUTING.md). 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.gg/rhsyWMUJxd) or [start a discussion](https://github.com/tldraw/tldraw/discussions/new). 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). diff --git a/tldraw-server/assets.ts b/tldraw-server/assets.ts new file mode 100644 index 0000000..f376958 --- /dev/null +++ b/tldraw-server/assets.ts @@ -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)) +} diff --git a/tldraw-server/bun.lockb b/tldraw-server/bun.lockb new file mode 100755 index 0000000..84434dc Binary files /dev/null and b/tldraw-server/bun.lockb differ diff --git a/tldraw-server/package.json b/tldraw-server/package.json new file mode 100644 index 0000000..4f4644e --- /dev/null +++ b/tldraw-server/package.json @@ -0,0 +1,53 @@ +{ + "name": "@tldraw/simple-server-example", + "description": "A tiny little drawing app (example node/bun servers).", + "version": "0.0.0", + "private": true, + "author": { + "name": "tldraw GB Ltd.", + "email": "hello@tldraw.com" + }, + "license": "MIT", + "main": "./src/server/server.ts", + "scripts": { + "dev-node": "concurrently -n server,client -c red,blue \"yarn dev-server-node\" \"yarn dev-client\"", + "dev-bun": "concurrently -n server,client -c red,blue \"yarn dev-server-bun\" \"yarn dev-client\"", + "dev-server-node": "yarn run -T tsx watch ./src/server/server.node.ts", + "dev-server-bun": "npx bun --watch ./src/server/server.bun.ts", + "dev-client": "vite dev", + "test-ci": "echo 'No tests yet'", + "test": "yarn run -T jest --passWithNoTests", + "test-coverage": "lazy inherit", + "lint": "yarn run -T tsx ../../scripts/lint.ts" + }, + "devDependencies": { + "@types/bun": "^1.1.6", + "@types/express": "^4.17.21", + "concurrently": "^8.2.2", + "lazyrepo": "0.0.0-alpha.27", + "tsx": "^4.19.1", + "typescript": "^5.3.3" + }, + "jest": { + "preset": "../../internal/config/jest/node/jest-preset.js", + "moduleNameMapper": { + "^~(.*)": "/src/$1" + } + }, + "dependencies": { + "@fastify/cors": "^9.0.1", + "@fastify/websocket": "^10.0.1", + "@tldraw/sync": "latest", + "@tldraw/sync-core": "latest", + "@vitejs/plugin-react-swc": "^3.7.0", + "fastify": "^4.28.1", + "itty-router": "^5.0.17", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.24.1", + "tldraw": "latest", + "unfurl.js": "^6.4.0", + "vite": "^5.4.2", + "ws": "^8.16.0" + } +} diff --git a/tldraw-server/rooms.ts b/tldraw-server/rooms.ts new file mode 100644 index 0000000..b2cf972 --- /dev/null +++ b/tldraw-server/rooms.ts @@ -0,0 +1,90 @@ +import { RoomSnapshot, TLSocketRoom } from '@tldraw/sync-core' +import { mkdir, readFile, writeFile } from 'fs/promises' +import { join } from 'path' + +// For this example we're just saving data to the local filesystem +const DIR = './.rooms' +async function readSnapshotIfExists(roomId: string) { + try { + const data = await readFile(join(DIR, roomId)) + return JSON.parse(data.toString()) ?? undefined + } catch (e) { + return undefined + } +} +async function saveSnapshot(roomId: string, snapshot: RoomSnapshot) { + await mkdir(DIR, { recursive: true }) + await writeFile(join(DIR, roomId), JSON.stringify(snapshot)) +} + +// We'll keep an in-memory map of rooms and their data +interface RoomState { + room: TLSocketRoom + id: string + needsPersist: boolean +} +const rooms = new Map() + +// Very simple mutex using promise chaining, to avoid race conditions +// when loading rooms. In production you probably want one mutex per room +// to avoid unnecessary blocking! +let mutex = Promise.resolve(null) + +export async function makeOrLoadRoom(roomId: string) { + mutex = mutex + .then(async () => { + if (rooms.has(roomId)) { + const roomState = await rooms.get(roomId)! + if (!roomState.room.isClosed()) { + return null // all good + } + } + console.log('loading room', roomId) + const initialSnapshot = await readSnapshotIfExists(roomId) + + const roomState: RoomState = { + needsPersist: false, + id: roomId, + room: new TLSocketRoom({ + initialSnapshot, + onSessionRemoved(room, args) { + console.log('client disconnected', args.sessionId, roomId) + if (args.numSessionsRemaining === 0) { + console.log('closing room', roomId) + room.close() + } + }, + onDataChange() { + roomState.needsPersist = true + }, + }), + } + rooms.set(roomId, roomState) + return null // all good + }) + .catch((error) => { + // return errors as normal values to avoid stopping the mutex chain + return error + }) + + const err = await mutex + if (err) throw err + return rooms.get(roomId)!.room +} + +// Do persistence on a regular interval. +// In production you probably want a smarter system with throttling. +setInterval(() => { + for (const roomState of rooms.values()) { + if (roomState.needsPersist) { + // persist room + roomState.needsPersist = false + console.log('saving snapshot', roomState.id) + saveSnapshot(roomState.id, roomState.room.getCurrentSnapshot()) + } + if (roomState.room.isClosed()) { + console.log('deleting room', roomState.id) + rooms.delete(roomState.id) + } + } +}, 2000) diff --git a/tldraw-server/server.bun.ts b/tldraw-server/server.bun.ts new file mode 100644 index 0000000..8932477 --- /dev/null +++ b/tldraw-server/server.bun.ts @@ -0,0 +1,90 @@ +import { TLSocketRoom } from '@tldraw/sync-core' +import { IRequest, Router, RouterType, cors, json } from 'itty-router' +import { loadAsset, storeAsset } from './assets' +import { makeOrLoadRoom } from './rooms' +import { unfurl } from './unfurl' +const PORT = 5858 + +// 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 = 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 blob storage for assets, we add a simple endpoint supporting PUT and GET requests + .put('/uploads/:id', async (req) => { + const id = (req.params as any).id as string + await storeAsset(id, req.raw) + return json({ ok: true }) + }) + .get('/uploads/:id', async (req) => { + const id = (req.params as any).id as string + return new Response(await loadAsset(id)) + }) + + // 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)) + }) + .all('*', () => { + new Response('Not found', { status: 404 }) + }) + +const server = Bun.serve<{ room?: TLSocketRoom; sessionId: string; roomId: string }>({ + 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) { + // 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}`) diff --git a/tldraw-server/unfurl.ts b/tldraw-server/unfurl.ts new file mode 100644 index 0000000..b26787d --- /dev/null +++ b/tldraw-server/unfurl.ts @@ -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, + } +} diff --git a/web/package.json b/web/package.json index 1859cdc..78f398c 100644 --- a/web/package.json +++ b/web/package.json @@ -20,10 +20,9 @@ "@mantine/nprogress": "^7.8.0", "@mantine/spotlight": "^7.8.0", "@tailwindcss/forms": "^0.5.7", - "@tldraw/tldraw": "^2.0.0-beta.2", + "@tldraw/sync": "^3.2.0", + "@tldraw/sync-core": "^3.2.0", "@types/seedrandom": "^3.0.4", - "@y-sweet/react": "^0.1.0", - "@y-sweet/sdk": "^0.1.0", "dayjs": "^1.11.10", "embla-carousel-react": "^8.0.2", "eslint-config-next": "^14.1.0", @@ -37,11 +36,7 @@ "react-use-websocket": "^4.5.0", "seedrandom": "^3.0.5", "tailwindcss": "^3.4.1", - "tldraw": "^2.0.2", - "y-sweet": "^0.1.0", - "y-utility": "^0.1.3", - "y-websocket": "^1.5.3", - "yjs": "^13.6.11", + "tldraw": "^3.2.0", "zustand": "^3.7.2" }, "devDependencies": { diff --git a/web/src/components/Overlay/Editor.tsx b/web/src/components/Overlay/Editor.tsx index bfe094d..c54968d 100644 --- a/web/src/components/Overlay/Editor.tsx +++ b/web/src/components/Overlay/Editor.tsx @@ -1,42 +1,55 @@ -import { AssetRecordType, Editor, TLAsset, TLAssetId, Tldraw, TldrawProps, getHashForString } from '@tldraw/tldraw'; -import { MediaHelpers, isGifAnimated } from 'tldraw'; -import '@tldraw/tldraw/tldraw.css'; +import { useSync } from '@tldraw/sync'; +import 'tldraw/tldraw.css'; +import { AssetRecordType, Editor, MediaHelpers, TLAsset, TLAssetId, TLAssetStore, TLBookmarkAsset, Tldraw, TldrawProps, getHashForString, uniqueId } from 'tldraw'; import { useAssetUploader } from '../../hooks/useAssetUploader'; -import { useYjsStore } from '../../hooks/useYjsStore'; type Props = { readonly?: boolean; + roomID: string; + username?: string; + userID?: string; } export function CustomEditor(props: Partial & Props) { - const store = useYjsStore(); + // Create a store connected to multiplayer. + const store = useSync({ + // We need to know the websocket's URI... + uri: `https://bot-tldraw.gempir.com/connect/${props.roomID}`, + // ...and how to handle static assets like images & videos + assets: multiplayerAssets, + userInfo: props.username && props.userID ? { name: props.username, id: props.userID } : undefined, + }) + const upload = useAssetUploader(); const handleMount = (editor: Editor) => { + // @ts-expect-error + window.editor = editor; console.log('editor mounted', props.readonly, editor); if (props.readonly) { editor.setCamera({ x: 0, y: 0, z: 1 }); - editor.updateInstanceState({ isReadonly: true, canMoveCamera: false }) + editor.updateInstanceState({ isReadonly: true }) editor.selectNone(); } else { + editor.registerExternalAssetHandler('url', unfurlBookmarkUrl) editor.registerExternalAssetHandler('file', async ({ file }: { type: 'file'; file: File }) => { const uploadedAsset = await upload(file); //[b] const assetId: TLAssetId = AssetRecordType.createId(getHashForString(uploadedAsset.url)) - + let size: { w: number h: number } let isAnimated: boolean let shapeType: 'image' | 'video' - + //[c] - if (['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'].includes(file.type)) { + if (['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp'].includes(file.type)) { shapeType = 'image' size = await MediaHelpers.getImageSize(file) - isAnimated = file.type === 'image/gif' && (await isGifAnimated(file)) + isAnimated = file.type === 'image/gif' } else { shapeType = 'video' isAnimated = true @@ -56,11 +69,69 @@ export function CustomEditor(props: Partial & Props) { isAnimated, }, }) - + return asset }) } } return -} \ No newline at end of file +} + +// 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 id = uniqueId() + + const objectName = `${id}-${file.name}` + const url = `https://bot-tldraw.gempir.com/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 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 { + 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(`https://bot-tldraw.gempir.com/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 +} diff --git a/web/src/components/Overlay/IframeOverlayPage.tsx b/web/src/components/Overlay/IframeOverlayPage.tsx index fff02b0..6e978d1 100644 --- a/web/src/components/Overlay/IframeOverlayPage.tsx +++ b/web/src/components/Overlay/IframeOverlayPage.tsx @@ -1,16 +1,13 @@ 'use client'; -import { YDocProvider } from '@y-sweet/react'; import dynamic from 'next/dynamic'; import Head from 'next/head'; import { useParams } from 'next/navigation'; -import { useOverlayByRoomId } from '../../hooks/useOverlays'; const Editor = dynamic(async () => (await import('./Editor')).CustomEditor, { ssr: false }) export function IframeOverlayPage() { const params = useParams<{ roomId: string }>(); - const [overlayAuth] = useOverlayByRoomId(params.roomId); return (
@@ -20,20 +17,26 @@ export function IframeOverlayPage() { background-color: transparent !important; } - .tl-background { + .tl-background__wrapper, .tl-background, .tl-canvas { background-color: transparent !important; } .tl-loading { display: none !important; } + + .tl-cursor, .tl-nametag, .tl-nametag-title, .tl-nametag-chat { + display: none !important; + } + + /** Please don't hate me tldraw, I can't show this in the overlay, that would suck for the stream. But it's still visible for the editors. **/ + + .tl-watermark_SEE-LICENSE { + display: none !important; + } `} - {overlayAuth && - - - - } +
); } \ No newline at end of file diff --git a/web/src/components/Overlay/OverlayEditPage.tsx b/web/src/components/Overlay/OverlayEditPage.tsx index 0327024..7743d95 100644 --- a/web/src/components/Overlay/OverlayEditPage.tsx +++ b/web/src/components/Overlay/OverlayEditPage.tsx @@ -1,20 +1,18 @@ const Editor = dynamic(async () => (await import('./Editor')).CustomEditor, { ssr: false }) -import { YDocProvider } from '@y-sweet/react'; import dynamic from "next/dynamic"; import { useParams } from "next/navigation"; import { useOverlay } from "../../hooks/useOverlays"; +import { useStore } from "../../store"; export function OverlayEditPage() { const params = useParams<{ overlayId: string }>(); - const [overlayAuth] = useOverlay(params.overlayId); - + const [overlayResponse] = useOverlay(params.overlayId); + const scTokenContent = useStore(state => state.scTokenContent); return
- {overlayAuth && - - - + {overlayResponse && + }
; } \ No newline at end of file diff --git a/web/src/hooks/useOverlays.ts b/web/src/hooks/useOverlays.ts index c81ef79..1a300f8 100644 --- a/web/src/hooks/useOverlays.ts +++ b/web/src/hooks/useOverlays.ts @@ -41,19 +41,12 @@ export function useOverlays(): [Overlay[], () => void, (id: string) => void, str return [overlays, addOverlay, deleteOverlay, errorMessage, loading]; } -type Auth = { - url: string; - docId: string; - token: string; -} - -type OverlayAuth = { +type OverlayResponse = { overlay: Overlay; - auth: Auth; } -export function useOverlay(id: string): [OverlayAuth|null, boolean] { - const [overlay, setOverlay] = useState(null); +export function useOverlay(id: string): [OverlayResponse|null, boolean] { + const [overlay, setOverlay] = useState(null); const [loading, setLoading] = useState(false); const managing = useStore(state => state.managing); const apiBaseUrl = useStore(state => state.apiBaseUrl); @@ -71,8 +64,8 @@ export function useOverlay(id: string): [OverlayAuth|null, boolean] { return [overlay, loading]; } -export function useOverlayByRoomId(roomId: string): [OverlayAuth|null, boolean] { - const [overlay, setOverlay] = useState(null); +export function useOverlayByRoomId(roomId: string): [OverlayResponse|null, boolean] { + const [overlay, setOverlay] = useState(null); const [loading, setLoading] = useState(false); const managing = useStore(state => state.managing); const apiBaseUrl = useStore(state => state.apiBaseUrl); diff --git a/web/src/hooks/useYjsStore.ts b/web/src/hooks/useYjsStore.ts deleted file mode 100644 index cc984d2..0000000 --- a/web/src/hooks/useYjsStore.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** Based on: https://github.com/tldraw/tldraw-yjs-example/blob/main/src/useYjsStore.ts */ - -import { - InstancePresenceRecordType, - TLInstancePresence, - TLRecord, - TLStoreWithStatus, - computed, - createPresenceStateDerivation, - createTLStore, - defaultShapeUtils, - defaultUserPreferences, - getUserPreferences, - setUserPreferences, - react, - transact, - } from '@tldraw/tldraw' - import { useEffect, useMemo, useState } from 'react' - import { YKeyValue } from 'y-utility/y-keyvalue' - import * as Y from 'yjs' - import { DEFAULT_STORE } from './default_store' - import { useYDoc, useYjsProvider } from '@y-sweet/react' - - export function useYjsStore() { - const [store] = useState(() => { - const store = createTLStore({ - shapeUtils: defaultShapeUtils, - }) - store.loadSnapshot(DEFAULT_STORE) - return store - }) - - const [storeWithStatus, setStoreWithStatus] = useState({ - status: 'loading', - }) - - const yDoc = useYDoc() - const room = useYjsProvider() - - const yStore = useMemo(() => { - const yArr = yDoc.getArray<{ key: string; val: TLRecord }>(`tl_room`) - const yStore = new YKeyValue(yArr) - - return yStore - }, [yDoc]) - - useEffect(() => { - setStoreWithStatus({ status: 'loading' }) - - const unsubs: (() => void)[] = [] - - function handleSync() { - // 1. - // Connect store to yjs store and vis versa, for both the document and awareness - - /* -------------------- Document -------------------- */ - - // Sync store changes to the yjs doc - unsubs.push( - store.listen( - function syncStoreChangesToYjsDoc({ changes }) { - yDoc.transact(() => { - Object.values(changes.added).forEach((record) => { - yStore.set(record.id, record) - }) - - Object.values(changes.updated).forEach(([_, record]) => { - yStore.set(record.id, record) - }) - - Object.values(changes.removed).forEach((record) => { - yStore.delete(record.id) - }) - }) - }, - { source: 'user', scope: 'document' }, // only sync user's document changes - ), - ) - - // Sync the yjs doc changes to the store - const handleChange = ( - changes: Map< - string, - | { action: 'delete'; oldValue: TLRecord } - | { action: 'update'; oldValue: TLRecord; newValue: TLRecord } - | { action: 'add'; newValue: TLRecord } - >, - transaction: Y.Transaction, - ) => { - if (transaction.local) return - - const toRemove: TLRecord['id'][] = [] - const toPut: TLRecord[] = [] - - changes.forEach((change, id) => { - switch (change.action) { - case 'add': - case 'update': { - const record = yStore.get(id)! - toPut.push(record) - break - } - case 'delete': { - toRemove.push(id as TLRecord['id']) - break - } - } - }) - - // put / remove the records in the store - store.mergeRemoteChanges(() => { - if (toRemove.length) store.remove(toRemove) - if (toPut.length) store.put(toPut) - }) - } - - yStore.on('change', handleChange) - unsubs.push(() => yStore.off('change', handleChange)) - - /* -------------------- Awareness ------------------- */ - - const yClientId = room.awareness.clientID.toString() - setUserPreferences({ id: yClientId }) - - const userPreferences = computed<{ - id: string - color: string - name: string - }>('userPreferences', () => { - const user = getUserPreferences() - return { - id: user.id, - color: user.color ?? defaultUserPreferences.color, - name: user.name ?? defaultUserPreferences.name, - } - }) - - // Create the instance presence derivation - const presenceId = InstancePresenceRecordType.createId(yClientId) - const presenceDerivation = createPresenceStateDerivation(userPreferences, presenceId)(store) - - // Set our initial presence from the derivation's current value - // @ts-expect-error - room.awareness.setLocalStateField('presence', presenceDerivation.value) - - // When the derivation change, sync presence to to yjs awareness - unsubs.push( - react('when presence changes', () => { - // @ts-expect-error - const presence = presenceDerivation.value - requestAnimationFrame(() => { - room.awareness.setLocalStateField('presence', presence) - }) - }), - ) - - // Sync yjs awareness changes to the store - const handleUpdate = (update: { added: number[]; updated: number[]; removed: number[] }) => { - const states = room.awareness.getStates() as Map - - const toRemove: TLInstancePresence['id'][] = [] - const toPut: TLInstancePresence[] = [] - - // Connect records to put / remove - for (const clientId of update.added) { - const state = states.get(clientId) - if (state?.presence && state.presence.id !== presenceId) { - toPut.push(state.presence) - } - } - - for (const clientId of update.updated) { - const state = states.get(clientId) - if (state?.presence && state.presence.id !== presenceId) { - toPut.push(state.presence) - } - } - - for (const clientId of update.removed) { - toRemove.push(InstancePresenceRecordType.createId(clientId.toString())) - } - - // put / remove the records in the store - store.mergeRemoteChanges(() => { - if (toRemove.length) store.remove(toRemove) - if (toPut.length) store.put(toPut) - }) - } - - room.awareness.on('update', handleUpdate) - unsubs.push(() => room.awareness.off('update', handleUpdate)) - - // 2. - // Initialize the store with the yjs doc records—or, if the yjs doc - // is empty, initialize the yjs doc with the default store records. - if (yStore.yarray.length) { - // Replace the store records with the yjs doc records - transact(() => { - // The records here should be compatible with what's in the store - store.clear() - const records = yStore.yarray.toJSON().map(({ val }) => val) - store.put(records) - }) - } else { - // Create the initial store records - // Sync the store records to the yjs doc - yDoc.transact(() => { - for (const record of store.allRecords()) { - yStore.set(record.id, record) - } - }) - } - - setStoreWithStatus({ - store, - status: 'synced-remote', - connectionStatus: 'online', - }) - } - - let hasConnectedBefore = false - - function handleStatusChange({ status }: { status: 'disconnected' | 'connected' }) { - // If we're disconnected, set the store status to 'synced-remote' and the connection status to 'offline' - if (status === 'disconnected') { - setStoreWithStatus({ - store, - status: 'synced-remote', - connectionStatus: 'offline', - }) - return - } - - room.off('synced', handleSync) - - if (status === 'connected') { - if (hasConnectedBefore) return - hasConnectedBefore = true - room.on('synced', handleSync) - unsubs.push(() => room.off('synced', handleSync)) - } - } - - room.on('status', handleStatusChange) - unsubs.push(() => room.off('status', handleStatusChange)) - - return () => { - unsubs.forEach((fn) => fn()) - unsubs.length = 0 - } - }, [room, yDoc, store, yStore]) - - return storeWithStatus - } \ No newline at end of file diff --git a/web/yarn.lock b/web/yarn.lock index a34f4b1..47ed4dd 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -729,16 +729,17 @@ dependencies: mini-svg-data-uri "^1.2.3" -"@tldraw/editor@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@tldraw/editor/-/editor-2.0.2.tgz#2e4fd3c14fd2d079554d7d111531ab4bbc4861cc" - integrity sha512-ik7JvBQv9AsvkGhBfNkPdM0m+cBiarfPLrzlgyB2dec4lWQOIIL/p9J+28FUWQ4bzDvD6JN0eGXnsGj754ypQg== - dependencies: - "@tldraw/state" "2.0.2" - "@tldraw/store" "2.0.2" - "@tldraw/tlschema" "2.0.2" - "@tldraw/utils" "2.0.2" - "@tldraw/validate" "2.0.2" +"@tldraw/editor@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@tldraw/editor/-/editor-3.2.0.tgz#e71a0b2a7e5a17577afb5a0c19990014d59c05f1" + integrity sha512-2HAIAh4qw7g5varnnxzJVyVjS4AzcbzY8ZegqFETQxBxxprGrxRXKf+kYxhA4ac5XAyAt5UV52b+qQ6tvpKHuw== + dependencies: + "@tldraw/state" "3.2.0" + "@tldraw/state-react" "3.2.0" + "@tldraw/store" "3.2.0" + "@tldraw/tlschema" "3.2.0" + "@tldraw/utils" "3.2.0" + "@tldraw/validate" "3.2.0" "@types/core-js" "^2.5.5" "@use-gesture/react" "^10.2.27" classnames "^2.3.2" @@ -746,54 +747,83 @@ eventemitter3 "^4.0.7" idb "^7.1.1" is-plain-object "^5.0.0" - lodash.throttle "^4.1.1" - lodash.uniq "^4.5.0" - nanoid "4.0.2" - -"@tldraw/state@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@tldraw/state/-/state-2.0.2.tgz#f23c724476c6d954d4f3f3e3b71e0bd33407f1c0" - integrity sha512-8736faV/vTDipYa6xpTG2l3XJgwu1lR4wAwobW/AdiflWBoxiejK4GZyVhb1HtSvcb9EV/ZDOG9SAwG2cKUw9A== -"@tldraw/store@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@tldraw/store/-/store-2.0.2.tgz#36090dfefa6945d3002668e19761665cf8db9e5f" - integrity sha512-02Wg37GiVGoUXxpcyLaoyOMUXn62OxuwcN3Ij7nVxHs7XGJmeMdWKodFMSss1Jh7lzJJMS5r8BJ3knB/ENdcWA== +"@tldraw/state-react@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@tldraw/state-react/-/state-react-3.2.0.tgz#58986ba440192cfae70c7527ce526dd32164f1c9" + integrity sha512-OTc8J9G8+8R/1MhaBkEM9PGt6g9QRD1ExGiL7+bZiNkz3mGMvgKTBjcUKkVE8JT9bd1myQDWI4MHmFSXWD17+w== dependencies: - "@tldraw/state" "2.0.2" - "@tldraw/utils" "2.0.2" - lodash.isequal "^4.5.0" - nanoid "4.0.2" + "@tldraw/state" "3.2.0" + "@tldraw/utils" "3.2.0" -"@tldraw/tldraw@^2.0.0-beta.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@tldraw/tldraw/-/tldraw-2.0.2.tgz#b53be55b0a8e727ccaaf0fa171d4b97f7dbdf179" - integrity sha512-34GK/SjYMyvPSrO7D0DR0sg3qi0XtFhyNKS4ls4PNGNa24Ov4Etm3MVEmbOe+stqAi8FC4hlfSDd7eE/vIsU5w== +"@tldraw/state@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@tldraw/state/-/state-3.2.0.tgz#ef67652b549fb5869c4193c78d0aaa1c020b73cb" + integrity sha512-JGXkrmbq+ffyEVsstvxljEhubTxDDVoXU+xk/wlsD0/ug/UMxynGyIkfUmsn18rc8l6aM14SeThg1knKB2K/uQ== dependencies: - tldraw "2.0.2" + "@tldraw/utils" "3.2.0" -"@tldraw/tlschema@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@tldraw/tlschema/-/tlschema-2.0.2.tgz#b4925a9155d74354b0b3608276f3fdf2e78d8b76" - integrity sha512-gpSdyl7n5vMutgSX7+1dvZj6DGxQhCyDMhOmuWQGkTuQrbp/RmOIRwxAfLRIZCJstKKohy/skc3XzAhL5wEbHQ== +"@tldraw/store@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@tldraw/store/-/store-3.2.0.tgz#a352187b1a781ebbc55dea28dfd56fc2cb09a378" + integrity sha512-kS66jGzZw28qARnWqGoj446Wzrxhzh6tq+YZakXNmO5FxegghstpGLkitoFkOlpSPUqAIqlvbiy6S2RjzuB43A== dependencies: - "@tldraw/state" "2.0.2" - "@tldraw/store" "2.0.2" - "@tldraw/utils" "2.0.2" - "@tldraw/validate" "2.0.2" - nanoid "4.0.2" + "@tldraw/state" "3.2.0" + "@tldraw/utils" "3.2.0" + lodash.isequal "^4.5.0" -"@tldraw/utils@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@tldraw/utils/-/utils-2.0.2.tgz#692e7a937efbb6e26575a9c258ccc2eec91cdd33" - integrity sha512-oXqCF2fERXG7e30npML/zMGtyRWIfgUXVvgdXtWR/S7/mwVU0MeY9YJ7eAuY+iIFshpV9MM99tQxqUZKVJ6NNw== +"@tldraw/sync-core@3.2.0", "@tldraw/sync-core@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@tldraw/sync-core/-/sync-core-3.2.0.tgz#0aea6f8933ebd3311891387b4fb1d2f223da4fdf" + integrity sha512-TNsjMQgWXTAV9cJY6c7F4pPpkMFMhaDAHMHltjDnRT4rdBJoV3nz57UwhmXSlfUQeqBlcSAW7r7z5R7xd7Xljg== + dependencies: + "@tldraw/state" "3.2.0" + "@tldraw/store" "3.2.0" + "@tldraw/tlschema" "3.2.0" + "@tldraw/utils" "3.2.0" + lodash.isequal "^4.5.0" + nanoevents "^7.0.1" + ws "^8.16.0" + +"@tldraw/sync@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@tldraw/sync/-/sync-3.2.0.tgz#29a2e047df9da17de2887d2470a81157ee2dfda6" + integrity sha512-b+7Qoh942235sK8Si75BisgwpSTR572SqZJ0JTctNepg6V4YyzTrrxeSJB5A/ZbSRw8svTGG6KPyAjECWpTSJQ== + dependencies: + "@tldraw/state" "3.2.0" + "@tldraw/state-react" "3.2.0" + "@tldraw/sync-core" "3.2.0" + "@tldraw/utils" "3.2.0" + lodash.isequal "^4.5.0" + nanoevents "^7.0.1" + tldraw "3.2.0" + ws "^8.16.0" + +"@tldraw/tlschema@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@tldraw/tlschema/-/tlschema-3.2.0.tgz#a6b5560d177da1029b9e1e69894e2683d861dda3" + integrity sha512-cG+EiuFRk074KW4RDLQNHPn3PU8sVjwmx9kthl58fkVYEssqHPxnFUcz1EOFGs7HvSXMRXJD3758CVtKB4WZug== + dependencies: + "@tldraw/state" "3.2.0" + "@tldraw/store" "3.2.0" + "@tldraw/utils" "3.2.0" + "@tldraw/validate" "3.2.0" + +"@tldraw/utils@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@tldraw/utils/-/utils-3.2.0.tgz#96d2b5bebc6272c2711a9071e23491df68df020c" + integrity sha512-YXDFYfN0zIFDi+zq8UUQizFqBIvl072hKX4c04K0jDn6K7Ox9G9tmMwbA3RyFHvzfHYGV8IEY/pLEjHCH/SRoQ== + dependencies: + fractional-indexing-jittered "^0.9.1" + lodash.throttle "^4.1.1" + lodash.uniq "^4.5.0" -"@tldraw/validate@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@tldraw/validate/-/validate-2.0.2.tgz#e03948a25d581f87167af7ffcdcc4d9cff937471" - integrity sha512-dc2WxVwDf4HKP0/5AXukv/gqjrEg2fNCbKTRrxGDgvXfsRe1Gg8o9cVYlzZLRmbvj1DL8ggw4DAvbyL1FZh5Gg== +"@tldraw/validate@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@tldraw/validate/-/validate-3.2.0.tgz#b8b1177a10f4cabd21348c23fa4215bc3a91fe2f" + integrity sha512-UFt5P5kiAYoL6bX+pq2WGxfMj+ntBpLEmZhIjAa9kscAF5rDKVFrWl5BRn7Dy39+mx/+DEy/348kxgu/55lA6Q== dependencies: - "@tldraw/utils" "2.0.2" + "@tldraw/utils" "3.2.0" "@types/core-js@^2.5.5": version "2.5.8" @@ -817,13 +847,6 @@ dependencies: undici-types "~5.26.4" -"@types/node@^20.5.9": - version "20.11.17" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.17.tgz#cdd642d0e62ef3a861f88ddbc2b61e32578a9292" - integrity sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw== - dependencies: - undici-types "~5.26.4" - "@types/prop-types@*": version "15.7.11" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" @@ -911,52 +934,6 @@ resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d" integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ== -"@y-sweet/client@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@y-sweet/client/-/client-0.1.0.tgz#1d4dd2eb03c651a06695e607fca8f22b52fd4787" - integrity sha512-X9+IZ/7t7So39TxYxmmjKKZ8SCKcFJrBmnEHHEJ9l6Jj6hPfoK8uHzQiYN+R0J5CpEoGsxuPevwAQZzENYX0dQ== - dependencies: - "@y-sweet/sdk" "0.1.0" - y-protocols "^1.0.5" - -"@y-sweet/react@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@y-sweet/react/-/react-0.1.0.tgz#7c80b2fafbb7d0c90acf3e53f78d7b1b792a466f" - integrity sha512-Ss6hsVmBxnkd6iIK1t9buOLR33mJVLFvrvp1A0KuuH3EA3UN0o5KcJaxPwErgrLyZxbPillyxZ+sCECsU6/4ZA== - dependencies: - "@y-sweet/client" "0.1.0" - "@y-sweet/sdk" "0.1.0" - y-protocols "^1.0.5" - -"@y-sweet/sdk@0.1.0", "@y-sweet/sdk@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@y-sweet/sdk/-/sdk-0.1.0.tgz#bf9cfb37a405f6a07819e49707565ecef6df38fe" - integrity sha512-rQDF3p3KhRVPBRpT9e8fNq0qyQlHe/LF3IfrkP3m/PjUmtTkMY/a3x1/vdRn1KXLz3tB5h7iP7KRPDP7bEs2qw== - dependencies: - "@types/node" "^20.5.9" - -abstract-leveldown@^6.2.1: - version "6.3.0" - resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz#d25221d1e6612f820c35963ba4bd739928f6026a" - integrity sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ== - dependencies: - buffer "^5.5.0" - immediate "^3.2.3" - level-concat-iterator "~2.0.0" - level-supports "~1.0.0" - xtend "~4.0.0" - -abstract-leveldown@~6.2.1, abstract-leveldown@~6.2.3: - version "6.2.3" - resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz#036543d87e3710f2528e47040bc3261b77a9a8eb" - integrity sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ== - dependencies: - buffer "^5.5.0" - immediate "^3.2.3" - level-concat-iterator "~2.0.0" - level-supports "~1.0.0" - xtend "~4.0.0" - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -1095,11 +1072,6 @@ ast-types-flow@^0.0.8: resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - asynciterator.prototype@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz#8c5df0514936cdd133604dfcc9d3fb93f09b2b62" @@ -1141,11 +1113,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -1188,14 +1155,6 @@ buffer-equal-constant-time@1.0.1: resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== -buffer@^5.5.0, buffer@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - busboy@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" @@ -1349,14 +1308,6 @@ debug@^4.3.4: dependencies: ms "2.1.2" -deferred-leveldown@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz#27a997ad95408b61161aa69bd489b86c71b78058" - integrity sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw== - dependencies: - abstract-leveldown "~6.2.1" - inherits "^2.0.3" - define-data-property@^1.0.1, define-data-property@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" @@ -1462,16 +1413,6 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -encoding-down@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/encoding-down/-/encoding-down-6.3.0.tgz#b1c4eb0e1728c146ecaef8e32963c549e76d082b" - integrity sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw== - dependencies: - abstract-leveldown "^6.2.1" - inherits "^2.0.3" - level-codec "^9.0.0" - level-errors "^2.0.0" - enhanced-resolve@^5.12.0: version "5.15.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" @@ -1480,13 +1421,6 @@ enhanced-resolve@^5.12.0: graceful-fs "^4.2.4" tapable "^2.2.0" -errno@~0.1.1: - version "0.1.8" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" - integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== - dependencies: - prr "~1.0.1" - error-stack-parser@^2.0.6: version "2.1.4" resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" @@ -1795,6 +1729,11 @@ fraction.js@^4.3.7: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== +fractional-indexing-jittered@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/fractional-indexing-jittered/-/fractional-indexing-jittered-0.9.1.tgz#d1bf552cb0ab460ba992000c108b19c894900ba0" + integrity sha512-qyzDZ7JXWf/yZT2rQDpQwFBbIaZS2o+zb0s740vqreXQ6bFQPd8tAy4D1gGN0CUeIcnNHjuvb0EaLnqHhGV/PA== + fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -1958,26 +1897,11 @@ idb@^7.1.1: resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - ignore@^5.2.0: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== -immediate@^3.2.3: - version "3.3.0" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" - integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q== - -inherits@^2.0.3, inherits@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - inline-style-prefixer@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-7.0.0.tgz#991d550735d42069f528ac1bcdacd378d1305442" @@ -2187,11 +2111,6 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isomorphic.js@^0.2.4: - version "0.2.5" - resolved "https://registry.yarnpkg.com/isomorphic.js/-/isomorphic.js-0.2.5.tgz#13eecf36f2dba53e85d355e11bf9d4208c6f7f88" - integrity sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw== - iterator.prototype@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" @@ -2299,95 +2218,6 @@ language-tags@^1.0.9: dependencies: language-subtag-registry "^0.3.20" -level-codec@^9.0.0: - version "9.0.2" - resolved "https://registry.yarnpkg.com/level-codec/-/level-codec-9.0.2.tgz#fd60df8c64786a80d44e63423096ffead63d8cbc" - integrity sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ== - dependencies: - buffer "^5.6.0" - -level-concat-iterator@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz#1d1009cf108340252cb38c51f9727311193e6263" - integrity sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw== - -level-errors@^2.0.0, level-errors@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/level-errors/-/level-errors-2.0.1.tgz#2132a677bf4e679ce029f517c2f17432800c05c8" - integrity sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw== - dependencies: - errno "~0.1.1" - -level-iterator-stream@~4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz#7ceba69b713b0d7e22fcc0d1f128ccdc8a24f79c" - integrity sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q== - dependencies: - inherits "^2.0.4" - readable-stream "^3.4.0" - xtend "^4.0.2" - -level-js@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/level-js/-/level-js-5.0.2.tgz#5e280b8f93abd9ef3a305b13faf0b5397c969b55" - integrity sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg== - dependencies: - abstract-leveldown "~6.2.3" - buffer "^5.5.0" - inherits "^2.0.3" - ltgt "^2.1.2" - -level-packager@^5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/level-packager/-/level-packager-5.1.1.tgz#323ec842d6babe7336f70299c14df2e329c18939" - integrity sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ== - dependencies: - encoding-down "^6.3.0" - levelup "^4.3.2" - -level-supports@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-1.0.1.tgz#2f530a596834c7301622521988e2c36bb77d122d" - integrity sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg== - dependencies: - xtend "^4.0.2" - -level@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/level/-/level-6.0.1.tgz#dc34c5edb81846a6de5079eac15706334b0d7cd6" - integrity sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw== - dependencies: - level-js "^5.0.0" - level-packager "^5.1.0" - leveldown "^5.4.0" - -leveldown@^5.4.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-5.6.0.tgz#16ba937bb2991c6094e13ac5a6898ee66d3eee98" - integrity sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ== - dependencies: - abstract-leveldown "~6.2.1" - napi-macros "~2.0.0" - node-gyp-build "~4.1.0" - -levelup@^4.3.2: - version "4.4.0" - resolved "https://registry.yarnpkg.com/levelup/-/levelup-4.4.0.tgz#f89da3a228c38deb49c48f88a70fb71f01cafed6" - integrity sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ== - dependencies: - deferred-leveldown "~5.3.0" - level-errors "~2.0.0" - level-iterator-stream "~4.0.0" - level-supports "~1.0.0" - xtend "~4.0.0" - -lib0@^0.2.31, lib0@^0.2.43, lib0@^0.2.52, lib0@^0.2.85, lib0@^0.2.86: - version "0.2.88" - resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.88.tgz#18618e0c3b63f6260255eb760f9247d9cc6c6a5b" - integrity sha512-KyroiEvCeZcZEMx5Ys+b4u4eEBbA1ch7XUaBhYpwa/nPMrzTjUhI4RfcytmQfYoTBPcdyx+FX6WFNIoNuJzJfQ== - dependencies: - isomorphic.js "^0.2.4" - lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -2403,11 +2233,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -2477,11 +2302,6 @@ lru-cache@^6.0.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== -ltgt@^2.1.2: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" - integrity sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA== - lz-string@^1.4.4: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" @@ -2567,21 +2387,16 @@ nano-css@^5.6.1: stacktrace-js "^2.0.2" stylis "^4.3.0" -nanoid@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e" - integrity sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw== +nanoevents@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/nanoevents/-/nanoevents-7.0.1.tgz#181580b47787688d8cac775b977b1cf24e26e570" + integrity sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q== nanoid@^3.3.6, nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== -napi-macros@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" - integrity sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg== - next@^14.1.0: version "14.1.0" resolved "https://registry.yarnpkg.com/next/-/next-14.1.0.tgz#b31c0261ff9caa6b4a17c5af019ed77387174b69" @@ -2605,11 +2420,6 @@ next@^14.1.0: "@next/swc-win32-ia32-msvc" "14.1.0" "@next/swc-win32-x64-msvc" "14.1.0" -node-gyp-build@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb" - integrity sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ== - node-releases@^2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" @@ -2846,11 +2656,6 @@ prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -2998,15 +2803,6 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -readable-stream@^3.4.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -3097,7 +2893,7 @@ safe-array-concat@^1.0.1: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@^5.0.1, safe-buffer@~5.2.0: +safe-buffer@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -3311,13 +3107,6 @@ string.prototype.trimstart@^1.0.7: define-properties "^1.2.0" es-abstract "^1.22.1" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: name strip-ansi-cjs version "6.0.1" @@ -3430,10 +3219,10 @@ throttle-debounce@^3.0.1: resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb" integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg== -tldraw@2.0.2, tldraw@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/tldraw/-/tldraw-2.0.2.tgz#2d4efd6671c363ceec53d1ea57b872fbd90c1f1d" - integrity sha512-qWIbsZkbCVcexGHtUBBjT28+upgzwSu3yImtQXRbfwDj8wbVadxxczAK9Uh6gSsRqlEx8WaQRu3Nkv/3QJSTQQ== +tldraw@3.2.0, tldraw@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/tldraw/-/tldraw-3.2.0.tgz#49fbf2a64d07a6216ab489482c550aaedc029aed" + integrity sha512-bu5vByx5g6JtBht7cYvOiv42W5uFcNCr8lCZxqIgORMCkg8EoWxDnxbkah2ZNJzG+cOkm/0G9JJlDfdKyaLy0A== dependencies: "@radix-ui/react-alert-dialog" "^1.0.5" "@radix-ui/react-context-menu" "^2.1.5" @@ -3443,10 +3232,12 @@ tldraw@2.0.2, tldraw@^2.0.2: "@radix-ui/react-select" "^1.2.0" "@radix-ui/react-slider" "^1.1.0" "@radix-ui/react-toast" "^1.1.1" - "@tldraw/editor" "2.0.2" + "@tldraw/editor" "3.2.0" + "@tldraw/store" "3.2.0" canvas-size "^1.2.6" classnames "^2.3.2" hotkeys-js "^3.11.2" + idb "^7.1.1" lz-string "^1.4.4" to-regex-range@^5.0.1: @@ -3595,7 +3386,7 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -util-deprecate@^1.0.1, util-deprecate@^1.0.2: +util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== @@ -3675,56 +3466,10 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" -ws@^6.2.1: - version "6.2.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" - integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== - dependencies: - async-limiter "~1.0.0" - -xtend@^4.0.2, xtend@~4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y-leveldb@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/y-leveldb/-/y-leveldb-0.1.2.tgz#43f6c5004b6891b57926d8a1e0eb0c883003e34b" - integrity sha512-6ulEn5AXfXJYi89rXPEg2mMHAyyw8+ZfeMMdOtBbV8FJpQ1NOrcgi6DTAcXof0dap84NjHPT2+9d0rb6cFsjEg== - dependencies: - level "^6.0.1" - lib0 "^0.2.31" - -y-protocols@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/y-protocols/-/y-protocols-1.0.6.tgz#66dad8a95752623443e8e28c0e923682d2c0d495" - integrity sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q== - dependencies: - lib0 "^0.2.85" - -y-sweet@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/y-sweet/-/y-sweet-0.1.0.tgz#2368fb54730aafc5e7f1221b0e02bc093e660ba8" - integrity sha512-45+EQQBMrDp9W5RS89HYqgX3B6W168i1O6otzEp68NrZ/OqiEjcMo1EdCAclaFM38Pcpu67gL/jrVRq5vD4gtQ== - -y-utility@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/y-utility/-/y-utility-0.1.3.tgz#f5c28a86ab7f0cd277d03ffed474c84a71bf302c" - integrity sha512-o9aXG5ZG4c/QgiK1Bt9UDXGVCNwn0dLti/rZSPTsjtuvwH6sshslU2SfoW65pfZqjLJYEHclM/JtUPPjv05lLw== - dependencies: - lib0 "^0.2.43" - -y-websocket@^1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/y-websocket/-/y-websocket-1.5.3.tgz#1648ae43e820639307237e92f8bde2ac067a41f6" - integrity sha512-wVpKunsDUnVApOymp/ZtaXVyb1Q3csxGdono8Lx6E5k11OfupKUJZV86Eb16mpdUTkEdi7I7L1LVBlZlwd92bA== - dependencies: - lib0 "^0.2.52" - lodash.debounce "^4.0.8" - y-protocols "^1.0.5" - optionalDependencies: - ws "^6.2.1" - y-leveldb "^0.1.0" +ws@^8.16.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== yallist@^4.0.0: version "4.0.0" @@ -3736,13 +3481,6 @@ yaml@^2.3.4: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== -yjs@^13.6.11: - version "13.6.11" - resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.11.tgz#2edc796981700576abd577bc1f6a25edbb2f08f8" - integrity sha512-FvRRJKX9u270dOLkllGF/UDCWwmIv2Z+ucM4v1QO1TuxdmoiMnSUXH1HAcOKOrkBEhQtPTkxep7tD2DrQB+l0g== - dependencies: - lib0 "^0.2.86" - zustand@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.2.tgz#7b44c4f4a5bfd7a8296a3957b13e1c346f42514d" diff --git a/web/yjs/callback.cjs b/web/yjs/callback.cjs deleted file mode 100644 index 9b9226f..0000000 --- a/web/yjs/callback.cjs +++ /dev/null @@ -1,76 +0,0 @@ -const http = require('http') - -const CALLBACK_URL = process.env.CALLBACK_URL ? new URL(process.env.CALLBACK_URL) : null -const CALLBACK_TIMEOUT = process.env.CALLBACK_TIMEOUT || 5000 -const CALLBACK_OBJECTS = process.env.CALLBACK_OBJECTS ? JSON.parse(process.env.CALLBACK_OBJECTS) : {} - -exports.isCallbackSet = !!CALLBACK_URL - -/** - * @param {Uint8Array} update - * @param {any} origin - * @param {WSSharedDoc} doc - */ -exports.callbackHandler = (update, origin, doc) => { - const room = doc.name - const dataToSend = { - room, - data: {} - } - const sharedObjectList = Object.keys(CALLBACK_OBJECTS) - sharedObjectList.forEach(sharedObjectName => { - const sharedObjectType = CALLBACK_OBJECTS[sharedObjectName] - dataToSend.data[sharedObjectName] = { - type: sharedObjectType, - content: getContent(sharedObjectName, sharedObjectType, doc).toJSON() - } - }) - callbackRequest(CALLBACK_URL, CALLBACK_TIMEOUT, dataToSend) -} - -/** - * @param {URL} url - * @param {number} timeout - * @param {Object} data - */ -const callbackRequest = (url, timeout, data) => { - data = JSON.stringify(data) - const options = { - hostname: url.hostname, - port: url.port, - path: url.pathname, - timeout, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': data.length - } - } - const req = http.request(options) - req.on('timeout', () => { - console.warn('Callback request timed out.') - req.abort() - }) - req.on('error', (e) => { - console.error('Callback request error.', e) - req.abort() - }) - req.write(data) - req.end() -} - -/** - * @param {string} objName - * @param {string} objType - * @param {WSSharedDoc} doc - */ -const getContent = (objName, objType, doc) => { - switch (objType) { - case 'Array': return doc.getArray(objName) - case 'Map': return doc.getMap(objName) - case 'Text': return doc.getText(objName) - case 'XmlFragment': return doc.getXmlFragment(objName) - case 'XmlElement': return doc.getXmlElement(objName) - default : return {} - } -} \ No newline at end of file diff --git a/web/yjs/server.js b/web/yjs/server.js deleted file mode 100755 index d30a99c..0000000 --- a/web/yjs/server.js +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env node - -import http from 'http'; -import josnwebtoken from 'jsonwebtoken'; -import WebSocket from 'ws'; -import { setupWSConnection } from './util.cjs'; - -// max payload size is 512MB -const wss = new WebSocket.Server({ noServer: true, maxPayload: 1024 * 1024 * 512}); - -const host = process.env.HOST ?? '127.0.0.1' -const port = process.env.PORT ?? 1234 -const jwtKey = parseEnv(process.env.SECRET ?? '') - -const server = http.createServer((request, response) => { - response.writeHead(200, { 'Content-Type': 'text/plain' }) - response.end('okay') -}) - -wss.on('connection', setupWSConnection) - -server.on('upgrade', (request, socket, head) => { - // You may check auth of request here.. - // See https://github.com/websockets/ws#client-authentication - /** - * @param {any} ws - */ - const handleAuth = ws => { - try { - // // parse cookie 'scToken' - // const cookies = parseCookie(request.headers.cookie); - // if (!cookies.scToken) { - // throw new Error('No cookie') - // } - - // try { - // const payload = josnwebtoken.verify(cookies.scToken, jwtKey); - - // // if user == managing etc.. - - // console.log('JWT Payload', payload); - // } catch (e) { - // throw new Error('invalid scToken') - // } - - wss.emit('connection', ws, request) - } catch (e) { - console.error(e.message); - ws.close(1008, 'Not authorized') - } - } - try { - wss.handleUpgrade(request, socket, head, handleAuth) - } catch (e) { - console.error(e.message); - socket.close() - } -}) - -server.listen(port, host, () => { - console.log(`running at '${host}' on port ${port}`) -}) - -export function parseCookie(str) { - if (!str || str.trim() === '') { - return {}; - } - - return str - .split(';') - .map(v => v.split('=')) - .reduce((acc, v) => { - acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim()); - return acc; - }, {}); -} - -function parseEnv(str) { - if (typeof str !== 'string') { - return ''; - } - if (str.charAt(0) === '"' && str.charAt(str.length - 1) === '"') { - return str.substr(1, str.length - 2); - } - return str; -} \ No newline at end of file diff --git a/web/yjs/util.cjs b/web/yjs/util.cjs deleted file mode 100644 index 007e5c6..0000000 --- a/web/yjs/util.cjs +++ /dev/null @@ -1,286 +0,0 @@ -const Y = require('yjs') -const syncProtocol = require('y-protocols/dist/sync.cjs') -const awarenessProtocol = require('y-protocols/dist/awareness.cjs') - -const encoding = require('lib0/dist/encoding.cjs') -const decoding = require('lib0/dist/decoding.cjs') -const map = require('lib0/dist/map.cjs') - -const debounce = require('lodash.debounce') - -const callbackHandler = require('./callback.cjs').callbackHandler -const isCallbackSet = require('./callback.cjs').isCallbackSet - -const CALLBACK_DEBOUNCE_WAIT = parseInt(process.env.CALLBACK_DEBOUNCE_WAIT) || 2000 -const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(process.env.CALLBACK_DEBOUNCE_MAXWAIT) || 10000 - -const wsReadyStateConnecting = 0 -const wsReadyStateOpen = 1 -const wsReadyStateClosing = 2 // eslint-disable-line -const wsReadyStateClosed = 3 // eslint-disable-line - -// disable gc when using snapshots! -const gcEnabled = process.env.GC !== 'false' && process.env.GC !== '0' -const persistenceDir = process.env.YPERSISTENCE -/** - * @type {{bindState: function(string,WSSharedDoc):void, writeState:function(string,WSSharedDoc):Promise, provider: any}|null} - */ -let persistence = null -if (typeof persistenceDir === 'string') { - console.info('Persisting documents to "' + persistenceDir + '"') - // @ts-ignore - const LeveldbPersistence = require('y-leveldb').LeveldbPersistence - const ldb = new LeveldbPersistence(persistenceDir) - persistence = { - provider: ldb, - bindState: async (docName, ydoc) => { - const persistedYdoc = await ldb.getYDoc(docName) - const newUpdates = Y.encodeStateAsUpdate(ydoc) - ldb.storeUpdate(docName, newUpdates) - Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc)) - ydoc.on('update', update => { - ldb.storeUpdate(docName, update) - }) - }, - writeState: async (docName, ydoc) => {} - } -} - -/** - * @param {{bindState: function(string,WSSharedDoc):void, - * writeState:function(string,WSSharedDoc):Promise,provider:any}|null} persistence_ - */ -exports.setPersistence = persistence_ => { - persistence = persistence_ -} - -/** - * @return {null|{bindState: function(string,WSSharedDoc):void, - * writeState:function(string,WSSharedDoc):Promise}|null} used persistence layer - */ -exports.getPersistence = () => persistence - -/** - * @type {Map} - */ -const docs = new Map() -// exporting docs so that others can use it -exports.docs = docs - -const messageSync = 0 -const messageAwareness = 1 -// const messageAuth = 2 - -/** - * @param {Uint8Array} update - * @param {any} origin - * @param {WSSharedDoc} doc - */ -const updateHandler = (update, origin, doc) => { - const encoder = encoding.createEncoder() - encoding.writeVarUint(encoder, messageSync) - syncProtocol.writeUpdate(encoder, update) - const message = encoding.toUint8Array(encoder) - doc.conns.forEach((_, conn) => send(doc, conn, message)) -} - -class WSSharedDoc extends Y.Doc { - /** - * @param {string} name - */ - constructor (name) { - super({ gc: gcEnabled }) - this.name = name - /** - * Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed - * @type {Map>} - */ - this.conns = new Map() - /** - * @type {awarenessProtocol.Awareness} - */ - this.awareness = new awarenessProtocol.Awareness(this) - this.awareness.setLocalState(null) - /** - * @param {{ added: Array, updated: Array, removed: Array }} changes - * @param {Object | null} conn Origin is the connection that made the change - */ - const awarenessChangeHandler = ({ added, updated, removed }, conn) => { - const changedClients = added.concat(updated, removed) - if (conn !== null) { - const connControlledIDs = /** @type {Set} */ (this.conns.get(conn)) - if (connControlledIDs !== undefined) { - added.forEach(clientID => { connControlledIDs.add(clientID) }) - removed.forEach(clientID => { connControlledIDs.delete(clientID) }) - } - } - // broadcast awareness update - const encoder = encoding.createEncoder() - encoding.writeVarUint(encoder, messageAwareness) - encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients)) - const buff = encoding.toUint8Array(encoder) - this.conns.forEach((_, c) => { - send(this, c, buff) - }) - } - this.awareness.on('update', awarenessChangeHandler) - this.on('update', updateHandler) - if (isCallbackSet) { - this.on('update', debounce( - callbackHandler, - CALLBACK_DEBOUNCE_WAIT, - { maxWait: CALLBACK_DEBOUNCE_MAXWAIT } - )) - } - } -} - -/** - * Gets a Y.Doc by name, whether in memory or on disk - * - * @param {string} docname - the name of the Y.Doc to find or create - * @param {boolean} gc - whether to allow gc on the doc (applies only when created) - * @return {WSSharedDoc} - */ -const getYDoc = (docname, gc = true) => map.setIfUndefined(docs, docname, () => { - const doc = new WSSharedDoc(docname) - doc.gc = gc - if (persistence !== null) { - persistence.bindState(docname, doc) - } - docs.set(docname, doc) - return doc -}) - -exports.getYDoc = getYDoc - -/** - * @param {any} conn - * @param {WSSharedDoc} doc - * @param {Uint8Array} message - */ -const messageListener = (conn, doc, message) => { - try { - const encoder = encoding.createEncoder() - const decoder = decoding.createDecoder(message) - const messageType = decoding.readVarUint(decoder) - switch (messageType) { - case messageSync: - encoding.writeVarUint(encoder, messageSync) - syncProtocol.readSyncMessage(decoder, encoder, doc, conn) - - // If the `encoder` only contains the type of reply message and no - // message, there is no need to send the message. When `encoder` only - // contains the type of reply, its length is 1. - if (encoding.length(encoder) > 1) { - send(doc, conn, encoding.toUint8Array(encoder)) - } - break - case messageAwareness: { - awarenessProtocol.applyAwarenessUpdate(doc.awareness, decoding.readVarUint8Array(decoder), conn) - break - } - } - } catch (err) { - console.error(err) - doc.emit('error', [err]) - } -} - -/** - * @param {WSSharedDoc} doc - * @param {any} conn - */ -const closeConn = (doc, conn) => { - if (doc.conns.has(conn)) { - /** - * @type {Set} - */ - // @ts-ignore - const controlledIds = doc.conns.get(conn) - doc.conns.delete(conn) - awarenessProtocol.removeAwarenessStates(doc.awareness, Array.from(controlledIds), null) - if (doc.conns.size === 0 && persistence !== null) { - // if persisted, we store state and destroy ydocument - persistence.writeState(doc.name, doc).then(() => { - doc.destroy() - }) - docs.delete(doc.name) - } - } - conn.close() -} - -/** - * @param {WSSharedDoc} doc - * @param {any} conn - * @param {Uint8Array} m - */ -const send = (doc, conn, m) => { - if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) { - closeConn(doc, conn) - } - try { - conn.send(m, /** @param {any} err */ err => { err != null && closeConn(doc, conn) }) - } catch (e) { - closeConn(doc, conn) - } -} - -const pingTimeout = 30000 - -/** - * @param {any} conn - * @param {any} req - * @param {any} opts - */ -exports.setupWSConnection = (conn, req, { docName = req.url.slice(1).split('?')[0], gc = true } = {}) => { - conn.binaryType = 'arraybuffer' - // get doc, initialize if it does not exist yet - const doc = getYDoc(docName, gc) - doc.conns.set(conn, new Set()) - // listen and reply to events - conn.on('message', /** @param {ArrayBuffer} message */ message => messageListener(conn, doc, new Uint8Array(message))) - - // Check if connection is still alive - let pongReceived = true - const pingInterval = setInterval(() => { - if (!pongReceived) { - if (doc.conns.has(conn)) { - closeConn(doc, conn) - } - clearInterval(pingInterval) - } else if (doc.conns.has(conn)) { - pongReceived = false - try { - conn.ping() - } catch (e) { - closeConn(doc, conn) - clearInterval(pingInterval) - } - } - }, pingTimeout) - conn.on('close', () => { - closeConn(doc, conn) - clearInterval(pingInterval) - }) - conn.on('pong', () => { - pongReceived = true - }) - // put the following in a variables in a block so the interval handlers don't keep in in - // scope - { - // send sync step 1 - const encoder = encoding.createEncoder() - encoding.writeVarUint(encoder, messageSync) - syncProtocol.writeSyncStep1(encoder, doc) - send(doc, conn, encoding.toUint8Array(encoder)) - const awarenessStates = doc.awareness.getStates() - if (awarenessStates.size > 0) { - const encoder = encoding.createEncoder() - encoding.writeVarUint(encoder, messageAwareness) - encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys()))) - send(doc, conn, encoding.toUint8Array(encoder)) - } - } -} \ No newline at end of file