All files / core/src broadcast.ts

100% Statements 80/80
100% Branches 31/31
100% Functions 5/5
100% Lines 80/80

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 1441x 1x 1x                   1x   1x                     1x 22x 22x                         1x 779x   779x                       779x 1075x   1075x 15x 15x   1075x 43x   43x 48x   48x 4x 1x 4x 3x 3x   3x 3x 3x 3x 48x 7x 7x 7x 44x 11x 11x 11x 37x 22x 22x 48x 43x 1075x 779x 29x 1x 1x 29x 29x                       779x 1221x 29x 29x 29x   29x 25x 25x 25x 25x 25x 25x 29x   1221x 594x 594x   594x 126x 121x 121x 591x 468x 468x 468x 468x 594x 594x 1221x 779x 779x  
import { BATCH_MUTATION_KEYS } from './constant.js';
import { type BatchMutations, OBSERVER_KEYS } from './enum.js';
import { captureStack } from './exception.js';
import type {
  BatchChange,
  Broadcaster,
  KeyLike,
  Linkable,
  StateChange,
  StateMetadata,
  StateSubscriber,
} from './types.js';
import { closure } from './utils/index.js';
 
const INSPECTOR_SYMBOL = Symbol('state-inspector');
 
/**
 * Sets a global inspector function that will be called once for the next state change event.
 *
 * This function is used internally to capture the next state change event for debugging
 * or inspection purposes. The inspector function is automatically cleared after being
 * called once.
 *
 * @param fn - A function that will be called with the next state change event.
 */
export function setInspector(fn?: (init: Linkable, event: StateChange) => void) {
  closure.set(INSPECTOR_SYMBOL, fn);
}
 
/**
 * Creates a broadcaster object for managing state change notifications.
 *
 * This function initializes a broadcaster with methods to emit events to observers
 * and broadcast state changes to subscribers. The broadcaster handles different
 * types of state changes based on the type of the initial value (array, set, map, or object).
 *
 * @param init - The initial linkable state value.
 * @param meta - Metadata containing subscribers and observers for the state.
 * @returns A broadcaster object with emit and broadcast methods.
 */
export function createBroadcaster<T extends Linkable = Linkable>(init: Linkable, meta: StateMetadata<T>): Broadcaster {
  const { observers, subscribers, exceptionHandlers } = meta;
 
  return {
    /**
     * Emits a state change event to registered observers.
     *
     * This method checks the type of the initial value and notifies observers
     * based on the specific keys they are interested in. Array and collection
     * mutations are handled specially, while other changes are notified based
     * on the event's key path.
     *
     * @param event - The state change event to emit.
     * @param prop - Optional property key that was changed.
     */
    emit(event, prop) {
      const currentInspector = closure.get<(init: Linkable, event: StateChange) => void>(INSPECTOR_SYMBOL);
 
      if (typeof currentInspector === 'function') {
        currentInspector(init, event);
      }
 
      if (observers.size) {
        const currentObservers = Array.from(observers);
 
        for (const observer of currentObservers) {
          const keys = observer.states.get(init) as Set<KeyLike>;
 
          if (BATCH_MUTATION_KEYS.has(event.type as BatchMutations)) {
            if (init instanceof Set) {
              observer.onChange(event);
            } else {
              const { changes = [] } = event as BatchChange;
              const needUpdate = changes.some((change) => keys.has(change));
 
              if (needUpdate) {
                observer.onChange(event);
              }
            }
          } else if (Array.isArray(init)) {
            if (keys.has(OBSERVER_KEYS.ARRAY_MUTATIONS)) {
              observer.onChange(event);
            }
          } else if (init instanceof Set || init instanceof Map) {
            if (keys.has(OBSERVER_KEYS.COLLECTION_MUTATIONS)) {
              observer.onChange(event);
            }
          } else if (keys.has(prop as KeyLike)) {
            observer.onChange(event);
          }
        }
      }
    },
    catch(error, event) {
      for (const handler of exceptionHandlers) {
        handler({ ...event, error, issues: error.issues });
      }
      return true;
    },
    /**
     * Broadcasts a state snapshot and change event to all subscribers.
     *
     * This method delegates to the broadcast function, passing the subscribers
     * set, current snapshot, event, and optional emitter ID to prevent
     * self-notification.
     *
     * @param snapshot - The current state snapshot.
     * @param event - The state change event.
     * @param emitter - Optional identifier of the emitting instance.
     */
    broadcast(snapshot, event, emitter) {
      if (event.error && !exceptionHandlers.size) {
        const handlers = Array.from(subscribers).filter(
          (fn) => (fn as never as { __internal_id__: string }).__internal_id__
        );
 
        if (!handlers.length) {
          captureStack.error.validation(
            `Unhandled schema validation of a state mutation: "${event.type}"`,
            event.error,
            meta.configs.strict
          );
        }
      }
 
      for (const subscriber of subscribers) {
        if (typeof subscriber === 'function') {
          const receiver = (subscriber as never as { __internal_id__: string }).__internal_id__;
 
          if (receiver) {
            if (receiver !== emitter) {
              (subscriber as StateSubscriber<unknown>)(snapshot, event, emitter);
            }
          } else {
            if (!event.error) {
              (subscriber as StateSubscriber<unknown>)(snapshot, event);
            }
          }
        }
      }
    },
  } as Broadcaster;
}