makeSafeTick.ts
1 /* eslint-disable import/prefer-default-export */ 2 // eslint-disable-next-line no-restricted-imports 3 import { tick as svelteTick, onDestroy } from 'svelte'; 4 5 // Unfortantely for TS to recognize that this can be awaited 6 // we need to leave `Promise<void | never>` otherwise TS hints 7 // will suggest removing the await. 8 // See @remarks for reason to disable `then` 9 type TickType = () => Omit<Promise<string>, 'then'> | Promise<void | never>; 10 11 type SafeTickCallback = (tick: TickType) => Promise<void | never>; 12 13 class DestroyedError extends Error { 14 constructor() { 15 super('component was destroyed before tick resolved.'); 16 this.name = 'DestroyedError'; 17 } 18 } 19 20 /** 21 * Provides a safer way to use svelte's tick helper. 22 * 23 * This prevents code that relies on tick() from running 24 * if the component is destroyed while the tick resolution 25 * is inflight. 26 * 27 * @remarks 28 * To avoid floating promises (promises with no return statements) 29 * it is safer to use the `async/await` syntax. 30 * 31 * If this is used with the `.then()` syntax without the promise 32 * being returned the DestroyedError will bubble up to sentry. 33 * 34 * @example 35 * ```ts 36 * const safeTick = makeSafeTick(); 37 * onMount(async() => { 38 * await safeTick(async (tick) => { 39 * // Use tick normally 40 * await tick(); 41 * // ... 42 * }); 43 * }); 44 * ``` 45 */ 46 export const makeSafeTick = (): (( 47 callback: SafeTickCallback, 48 ) => Promise<void | never>) => { 49 let destroyed = false; 50 onDestroy(() => { 51 destroyed = true; 52 }); 53 54 return async (callback) => { 55 try { 56 await callback(async () => { 57 await svelteTick(); 58 if (destroyed) throw new DestroyedError(); 59 }); 60 } catch (e) { 61 if (!(e instanceof DestroyedError)) throw e; 62 } 63 }; 64 };