New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@jacobtipp/bloc

Package Overview
Dependencies
Maintainers
1
Versions
68
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@jacobtipp/bloc

## Introduction

latest
npmnpm
Version
4.0.3
Version published
Maintainers
1
Created
Source

@jacobtipp/bloc

Introduction

@jacobtipp/bloc is an npm package that helps implement the BLoC (Business Logic only Components) pattern.

Installation

rxjs is a peerDependency

npm install @jacobtipp/bloc rxjs

Cubit

A cubit is a class that extends BlocBase and is a primitive type of Bloc that can be extended to manage any type of state.

Cubit Architecture

This is similar to stores like Zustand. Your Cubit encapsulates state and you have methods of the Cubit that mutate the state by invoking this.emit.

Creating a Cubit

import { Cubit } from "@jacobtipp/bloc"

export class CounterCubit extends Cubit<number> {
  constructor() {
    super(0); 
  }

  increment(): void {
    this.emit(this.state + 1);
  }

  decrement(): void {
    this.emit(this.state - 1);
  }
}

Using a Cubit

function main() {
  const counterCubit = new CounterCubit();

  console.log(counterCubit.state); // 0

  counterCubit.increment();

  console.log(counterCubit.state); // 1

  // cubit is closed and disposed, it will no longer emit new state and all observers will be unsubscribed
  cubit.close();
}

Observing a Cubit

When a Cubit emits a new state, a Change occurs. We can observe all changes for a given Cubit by overriding onChange.

import { Cubit } from "@jacobtipp/bloc"

export class CounterCubit extends Cubit<number> {
  constructor() {
    super(0); 
  }

  protected override onChange(change: Change<number>): void {
    console.log(change); // { current: 0, nextState: 1 }
  }
  
  increment(): void {
    this.emit(this.state + 1);
  }

  decrement(): void {
    this.emit(this.state - 1);
  }
}

Bloc

A Bloc is a more advanced class which relies on events to trigger state changes rather than functions. Bloc also extends BlocBase which means it has a similar public API as Cubit. However, rather than calling a function on a Bloc and directly emitting a new state, Blocs receive events and convert the incoming events into outgoing states.

Bloc Architecture

Creating a Bloc

import { Bloc } from "@jacobtipp/bloc"

/// The events which `CounterBloc` will react to.
abstract class CounterEvent {
  protected _!: void
}

class CounterIncrement extends CounterEvent {}

class CounterDecrement extends CounterEvent {}

export class CounterBloc extends Bloc<CounterEvent, number> {
  constructor() {
    super(0);

    this.on(CounterIncrement, (event, emit) => emit(this.state + 1));

    this.on(CounterDecrement, (event, emit) => emit(this.state - 1));
  }
}

Using a Bloc

async function main() {
  const delay = (n) => new Promise((resolve) => setTimeout(resolve, n));

  const counterBloc = new CounterBloc();

  console.log(counterBloc.state); // 0

  counterBloc.add(new CounterIncrement());

  // wait for next tick in event loop
  await delay(0);

  console.log(counterBloc.state); // 1

  counterBloc.close(); // close the bloc when no longer needed
}

Observing a Bloc

Similar to Cubits, a Bloc can be observed by overriding onChange, however, because Bloc is event-driven, we are also able to capture information about what triggered the state change.

We can do this by overriding onTransition.

The change from one state to another is called a Transition. A Transition consists of the current state, the event, and the next state.

import { Bloc } from "@jacobtipp/bloc"

export class CounterBloc extends Bloc<CounterEvent, number> {
  constructor() {
    super(0);

    this.on(CounterIncrement, (event, emit) => emit(this.state + 1));

    this.on(CounterDecrement, (event, emit) => emit(this.state - 1));
  }

  protected override onTransition(transition: Transition<CounterEvent, number>): void {
    console.log(transition); // { currentState: 0, event: CounterIncrement, nextState: 1 }
  }
}

Error Handling

Both Bloc and Cubit have an addError and onError method. We can indicate that an error has occurred by calling addError from anywhere inside our Bloc. We can then react to all errors by overriding onError just as with Cubit.

import { Bloc } from "@jacobtipp/bloc"

export function assertIsError(error: unknown): asserts error is Error {
  if (!(error instanceof Error)) {
    throw error;
  }
}

export class CounterBloc extends Bloc<CounterEvent, number> {
  constructor(private readonly counterRepo: CounterRepository) {
    super(0);

    this.on(CounterIncrement, this.onIncrement);
  }

  private onIncrement(event: CounterIncrement, emit: Emitter<number>) {
    try {
      await this.counterRepo.increment()
      this.emit(this.state + 1)
    } catch (e: unknown) {
      assertIsError(e);
      addError(e);
    }
  }

  protected override onError(error: Error): void {
    console.log(error: Error) // Error object with stack trace
  }
}

BlocObserver

BlocObserver is a class that you can extend to listen to all Blocs/Cubits that exist at runtime.

This allows you to trace multiple lifecycle events for all Blocs/Cubits.

// app-bloc-observer.tsx

export class AppBlocObserver extends BlocObserver {
  override onCreate(bloc: BlocBase<any>): void {
    console.log(`onCreate: ${bloc.name}`);
  }

  override onEvent(bloc: Bloc<any, any>, event: any): void {
    console.log(`onEvent: ${bloc.name}`)
  }

  override onError(bloc: BlocBase<any>, error: any): void {
    console.log(`onError: ${bloc.name}`, error);
  }

  override onChange(bloc: BlocBase<any>, change: Change<any>): void {
    console.log(`onChange: ${bloc.name}`, change);
  }

  override onTransition(
    bloc: Bloc<any, any>,
    transition: Transition<any, any>
  ): void {
    console.log(`onTransition: ${bloc.name}`, transition)
  }

  override onClose(bloc: BlocBase<any>): void {
    console.log(`onClose: ${bloc.name}`);
  }
}
// main.tsx
// composition root of your application

BlocObserver.observer = new AppBlocObserver();

Event Transformations

bloc has an event sink that allows us to control and transform the incoming flow of events.

For example, if we were building a real-time search, we would probably want to debounce the requests to the backend in order to avoid getting rate-limited as well as to cut down on cost/load on the backend.

With bloc we can provide a custom EventTransformer to change the way incoming events are processed by the bloc.

Event Transformation is only available for Bloc and not Cubit.


import { Bloc, EventMapper, EventTransformer } from '@jacobtipp/bloc';
import { Observable, debounceTime, mergeMap } from 'rxjs';

// A debounce event transformer
export const debounce =
  <Event>(duration: number): EventTransformer<Event> =>
  (events$: Observable<Event>, mapper: EventMapper<Event>): Observable<Event> =>
    events$.pipe(debounceTime(duration), mergeMap(mapper));

export class CounterBloc extends Bloc<CounterEvent, number> {
  constructor() {
    super(0);

    // apply the debounce event transformer to the event handler
    this.on(CounterIncrement, (event, emit) => emit(this.state + 1), debounce(200));
  }
}

⚠️ Please refer to the bloc-concurrency package, which has commonly used Event Transformers.

Observable

Both Bloc and Cubit extend BlocBase which exposes a state$ property, which is an rxjs observable stream. This can be used to directly subscribe to the state stream.

function main() {
  const counterCubit = new CounterCubit();

  counterCubit.state$.subscribe((state) => {
    console.log(state); 
  });

  counterCubit.increment();

  cubit.close();
}

FAQs

Package last updated on 04 Mar 2024

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts