npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@jacobtipp/bloc

v4.0.3

Published

## Introduction

Downloads

36

Readme

@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();
}