@tldraw/utils 4.6.0-canary.6d34f9a01c8a → 4.6.0-canary.6d6420a1bc9c

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.
@@ -13,6 +13,14 @@ import { default as uniq } from 'lodash.uniq';
13
13
 
14
14
  /* Excluded from this release type: assertExists */
15
15
 
16
+ /**
17
+ * A value that may be returned synchronously or as a `Promise` / `PromiseLike`.
18
+ * Use with `await` or `Promise.resolve(...)` to normalize to a single `Promise`.
19
+ *
20
+ * @public
21
+ */
22
+ export declare type Awaitable<T> = PromiseLike<T> | T;
23
+
16
24
  /**
17
25
  * Decorator that binds a method to its class instance (legacy stage-2 TypeScript decorators).
18
26
  * When applied to a class method, ensures `this` always refers to the class instance,
@@ -109,7 +117,7 @@ export declare function bind<This extends object, T extends (...args: any[]) =>
109
117
  * @public
110
118
  * @see source - https://gist.github.com/ca0v/73a31f57b397606c9813472f7493a940
111
119
  */
112
- export declare function debounce<T extends unknown[], U>(callback: (...args: T) => PromiseLike<U> | U, wait: number): {
120
+ export declare function debounce<T extends unknown[], U>(callback: (...args: T) => Awaitable<U>, wait: number): {
113
121
  (...args: T): Promise<U>;
114
122
  cancel: () => void;
115
123
  };
@@ -878,6 +886,17 @@ export declare function lerp(a: number, b: number, t: number): number;
878
886
  */
879
887
  export declare function lns(str: string): string;
880
888
 
889
+ /** Simple LRU cache backed by a Map's insertion-order iteration. @public */
890
+ export declare class LruCache<K, V> {
891
+ private maxSize;
892
+ private map;
893
+ constructor(maxSize: number);
894
+ get(key: K): undefined | V;
895
+ set(key: K, value: V): void;
896
+ has(key: K): boolean;
897
+ get size(): number;
898
+ }
899
+
881
900
  /**
882
901
  * Automatically makes properties optional if their type includes `undefined`.
883
902
  * This transforms properties like `prop: string | undefined` to `prop?: string | undefined`,
package/dist-cjs/index.js CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  FileHelpers: () => import_file.FileHelpers,
37
37
  FpsScheduler: () => import_throttle.FpsScheduler,
38
38
  Image: () => import_network.Image,
39
+ LruCache: () => import_LruCache.LruCache,
39
40
  MediaHelpers: () => import_media.MediaHelpers,
40
41
  PerformanceTracker: () => import_PerformanceTracker.PerformanceTracker,
41
42
  PngHelpers: () => import_png.PngHelpers,
@@ -149,6 +150,7 @@ var import_function = require("./lib/function");
149
150
  var import_hash = require("./lib/hash");
150
151
  var import_id = require("./lib/id");
151
152
  var import_iterable = require("./lib/iterable");
153
+ var import_LruCache = require("./lib/LruCache");
152
154
  var import_media = require("./lib/media/media");
153
155
  var import_png = require("./lib/media/png");
154
156
  var import_network = require("./lib/network");
@@ -169,7 +171,7 @@ var import_version2 = require("./lib/version");
169
171
  var import_warn = require("./lib/warn");
170
172
  (0, import_version.registerTldrawLibraryVersion)(
171
173
  "@tldraw/utils",
172
- "4.6.0-canary.6d34f9a01c8a",
174
+ "4.6.0-canary.6d6420a1bc9c",
173
175
  "cjs"
174
176
  );
175
177
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import { registerTldrawLibraryVersion } from './lib/version'\n\nexport { default as isEqual } from 'lodash.isequal'\nexport { default as isEqualWith } from 'lodash.isequalwith'\nexport { default as throttle } from 'lodash.throttle'\nexport { default as uniq } from 'lodash.uniq'\nexport {\n\tareArraysShallowEqual,\n\tcompact,\n\tdedupe,\n\tlast,\n\tmaxBy,\n\tmergeArraysAndReplaceDefaults,\n\tminBy,\n\tpartition,\n\trotateArray,\n} from './lib/array'\nexport { bind } from './lib/bind'\nexport { WeakCache } from './lib/cache'\nexport {\n\tassert,\n\tassertExists,\n\texhaustiveSwitchError,\n\tpromiseWithResolve,\n\tResult,\n\tsleep,\n\ttype ErrorResult,\n\ttype OkResult,\n} from './lib/control'\nexport { debounce } from './lib/debounce'\nexport { annotateError, getErrorAnnotations, type ErrorAnnotations } from './lib/error'\nexport { ExecutionQueue } from './lib/ExecutionQueue'\nexport { FileHelpers } from './lib/file'\nexport { noop, omitFromStackTrace } from './lib/function'\nexport { getHashForBuffer, getHashForObject, getHashForString, lns } from './lib/hash'\nexport { mockUniqueId, restoreUniqueId, uniqueId } from './lib/id'\nexport { getFirstFromIterable } from './lib/iterable'\nexport type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'\nexport {\n\tDEFAULT_SUPPORT_VIDEO_TYPES,\n\tDEFAULT_SUPPORTED_IMAGE_TYPES,\n\tDEFAULT_SUPPORTED_MEDIA_TYPE_LIST,\n\tDEFAULT_SUPPORTED_MEDIA_TYPES,\n\tMediaHelpers,\n} from './lib/media/media'\nexport { PngHelpers } from './lib/media/png'\nexport { fetch, Image } from './lib/network'\nexport { invLerp, lerp, modulate, rng } from './lib/number'\nexport {\n\tareObjectsShallowEqual,\n\tfilterEntries,\n\tgetChangedKeys,\n\tgetOwnProperty,\n\tgroupBy,\n\thasOwnProperty,\n\tisEqualAllowingForFloatingPointErrors,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n\tobjectMapEntriesIterable,\n\tobjectMapFromEntries,\n\tobjectMapKeys,\n\tobjectMapValues,\n\tomit,\n} from './lib/object'\nexport { measureAverageDuration, measureCbDuration, measureDuration } from './lib/perf'\nexport { PerformanceTracker } from './lib/PerformanceTracker'\nexport {\n\tgetIndexAbove,\n\tgetIndexBelow,\n\tgetIndexBetween,\n\tgetIndices,\n\tgetIndicesAbove,\n\tgetIndicesBelow,\n\tgetIndicesBetween,\n\tsortByIndex,\n\tsortByMaybeIndex,\n\tvalidateIndexKey,\n\tZERO_INDEX_KEY,\n\ttype IndexKey,\n} from './lib/reordering'\nexport { retry } from './lib/retry'\nexport { sortById } from './lib/sort'\nexport {\n\tclearLocalStorage,\n\tclearSessionStorage,\n\tdeleteFromLocalStorage,\n\tdeleteFromSessionStorage,\n\tgetFromLocalStorage,\n\tgetFromSessionStorage,\n\tsetInLocalStorage,\n\tsetInSessionStorage,\n} from './lib/storage'\nexport { stringEnum } from './lib/stringEnum'\nexport { FpsScheduler, fpsThrottle, throttleToNextFrame } from './lib/throttle'\nexport { Timers } from './lib/timers'\nexport {\n\ttype Expand,\n\ttype MakeUndefinedOptional,\n\ttype RecursivePartial,\n\ttype Required,\n} from './lib/types'\nexport { safeParseUrl } from './lib/url'\nexport {\n\tisDefined,\n\tisNativeStructuredClone,\n\tisNonNull,\n\tisNonNullish,\n\tSTRUCTURED_CLONE_OBJECT_PROTOTYPE,\n\tstructuredClone,\n} from './lib/value'\nexport { registerTldrawLibraryVersion } from './lib/version'\nexport { warnDeprecatedGetter, warnOnce } from './lib/warn'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA6C;AAE7C,oBAAmC;AACnC,IAAAA,iBAAuC;AACvC,IAAAA,iBAAoC;AACpC,IAAAA,iBAAgC;AAChC,mBAUO;AACP,kBAAqB;AACrB,mBAA0B;AAC1B,qBASO;AACP,sBAAyB;AACzB,mBAA0E;AAC1E,4BAA+B;AAC/B,kBAA4B;AAC5B,sBAAyC;AACzC,kBAA0E;AAC1E,gBAAwD;AACxD,sBAAqC;AAErC,mBAMO;AACP,iBAA2B;AAC3B,qBAA6B;AAC7B,oBAA6C;AAC7C,oBAeO;AACP,kBAA2E;AAC3E,gCAAmC;AACnC,wBAaO;AACP,mBAAsB;AACtB,kBAAyB;AACzB,qBASO;AACP,wBAA2B;AAC3B,sBAA+D;AAC/D,oBAAuB;AAOvB,iBAA6B;AAC7B,mBAOO;AACP,IAAAC,kBAA6C;AAC7C,kBAA+C;AAAA,IAE/C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
4
+ "sourcesContent": ["import { registerTldrawLibraryVersion } from './lib/version'\n\nexport { default as isEqual } from 'lodash.isequal'\nexport { default as isEqualWith } from 'lodash.isequalwith'\nexport { default as throttle } from 'lodash.throttle'\nexport { default as uniq } from 'lodash.uniq'\nexport {\n\tareArraysShallowEqual,\n\tcompact,\n\tdedupe,\n\tlast,\n\tmaxBy,\n\tmergeArraysAndReplaceDefaults,\n\tminBy,\n\tpartition,\n\trotateArray,\n} from './lib/array'\nexport { bind } from './lib/bind'\nexport { WeakCache } from './lib/cache'\nexport {\n\tassert,\n\tassertExists,\n\texhaustiveSwitchError,\n\tpromiseWithResolve,\n\tResult,\n\tsleep,\n\ttype ErrorResult,\n\ttype OkResult,\n} from './lib/control'\nexport { debounce } from './lib/debounce'\nexport { annotateError, getErrorAnnotations, type ErrorAnnotations } from './lib/error'\nexport { ExecutionQueue } from './lib/ExecutionQueue'\nexport { FileHelpers } from './lib/file'\nexport { noop, omitFromStackTrace } from './lib/function'\nexport { getHashForBuffer, getHashForObject, getHashForString, lns } from './lib/hash'\nexport { mockUniqueId, restoreUniqueId, uniqueId } from './lib/id'\nexport { getFirstFromIterable } from './lib/iterable'\nexport { LruCache } from './lib/LruCache'\nexport type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'\nexport {\n\tDEFAULT_SUPPORT_VIDEO_TYPES,\n\tDEFAULT_SUPPORTED_IMAGE_TYPES,\n\tDEFAULT_SUPPORTED_MEDIA_TYPE_LIST,\n\tDEFAULT_SUPPORTED_MEDIA_TYPES,\n\tMediaHelpers,\n} from './lib/media/media'\nexport { PngHelpers } from './lib/media/png'\nexport { fetch, Image } from './lib/network'\nexport { invLerp, lerp, modulate, rng } from './lib/number'\nexport {\n\tareObjectsShallowEqual,\n\tfilterEntries,\n\tgetChangedKeys,\n\tgetOwnProperty,\n\tgroupBy,\n\thasOwnProperty,\n\tisEqualAllowingForFloatingPointErrors,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n\tobjectMapEntriesIterable,\n\tobjectMapFromEntries,\n\tobjectMapKeys,\n\tobjectMapValues,\n\tomit,\n} from './lib/object'\nexport { measureAverageDuration, measureCbDuration, measureDuration } from './lib/perf'\nexport { PerformanceTracker } from './lib/PerformanceTracker'\nexport {\n\tgetIndexAbove,\n\tgetIndexBelow,\n\tgetIndexBetween,\n\tgetIndices,\n\tgetIndicesAbove,\n\tgetIndicesBelow,\n\tgetIndicesBetween,\n\tsortByIndex,\n\tsortByMaybeIndex,\n\tvalidateIndexKey,\n\tZERO_INDEX_KEY,\n\ttype IndexKey,\n} from './lib/reordering'\nexport { retry } from './lib/retry'\nexport { sortById } from './lib/sort'\nexport {\n\tclearLocalStorage,\n\tclearSessionStorage,\n\tdeleteFromLocalStorage,\n\tdeleteFromSessionStorage,\n\tgetFromLocalStorage,\n\tgetFromSessionStorage,\n\tsetInLocalStorage,\n\tsetInSessionStorage,\n} from './lib/storage'\nexport { stringEnum } from './lib/stringEnum'\nexport { FpsScheduler, fpsThrottle, throttleToNextFrame } from './lib/throttle'\nexport { Timers } from './lib/timers'\nexport {\n\ttype Awaitable,\n\ttype Expand,\n\ttype MakeUndefinedOptional,\n\ttype RecursivePartial,\n\ttype Required,\n} from './lib/types'\nexport { safeParseUrl } from './lib/url'\nexport {\n\tisDefined,\n\tisNativeStructuredClone,\n\tisNonNull,\n\tisNonNullish,\n\tSTRUCTURED_CLONE_OBJECT_PROTOTYPE,\n\tstructuredClone,\n} from './lib/value'\nexport { registerTldrawLibraryVersion } from './lib/version'\nexport { warnDeprecatedGetter, warnOnce } from './lib/warn'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA6C;AAE7C,oBAAmC;AACnC,IAAAA,iBAAuC;AACvC,IAAAA,iBAAoC;AACpC,IAAAA,iBAAgC;AAChC,mBAUO;AACP,kBAAqB;AACrB,mBAA0B;AAC1B,qBASO;AACP,sBAAyB;AACzB,mBAA0E;AAC1E,4BAA+B;AAC/B,kBAA4B;AAC5B,sBAAyC;AACzC,kBAA0E;AAC1E,gBAAwD;AACxD,sBAAqC;AACrC,sBAAyB;AAEzB,mBAMO;AACP,iBAA2B;AAC3B,qBAA6B;AAC7B,oBAA6C;AAC7C,oBAeO;AACP,kBAA2E;AAC3E,gCAAmC;AACnC,wBAaO;AACP,mBAAsB;AACtB,kBAAyB;AACzB,qBASO;AACP,wBAA2B;AAC3B,sBAA+D;AAC/D,oBAAuB;AAQvB,iBAA6B;AAC7B,mBAOO;AACP,IAAAC,kBAA6C;AAC7C,kBAA+C;AAAA,IAE/C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": ["import_lodash", "import_version"]
7
7
  }
@@ -43,6 +43,7 @@ class ExecutionQueue {
43
43
  constructor(timeout) {
44
44
  this.timeout = timeout;
45
45
  }
46
+ timeout;
46
47
  queue = [];
47
48
  running = false;
48
49
  /**
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/ExecutionQueue.ts"],
4
4
  "sourcesContent": ["import { sleep } from './control'\n\n/**\n * A queue that executes tasks sequentially with optional delay between tasks.\n *\n * ExecutionQueue ensures that tasks are executed one at a time in the order they were added,\n * with an optional timeout delay between each task execution. This is useful for rate limiting,\n * preventing race conditions, or controlling the flow of asynchronous operations.\n *\n * @example\n * ```ts\n * // Create a queue with 100ms delay between tasks\n * const queue = new ExecutionQueue(100)\n *\n * // Add tasks to the queue\n * const result1 = await queue.push(() => fetch('/api/data'))\n * const result2 = await queue.push(async () => {\n * const data = await processData()\n * return data\n * })\n *\n * // Check if queue is empty\n * if (queue.isEmpty()) {\n * console.log('All tasks completed')\n * }\n *\n * // Clean up\n * queue.close()\n * ```\n *\n * @internal\n */\nexport class ExecutionQueue {\n\tprivate queue: (() => Promise<any>)[] = []\n\tprivate running = false\n\n\t/**\n\t * Creates a new ExecutionQueue.\n\t *\n\t * Creates a new execution queue that will process tasks sequentially.\n\t * If a timeout is provided, there will be a delay between each task execution,\n\t * which is useful for rate limiting or controlling execution flow.\n\t *\n\t * timeout - Optional delay in milliseconds between task executions\n\t * @example\n\t * ```ts\n\t * // Create queue without delay\n\t * const fastQueue = new ExecutionQueue()\n\t *\n\t * // Create queue with 500ms delay between tasks\n\t * const slowQueue = new ExecutionQueue(500)\n\t * ```\n\t */\n\tconstructor(private readonly timeout?: number) {}\n\n\t/**\n\t * Checks if the queue is empty and not currently running a task.\n\t *\n\t * Determines whether the execution queue has completed all tasks and is idle.\n\t * Returns true only when there are no pending tasks in the queue AND no task is currently being executed.\n\t *\n\t * @returns True if the queue has no pending tasks and is not currently executing\n\t * @example\n\t * ```ts\n\t * const queue = new ExecutionQueue()\n\t *\n\t * console.log(queue.isEmpty()) // true - queue is empty\n\t *\n\t * queue.push(() => console.log('task'))\n\t * console.log(queue.isEmpty()) // false - task is running/pending\n\t * ```\n\t */\n\tisEmpty() {\n\t\treturn this.queue.length === 0 && !this.running\n\t}\n\n\tprivate async run() {\n\t\tif (this.running) return\n\t\ttry {\n\t\t\tthis.running = true\n\t\t\twhile (this.queue.length) {\n\t\t\t\tconst task = this.queue.shift()!\n\t\t\t\tawait task()\n\t\t\t\tif (this.timeout) {\n\t\t\t\t\tawait sleep(this.timeout)\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\t// this try/finally should not be needed because the tasks don't throw\n\t\t\t// but better safe than sorry\n\t\t\t// console.log('\\n\\n\\nrunning false\\n\\n\\n')\n\t\t\tthis.running = false\n\t\t}\n\t}\n\n\t/**\n\t * Adds a task to the queue and returns a promise that resolves with the task's result.\n\t *\n\t * Enqueues a task for sequential execution. The task will be executed after all\n\t * previously queued tasks have completed. If a timeout was specified in the constructor,\n\t * there will be a delay between this task and the next one.\n\t *\n\t * @param task - The function to execute (can be sync or async)\n\t * @returns Promise that resolves with the task's return value\n\t * @example\n\t * ```ts\n\t * const queue = new ExecutionQueue(100)\n\t *\n\t * // Add async task\n\t * const result = await queue.push(async () => {\n\t * const response = await fetch('/api/data')\n\t * return response.json()\n\t * })\n\t *\n\t * // Add sync task\n\t * const number = await queue.push(() => 42)\n\t * ```\n\t */\n\tasync push<T>(task: () => T): Promise<Awaited<T>> {\n\t\treturn new Promise<Awaited<T>>((resolve, reject) => {\n\t\t\tthis.queue.push(() => Promise.resolve(task()).then(resolve).catch(reject))\n\t\t\tthis.run()\n\t\t})\n\t}\n\n\t/**\n\t * Clears all pending tasks from the queue.\n\t *\n\t * Immediately removes all pending tasks from the queue. Any currently\n\t * running task will complete normally, but no additional tasks will be executed.\n\t * This method does not wait for the current task to finish.\n\t *\n\t * @returns void\n\t * @example\n\t * ```ts\n\t * const queue = new ExecutionQueue()\n\t *\n\t * // Add several tasks\n\t * queue.push(() => console.log('task 1'))\n\t * queue.push(() => console.log('task 2'))\n\t * queue.push(() => console.log('task 3'))\n\t *\n\t * // Clear all pending tasks\n\t * queue.close()\n\t * // Only 'task 1' will execute if it was already running\n\t * ```\n\t */\n\tclose() {\n\t\tthis.queue = []\n\t}\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAsB;AAgCf,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqB3B,YAA6B,SAAkB;AAAlB;AAAA,EAAmB;AAAA,EApBxC,QAAgC,CAAC;AAAA,EACjC,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsClB,UAAU;AACT,WAAO,KAAK,MAAM,WAAW,KAAK,CAAC,KAAK;AAAA,EACzC;AAAA,EAEA,MAAc,MAAM;AACnB,QAAI,KAAK,QAAS;AAClB,QAAI;AACH,WAAK,UAAU;AACf,aAAO,KAAK,MAAM,QAAQ;AACzB,cAAM,OAAO,KAAK,MAAM,MAAM;AAC9B,cAAM,KAAK;AACX,YAAI,KAAK,SAAS;AACjB,oBAAM,sBAAM,KAAK,OAAO;AAAA,QACzB;AAAA,MACD;AAAA,IACD,UAAE;AAID,WAAK,UAAU;AAAA,IAChB;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBA,MAAM,KAAQ,MAAoC;AACjD,WAAO,IAAI,QAAoB,CAAC,SAAS,WAAW;AACnD,WAAK,MAAM,KAAK,MAAM,QAAQ,QAAQ,KAAK,CAAC,EAAE,KAAK,OAAO,EAAE,MAAM,MAAM,CAAC;AACzE,WAAK,IAAI;AAAA,IACV,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwBA,QAAQ;AACP,SAAK,QAAQ,CAAC;AAAA,EACf;AACD;",
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAsB;AAgCf,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqB3B,YAA6B,SAAkB;AAAlB;AAAA,EAAmB;AAAA,EAAnB;AAAA,EApBrB,QAAgC,CAAC;AAAA,EACjC,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsClB,UAAU;AACT,WAAO,KAAK,MAAM,WAAW,KAAK,CAAC,KAAK;AAAA,EACzC;AAAA,EAEA,MAAc,MAAM;AACnB,QAAI,KAAK,QAAS;AAClB,QAAI;AACH,WAAK,UAAU;AACf,aAAO,KAAK,MAAM,QAAQ;AACzB,cAAM,OAAO,KAAK,MAAM,MAAM;AAC9B,cAAM,KAAK;AACX,YAAI,KAAK,SAAS;AACjB,oBAAM,sBAAM,KAAK,OAAO;AAAA,QACzB;AAAA,MACD;AAAA,IACD,UAAE;AAID,WAAK,UAAU;AAAA,IAChB;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBA,MAAM,KAAQ,MAAoC;AACjD,WAAO,IAAI,QAAoB,CAAC,SAAS,WAAW;AACnD,WAAK,MAAM,KAAK,MAAM,QAAQ,QAAQ,KAAK,CAAC,EAAE,KAAK,OAAO,EAAE,MAAM,MAAM,CAAC;AACzE,WAAK,IAAI;AAAA,IACV,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwBA,QAAQ;AACP,SAAK,QAAQ,CAAC;AAAA,EACf;AACD;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var LruCache_exports = {};
20
+ __export(LruCache_exports, {
21
+ LruCache: () => LruCache
22
+ });
23
+ module.exports = __toCommonJS(LruCache_exports);
24
+ class LruCache {
25
+ constructor(maxSize) {
26
+ this.maxSize = maxSize;
27
+ }
28
+ maxSize;
29
+ map = /* @__PURE__ */ new Map();
30
+ get(key) {
31
+ if (!this.map.has(key)) return void 0;
32
+ const value = this.map.get(key);
33
+ this.map.delete(key);
34
+ this.map.set(key, value);
35
+ return value;
36
+ }
37
+ set(key, value) {
38
+ if (this.map.has(key)) this.map.delete(key);
39
+ this.map.set(key, value);
40
+ if (this.map.size > this.maxSize) {
41
+ this.map.delete(this.map.keys().next().value);
42
+ }
43
+ }
44
+ has(key) {
45
+ return this.map.has(key);
46
+ }
47
+ // eslint-disable-next-line tldraw/no-setter-getter
48
+ get size() {
49
+ return this.map.size;
50
+ }
51
+ }
52
+ //# sourceMappingURL=LruCache.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/LruCache.ts"],
4
+ "sourcesContent": ["/** Simple LRU cache backed by a Map's insertion-order iteration. @public */\nexport class LruCache<K, V> {\n\tprivate map = new Map<K, V>()\n\tconstructor(private maxSize: number) {}\n\n\tget(key: K): V | undefined {\n\t\tif (!this.map.has(key)) return undefined\n\t\tconst value = this.map.get(key)!\n\t\t// Move to most-recent position\n\t\tthis.map.delete(key)\n\t\tthis.map.set(key, value)\n\t\treturn value\n\t}\n\n\tset(key: K, value: V): void {\n\t\tif (this.map.has(key)) this.map.delete(key)\n\t\tthis.map.set(key, value)\n\t\tif (this.map.size > this.maxSize) {\n\t\t\t// Evict oldest entry\n\t\t\tthis.map.delete(this.map.keys().next().value!)\n\t\t}\n\t}\n\n\thas(key: K): boolean {\n\t\treturn this.map.has(key)\n\t}\n\n\t// eslint-disable-next-line tldraw/no-setter-getter\n\tget size(): number {\n\t\treturn this.map.size\n\t}\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACO,MAAM,SAAe;AAAA,EAE3B,YAAoB,SAAiB;AAAjB;AAAA,EAAkB;AAAA,EAAlB;AAAA,EADZ,MAAM,oBAAI,IAAU;AAAA,EAG5B,IAAI,KAAuB;AAC1B,QAAI,CAAC,KAAK,IAAI,IAAI,GAAG,EAAG,QAAO;AAC/B,UAAM,QAAQ,KAAK,IAAI,IAAI,GAAG;AAE9B,SAAK,IAAI,OAAO,GAAG;AACnB,SAAK,IAAI,IAAI,KAAK,KAAK;AACvB,WAAO;AAAA,EACR;AAAA,EAEA,IAAI,KAAQ,OAAgB;AAC3B,QAAI,KAAK,IAAI,IAAI,GAAG,EAAG,MAAK,IAAI,OAAO,GAAG;AAC1C,SAAK,IAAI,IAAI,KAAK,KAAK;AACvB,QAAI,KAAK,IAAI,OAAO,KAAK,SAAS;AAEjC,WAAK,IAAI,OAAO,KAAK,IAAI,KAAK,EAAE,KAAK,EAAE,KAAM;AAAA,IAC9C;AAAA,EACD;AAAA,EAEA,IAAI,KAAiB;AACpB,WAAO,KAAK,IAAI,IAAI,GAAG;AAAA,EACxB;AAAA;AAAA,EAGA,IAAI,OAAe;AAClB,WAAO,KAAK,IAAI;AAAA,EACjB;AACD;",
6
+ "names": []
7
+ }
@@ -47,6 +47,7 @@ function debounce(callback, wait) {
47
47
  fn.cancel = () => {
48
48
  if (!state) return;
49
49
  clearTimeout(state.timeout);
50
+ state = void 0;
50
51
  };
51
52
  return fn;
52
53
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/debounce.ts"],
4
- "sourcesContent": ["/**\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) => PromiseLike<U> | 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}\n\treturn fn\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAqCO,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;AAAA,EAC3B;AACA,SAAO;AACR;",
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;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/types.ts"],
4
- "sourcesContent": ["/**\n * Makes all properties in a type and all nested properties optional recursively.\n * This is useful for creating partial update objects where you only want to specify\n * some deeply nested properties while leaving others unchanged.\n *\n * @example\n * ```ts\n * interface User {\n * name: string\n * settings: {\n * theme: string\n * notifications: {\n * email: boolean\n * push: boolean\n * }\n * }\n * }\n *\n * type PartialUser = RecursivePartial<User>\n * // Result: {\n * // name?: string\n * // settings?: {\n * // theme?: string\n * // notifications?: {\n * // email?: boolean\n * // push?: boolean\n * // }\n * // }\n * // }\n *\n * const update: PartialUser = {\n * settings: {\n * notifications: {\n * email: false\n * }\n * }\n * }\n * ```\n *\n * @public\n */\nexport type RecursivePartial<T> = {\n\t[P in keyof T]?: RecursivePartial<T[P]>\n}\n\n/**\n * Expands a type definition to show its full structure in IDE tooltips and error messages.\n * This utility type forces TypeScript to resolve and display the complete type structure\n * instead of showing complex conditional types or intersections as-is.\n *\n * @example\n * ```ts\n * type User = { name: string }\n * type WithId = { id: string }\n * type UserWithId = User & WithId\n *\n * // Without Expand, IDE shows: User & WithId\n * // With Expand, IDE shows: { name: string; id: string }\n * type ExpandedUserWithId = Expand<UserWithId>\n *\n * // Useful for complex intersections\n * type ComplexType = Expand<BaseType & Mixin1 & Mixin2>\n * ```\n *\n * @public\n */\nexport type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never\n\n/**\n * Makes specified keys in a type required while keeping all other properties as-is.\n * This is useful when you need to ensure certain optional properties are provided\n * in specific contexts without affecting the entire type structure.\n *\n * @example\n * ```ts\n * interface Shape {\n * id: string\n * x?: number\n * y?: number\n * visible?: boolean\n * }\n *\n * // Make position properties required\n * type PositionedShape = Required<Shape, 'x' | 'y'>\n * // Result: {\n * // id: string\n * // x: number // now required\n * // y: number // now required\n * // visible?: boolean\n * // }\n *\n * const shape: PositionedShape = {\n * id: 'rect1',\n * x: 10, // must provide\n * y: 20, // must provide\n * // visible is still optional\n * }\n * ```\n *\n * @internal\n */\nexport type Required<T, K extends keyof T> = Expand<Omit<T, K> & { [P in K]-?: T[P] }>\n\n/**\n * Automatically makes properties optional if their type includes `undefined`.\n * This transforms properties like `prop: string | undefined` to `prop?: string | undefined`,\n * making the API more ergonomic by not requiring explicit undefined assignments.\n *\n * @example\n * ```ts\n * interface RawConfig {\n * name: string\n * theme: string | undefined\n * debug: boolean | undefined\n * version: number\n * }\n *\n * type Config = MakeUndefinedOptional<RawConfig>\n * // Result: {\n * // name: string\n * // theme?: string | undefined // now optional\n * // debug?: boolean | undefined // now optional\n * // version: number\n * // }\n *\n * const config: Config = {\n * name: 'MyApp',\n * version: 1\n * // theme and debug can be omitted instead of explicitly set to undefined\n * }\n * ```\n *\n * @public\n */\nexport type MakeUndefinedOptional<T extends object> = Expand<\n\t{\n\t\t[P in { [K in keyof T]: undefined extends T[K] ? never : K }[keyof T]]: T[P]\n\t} & {\n\t\t[P in { [K in keyof T]: undefined extends T[K] ? K : never }[keyof T]]?: T[P]\n\t}\n>\n"],
4
+ "sourcesContent": ["/**\n * Makes all properties in a type and all nested properties optional recursively.\n * This is useful for creating partial update objects where you only want to specify\n * some deeply nested properties while leaving others unchanged.\n *\n * @example\n * ```ts\n * interface User {\n * name: string\n * settings: {\n * theme: string\n * notifications: {\n * email: boolean\n * push: boolean\n * }\n * }\n * }\n *\n * type PartialUser = RecursivePartial<User>\n * // Result: {\n * // name?: string\n * // settings?: {\n * // theme?: string\n * // notifications?: {\n * // email?: boolean\n * // push?: boolean\n * // }\n * // }\n * // }\n *\n * const update: PartialUser = {\n * settings: {\n * notifications: {\n * email: false\n * }\n * }\n * }\n * ```\n *\n * @public\n */\nexport type RecursivePartial<T> = {\n\t[P in keyof T]?: RecursivePartial<T[P]>\n}\n\n/**\n * Expands a type definition to show its full structure in IDE tooltips and error messages.\n * This utility type forces TypeScript to resolve and display the complete type structure\n * instead of showing complex conditional types or intersections as-is.\n *\n * @example\n * ```ts\n * type User = { name: string }\n * type WithId = { id: string }\n * type UserWithId = User & WithId\n *\n * // Without Expand, IDE shows: User & WithId\n * // With Expand, IDE shows: { name: string; id: string }\n * type ExpandedUserWithId = Expand<UserWithId>\n *\n * // Useful for complex intersections\n * type ComplexType = Expand<BaseType & Mixin1 & Mixin2>\n * ```\n *\n * @public\n */\nexport type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never\n\n/**\n * A value that may be returned synchronously or as a `Promise` / `PromiseLike`.\n * Use with `await` or `Promise.resolve(...)` to normalize to a single `Promise`.\n *\n * @public\n */\nexport type Awaitable<T> = T | PromiseLike<T>\n\n/**\n * Makes specified keys in a type required while keeping all other properties as-is.\n * This is useful when you need to ensure certain optional properties are provided\n * in specific contexts without affecting the entire type structure.\n *\n * @example\n * ```ts\n * interface Shape {\n * id: string\n * x?: number\n * y?: number\n * visible?: boolean\n * }\n *\n * // Make position properties required\n * type PositionedShape = Required<Shape, 'x' | 'y'>\n * // Result: {\n * // id: string\n * // x: number // now required\n * // y: number // now required\n * // visible?: boolean\n * // }\n *\n * const shape: PositionedShape = {\n * id: 'rect1',\n * x: 10, // must provide\n * y: 20, // must provide\n * // visible is still optional\n * }\n * ```\n *\n * @internal\n */\nexport type Required<T, K extends keyof T> = Expand<Omit<T, K> & { [P in K]-?: T[P] }>\n\n/**\n * Automatically makes properties optional if their type includes `undefined`.\n * This transforms properties like `prop: string | undefined` to `prop?: string | undefined`,\n * making the API more ergonomic by not requiring explicit undefined assignments.\n *\n * @example\n * ```ts\n * interface RawConfig {\n * name: string\n * theme: string | undefined\n * debug: boolean | undefined\n * version: number\n * }\n *\n * type Config = MakeUndefinedOptional<RawConfig>\n * // Result: {\n * // name: string\n * // theme?: string | undefined // now optional\n * // debug?: boolean | undefined // now optional\n * // version: number\n * // }\n *\n * const config: Config = {\n * name: 'MyApp',\n * version: 1\n * // theme and debug can be omitted instead of explicitly set to undefined\n * }\n * ```\n *\n * @public\n */\nexport type MakeUndefinedOptional<T extends object> = Expand<\n\t{\n\t\t[P in { [K in keyof T]: undefined extends T[K] ? never : K }[keyof T]]: T[P]\n\t} & {\n\t\t[P in { [K in keyof T]: undefined extends T[K] ? K : never }[keyof T]]?: T[P]\n\t}\n>\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
@@ -13,6 +13,14 @@ import { default as uniq } from 'lodash.uniq';
13
13
 
14
14
  /* Excluded from this release type: assertExists */
15
15
 
16
+ /**
17
+ * A value that may be returned synchronously or as a `Promise` / `PromiseLike`.
18
+ * Use with `await` or `Promise.resolve(...)` to normalize to a single `Promise`.
19
+ *
20
+ * @public
21
+ */
22
+ export declare type Awaitable<T> = PromiseLike<T> | T;
23
+
16
24
  /**
17
25
  * Decorator that binds a method to its class instance (legacy stage-2 TypeScript decorators).
18
26
  * When applied to a class method, ensures `this` always refers to the class instance,
@@ -109,7 +117,7 @@ export declare function bind<This extends object, T extends (...args: any[]) =>
109
117
  * @public
110
118
  * @see source - https://gist.github.com/ca0v/73a31f57b397606c9813472f7493a940
111
119
  */
112
- export declare function debounce<T extends unknown[], U>(callback: (...args: T) => PromiseLike<U> | U, wait: number): {
120
+ export declare function debounce<T extends unknown[], U>(callback: (...args: T) => Awaitable<U>, wait: number): {
113
121
  (...args: T): Promise<U>;
114
122
  cancel: () => void;
115
123
  };
@@ -878,6 +886,17 @@ export declare function lerp(a: number, b: number, t: number): number;
878
886
  */
879
887
  export declare function lns(str: string): string;
880
888
 
889
+ /** Simple LRU cache backed by a Map's insertion-order iteration. @public */
890
+ export declare class LruCache<K, V> {
891
+ private maxSize;
892
+ private map;
893
+ constructor(maxSize: number);
894
+ get(key: K): undefined | V;
895
+ set(key: K, value: V): void;
896
+ has(key: K): boolean;
897
+ get size(): number;
898
+ }
899
+
881
900
  /**
882
901
  * Automatically makes properties optional if their type includes `undefined`.
883
902
  * This transforms properties like `prop: string | undefined` to `prop?: string | undefined`,
@@ -32,6 +32,7 @@ import { noop, omitFromStackTrace } from "./lib/function.mjs";
32
32
  import { getHashForBuffer, getHashForObject, getHashForString, lns } from "./lib/hash.mjs";
33
33
  import { mockUniqueId, restoreUniqueId, uniqueId } from "./lib/id.mjs";
34
34
  import { getFirstFromIterable } from "./lib/iterable.mjs";
35
+ import { LruCache } from "./lib/LruCache.mjs";
35
36
  import {
36
37
  DEFAULT_SUPPORT_VIDEO_TYPES,
37
38
  DEFAULT_SUPPORTED_IMAGE_TYPES,
@@ -101,7 +102,7 @@ import { registerTldrawLibraryVersion as registerTldrawLibraryVersion2 } from ".
101
102
  import { warnDeprecatedGetter, warnOnce } from "./lib/warn.mjs";
102
103
  registerTldrawLibraryVersion(
103
104
  "@tldraw/utils",
104
- "4.6.0-canary.6d34f9a01c8a",
105
+ "4.6.0-canary.6d6420a1bc9c",
105
106
  "esm"
106
107
  );
107
108
  export {
@@ -113,6 +114,7 @@ export {
113
114
  FileHelpers,
114
115
  FpsScheduler,
115
116
  Image,
117
+ LruCache,
116
118
  MediaHelpers,
117
119
  PerformanceTracker,
118
120
  PngHelpers,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import { registerTldrawLibraryVersion } from './lib/version'\n\nexport { default as isEqual } from 'lodash.isequal'\nexport { default as isEqualWith } from 'lodash.isequalwith'\nexport { default as throttle } from 'lodash.throttle'\nexport { default as uniq } from 'lodash.uniq'\nexport {\n\tareArraysShallowEqual,\n\tcompact,\n\tdedupe,\n\tlast,\n\tmaxBy,\n\tmergeArraysAndReplaceDefaults,\n\tminBy,\n\tpartition,\n\trotateArray,\n} from './lib/array'\nexport { bind } from './lib/bind'\nexport { WeakCache } from './lib/cache'\nexport {\n\tassert,\n\tassertExists,\n\texhaustiveSwitchError,\n\tpromiseWithResolve,\n\tResult,\n\tsleep,\n\ttype ErrorResult,\n\ttype OkResult,\n} from './lib/control'\nexport { debounce } from './lib/debounce'\nexport { annotateError, getErrorAnnotations, type ErrorAnnotations } from './lib/error'\nexport { ExecutionQueue } from './lib/ExecutionQueue'\nexport { FileHelpers } from './lib/file'\nexport { noop, omitFromStackTrace } from './lib/function'\nexport { getHashForBuffer, getHashForObject, getHashForString, lns } from './lib/hash'\nexport { mockUniqueId, restoreUniqueId, uniqueId } from './lib/id'\nexport { getFirstFromIterable } from './lib/iterable'\nexport type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'\nexport {\n\tDEFAULT_SUPPORT_VIDEO_TYPES,\n\tDEFAULT_SUPPORTED_IMAGE_TYPES,\n\tDEFAULT_SUPPORTED_MEDIA_TYPE_LIST,\n\tDEFAULT_SUPPORTED_MEDIA_TYPES,\n\tMediaHelpers,\n} from './lib/media/media'\nexport { PngHelpers } from './lib/media/png'\nexport { fetch, Image } from './lib/network'\nexport { invLerp, lerp, modulate, rng } from './lib/number'\nexport {\n\tareObjectsShallowEqual,\n\tfilterEntries,\n\tgetChangedKeys,\n\tgetOwnProperty,\n\tgroupBy,\n\thasOwnProperty,\n\tisEqualAllowingForFloatingPointErrors,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n\tobjectMapEntriesIterable,\n\tobjectMapFromEntries,\n\tobjectMapKeys,\n\tobjectMapValues,\n\tomit,\n} from './lib/object'\nexport { measureAverageDuration, measureCbDuration, measureDuration } from './lib/perf'\nexport { PerformanceTracker } from './lib/PerformanceTracker'\nexport {\n\tgetIndexAbove,\n\tgetIndexBelow,\n\tgetIndexBetween,\n\tgetIndices,\n\tgetIndicesAbove,\n\tgetIndicesBelow,\n\tgetIndicesBetween,\n\tsortByIndex,\n\tsortByMaybeIndex,\n\tvalidateIndexKey,\n\tZERO_INDEX_KEY,\n\ttype IndexKey,\n} from './lib/reordering'\nexport { retry } from './lib/retry'\nexport { sortById } from './lib/sort'\nexport {\n\tclearLocalStorage,\n\tclearSessionStorage,\n\tdeleteFromLocalStorage,\n\tdeleteFromSessionStorage,\n\tgetFromLocalStorage,\n\tgetFromSessionStorage,\n\tsetInLocalStorage,\n\tsetInSessionStorage,\n} from './lib/storage'\nexport { stringEnum } from './lib/stringEnum'\nexport { FpsScheduler, fpsThrottle, throttleToNextFrame } from './lib/throttle'\nexport { Timers } from './lib/timers'\nexport {\n\ttype Expand,\n\ttype MakeUndefinedOptional,\n\ttype RecursivePartial,\n\ttype Required,\n} from './lib/types'\nexport { safeParseUrl } from './lib/url'\nexport {\n\tisDefined,\n\tisNativeStructuredClone,\n\tisNonNull,\n\tisNonNullish,\n\tSTRUCTURED_CLONE_OBJECT_PROTOTYPE,\n\tstructuredClone,\n} from './lib/value'\nexport { registerTldrawLibraryVersion } from './lib/version'\nexport { warnDeprecatedGetter, warnOnce } from './lib/warn'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
- "mappings": "AAAA,SAAS,oCAAoC;AAE7C,SAAoB,WAAXA,gBAA0B;AACnC,SAAoB,WAAXA,gBAA8B;AACvC,SAAoB,WAAXA,gBAA2B;AACpC,SAAoB,WAAXA,gBAAuB;AAChC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,YAAY;AACrB,SAAS,iBAAiB;AAC1B;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGM;AACP,SAAS,gBAAgB;AACzB,SAAS,eAAe,2BAAkD;AAC1E,SAAS,sBAAsB;AAC/B,SAAS,mBAAmB;AAC5B,SAAS,MAAM,0BAA0B;AACzC,SAAS,kBAAkB,kBAAkB,kBAAkB,WAAW;AAC1E,SAAS,cAAc,iBAAiB,gBAAgB;AACxD,SAAS,4BAA4B;AAErC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,kBAAkB;AAC3B,SAAS,OAAO,aAAa;AAC7B,SAAS,SAAS,MAAM,UAAU,WAAW;AAC7C;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,wBAAwB,mBAAmB,uBAAuB;AAC3E,SAAS,0BAA0B;AACnC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEM;AACP,SAAS,aAAa;AACtB,SAAS,gBAAgB;AACzB;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,kBAAkB;AAC3B,SAAS,cAAc,aAAa,2BAA2B;AAC/D,SAAS,cAAc;AAOvB,SAAS,oBAAoB;AAC7B;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,gCAAAC,qCAAoC;AAC7C,SAAS,sBAAsB,gBAAgB;AAE/C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
4
+ "sourcesContent": ["import { registerTldrawLibraryVersion } from './lib/version'\n\nexport { default as isEqual } from 'lodash.isequal'\nexport { default as isEqualWith } from 'lodash.isequalwith'\nexport { default as throttle } from 'lodash.throttle'\nexport { default as uniq } from 'lodash.uniq'\nexport {\n\tareArraysShallowEqual,\n\tcompact,\n\tdedupe,\n\tlast,\n\tmaxBy,\n\tmergeArraysAndReplaceDefaults,\n\tminBy,\n\tpartition,\n\trotateArray,\n} from './lib/array'\nexport { bind } from './lib/bind'\nexport { WeakCache } from './lib/cache'\nexport {\n\tassert,\n\tassertExists,\n\texhaustiveSwitchError,\n\tpromiseWithResolve,\n\tResult,\n\tsleep,\n\ttype ErrorResult,\n\ttype OkResult,\n} from './lib/control'\nexport { debounce } from './lib/debounce'\nexport { annotateError, getErrorAnnotations, type ErrorAnnotations } from './lib/error'\nexport { ExecutionQueue } from './lib/ExecutionQueue'\nexport { FileHelpers } from './lib/file'\nexport { noop, omitFromStackTrace } from './lib/function'\nexport { getHashForBuffer, getHashForObject, getHashForString, lns } from './lib/hash'\nexport { mockUniqueId, restoreUniqueId, uniqueId } from './lib/id'\nexport { getFirstFromIterable } from './lib/iterable'\nexport { LruCache } from './lib/LruCache'\nexport type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'\nexport {\n\tDEFAULT_SUPPORT_VIDEO_TYPES,\n\tDEFAULT_SUPPORTED_IMAGE_TYPES,\n\tDEFAULT_SUPPORTED_MEDIA_TYPE_LIST,\n\tDEFAULT_SUPPORTED_MEDIA_TYPES,\n\tMediaHelpers,\n} from './lib/media/media'\nexport { PngHelpers } from './lib/media/png'\nexport { fetch, Image } from './lib/network'\nexport { invLerp, lerp, modulate, rng } from './lib/number'\nexport {\n\tareObjectsShallowEqual,\n\tfilterEntries,\n\tgetChangedKeys,\n\tgetOwnProperty,\n\tgroupBy,\n\thasOwnProperty,\n\tisEqualAllowingForFloatingPointErrors,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n\tobjectMapEntriesIterable,\n\tobjectMapFromEntries,\n\tobjectMapKeys,\n\tobjectMapValues,\n\tomit,\n} from './lib/object'\nexport { measureAverageDuration, measureCbDuration, measureDuration } from './lib/perf'\nexport { PerformanceTracker } from './lib/PerformanceTracker'\nexport {\n\tgetIndexAbove,\n\tgetIndexBelow,\n\tgetIndexBetween,\n\tgetIndices,\n\tgetIndicesAbove,\n\tgetIndicesBelow,\n\tgetIndicesBetween,\n\tsortByIndex,\n\tsortByMaybeIndex,\n\tvalidateIndexKey,\n\tZERO_INDEX_KEY,\n\ttype IndexKey,\n} from './lib/reordering'\nexport { retry } from './lib/retry'\nexport { sortById } from './lib/sort'\nexport {\n\tclearLocalStorage,\n\tclearSessionStorage,\n\tdeleteFromLocalStorage,\n\tdeleteFromSessionStorage,\n\tgetFromLocalStorage,\n\tgetFromSessionStorage,\n\tsetInLocalStorage,\n\tsetInSessionStorage,\n} from './lib/storage'\nexport { stringEnum } from './lib/stringEnum'\nexport { FpsScheduler, fpsThrottle, throttleToNextFrame } from './lib/throttle'\nexport { Timers } from './lib/timers'\nexport {\n\ttype Awaitable,\n\ttype Expand,\n\ttype MakeUndefinedOptional,\n\ttype RecursivePartial,\n\ttype Required,\n} from './lib/types'\nexport { safeParseUrl } from './lib/url'\nexport {\n\tisDefined,\n\tisNativeStructuredClone,\n\tisNonNull,\n\tisNonNullish,\n\tSTRUCTURED_CLONE_OBJECT_PROTOTYPE,\n\tstructuredClone,\n} from './lib/value'\nexport { registerTldrawLibraryVersion } from './lib/version'\nexport { warnDeprecatedGetter, warnOnce } from './lib/warn'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
+ "mappings": "AAAA,SAAS,oCAAoC;AAE7C,SAAoB,WAAXA,gBAA0B;AACnC,SAAoB,WAAXA,gBAA8B;AACvC,SAAoB,WAAXA,gBAA2B;AACpC,SAAoB,WAAXA,gBAAuB;AAChC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,YAAY;AACrB,SAAS,iBAAiB;AAC1B;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGM;AACP,SAAS,gBAAgB;AACzB,SAAS,eAAe,2BAAkD;AAC1E,SAAS,sBAAsB;AAC/B,SAAS,mBAAmB;AAC5B,SAAS,MAAM,0BAA0B;AACzC,SAAS,kBAAkB,kBAAkB,kBAAkB,WAAW;AAC1E,SAAS,cAAc,iBAAiB,gBAAgB;AACxD,SAAS,4BAA4B;AACrC,SAAS,gBAAgB;AAEzB;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,kBAAkB;AAC3B,SAAS,OAAO,aAAa;AAC7B,SAAS,SAAS,MAAM,UAAU,WAAW;AAC7C;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,wBAAwB,mBAAmB,uBAAuB;AAC3E,SAAS,0BAA0B;AACnC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEM;AACP,SAAS,aAAa;AACtB,SAAS,gBAAgB;AACzB;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,kBAAkB;AAC3B,SAAS,cAAc,aAAa,2BAA2B;AAC/D,SAAS,cAAc;AAQvB,SAAS,oBAAoB;AAC7B;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,gCAAAC,qCAAoC;AAC7C,SAAS,sBAAsB,gBAAgB;AAE/C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": ["default", "registerTldrawLibraryVersion"]
7
7
  }
@@ -20,6 +20,7 @@ class ExecutionQueue {
20
20
  constructor(timeout) {
21
21
  this.timeout = timeout;
22
22
  }
23
+ timeout;
23
24
  queue = [];
24
25
  running = false;
25
26
  /**
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/ExecutionQueue.ts"],
4
4
  "sourcesContent": ["import { sleep } from './control'\n\n/**\n * A queue that executes tasks sequentially with optional delay between tasks.\n *\n * ExecutionQueue ensures that tasks are executed one at a time in the order they were added,\n * with an optional timeout delay between each task execution. This is useful for rate limiting,\n * preventing race conditions, or controlling the flow of asynchronous operations.\n *\n * @example\n * ```ts\n * // Create a queue with 100ms delay between tasks\n * const queue = new ExecutionQueue(100)\n *\n * // Add tasks to the queue\n * const result1 = await queue.push(() => fetch('/api/data'))\n * const result2 = await queue.push(async () => {\n * const data = await processData()\n * return data\n * })\n *\n * // Check if queue is empty\n * if (queue.isEmpty()) {\n * console.log('All tasks completed')\n * }\n *\n * // Clean up\n * queue.close()\n * ```\n *\n * @internal\n */\nexport class ExecutionQueue {\n\tprivate queue: (() => Promise<any>)[] = []\n\tprivate running = false\n\n\t/**\n\t * Creates a new ExecutionQueue.\n\t *\n\t * Creates a new execution queue that will process tasks sequentially.\n\t * If a timeout is provided, there will be a delay between each task execution,\n\t * which is useful for rate limiting or controlling execution flow.\n\t *\n\t * timeout - Optional delay in milliseconds between task executions\n\t * @example\n\t * ```ts\n\t * // Create queue without delay\n\t * const fastQueue = new ExecutionQueue()\n\t *\n\t * // Create queue with 500ms delay between tasks\n\t * const slowQueue = new ExecutionQueue(500)\n\t * ```\n\t */\n\tconstructor(private readonly timeout?: number) {}\n\n\t/**\n\t * Checks if the queue is empty and not currently running a task.\n\t *\n\t * Determines whether the execution queue has completed all tasks and is idle.\n\t * Returns true only when there are no pending tasks in the queue AND no task is currently being executed.\n\t *\n\t * @returns True if the queue has no pending tasks and is not currently executing\n\t * @example\n\t * ```ts\n\t * const queue = new ExecutionQueue()\n\t *\n\t * console.log(queue.isEmpty()) // true - queue is empty\n\t *\n\t * queue.push(() => console.log('task'))\n\t * console.log(queue.isEmpty()) // false - task is running/pending\n\t * ```\n\t */\n\tisEmpty() {\n\t\treturn this.queue.length === 0 && !this.running\n\t}\n\n\tprivate async run() {\n\t\tif (this.running) return\n\t\ttry {\n\t\t\tthis.running = true\n\t\t\twhile (this.queue.length) {\n\t\t\t\tconst task = this.queue.shift()!\n\t\t\t\tawait task()\n\t\t\t\tif (this.timeout) {\n\t\t\t\t\tawait sleep(this.timeout)\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\t// this try/finally should not be needed because the tasks don't throw\n\t\t\t// but better safe than sorry\n\t\t\t// console.log('\\n\\n\\nrunning false\\n\\n\\n')\n\t\t\tthis.running = false\n\t\t}\n\t}\n\n\t/**\n\t * Adds a task to the queue and returns a promise that resolves with the task's result.\n\t *\n\t * Enqueues a task for sequential execution. The task will be executed after all\n\t * previously queued tasks have completed. If a timeout was specified in the constructor,\n\t * there will be a delay between this task and the next one.\n\t *\n\t * @param task - The function to execute (can be sync or async)\n\t * @returns Promise that resolves with the task's return value\n\t * @example\n\t * ```ts\n\t * const queue = new ExecutionQueue(100)\n\t *\n\t * // Add async task\n\t * const result = await queue.push(async () => {\n\t * const response = await fetch('/api/data')\n\t * return response.json()\n\t * })\n\t *\n\t * // Add sync task\n\t * const number = await queue.push(() => 42)\n\t * ```\n\t */\n\tasync push<T>(task: () => T): Promise<Awaited<T>> {\n\t\treturn new Promise<Awaited<T>>((resolve, reject) => {\n\t\t\tthis.queue.push(() => Promise.resolve(task()).then(resolve).catch(reject))\n\t\t\tthis.run()\n\t\t})\n\t}\n\n\t/**\n\t * Clears all pending tasks from the queue.\n\t *\n\t * Immediately removes all pending tasks from the queue. Any currently\n\t * running task will complete normally, but no additional tasks will be executed.\n\t * This method does not wait for the current task to finish.\n\t *\n\t * @returns void\n\t * @example\n\t * ```ts\n\t * const queue = new ExecutionQueue()\n\t *\n\t * // Add several tasks\n\t * queue.push(() => console.log('task 1'))\n\t * queue.push(() => console.log('task 2'))\n\t * queue.push(() => console.log('task 3'))\n\t *\n\t * // Clear all pending tasks\n\t * queue.close()\n\t * // Only 'task 1' will execute if it was already running\n\t * ```\n\t */\n\tclose() {\n\t\tthis.queue = []\n\t}\n}\n"],
5
- "mappings": "AAAA,SAAS,aAAa;AAgCf,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqB3B,YAA6B,SAAkB;AAAlB;AAAA,EAAmB;AAAA,EApBxC,QAAgC,CAAC;AAAA,EACjC,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsClB,UAAU;AACT,WAAO,KAAK,MAAM,WAAW,KAAK,CAAC,KAAK;AAAA,EACzC;AAAA,EAEA,MAAc,MAAM;AACnB,QAAI,KAAK,QAAS;AAClB,QAAI;AACH,WAAK,UAAU;AACf,aAAO,KAAK,MAAM,QAAQ;AACzB,cAAM,OAAO,KAAK,MAAM,MAAM;AAC9B,cAAM,KAAK;AACX,YAAI,KAAK,SAAS;AACjB,gBAAM,MAAM,KAAK,OAAO;AAAA,QACzB;AAAA,MACD;AAAA,IACD,UAAE;AAID,WAAK,UAAU;AAAA,IAChB;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBA,MAAM,KAAQ,MAAoC;AACjD,WAAO,IAAI,QAAoB,CAAC,SAAS,WAAW;AACnD,WAAK,MAAM,KAAK,MAAM,QAAQ,QAAQ,KAAK,CAAC,EAAE,KAAK,OAAO,EAAE,MAAM,MAAM,CAAC;AACzE,WAAK,IAAI;AAAA,IACV,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwBA,QAAQ;AACP,SAAK,QAAQ,CAAC;AAAA,EACf;AACD;",
5
+ "mappings": "AAAA,SAAS,aAAa;AAgCf,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqB3B,YAA6B,SAAkB;AAAlB;AAAA,EAAmB;AAAA,EAAnB;AAAA,EApBrB,QAAgC,CAAC;AAAA,EACjC,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsClB,UAAU;AACT,WAAO,KAAK,MAAM,WAAW,KAAK,CAAC,KAAK;AAAA,EACzC;AAAA,EAEA,MAAc,MAAM;AACnB,QAAI,KAAK,QAAS;AAClB,QAAI;AACH,WAAK,UAAU;AACf,aAAO,KAAK,MAAM,QAAQ;AACzB,cAAM,OAAO,KAAK,MAAM,MAAM;AAC9B,cAAM,KAAK;AACX,YAAI,KAAK,SAAS;AACjB,gBAAM,MAAM,KAAK,OAAO;AAAA,QACzB;AAAA,MACD;AAAA,IACD,UAAE;AAID,WAAK,UAAU;AAAA,IAChB;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBA,MAAM,KAAQ,MAAoC;AACjD,WAAO,IAAI,QAAoB,CAAC,SAAS,WAAW;AACnD,WAAK,MAAM,KAAK,MAAM,QAAQ,QAAQ,KAAK,CAAC,EAAE,KAAK,OAAO,EAAE,MAAM,MAAM,CAAC;AACzE,WAAK,IAAI;AAAA,IACV,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwBA,QAAQ;AACP,SAAK,QAAQ,CAAC;AAAA,EACf;AACD;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,32 @@
1
+ class LruCache {
2
+ constructor(maxSize) {
3
+ this.maxSize = maxSize;
4
+ }
5
+ maxSize;
6
+ map = /* @__PURE__ */ new Map();
7
+ get(key) {
8
+ if (!this.map.has(key)) return void 0;
9
+ const value = this.map.get(key);
10
+ this.map.delete(key);
11
+ this.map.set(key, value);
12
+ return value;
13
+ }
14
+ set(key, value) {
15
+ if (this.map.has(key)) this.map.delete(key);
16
+ this.map.set(key, value);
17
+ if (this.map.size > this.maxSize) {
18
+ this.map.delete(this.map.keys().next().value);
19
+ }
20
+ }
21
+ has(key) {
22
+ return this.map.has(key);
23
+ }
24
+ // eslint-disable-next-line tldraw/no-setter-getter
25
+ get size() {
26
+ return this.map.size;
27
+ }
28
+ }
29
+ export {
30
+ LruCache
31
+ };
32
+ //# sourceMappingURL=LruCache.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/LruCache.ts"],
4
+ "sourcesContent": ["/** Simple LRU cache backed by a Map's insertion-order iteration. @public */\nexport class LruCache<K, V> {\n\tprivate map = new Map<K, V>()\n\tconstructor(private maxSize: number) {}\n\n\tget(key: K): V | undefined {\n\t\tif (!this.map.has(key)) return undefined\n\t\tconst value = this.map.get(key)!\n\t\t// Move to most-recent position\n\t\tthis.map.delete(key)\n\t\tthis.map.set(key, value)\n\t\treturn value\n\t}\n\n\tset(key: K, value: V): void {\n\t\tif (this.map.has(key)) this.map.delete(key)\n\t\tthis.map.set(key, value)\n\t\tif (this.map.size > this.maxSize) {\n\t\t\t// Evict oldest entry\n\t\t\tthis.map.delete(this.map.keys().next().value!)\n\t\t}\n\t}\n\n\thas(key: K): boolean {\n\t\treturn this.map.has(key)\n\t}\n\n\t// eslint-disable-next-line tldraw/no-setter-getter\n\tget size(): number {\n\t\treturn this.map.size\n\t}\n}\n"],
5
+ "mappings": "AACO,MAAM,SAAe;AAAA,EAE3B,YAAoB,SAAiB;AAAjB;AAAA,EAAkB;AAAA,EAAlB;AAAA,EADZ,MAAM,oBAAI,IAAU;AAAA,EAG5B,IAAI,KAAuB;AAC1B,QAAI,CAAC,KAAK,IAAI,IAAI,GAAG,EAAG,QAAO;AAC/B,UAAM,QAAQ,KAAK,IAAI,IAAI,GAAG;AAE9B,SAAK,IAAI,OAAO,GAAG;AACnB,SAAK,IAAI,IAAI,KAAK,KAAK;AACvB,WAAO;AAAA,EACR;AAAA,EAEA,IAAI,KAAQ,OAAgB;AAC3B,QAAI,KAAK,IAAI,IAAI,GAAG,EAAG,MAAK,IAAI,OAAO,GAAG;AAC1C,SAAK,IAAI,IAAI,KAAK,KAAK;AACvB,QAAI,KAAK,IAAI,OAAO,KAAK,SAAS;AAEjC,WAAK,IAAI,OAAO,KAAK,IAAI,KAAK,EAAE,KAAK,EAAE,KAAM;AAAA,IAC9C;AAAA,EACD;AAAA,EAEA,IAAI,KAAiB;AACpB,WAAO,KAAK,IAAI,IAAI,GAAG;AAAA,EACxB;AAAA;AAAA,EAGA,IAAI,OAAe;AAClB,WAAO,KAAK,IAAI;AAAA,EACjB;AACD;",
6
+ "names": []
7
+ }
@@ -24,6 +24,7 @@ function debounce(callback, wait) {
24
24
  fn.cancel = () => {
25
25
  if (!state) return;
26
26
  clearTimeout(state.timeout);
27
+ state = void 0;
27
28
  };
28
29
  return fn;
29
30
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/debounce.ts"],
4
- "sourcesContent": ["/**\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) => PromiseLike<U> | 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}\n\treturn fn\n}\n"],
5
- "mappings": "AAqCO,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;AAAA,EAC3B;AACA,SAAO;AACR;",
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;",
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": "4.6.0-canary.6d34f9a01c8a",
4
+ "version": "4.6.0-canary.6d6420a1bc9c",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -40,7 +40,7 @@
40
40
  "prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
41
41
  "postpack": "../../internal/scripts/postpack.sh",
42
42
  "pack-tarball": "yarn pack",
43
- "lint": "cd ../.. && yarn run -T oxlint packages/utils"
43
+ "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
44
44
  },
45
45
  "dependencies": {
46
46
  "jittered-fractional-indexing": "^1.0.0",
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ export { noop, omitFromStackTrace } from './lib/function'
35
35
  export { getHashForBuffer, getHashForObject, getHashForString, lns } from './lib/hash'
36
36
  export { mockUniqueId, restoreUniqueId, uniqueId } from './lib/id'
37
37
  export { getFirstFromIterable } from './lib/iterable'
38
+ export { LruCache } from './lib/LruCache'
38
39
  export type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'
39
40
  export {
40
41
  DEFAULT_SUPPORT_VIDEO_TYPES,
@@ -94,6 +95,7 @@ export { stringEnum } from './lib/stringEnum'
94
95
  export { FpsScheduler, fpsThrottle, throttleToNextFrame } from './lib/throttle'
95
96
  export { Timers } from './lib/timers'
96
97
  export {
98
+ type Awaitable,
97
99
  type Expand,
98
100
  type MakeUndefinedOptional,
99
101
  type RecursivePartial,
@@ -1,5 +1,5 @@
1
- import { ExecutionQueue } from './ExecutionQueue'
2
1
  import { promiseWithResolve, sleep } from './control'
2
+ import { ExecutionQueue } from './ExecutionQueue'
3
3
 
4
4
  const tick = () => Promise.resolve()
5
5
 
@@ -0,0 +1,89 @@
1
+ import { LruCache } from './LruCache'
2
+
3
+ describe('LruCache', () => {
4
+ it('stores and retrieves values', () => {
5
+ const cache = new LruCache<string, number>(3)
6
+ cache.set('a', 1)
7
+ cache.set('b', 2)
8
+ expect(cache.get('a')).toBe(1)
9
+ expect(cache.get('b')).toBe(2)
10
+ expect(cache.get('c')).toBeUndefined()
11
+ })
12
+
13
+ it('reports size', () => {
14
+ const cache = new LruCache<string, number>(5)
15
+ expect(cache.size).toBe(0)
16
+ cache.set('a', 1)
17
+ expect(cache.size).toBe(1)
18
+ cache.set('b', 2)
19
+ expect(cache.size).toBe(2)
20
+ })
21
+
22
+ it('has() checks existence without promoting', () => {
23
+ const cache = new LruCache<string, number>(2)
24
+ cache.set('a', 1)
25
+ cache.set('b', 2)
26
+ expect(cache.has('a')).toBe(true)
27
+ expect(cache.has('z')).toBe(false)
28
+
29
+ // 'a' was not promoted by has(), so adding 'c' should evict 'a'
30
+ cache.set('c', 3)
31
+ expect(cache.has('a')).toBe(false)
32
+ })
33
+
34
+ it('evicts the oldest entry when exceeding capacity', () => {
35
+ const cache = new LruCache<string, number>(2)
36
+ cache.set('a', 1)
37
+ cache.set('b', 2)
38
+ cache.set('c', 3) // should evict 'a'
39
+
40
+ expect(cache.get('a')).toBeUndefined()
41
+ expect(cache.get('b')).toBe(2)
42
+ expect(cache.get('c')).toBe(3)
43
+ expect(cache.size).toBe(2)
44
+ })
45
+
46
+ it('get() promotes entry so it is not evicted next', () => {
47
+ const cache = new LruCache<string, number>(2)
48
+ cache.set('a', 1)
49
+ cache.set('b', 2)
50
+
51
+ // Access 'a' to promote it; now 'b' is oldest
52
+ cache.get('a')
53
+ cache.set('c', 3) // should evict 'b', not 'a'
54
+
55
+ expect(cache.get('b')).toBeUndefined()
56
+ expect(cache.get('a')).toBe(1)
57
+ expect(cache.get('c')).toBe(3)
58
+ })
59
+
60
+ it('set() on existing key updates value and promotes it', () => {
61
+ const cache = new LruCache<string, number>(2)
62
+ cache.set('a', 1)
63
+ cache.set('b', 2)
64
+
65
+ // Update 'a' — promotes it, 'b' becomes oldest
66
+ cache.set('a', 10)
67
+ expect(cache.get('a')).toBe(10)
68
+
69
+ cache.set('c', 3) // should evict 'b'
70
+ expect(cache.get('b')).toBeUndefined()
71
+ expect(cache.get('a')).toBe(10)
72
+ expect(cache.size).toBe(2)
73
+ })
74
+
75
+ it('evicts entries in insertion order across many inserts', () => {
76
+ const cache = new LruCache<number, number>(3)
77
+ for (let i = 0; i < 10; i++) {
78
+ cache.set(i, i * 10)
79
+ }
80
+ // Only the last 3 should remain
81
+ expect(cache.size).toBe(3)
82
+ expect(cache.get(7)).toBe(70)
83
+ expect(cache.get(8)).toBe(80)
84
+ expect(cache.get(9)).toBe(90)
85
+ for (let i = 0; i < 7; i++) {
86
+ expect(cache.has(i)).toBe(false)
87
+ }
88
+ })
89
+ })
@@ -0,0 +1,32 @@
1
+ /** Simple LRU cache backed by a Map's insertion-order iteration. @public */
2
+ export class LruCache<K, V> {
3
+ private map = new Map<K, V>()
4
+ constructor(private maxSize: number) {}
5
+
6
+ get(key: K): V | undefined {
7
+ if (!this.map.has(key)) return undefined
8
+ const value = this.map.get(key)!
9
+ // Move to most-recent position
10
+ this.map.delete(key)
11
+ this.map.set(key, value)
12
+ return value
13
+ }
14
+
15
+ set(key: K, value: V): void {
16
+ if (this.map.has(key)) this.map.delete(key)
17
+ this.map.set(key, value)
18
+ if (this.map.size > this.maxSize) {
19
+ // Evict oldest entry
20
+ this.map.delete(this.map.keys().next().value!)
21
+ }
22
+ }
23
+
24
+ has(key: K): boolean {
25
+ return this.map.has(key)
26
+ }
27
+
28
+ // eslint-disable-next-line tldraw/no-setter-getter
29
+ get size(): number {
30
+ return this.map.size
31
+ }
32
+ }
@@ -1,6 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
- import { PerformanceTracker } from './PerformanceTracker'
3
2
  import { PERFORMANCE_COLORS, PERFORMANCE_PREFIX_COLOR } from './perf'
3
+ import { PerformanceTracker } from './PerformanceTracker'
4
4
 
5
5
  describe('PerformanceTracker', () => {
6
6
  let tracker: PerformanceTracker
@@ -1,3 +1,5 @@
1
+ import type { Awaitable } from './types'
2
+
1
3
  /**
2
4
  * Create a debounced version of a function that delays execution until after a specified wait time.
3
5
  *
@@ -36,7 +38,7 @@
36
38
  * @see source - https://gist.github.com/ca0v/73a31f57b397606c9813472f7493a940
37
39
  */
38
40
  export function debounce<T extends unknown[], U>(
39
- callback: (...args: T) => PromiseLike<U> | U,
41
+ callback: (...args: T) => Awaitable<U>,
40
42
  wait: number
41
43
  ) {
42
44
  let state:
@@ -77,6 +79,7 @@ export function debounce<T extends unknown[], U>(
77
79
  fn.cancel = () => {
78
80
  if (!state) return
79
81
  clearTimeout(state.timeout)
82
+ state = undefined
80
83
  }
81
84
  return fn
82
85
  }
package/src/lib/types.ts CHANGED
@@ -66,6 +66,14 @@ export type RecursivePartial<T> = {
66
66
  */
67
67
  export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
68
68
 
69
+ /**
70
+ * A value that may be returned synchronously or as a `Promise` / `PromiseLike`.
71
+ * Use with `await` or `Promise.resolve(...)` to normalize to a single `Promise`.
72
+ *
73
+ * @public
74
+ */
75
+ export type Awaitable<T> = T | PromiseLike<T>
76
+
69
77
  /**
70
78
  * Makes specified keys in a type required while keeping all other properties as-is.
71
79
  * This is useful when you need to ensure certain optional properties are provided