All files / core host.ts

100% Statements 54/54
100% Branches 18/18
100% Functions 14/14
100% Lines 45/45

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                                    6x 2250x 2250x           483x 483x 483x   483x 483x 483x 483x 1775x 1768x 5x 5x   1763x 1763x 1763x 1763x 1767x 1767x 1767x 1766x   1762x   1x 1x   1763x     483x   4861x 4211x   271x 267x 267x 267x 268x 264x 264x                 1290x           1x           8x     486x 481x 481x 481x 481x 481x        
import { createDisposableScope } from './disposables';
import { createEventEmitter } from '../events/emitter';
import type {
  RuntimeController,
  SnapshotListener,
  RuntimeEventListener,
  RuntimeEventSource,
  Unsubscribe,
} from './types';
 
export interface ControllerHost<TSnapshot extends object, TEvents extends object>
  extends RuntimeController<TSnapshot>, RuntimeEventSource<TEvents> {
  readonly resources: ReturnType<typeof createDisposableScope>;
  readonly alive: () => boolean;
  update(next: TSnapshot): boolean;
  emit<TKey extends keyof TEvents>(type: TKey, detail: TEvents[TKey]): boolean;
}
 
const freezeSnapshot = <TSnapshot extends object>(snapshot: TSnapshot): Readonly<TSnapshot> => {
  if (!Object.isFrozen(snapshot)) Object.freeze(snapshot);
  return snapshot;
};
 
export function createControllerHost<TSnapshot extends object, TEvents extends object>(
  initial: TSnapshot,
): ControllerHost<TSnapshot, TEvents> {
  let current = freezeSnapshot(initial);
  let destroyed = false;
  let publishing = false;
  let pending: TSnapshot | undefined;
  const listeners = new Set<SnapshotListener<TSnapshot>>();
  const events = createEventEmitter<TEvents>();
  const resources = createDisposableScope();
  const update = (next: TSnapshot): boolean => {
    if (destroyed || Object.is(current, next)) return false;
    if (publishing) {
      pending = next;
      return true;
    }
    publishing = true;
    try {
      let candidate: TSnapshot | undefined = next;
      while (candidate) {
        pending = undefined;
        current = freezeSnapshot(candidate);
        for (const listener of [...listeners]) listener(current);
        candidate = pending;
      }
      return true;
    } catch (error) {
      pending = undefined;
      throw error;
    } finally {
      publishing = false;
    }
  };
  return {
    resources,
    alive: () => !destroyed,
    getSnapshot: () => current,
    subscribe(listener) {
      if (destroyed) return () => undefined;
      listeners.add(listener);
      let active = true;
      return () => {
        if (!active) return;
        active = false;
        listeners.delete(listener);
      };
    },
    update,
    emit: events.emit,
    on<TKey extends keyof TEvents>(
      type: TKey,
      listener: RuntimeEventListener<TEvents[TKey]>,
    ): Unsubscribe {
      return destroyed ? () => undefined : events.on(type, listener);
    },
    off<TKey extends keyof TEvents>(
      type: TKey,
      listener: RuntimeEventListener<TEvents[TKey]>,
    ): void {
      events.off(type, listener);
    },
    once<TKey extends keyof TEvents>(
      type: TKey,
      listener: RuntimeEventListener<TEvents[TKey]>,
    ): Unsubscribe {
      return destroyed ? () => undefined : events.once(type, listener);
    },
    destroy() {
      if (destroyed) return;
      destroyed = true;
      pending = undefined;
      listeners.clear();
      events.clear();
      resources.dispose();
    },
  };
}