All files / core/src subscription.ts

100% Statements 123/123
100% Branches 47/47
100% Functions 7/7
100% Lines 123/123

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 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 1701x 1x 1x 1x 1x   1x                       119x 119x 119x 119x 119x 119x   119x 5x 5x 5x 5x 5x 5x 5x   5x 5x 5x 2x 2x 2x 2x 2x 2x 2x   5x   3x 5x   119x 119x 119x 119x   1x 2x 2x   1x 61x 61x   1x 10x 3x 3x 3x 3x   10x 2x 2x 2x 2x   10x 2x 4x 2x 2x   3x 7x 3x 3x   1x 13x 3x 3x 3x 3x   13x 1x 1x 1x 1x   9x 9x   9x 35x   20x   35x 8x   8x 6x 8x 2x 2x 2x 2x 2x 2x 35x 12x 12x   20x 9x   9x 27x   17x   27x 8x   8x 6x 8x 2x 2x 2x 2x 2x 2x 13x 9x 9x   17x 9x   9x 9x 9x 9x   9x   9x 9x   1x             1x  
import { anchor } from './anchor.js';
import { captureStack } from './exception.js';
import { assign } from './helper.js';
import { onCleanup } from './lifecycle.js';
import { CONTROLLER_REGISTRY } from './registry.js';
import type { Linkable, ObjLike, State, StateSubscriber, StateUnsubscribe, SubscribeFn } from './types.js';
import { isFunction } from './utils/index.js';
 
/**
 * Create a new subscription from an existing anchored state.
 * This is a convenience function to subscribe to changes of an already anchored state.
 *
 * @template T The type of the state.
 * @param state - The anchored state object to derive from.
 * @param handler - The subscriber function to call on state changes.
 * @param recursive - Whether to recursively subscribe to child states (Default: follow).
 * @returns A function to unsubscribe from the subscribed state.
 */
function subscribeFn<T extends Linkable>(
  state: State<T>,
  handler: StateSubscriber<T>,
  recursive?: boolean
): StateUnsubscribe {
  const ctrl = CONTROLLER_REGISTRY.get(state);
 
  if (typeof ctrl?.subscribe !== 'function') {
    captureStack.warning.external(
      'Invalid subscription target:',
      'Attempted to subscribe to non-reactive state.',
      'Object is not reactive',
      subscribeFn,
      subscribeFn.pipe
    );
 
    try {
      handler(state, { type: 'init', keys: [] });
    } catch (error) {
      captureStack.error.external(
        'Unable to execute the subscription handler function.',
        error as Error,
        subscribeFn,
        subscribeFn.pipe
      );
    }
 
    return () => {
      // No-op, as there is no subscription to unsubscribe from.
    };
  }
 
  const unsubscribe = ctrl?.subscribe(handler as StateSubscriber<unknown>, undefined, recursive);
  onCleanup(unsubscribe);
  return unsubscribe;
}
 
subscribeFn.log = ((state) => {
  return subscribeFn(state, console.log);
}) satisfies SubscribeFn['log'];
 
subscribeFn.resolve = ((state) => {
  return CONTROLLER_REGISTRY.get(state) as never;
}) satisfies SubscribeFn['resolve'];
 
subscribeFn.pipe = ((source, target, transform) => {
  if (!anchor.has(source)) {
    const error = new Error('State is not reactive.');
    captureStack.violation.derivation('Attempted to pipe state from a non-reactive state.', error);
    return () => {};
  }
 
  if (typeof target !== 'object' || target === null) {
    const error = new Error('Target is not an assignable object.');
    captureStack.violation.derivation('Attempted to pipe state to a non-assignable target.', error);
    return () => {};
  }
 
  if (!isFunction(transform)) {
    return subscribeFn(source, (current) => {
      assign(target as ObjLike, current as ObjLike);
    });
  }
 
  return subscribeFn(source, (current) => {
    assign(target as ObjLike, transform(current));
  });
}) satisfies SubscribeFn['pipe'];
 
subscribeFn.bind = ((left, right, transformLeft, transformRight) => {
  if (!anchor.has(left)) {
    const error = new Error('State is not reactive.');
    captureStack.violation.derivation('Attempted to bind state from a non-reactive state.', error);
    return () => {};
  }
 
  if (!anchor.has(right)) {
    const error = new Error('State is not reactive.');
    captureStack.violation.derivation('Attempted to bind state to a non-reactive state.', error);
    return () => {};
  }
 
  let updatingLeft = false;
  let updatingRight = false;
 
  const unsubscribeLeft = subscribeFn(left, (current) => {
    if (updatingLeft) return;
 
    updatingRight = true;
 
    if (isFunction(transformLeft)) {
      const result = transformLeft(current);
 
      if (result) {
        anchor.assign(right, result);
      } else {
        captureStack.warning.external(
          'Invalid binding transformation:',
          'The transformation function returned an invalid value. Please check your transformation function.',
          'Undefined is not assignable value.'
        );
      }
    } else {
      anchor.assign(right as ObjLike, current as ObjLike);
    }
 
    updatingRight = false;
  });
 
  const unsubscribeRight = subscribeFn(right, (current) => {
    if (updatingRight) return;
 
    updatingLeft = true;
 
    if (isFunction(transformRight)) {
      const result = transformRight(current);
 
      if (result) {
        anchor.assign(left, result);
      } else {
        captureStack.warning.external(
          'Invalid binding transformation:',
          'The transformation function returned an invalid value. Please check your transformation function.',
          'Undefined is not assignable value.'
        );
      }
    } else {
      assign(left as ObjLike, current as ObjLike);
    }
 
    updatingLeft = false;
  });
 
  const unsubscribeAll = () => {
    unsubscribeLeft();
    unsubscribeRight();
  };
 
  onCleanup(unsubscribeAll);
 
  return unsubscribeAll;
}) satisfies SubscribeFn['bind'];
 
export const subscribe: SubscribeFn = subscribeFn as SubscribeFn;
 
/**
 * @deprecated Use `subscribe` instead.
 * The `derive` function is an alias for the `subscribe` function.
 * @type {SubscribeFn}
 */
export const derive: SubscribeFn = subscribeFn as SubscribeFn;