3.0.1 • Published 8 months ago

rxjs-util-classes v3.0.1

Weekly downloads
4
License
MIT
Repository
github
Last release
8 months ago

Build Status codecov npm version dependabot-status semantic-release Commitizen friendly

RxJS-Util-Classes

Simple RxJS implementations for common classes used across many different types of projects. Implementations include: redux-like store (for dynamic state management), rxjs maps, rxjs lists (useful for caching), and rxjs event emitters.

Index


Installation

Install rxjs and rxjs-util-classes (rxjs v6.x is required):

npm install --save rxjs rxjs-util-classes

# or via yarn

yarn add rxjs rxjs-util-classes

Importing:

import { ObservableMap } from 'rxjs-util-classes';

// or 

const { ObservableMap } = require('rxjs-util-classes');

Observable Maps

This is a wrapper around the native JavaScript Map except it returns observables. There are three main map types:

See the Maps API and Important Notes about ObservableMaps for additional information

See map recipes for commom use cases.

ObservableMap

Uses the standard RxJS Subject so subscribers will only receive values emitted after they subscribe. (Full API)

import { ObservableMap } from 'rxjs-util-classes';

const observableMap = new ObservableMap<string, string>();

observableMap.set('my-key', 'this value will not be received');

observableMap.get$('my-key').subscribe(
  value => console.log('Value: ' + value),
  error => console.log('Error: ' + error),
  () => console.log('complete')
);

// `.set()` will emit the value to all subscribers
observableMap.set('my-key', 'first-data');
observableMap.set('my-key', 'second-data');

// delete calls `.complete()` to clean up 
// the observable
observableMap.delete('my-key');

// OUTPUT:
// Value: first-data
// Value: second-data
// complete

BehaviorMap

Uses the RxJS BehaviorSubject so subscribers will always receive the last emitted value. This class requires an initial value to construct all underlying BehaviorSubjects. (Full API)

import { BehaviorMap } from 'rxjs-util-classes';

const behaviorMap = new BehaviorMap<string, string>('initial-data');

behaviorMap.get$('my-key').subscribe(
  value => console.log('Value: ' + value),
  error => console.log('Error: ' + error),
  () => console.log('complete')
);

behaviorMap.set('my-key', 'first-data');
behaviorMap.set('my-key', 'second-data');

// emitError calls `.error()` which ends the observable stream
//  it will also remove the mey-value from the map
behaviorMap.emitError('my-key', 'there was an error!');

// OUTPUT:
// Value: initial-data
// Value: first-data
// Value: second-data
// Error: there was an error!

ReplayMap

Uses the RxJS ReplaySubject so subscribers will receive the last nth emitted values. This class requires an initial replay number to construct all underlying ReplaySubject. (Full API)

import { ReplayMap } from 'rxjs-util-classes';

const replayMap = new ReplayMap<string, string>(2);

replayMap.set('my-key', 'first-data');
replayMap.set('my-key', 'second-data');
replayMap.set('my-key', 'third-data');
replayMap.set('my-key', 'fourth-data');

replayMap.get$('my-key').subscribe(
  value => console.log('Value: ' + value),
  error => console.log('Error: ' + error),
  () => console.log('complete')
);

// delete calls `.complete()` to clean up 
// the observable
replayMap.delete('my-key');

// OUTPUT:
// Value: third-data
// Value: fourth-data
// complete

Important Notes about ObservableMaps

  • map.get$() (or map.get() if using BehaviorMap)
    • This will always return an Observable and never return undefined. This is different than the standard JS Map class which could return undefined if the value was not set. The reason for this because callers need something to subsribe to.

Available Map API methods

The ObservableMap, BehaviorMap, & ReplayMap all share the same methods. The only exception is the constructors and BehaviorMap has some additional synchornous methods. Any method that returns an observable will following the standard practice of ending with a $. All methods listed are public

See the Maps API for more details

K = generic type defined as map keys (string | number | boolean)

V = generic type defined as map value (any)

Differences in Constructors
  • new ObservableMap<K, V>() - blank constructor. All underlying observables will be constructed with the standard Subject.
  • new ReplayMap<K, V>(relayCount: number) - number of replays to share. Number passed in will be passed to all underlying ReplaySubjects
  • new BehaviorMap<K, V>(initialValue: V) - initial value to start each observable with. Value will be passed into each underlying BehaviorSubject
Methods available to all Maps
  • size - value of the current size of the underlying Map
  • has(key: K): boolean - returns if a given key exists
  • set(key: K, value: V): this - emits a value on the given key's observable. It will create an observable for the key if there is not already one.
  • get$ (key: K): Observable<V> - returns the observable for a given key. It will create an observable for the key if there is not already one _(meaning this will never return falsy).
  • emitError(key: K, error: any): this - calls error() on the given key's underlying subject. Emiting an error on an observable will terminate that observable. It will create an observable for the key if there is not already one. It will then call delete(key) to remove the key/value from the underlying Map.
  • clear(): void - will clear out the underlying Map of all key/value pairs. It will call complete() on all observables
  • delete(key: K): boolean - returns false if the key did not exist. Returns true is the key did exists. It then calls complete() on the observable and removes that key/value pair from the underlying Map
  • keys(): IterableIterator<K> - returns an iterable iterator for the underlying Map's keys
  • forEach$(callbackfn: (value: Observable<V>, key: K) => void): void - takes a callback function that will be applied to all the underlying Map's observables
  • entries$ (): IterableIterator<[K, Observable<V>]> - returns an iterable iterator over key/value pairs of the underlying Map where the value is the key's observable
  • values$ (): IterableIterator<Observable<V>> - returns an iterable iterator over the underlying Map's values as observables
Methods only available on BehaviorMap

Because Behavior Subjects keep their last value, we can interact with that value synchronously.

  • get(key: K): V - returns the current value of a given key. It will create an observable for the key if there is not already one which will return the initialValue passed into the constructor.
  • forEach (callbackfn: (value: V, key: K) => void): void - takes a callback function that will be applied to all the underlying Map's observables values
  • values (): IterableIterator<V> - returns an iterable iterator over the underlying Map's current observable values
  • entries (): IterableIterator<[K, V]> - returns an iterable iterator over key/value pairs of the underlying Map where the value is the key's current observable value

Base Store

This is a simple RxJS implementation of Redux and state management.

Redux is a very popular state management solution. The main concepts of redux-like state is that:

  • State in a central location (in the "store")
  • State can only be modified by "dispatching" events to the "store" that are allowed/configured

This makes keeping track of state uniformed because the state is always in one location, and can only be changed in ways the store allows.

One huge advantage of this implementation is its ability to have a dyanmic store. See the Dynamic Store recipe for further details and implementation.

BaseStore

src/app-store.ts (pseudo file to show store implementation)

import { BaseStore } from 'rxjs-util-classes';

export interface IUser {
  username: string;
  authToken: string;
}

export interface IAppState {
  isLoading: boolean;
  authenticatedUser?: IUser;
  // any other state your app needs
}

const initialState: IAppState = {
  isLoading: false,
  authenticatedUser: undefined
};

/**
 * Extend the BaseStore and expose methods for components/services 
 *  to call to update the state
 */
class AppStore extends BaseStore<IAppState> {
  constructor () {
    super(initialState); // set the store's initial state
  }

  public setIsLoading (isLoading: boolean): void {
    this.dispatch({ isLoading });
  }

  public setAuthenticatedUser (authenticatedUser?: IUser): void {
    this.dispatch({ authenticatedUser });
  }

  // these methods are inherited from BaseStore
  // getState(): IAppState
  // getState$(): Observable<IAppState>
}

/* export a singleton instance of the store */
export const store = new AppStore();

src/example-component.ts (pseudo component that will authenticate the user and interact with the app's state)

import { store, IUser, IAppState } from './app-store';

/**
 * Function to mock an authentication task
 */
function authenticate () {
  return new Promise(res => {
    setTimeout(() => {
      res({ username: 'bob-samuel', authToken: 'qwerty-123' });
    }, 1000);
  });
}

store.getState$().subscribe((appState: IAppState) => {
  /* do something with the state as it changes; 
    maybe show a spinner or the authenticatedUser's username */
});

/* authenticate to get the user */
store.setIsLoading(true);
authenticate()
  .then((user: IUser) => {
    /* here we set the store's state via the methods the 
      store exposed */
    store.setAuthenticatedUser(user);
    store.setIsLoading(false);
  })
  .catch(err => store.setIsLoading(false));

Available BaseStore API methods

BaseStore is an abstract class and must be extended.

T = generic type defined as the state type ({ [key: string]: any }) WithPreviousState<T> = generic type defined as the state type (T & { __previousState: T } where T = { [key: string]: any })

  • protected constructor (initialState: T) - construct with the intial state of the store. Must be called from an extending class
  • public getState$ (): Observable<WithPreviousState<T>> - returns an observable of the store's state. Underlying implementation uses a BehaviorSubject so this call will always receive the current state
  • public getState (): WithPreviousState<T> - returns the current state synchronously
  • public destroy (): void - calls complete() on the underlying BehaviorSubject. Once a store has destroyed, it can no longer be used
  • protected dispatch (state: Partial<T>): void - updates the state with the passed in state then calls next() on the underlying BehaviorSubject. This will do a shallow copy of the state using the spread operator (...). This is to keep state immutable.

You can access the immediate

Future Features

  • WildEmitter implementation
  • List Map for caching and watching changes on a list

Contributing

Install

# clone repo
git clone https://github.com/djhouseknecht/rxjs-util-classes.git

# move into directory
cd ./rxjs-util-classes

# install
npm install

Commits

All commits must be compliant to commitizen's standard. Useful commit message tips can be found on angular.js' DEVELOPER.md.

# utility to format commit messages
npm run commit

If you are releasing a new version, make sure to update the CHANGELOG.md The changelog adheres to Keep a Changelog.

Know what messages will trigger a release. Check semantic-release defaults and any added to ./package.json#release

Versioning & Deploying

Deploying is managed by semantic-release. Messages must comply with commitizen see above.

Testing & Linting

Testing coverage must remain at 100% and all code must pass the linter.

# lint
npm run lint 
# npm run lint:fix # will fix some issues

npm run test

Building

npm run build

TODO

  • Add more features
3.0.2

8 months ago

3.0.1

8 months ago

3.0.0

1 year ago

2.2.1

4 years ago

2.2.0

4 years ago

2.1.0

4 years ago

2.0.2

5 years ago

2.0.1

5 years ago

1.2.0

5 years ago

2.0.0

5 years ago

1.1.3

5 years ago

1.1.2

5 years ago

1.1.1

5 years ago

1.1.0

5 years ago

1.0.0

5 years ago