@tldraw/utils 5.1.0 → 5.2.0-canary.e22474d279fb

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.
@@ -88,6 +88,12 @@ export declare function bind<This extends object, T extends (...args: any[]) =>
88
88
  * function returns a Promise that resolves with the result of the original function. Includes a
89
89
  * cancel method to prevent execution if needed.
90
90
  *
91
+ * Calling `cancel()` while a call is pending rejects the returned promise with an `Error`
92
+ * whose message is `'Debounced function was cancelled'`. Callers that discard the promise
93
+ * are not affected — internal handling suppresses the unhandled-rejection warning — but
94
+ * any code that `await`s or chains `.then()` on the promise must be prepared to handle the
95
+ * rejection.
96
+ *
91
97
  * @param callback - The function to debounce (can be sync or async)
92
98
  * @param wait - The delay in milliseconds before executing the function
93
99
  * @returns A debounced function that returns a Promise and includes a cancel method
@@ -103,15 +109,19 @@ export declare function bind<This extends object, T extends (...args: any[]) =>
103
109
  * debouncedSearch('react hooks') // This cancels the previous call
104
110
  * debouncedSearch('react typescript') // Only this will execute
105
111
  *
106
- * // Cancel pending execution
112
+ * // Cancel pending execution — any in-flight promise rejects
107
113
  * debouncedSearch.cancel()
108
114
  *
109
- * // With async/await
115
+ * // With async/await — wrap in try/catch if you might cancel
110
116
  * const saveData = debounce(async (data: any) => {
111
117
  * return await api.save(data)
112
118
  * }, 1000)
113
119
  *
114
- * const result = await saveData({name: 'John'})
120
+ * try {
121
+ * const result = await saveData({name: 'John'})
122
+ * } catch (err) {
123
+ * // handle cancellation or callback error
124
+ * }
115
125
  * ```
116
126
  *
117
127
  * @public
@@ -1658,7 +1668,7 @@ export declare function rotateArray<T>(arr: T[], offset: number): T[];
1658
1668
  *
1659
1669
  * @public
1660
1670
  */
1661
- export declare const safeParseUrl: (url: string, baseUrl?: string | undefined | URL) => undefined | URL;
1671
+ export declare function safeParseUrl(url: string, baseUrl?: string | URL): undefined | URL;
1662
1672
 
1663
1673
  /* Excluded from this release type: setInLocalStorage */
1664
1674
 
package/dist-cjs/index.js CHANGED
@@ -171,7 +171,7 @@ var import_version2 = require("./lib/version");
171
171
  var import_warn = require("./lib/warn");
172
172
  (0, import_version.registerTldrawLibraryVersion)(
173
173
  "@tldraw/utils",
174
- "5.1.0",
174
+ "5.2.0-canary.e22474d279fb",
175
175
  "cjs"
176
176
  );
177
177
  //# sourceMappingURL=index.js.map
@@ -21,6 +21,7 @@ __export(debounce_exports, {
21
21
  debounce: () => debounce
22
22
  });
23
23
  module.exports = __toCommonJS(debounce_exports);
24
+ var import_function = require("./function");
24
25
  function debounce(callback, wait) {
25
26
  let state = void 0;
26
27
  const fn = (...args) => {
@@ -47,7 +48,10 @@ function debounce(callback, wait) {
47
48
  fn.cancel = () => {
48
49
  if (!state) return;
49
50
  clearTimeout(state.timeout);
51
+ const s = state;
50
52
  state = void 0;
53
+ s.promise.catch(import_function.noop);
54
+ s.reject(new Error("Debounced function was cancelled"));
51
55
  };
52
56
  return fn;
53
57
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/debounce.ts"],
4
- "sourcesContent": ["import type { Awaitable } from './types'\n\n/**\n * Create a debounced version of a function that delays execution until after a specified wait time.\n *\n * Debouncing ensures that a function is only executed once after a specified delay,\n * even if called multiple times in rapid succession. Each new call resets the timer. The debounced\n * function returns a Promise that resolves with the result of the original function. Includes a\n * cancel method to prevent execution if needed.\n *\n * @param callback - The function to debounce (can be sync or async)\n * @param wait - The delay in milliseconds before executing the function\n * @returns A debounced function that returns a Promise and includes a cancel method\n *\n * @example\n * ```ts\n * // Debounce a search function\n * const searchAPI = (query: string) => fetch(`/search?q=${query}`)\n * const debouncedSearch = debounce(searchAPI, 300)\n *\n * // Multiple rapid calls will only execute the last one after 300ms\n * debouncedSearch('react').then(result => console.log(result))\n * debouncedSearch('react hooks') // This cancels the previous call\n * debouncedSearch('react typescript') // Only this will execute\n *\n * // Cancel pending execution\n * debouncedSearch.cancel()\n *\n * // With async/await\n * const saveData = debounce(async (data: any) => {\n * return await api.save(data)\n * }, 1000)\n *\n * const result = await saveData({name: 'John'})\n * ```\n *\n * @public\n * @see source - https://gist.github.com/ca0v/73a31f57b397606c9813472f7493a940\n */\nexport function debounce<T extends unknown[], U>(\n\tcallback: (...args: T) => Awaitable<U>,\n\twait: number\n) {\n\tlet state:\n\t\t| undefined\n\t\t| {\n\t\t\t\t// eslint-disable-next-line no-restricted-globals\n\t\t\t\ttimeout: ReturnType<typeof setTimeout>\n\t\t\t\tpromise: Promise<U>\n\t\t\t\tresolve(value: U | PromiseLike<U>): void\n\t\t\t\treject(value: any): void\n\t\t\t\tlatestArgs: T\n\t\t } = undefined\n\n\tconst fn = (...args: T): Promise<U> => {\n\t\tif (!state) {\n\t\t\tstate = {} as any\n\t\t\tstate!.promise = new Promise((resolve, reject) => {\n\t\t\t\tstate!.resolve = resolve\n\t\t\t\tstate!.reject = reject\n\t\t\t})\n\t\t}\n\t\tclearTimeout(state!.timeout)\n\t\tstate!.latestArgs = args\n\t\t// It's up to the consumer of debounce to call `cancel`\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tstate!.timeout = setTimeout(() => {\n\t\t\tconst s = state!\n\t\t\tstate = undefined\n\t\t\ttry {\n\t\t\t\ts.resolve(callback(...s.latestArgs))\n\t\t\t} catch (e) {\n\t\t\t\ts.reject(e)\n\t\t\t}\n\t\t}, wait)\n\n\t\treturn state!.promise\n\t}\n\tfn.cancel = () => {\n\t\tif (!state) return\n\t\tclearTimeout(state.timeout)\n\t\tstate = undefined\n\t}\n\treturn fn\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAuCO,SAAS,SACf,UACA,MACC;AACD,MAAI,QASG;AAEP,QAAM,KAAK,IAAI,SAAwB;AACtC,QAAI,CAAC,OAAO;AACX,cAAQ,CAAC;AACT,YAAO,UAAU,IAAI,QAAQ,CAAC,SAAS,WAAW;AACjD,cAAO,UAAU;AACjB,cAAO,SAAS;AAAA,MACjB,CAAC;AAAA,IACF;AACA,iBAAa,MAAO,OAAO;AAC3B,UAAO,aAAa;AAGpB,UAAO,UAAU,WAAW,MAAM;AACjC,YAAM,IAAI;AACV,cAAQ;AACR,UAAI;AACH,UAAE,QAAQ,SAAS,GAAG,EAAE,UAAU,CAAC;AAAA,MACpC,SAAS,GAAG;AACX,UAAE,OAAO,CAAC;AAAA,MACX;AAAA,IACD,GAAG,IAAI;AAEP,WAAO,MAAO;AAAA,EACf;AACA,KAAG,SAAS,MAAM;AACjB,QAAI,CAAC,MAAO;AACZ,iBAAa,MAAM,OAAO;AAC1B,YAAQ;AAAA,EACT;AACA,SAAO;AACR;",
4
+ "sourcesContent": ["import { noop } from './function'\nimport type { Awaitable } from './types'\n\n/**\n * Create a debounced version of a function that delays execution until after a specified wait time.\n *\n * Debouncing ensures that a function is only executed once after a specified delay,\n * even if called multiple times in rapid succession. Each new call resets the timer. The debounced\n * function returns a Promise that resolves with the result of the original function. Includes a\n * cancel method to prevent execution if needed.\n *\n * Calling `cancel()` while a call is pending rejects the returned promise with an `Error`\n * whose message is `'Debounced function was cancelled'`. Callers that discard the promise\n * are not affected \u2014 internal handling suppresses the unhandled-rejection warning \u2014 but\n * any code that `await`s or chains `.then()` on the promise must be prepared to handle the\n * rejection.\n *\n * @param callback - The function to debounce (can be sync or async)\n * @param wait - The delay in milliseconds before executing the function\n * @returns A debounced function that returns a Promise and includes a cancel method\n *\n * @example\n * ```ts\n * // Debounce a search function\n * const searchAPI = (query: string) => fetch(`/search?q=${query}`)\n * const debouncedSearch = debounce(searchAPI, 300)\n *\n * // Multiple rapid calls will only execute the last one after 300ms\n * debouncedSearch('react').then(result => console.log(result))\n * debouncedSearch('react hooks') // This cancels the previous call\n * debouncedSearch('react typescript') // Only this will execute\n *\n * // Cancel pending execution \u2014 any in-flight promise rejects\n * debouncedSearch.cancel()\n *\n * // With async/await \u2014 wrap in try/catch if you might cancel\n * const saveData = debounce(async (data: any) => {\n * return await api.save(data)\n * }, 1000)\n *\n * try {\n * const result = await saveData({name: 'John'})\n * } catch (err) {\n * // handle cancellation or callback error\n * }\n * ```\n *\n * @public\n * @see source - https://gist.github.com/ca0v/73a31f57b397606c9813472f7493a940\n */\nexport function debounce<T extends unknown[], U>(\n\tcallback: (...args: T) => Awaitable<U>,\n\twait: number\n) {\n\tlet state:\n\t\t| undefined\n\t\t| {\n\t\t\t\t// eslint-disable-next-line no-restricted-globals\n\t\t\t\ttimeout: ReturnType<typeof setTimeout>\n\t\t\t\tpromise: Promise<U>\n\t\t\t\tresolve(value: U | PromiseLike<U>): void\n\t\t\t\treject(value: any): void\n\t\t\t\tlatestArgs: T\n\t\t } = undefined\n\n\tconst fn = (...args: T): Promise<U> => {\n\t\tif (!state) {\n\t\t\tstate = {} as any\n\t\t\tstate!.promise = new Promise((resolve, reject) => {\n\t\t\t\tstate!.resolve = resolve\n\t\t\t\tstate!.reject = reject\n\t\t\t})\n\t\t}\n\t\tclearTimeout(state!.timeout)\n\t\tstate!.latestArgs = args\n\t\t// It's up to the consumer of debounce to call `cancel`\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tstate!.timeout = setTimeout(() => {\n\t\t\tconst s = state!\n\t\t\tstate = undefined\n\t\t\ttry {\n\t\t\t\ts.resolve(callback(...s.latestArgs))\n\t\t\t} catch (e) {\n\t\t\t\ts.reject(e)\n\t\t\t}\n\t\t}, wait)\n\n\t\treturn state!.promise\n\t}\n\tfn.cancel = () => {\n\t\tif (!state) return\n\t\tclearTimeout(state.timeout)\n\t\tconst s = state\n\t\tstate = undefined\n\t\t// Attach a no-op handler so callers that discard the promise don't\n\t\t// trigger an unhandled-rejection warning. Consumers that did `.then`,\n\t\t// `.catch`, or `await` on the returned promise still observe the\n\t\t// rejection through their own chain.\n\t\ts.promise.catch(noop)\n\t\ts.reject(new Error('Debounced function was cancelled'))\n\t}\n\treturn fn\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAAqB;AAkDd,SAAS,SACf,UACA,MACC;AACD,MAAI,QASG;AAEP,QAAM,KAAK,IAAI,SAAwB;AACtC,QAAI,CAAC,OAAO;AACX,cAAQ,CAAC;AACT,YAAO,UAAU,IAAI,QAAQ,CAAC,SAAS,WAAW;AACjD,cAAO,UAAU;AACjB,cAAO,SAAS;AAAA,MACjB,CAAC;AAAA,IACF;AACA,iBAAa,MAAO,OAAO;AAC3B,UAAO,aAAa;AAGpB,UAAO,UAAU,WAAW,MAAM;AACjC,YAAM,IAAI;AACV,cAAQ;AACR,UAAI;AACH,UAAE,QAAQ,SAAS,GAAG,EAAE,UAAU,CAAC;AAAA,MACpC,SAAS,GAAG;AACX,UAAE,OAAO,CAAC;AAAA,MACX;AAAA,IACD,GAAG,IAAI;AAEP,WAAO,MAAO;AAAA,EACf;AACA,KAAG,SAAS,MAAM;AACjB,QAAI,CAAC,MAAO;AACZ,iBAAa,MAAM,OAAO;AAC1B,UAAM,IAAI;AACV,YAAQ;AAKR,MAAE,QAAQ,MAAM,oBAAI;AACpB,MAAE,OAAO,IAAI,MAAM,kCAAkC,CAAC;AAAA,EACvD;AACA,SAAO;AACR;",
6
6
  "names": []
7
7
  }
@@ -35,6 +35,6 @@ function omitFromStackTrace(fn) {
35
35
  };
36
36
  return wrappedFn;
37
37
  }
38
- const noop = () => {
39
- };
38
+ function noop() {
39
+ }
40
40
  //# sourceMappingURL=function.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/function.ts"],
4
- "sourcesContent": ["/**\n * When a function is wrapped in `omitFromStackTrace`, if it throws an error the stack trace won't\n * include the function itself or any stack frames above it. Useful for assertion-style function\n * where the error will ideally originate from the call-site rather than within the implementation\n * of the assert fn.\n *\n * Only works in platforms that support `Error.captureStackTrace` (ie v8).\n *\n * @param fn - The function to wrap and exclude from stack traces\n * @returns A wrapped version of the function that omits itself from error stack traces\n * @example\n * ```ts\n * const assertPositive = omitFromStackTrace((value: number) => {\n * if (value <= 0) throw new Error('Value must be positive')\n * return value\n * })\n *\n * assertPositive(-1) // Error stack trace will point to this line, not inside assertPositive\n * ```\n * @internal\n */\nexport function omitFromStackTrace<Args extends Array<unknown>, Return>(\n\tfn: (...args: Args) => Return\n): (...args: Args) => Return {\n\tconst wrappedFn = (...args: Args) => {\n\t\ttry {\n\t\t\treturn fn(...args)\n\t\t} catch (error) {\n\t\t\tif (error instanceof Error && Error.captureStackTrace) {\n\t\t\t\tError.captureStackTrace(error, wrappedFn)\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t}\n\n\treturn wrappedFn\n}\n\n/**\n * Does nothing, but it's really really good at it.\n * @internal\n */\nexport const noop: () => void = () => {}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqBO,SAAS,mBACf,IAC4B;AAC5B,QAAM,YAAY,IAAI,SAAe;AACpC,QAAI;AACH,aAAO,GAAG,GAAG,IAAI;AAAA,IAClB,SAAS,OAAO;AACf,UAAI,iBAAiB,SAAS,MAAM,mBAAmB;AACtD,cAAM,kBAAkB,OAAO,SAAS;AAAA,MACzC;AACA,YAAM;AAAA,IACP;AAAA,EACD;AAEA,SAAO;AACR;AAMO,MAAM,OAAmB,MAAM;AAAC;",
4
+ "sourcesContent": ["/**\n * When a function is wrapped in `omitFromStackTrace`, if it throws an error the stack trace won't\n * include the function itself or any stack frames above it. Useful for assertion-style function\n * where the error will ideally originate from the call-site rather than within the implementation\n * of the assert fn.\n *\n * Only works in platforms that support `Error.captureStackTrace` (ie v8).\n *\n * @param fn - The function to wrap and exclude from stack traces\n * @returns A wrapped version of the function that omits itself from error stack traces\n * @example\n * ```ts\n * const assertPositive = omitFromStackTrace((value: number) => {\n * if (value <= 0) throw new Error('Value must be positive')\n * return value\n * })\n *\n * assertPositive(-1) // Error stack trace will point to this line, not inside assertPositive\n * ```\n * @internal\n */\nexport function omitFromStackTrace<Args extends Array<unknown>, Return>(\n\tfn: (...args: Args) => Return\n): (...args: Args) => Return {\n\tconst wrappedFn = (...args: Args) => {\n\t\ttry {\n\t\t\treturn fn(...args)\n\t\t} catch (error) {\n\t\t\tif (error instanceof Error && Error.captureStackTrace) {\n\t\t\t\tError.captureStackTrace(error, wrappedFn)\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t}\n\n\treturn wrappedFn\n}\n\n/**\n * Does nothing, but it's really really good at it.\n * @internal\n */\nexport function noop(): void {}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqBO,SAAS,mBACf,IAC4B;AAC5B,QAAM,YAAY,IAAI,SAAe;AACpC,QAAI;AACH,aAAO,GAAG,GAAG,IAAI;AAAA,IAClB,SAAS,OAAO;AACf,UAAI,iBAAiB,SAAS,MAAM,mBAAmB;AACtD,cAAM,kBAAkB,OAAO,SAAS;AAAA,MACzC;AACA,YAAM;AAAA,IACP;AAAA,EACD;AAEA,SAAO;AACR;AAMO,SAAS,OAAa;AAAC;",
6
6
  "names": []
7
7
  }
@@ -21,8 +21,8 @@ __export(avif_exports, {
21
21
  isAvifAnimated: () => isAvifAnimated
22
22
  });
23
23
  module.exports = __toCommonJS(avif_exports);
24
- const isAvifAnimated = (buffer) => {
24
+ function isAvifAnimated(buffer) {
25
25
  const view = new Uint8Array(buffer);
26
26
  return view[3] === 44;
27
- };
27
+ }
28
28
  //# sourceMappingURL=avif.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/media/avif.ts"],
4
- "sourcesContent": ["/**\n * Determines whether an ArrayBuffer contains an animated AVIF image.\n *\n * This function performs a simple check by examining the 4th byte of the buffer.\n * AVIF animation is indicated when the byte at index 3 equals 44.\n *\n * @param buffer - The ArrayBuffer containing the AVIF image data to analyze\n * @returns True if the buffer contains an animated AVIF, false otherwise\n *\n * @example\n * ```typescript\n * // Check if an AVIF file is animated\n * const response = await fetch('image.avif')\n * const buffer = await response.arrayBuffer()\n * const isAnimated = isAvifAnimated(buffer)\n * if (isAnimated) {\n * console.log('This AVIF contains animation!')\n * }\n * ```\n *\n * @example\n * ```typescript\n * // Use with file input\n * const fileInput = document.querySelector('input[type=\"file\"]')\n * fileInput.addEventListener('change', async (event) => {\n * const file = event.target.files[0]\n * const buffer = await file.arrayBuffer()\n * const hasAnimation = isAvifAnimated(buffer)\n * console.log(hasAnimation ? 'Animated AVIF' : 'Static AVIF')\n * })\n * ```\n *\n * @public\n */\nexport const isAvifAnimated = (buffer: ArrayBuffer) => {\n\tconst view = new Uint8Array(buffer)\n\treturn view[3] === 44\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAkCO,MAAM,iBAAiB,CAAC,WAAwB;AACtD,QAAM,OAAO,IAAI,WAAW,MAAM;AAClC,SAAO,KAAK,CAAC,MAAM;AACpB;",
4
+ "sourcesContent": ["/**\n * Determines whether an ArrayBuffer contains an animated AVIF image.\n *\n * This function performs a simple check by examining the 4th byte of the buffer.\n * AVIF animation is indicated when the byte at index 3 equals 44.\n *\n * @param buffer - The ArrayBuffer containing the AVIF image data to analyze\n * @returns True if the buffer contains an animated AVIF, false otherwise\n *\n * @example\n * ```typescript\n * // Check if an AVIF file is animated\n * const response = await fetch('image.avif')\n * const buffer = await response.arrayBuffer()\n * const isAnimated = isAvifAnimated(buffer)\n * if (isAnimated) {\n * console.log('This AVIF contains animation!')\n * }\n * ```\n *\n * @example\n * ```typescript\n * // Use with file input\n * const fileInput = document.querySelector('input[type=\"file\"]')\n * fileInput.addEventListener('change', async (event) => {\n * const file = event.target.files[0]\n * const buffer = await file.arrayBuffer()\n * const hasAnimation = isAvifAnimated(buffer)\n * console.log(hasAnimation ? 'Animated AVIF' : 'Static AVIF')\n * })\n * ```\n *\n * @public\n */\nexport function isAvifAnimated(buffer: ArrayBuffer) {\n\tconst view = new Uint8Array(buffer)\n\treturn view[3] === 44\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAkCO,SAAS,eAAe,QAAqB;AACnD,QAAM,OAAO,IAAI,WAAW,MAAM;AAClC,SAAO,KAAK,CAAC,MAAM;AACpB;",
6
6
  "names": []
7
7
  }
@@ -29,9 +29,9 @@ async function fetch(input, init) {
29
29
  ...init
30
30
  });
31
31
  }
32
- const Image = (width, height) => {
32
+ function Image(width, height) {
33
33
  const img = new window.Image(width, height);
34
34
  img.referrerPolicy = "strict-origin-when-cross-origin";
35
35
  return img;
36
- };
36
+ }
37
37
  //# sourceMappingURL=network.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/network.ts"],
4
- "sourcesContent": ["/**\n * Just a wrapper around `window.fetch` that sets the `referrerPolicy` to `strict-origin-when-cross-origin`.\n *\n * @param input - A Request object or string containing the URL to fetch\n * @param init - Optional request initialization options\n * @returns Promise that resolves to the Response object\n * @internal\n */\nexport async function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n\t// eslint-disable-next-line tldraw/no-restricted-properties\n\treturn window.fetch(input, {\n\t\t// We want to make sure that the referrer is not sent to other domains.\n\t\treferrerPolicy: 'strict-origin-when-cross-origin',\n\t\t...init,\n\t})\n}\n\n/**\n * Just a wrapper around `new Image`, and yeah, it's a bit strange that it's in the network.ts file\n * but the main concern here is the referrerPolicy and setting it correctly.\n *\n * @param width - Optional width for the image element\n * @param height - Optional height for the image element\n * @returns HTMLImageElement with referrerPolicy set to 'strict-origin-when-cross-origin'\n * @internal\n */\nexport const Image = (width?: number, height?: number) => {\n\t// eslint-disable-next-line tldraw/no-restricted-properties\n\tconst img = new window.Image(width, height)\n\timg.referrerPolicy = 'strict-origin-when-cross-origin'\n\treturn img\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,eAAsB,MAAM,OAA0B,MAAuC;AAE5F,SAAO,OAAO,MAAM,OAAO;AAAA;AAAA,IAE1B,gBAAgB;AAAA,IAChB,GAAG;AAAA,EACJ,CAAC;AACF;AAWO,MAAM,QAAQ,CAAC,OAAgB,WAAoB;AAEzD,QAAM,MAAM,IAAI,OAAO,MAAM,OAAO,MAAM;AAC1C,MAAI,iBAAiB;AACrB,SAAO;AACR;",
4
+ "sourcesContent": ["/**\n * Just a wrapper around `window.fetch` that sets the `referrerPolicy` to `strict-origin-when-cross-origin`.\n *\n * @param input - A Request object or string containing the URL to fetch\n * @param init - Optional request initialization options\n * @returns Promise that resolves to the Response object\n * @internal\n */\nexport async function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n\t// eslint-disable-next-line tldraw/no-restricted-properties\n\treturn window.fetch(input, {\n\t\t// We want to make sure that the referrer is not sent to other domains.\n\t\treferrerPolicy: 'strict-origin-when-cross-origin',\n\t\t...init,\n\t})\n}\n\n/**\n * Just a wrapper around `new Image`, and yeah, it's a bit strange that it's in the network.ts file\n * but the main concern here is the referrerPolicy and setting it correctly.\n *\n * @param width - Optional width for the image element\n * @param height - Optional height for the image element\n * @returns HTMLImageElement with referrerPolicy set to 'strict-origin-when-cross-origin'\n * @internal\n */\nexport function Image(width?: number, height?: number) {\n\t// eslint-disable-next-line tldraw/no-restricted-properties\n\tconst img = new window.Image(width, height)\n\timg.referrerPolicy = 'strict-origin-when-cross-origin'\n\treturn img\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,eAAsB,MAAM,OAA0B,MAAuC;AAE5F,SAAO,OAAO,MAAM,OAAO;AAAA;AAAA,IAE1B,gBAAgB;AAAA,IAChB,GAAG;AAAA,EACJ,CAAC;AACF;AAWO,SAAS,MAAM,OAAgB,QAAiB;AAEtD,QAAM,MAAM,IAAI,OAAO,MAAM,OAAO,MAAM;AAC1C,MAAI,iBAAiB;AACrB,SAAO;AACR;",
6
6
  "names": []
7
7
  }
@@ -21,11 +21,11 @@ __export(url_exports, {
21
21
  safeParseUrl: () => safeParseUrl
22
22
  });
23
23
  module.exports = __toCommonJS(url_exports);
24
- const safeParseUrl = (url, baseUrl) => {
24
+ function safeParseUrl(url, baseUrl) {
25
25
  try {
26
26
  return new URL(url, baseUrl);
27
27
  } catch {
28
28
  return;
29
29
  }
30
- };
30
+ }
31
31
  //# sourceMappingURL=url.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/url.ts"],
4
- "sourcesContent": ["/**\n * Safely parses a URL string without throwing exceptions on invalid input.\n * Returns a URL object for valid URLs or undefined for invalid ones.\n *\n * @param url - The URL string to parse\n * @param baseUrl - Optional base URL to resolve relative URLs against\n * @returns A URL object if parsing succeeds, undefined if it fails\n *\n * @example\n * ```ts\n * // Valid absolute URL\n * const url1 = safeParseUrl('https://example.com')\n * if (url1) {\n * console.log(`Valid URL: ${url1.href}`) // \"Valid URL: https://example.com/\"\n * }\n *\n * // Invalid URL\n * const url2 = safeParseUrl('not-a-url')\n * console.log(url2) // undefined\n *\n * // Relative URL with base\n * const url3 = safeParseUrl('/path', 'https://example.com')\n * if (url3) {\n * console.log(url3.href) // \"https://example.com/path\"\n * }\n *\n * // Error handling\n * function handleUserUrl(input: string) {\n * const url = safeParseUrl(input)\n * if (url) {\n * return url\n * } else {\n * console.log('Invalid URL provided')\n * return null\n * }\n * }\n * ```\n *\n * @public\n */\nexport const safeParseUrl = (url: string, baseUrl?: string | URL) => {\n\ttry {\n\t\treturn new URL(url, baseUrl)\n\t} catch {\n\t\treturn\n\t}\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAwCO,MAAM,eAAe,CAAC,KAAa,YAA2B;AACpE,MAAI;AACH,WAAO,IAAI,IAAI,KAAK,OAAO;AAAA,EAC5B,QAAQ;AACP;AAAA,EACD;AACD;",
4
+ "sourcesContent": ["/**\n * Safely parses a URL string without throwing exceptions on invalid input.\n * Returns a URL object for valid URLs or undefined for invalid ones.\n *\n * @param url - The URL string to parse\n * @param baseUrl - Optional base URL to resolve relative URLs against\n * @returns A URL object if parsing succeeds, undefined if it fails\n *\n * @example\n * ```ts\n * // Valid absolute URL\n * const url1 = safeParseUrl('https://example.com')\n * if (url1) {\n * console.log(`Valid URL: ${url1.href}`) // \"Valid URL: https://example.com/\"\n * }\n *\n * // Invalid URL\n * const url2 = safeParseUrl('not-a-url')\n * console.log(url2) // undefined\n *\n * // Relative URL with base\n * const url3 = safeParseUrl('/path', 'https://example.com')\n * if (url3) {\n * console.log(url3.href) // \"https://example.com/path\"\n * }\n *\n * // Error handling\n * function handleUserUrl(input: string) {\n * const url = safeParseUrl(input)\n * if (url) {\n * return url\n * } else {\n * console.log('Invalid URL provided')\n * return null\n * }\n * }\n * ```\n *\n * @public\n */\nexport function safeParseUrl(url: string, baseUrl?: string | URL) {\n\ttry {\n\t\treturn new URL(url, baseUrl)\n\t} catch {\n\t\treturn\n\t}\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAwCO,SAAS,aAAa,KAAa,SAAwB;AACjE,MAAI;AACH,WAAO,IAAI,IAAI,KAAK,OAAO;AAAA,EAC5B,QAAQ;AACP;AAAA,EACD;AACD;",
6
6
  "names": []
7
7
  }
@@ -88,6 +88,12 @@ export declare function bind<This extends object, T extends (...args: any[]) =>
88
88
  * function returns a Promise that resolves with the result of the original function. Includes a
89
89
  * cancel method to prevent execution if needed.
90
90
  *
91
+ * Calling `cancel()` while a call is pending rejects the returned promise with an `Error`
92
+ * whose message is `'Debounced function was cancelled'`. Callers that discard the promise
93
+ * are not affected — internal handling suppresses the unhandled-rejection warning — but
94
+ * any code that `await`s or chains `.then()` on the promise must be prepared to handle the
95
+ * rejection.
96
+ *
91
97
  * @param callback - The function to debounce (can be sync or async)
92
98
  * @param wait - The delay in milliseconds before executing the function
93
99
  * @returns A debounced function that returns a Promise and includes a cancel method
@@ -103,15 +109,19 @@ export declare function bind<This extends object, T extends (...args: any[]) =>
103
109
  * debouncedSearch('react hooks') // This cancels the previous call
104
110
  * debouncedSearch('react typescript') // Only this will execute
105
111
  *
106
- * // Cancel pending execution
112
+ * // Cancel pending execution — any in-flight promise rejects
107
113
  * debouncedSearch.cancel()
108
114
  *
109
- * // With async/await
115
+ * // With async/await — wrap in try/catch if you might cancel
110
116
  * const saveData = debounce(async (data: any) => {
111
117
  * return await api.save(data)
112
118
  * }, 1000)
113
119
  *
114
- * const result = await saveData({name: 'John'})
120
+ * try {
121
+ * const result = await saveData({name: 'John'})
122
+ * } catch (err) {
123
+ * // handle cancellation or callback error
124
+ * }
115
125
  * ```
116
126
  *
117
127
  * @public
@@ -1658,7 +1668,7 @@ export declare function rotateArray<T>(arr: T[], offset: number): T[];
1658
1668
  *
1659
1669
  * @public
1660
1670
  */
1661
- export declare const safeParseUrl: (url: string, baseUrl?: string | undefined | URL) => undefined | URL;
1671
+ export declare function safeParseUrl(url: string, baseUrl?: string | URL): undefined | URL;
1662
1672
 
1663
1673
  /* Excluded from this release type: setInLocalStorage */
1664
1674
 
@@ -102,7 +102,7 @@ import { registerTldrawLibraryVersion as registerTldrawLibraryVersion2 } from ".
102
102
  import { warnDeprecatedGetter, warnOnce } from "./lib/warn.mjs";
103
103
  registerTldrawLibraryVersion(
104
104
  "@tldraw/utils",
105
- "5.1.0",
105
+ "5.2.0-canary.e22474d279fb",
106
106
  "esm"
107
107
  );
108
108
  export {
@@ -1,3 +1,4 @@
1
+ import { noop } from "./function.mjs";
1
2
  function debounce(callback, wait) {
2
3
  let state = void 0;
3
4
  const fn = (...args) => {
@@ -24,7 +25,10 @@ function debounce(callback, wait) {
24
25
  fn.cancel = () => {
25
26
  if (!state) return;
26
27
  clearTimeout(state.timeout);
28
+ const s = state;
27
29
  state = void 0;
30
+ s.promise.catch(noop);
31
+ s.reject(new Error("Debounced function was cancelled"));
28
32
  };
29
33
  return fn;
30
34
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/debounce.ts"],
4
- "sourcesContent": ["import type { Awaitable } from './types'\n\n/**\n * Create a debounced version of a function that delays execution until after a specified wait time.\n *\n * Debouncing ensures that a function is only executed once after a specified delay,\n * even if called multiple times in rapid succession. Each new call resets the timer. The debounced\n * function returns a Promise that resolves with the result of the original function. Includes a\n * cancel method to prevent execution if needed.\n *\n * @param callback - The function to debounce (can be sync or async)\n * @param wait - The delay in milliseconds before executing the function\n * @returns A debounced function that returns a Promise and includes a cancel method\n *\n * @example\n * ```ts\n * // Debounce a search function\n * const searchAPI = (query: string) => fetch(`/search?q=${query}`)\n * const debouncedSearch = debounce(searchAPI, 300)\n *\n * // Multiple rapid calls will only execute the last one after 300ms\n * debouncedSearch('react').then(result => console.log(result))\n * debouncedSearch('react hooks') // This cancels the previous call\n * debouncedSearch('react typescript') // Only this will execute\n *\n * // Cancel pending execution\n * debouncedSearch.cancel()\n *\n * // With async/await\n * const saveData = debounce(async (data: any) => {\n * return await api.save(data)\n * }, 1000)\n *\n * const result = await saveData({name: 'John'})\n * ```\n *\n * @public\n * @see source - https://gist.github.com/ca0v/73a31f57b397606c9813472f7493a940\n */\nexport function debounce<T extends unknown[], U>(\n\tcallback: (...args: T) => Awaitable<U>,\n\twait: number\n) {\n\tlet state:\n\t\t| undefined\n\t\t| {\n\t\t\t\t// eslint-disable-next-line no-restricted-globals\n\t\t\t\ttimeout: ReturnType<typeof setTimeout>\n\t\t\t\tpromise: Promise<U>\n\t\t\t\tresolve(value: U | PromiseLike<U>): void\n\t\t\t\treject(value: any): void\n\t\t\t\tlatestArgs: T\n\t\t } = undefined\n\n\tconst fn = (...args: T): Promise<U> => {\n\t\tif (!state) {\n\t\t\tstate = {} as any\n\t\t\tstate!.promise = new Promise((resolve, reject) => {\n\t\t\t\tstate!.resolve = resolve\n\t\t\t\tstate!.reject = reject\n\t\t\t})\n\t\t}\n\t\tclearTimeout(state!.timeout)\n\t\tstate!.latestArgs = args\n\t\t// It's up to the consumer of debounce to call `cancel`\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tstate!.timeout = setTimeout(() => {\n\t\t\tconst s = state!\n\t\t\tstate = undefined\n\t\t\ttry {\n\t\t\t\ts.resolve(callback(...s.latestArgs))\n\t\t\t} catch (e) {\n\t\t\t\ts.reject(e)\n\t\t\t}\n\t\t}, wait)\n\n\t\treturn state!.promise\n\t}\n\tfn.cancel = () => {\n\t\tif (!state) return\n\t\tclearTimeout(state.timeout)\n\t\tstate = undefined\n\t}\n\treturn fn\n}\n"],
5
- "mappings": "AAuCO,SAAS,SACf,UACA,MACC;AACD,MAAI,QASG;AAEP,QAAM,KAAK,IAAI,SAAwB;AACtC,QAAI,CAAC,OAAO;AACX,cAAQ,CAAC;AACT,YAAO,UAAU,IAAI,QAAQ,CAAC,SAAS,WAAW;AACjD,cAAO,UAAU;AACjB,cAAO,SAAS;AAAA,MACjB,CAAC;AAAA,IACF;AACA,iBAAa,MAAO,OAAO;AAC3B,UAAO,aAAa;AAGpB,UAAO,UAAU,WAAW,MAAM;AACjC,YAAM,IAAI;AACV,cAAQ;AACR,UAAI;AACH,UAAE,QAAQ,SAAS,GAAG,EAAE,UAAU,CAAC;AAAA,MACpC,SAAS,GAAG;AACX,UAAE,OAAO,CAAC;AAAA,MACX;AAAA,IACD,GAAG,IAAI;AAEP,WAAO,MAAO;AAAA,EACf;AACA,KAAG,SAAS,MAAM;AACjB,QAAI,CAAC,MAAO;AACZ,iBAAa,MAAM,OAAO;AAC1B,YAAQ;AAAA,EACT;AACA,SAAO;AACR;",
4
+ "sourcesContent": ["import { noop } from './function'\nimport type { Awaitable } from './types'\n\n/**\n * Create a debounced version of a function that delays execution until after a specified wait time.\n *\n * Debouncing ensures that a function is only executed once after a specified delay,\n * even if called multiple times in rapid succession. Each new call resets the timer. The debounced\n * function returns a Promise that resolves with the result of the original function. Includes a\n * cancel method to prevent execution if needed.\n *\n * Calling `cancel()` while a call is pending rejects the returned promise with an `Error`\n * whose message is `'Debounced function was cancelled'`. Callers that discard the promise\n * are not affected \u2014 internal handling suppresses the unhandled-rejection warning \u2014 but\n * any code that `await`s or chains `.then()` on the promise must be prepared to handle the\n * rejection.\n *\n * @param callback - The function to debounce (can be sync or async)\n * @param wait - The delay in milliseconds before executing the function\n * @returns A debounced function that returns a Promise and includes a cancel method\n *\n * @example\n * ```ts\n * // Debounce a search function\n * const searchAPI = (query: string) => fetch(`/search?q=${query}`)\n * const debouncedSearch = debounce(searchAPI, 300)\n *\n * // Multiple rapid calls will only execute the last one after 300ms\n * debouncedSearch('react').then(result => console.log(result))\n * debouncedSearch('react hooks') // This cancels the previous call\n * debouncedSearch('react typescript') // Only this will execute\n *\n * // Cancel pending execution \u2014 any in-flight promise rejects\n * debouncedSearch.cancel()\n *\n * // With async/await \u2014 wrap in try/catch if you might cancel\n * const saveData = debounce(async (data: any) => {\n * return await api.save(data)\n * }, 1000)\n *\n * try {\n * const result = await saveData({name: 'John'})\n * } catch (err) {\n * // handle cancellation or callback error\n * }\n * ```\n *\n * @public\n * @see source - https://gist.github.com/ca0v/73a31f57b397606c9813472f7493a940\n */\nexport function debounce<T extends unknown[], U>(\n\tcallback: (...args: T) => Awaitable<U>,\n\twait: number\n) {\n\tlet state:\n\t\t| undefined\n\t\t| {\n\t\t\t\t// eslint-disable-next-line no-restricted-globals\n\t\t\t\ttimeout: ReturnType<typeof setTimeout>\n\t\t\t\tpromise: Promise<U>\n\t\t\t\tresolve(value: U | PromiseLike<U>): void\n\t\t\t\treject(value: any): void\n\t\t\t\tlatestArgs: T\n\t\t } = undefined\n\n\tconst fn = (...args: T): Promise<U> => {\n\t\tif (!state) {\n\t\t\tstate = {} as any\n\t\t\tstate!.promise = new Promise((resolve, reject) => {\n\t\t\t\tstate!.resolve = resolve\n\t\t\t\tstate!.reject = reject\n\t\t\t})\n\t\t}\n\t\tclearTimeout(state!.timeout)\n\t\tstate!.latestArgs = args\n\t\t// It's up to the consumer of debounce to call `cancel`\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tstate!.timeout = setTimeout(() => {\n\t\t\tconst s = state!\n\t\t\tstate = undefined\n\t\t\ttry {\n\t\t\t\ts.resolve(callback(...s.latestArgs))\n\t\t\t} catch (e) {\n\t\t\t\ts.reject(e)\n\t\t\t}\n\t\t}, wait)\n\n\t\treturn state!.promise\n\t}\n\tfn.cancel = () => {\n\t\tif (!state) return\n\t\tclearTimeout(state.timeout)\n\t\tconst s = state\n\t\tstate = undefined\n\t\t// Attach a no-op handler so callers that discard the promise don't\n\t\t// trigger an unhandled-rejection warning. Consumers that did `.then`,\n\t\t// `.catch`, or `await` on the returned promise still observe the\n\t\t// rejection through their own chain.\n\t\ts.promise.catch(noop)\n\t\ts.reject(new Error('Debounced function was cancelled'))\n\t}\n\treturn fn\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY;AAkDd,SAAS,SACf,UACA,MACC;AACD,MAAI,QASG;AAEP,QAAM,KAAK,IAAI,SAAwB;AACtC,QAAI,CAAC,OAAO;AACX,cAAQ,CAAC;AACT,YAAO,UAAU,IAAI,QAAQ,CAAC,SAAS,WAAW;AACjD,cAAO,UAAU;AACjB,cAAO,SAAS;AAAA,MACjB,CAAC;AAAA,IACF;AACA,iBAAa,MAAO,OAAO;AAC3B,UAAO,aAAa;AAGpB,UAAO,UAAU,WAAW,MAAM;AACjC,YAAM,IAAI;AACV,cAAQ;AACR,UAAI;AACH,UAAE,QAAQ,SAAS,GAAG,EAAE,UAAU,CAAC;AAAA,MACpC,SAAS,GAAG;AACX,UAAE,OAAO,CAAC;AAAA,MACX;AAAA,IACD,GAAG,IAAI;AAEP,WAAO,MAAO;AAAA,EACf;AACA,KAAG,SAAS,MAAM;AACjB,QAAI,CAAC,MAAO;AACZ,iBAAa,MAAM,OAAO;AAC1B,UAAM,IAAI;AACV,YAAQ;AAKR,MAAE,QAAQ,MAAM,IAAI;AACpB,MAAE,OAAO,IAAI,MAAM,kCAAkC,CAAC;AAAA,EACvD;AACA,SAAO;AACR;",
6
6
  "names": []
7
7
  }
@@ -11,8 +11,8 @@ function omitFromStackTrace(fn) {
11
11
  };
12
12
  return wrappedFn;
13
13
  }
14
- const noop = () => {
15
- };
14
+ function noop() {
15
+ }
16
16
  export {
17
17
  noop,
18
18
  omitFromStackTrace
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/function.ts"],
4
- "sourcesContent": ["/**\n * When a function is wrapped in `omitFromStackTrace`, if it throws an error the stack trace won't\n * include the function itself or any stack frames above it. Useful for assertion-style function\n * where the error will ideally originate from the call-site rather than within the implementation\n * of the assert fn.\n *\n * Only works in platforms that support `Error.captureStackTrace` (ie v8).\n *\n * @param fn - The function to wrap and exclude from stack traces\n * @returns A wrapped version of the function that omits itself from error stack traces\n * @example\n * ```ts\n * const assertPositive = omitFromStackTrace((value: number) => {\n * if (value <= 0) throw new Error('Value must be positive')\n * return value\n * })\n *\n * assertPositive(-1) // Error stack trace will point to this line, not inside assertPositive\n * ```\n * @internal\n */\nexport function omitFromStackTrace<Args extends Array<unknown>, Return>(\n\tfn: (...args: Args) => Return\n): (...args: Args) => Return {\n\tconst wrappedFn = (...args: Args) => {\n\t\ttry {\n\t\t\treturn fn(...args)\n\t\t} catch (error) {\n\t\t\tif (error instanceof Error && Error.captureStackTrace) {\n\t\t\t\tError.captureStackTrace(error, wrappedFn)\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t}\n\n\treturn wrappedFn\n}\n\n/**\n * Does nothing, but it's really really good at it.\n * @internal\n */\nexport const noop: () => void = () => {}\n"],
5
- "mappings": "AAqBO,SAAS,mBACf,IAC4B;AAC5B,QAAM,YAAY,IAAI,SAAe;AACpC,QAAI;AACH,aAAO,GAAG,GAAG,IAAI;AAAA,IAClB,SAAS,OAAO;AACf,UAAI,iBAAiB,SAAS,MAAM,mBAAmB;AACtD,cAAM,kBAAkB,OAAO,SAAS;AAAA,MACzC;AACA,YAAM;AAAA,IACP;AAAA,EACD;AAEA,SAAO;AACR;AAMO,MAAM,OAAmB,MAAM;AAAC;",
4
+ "sourcesContent": ["/**\n * When a function is wrapped in `omitFromStackTrace`, if it throws an error the stack trace won't\n * include the function itself or any stack frames above it. Useful for assertion-style function\n * where the error will ideally originate from the call-site rather than within the implementation\n * of the assert fn.\n *\n * Only works in platforms that support `Error.captureStackTrace` (ie v8).\n *\n * @param fn - The function to wrap and exclude from stack traces\n * @returns A wrapped version of the function that omits itself from error stack traces\n * @example\n * ```ts\n * const assertPositive = omitFromStackTrace((value: number) => {\n * if (value <= 0) throw new Error('Value must be positive')\n * return value\n * })\n *\n * assertPositive(-1) // Error stack trace will point to this line, not inside assertPositive\n * ```\n * @internal\n */\nexport function omitFromStackTrace<Args extends Array<unknown>, Return>(\n\tfn: (...args: Args) => Return\n): (...args: Args) => Return {\n\tconst wrappedFn = (...args: Args) => {\n\t\ttry {\n\t\t\treturn fn(...args)\n\t\t} catch (error) {\n\t\t\tif (error instanceof Error && Error.captureStackTrace) {\n\t\t\t\tError.captureStackTrace(error, wrappedFn)\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t}\n\n\treturn wrappedFn\n}\n\n/**\n * Does nothing, but it's really really good at it.\n * @internal\n */\nexport function noop(): void {}\n"],
5
+ "mappings": "AAqBO,SAAS,mBACf,IAC4B;AAC5B,QAAM,YAAY,IAAI,SAAe;AACpC,QAAI;AACH,aAAO,GAAG,GAAG,IAAI;AAAA,IAClB,SAAS,OAAO;AACf,UAAI,iBAAiB,SAAS,MAAM,mBAAmB;AACtD,cAAM,kBAAkB,OAAO,SAAS;AAAA,MACzC;AACA,YAAM;AAAA,IACP;AAAA,EACD;AAEA,SAAO;AACR;AAMO,SAAS,OAAa;AAAC;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
- const isAvifAnimated = (buffer) => {
1
+ function isAvifAnimated(buffer) {
2
2
  const view = new Uint8Array(buffer);
3
3
  return view[3] === 44;
4
- };
4
+ }
5
5
  export {
6
6
  isAvifAnimated
7
7
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/media/avif.ts"],
4
- "sourcesContent": ["/**\n * Determines whether an ArrayBuffer contains an animated AVIF image.\n *\n * This function performs a simple check by examining the 4th byte of the buffer.\n * AVIF animation is indicated when the byte at index 3 equals 44.\n *\n * @param buffer - The ArrayBuffer containing the AVIF image data to analyze\n * @returns True if the buffer contains an animated AVIF, false otherwise\n *\n * @example\n * ```typescript\n * // Check if an AVIF file is animated\n * const response = await fetch('image.avif')\n * const buffer = await response.arrayBuffer()\n * const isAnimated = isAvifAnimated(buffer)\n * if (isAnimated) {\n * console.log('This AVIF contains animation!')\n * }\n * ```\n *\n * @example\n * ```typescript\n * // Use with file input\n * const fileInput = document.querySelector('input[type=\"file\"]')\n * fileInput.addEventListener('change', async (event) => {\n * const file = event.target.files[0]\n * const buffer = await file.arrayBuffer()\n * const hasAnimation = isAvifAnimated(buffer)\n * console.log(hasAnimation ? 'Animated AVIF' : 'Static AVIF')\n * })\n * ```\n *\n * @public\n */\nexport const isAvifAnimated = (buffer: ArrayBuffer) => {\n\tconst view = new Uint8Array(buffer)\n\treturn view[3] === 44\n}\n"],
5
- "mappings": "AAkCO,MAAM,iBAAiB,CAAC,WAAwB;AACtD,QAAM,OAAO,IAAI,WAAW,MAAM;AAClC,SAAO,KAAK,CAAC,MAAM;AACpB;",
4
+ "sourcesContent": ["/**\n * Determines whether an ArrayBuffer contains an animated AVIF image.\n *\n * This function performs a simple check by examining the 4th byte of the buffer.\n * AVIF animation is indicated when the byte at index 3 equals 44.\n *\n * @param buffer - The ArrayBuffer containing the AVIF image data to analyze\n * @returns True if the buffer contains an animated AVIF, false otherwise\n *\n * @example\n * ```typescript\n * // Check if an AVIF file is animated\n * const response = await fetch('image.avif')\n * const buffer = await response.arrayBuffer()\n * const isAnimated = isAvifAnimated(buffer)\n * if (isAnimated) {\n * console.log('This AVIF contains animation!')\n * }\n * ```\n *\n * @example\n * ```typescript\n * // Use with file input\n * const fileInput = document.querySelector('input[type=\"file\"]')\n * fileInput.addEventListener('change', async (event) => {\n * const file = event.target.files[0]\n * const buffer = await file.arrayBuffer()\n * const hasAnimation = isAvifAnimated(buffer)\n * console.log(hasAnimation ? 'Animated AVIF' : 'Static AVIF')\n * })\n * ```\n *\n * @public\n */\nexport function isAvifAnimated(buffer: ArrayBuffer) {\n\tconst view = new Uint8Array(buffer)\n\treturn view[3] === 44\n}\n"],
5
+ "mappings": "AAkCO,SAAS,eAAe,QAAqB;AACnD,QAAM,OAAO,IAAI,WAAW,MAAM;AAClC,SAAO,KAAK,CAAC,MAAM;AACpB;",
6
6
  "names": []
7
7
  }
@@ -5,11 +5,11 @@ async function fetch(input, init) {
5
5
  ...init
6
6
  });
7
7
  }
8
- const Image = (width, height) => {
8
+ function Image(width, height) {
9
9
  const img = new window.Image(width, height);
10
10
  img.referrerPolicy = "strict-origin-when-cross-origin";
11
11
  return img;
12
- };
12
+ }
13
13
  export {
14
14
  Image,
15
15
  fetch
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/network.ts"],
4
- "sourcesContent": ["/**\n * Just a wrapper around `window.fetch` that sets the `referrerPolicy` to `strict-origin-when-cross-origin`.\n *\n * @param input - A Request object or string containing the URL to fetch\n * @param init - Optional request initialization options\n * @returns Promise that resolves to the Response object\n * @internal\n */\nexport async function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n\t// eslint-disable-next-line tldraw/no-restricted-properties\n\treturn window.fetch(input, {\n\t\t// We want to make sure that the referrer is not sent to other domains.\n\t\treferrerPolicy: 'strict-origin-when-cross-origin',\n\t\t...init,\n\t})\n}\n\n/**\n * Just a wrapper around `new Image`, and yeah, it's a bit strange that it's in the network.ts file\n * but the main concern here is the referrerPolicy and setting it correctly.\n *\n * @param width - Optional width for the image element\n * @param height - Optional height for the image element\n * @returns HTMLImageElement with referrerPolicy set to 'strict-origin-when-cross-origin'\n * @internal\n */\nexport const Image = (width?: number, height?: number) => {\n\t// eslint-disable-next-line tldraw/no-restricted-properties\n\tconst img = new window.Image(width, height)\n\timg.referrerPolicy = 'strict-origin-when-cross-origin'\n\treturn img\n}\n"],
5
- "mappings": "AAQA,eAAsB,MAAM,OAA0B,MAAuC;AAE5F,SAAO,OAAO,MAAM,OAAO;AAAA;AAAA,IAE1B,gBAAgB;AAAA,IAChB,GAAG;AAAA,EACJ,CAAC;AACF;AAWO,MAAM,QAAQ,CAAC,OAAgB,WAAoB;AAEzD,QAAM,MAAM,IAAI,OAAO,MAAM,OAAO,MAAM;AAC1C,MAAI,iBAAiB;AACrB,SAAO;AACR;",
4
+ "sourcesContent": ["/**\n * Just a wrapper around `window.fetch` that sets the `referrerPolicy` to `strict-origin-when-cross-origin`.\n *\n * @param input - A Request object or string containing the URL to fetch\n * @param init - Optional request initialization options\n * @returns Promise that resolves to the Response object\n * @internal\n */\nexport async function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n\t// eslint-disable-next-line tldraw/no-restricted-properties\n\treturn window.fetch(input, {\n\t\t// We want to make sure that the referrer is not sent to other domains.\n\t\treferrerPolicy: 'strict-origin-when-cross-origin',\n\t\t...init,\n\t})\n}\n\n/**\n * Just a wrapper around `new Image`, and yeah, it's a bit strange that it's in the network.ts file\n * but the main concern here is the referrerPolicy and setting it correctly.\n *\n * @param width - Optional width for the image element\n * @param height - Optional height for the image element\n * @returns HTMLImageElement with referrerPolicy set to 'strict-origin-when-cross-origin'\n * @internal\n */\nexport function Image(width?: number, height?: number) {\n\t// eslint-disable-next-line tldraw/no-restricted-properties\n\tconst img = new window.Image(width, height)\n\timg.referrerPolicy = 'strict-origin-when-cross-origin'\n\treturn img\n}\n"],
5
+ "mappings": "AAQA,eAAsB,MAAM,OAA0B,MAAuC;AAE5F,SAAO,OAAO,MAAM,OAAO;AAAA;AAAA,IAE1B,gBAAgB;AAAA,IAChB,GAAG;AAAA,EACJ,CAAC;AACF;AAWO,SAAS,MAAM,OAAgB,QAAiB;AAEtD,QAAM,MAAM,IAAI,OAAO,MAAM,OAAO,MAAM;AAC1C,MAAI,iBAAiB;AACrB,SAAO;AACR;",
6
6
  "names": []
7
7
  }
@@ -1,10 +1,10 @@
1
- const safeParseUrl = (url, baseUrl) => {
1
+ function safeParseUrl(url, baseUrl) {
2
2
  try {
3
3
  return new URL(url, baseUrl);
4
4
  } catch {
5
5
  return;
6
6
  }
7
- };
7
+ }
8
8
  export {
9
9
  safeParseUrl
10
10
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/url.ts"],
4
- "sourcesContent": ["/**\n * Safely parses a URL string without throwing exceptions on invalid input.\n * Returns a URL object for valid URLs or undefined for invalid ones.\n *\n * @param url - The URL string to parse\n * @param baseUrl - Optional base URL to resolve relative URLs against\n * @returns A URL object if parsing succeeds, undefined if it fails\n *\n * @example\n * ```ts\n * // Valid absolute URL\n * const url1 = safeParseUrl('https://example.com')\n * if (url1) {\n * console.log(`Valid URL: ${url1.href}`) // \"Valid URL: https://example.com/\"\n * }\n *\n * // Invalid URL\n * const url2 = safeParseUrl('not-a-url')\n * console.log(url2) // undefined\n *\n * // Relative URL with base\n * const url3 = safeParseUrl('/path', 'https://example.com')\n * if (url3) {\n * console.log(url3.href) // \"https://example.com/path\"\n * }\n *\n * // Error handling\n * function handleUserUrl(input: string) {\n * const url = safeParseUrl(input)\n * if (url) {\n * return url\n * } else {\n * console.log('Invalid URL provided')\n * return null\n * }\n * }\n * ```\n *\n * @public\n */\nexport const safeParseUrl = (url: string, baseUrl?: string | URL) => {\n\ttry {\n\t\treturn new URL(url, baseUrl)\n\t} catch {\n\t\treturn\n\t}\n}\n"],
5
- "mappings": "AAwCO,MAAM,eAAe,CAAC,KAAa,YAA2B;AACpE,MAAI;AACH,WAAO,IAAI,IAAI,KAAK,OAAO;AAAA,EAC5B,QAAQ;AACP;AAAA,EACD;AACD;",
4
+ "sourcesContent": ["/**\n * Safely parses a URL string without throwing exceptions on invalid input.\n * Returns a URL object for valid URLs or undefined for invalid ones.\n *\n * @param url - The URL string to parse\n * @param baseUrl - Optional base URL to resolve relative URLs against\n * @returns A URL object if parsing succeeds, undefined if it fails\n *\n * @example\n * ```ts\n * // Valid absolute URL\n * const url1 = safeParseUrl('https://example.com')\n * if (url1) {\n * console.log(`Valid URL: ${url1.href}`) // \"Valid URL: https://example.com/\"\n * }\n *\n * // Invalid URL\n * const url2 = safeParseUrl('not-a-url')\n * console.log(url2) // undefined\n *\n * // Relative URL with base\n * const url3 = safeParseUrl('/path', 'https://example.com')\n * if (url3) {\n * console.log(url3.href) // \"https://example.com/path\"\n * }\n *\n * // Error handling\n * function handleUserUrl(input: string) {\n * const url = safeParseUrl(input)\n * if (url) {\n * return url\n * } else {\n * console.log('Invalid URL provided')\n * return null\n * }\n * }\n * ```\n *\n * @public\n */\nexport function safeParseUrl(url: string, baseUrl?: string | URL) {\n\ttry {\n\t\treturn new URL(url, baseUrl)\n\t} catch {\n\t\treturn\n\t}\n}\n"],
5
+ "mappings": "AAwCO,SAAS,aAAa,KAAa,SAAwB;AACjE,MAAI;AACH,WAAO,IAAI,IAAI,KAAK,OAAO;AAAA,EAC5B,QAAQ;AACP;AAAA,EACD;AACD;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/utils",
3
3
  "description": "tldraw infinite canvas SDK (private utilities).",
4
- "version": "5.1.0",
4
+ "version": "5.2.0-canary.e22474d279fb",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -65,4 +65,64 @@ describe(debounce, () => {
65
65
  expect(fn).toHaveBeenCalledTimes(2)
66
66
  expect(await Promise.all([promiseC, promiseD])).toEqual(['gh', 'gh'])
67
67
  })
68
+
69
+ it('rejects the pending promise when cancelled', async () => {
70
+ const fn = vi.fn()
71
+ const debounced = debounce(fn, 100)
72
+ const promiseA = debounced()
73
+ const promiseB = debounced()
74
+
75
+ debounced.cancel()
76
+ vi.advanceTimersByTime(200)
77
+
78
+ expect(fn).not.toHaveBeenCalled()
79
+ await expect(promiseA).rejects.toThrow('Debounced function was cancelled')
80
+ await expect(promiseB).rejects.toThrow('Debounced function was cancelled')
81
+ })
82
+
83
+ it('starts a fresh promise after cancel', async () => {
84
+ const fn = vi.fn((a, b) => a + b)
85
+ const debounced = debounce(fn, 100)
86
+ const cancelledPromise = debounced('a', 'b')
87
+
88
+ debounced.cancel()
89
+ await expect(cancelledPromise).rejects.toThrow('Debounced function was cancelled')
90
+
91
+ const freshPromise = debounced('c', 'd')
92
+ expect(freshPromise).not.toBe(cancelledPromise)
93
+
94
+ vi.advanceTimersByTime(200)
95
+ expect(fn).toHaveBeenCalledTimes(1)
96
+ expect(fn).toHaveBeenCalledWith('c', 'd')
97
+ expect(await freshPromise).toBe('cd')
98
+ })
99
+
100
+ it('cancel is a no-op when no call is pending', async () => {
101
+ const fn = vi.fn()
102
+ const debounced = debounce(fn, 100)
103
+ expect(() => debounced.cancel()).not.toThrow()
104
+
105
+ const promise = debounced()
106
+ debounced.cancel()
107
+ await expect(promise).rejects.toThrow('Debounced function was cancelled')
108
+
109
+ expect(() => debounced.cancel()).not.toThrow()
110
+ })
111
+
112
+ it('does not emit an unhandledrejection when the promise is discarded on cancel', async () => {
113
+ const handler = vi.fn()
114
+ process.on('unhandledRejection', handler)
115
+ try {
116
+ const fn = vi.fn()
117
+ const debounced = debounce(fn, 100)
118
+ debounced()
119
+ debounced.cancel()
120
+
121
+ await new Promise(process.nextTick)
122
+
123
+ expect(handler).not.toHaveBeenCalled()
124
+ } finally {
125
+ process.off('unhandledRejection', handler)
126
+ }
127
+ })
68
128
  })
@@ -1,3 +1,4 @@
1
+ import { noop } from './function'
1
2
  import type { Awaitable } from './types'
2
3
 
3
4
  /**
@@ -8,6 +9,12 @@ import type { Awaitable } from './types'
8
9
  * function returns a Promise that resolves with the result of the original function. Includes a
9
10
  * cancel method to prevent execution if needed.
10
11
  *
12
+ * Calling `cancel()` while a call is pending rejects the returned promise with an `Error`
13
+ * whose message is `'Debounced function was cancelled'`. Callers that discard the promise
14
+ * are not affected — internal handling suppresses the unhandled-rejection warning — but
15
+ * any code that `await`s or chains `.then()` on the promise must be prepared to handle the
16
+ * rejection.
17
+ *
11
18
  * @param callback - The function to debounce (can be sync or async)
12
19
  * @param wait - The delay in milliseconds before executing the function
13
20
  * @returns A debounced function that returns a Promise and includes a cancel method
@@ -23,15 +30,19 @@ import type { Awaitable } from './types'
23
30
  * debouncedSearch('react hooks') // This cancels the previous call
24
31
  * debouncedSearch('react typescript') // Only this will execute
25
32
  *
26
- * // Cancel pending execution
33
+ * // Cancel pending execution — any in-flight promise rejects
27
34
  * debouncedSearch.cancel()
28
35
  *
29
- * // With async/await
36
+ * // With async/await — wrap in try/catch if you might cancel
30
37
  * const saveData = debounce(async (data: any) => {
31
38
  * return await api.save(data)
32
39
  * }, 1000)
33
40
  *
34
- * const result = await saveData({name: 'John'})
41
+ * try {
42
+ * const result = await saveData({name: 'John'})
43
+ * } catch (err) {
44
+ * // handle cancellation or callback error
45
+ * }
35
46
  * ```
36
47
  *
37
48
  * @public
@@ -79,7 +90,14 @@ export function debounce<T extends unknown[], U>(
79
90
  fn.cancel = () => {
80
91
  if (!state) return
81
92
  clearTimeout(state.timeout)
93
+ const s = state
82
94
  state = undefined
95
+ // Attach a no-op handler so callers that discard the promise don't
96
+ // trigger an unhandled-rejection warning. Consumers that did `.then`,
97
+ // `.catch`, or `await` on the returned promise still observe the
98
+ // rejection through their own chain.
99
+ s.promise.catch(noop)
100
+ s.reject(new Error('Debounced function was cancelled'))
83
101
  }
84
102
  return fn
85
103
  }
@@ -40,4 +40,4 @@ export function omitFromStackTrace<Args extends Array<unknown>, Return>(
40
40
  * Does nothing, but it's really really good at it.
41
41
  * @internal
42
42
  */
43
- export const noop: () => void = () => {}
43
+ export function noop(): void {}
@@ -32,7 +32,7 @@
32
32
  *
33
33
  * @public
34
34
  */
35
- export const isAvifAnimated = (buffer: ArrayBuffer) => {
35
+ export function isAvifAnimated(buffer: ArrayBuffer) {
36
36
  const view = new Uint8Array(buffer)
37
37
  return view[3] === 44
38
38
  }
@@ -24,7 +24,7 @@ export async function fetch(input: RequestInfo | URL, init?: RequestInit): Promi
24
24
  * @returns HTMLImageElement with referrerPolicy set to 'strict-origin-when-cross-origin'
25
25
  * @internal
26
26
  */
27
- export const Image = (width?: number, height?: number) => {
27
+ export function Image(width?: number, height?: number) {
28
28
  // eslint-disable-next-line tldraw/no-restricted-properties
29
29
  const img = new window.Image(width, height)
30
30
  img.referrerPolicy = 'strict-origin-when-cross-origin'
package/src/lib/url.ts CHANGED
@@ -38,7 +38,7 @@
38
38
  *
39
39
  * @public
40
40
  */
41
- export const safeParseUrl = (url: string, baseUrl?: string | URL) => {
41
+ export function safeParseUrl(url: string, baseUrl?: string | URL) {
42
42
  try {
43
43
  return new URL(url, baseUrl)
44
44
  } catch {