@tldraw/sync 5.6.0-canary.ce8fde24c9aa → 5.6.0-canary.dd9b57126e5d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist-cjs/index.js CHANGED
@@ -29,7 +29,7 @@ var import_useSync = require("./useSync");
29
29
  var import_useSyncDemo = require("./useSyncDemo");
30
30
  (0, import_utils.registerTldrawLibraryVersion)(
31
31
  "@tldraw/sync",
32
- "5.6.0-canary.ce8fde24c9aa",
32
+ "5.6.0-canary.dd9b57126e5d",
33
33
  "cjs"
34
34
  );
35
35
  //# sourceMappingURL=index.js.map
@@ -33,6 +33,7 @@ function getEnv(cb) {
33
33
  }
34
34
  const DEMO_WORKER = getEnv(() => "https://canary-demo.tldraw.xyz") ?? "https://demo.tldraw.xyz";
35
35
  const IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? "https://images.tldraw.xyz";
36
+ const MIN_RESIZE_DIMENSION = 400;
36
37
  function useSyncDemo(options) {
37
38
  const { roomId, host = DEMO_WORKER, ..._syncOpts } = options;
38
39
  const assets = (0, import_react.useMemo)(() => createDemoAssetStore(host), [host]);
@@ -98,18 +99,16 @@ function createDemoAssetStore(host) {
98
99
  const url = new URL(src);
99
100
  const isTldrawImage = url.origin === host || /\.tldraw\.(?:com|xyz|dev|workers\.dev)$/.test(url.host);
100
101
  if (!isTldrawImage) return src;
101
- const { fileSize = 0 } = asset.props;
102
- const isWorthResizing = fileSize >= 1024 * 1024 * 1.5;
103
- if (isWorthResizing) {
104
- const networkCompensation = !context.networkEffectiveType || context.networkEffectiveType === "4g" ? 1 : 0.5;
105
- const pixelRatio = asset.props.pixelRatio ?? 1;
106
- const trueWidth = asset.props.w * pixelRatio;
107
- const width = Math.ceil(
108
- Math.min(
109
- trueWidth * (0, import_tldraw.clamp)(context.steppedScreenScale, 1 / 32, 1) * networkCompensation * context.dpr,
110
- trueWidth
111
- )
112
- );
102
+ const networkCompensation = !context.networkEffectiveType || context.networkEffectiveType === "4g" ? 1 : 0.5;
103
+ const pixelRatio = asset.props.pixelRatio ?? 1;
104
+ const trueWidth = asset.props.w * pixelRatio;
105
+ const width = Math.ceil(
106
+ Math.min(
107
+ trueWidth * (0, import_tldraw.clamp)(context.steppedScreenScale, 1 / 32, 1) * networkCompensation * context.dpr,
108
+ trueWidth
109
+ )
110
+ );
111
+ if (trueWidth >= MIN_RESIZE_DIMENSION && asset.props.h * pixelRatio >= MIN_RESIZE_DIMENSION && width < trueWidth * 0.75) {
113
112
  url.searchParams.set("w", width.toString());
114
113
  }
115
114
  return `${IMAGE_WORKER}/${url.host}/${url.toString().slice(url.origin.length + 1)}`;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/useSyncDemo.ts"],
4
- "sourcesContent": ["import { useCallback, useMemo } from 'react'\nimport {\n\tAssetRecordType,\n\tEditor,\n\tMediaHelpers,\n\tTLAsset,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tTLUser,\n\tTLUserStore,\n\tclamp,\n\tdefaultBindingUtils,\n\tdefaultShapeUtils,\n\tgetHashForString,\n\tuniqueId,\n\tuseShallowObjectIdentity,\n} from 'tldraw'\nimport { RemoteTLStoreWithStatus, useSync } from './useSync'\n\n/** @public */\nexport interface UseSyncDemoOptions {\n\t/**\n\t * The room ID to sync with. Make sure the room ID is unique. The namespace is shared by\n\t * everyone using the demo server. Consider prefixing it with your company or project name.\n\t */\n\troomId: string\n\n\t/**\n\t * User store for identity, presence and attribution.\n\t * If not provided, a default implementation based on localStorage will be used.\n\t */\n\tusers?: TLUserStore\n\n\t/** @internal */\n\thost?: string\n\n\t/**\n\t * {@inheritdoc UseSyncOptions.getUserPresence}\n\t * @public\n\t */\n\tgetUserPresence?(store: TLStore, user: TLUser): TLPresenceStateInfo | null\n}\n\n/**\n * Safely accesses environment variables across different bundling environments.\n *\n * Depending on the environment this package is used in, process.env may not be available. This function\n * wraps `process.env` accesses in a try/catch to prevent runtime errors in environments where process\n * is not defined.\n *\n * The reason that this is just a try/catch and not a dynamic check e.g. `process &&\n * process.env[key]` is that many bundlers implement `process.env.WHATEVER` using compile-time\n * string replacement, rather than actually creating a runtime implementation of a `process` object.\n *\n * @param cb - Callback function that accesses an environment variable\n * @returns The environment variable value if available, otherwise undefined\n * @internal\n */\nfunction getEnv(cb: () => string | undefined): string | undefined {\n\ttry {\n\t\treturn cb()\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\nconst DEMO_WORKER = getEnv(() => process.env.TLDRAW_BEMO_URL) ?? 'https://demo.tldraw.xyz'\nconst IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? 'https://images.tldraw.xyz'\n\n/**\n * Creates a tldraw store synced with a multiplayer room hosted on tldraw's demo server `https://demo.tldraw.xyz`.\n *\n * The store can be passed directly into the `<Tldraw />` component to enable multiplayer features.\n * It will handle loading states, and enable multiplayer UX like user cursors and following.\n *\n * All data on the demo server is\n *\n * - Deleted after a day or so.\n * - Publicly accessible to anyone who knows the room ID. Use your company name as a prefix to help avoid collisions, or generate UUIDs for maximum privacy.\n *\n * @example\n * ```tsx\n * function MyApp() {\n * const store = useSyncDemo({roomId: 'my-app-test-room'})\n * return <Tldraw store={store} />\n * }\n * ```\n *\n * @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link @tldraw/editor#TLStoreSchemaOptions}.\n *\n * @public\n */\nexport function useSyncDemo(\n\toptions: UseSyncDemoOptions & TLStoreSchemaOptions\n): RemoteTLStoreWithStatus {\n\tconst { roomId, host = DEMO_WORKER, ..._syncOpts } = options\n\tconst assets = useMemo(() => createDemoAssetStore(host), [host])\n\n\tconst syncOpts = useShallowObjectIdentity(_syncOpts)\n\tconst syncOptsWithDefaults = useMemo(() => {\n\t\tif ('schema' in syncOpts && syncOpts.schema) return syncOpts\n\n\t\treturn {\n\t\t\t...syncOpts,\n\t\t\tshapeUtils:\n\t\t\t\t'shapeUtils' in syncOpts\n\t\t\t\t\t? [...defaultShapeUtils, ...(syncOpts.shapeUtils ?? [])]\n\t\t\t\t\t: defaultShapeUtils,\n\t\t\tbindingUtils:\n\t\t\t\t'bindingUtils' in syncOpts\n\t\t\t\t\t? [...defaultBindingUtils, ...(syncOpts.bindingUtils ?? [])]\n\t\t\t\t\t: defaultBindingUtils,\n\t\t}\n\t}, [syncOpts])\n\n\treturn useSync({\n\t\turi: `${host}/connect/${encodeURIComponent(roomId)}`,\n\t\troomId,\n\t\tassets,\n\t\tonMount: useCallback(\n\t\t\t(editor: Editor) => {\n\t\t\t\teditor.registerExternalAssetHandler('url', ({ url }) =>\n\t\t\t\t\tcreateAssetFromUrlUsingDemoServer(host, url)\n\t\t\t\t)\n\t\t\t},\n\t\t\t[host]\n\t\t),\n\t\t...syncOptsWithDefaults,\n\t})\n}\n\n/**\n * Determines whether file uploads should be disabled for a given host.\n *\n * Uploads are disabled for production tldraw domains to prevent abuse of the demo server\n * infrastructure. This includes tldraw.com and tldraw.xyz domains and their subdomains.\n *\n * @param host - The host URL to check for upload restrictions\n * @returns True if uploads should be disabled, false otherwise\n * @internal\n */\nfunction shouldDisallowUploads(host: string) {\n\tconst disallowedHosts = ['tldraw.com', 'tldraw.xyz']\n\treturn disallowedHosts.some(\n\t\t(disallowedHost) => host === disallowedHost || host.endsWith(`.${disallowedHost}`)\n\t)\n}\n\n/**\n * Creates an asset store implementation optimized for the tldraw demo server.\n *\n * This asset store handles file uploads to the demo server and provides intelligent\n * asset resolution with automatic image optimization based on network conditions,\n * screen density, and display size. It includes safeguards to prevent uploads to\n * production domains and optimizes images through the tldraw image processing service.\n *\n * @param host - The demo server host URL for file uploads and asset resolution\n * @returns A TLAssetStore implementation with upload and resolve capabilities\n * @example\n * ```ts\n * const assetStore = createDemoAssetStore('https://demo.tldraw.xyz')\n *\n * // Upload a file\n * const result = await assetStore.upload(asset, file)\n * console.log('Uploaded to:', result.src)\n *\n * // Resolve optimized asset URL\n * const optimizedUrl = assetStore.resolve(imageAsset, {\n * steppedScreenScale: 1.5,\n * dpr: 2,\n * networkEffectiveType: '4g'\n * })\n * ```\n * @internal\n */\nfunction createDemoAssetStore(host: string): TLAssetStore {\n\treturn {\n\t\tupload: async (_asset, file) => {\n\t\t\tif (shouldDisallowUploads(host)) {\n\t\t\t\talert('Uploading images is disabled in this demo.')\n\t\t\t\tthrow new Error('Uploading images is disabled in this demo.')\n\t\t\t}\n\t\t\tconst objectName = `${uniqueId()}-${file.name}`.replace(/\\W/g, '-')\n\t\t\tconst url = `${host}/uploads/${objectName}`\n\n\t\t\tconst response = await fetch(url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tbody: file,\n\t\t\t})\n\t\t\t// a rejected upload (invalid name, duplicate, server error) resolves the fetch; without\n\t\t\t// this the asset would point at a URL that was never stored\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`Failed to upload asset (${response.status})`)\n\t\t\t}\n\n\t\t\treturn { src: url }\n\t\t},\n\n\t\tresolve(asset, context) {\n\t\t\tconst { src } = asset.props\n\t\t\tif (!src) return null\n\n\t\t\t// We don't deal with videos at the moment.\n\t\t\tif (asset.type === 'video') return src\n\n\t\t\t// Assert it's an image to make TS happy.\n\t\t\tif (asset.type !== 'image') return null\n\n\t\t\t// Don't try to transform data: URLs, yikes.\n\t\t\tif (!src.startsWith('http:') && !src.startsWith('https:')) return src\n\n\t\t\tif (context.shouldResolveToOriginal) return src\n\n\t\t\t// Don't try to transform animated images.\n\t\t\tif (MediaHelpers.isAnimatedImageType(asset.props.mimeType) || asset.props.isAnimated)\n\t\t\t\treturn src\n\n\t\t\t// Don't try to transform vector images.\n\t\t\tif (MediaHelpers.isVectorImageType(asset.props.mimeType)) return src\n\n\t\t\tconst url = new URL(src)\n\n\t\t\t// we only transform images that are hosted on domains we control\n\t\t\tconst isTldrawImage =\n\t\t\t\turl.origin === host || /\\.tldraw\\.(?:com|xyz|dev|workers\\.dev)$/.test(url.host)\n\n\t\t\tif (!isTldrawImage) return src\n\n\t\t\t// Assets that are under a certain file size aren't worth transforming (and incurring cost).\n\t\t\t// We still send them through the image worker to get them optimized though.\n\t\t\tconst { fileSize = 0 } = asset.props\n\t\t\tconst isWorthResizing = fileSize >= 1024 * 1024 * 1.5\n\n\t\t\tif (isWorthResizing) {\n\t\t\t\t// N.B. navigator.connection is only available in certain browsers (mainly Blink-based browsers)\n\t\t\t\t// 4g is as high the 'effectiveType' goes and we can pick a lower effective image quality for slower connections.\n\t\t\t\tconst networkCompensation =\n\t\t\t\t\t!context.networkEffectiveType || context.networkEffectiveType === '4g' ? 1 : 0.5\n\n\t\t\t\tconst pixelRatio = asset.props.pixelRatio ?? 1\n\t\t\t\tconst trueWidth = asset.props.w * pixelRatio\n\t\t\t\tconst width = Math.ceil(\n\t\t\t\t\tMath.min(\n\t\t\t\t\t\ttrueWidth *\n\t\t\t\t\t\t\tclamp(context.steppedScreenScale, 1 / 32, 1) *\n\t\t\t\t\t\t\tnetworkCompensation *\n\t\t\t\t\t\t\tcontext.dpr,\n\t\t\t\t\t\ttrueWidth\n\t\t\t\t\t)\n\t\t\t\t)\n\n\t\t\t\turl.searchParams.set('w', width.toString())\n\t\t\t}\n\n\t\t\treturn `${IMAGE_WORKER}/${url.host}/${url.toString().slice(url.origin.length + 1)}`\n\t\t},\n\t}\n}\n\n/**\n * Creates a bookmark asset by fetching metadata from a URL using the demo server.\n *\n * This function uses the demo server's bookmark unfurling service to extract metadata\n * like title, description, favicon, and preview image from a given URL. If the metadata\n * fetch fails, it returns a blank bookmark asset with just the URL.\n *\n * @param host - The demo server host URL to use for bookmark unfurling\n * @param url - The URL to create a bookmark asset from\n * @returns A promise that resolves to a TLAsset of type 'bookmark' with extracted metadata\n * @example\n * ```ts\n * const asset = await createAssetFromUrlUsingDemoServer(\n * 'https://demo.tldraw.xyz',\n * 'https://example.com'\n * )\n *\n * console.log(asset.props.title) // \"Example Domain\"\n * console.log(asset.props.description) // \"This domain is for use in illustrative examples...\"\n * ```\n * @internal\n */\nasync function createAssetFromUrlUsingDemoServer(host: string, url: string): Promise<TLAsset> {\n\tlet meta: { description?: string; image?: string; favicon?: string; title?: string } | null = null\n\ttry {\n\t\t// First, try to get the meta data from our endpoint\n\t\tconst fetchUrl = new URL(`${host}/bookmarks/unfurl`)\n\t\tfetchUrl.searchParams.set('url', url)\n\t\tmeta = await (await fetch(fetchUrl, { method: 'POST' })).json()\n\t} catch (error) {\n\t\t// Otherwise, fall back to a blank bookmark\n\t\tconsole.error(error)\n\t}\n\n\treturn {\n\t\tid: AssetRecordType.createId(getHashForString(url)),\n\t\ttypeName: 'asset',\n\t\ttype: 'bookmark',\n\t\tprops: {\n\t\t\tsrc: url,\n\t\t\tdescription: meta?.description ?? '',\n\t\t\timage: meta?.image ?? '',\n\t\t\tfavicon: meta?.favicon ?? '',\n\t\t\ttitle: meta?.title ?? '',\n\t\t},\n\t\tmeta: {},\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { useCallback, useMemo } from 'react'\nimport {\n\tAssetRecordType,\n\tEditor,\n\tMediaHelpers,\n\tTLAsset,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tTLUser,\n\tTLUserStore,\n\tclamp,\n\tdefaultBindingUtils,\n\tdefaultShapeUtils,\n\tgetHashForString,\n\tuniqueId,\n\tuseShallowObjectIdentity,\n} from 'tldraw'\nimport { RemoteTLStoreWithStatus, useSync } from './useSync'\n\n/** @public */\nexport interface UseSyncDemoOptions {\n\t/**\n\t * The room ID to sync with. Make sure the room ID is unique. The namespace is shared by\n\t * everyone using the demo server. Consider prefixing it with your company or project name.\n\t */\n\troomId: string\n\n\t/**\n\t * User store for identity, presence and attribution.\n\t * If not provided, a default implementation based on localStorage will be used.\n\t */\n\tusers?: TLUserStore\n\n\t/** @internal */\n\thost?: string\n\n\t/**\n\t * {@inheritdoc UseSyncOptions.getUserPresence}\n\t * @public\n\t */\n\tgetUserPresence?(store: TLStore, user: TLUser): TLPresenceStateInfo | null\n}\n\n/**\n * Safely accesses environment variables across different bundling environments.\n *\n * Depending on the environment this package is used in, process.env may not be available. This function\n * wraps `process.env` accesses in a try/catch to prevent runtime errors in environments where process\n * is not defined.\n *\n * The reason that this is just a try/catch and not a dynamic check e.g. `process &&\n * process.env[key]` is that many bundlers implement `process.env.WHATEVER` using compile-time\n * string replacement, rather than actually creating a runtime implementation of a `process` object.\n *\n * @param cb - Callback function that accesses an environment variable\n * @returns The environment variable value if available, otherwise undefined\n * @internal\n */\nfunction getEnv(cb: () => string | undefined): string | undefined {\n\ttry {\n\t\treturn cb()\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\nconst DEMO_WORKER = getEnv(() => process.env.TLDRAW_BEMO_URL) ?? 'https://demo.tldraw.xyz'\nconst IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? 'https://images.tldraw.xyz'\nconst MIN_RESIZE_DIMENSION = 400\n\n/**\n * Creates a tldraw store synced with a multiplayer room hosted on tldraw's demo server `https://demo.tldraw.xyz`.\n *\n * The store can be passed directly into the `<Tldraw />` component to enable multiplayer features.\n * It will handle loading states, and enable multiplayer UX like user cursors and following.\n *\n * All data on the demo server is\n *\n * - Deleted after a day or so.\n * - Publicly accessible to anyone who knows the room ID. Use your company name as a prefix to help avoid collisions, or generate UUIDs for maximum privacy.\n *\n * @example\n * ```tsx\n * function MyApp() {\n * const store = useSyncDemo({roomId: 'my-app-test-room'})\n * return <Tldraw store={store} />\n * }\n * ```\n *\n * @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link @tldraw/editor#TLStoreSchemaOptions}.\n *\n * @public\n */\nexport function useSyncDemo(\n\toptions: UseSyncDemoOptions & TLStoreSchemaOptions\n): RemoteTLStoreWithStatus {\n\tconst { roomId, host = DEMO_WORKER, ..._syncOpts } = options\n\tconst assets = useMemo(() => createDemoAssetStore(host), [host])\n\n\tconst syncOpts = useShallowObjectIdentity(_syncOpts)\n\tconst syncOptsWithDefaults = useMemo(() => {\n\t\tif ('schema' in syncOpts && syncOpts.schema) return syncOpts\n\n\t\treturn {\n\t\t\t...syncOpts,\n\t\t\tshapeUtils:\n\t\t\t\t'shapeUtils' in syncOpts\n\t\t\t\t\t? [...defaultShapeUtils, ...(syncOpts.shapeUtils ?? [])]\n\t\t\t\t\t: defaultShapeUtils,\n\t\t\tbindingUtils:\n\t\t\t\t'bindingUtils' in syncOpts\n\t\t\t\t\t? [...defaultBindingUtils, ...(syncOpts.bindingUtils ?? [])]\n\t\t\t\t\t: defaultBindingUtils,\n\t\t}\n\t}, [syncOpts])\n\n\treturn useSync({\n\t\turi: `${host}/connect/${encodeURIComponent(roomId)}`,\n\t\troomId,\n\t\tassets,\n\t\tonMount: useCallback(\n\t\t\t(editor: Editor) => {\n\t\t\t\teditor.registerExternalAssetHandler('url', ({ url }) =>\n\t\t\t\t\tcreateAssetFromUrlUsingDemoServer(host, url)\n\t\t\t\t)\n\t\t\t},\n\t\t\t[host]\n\t\t),\n\t\t...syncOptsWithDefaults,\n\t})\n}\n\n/**\n * Determines whether file uploads should be disabled for a given host.\n *\n * Uploads are disabled for production tldraw domains to prevent abuse of the demo server\n * infrastructure. This includes tldraw.com and tldraw.xyz domains and their subdomains.\n *\n * @param host - The host URL to check for upload restrictions\n * @returns True if uploads should be disabled, false otherwise\n * @internal\n */\nfunction shouldDisallowUploads(host: string) {\n\tconst disallowedHosts = ['tldraw.com', 'tldraw.xyz']\n\treturn disallowedHosts.some(\n\t\t(disallowedHost) => host === disallowedHost || host.endsWith(`.${disallowedHost}`)\n\t)\n}\n\n/**\n * Creates an asset store implementation optimized for the tldraw demo server.\n *\n * This asset store handles file uploads to the demo server and provides intelligent\n * asset resolution with automatic image optimization based on network conditions,\n * screen density, and display size. It includes safeguards to prevent uploads to\n * production domains and optimizes images through the tldraw image processing service.\n *\n * @param host - The demo server host URL for file uploads and asset resolution\n * @returns A TLAssetStore implementation with upload and resolve capabilities\n * @example\n * ```ts\n * const assetStore = createDemoAssetStore('https://demo.tldraw.xyz')\n *\n * // Upload a file\n * const result = await assetStore.upload(asset, file)\n * console.log('Uploaded to:', result.src)\n *\n * // Resolve optimized asset URL\n * const optimizedUrl = assetStore.resolve(imageAsset, {\n * steppedScreenScale: 1.5,\n * dpr: 2,\n * networkEffectiveType: '4g'\n * })\n * ```\n * @internal\n */\nfunction createDemoAssetStore(host: string): TLAssetStore {\n\treturn {\n\t\tupload: async (_asset, file) => {\n\t\t\tif (shouldDisallowUploads(host)) {\n\t\t\t\talert('Uploading images is disabled in this demo.')\n\t\t\t\tthrow new Error('Uploading images is disabled in this demo.')\n\t\t\t}\n\t\t\tconst objectName = `${uniqueId()}-${file.name}`.replace(/\\W/g, '-')\n\t\t\tconst url = `${host}/uploads/${objectName}`\n\n\t\t\tconst response = await fetch(url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tbody: file,\n\t\t\t})\n\t\t\t// a rejected upload (invalid name, duplicate, server error) resolves the fetch; without\n\t\t\t// this the asset would point at a URL that was never stored\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`Failed to upload asset (${response.status})`)\n\t\t\t}\n\n\t\t\treturn { src: url }\n\t\t},\n\n\t\tresolve(asset, context) {\n\t\t\tconst { src } = asset.props\n\t\t\tif (!src) return null\n\n\t\t\t// We don't deal with videos at the moment.\n\t\t\tif (asset.type === 'video') return src\n\n\t\t\t// Assert it's an image to make TS happy.\n\t\t\tif (asset.type !== 'image') return null\n\n\t\t\t// Don't try to transform data: URLs, yikes.\n\t\t\tif (!src.startsWith('http:') && !src.startsWith('https:')) return src\n\n\t\t\tif (context.shouldResolveToOriginal) return src\n\n\t\t\t// Don't try to transform animated images.\n\t\t\tif (MediaHelpers.isAnimatedImageType(asset.props.mimeType) || asset.props.isAnimated)\n\t\t\t\treturn src\n\n\t\t\t// Don't try to transform vector images.\n\t\t\tif (MediaHelpers.isVectorImageType(asset.props.mimeType)) return src\n\n\t\t\tconst url = new URL(src)\n\n\t\t\t// we only transform images that are hosted on domains we control\n\t\t\tconst isTldrawImage =\n\t\t\t\turl.origin === host || /\\.tldraw\\.(?:com|xyz|dev|workers\\.dev)$/.test(url.host)\n\n\t\t\tif (!isTldrawImage) return src\n\n\t\t\t// Reduce image bandwidth on slow connections; unsupported browsers keep full quality.\n\t\t\tconst networkCompensation =\n\t\t\t\t!context.networkEffectiveType || context.networkEffectiveType === '4g' ? 1 : 0.5\n\n\t\t\tconst pixelRatio = asset.props.pixelRatio ?? 1\n\t\t\tconst trueWidth = asset.props.w * pixelRatio\n\t\t\tconst width = Math.ceil(\n\t\t\t\tMath.min(\n\t\t\t\t\ttrueWidth *\n\t\t\t\t\t\tclamp(context.steppedScreenScale, 1 / 32, 1) *\n\t\t\t\t\t\tnetworkCompensation *\n\t\t\t\t\t\tcontext.dpr,\n\t\t\t\t\ttrueWidth\n\t\t\t\t)\n\t\t\t)\n\n\t\t\t// Compressed file size can hide a large decoded bitmap. Avoid near-native resizes\n\t\t\t// so small dimension savings don't create extra CDN variants.\n\t\t\tif (\n\t\t\t\ttrueWidth >= MIN_RESIZE_DIMENSION &&\n\t\t\t\tasset.props.h * pixelRatio >= MIN_RESIZE_DIMENSION &&\n\t\t\t\twidth < trueWidth * 0.75\n\t\t\t) {\n\t\t\t\turl.searchParams.set('w', width.toString())\n\t\t\t}\n\n\t\t\treturn `${IMAGE_WORKER}/${url.host}/${url.toString().slice(url.origin.length + 1)}`\n\t\t},\n\t}\n}\n\n/**\n * Creates a bookmark asset by fetching metadata from a URL using the demo server.\n *\n * This function uses the demo server's bookmark unfurling service to extract metadata\n * like title, description, favicon, and preview image from a given URL. If the metadata\n * fetch fails, it returns a blank bookmark asset with just the URL.\n *\n * @param host - The demo server host URL to use for bookmark unfurling\n * @param url - The URL to create a bookmark asset from\n * @returns A promise that resolves to a TLAsset of type 'bookmark' with extracted metadata\n * @example\n * ```ts\n * const asset = await createAssetFromUrlUsingDemoServer(\n * 'https://demo.tldraw.xyz',\n * 'https://example.com'\n * )\n *\n * console.log(asset.props.title) // \"Example Domain\"\n * console.log(asset.props.description) // \"This domain is for use in illustrative examples...\"\n * ```\n * @internal\n */\nasync function createAssetFromUrlUsingDemoServer(host: string, url: string): Promise<TLAsset> {\n\tlet meta: { description?: string; image?: string; favicon?: string; title?: string } | null = null\n\ttry {\n\t\t// First, try to get the meta data from our endpoint\n\t\tconst fetchUrl = new URL(`${host}/bookmarks/unfurl`)\n\t\tfetchUrl.searchParams.set('url', url)\n\t\tmeta = await (await fetch(fetchUrl, { method: 'POST' })).json()\n\t} catch (error) {\n\t\t// Otherwise, fall back to a blank bookmark\n\t\tconsole.error(error)\n\t}\n\n\treturn {\n\t\tid: AssetRecordType.createId(getHashForString(url)),\n\t\ttypeName: 'asset',\n\t\ttype: 'bookmark',\n\t\tprops: {\n\t\t\tsrc: url,\n\t\t\tdescription: meta?.description ?? '',\n\t\t\timage: meta?.image ?? '',\n\t\t\tfavicon: meta?.favicon ?? '',\n\t\t\ttitle: meta?.title ?? '',\n\t\t},\n\t\tmeta: {},\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,7 +6,7 @@ import {
6
6
  import { useSyncDemo } from "./useSyncDemo.mjs";
7
7
  registerTldrawLibraryVersion(
8
8
  "@tldraw/sync",
9
- "5.6.0-canary.ce8fde24c9aa",
9
+ "5.6.0-canary.dd9b57126e5d",
10
10
  "esm"
11
11
  );
12
12
  export {
@@ -19,6 +19,7 @@ function getEnv(cb) {
19
19
  }
20
20
  const DEMO_WORKER = getEnv(() => "https://canary-demo.tldraw.xyz") ?? "https://demo.tldraw.xyz";
21
21
  const IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? "https://images.tldraw.xyz";
22
+ const MIN_RESIZE_DIMENSION = 400;
22
23
  function useSyncDemo(options) {
23
24
  const { roomId, host = DEMO_WORKER, ..._syncOpts } = options;
24
25
  const assets = useMemo(() => createDemoAssetStore(host), [host]);
@@ -84,18 +85,16 @@ function createDemoAssetStore(host) {
84
85
  const url = new URL(src);
85
86
  const isTldrawImage = url.origin === host || /\.tldraw\.(?:com|xyz|dev|workers\.dev)$/.test(url.host);
86
87
  if (!isTldrawImage) return src;
87
- const { fileSize = 0 } = asset.props;
88
- const isWorthResizing = fileSize >= 1024 * 1024 * 1.5;
89
- if (isWorthResizing) {
90
- const networkCompensation = !context.networkEffectiveType || context.networkEffectiveType === "4g" ? 1 : 0.5;
91
- const pixelRatio = asset.props.pixelRatio ?? 1;
92
- const trueWidth = asset.props.w * pixelRatio;
93
- const width = Math.ceil(
94
- Math.min(
95
- trueWidth * clamp(context.steppedScreenScale, 1 / 32, 1) * networkCompensation * context.dpr,
96
- trueWidth
97
- )
98
- );
88
+ const networkCompensation = !context.networkEffectiveType || context.networkEffectiveType === "4g" ? 1 : 0.5;
89
+ const pixelRatio = asset.props.pixelRatio ?? 1;
90
+ const trueWidth = asset.props.w * pixelRatio;
91
+ const width = Math.ceil(
92
+ Math.min(
93
+ trueWidth * clamp(context.steppedScreenScale, 1 / 32, 1) * networkCompensation * context.dpr,
94
+ trueWidth
95
+ )
96
+ );
97
+ if (trueWidth >= MIN_RESIZE_DIMENSION && asset.props.h * pixelRatio >= MIN_RESIZE_DIMENSION && width < trueWidth * 0.75) {
99
98
  url.searchParams.set("w", width.toString());
100
99
  }
101
100
  return `${IMAGE_WORKER}/${url.host}/${url.toString().slice(url.origin.length + 1)}`;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/useSyncDemo.ts"],
4
- "sourcesContent": ["import { useCallback, useMemo } from 'react'\nimport {\n\tAssetRecordType,\n\tEditor,\n\tMediaHelpers,\n\tTLAsset,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tTLUser,\n\tTLUserStore,\n\tclamp,\n\tdefaultBindingUtils,\n\tdefaultShapeUtils,\n\tgetHashForString,\n\tuniqueId,\n\tuseShallowObjectIdentity,\n} from 'tldraw'\nimport { RemoteTLStoreWithStatus, useSync } from './useSync'\n\n/** @public */\nexport interface UseSyncDemoOptions {\n\t/**\n\t * The room ID to sync with. Make sure the room ID is unique. The namespace is shared by\n\t * everyone using the demo server. Consider prefixing it with your company or project name.\n\t */\n\troomId: string\n\n\t/**\n\t * User store for identity, presence and attribution.\n\t * If not provided, a default implementation based on localStorage will be used.\n\t */\n\tusers?: TLUserStore\n\n\t/** @internal */\n\thost?: string\n\n\t/**\n\t * {@inheritdoc UseSyncOptions.getUserPresence}\n\t * @public\n\t */\n\tgetUserPresence?(store: TLStore, user: TLUser): TLPresenceStateInfo | null\n}\n\n/**\n * Safely accesses environment variables across different bundling environments.\n *\n * Depending on the environment this package is used in, process.env may not be available. This function\n * wraps `process.env` accesses in a try/catch to prevent runtime errors in environments where process\n * is not defined.\n *\n * The reason that this is just a try/catch and not a dynamic check e.g. `process &&\n * process.env[key]` is that many bundlers implement `process.env.WHATEVER` using compile-time\n * string replacement, rather than actually creating a runtime implementation of a `process` object.\n *\n * @param cb - Callback function that accesses an environment variable\n * @returns The environment variable value if available, otherwise undefined\n * @internal\n */\nfunction getEnv(cb: () => string | undefined): string | undefined {\n\ttry {\n\t\treturn cb()\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\nconst DEMO_WORKER = getEnv(() => process.env.TLDRAW_BEMO_URL) ?? 'https://demo.tldraw.xyz'\nconst IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? 'https://images.tldraw.xyz'\n\n/**\n * Creates a tldraw store synced with a multiplayer room hosted on tldraw's demo server `https://demo.tldraw.xyz`.\n *\n * The store can be passed directly into the `<Tldraw />` component to enable multiplayer features.\n * It will handle loading states, and enable multiplayer UX like user cursors and following.\n *\n * All data on the demo server is\n *\n * - Deleted after a day or so.\n * - Publicly accessible to anyone who knows the room ID. Use your company name as a prefix to help avoid collisions, or generate UUIDs for maximum privacy.\n *\n * @example\n * ```tsx\n * function MyApp() {\n * const store = useSyncDemo({roomId: 'my-app-test-room'})\n * return <Tldraw store={store} />\n * }\n * ```\n *\n * @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link @tldraw/editor#TLStoreSchemaOptions}.\n *\n * @public\n */\nexport function useSyncDemo(\n\toptions: UseSyncDemoOptions & TLStoreSchemaOptions\n): RemoteTLStoreWithStatus {\n\tconst { roomId, host = DEMO_WORKER, ..._syncOpts } = options\n\tconst assets = useMemo(() => createDemoAssetStore(host), [host])\n\n\tconst syncOpts = useShallowObjectIdentity(_syncOpts)\n\tconst syncOptsWithDefaults = useMemo(() => {\n\t\tif ('schema' in syncOpts && syncOpts.schema) return syncOpts\n\n\t\treturn {\n\t\t\t...syncOpts,\n\t\t\tshapeUtils:\n\t\t\t\t'shapeUtils' in syncOpts\n\t\t\t\t\t? [...defaultShapeUtils, ...(syncOpts.shapeUtils ?? [])]\n\t\t\t\t\t: defaultShapeUtils,\n\t\t\tbindingUtils:\n\t\t\t\t'bindingUtils' in syncOpts\n\t\t\t\t\t? [...defaultBindingUtils, ...(syncOpts.bindingUtils ?? [])]\n\t\t\t\t\t: defaultBindingUtils,\n\t\t}\n\t}, [syncOpts])\n\n\treturn useSync({\n\t\turi: `${host}/connect/${encodeURIComponent(roomId)}`,\n\t\troomId,\n\t\tassets,\n\t\tonMount: useCallback(\n\t\t\t(editor: Editor) => {\n\t\t\t\teditor.registerExternalAssetHandler('url', ({ url }) =>\n\t\t\t\t\tcreateAssetFromUrlUsingDemoServer(host, url)\n\t\t\t\t)\n\t\t\t},\n\t\t\t[host]\n\t\t),\n\t\t...syncOptsWithDefaults,\n\t})\n}\n\n/**\n * Determines whether file uploads should be disabled for a given host.\n *\n * Uploads are disabled for production tldraw domains to prevent abuse of the demo server\n * infrastructure. This includes tldraw.com and tldraw.xyz domains and their subdomains.\n *\n * @param host - The host URL to check for upload restrictions\n * @returns True if uploads should be disabled, false otherwise\n * @internal\n */\nfunction shouldDisallowUploads(host: string) {\n\tconst disallowedHosts = ['tldraw.com', 'tldraw.xyz']\n\treturn disallowedHosts.some(\n\t\t(disallowedHost) => host === disallowedHost || host.endsWith(`.${disallowedHost}`)\n\t)\n}\n\n/**\n * Creates an asset store implementation optimized for the tldraw demo server.\n *\n * This asset store handles file uploads to the demo server and provides intelligent\n * asset resolution with automatic image optimization based on network conditions,\n * screen density, and display size. It includes safeguards to prevent uploads to\n * production domains and optimizes images through the tldraw image processing service.\n *\n * @param host - The demo server host URL for file uploads and asset resolution\n * @returns A TLAssetStore implementation with upload and resolve capabilities\n * @example\n * ```ts\n * const assetStore = createDemoAssetStore('https://demo.tldraw.xyz')\n *\n * // Upload a file\n * const result = await assetStore.upload(asset, file)\n * console.log('Uploaded to:', result.src)\n *\n * // Resolve optimized asset URL\n * const optimizedUrl = assetStore.resolve(imageAsset, {\n * steppedScreenScale: 1.5,\n * dpr: 2,\n * networkEffectiveType: '4g'\n * })\n * ```\n * @internal\n */\nfunction createDemoAssetStore(host: string): TLAssetStore {\n\treturn {\n\t\tupload: async (_asset, file) => {\n\t\t\tif (shouldDisallowUploads(host)) {\n\t\t\t\talert('Uploading images is disabled in this demo.')\n\t\t\t\tthrow new Error('Uploading images is disabled in this demo.')\n\t\t\t}\n\t\t\tconst objectName = `${uniqueId()}-${file.name}`.replace(/\\W/g, '-')\n\t\t\tconst url = `${host}/uploads/${objectName}`\n\n\t\t\tconst response = await fetch(url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tbody: file,\n\t\t\t})\n\t\t\t// a rejected upload (invalid name, duplicate, server error) resolves the fetch; without\n\t\t\t// this the asset would point at a URL that was never stored\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`Failed to upload asset (${response.status})`)\n\t\t\t}\n\n\t\t\treturn { src: url }\n\t\t},\n\n\t\tresolve(asset, context) {\n\t\t\tconst { src } = asset.props\n\t\t\tif (!src) return null\n\n\t\t\t// We don't deal with videos at the moment.\n\t\t\tif (asset.type === 'video') return src\n\n\t\t\t// Assert it's an image to make TS happy.\n\t\t\tif (asset.type !== 'image') return null\n\n\t\t\t// Don't try to transform data: URLs, yikes.\n\t\t\tif (!src.startsWith('http:') && !src.startsWith('https:')) return src\n\n\t\t\tif (context.shouldResolveToOriginal) return src\n\n\t\t\t// Don't try to transform animated images.\n\t\t\tif (MediaHelpers.isAnimatedImageType(asset.props.mimeType) || asset.props.isAnimated)\n\t\t\t\treturn src\n\n\t\t\t// Don't try to transform vector images.\n\t\t\tif (MediaHelpers.isVectorImageType(asset.props.mimeType)) return src\n\n\t\t\tconst url = new URL(src)\n\n\t\t\t// we only transform images that are hosted on domains we control\n\t\t\tconst isTldrawImage =\n\t\t\t\turl.origin === host || /\\.tldraw\\.(?:com|xyz|dev|workers\\.dev)$/.test(url.host)\n\n\t\t\tif (!isTldrawImage) return src\n\n\t\t\t// Assets that are under a certain file size aren't worth transforming (and incurring cost).\n\t\t\t// We still send them through the image worker to get them optimized though.\n\t\t\tconst { fileSize = 0 } = asset.props\n\t\t\tconst isWorthResizing = fileSize >= 1024 * 1024 * 1.5\n\n\t\t\tif (isWorthResizing) {\n\t\t\t\t// N.B. navigator.connection is only available in certain browsers (mainly Blink-based browsers)\n\t\t\t\t// 4g is as high the 'effectiveType' goes and we can pick a lower effective image quality for slower connections.\n\t\t\t\tconst networkCompensation =\n\t\t\t\t\t!context.networkEffectiveType || context.networkEffectiveType === '4g' ? 1 : 0.5\n\n\t\t\t\tconst pixelRatio = asset.props.pixelRatio ?? 1\n\t\t\t\tconst trueWidth = asset.props.w * pixelRatio\n\t\t\t\tconst width = Math.ceil(\n\t\t\t\t\tMath.min(\n\t\t\t\t\t\ttrueWidth *\n\t\t\t\t\t\t\tclamp(context.steppedScreenScale, 1 / 32, 1) *\n\t\t\t\t\t\t\tnetworkCompensation *\n\t\t\t\t\t\t\tcontext.dpr,\n\t\t\t\t\t\ttrueWidth\n\t\t\t\t\t)\n\t\t\t\t)\n\n\t\t\t\turl.searchParams.set('w', width.toString())\n\t\t\t}\n\n\t\t\treturn `${IMAGE_WORKER}/${url.host}/${url.toString().slice(url.origin.length + 1)}`\n\t\t},\n\t}\n}\n\n/**\n * Creates a bookmark asset by fetching metadata from a URL using the demo server.\n *\n * This function uses the demo server's bookmark unfurling service to extract metadata\n * like title, description, favicon, and preview image from a given URL. If the metadata\n * fetch fails, it returns a blank bookmark asset with just the URL.\n *\n * @param host - The demo server host URL to use for bookmark unfurling\n * @param url - The URL to create a bookmark asset from\n * @returns A promise that resolves to a TLAsset of type 'bookmark' with extracted metadata\n * @example\n * ```ts\n * const asset = await createAssetFromUrlUsingDemoServer(\n * 'https://demo.tldraw.xyz',\n * 'https://example.com'\n * )\n *\n * console.log(asset.props.title) // \"Example Domain\"\n * console.log(asset.props.description) // \"This domain is for use in illustrative examples...\"\n * ```\n * @internal\n */\nasync function createAssetFromUrlUsingDemoServer(host: string, url: string): Promise<TLAsset> {\n\tlet meta: { description?: string; image?: string; favicon?: string; title?: string } | null = null\n\ttry {\n\t\t// First, try to get the meta data from our endpoint\n\t\tconst fetchUrl = new URL(`${host}/bookmarks/unfurl`)\n\t\tfetchUrl.searchParams.set('url', url)\n\t\tmeta = await (await fetch(fetchUrl, { method: 'POST' })).json()\n\t} catch (error) {\n\t\t// Otherwise, fall back to a blank bookmark\n\t\tconsole.error(error)\n\t}\n\n\treturn {\n\t\tid: AssetRecordType.createId(getHashForString(url)),\n\t\ttypeName: 'asset',\n\t\ttype: 'bookmark',\n\t\tprops: {\n\t\t\tsrc: url,\n\t\t\tdescription: meta?.description ?? '',\n\t\t\timage: meta?.image ?? '',\n\t\t\tfavicon: meta?.favicon ?? '',\n\t\t\ttitle: meta?.title ?? '',\n\t\t},\n\t\tmeta: {},\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { useCallback, useMemo } from 'react'\nimport {\n\tAssetRecordType,\n\tEditor,\n\tMediaHelpers,\n\tTLAsset,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tTLUser,\n\tTLUserStore,\n\tclamp,\n\tdefaultBindingUtils,\n\tdefaultShapeUtils,\n\tgetHashForString,\n\tuniqueId,\n\tuseShallowObjectIdentity,\n} from 'tldraw'\nimport { RemoteTLStoreWithStatus, useSync } from './useSync'\n\n/** @public */\nexport interface UseSyncDemoOptions {\n\t/**\n\t * The room ID to sync with. Make sure the room ID is unique. The namespace is shared by\n\t * everyone using the demo server. Consider prefixing it with your company or project name.\n\t */\n\troomId: string\n\n\t/**\n\t * User store for identity, presence and attribution.\n\t * If not provided, a default implementation based on localStorage will be used.\n\t */\n\tusers?: TLUserStore\n\n\t/** @internal */\n\thost?: string\n\n\t/**\n\t * {@inheritdoc UseSyncOptions.getUserPresence}\n\t * @public\n\t */\n\tgetUserPresence?(store: TLStore, user: TLUser): TLPresenceStateInfo | null\n}\n\n/**\n * Safely accesses environment variables across different bundling environments.\n *\n * Depending on the environment this package is used in, process.env may not be available. This function\n * wraps `process.env` accesses in a try/catch to prevent runtime errors in environments where process\n * is not defined.\n *\n * The reason that this is just a try/catch and not a dynamic check e.g. `process &&\n * process.env[key]` is that many bundlers implement `process.env.WHATEVER` using compile-time\n * string replacement, rather than actually creating a runtime implementation of a `process` object.\n *\n * @param cb - Callback function that accesses an environment variable\n * @returns The environment variable value if available, otherwise undefined\n * @internal\n */\nfunction getEnv(cb: () => string | undefined): string | undefined {\n\ttry {\n\t\treturn cb()\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\nconst DEMO_WORKER = getEnv(() => process.env.TLDRAW_BEMO_URL) ?? 'https://demo.tldraw.xyz'\nconst IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? 'https://images.tldraw.xyz'\nconst MIN_RESIZE_DIMENSION = 400\n\n/**\n * Creates a tldraw store synced with a multiplayer room hosted on tldraw's demo server `https://demo.tldraw.xyz`.\n *\n * The store can be passed directly into the `<Tldraw />` component to enable multiplayer features.\n * It will handle loading states, and enable multiplayer UX like user cursors and following.\n *\n * All data on the demo server is\n *\n * - Deleted after a day or so.\n * - Publicly accessible to anyone who knows the room ID. Use your company name as a prefix to help avoid collisions, or generate UUIDs for maximum privacy.\n *\n * @example\n * ```tsx\n * function MyApp() {\n * const store = useSyncDemo({roomId: 'my-app-test-room'})\n * return <Tldraw store={store} />\n * }\n * ```\n *\n * @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link @tldraw/editor#TLStoreSchemaOptions}.\n *\n * @public\n */\nexport function useSyncDemo(\n\toptions: UseSyncDemoOptions & TLStoreSchemaOptions\n): RemoteTLStoreWithStatus {\n\tconst { roomId, host = DEMO_WORKER, ..._syncOpts } = options\n\tconst assets = useMemo(() => createDemoAssetStore(host), [host])\n\n\tconst syncOpts = useShallowObjectIdentity(_syncOpts)\n\tconst syncOptsWithDefaults = useMemo(() => {\n\t\tif ('schema' in syncOpts && syncOpts.schema) return syncOpts\n\n\t\treturn {\n\t\t\t...syncOpts,\n\t\t\tshapeUtils:\n\t\t\t\t'shapeUtils' in syncOpts\n\t\t\t\t\t? [...defaultShapeUtils, ...(syncOpts.shapeUtils ?? [])]\n\t\t\t\t\t: defaultShapeUtils,\n\t\t\tbindingUtils:\n\t\t\t\t'bindingUtils' in syncOpts\n\t\t\t\t\t? [...defaultBindingUtils, ...(syncOpts.bindingUtils ?? [])]\n\t\t\t\t\t: defaultBindingUtils,\n\t\t}\n\t}, [syncOpts])\n\n\treturn useSync({\n\t\turi: `${host}/connect/${encodeURIComponent(roomId)}`,\n\t\troomId,\n\t\tassets,\n\t\tonMount: useCallback(\n\t\t\t(editor: Editor) => {\n\t\t\t\teditor.registerExternalAssetHandler('url', ({ url }) =>\n\t\t\t\t\tcreateAssetFromUrlUsingDemoServer(host, url)\n\t\t\t\t)\n\t\t\t},\n\t\t\t[host]\n\t\t),\n\t\t...syncOptsWithDefaults,\n\t})\n}\n\n/**\n * Determines whether file uploads should be disabled for a given host.\n *\n * Uploads are disabled for production tldraw domains to prevent abuse of the demo server\n * infrastructure. This includes tldraw.com and tldraw.xyz domains and their subdomains.\n *\n * @param host - The host URL to check for upload restrictions\n * @returns True if uploads should be disabled, false otherwise\n * @internal\n */\nfunction shouldDisallowUploads(host: string) {\n\tconst disallowedHosts = ['tldraw.com', 'tldraw.xyz']\n\treturn disallowedHosts.some(\n\t\t(disallowedHost) => host === disallowedHost || host.endsWith(`.${disallowedHost}`)\n\t)\n}\n\n/**\n * Creates an asset store implementation optimized for the tldraw demo server.\n *\n * This asset store handles file uploads to the demo server and provides intelligent\n * asset resolution with automatic image optimization based on network conditions,\n * screen density, and display size. It includes safeguards to prevent uploads to\n * production domains and optimizes images through the tldraw image processing service.\n *\n * @param host - The demo server host URL for file uploads and asset resolution\n * @returns A TLAssetStore implementation with upload and resolve capabilities\n * @example\n * ```ts\n * const assetStore = createDemoAssetStore('https://demo.tldraw.xyz')\n *\n * // Upload a file\n * const result = await assetStore.upload(asset, file)\n * console.log('Uploaded to:', result.src)\n *\n * // Resolve optimized asset URL\n * const optimizedUrl = assetStore.resolve(imageAsset, {\n * steppedScreenScale: 1.5,\n * dpr: 2,\n * networkEffectiveType: '4g'\n * })\n * ```\n * @internal\n */\nfunction createDemoAssetStore(host: string): TLAssetStore {\n\treturn {\n\t\tupload: async (_asset, file) => {\n\t\t\tif (shouldDisallowUploads(host)) {\n\t\t\t\talert('Uploading images is disabled in this demo.')\n\t\t\t\tthrow new Error('Uploading images is disabled in this demo.')\n\t\t\t}\n\t\t\tconst objectName = `${uniqueId()}-${file.name}`.replace(/\\W/g, '-')\n\t\t\tconst url = `${host}/uploads/${objectName}`\n\n\t\t\tconst response = await fetch(url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tbody: file,\n\t\t\t})\n\t\t\t// a rejected upload (invalid name, duplicate, server error) resolves the fetch; without\n\t\t\t// this the asset would point at a URL that was never stored\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`Failed to upload asset (${response.status})`)\n\t\t\t}\n\n\t\t\treturn { src: url }\n\t\t},\n\n\t\tresolve(asset, context) {\n\t\t\tconst { src } = asset.props\n\t\t\tif (!src) return null\n\n\t\t\t// We don't deal with videos at the moment.\n\t\t\tif (asset.type === 'video') return src\n\n\t\t\t// Assert it's an image to make TS happy.\n\t\t\tif (asset.type !== 'image') return null\n\n\t\t\t// Don't try to transform data: URLs, yikes.\n\t\t\tif (!src.startsWith('http:') && !src.startsWith('https:')) return src\n\n\t\t\tif (context.shouldResolveToOriginal) return src\n\n\t\t\t// Don't try to transform animated images.\n\t\t\tif (MediaHelpers.isAnimatedImageType(asset.props.mimeType) || asset.props.isAnimated)\n\t\t\t\treturn src\n\n\t\t\t// Don't try to transform vector images.\n\t\t\tif (MediaHelpers.isVectorImageType(asset.props.mimeType)) return src\n\n\t\t\tconst url = new URL(src)\n\n\t\t\t// we only transform images that are hosted on domains we control\n\t\t\tconst isTldrawImage =\n\t\t\t\turl.origin === host || /\\.tldraw\\.(?:com|xyz|dev|workers\\.dev)$/.test(url.host)\n\n\t\t\tif (!isTldrawImage) return src\n\n\t\t\t// Reduce image bandwidth on slow connections; unsupported browsers keep full quality.\n\t\t\tconst networkCompensation =\n\t\t\t\t!context.networkEffectiveType || context.networkEffectiveType === '4g' ? 1 : 0.5\n\n\t\t\tconst pixelRatio = asset.props.pixelRatio ?? 1\n\t\t\tconst trueWidth = asset.props.w * pixelRatio\n\t\t\tconst width = Math.ceil(\n\t\t\t\tMath.min(\n\t\t\t\t\ttrueWidth *\n\t\t\t\t\t\tclamp(context.steppedScreenScale, 1 / 32, 1) *\n\t\t\t\t\t\tnetworkCompensation *\n\t\t\t\t\t\tcontext.dpr,\n\t\t\t\t\ttrueWidth\n\t\t\t\t)\n\t\t\t)\n\n\t\t\t// Compressed file size can hide a large decoded bitmap. Avoid near-native resizes\n\t\t\t// so small dimension savings don't create extra CDN variants.\n\t\t\tif (\n\t\t\t\ttrueWidth >= MIN_RESIZE_DIMENSION &&\n\t\t\t\tasset.props.h * pixelRatio >= MIN_RESIZE_DIMENSION &&\n\t\t\t\twidth < trueWidth * 0.75\n\t\t\t) {\n\t\t\t\turl.searchParams.set('w', width.toString())\n\t\t\t}\n\n\t\t\treturn `${IMAGE_WORKER}/${url.host}/${url.toString().slice(url.origin.length + 1)}`\n\t\t},\n\t}\n}\n\n/**\n * Creates a bookmark asset by fetching metadata from a URL using the demo server.\n *\n * This function uses the demo server's bookmark unfurling service to extract metadata\n * like title, description, favicon, and preview image from a given URL. If the metadata\n * fetch fails, it returns a blank bookmark asset with just the URL.\n *\n * @param host - The demo server host URL to use for bookmark unfurling\n * @param url - The URL to create a bookmark asset from\n * @returns A promise that resolves to a TLAsset of type 'bookmark' with extracted metadata\n * @example\n * ```ts\n * const asset = await createAssetFromUrlUsingDemoServer(\n * 'https://demo.tldraw.xyz',\n * 'https://example.com'\n * )\n *\n * console.log(asset.props.title) // \"Example Domain\"\n * console.log(asset.props.description) // \"This domain is for use in illustrative examples...\"\n * ```\n * @internal\n */\nasync function createAssetFromUrlUsingDemoServer(host: string, url: string): Promise<TLAsset> {\n\tlet meta: { description?: string; image?: string; favicon?: string; title?: string } | null = null\n\ttry {\n\t\t// First, try to get the meta data from our endpoint\n\t\tconst fetchUrl = new URL(`${host}/bookmarks/unfurl`)\n\t\tfetchUrl.searchParams.set('url', url)\n\t\tmeta = await (await fetch(fetchUrl, { method: 'POST' })).json()\n\t} catch (error) {\n\t\t// Otherwise, fall back to a blank bookmark\n\t\tconsole.error(error)\n\t}\n\n\treturn {\n\t\tid: AssetRecordType.createId(getHashForString(url)),\n\t\ttypeName: 'asset',\n\t\ttype: 'bookmark',\n\t\tprops: {\n\t\t\tsrc: url,\n\t\t\tdescription: meta?.description ?? '',\n\t\t\timage: meta?.image ?? '',\n\t\t\tfavicon: meta?.favicon ?? '',\n\t\t\ttitle: meta?.title ?? '',\n\t\t},\n\t\tmeta: {},\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/sync",
3
3
  "description": "tldraw infinite canvas SDK (multiplayer sync react bindings).",
4
- "version": "5.6.0-canary.ce8fde24c9aa",
4
+ "version": "5.6.0-canary.dd9b57126e5d",
5
5
  "author": {
6
6
  "name": "tldraw GB Ltd.",
7
7
  "email": "hello@tldraw.com"
@@ -50,12 +50,12 @@
50
50
  "vitest": "^5.0.0"
51
51
  },
52
52
  "dependencies": {
53
- "@tldraw/state": "5.6.0-canary.ce8fde24c9aa",
54
- "@tldraw/state-react": "5.6.0-canary.ce8fde24c9aa",
55
- "@tldraw/sync-core": "5.6.0-canary.ce8fde24c9aa",
56
- "@tldraw/utils": "5.6.0-canary.ce8fde24c9aa",
53
+ "@tldraw/state": "5.6.0-canary.dd9b57126e5d",
54
+ "@tldraw/state-react": "5.6.0-canary.dd9b57126e5d",
55
+ "@tldraw/sync-core": "5.6.0-canary.dd9b57126e5d",
56
+ "@tldraw/utils": "5.6.0-canary.dd9b57126e5d",
57
57
  "nanoevents": "^7.0.1",
58
- "tldraw": "5.6.0-canary.ce8fde24c9aa",
58
+ "tldraw": "5.6.0-canary.dd9b57126e5d",
59
59
  "ws": "^8.18.0"
60
60
  },
61
61
  "peerDependencies": {
@@ -135,6 +135,39 @@ describe('useSyncDemo', () => {
135
135
  })
136
136
  })
137
137
 
138
+ describe('asset resolution', () => {
139
+ function resolveImage(props: Record<string, unknown>, scale: number) {
140
+ const assetStore = vi.mocked(useSync).mock.calls[0][0].assets
141
+ const asset = {
142
+ type: 'image',
143
+ props: { src: 'https://demo.tldraw.xyz/uploads/image', ...props },
144
+ }
145
+ return assetStore.resolve!(asset as TLAsset, {
146
+ screenScale: scale,
147
+ steppedScreenScale: scale,
148
+ dpr: 1,
149
+ networkEffectiveType: '4g',
150
+ shouldResolveToOriginal: false,
151
+ })
152
+ }
153
+
154
+ it.each([
155
+ { mimeType: 'image/png', w: 2000, h: 1500, fileSize: 1000, scale: 0.5, expected: '?w=1000' },
156
+ { mimeType: 'image/jpeg', w: 2000, h: 1500, fileSize: 1000, scale: 0.5, expected: '?w=1000' },
157
+ { mimeType: 'image/webp', w: 2000, h: 1500, fileSize: 1000, scale: 0.5, expected: '?w=1000' },
158
+ { mimeType: 'image/jpeg', w: 399, h: 400, fileSize: 1000, scale: 0.5, expected: '' },
159
+ { mimeType: 'image/jpeg', w: 2000, h: 1500, fileSize: 1000, scale: 0.75, expected: '' },
160
+ ])(
161
+ 'should resize $mimeType at $w × $h and $scale scale',
162
+ async ({ expected, scale, ...props }) => {
163
+ useSyncDemo({ roomId: 'test-room', host: 'https://demo.server.com' })
164
+ expect(await resolveImage(props, scale)).toBe(
165
+ `https://images.tldraw.xyz/demo.tldraw.xyz/uploads/image${expected}`
166
+ )
167
+ }
168
+ )
169
+ })
170
+
138
171
  describe('bookmark asset creation', () => {
139
172
  it('should create bookmark assets with metadata when successful', async () => {
140
173
  const mockEditor = { registerExternalAssetHandler: vi.fn() } as any
@@ -68,6 +68,7 @@ function getEnv(cb: () => string | undefined): string | undefined {
68
68
 
69
69
  const DEMO_WORKER = getEnv(() => process.env.TLDRAW_BEMO_URL) ?? 'https://demo.tldraw.xyz'
70
70
  const IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? 'https://images.tldraw.xyz'
71
+ const MIN_RESIZE_DIMENSION = 400
71
72
 
72
73
  /**
73
74
  * Creates a tldraw store synced with a multiplayer room hosted on tldraw's demo server `https://demo.tldraw.xyz`.
@@ -228,29 +229,29 @@ function createDemoAssetStore(host: string): TLAssetStore {
228
229
 
229
230
  if (!isTldrawImage) return src
230
231
 
231
- // Assets that are under a certain file size aren't worth transforming (and incurring cost).
232
- // We still send them through the image worker to get them optimized though.
233
- const { fileSize = 0 } = asset.props
234
- const isWorthResizing = fileSize >= 1024 * 1024 * 1.5
232
+ // Reduce image bandwidth on slow connections; unsupported browsers keep full quality.
233
+ const networkCompensation =
234
+ !context.networkEffectiveType || context.networkEffectiveType === '4g' ? 1 : 0.5
235
235
 
236
- if (isWorthResizing) {
237
- // N.B. navigator.connection is only available in certain browsers (mainly Blink-based browsers)
238
- // 4g is as high the 'effectiveType' goes and we can pick a lower effective image quality for slower connections.
239
- const networkCompensation =
240
- !context.networkEffectiveType || context.networkEffectiveType === '4g' ? 1 : 0.5
241
-
242
- const pixelRatio = asset.props.pixelRatio ?? 1
243
- const trueWidth = asset.props.w * pixelRatio
244
- const width = Math.ceil(
245
- Math.min(
246
- trueWidth *
247
- clamp(context.steppedScreenScale, 1 / 32, 1) *
248
- networkCompensation *
249
- context.dpr,
250
- trueWidth
251
- )
236
+ const pixelRatio = asset.props.pixelRatio ?? 1
237
+ const trueWidth = asset.props.w * pixelRatio
238
+ const width = Math.ceil(
239
+ Math.min(
240
+ trueWidth *
241
+ clamp(context.steppedScreenScale, 1 / 32, 1) *
242
+ networkCompensation *
243
+ context.dpr,
244
+ trueWidth
252
245
  )
246
+ )
253
247
 
248
+ // Compressed file size can hide a large decoded bitmap. Avoid near-native resizes
249
+ // so small dimension savings don't create extra CDN variants.
250
+ if (
251
+ trueWidth >= MIN_RESIZE_DIMENSION &&
252
+ asset.props.h * pixelRatio >= MIN_RESIZE_DIMENSION &&
253
+ width < trueWidth * 0.75
254
+ ) {
254
255
  url.searchParams.set('w', width.toString())
255
256
  }
256
257