All files / state/src/handler stateful.ts

100% Statements 107/107
100% Branches 11/11
100% Functions 4/4
100% Lines 107/107

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 1081x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
import { Bus, BusHandler } from '@sgrud/bus';
import { Symbol } from '@sgrud/core';
import { Observable, ReplaySubject, connectable, dematerialize, from, switchMap } from 'rxjs';
import { Store } from '../store/store';
import { StateHandler } from './handler';
 
/**
 * The **Stateful** decorator, when applied to classes extending the abstract
 * {@link Store} base class, converts those extending classes into type-guarding
 * {@link Store} facades implementing only the {@link Store.dispatch} and the
 * well-known `Symbol.observable` methods. This resulting facade provides
 * convenient access to the current and upcoming {@link Store.State}s of the
 * decorated {@link Store} and its {@link Store.dispatch} method. The decorated
 * class is {@link StateHandler.deploy}ed under the supplied `handle` using the
 * supplied `state` as an initial {@link Store.State}. If the {@link Store} is
 * to be {@link StateHandler.deploy}ed `transient`ly, the supplied `state` is
 * guaranteed to be used as initial {@link Store.State}. Otherwise, a previously
 * persisted {@link Store.State} takes precedence over the supplied `state`.
 *
 * @param handle - The {@link Bus.Handle} representing the {@link Store}.
 * @param state - An initial {@link Store.State} for the {@link Store}.
 * @param transient - Whether the {@link Store} is considered `transient`.
 * @typeParam I - The extending {@link Store} {@link InstanceType}.
 * @typeParam T - A constructor type extending the {@link Store.Type}.
 * @returns A class constructor decorator.
 *
 * @example
 * A simple `ExampleStore` facade:
 * ```ts
 * import { Stateful, Store } from '@sgrud/state';
 *
 * ⁠@Stateful('io.github.sgrud.store.example', {
 *   property: 'default',
 *   timestamp: Date.now()
 * })
 * export class ExampleStore extends Store<ExampleStore> {
 *
 *   public readonly property!: string;
 *
 *   public readonly timestamp!: number;
 *
 *   public async action(property: string): Promise<Store.State<this>> {
 *     return { ...this, property, timestamp: Date.now() };
 *   }
 *
 * }
 * ```
 *
 * @example
 * Subscribe to the `ExampleStore` facade:
 * ```ts
 * import { ExampleStore } from './example-store';
 *
 * const store = new ExampleStore();
 * from(store).subscribe(console.log);
 * // { property: 'default', timestamp: [...] }
 * ```
 *
 * @example
 * Dispatch an {@link Store.Action} through the `ExampleStore` facade:
 * ```ts
 * import { ExampleStore } from './example-store';
 *
 * const store = new ExampleStore();
 * store.dispatch('action', ['value']).subscribe(console.log);
 * // { property: 'value', timestamp: [...] }
 * ```
 * @see {@link StateHandler}
 * @see {@link Implant}
 */
export function Stateful<
  T extends Store.Type<I>,
  I extends Store = InstanceType<T>
>(handle: Bus.Handle, state: Store.State<I>, transient: boolean = false) {
 
  /**
   * @param constructor - The class `constructor` to be decorated.
   * @returns The decorated class `constructor`.
   */
  return function(constructor: T): T {
    let loader: Observable<void>;
 
    (loader = connectable(from(StateHandler).pipe(switchMap((handler) => {
      return handler.deploy(handle, constructor, state, transient);
    })), {
      connector: () => new ReplaySubject<void>(1),
      resetOnDisconnect: false
    })).connect();
 
    return class {
 
      public [Symbol.observable]() {
        return loader.pipe(switchMap(() => from(BusHandler).pipe(
          switchMap((handler) => handler.observe(handle).pipe(dematerialize()))
        )));
      }
 
      public dispatch(...action: Store.Action<I>) {
        return loader.pipe(switchMap(() => from(StateHandler).pipe(
          switchMap((handler) => handler.dispatch<I>(handle, ...action))
        )));
      }
 
    } as unknown as T;
  };
 
}