put sync stuff in bemo worker (#4060)
this PR puts sync stuff in the bemo worker, and sets up a temporary dev-only page in dotcom for testing bemo stuff ### Change type - [ ] `bugfix` - [ ] `improvement` - [x] `feature` - [ ] `api` - [ ] `other` ### Test plan 1. Create a shape... 2. - [ ] Unit tests - [ ] End to end tests ### Release notes - Fixed a bug with...
This commit is contained in:
parent
8906bd8ffa
commit
c1fe8ec99a
83 changed files with 571 additions and 120 deletions
|
@ -32,3 +32,5 @@ apps/vscode/extension/editor/tldraw-assets.json
|
|||
apps/dotcom/public/sw.js
|
||||
|
||||
patchedJestJsDom.js
|
||||
|
||||
**/.clasp.json
|
2
.ignore
2
.ignore
|
@ -28,3 +28,5 @@ apps/docs/utils/vector-db
|
|||
apps/docs/content/releases/**/*
|
||||
apps/docs/content/reference/**/*
|
||||
packages/**/api
|
||||
|
||||
**/.clasp.json
|
|
@ -28,3 +28,5 @@ apps/docs/content/reference/**/*
|
|||
**/.out/*
|
||||
**/.temp/*
|
||||
apps/dotcom/public/**/*.*
|
||||
|
||||
**/.clasp.json
|
|
@ -21,9 +21,10 @@
|
|||
"dependencies": {
|
||||
"@tldraw/dotcom-shared": "workspace:*",
|
||||
"@tldraw/store": "workspace:*",
|
||||
"@tldraw/sync": "workspace:*",
|
||||
"@tldraw/tlschema": "workspace:*",
|
||||
"@tldraw/tlsync": "workspace:*",
|
||||
"@tldraw/utils": "workspace:*",
|
||||
"@tldraw/validate": "workspace:*",
|
||||
"@tldraw/worker-shared": "workspace:*",
|
||||
"itty-router": "^4.0.13",
|
||||
"nanoid": "4.0.2",
|
||||
|
|
|
@ -1,9 +1,33 @@
|
|||
import { createSentry } from '@tldraw/worker-shared'
|
||||
import { RoomSnapshot, TLCloseEventCode, TLSocketRoom } from '@tldraw/sync'
|
||||
import { TLRecord } from '@tldraw/tlschema'
|
||||
import { throttle } from '@tldraw/utils'
|
||||
import { T } from '@tldraw/validate'
|
||||
import { createPersistQueue, createSentry, parseRequestQuery } from '@tldraw/worker-shared'
|
||||
import { DurableObject } from 'cloudflare:workers'
|
||||
import { Router } from 'itty-router'
|
||||
import { IRequest, Router } from 'itty-router'
|
||||
import { Environment } from './types'
|
||||
|
||||
const connectRequestQuery = T.object({
|
||||
sessionKey: T.string,
|
||||
storeId: T.string.optional(),
|
||||
})
|
||||
|
||||
export class BemoDO extends DurableObject<Environment> {
|
||||
r2: R2Bucket
|
||||
_slug: string | null = null
|
||||
|
||||
constructor(
|
||||
public state: DurableObjectState,
|
||||
env: Environment
|
||||
) {
|
||||
super(state, env)
|
||||
this.r2 = env.BEMO_BUCKET
|
||||
|
||||
state.blockConcurrencyWhile(async () => {
|
||||
this._slug = ((await this.state.storage.get('slug')) ?? null) as string | null
|
||||
})
|
||||
}
|
||||
|
||||
private reportError(e: unknown, request?: Request) {
|
||||
const sentry = createSentry(this.ctx, this.env, request)
|
||||
console.error(e)
|
||||
|
@ -12,19 +36,18 @@ export class BemoDO extends DurableObject<Environment> {
|
|||
}
|
||||
|
||||
private readonly router = Router()
|
||||
.get('/do', async () => {
|
||||
return Response.json({ message: 'Hello from a durable object!' })
|
||||
.all('/connect/:slug', async (req) => {
|
||||
if (!this._slug) {
|
||||
await this.state.blockConcurrencyWhile(async () => {
|
||||
await this.state.storage.put('slug', req.params.slug)
|
||||
this._slug = req.params.slug
|
||||
})
|
||||
.get('/do/error', async () => {
|
||||
this.doAnError()
|
||||
}
|
||||
return this.handleConnect(req)
|
||||
})
|
||||
.all('*', async () => new Response('Not found', { status: 404 }))
|
||||
|
||||
private doAnError() {
|
||||
throw new Error('this is an error from a DO')
|
||||
}
|
||||
|
||||
override async fetch(request: Request<unknown, CfProperties<unknown>>): Promise<Response> {
|
||||
override async fetch(request: Request): Promise<Response> {
|
||||
try {
|
||||
return await this.router.handle(request)
|
||||
} catch (error) {
|
||||
|
@ -35,4 +58,148 @@ export class BemoDO extends DurableObject<Environment> {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
async handleConnect(req: IRequest) {
|
||||
// extract query params from request, should include instanceId
|
||||
const { sessionKey } = parseRequestQuery(req, connectRequestQuery)
|
||||
|
||||
// Create the websocket pair for the client
|
||||
const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair()
|
||||
serverWebSocket.accept()
|
||||
|
||||
try {
|
||||
const room = await this.getRoom()
|
||||
// Don't connect if we're already at max connections
|
||||
if (room.getNumActiveSessions() >= MAX_CONNECTIONS) {
|
||||
// TODO: this is not handled on the client, it just gets stuck in a loading state.
|
||||
// With hibernatable sockets it should be fine to send a .close() event here.
|
||||
// but we should really handle unknown errors better on the client.
|
||||
return new Response('Room is full', {
|
||||
status: 403,
|
||||
})
|
||||
}
|
||||
|
||||
// all good
|
||||
room.handleSocketConnect(sessionKey, serverWebSocket)
|
||||
return new Response(null, { status: 101, webSocket: clientWebSocket })
|
||||
} catch (e) {
|
||||
if (e === ROOM_NOT_FOUND) {
|
||||
serverWebSocket.close(TLCloseEventCode.NOT_FOUND, 'Room not found')
|
||||
return new Response(null, { status: 101, webSocket: clientWebSocket })
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// For TLSyncRoom
|
||||
_room: Promise<TLSocketRoom<TLRecord, void>> | null = null
|
||||
|
||||
getSlug() {
|
||||
if (!this._slug) {
|
||||
throw new Error('slug must be present')
|
||||
}
|
||||
return this._slug
|
||||
}
|
||||
|
||||
getRoom() {
|
||||
const slug = this.getSlug()
|
||||
if (!this._room) {
|
||||
this._room = this.loadFromDatabase(slug).then((result) => {
|
||||
return new TLSocketRoom<TLRecord, void>({
|
||||
initialSnapshot: result.type === 'room_found' ? result.snapshot : undefined,
|
||||
onSessionRemoved: async (room, args) => {
|
||||
if (args.numSessionsRemaining > 0) return
|
||||
if (!this._room) return
|
||||
try {
|
||||
await this.persistToDatabase()
|
||||
} catch (err) {
|
||||
// already logged
|
||||
}
|
||||
this._room = null
|
||||
room.close()
|
||||
},
|
||||
onDataChange: () => {
|
||||
// when we send a message, we make sure to persist the room
|
||||
this.triggerPersistSchedule()
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
return this._room
|
||||
}
|
||||
|
||||
triggerPersistSchedule = throttle(() => {
|
||||
this.schedulePersist()
|
||||
}, 2000)
|
||||
|
||||
async loadFromDatabase(persistenceKey: string): Promise<DBLoadResult> {
|
||||
try {
|
||||
const key = getR2KeyForSlug(persistenceKey)
|
||||
// when loading, prefer to fetch documents from the bucket
|
||||
const roomFromBucket = await this.r2.get(key)
|
||||
if (roomFromBucket) {
|
||||
return { type: 'room_found', snapshot: await roomFromBucket.json() }
|
||||
}
|
||||
return { type: 'room_not_found' }
|
||||
} catch (error) {
|
||||
console.error('failed to fetch doc', persistenceKey, error)
|
||||
return { type: 'error', error: error as Error }
|
||||
}
|
||||
}
|
||||
|
||||
_lastPersistedClock: number | null = null
|
||||
_persistQueue = createPersistQueue(async () => {
|
||||
// check whether the worker was woken up to persist after having gone to sleep
|
||||
if (!this._room) return
|
||||
const slug = this.getSlug()
|
||||
const room = await this.getRoom()
|
||||
const clock = room.getCurrentDocumentClock()
|
||||
if (this._lastPersistedClock === clock) return
|
||||
|
||||
const snapshot = JSON.stringify(room.getCurrentSnapshot())
|
||||
|
||||
const key = getR2KeyForSlug(slug)
|
||||
await Promise.all([this.r2.put(key, snapshot)])
|
||||
this._lastPersistedClock = clock
|
||||
// use a shorter timeout for this 'inner' loop than the 'outer' alarm-scheduled loop
|
||||
// just in case there's any possibility of setting up a neverending queue
|
||||
}, PERSIST_INTERVAL_MS / 2)
|
||||
|
||||
// Save the room to supabase
|
||||
async persistToDatabase() {
|
||||
await this._persistQueue()
|
||||
}
|
||||
|
||||
async schedulePersist() {
|
||||
const existing = await this.state.storage.getAlarm()
|
||||
if (!existing) {
|
||||
this.state.storage.setAlarm(PERSIST_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
// Will be called automatically when the alarm ticks.
|
||||
override async alarm() {
|
||||
this.persistToDatabase()
|
||||
}
|
||||
}
|
||||
|
||||
function getR2KeyForSlug(persistenceKey: string) {
|
||||
return `rooms/${persistenceKey}`
|
||||
}
|
||||
|
||||
const ROOM_NOT_FOUND = new Error('Room not found')
|
||||
const MAX_CONNECTIONS = 30
|
||||
const PERSIST_INTERVAL_MS = 5000
|
||||
|
||||
type DBLoadResult =
|
||||
| {
|
||||
type: 'error'
|
||||
error?: Error | undefined
|
||||
}
|
||||
| {
|
||||
type: 'room_found'
|
||||
snapshot: RoomSnapshot
|
||||
}
|
||||
| {
|
||||
type: 'room_not_found'
|
||||
}
|
||||
|
|
|
@ -41,10 +41,13 @@ export default class Worker extends WorkerEntrypoint<Environment> {
|
|||
const query = parseRequestQuery(request, urlMetadataQueryValidator)
|
||||
return Response.json(await getUrlMetadata(query))
|
||||
})
|
||||
.get('/do', async (request) => {
|
||||
const bemo = this.env.BEMO_DO.get(this.env.BEMO_DO.idFromName('bemo-do'))
|
||||
const message = await (await bemo.fetch(request)).json()
|
||||
return Response.json(message)
|
||||
.get('/connect/:slug', (request) => {
|
||||
const slug = request.params.slug
|
||||
if (!slug) return new Response('Not found', { status: 404 })
|
||||
|
||||
// Set up the durable object for this room
|
||||
const id = this.env.BEMO_DO.idFromName(slug)
|
||||
return this.env.BEMO_DO.get(id).fetch(request)
|
||||
})
|
||||
.all('*', notFound)
|
||||
|
||||
|
|
|
@ -11,19 +11,22 @@
|
|||
"path": "../../packages/dotcom-shared"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/worker-shared"
|
||||
"path": "../../packages/store"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/store"
|
||||
"path": "../../packages/sync"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/tlschema"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/tlsync"
|
||||
"path": "../../packages/utils"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/utils"
|
||||
"path": "../../packages/validate"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/worker-shared"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ We have a [sockets example](https://github.com/tldraw/tldraw-sockets-example) th
|
|||
|
||||
### Our own sync engine
|
||||
|
||||
We developed our own sync engine for use on tldraw.com based on a push/pull/rebase-style algorithm. It powers our "shared projects", such as [this one](https://tldraw.com/r). The engine's source code can be found [here](https://github.com/tldraw/tldraw/tree/main/packages/tlsync). It was designed to be hosted on Cloudflare workers with [DurableObjects](https://developers.cloudflare.com/durable-objects/).
|
||||
We developed our own sync engine for use on tldraw.com based on a push/pull/rebase-style algorithm. It powers our "shared projects", such as [this one](https://tldraw.com/r). The engine's source code can be found [here](https://github.com/tldraw/tldraw/tree/main/packages/sync). It was designed to be hosted on Cloudflare workers with [DurableObjects](https://developers.cloudflare.com/durable-objects/).
|
||||
|
||||
We don't suggest using this code directly. However, like our other examples, it may serve as a good reference for your own sync engine.
|
||||
|
||||
|
|
|
@ -409,7 +409,7 @@ Tldraw ships with a local-only sync engine based on `IndexedDb` and `BroadcastCh
|
|||
### Tldraw.com sync engine
|
||||
|
||||
[tldraw.com/r](https://tldraw.com/r) currently uses a simple custom sync engine based on a push/pull/rebase-style algorithm.
|
||||
It can be found [here](https://github.com/tldraw/tldraw/tree/main/packages/tlsync).
|
||||
It can be found [here](https://github.com/tldraw/tldraw/tree/main/packages/sync).
|
||||
It was optimized for Cloudflare workers with [DurableObjects](https://developers.cloudflare.com/durable-objects/)
|
||||
|
||||
We don't suggest using our code directly yet, but it may serve as a good reference for your own sync engine.
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
"@supabase/supabase-js": "^2.33.2",
|
||||
"@tldraw/dotcom-shared": "workspace:*",
|
||||
"@tldraw/store": "workspace:*",
|
||||
"@tldraw/sync": "workspace:*",
|
||||
"@tldraw/tlschema": "workspace:*",
|
||||
"@tldraw/tlsync": "workspace:*",
|
||||
"@tldraw/utils": "workspace:*",
|
||||
"@tldraw/validate": "workspace:*",
|
||||
"@tldraw/worker-shared": "workspace:*",
|
||||
|
|
|
@ -9,21 +9,20 @@ import {
|
|||
ROOM_PREFIX,
|
||||
type RoomOpenMode,
|
||||
} from '@tldraw/dotcom-shared'
|
||||
import { TLRecord } from '@tldraw/tlschema'
|
||||
import {
|
||||
RoomSnapshot,
|
||||
TLCloseEventCode,
|
||||
TLSocketRoom,
|
||||
type PersistedRoomSnapshotForSupabase,
|
||||
} from '@tldraw/tlsync'
|
||||
} from '@tldraw/sync'
|
||||
import { TLRecord } from '@tldraw/tlschema'
|
||||
import { assert, assertExists, exhaustiveSwitchError } from '@tldraw/utils'
|
||||
import { createSentry } from '@tldraw/worker-shared'
|
||||
import { createPersistQueue, createSentry } from '@tldraw/worker-shared'
|
||||
import { IRequest, Router } from 'itty-router'
|
||||
import { AlarmScheduler } from './AlarmScheduler'
|
||||
import { PERSIST_INTERVAL_MS } from './config'
|
||||
import { getR2KeyForRoom } from './r2'
|
||||
import { Analytics, DBLoadResult, Environment, TLServerEvent } from './types'
|
||||
import { createPersistQueue } from './utils/createPersistQueue'
|
||||
import { createSupabaseClient } from './utils/createSupabaseClient'
|
||||
import { getSlug } from './utils/roomOpenMode'
|
||||
import { throttle } from './utils/throttle'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CreateRoomRequestBody } from '@tldraw/dotcom-shared'
|
||||
import { RoomSnapshot, schema } from '@tldraw/tlsync'
|
||||
import { RoomSnapshot, schema } from '@tldraw/sync'
|
||||
import { IRequest } from 'itty-router'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { getR2KeyForRoom } from '../r2'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CreateSnapshotRequestBody } from '@tldraw/dotcom-shared'
|
||||
import { RoomSnapshot } from '@tldraw/tlsync'
|
||||
import { RoomSnapshot } from '@tldraw/sync'
|
||||
import { IRequest } from 'itty-router'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { getR2KeyForSnapshot } from '../r2'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RoomSnapshot } from '@tldraw/tlsync'
|
||||
import { RoomSnapshot } from '@tldraw/sync'
|
||||
import { notFound } from '@tldraw/worker-shared'
|
||||
import { IRequest } from 'itty-router'
|
||||
import { getR2KeyForSnapshot } from '../r2'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// https://developers.cloudflare.com/analytics/analytics-engine/
|
||||
|
||||
import { RoomSnapshot } from '@tldraw/tlsync'
|
||||
import { RoomSnapshot } from '@tldraw/sync'
|
||||
|
||||
// This type isn't available in @cloudflare/workers-types yet
|
||||
export interface Analytics {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { SerializedSchema, SerializedStore } from '@tldraw/store'
|
||||
import { schema } from '@tldraw/sync'
|
||||
import { TLRecord } from '@tldraw/tlschema'
|
||||
import { schema } from '@tldraw/tlsync'
|
||||
import { Result, objectMapEntries } from '@tldraw/utils'
|
||||
|
||||
interface SnapshotRequestBody {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
"path": "../../packages/tlschema"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/tlsync"
|
||||
"path": "../../packages/sync"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/utils"
|
||||
|
|
|
@ -24,7 +24,8 @@
|
|||
"@sentry/react": "^7.77.0",
|
||||
"@tldraw/assets": "workspace:*",
|
||||
"@tldraw/dotcom-shared": "workspace:*",
|
||||
"@tldraw/tlsync": "workspace:*",
|
||||
"@tldraw/sync": "workspace:*",
|
||||
"@tldraw/sync-react": "workspace:*",
|
||||
"@tldraw/utils": "workspace:*",
|
||||
"@vercel/analytics": "^1.1.1",
|
||||
"browser-fs-access": "^0.35.0",
|
||||
|
@ -54,7 +55,6 @@
|
|||
"ws": "^8.16.0"
|
||||
},
|
||||
"jest": {
|
||||
"resolver": "<rootDir>/jestResolver.js",
|
||||
"preset": "config/jest/node",
|
||||
"roots": [
|
||||
"<rootDir>"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
|
||||
import { RoomSnapshot } from '@tldraw/tlsync'
|
||||
import { RoomSnapshot } from '@tldraw/sync'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Tldraw, fetch } from 'tldraw'
|
||||
import '../../../styles/core.css'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { ROOM_OPEN_MODE, RoomOpenModeToPath, type RoomOpenMode } from '@tldraw/dotcom-shared'
|
||||
import { useRemoteSyncClient } from '@tldraw/sync-react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import {
|
||||
DefaultContextMenu,
|
||||
|
@ -22,7 +23,6 @@ import {
|
|||
useActions,
|
||||
useValue,
|
||||
} from 'tldraw'
|
||||
import { useRemoteSyncClient } from '../hooks/useRemoteSyncClient'
|
||||
import { UrlStateParams, useUrlState } from '../hooks/useUrlState'
|
||||
import { resolveAsset } from '../utils/assetHandler'
|
||||
import { assetUrls } from '../utils/assetUrls'
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { TLIncompatibilityReason } from '@tldraw/tlsync'
|
||||
import { TLIncompatibilityReason, TLRemoteSyncError } from '@tldraw/sync'
|
||||
import { exhaustiveSwitchError } from 'tldraw'
|
||||
import { RemoteSyncError } from '../utils/remote-sync/remote-sync'
|
||||
import { ErrorPage } from './ErrorPage/ErrorPage'
|
||||
|
||||
export function StoreErrorScreen({ error }: { error: Error }) {
|
||||
let header = 'Could not connect to server.'
|
||||
let message = ''
|
||||
if (error instanceof RemoteSyncError) {
|
||||
if (error instanceof TLRemoteSyncError) {
|
||||
switch (error.reason) {
|
||||
case TLIncompatibilityReason.ClientTooOld: {
|
||||
return (
|
||||
|
|
98
apps/dotcom/src/components/TemporaryBemoDevEditor.tsx
Normal file
98
apps/dotcom/src/components/TemporaryBemoDevEditor.tsx
Normal file
|
@ -0,0 +1,98 @@
|
|||
import { useRemoteSyncClient } from '@tldraw/sync-react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { DefaultContextMenu, DefaultContextMenuContent, TLComponents, Tldraw, atom } from 'tldraw'
|
||||
import { UrlStateParams, useUrlState } from '../hooks/useUrlState'
|
||||
import { assetUrls } from '../utils/assetUrls'
|
||||
import { CursorChatMenuItem } from '../utils/context-menu/CursorChatMenuItem'
|
||||
import { useCursorChat } from '../utils/useCursorChat'
|
||||
import { useFileSystem } from '../utils/useFileSystem'
|
||||
import { useHandleUiEvents } from '../utils/useHandleUiEvent'
|
||||
import { CursorChatBubble } from './CursorChatBubble'
|
||||
import { PeopleMenu } from './PeopleMenu/PeopleMenu'
|
||||
import { SneakyOnDropOverride } from './SneakyOnDropOverride'
|
||||
import { StoreErrorScreen } from './StoreErrorScreen'
|
||||
import { ThemeUpdater } from './ThemeUpdater/ThemeUpdater'
|
||||
|
||||
const shittyOfflineAtom = atom('shitty offline atom', false)
|
||||
|
||||
const components: TLComponents = {
|
||||
ErrorFallback: ({ error }) => {
|
||||
throw error
|
||||
},
|
||||
ContextMenu: (props) => (
|
||||
<DefaultContextMenu {...props}>
|
||||
<CursorChatMenuItem />
|
||||
<DefaultContextMenuContent />
|
||||
</DefaultContextMenu>
|
||||
),
|
||||
SharePanel: () => {
|
||||
return (
|
||||
<div className="tlui-share-zone" draggable={false}>
|
||||
<PeopleMenu />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export function TemporaryBemoDevEditor({ slug }: { slug: string }) {
|
||||
const handleUiEvent = useHandleUiEvents()
|
||||
|
||||
const storeWithStatus = useRemoteSyncClient({
|
||||
uri: `http://127.0.0.1:8989/connect/${slug}`,
|
||||
roomId: slug,
|
||||
})
|
||||
|
||||
const isOffline =
|
||||
storeWithStatus.status === 'synced-remote' && storeWithStatus.connectionStatus === 'offline'
|
||||
useEffect(() => {
|
||||
shittyOfflineAtom.set(isOffline)
|
||||
}, [isOffline])
|
||||
|
||||
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
|
||||
const cursorChatOverrides = useCursorChat()
|
||||
|
||||
// TODO: handle assets and bookmarks
|
||||
// const handleMount = useCallback(
|
||||
// (editor: Editor) => {
|
||||
// editor.registerExternalAssetHandler('file', createAssetFromFile)
|
||||
// editor.registerExternalAssetHandler('url', createAssetFromUrl)
|
||||
// },
|
||||
// []
|
||||
// )
|
||||
|
||||
if (storeWithStatus.error) {
|
||||
return <StoreErrorScreen error={storeWithStatus.error} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
store={storeWithStatus}
|
||||
assetUrls={assetUrls}
|
||||
overrides={[fileSystemUiOverrides, cursorChatOverrides]}
|
||||
onUiEvent={handleUiEvent}
|
||||
components={components}
|
||||
inferDarkMode
|
||||
>
|
||||
<UrlStateSync />
|
||||
<CursorChatBubble />
|
||||
<SneakyOnDropOverride isMultiplayer />
|
||||
<ThemeUpdater />
|
||||
</Tldraw>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function UrlStateSync() {
|
||||
const syncViewport = useCallback((params: UrlStateParams) => {
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
window.location.pathname + `?v=${params.v}&p=${params.p}`
|
||||
)
|
||||
}, [])
|
||||
|
||||
useUrlState(syncViewport)
|
||||
|
||||
return null
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { schema } from '@tldraw/tlsync'
|
||||
import { schema } from '@tldraw/sync'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
MigrationFailureReason,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
|
||||
import { RoomSnapshot } from '@tldraw/tlsync'
|
||||
import { RoomSnapshot } from '@tldraw/sync'
|
||||
import { fetch } from 'tldraw'
|
||||
import '../../styles/globals.css'
|
||||
import { BoardHistorySnapshot } from '../components/BoardHistorySnapshot/BoardHistorySnapshot'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ROOM_PREFIX, Snapshot } from '@tldraw/dotcom-shared'
|
||||
import { schema } from '@tldraw/tlsync'
|
||||
import { schema } from '@tldraw/sync'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import '../../styles/globals.css'
|
||||
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CreateRoomRequestBody, ROOM_PREFIX, Snapshot } from '@tldraw/dotcom-shared'
|
||||
import { schema } from '@tldraw/tlsync'
|
||||
import { schema } from '@tldraw/sync'
|
||||
import { useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { TldrawUiButton, fetch } from 'tldraw'
|
||||
|
|
13
apps/dotcom/src/pages/temporary-bemo.tsx
Normal file
13
apps/dotcom/src/pages/temporary-bemo.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { useParams } from 'react-router-dom'
|
||||
import '../../styles/globals.css'
|
||||
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
|
||||
import { TemporaryBemoDevEditor } from '../components/TemporaryBemoDevEditor'
|
||||
|
||||
export function Component() {
|
||||
const id = useParams()['roomId'] as string
|
||||
return (
|
||||
<IFrameProtector slug={id} context={ROOM_CONTEXT.PUBLIC_MULTIPLAYER}>
|
||||
<TemporaryBemoDevEditor slug={id} />
|
||||
</IFrameProtector>
|
||||
)
|
||||
}
|
|
@ -10,6 +10,11 @@ import { Outlet, Route, createRoutesFromElements, useRouteError } from 'react-ro
|
|||
import { DefaultErrorFallback } from './components/DefaultErrorFallback/DefaultErrorFallback'
|
||||
import { ErrorPage } from './components/ErrorPage/ErrorPage'
|
||||
|
||||
const enableTemporaryLocalBemo =
|
||||
window.location.hostname === 'localhost' &&
|
||||
window.location.port === '3000' &&
|
||||
typeof jest === 'undefined'
|
||||
|
||||
export const router = createRoutesFromElements(
|
||||
<Route
|
||||
element={
|
||||
|
@ -50,6 +55,9 @@ export const router = createRoutesFromElements(
|
|||
lazy={() => import('./pages/public-readonly-legacy')}
|
||||
/>
|
||||
<Route path={`/${READ_ONLY_PREFIX}/:roomId`} lazy={() => import('./pages/public-readonly')} />
|
||||
{enableTemporaryLocalBemo && (
|
||||
<Route path={`/bemo/:roomId`} lazy={() => import('./pages/temporary-bemo')} />
|
||||
)}
|
||||
</Route>
|
||||
<Route path="*" lazy={() => import('./pages/not-found')} />
|
||||
</Route>
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import { TLIncompatibilityReason } from '@tldraw/tlsync'
|
||||
import { Signal, TLStoreSnapshot, TLUserPreferences } from 'tldraw'
|
||||
|
||||
/** @public */
|
||||
export class RemoteSyncError extends Error {
|
||||
override name = 'RemoteSyncError'
|
||||
constructor(public readonly reason: TLIncompatibilityReason) {
|
||||
super(`remote sync error: ${reason}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface UseSyncClientConfig {
|
||||
uri: string
|
||||
roomId?: string
|
||||
userPreferences?: Signal<TLUserPreferences>
|
||||
snapshotForNewRoomRef?: { current: null | TLStoreSnapshot }
|
||||
}
|
|
@ -32,10 +32,13 @@
|
|||
"path": "../../packages/dotcom-shared"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/tldraw"
|
||||
"path": "../../packages/sync"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/tlsync"
|
||||
"path": "../../packages/sync-react"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/tldraw"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/utils"
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { TLStoreSnapshot } from '@tldraw/tlschema'
|
||||
import { areObjectsShallowEqual } from '@tldraw/utils'
|
||||
import { useState } from 'react'
|
||||
import { TLEditorSnapshot } from '../..'
|
||||
import { loadSnapshot } from '../config/TLEditorSnapshot'
|
||||
import { TLEditorSnapshot, loadSnapshot } from '../config/TLEditorSnapshot'
|
||||
import { TLStoreOptions, createTLStore } from '../config/createTLStore'
|
||||
|
||||
/** @public */
|
||||
|
|
3
packages/sync-react/README.md
Normal file
3
packages/sync-react/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @tldraw/sync-react
|
||||
|
||||
react bindings for tldraw sync
|
70
packages/sync-react/package.json
Normal file
70
packages/sync-react/package.json
Normal file
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"name": "@tldraw/sync-react",
|
||||
"description": "A tiny little drawing app (multiplayer sync react bindings).",
|
||||
"version": "2.0.0-alpha.11",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "tldraw GB Ltd.",
|
||||
"email": "hello@tldraw.com"
|
||||
},
|
||||
"homepage": "https://tldraw.dev",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tldraw/tldraw"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tldraw/tldraw/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"tldraw",
|
||||
"drawing",
|
||||
"app",
|
||||
"development",
|
||||
"whiteboard",
|
||||
"canvas",
|
||||
"infinite"
|
||||
],
|
||||
"/* NOTE */": "These `main` and `types` fields are rewritten by the build script. They are not the actual values we publish",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./.tsbuild/index.d.ts",
|
||||
"/* GOTCHA */": "files will include ./dist and index.d.ts by default, add any others you want to include in here",
|
||||
"files": [],
|
||||
"scripts": {
|
||||
"test-ci": "lazy inherit",
|
||||
"test": "yarn run -T jest",
|
||||
"test-coverage": "lazy inherit",
|
||||
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3",
|
||||
"uuid-by-string": "^4.0.0",
|
||||
"uuid-readable": "^0.0.2"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "config/jest/node",
|
||||
"testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
|
||||
"moduleNameMapper": {
|
||||
"^~(.*)": "<rootDir>/src/$1"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"ignore everything. swc is fast enough to transform everything"
|
||||
],
|
||||
"setupFiles": [
|
||||
"./setupJest.js"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@tldraw/sync": "workspace:*",
|
||||
"@tldraw/utils": "workspace:*",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"nanoevents": "^7.0.1",
|
||||
"nanoid": "4.0.2",
|
||||
"tldraw": "workspace:*",
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
}
|
||||
}
|
0
packages/sync-react/setupJest.js
Normal file
0
packages/sync-react/setupJest.js
Normal file
3
packages/sync-react/src/index.test.ts
Normal file
3
packages/sync-react/src/index.test.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
test('make ci pass with empty test', () => {
|
||||
// empty
|
||||
})
|
1
packages/sync-react/src/index.ts
Normal file
1
packages/sync-react/src/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { useRemoteSyncClient, type RemoteTLStoreWithStatus } from './useRemoteSyncClient'
|
|
@ -1,16 +1,21 @@
|
|||
import {
|
||||
ClientWebSocketAdapter,
|
||||
TLCloseEventCode,
|
||||
TLIncompatibilityReason,
|
||||
TLPersistentClientSocketStatus,
|
||||
TLRemoteSyncError,
|
||||
TLSyncClient,
|
||||
schema,
|
||||
} from '@tldraw/tlsync'
|
||||
} from '@tldraw/sync'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Signal,
|
||||
TAB_ID,
|
||||
TLRecord,
|
||||
TLStore,
|
||||
TLStoreSnapshot,
|
||||
TLStoreWithStatus,
|
||||
TLUserPreferences,
|
||||
computed,
|
||||
createPresenceStateDerivation,
|
||||
defaultUserPreferences,
|
||||
|
@ -18,9 +23,6 @@ import {
|
|||
useTLStore,
|
||||
useValue,
|
||||
} from 'tldraw'
|
||||
import { ClientWebSocketAdapter } from '../utils/remote-sync/ClientWebSocketAdapter'
|
||||
import { RemoteSyncError, UseSyncClientConfig } from '../utils/remote-sync/remote-sync'
|
||||
import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
|
||||
|
||||
const MULTIPLAYER_EVENT_NAME = 'multiplayer.client'
|
||||
|
||||
|
@ -41,6 +43,7 @@ export function useRemoteSyncClient(opts: UseSyncClientConfig): RemoteTLStoreWit
|
|||
const store = useTLStore({ schema })
|
||||
|
||||
const error: NonNullable<typeof state>['error'] = state?.error ?? undefined
|
||||
const track = opts.trackAnalyticsEvent
|
||||
|
||||
useEffect(() => {
|
||||
if (error) return
|
||||
|
@ -67,8 +70,8 @@ export function useRemoteSyncClient(opts: UseSyncClientConfig): RemoteTLStoreWit
|
|||
|
||||
socket.onStatusChange((val: TLPersistentClientSocketStatus, closeCode?: number) => {
|
||||
if (val === 'error' && closeCode === TLCloseEventCode.NOT_FOUND) {
|
||||
trackAnalyticsEvent(MULTIPLAYER_EVENT_NAME, { name: 'room-not-found', roomId })
|
||||
setState({ error: new RemoteSyncError(TLIncompatibilityReason.RoomNotFound) })
|
||||
track?.(MULTIPLAYER_EVENT_NAME, { name: 'room-not-found', roomId })
|
||||
setState({ error: new TLRemoteSyncError(TLIncompatibilityReason.RoomNotFound) })
|
||||
client.close()
|
||||
socket.close()
|
||||
return
|
||||
|
@ -82,17 +85,17 @@ export function useRemoteSyncClient(opts: UseSyncClientConfig): RemoteTLStoreWit
|
|||
socket,
|
||||
didCancel: () => didCancel,
|
||||
onLoad(client) {
|
||||
trackAnalyticsEvent(MULTIPLAYER_EVENT_NAME, { name: 'load', roomId })
|
||||
track?.(MULTIPLAYER_EVENT_NAME, { name: 'load', roomId })
|
||||
setState({ readyClient: client })
|
||||
},
|
||||
onLoadError(err) {
|
||||
trackAnalyticsEvent(MULTIPLAYER_EVENT_NAME, { name: 'load-error', roomId })
|
||||
track?.(MULTIPLAYER_EVENT_NAME, { name: 'load-error', roomId })
|
||||
console.error(err)
|
||||
setState({ error: err })
|
||||
},
|
||||
onSyncError(reason) {
|
||||
trackAnalyticsEvent(MULTIPLAYER_EVENT_NAME, { name: 'sync-error', roomId, reason })
|
||||
setState({ error: new RemoteSyncError(reason) })
|
||||
track?.(MULTIPLAYER_EVENT_NAME, { name: 'sync-error', roomId, reason })
|
||||
setState({ error: new TLRemoteSyncError(reason) })
|
||||
},
|
||||
onAfterConnect() {
|
||||
// if the server crashes and loses all data it can return an empty document
|
||||
|
@ -111,7 +114,7 @@ export function useRemoteSyncClient(opts: UseSyncClientConfig): RemoteTLStoreWit
|
|||
client.close()
|
||||
socket.close()
|
||||
}
|
||||
}, [prefs, roomId, store, uri, error])
|
||||
}, [prefs, roomId, store, uri, error, track])
|
||||
|
||||
return useValue<RemoteTLStoreWithStatus>(
|
||||
'remote synced store',
|
||||
|
@ -129,3 +132,13 @@ export function useRemoteSyncClient(opts: UseSyncClientConfig): RemoteTLStoreWit
|
|||
[state]
|
||||
)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface UseSyncClientConfig {
|
||||
uri: string
|
||||
roomId?: string
|
||||
userPreferences?: Signal<TLUserPreferences>
|
||||
snapshotForNewRoomRef?: { current: null | TLStoreSnapshot }
|
||||
/* @internal */
|
||||
trackAnalyticsEvent?(name: string, data: { [key: string]: any }): void
|
||||
}
|
20
packages/sync-react/tsconfig.json
Normal file
20
packages/sync-react/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "../../config/tsconfig.base.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "docs", ".tsbuild*"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./.tsbuild",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../sync"
|
||||
},
|
||||
{
|
||||
"path": "../tldraw"
|
||||
},
|
||||
{
|
||||
"path": "../utils"
|
||||
}
|
||||
]
|
||||
}
|
1
packages/sync/LICENSE.md
Normal file
1
packages/sync/LICENSE.md
Normal file
|
@ -0,0 +1 @@
|
|||
This code is licensed under the [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md)
|
4
packages/sync/api-extractor.json
Normal file
4
packages/sync/api-extractor.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
|
||||
"extends": "../../config/api-extractor.json"
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@tldraw/tlsync",
|
||||
"name": "@tldraw/sync",
|
||||
"description": "A tiny little drawing app (multiplayer sync).",
|
||||
"version": "2.0.0-alpha.11",
|
||||
"private": true,
|
||||
|
@ -43,6 +43,7 @@
|
|||
"uuid-readable": "^0.0.2"
|
||||
},
|
||||
"jest": {
|
||||
"resolver": "<rootDir>/jestResolver.js",
|
||||
"preset": "config/jest/node",
|
||||
"testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
|
||||
"moduleNameMapper": {
|
|
@ -1,3 +1,5 @@
|
|||
export { ClientWebSocketAdapter } from './lib/ClientWebSocketAdapter'
|
||||
export { TLRemoteSyncError } from './lib/TLRemoteSyncError'
|
||||
export { TLSocketRoom } from './lib/TLSocketRoom'
|
||||
export {
|
||||
TLCloseEventCode,
|
|
@ -1,8 +1,8 @@
|
|||
import { TLSocketClientSentEvent, getTlsyncProtocolVersion } from '@tldraw/tlsync'
|
||||
import { TLRecord } from 'tldraw'
|
||||
import { ClientWebSocketAdapter, INACTIVE_MIN_DELAY } from './ClientWebSocketAdapter'
|
||||
// NOTE: there is a hack in apps/dotcom/jestResolver.js to make this import work
|
||||
import { WebSocketServer, WebSocket as WsWebSocket } from 'ws'
|
||||
import { TLSocketClientSentEvent, getTlsyncProtocolVersion } from './protocol'
|
||||
|
||||
async function waitFor(predicate: () => boolean) {
|
||||
let safety = 0
|
|
@ -1,13 +1,13 @@
|
|||
import { atom, Atom } from '@tldraw/state'
|
||||
import { TLRecord } from '@tldraw/tlschema'
|
||||
import { assert } from '@tldraw/utils'
|
||||
import { chunk } from './chunk'
|
||||
import { TLSocketClientSentEvent, TLSocketServerSentEvent } from './protocol'
|
||||
import {
|
||||
chunk,
|
||||
TLCloseEventCode,
|
||||
TLPersistentClientSocket,
|
||||
TLPersistentClientSocketStatus,
|
||||
TLSocketClientSentEvent,
|
||||
TLSocketServerSentEvent,
|
||||
} from '@tldraw/tlsync'
|
||||
import { assert } from '@tldraw/utils'
|
||||
import { atom, Atom, TLRecord } from 'tldraw'
|
||||
} from './TLSyncClient'
|
||||
|
||||
function listenTo<T extends EventTarget>(target: T, event: string, handler: () => void) {
|
||||
target.addEventListener(event, handler)
|
9
packages/sync/src/lib/TLRemoteSyncError.ts
Normal file
9
packages/sync/src/lib/TLRemoteSyncError.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { TLIncompatibilityReason } from './protocol'
|
||||
|
||||
/** @public */
|
||||
export class TLRemoteSyncError extends Error {
|
||||
override name = 'RemoteSyncError'
|
||||
constructor(public readonly reason: TLIncompatibilityReason) {
|
||||
super(`remote sync error: ${reason}`)
|
||||
}
|
||||
}
|
|
@ -36,8 +36,8 @@ import {
|
|||
} from '../shared/default-shape-constants'
|
||||
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
||||
|
||||
import { useDefaultColorTheme } from '../../..'
|
||||
import { startEditingShapeWithLabel } from '../../tools/SelectTool/selectHelpers'
|
||||
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
|
||||
import {
|
||||
CLONE_HANDLE_MARGIN,
|
||||
NOTE_CENTER_OFFSET,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/// <reference no-default-lib="true"/>
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
export { createPersistQueue } from './createPersistQueue'
|
||||
export { notFound } from './errors'
|
||||
export { getUrlMetadata, urlMetadataQueryValidator } from './getUrlMetadata'
|
||||
export {
|
||||
|
|
|
@ -202,3 +202,9 @@ function retry(
|
|||
attempt()
|
||||
})
|
||||
}
|
||||
|
||||
export async function publishProductionDocsAndExamples({
|
||||
gitRef = 'HEAD',
|
||||
}: { gitRef?: string } = {}) {
|
||||
await exec('git', ['push', 'origin', `${gitRef}:docs-production`, `--force`])
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { appendFileSync } from 'fs'
|
||||
import { exec } from './lib/exec'
|
||||
import { getLatestVersion, publish } from './lib/publishing'
|
||||
import { getLatestVersion, publish, publishProductionDocsAndExamples } from './lib/publishing'
|
||||
import { uploadStaticAssets } from './upload-static-assets'
|
||||
|
||||
// This expects the package.json files to be in the correct state.
|
||||
|
@ -20,6 +20,10 @@ async function main() {
|
|||
await uploadStaticAssets(latestVersionInBranch.version)
|
||||
|
||||
await publish()
|
||||
|
||||
if (isLatestVersion) {
|
||||
await publishProductionDocsAndExamples()
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
|
|
@ -7,7 +7,12 @@ import { SemVer, parse } from 'semver'
|
|||
import { exec } from './lib/exec'
|
||||
import { generateAutoRcFile } from './lib/labels'
|
||||
import { nicelog } from './lib/nicelog'
|
||||
import { getLatestVersion, publish, setAllVersions } from './lib/publishing'
|
||||
import {
|
||||
getLatestVersion,
|
||||
publish,
|
||||
publishProductionDocsAndExamples,
|
||||
setAllVersions,
|
||||
} from './lib/publishing'
|
||||
import { getAllWorkspacePackages } from './lib/workspace'
|
||||
import { uploadStaticAssets } from './upload-static-assets'
|
||||
|
||||
|
@ -126,7 +131,7 @@ async function main() {
|
|||
if (!isPrerelease) {
|
||||
const { major, minor } = parse(nextVersion)!
|
||||
await exec('git', ['push', 'origin', `${gitTag}:refs/heads/v${major}.${minor}.x`])
|
||||
await exec('git', ['push', 'origin', `${gitTag}:docs-production`, `--force`])
|
||||
await publishProductionDocsAndExamples({ gitRef: gitTag })
|
||||
}
|
||||
|
||||
// create a release on github
|
||||
|
|
|
@ -7,7 +7,12 @@ import { didAnyPackageChange } from './lib/didAnyPackageChange'
|
|||
import { exec } from './lib/exec'
|
||||
import { generateAutoRcFile } from './lib/labels'
|
||||
import { nicelog } from './lib/nicelog'
|
||||
import { getLatestVersion, publish, setAllVersions } from './lib/publishing'
|
||||
import {
|
||||
getLatestVersion,
|
||||
publish,
|
||||
publishProductionDocsAndExamples,
|
||||
setAllVersions,
|
||||
} from './lib/publishing'
|
||||
import { getAllWorkspacePackages } from './lib/workspace'
|
||||
import { uploadStaticAssets } from './upload-static-assets'
|
||||
|
||||
|
@ -41,7 +46,7 @@ async function main() {
|
|||
}
|
||||
|
||||
if (isLatestVersion) {
|
||||
await exec('git', ['push', 'origin', `HEAD:docs-production`, '--force'])
|
||||
await publishProductionDocsAndExamples()
|
||||
}
|
||||
|
||||
// Skip releasing a new version if the package contents are identical.
|
||||
|
|
72
yarn.lock
72
yarn.lock
|
@ -5997,9 +5997,10 @@ __metadata:
|
|||
"@cloudflare/workers-types": "npm:^4.20240620.0"
|
||||
"@tldraw/dotcom-shared": "workspace:*"
|
||||
"@tldraw/store": "workspace:*"
|
||||
"@tldraw/sync": "workspace:*"
|
||||
"@tldraw/tlschema": "workspace:*"
|
||||
"@tldraw/tlsync": "workspace:*"
|
||||
"@tldraw/utils": "workspace:*"
|
||||
"@tldraw/validate": "workspace:*"
|
||||
"@tldraw/worker-shared": "workspace:*"
|
||||
esbuild: "npm:^0.21.5"
|
||||
itty-router: "npm:^4.0.13"
|
||||
|
@ -6085,8 +6086,8 @@ __metadata:
|
|||
"@supabase/supabase-js": "npm:^2.33.2"
|
||||
"@tldraw/dotcom-shared": "workspace:*"
|
||||
"@tldraw/store": "workspace:*"
|
||||
"@tldraw/sync": "workspace:*"
|
||||
"@tldraw/tlschema": "workspace:*"
|
||||
"@tldraw/tlsync": "workspace:*"
|
||||
"@tldraw/utils": "workspace:*"
|
||||
"@tldraw/validate": "workspace:*"
|
||||
"@tldraw/worker-shared": "workspace:*"
|
||||
|
@ -6254,6 +6255,48 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@tldraw/sync-react@workspace:*, @tldraw/sync-react@workspace:packages/sync-react":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@tldraw/sync-react@workspace:packages/sync-react"
|
||||
dependencies:
|
||||
"@tldraw/sync": "workspace:*"
|
||||
"@tldraw/utils": "workspace:*"
|
||||
lodash.isequal: "npm:^4.5.0"
|
||||
nanoevents: "npm:^7.0.1"
|
||||
nanoid: "npm:4.0.2"
|
||||
tldraw: "workspace:*"
|
||||
typescript: "npm:^5.3.3"
|
||||
uuid-by-string: "npm:^4.0.0"
|
||||
uuid-readable: "npm:^0.0.2"
|
||||
ws: "npm:^8.16.0"
|
||||
peerDependencies:
|
||||
react: ^18
|
||||
react-dom: ^18
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@tldraw/sync@workspace:*, @tldraw/sync@workspace:packages/sync":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@tldraw/sync@workspace:packages/sync"
|
||||
dependencies:
|
||||
"@tldraw/state": "workspace:*"
|
||||
"@tldraw/store": "workspace:*"
|
||||
"@tldraw/tlschema": "workspace:*"
|
||||
"@tldraw/utils": "workspace:*"
|
||||
lodash.isequal: "npm:^4.5.0"
|
||||
nanoevents: "npm:^7.0.1"
|
||||
nanoid: "npm:4.0.2"
|
||||
tldraw: "workspace:*"
|
||||
typescript: "npm:^5.3.3"
|
||||
uuid-by-string: "npm:^4.0.0"
|
||||
uuid-readable: "npm:^0.0.2"
|
||||
ws: "npm:^8.16.0"
|
||||
peerDependencies:
|
||||
react: ^18
|
||||
react-dom: ^18
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@tldraw/tldraw@workspace:packages/namespaced-tldraw":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@tldraw/tldraw@workspace:packages/namespaced-tldraw"
|
||||
|
@ -6283,28 +6326,6 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@tldraw/tlsync@workspace:*, @tldraw/tlsync@workspace:packages/tlsync":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@tldraw/tlsync@workspace:packages/tlsync"
|
||||
dependencies:
|
||||
"@tldraw/state": "workspace:*"
|
||||
"@tldraw/store": "workspace:*"
|
||||
"@tldraw/tlschema": "workspace:*"
|
||||
"@tldraw/utils": "workspace:*"
|
||||
lodash.isequal: "npm:^4.5.0"
|
||||
nanoevents: "npm:^7.0.1"
|
||||
nanoid: "npm:4.0.2"
|
||||
tldraw: "workspace:*"
|
||||
typescript: "npm:^5.3.3"
|
||||
uuid-by-string: "npm:^4.0.0"
|
||||
uuid-readable: "npm:^0.0.2"
|
||||
ws: "npm:^8.16.0"
|
||||
peerDependencies:
|
||||
react: ^18
|
||||
react-dom: ^18
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@tldraw/utils@workspace:*, @tldraw/utils@workspace:packages/utils":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@tldraw/utils@workspace:packages/utils"
|
||||
|
@ -10334,7 +10355,8 @@ __metadata:
|
|||
"@sentry/react": "npm:^7.77.0"
|
||||
"@tldraw/assets": "workspace:*"
|
||||
"@tldraw/dotcom-shared": "workspace:*"
|
||||
"@tldraw/tlsync": "workspace:*"
|
||||
"@tldraw/sync": "workspace:*"
|
||||
"@tldraw/sync-react": "workspace:*"
|
||||
"@tldraw/utils": "workspace:*"
|
||||
"@tldraw/validate": "workspace:*"
|
||||
"@types/qrcode": "npm:^1.5.0"
|
||||
|
|
Loading…
Reference in a new issue