@tldraw/sync 4.2.0 → 4.3.0-canary.9490c19e233a
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
|
-
"4.
|
|
32
|
+
"4.3.0-canary.9490c19e233a",
|
|
33
33
|
"cjs"
|
|
34
34
|
);
|
|
35
35
|
//# sourceMappingURL=index.js.map
|
package/dist-cjs/useSyncDemo.js
CHANGED
|
@@ -31,7 +31,7 @@ function getEnv(cb) {
|
|
|
31
31
|
return void 0;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
const DEMO_WORKER = getEnv(() => "https://demo.tldraw.xyz") ?? "https://demo.tldraw.xyz";
|
|
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
36
|
function useSyncDemo(options) {
|
|
37
37
|
const { roomId, host = DEMO_WORKER, ..._syncOpts } = options;
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/useSyncDemo.ts"],
|
|
4
4
|
"sourcesContent": ["import { useCallback, useMemo } from 'react'\nimport {\n\tAssetRecordType,\n\tEditor,\n\tMediaHelpers,\n\tSignal,\n\tTLAsset,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLStore,\n\tTLStoreSchemaOptions,\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\t/**\n\t * A signal that contains the user information needed for multiplayer features.\n\t * This should be synchronized with the `userPreferences` configuration for the main `<Tldraw />` component.\n\t * If not provided, a default implementation based on localStorage will be used.\n\t */\n\tuserInfo?: TLPresenceUserInfo | Signal<TLPresenceUserInfo>\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: TLPresenceUserInfo): 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#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', async ({ url }) => {\n\t\t\t\t\treturn await createAssetFromUrlUsingDemoServer(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 id = uniqueId()\n\n\t\t\tconst objectName = `${id}-${file.name}`.replace(/\\W/g, '-')\n\t\t\tconst url = `${host}/uploads/${objectName}`\n\n\t\t\tawait fetch(url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tbody: file,\n\t\t\t})\n\n\t\t\treturn { src: url }\n\t\t},\n\n\t\tresolve(asset, context) {\n\t\t\tif (!asset.props.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 asset.props.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 (!asset.props.src.startsWith('http:') && !asset.props.src.startsWith('https:'))\n\t\t\t\treturn asset.props.src\n\n\t\t\tif (context.shouldResolveToOriginal) return asset.props.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 asset.props.src\n\n\t\t\t// Don't try to transform vector images.\n\t\t\tif (MediaHelpers.isVectorImageType(asset?.props.mimeType)) return asset.props.src\n\n\t\t\tconst url = new URL(asset.props.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 asset.props.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 width = Math.ceil(\n\t\t\t\t\tMath.min(\n\t\t\t\t\t\tasset.props.w *\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\tasset.props.w\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\tconst newUrl = `${IMAGE_WORKER}/${url.host}/${url.toString().slice(url.origin.length + 1)}`\n\t\t\treturn newUrl\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\tconst urlHash = getHashForString(url)\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\n\t\tconst meta = (await (await fetch(fetchUrl, { method: 'POST' })).json()) as {\n\t\t\tdescription?: string\n\t\t\timage?: string\n\t\t\tfavicon?: string\n\t\t\ttitle?: string\n\t\t} | null\n\n\t\treturn {\n\t\t\tid: AssetRecordType.createId(urlHash),\n\t\t\ttypeName: 'asset',\n\t\t\ttype: 'bookmark',\n\t\t\tprops: {\n\t\t\t\tsrc: url,\n\t\t\t\tdescription: meta?.description ?? '',\n\t\t\t\timage: meta?.image ?? '',\n\t\t\t\tfavicon: meta?.favicon ?? '',\n\t\t\t\ttitle: meta?.title ?? '',\n\t\t\t},\n\t\t\tmeta: {},\n\t\t}\n\t} catch (error) {\n\t\t// Otherwise, fallback to a blank bookmark\n\t\tconsole.error(error)\n\t\treturn {\n\t\t\tid: AssetRecordType.createId(urlHash),\n\t\t\ttypeName: 'asset',\n\t\t\ttype: 'bookmark',\n\t\t\tprops: {\n\t\t\t\tsrc: url,\n\t\t\t\tdescription: '',\n\t\t\t\timage: '',\n\t\t\t\tfavicon: '',\n\t\t\t\ttitle: '',\n\t\t\t},\n\t\t\tmeta: {},\n\t\t}\n\t}\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAqC;AACrC,oBAiBO;AACP,qBAAiD;AAyCjD,SAAS,OAAO,IAAkD;AACjE,MAAI;AACH,WAAO,GAAG;AAAA,EACX,QAAQ;AACP,WAAO;AAAA,EACR;AACD;AAEA,MAAM,cAAc,OAAO,MAAM,
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/index.mjs
CHANGED
package/dist-esm/useSyncDemo.mjs
CHANGED
|
@@ -17,7 +17,7 @@ function getEnv(cb) {
|
|
|
17
17
|
return void 0;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
const DEMO_WORKER = getEnv(() => "https://demo.tldraw.xyz") ?? "https://demo.tldraw.xyz";
|
|
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
22
|
function useSyncDemo(options) {
|
|
23
23
|
const { roomId, host = DEMO_WORKER, ..._syncOpts } = options;
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/useSyncDemo.ts"],
|
|
4
4
|
"sourcesContent": ["import { useCallback, useMemo } from 'react'\nimport {\n\tAssetRecordType,\n\tEditor,\n\tMediaHelpers,\n\tSignal,\n\tTLAsset,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLStore,\n\tTLStoreSchemaOptions,\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\t/**\n\t * A signal that contains the user information needed for multiplayer features.\n\t * This should be synchronized with the `userPreferences` configuration for the main `<Tldraw />` component.\n\t * If not provided, a default implementation based on localStorage will be used.\n\t */\n\tuserInfo?: TLPresenceUserInfo | Signal<TLPresenceUserInfo>\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: TLPresenceUserInfo): 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#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', async ({ url }) => {\n\t\t\t\t\treturn await createAssetFromUrlUsingDemoServer(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 id = uniqueId()\n\n\t\t\tconst objectName = `${id}-${file.name}`.replace(/\\W/g, '-')\n\t\t\tconst url = `${host}/uploads/${objectName}`\n\n\t\t\tawait fetch(url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tbody: file,\n\t\t\t})\n\n\t\t\treturn { src: url }\n\t\t},\n\n\t\tresolve(asset, context) {\n\t\t\tif (!asset.props.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 asset.props.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 (!asset.props.src.startsWith('http:') && !asset.props.src.startsWith('https:'))\n\t\t\t\treturn asset.props.src\n\n\t\t\tif (context.shouldResolveToOriginal) return asset.props.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 asset.props.src\n\n\t\t\t// Don't try to transform vector images.\n\t\t\tif (MediaHelpers.isVectorImageType(asset?.props.mimeType)) return asset.props.src\n\n\t\t\tconst url = new URL(asset.props.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 asset.props.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 width = Math.ceil(\n\t\t\t\t\tMath.min(\n\t\t\t\t\t\tasset.props.w *\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\tasset.props.w\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\tconst newUrl = `${IMAGE_WORKER}/${url.host}/${url.toString().slice(url.origin.length + 1)}`\n\t\t\treturn newUrl\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\tconst urlHash = getHashForString(url)\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\n\t\tconst meta = (await (await fetch(fetchUrl, { method: 'POST' })).json()) as {\n\t\t\tdescription?: string\n\t\t\timage?: string\n\t\t\tfavicon?: string\n\t\t\ttitle?: string\n\t\t} | null\n\n\t\treturn {\n\t\t\tid: AssetRecordType.createId(urlHash),\n\t\t\ttypeName: 'asset',\n\t\t\ttype: 'bookmark',\n\t\t\tprops: {\n\t\t\t\tsrc: url,\n\t\t\t\tdescription: meta?.description ?? '',\n\t\t\t\timage: meta?.image ?? '',\n\t\t\t\tfavicon: meta?.favicon ?? '',\n\t\t\t\ttitle: meta?.title ?? '',\n\t\t\t},\n\t\t\tmeta: {},\n\t\t}\n\t} catch (error) {\n\t\t// Otherwise, fallback to a blank bookmark\n\t\tconsole.error(error)\n\t\treturn {\n\t\t\tid: AssetRecordType.createId(urlHash),\n\t\t\ttypeName: 'asset',\n\t\t\ttype: 'bookmark',\n\t\t\tprops: {\n\t\t\t\tsrc: url,\n\t\t\t\tdescription: '',\n\t\t\t\timage: '',\n\t\t\t\tfavicon: '',\n\t\t\t\ttitle: '',\n\t\t\t},\n\t\t\tmeta: {},\n\t\t}\n\t}\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,aAAa,eAAe;AACrC;AAAA,EACC;AAAA,EAEA;AAAA,EAQA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAkC,eAAe;AAyCjD,SAAS,OAAO,IAAkD;AACjE,MAAI;AACH,WAAO,GAAG;AAAA,EACX,QAAQ;AACP,WAAO;AAAA,EACR;AACD;AAEA,MAAM,cAAc,OAAO,MAAM,
|
|
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": "4.
|
|
4
|
+
"version": "4.3.0-canary.9490c19e233a",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw GB Ltd.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"vitest": "^3.2.4"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@tldraw/state": "4.
|
|
57
|
-
"@tldraw/state-react": "4.
|
|
58
|
-
"@tldraw/sync-core": "4.
|
|
59
|
-
"@tldraw/utils": "4.
|
|
56
|
+
"@tldraw/state": "4.3.0-canary.9490c19e233a",
|
|
57
|
+
"@tldraw/state-react": "4.3.0-canary.9490c19e233a",
|
|
58
|
+
"@tldraw/sync-core": "4.3.0-canary.9490c19e233a",
|
|
59
|
+
"@tldraw/utils": "4.3.0-canary.9490c19e233a",
|
|
60
60
|
"nanoevents": "^7.0.1",
|
|
61
|
-
"tldraw": "4.
|
|
61
|
+
"tldraw": "4.3.0-canary.9490c19e233a",
|
|
62
62
|
"ws": "^8.18.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|