All files / src watch.ts

100% Statements 47/47
100% Branches 16/16
100% Functions 5/5
100% Lines 46/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111                                                        18x             62x 136x 136x   134x 130x 130x     134x     62x   46x 46x 46x   46x     62x 16x 16x 16x 16x 16x 16x 2x 2x 2x       62x 108x 9x 9x 9x     107x 1x     106x 106x   106x   106x 106x   5x 5x 5x   106x 2x       106x 106x     106x     62x   62x   62x    
import { batch, batchFlush, batchStart, batchTasks } from "./batch";
import { type Disposer, type Get, type Readable, type ReadableLike } from "./interface";
import { getReadable, unsubscribe } from "./utils";
 
export interface WatchEffect {
  (get: Get, dispose: Disposer): (() => void) | undefined | void;
}
 
/**
 * Watch a reactive effect and re-run it when its dependencies change.
 *
 * @category SideEffects
 * @param effect - The reactive effect to watch.
 * @returns A disposer function to stop watching the effect.
 *
 * @example
 * ```ts
 * import { watch, writable, reactiveMap } from "./watch";
 *
 * const count$ = writable(0);
 * const map$ = reactiveMap();
 *
 * watch((get) => {
 *   console.log(get(count$));
 *   console.log(get(map$).get("key"));
 * });
 * ```
 */
export const watch = (effect: WatchEffect): Disposer => {
  let running: boolean | undefined;
  let disposed: boolean | undefined;
  let collectedDeps: Set<Readable> | undefined;
 
  let cleanupEffect: Disposer | null | undefined | void;
 
  const get: Get = ($?: ReadableLike): unknown => {
    const readable = getReadable($);
    if (!readable) return $;
 
    if (!collectedDeps?.has(readable)) {
      (collectedDeps ??= new Set()).add(readable);
      readable.onReaction_(subscription);
    }
 
    return readable.get();
  };
 
  const subscription = () => {
    /** c8 ignore else -- @preserve */
    if (!running) {
      unsubscribe(collectedDeps, subscription);
      collectedDeps?.clear();
    }
    batchTasks.add(runner);
  };
 
  const dispose = () => {
    disposed = true;
    batchTasks.delete(runner);
    effect = null as any; // enable gc
    unsubscribe(collectedDeps, subscription);
    collectedDeps = undefined;
    if (cleanupEffect) {
      const cleanup = cleanupEffect;
      cleanupEffect = null;
      batch(cleanup);
    }
  };
 
  const runner = () => {
    if (cleanupEffect) {
      const cleanup = cleanupEffect;
      cleanupEffect = null;
      batch(cleanup);
    }
 
    if (disposed) {
      return;
    }
 
    const isTopRunner = !running;
    running = true;
 
    const isBatchTop = batchStart();
 
    try {
      cleanupEffect = effect(get, dispose);
    } catch (e) {
      unsubscribe(collectedDeps, subscription);
      collectedDeps?.clear();
      throw e;
    } finally {
      if (disposed) {
        dispose();
      }
 
      /** c8 ignore else -- @preserve */
      if (isTopRunner) {
        running = false;
      }
 
      isBatchTop && batchFlush();
    }
  };
  runner.batchTask_ = runner;
 
  runner();
 
  return dispose;
};