/ shared / components / src / utils / makeSafeTick.ts
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  };