[bemo] allow custom shapes (#4144)

I tested this by adding a custom shape on the bemo example page, it
works 👍🏼

### 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:
David Sheldrick 2024-07-11 14:48:14 +01:00 committed by GitHub
parent 69a1c17b46
commit 34eaf12bff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 79 additions and 19 deletions

View file

@ -5,6 +5,7 @@ import { T } from '@tldraw/validate'
import { createPersistQueue, createSentry, parseRequestQuery } from '@tldraw/worker-shared'
import { DurableObject } from 'cloudflare:workers'
import { IRequest, Router } from 'itty-router'
import { makePermissiveSchema } from './makePermissiveSchema'
import { Environment } from './types'
const connectRequestQuery = T.object({
@ -106,6 +107,7 @@ export class BemoDO extends DurableObject<Environment> {
if (!this._room) {
this._room = this.loadFromDatabase(slug).then((result) => {
return new TLSocketRoom<TLRecord, void>({
schema: makePermissiveSchema(),
initialSnapshot: result.type === 'room_found' ? result.snapshot : undefined,
onSessionRemoved: async (room, args) => {
if (args.numSessionsRemaining > 0) return

View file

@ -0,0 +1,49 @@
import {
TLBaseBinding,
TLBaseShape,
TLSchema,
bindingIdValidator,
createTLSchema,
opacityValidator,
parentIdValidator,
shapeIdValidator,
} from '@tldraw/tlschema'
import { T } from '@tldraw/validate'
export function makePermissiveSchema(): TLSchema {
const schema = createTLSchema()
const shapeValidator = T.object<TLBaseShape<any, any>>({
id: shapeIdValidator,
typeName: T.literal('shape'),
x: T.number,
y: T.number,
rotation: T.number,
index: T.indexKey,
parentId: parentIdValidator,
type: T.string,
isLocked: T.boolean,
opacity: opacityValidator,
props: T.jsonValue as any,
meta: T.jsonValue as any,
}) as (typeof schema)['types']['shape']['validator']
const shapeType = schema.getType('shape')
// @ts-expect-error
shapeType.validator = shapeValidator
const bindingValidator = T.object<TLBaseBinding<any, any>>({
id: bindingIdValidator,
typeName: T.literal('binding'),
type: T.string,
fromId: shapeIdValidator,
toId: shapeIdValidator,
props: T.jsonValue as any,
meta: T.jsonValue as any,
}) as (typeof schema)['types']['binding']['validator']
const bindingType = schema.getType('binding')
// @ts-expect-error
bindingType.validator = bindingValidator
return schema
}

View file

@ -1,5 +1,6 @@
import { CreateRoomRequestBody } from '@tldraw/dotcom-shared'
import { RoomSnapshot, schema } from '@tldraw/sync-core'
import { RoomSnapshot } from '@tldraw/sync-core'
import { createTLSchema } from '@tldraw/tlschema'
import { IRequest } from 'itty-router'
import { nanoid } from 'nanoid'
import { getR2KeyForRoom } from '../r2'
@ -22,7 +23,7 @@ export async function createRoom(request: IRequest, env: Environment): Promise<R
// Create the new snapshot
const snapshot: RoomSnapshot = {
schema: schema.serialize(),
schema: createTLSchema().serialize(),
clock: 0,
documents: Object.values(snapshotResult.value).map((r) => ({
state: r,

View file

@ -1,6 +1,5 @@
import { SerializedSchema, SerializedStore } from '@tldraw/store'
import { schema } from '@tldraw/sync-core'
import { TLRecord } from '@tldraw/tlschema'
import { TLRecord, createTLSchema } from '@tldraw/tlschema'
import { Result, objectMapEntries } from '@tldraw/utils'
interface SnapshotRequestBody {
@ -8,6 +7,8 @@ interface SnapshotRequestBody {
snapshot: SerializedStore<TLRecord>
}
const schema = createTLSchema()
export function validateSnapshot(
body: SnapshotRequestBody
): Result<SerializedStore<TLRecord>, string> {

View file

@ -1,12 +1,14 @@
import { ROOM_PREFIX, Snapshot } from '@tldraw/dotcom-shared'
import { schema } from '@tldraw/sync-core'
import { Navigate } from 'react-router-dom'
import { createTLSchema } from 'tldraw'
import '../../styles/globals.css'
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
import { defineLoader } from '../utils/defineLoader'
import { isInIframe } from '../utils/iFrame'
import { getNewRoomResponse } from '../utils/sharing'
const schema = createTLSchema()
const { loader, useData } = defineLoader(async (_args) => {
if (isInIframe()) return null

View file

@ -1,11 +1,12 @@
import { CreateRoomRequestBody, ROOM_PREFIX, Snapshot } from '@tldraw/dotcom-shared'
import { schema } from '@tldraw/sync-core'
import { useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { TldrawUiButton, fetch } from 'tldraw'
import { TldrawUiButton, createTLSchema, fetch } from 'tldraw'
import '../../styles/globals.css'
import { getParentOrigin } from '../utils/iFrame'
const schema = createTLSchema()
export function Component() {
const [isCreating, setIsCreating] = useState(false)
const [isSessionStarted, setIsSessionStarted] = useState(false)

View file

@ -75,6 +75,7 @@ export class MyShapeUtil extends ShapeUtil<ICustomShape> {
// [3]
const customShape = [MyShapeUtil]
export default function CustomShapeExample() {
return (
<div className="tldraw__editor">

View file

@ -33,5 +33,4 @@ export {
type TLSocketClientSentEvent,
type TLSocketServerSentEvent,
} from './lib/protocol'
export { schema } from './lib/schema'
export type { PersistedRoomSnapshotForSupabase } from './lib/server-types'

View file

@ -22,7 +22,7 @@ export class TLSocketRoom<R extends UnknownRecord, SessionMeta> {
constructor(
public readonly opts: {
initialSnapshot?: RoomSnapshot
schema?: StoreSchema<R>
schema?: StoreSchema<R, any>
// how long to wait for a client to communicate before disconnecting them
clientTimeout?: number
log?: TLSyncLog

View file

@ -1,5 +0,0 @@
import { createTLSchema, defaultShapeSchemas } from '@tldraw/tlschema'
export const schema = createTLSchema({
shapes: defaultShapeSchemas,
})

View file

@ -18,8 +18,8 @@ import {
TLSyncRoom,
TOMBSTONE_PRUNE_BUFFER_SIZE,
} from '../lib/TLSyncRoom'
import { schema } from '../lib/schema'
const schema = createTLSchema()
const compareById = (a: { id: string }, b: { id: string }) => a.id.localeCompare(b.id)
const records = [

View file

@ -7,17 +7,19 @@ import {
TLStore,
computed,
createPresenceStateDerivation,
createTLSchema,
createTLStore,
isRecordsDiffEmpty,
} from 'tldraw'
import { prettyPrintDiff } from '../../../tldraw/src/test/testutils/pretty'
import { TLSyncClient } from '../lib/TLSyncClient'
import { schema } from '../lib/schema'
import { FuzzEditor, Op } from './FuzzEditor'
import { RandomSource } from './RandomSource'
import { TestServer } from './TestServer'
import { TestSocketPair } from './TestSocketPair'
const schema = createTLSchema()
jest.mock('@tldraw/editor/src/lib/editor/managers/TickManager.ts', () => {
return {
TickManager: class {

View file

@ -5,7 +5,6 @@ import {
TLPersistentClientSocketStatus,
TLRemoteSyncError,
TLSyncClient,
schema,
} from '@tldraw/sync-core'
import { useEffect } from 'react'
import {
@ -14,11 +13,13 @@ import {
TAB_ID,
TLAssetStore,
TLRecord,
TLSchema,
TLStore,
TLStoreWithStatus,
TLUserPreferences,
computed,
createPresenceStateDerivation,
createTLSchema,
createTLStore,
defaultUserPreferences,
getUserPreferences,
@ -48,6 +49,7 @@ export function useMultiplayerSync(opts: UseMultiplayerSyncOptions): RemoteTLSto
assets,
onEditorMount,
trackAnalyticsEvent: track,
schema,
} = opts
const error: NonNullable<typeof state>['error'] = state?.error ?? undefined
@ -91,7 +93,7 @@ export function useMultiplayerSync(opts: UseMultiplayerSyncOptions): RemoteTLSto
const store = createTLStore({
id: storeId,
schema,
schema: schema ?? createTLSchema(),
assets,
onEditorMount,
multiplayerStatus: computed('multiplayer status', () =>
@ -133,7 +135,7 @@ export function useMultiplayerSync(opts: UseMultiplayerSyncOptions): RemoteTLSto
client.close()
socket.close()
}
}, [assets, error, onEditorMount, prefs, roomId, setState, track, uri])
}, [assets, error, onEditorMount, prefs, roomId, setState, track, uri, schema])
return useValue<RemoteTLStoreWithStatus>(
'remote synced store',
@ -161,4 +163,5 @@ export interface UseMultiplayerSyncOptions {
trackAnalyticsEvent?(name: string, data: { [key: string]: any }): void
assets?: Partial<TLAssetStore>
onEditorMount?: (editor: Editor) => void
schema?: TLSchema
}

View file

@ -6,6 +6,7 @@ import {
Signal,
TLAsset,
TLAssetStore,
TLSchema,
TLUserPreferences,
getHashForString,
uniqueId,
@ -18,6 +19,7 @@ export interface UseMultiplayerDemoOptions {
userPreferences?: Signal<TLUserPreferences>
/** @internal */
host?: string
schema?: TLSchema
}
/**
@ -43,6 +45,7 @@ export function useMultiplayerDemo({
roomId,
userPreferences,
host = DEMO_WORKER,
schema,
}: UseMultiplayerDemoOptions): RemoteTLStoreWithStatus {
const assets = useMemo(() => createDemoAssetStore(host), [host])
@ -51,6 +54,7 @@ export function useMultiplayerDemo({
roomId,
userPreferences,
assets,
schema,
onEditorMount: useCallback(
(editor: Editor) => {
editor.registerExternalAssetHandler('url', async ({ url }) => {