Skip to content

A scalable event-driven application framework.

License

Notifications You must be signed in to change notification settings

tryshapex/shapex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

37 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ShapeX

Create scalable event-driven applications with ShapeX, inspired by re-frame. ShapeX uses zero dependencies and is runtime agnostic, meaning that you can use it in Node, Deno, Bun, browsers, or really anywhere where JavaScript runs.

Example application

This is an example application that demonstrates how to use the ShapeX library. It has a single starting point event called request, which returns an updated state, which changes the counter. When that state changes, the subscriber for the counter state fires.

import ShapeX from "@shapex/shapex";

type AppState = {
  counter: number;
};

const app = ShapeX<AppState>({
  counter: 1,
});

app.subscribe("$.counter", (state) => {
  console.log("counter changed", state);

  return {
    state,
  };
});

app.subscribe<[]>("request", (state) => {
  return {
    state: {
      ...state,
      counter: state.counter + 1;
    }
  }
});

// Dispatch an event somewhere.
app.dispatch("request");

Installation

ShapeX is available via JSR for Node, Deno, Bun, Cloudflare Workers and browsers.

Documentation

State

At the core of your application is state. You start by initiating ShapeX with some initial state, like so:

import ShapeX from "@shapex/shapex";

type AppState = {
  counter: number;
};

const app = ShapeX<AppState>({
  counter: 1,
});

You can model your AppState however you like. It does not have to be called AppState.

Events

Events set things in motion. You can dispatch events like so:

app.dispatch("some-event-name");

And, if there's a subscription for that event name, that subscription will then fire. The above example is a data-less event, but you can also dispatch events with data, like so:

app.dispatch("some-event-name", {
  hello: "world",
});

Subscriptions

Subscriptions listen to events or changes to state. Each subscription must return a SubscriptionResponse object, which looks like this:

{
  state: T, // optional
  dispatch: {
    to: "event-to-dispatch",
    with: {} // optional
  } // optional
}

You can also dispatch multiple events by passing an array of objects, like so:

{
  state: T,
  dispatch: [{
    to: "event-to-dispatch",
    with: {}
  },{
    to: "another-event-to-dispatch",
    with: {}
  }]
}

Event subscriptions

You can listen to events like so:

app.subscribe("some-event-name", (state, data: <{hello: string}>) => {
  return {
    state,
  };
});

Each subscription has a callback function which gets passed to it the app state and whatever data was passed when the event was dispatched. Subscription callbacks must return an Response which consists of updated state and/or further event dispatches. If you don't want to update state, just return the same state that the callback got in the first place.

State change subscriptions

You can also listen to state changes with subscriptions, which will fire when the listened state changes. You can listen to state changes like so:

app.subscribe("$.counter", (state) => {
  return {
    state,
  };
});

Notable difference here is the $. prefix in the subscription listener name, which tells ShapeX what state to look for. Here $.counter will look for the root-level counter key in state. To look for nested state, simply add a dot (.) followed by the key name, i.e: $.counter.nestedKey. Additionally, state change subscriptions do not get any additional data passed to them, only state.

Subscribe only once

If you want to subscribe to an event or state change only once, you can use the subscribeOnce method. This method works similarly to subscribe, but it will automatically unsubscribe after the first event or state change.

app.subscribeOnce("$.counter", (state) => {
  return {
    state,
  };
});

Unsubscribe

If you want to unsubscribe from an event or state change, you can use the unsubscribe method. This method takes the event or state change name as its argument and removes the subscription.

app.unsubscribe("counter++");

Change state

You can change state by returning a new state object, like so:

app.subscribe("counter++", (state) => {
  return {
    state: {
      ...state,
      counter: state.counter + 1,
    },
  };
});

Dispatch events from subscriptions

You can also dispatch events from within subscriptions, like so:

app.subscribe("counter++", (state) => {
  return {
    state: {
      ...state,
      counter: state.counter + 1,
    },
  };
});

app.subscribe("some-event-name", (state) => {
  return {
    state,
    dispatch: {
      to: "counter++",
    },
  };
});

Now if some-event-name is dispatched, it also dispatches counter++. You can also pass data along, like so:

app.subscribe("counter-increase", (state, increase: number) => {
  return {
    state: {
      ...state,
      counter: state.counter + increase,
    },
  };
});

app.subscribe("some-event-name", (state) => {
  return {
    state,
    dispatch: {
      to: "counter-increase",
      with: 5,
    },
  };
});

So now if some-event-name is dispatched, it also dispatches counter-increase with an increase of 5.

Get the subscription count

If you want to get the number of subscriptions for a specific event or state change, you can use the subscriptionCount method. This method takes the event or state change name as its argument and returns the number of subscriptions.

// State change subscriptions
app.subscriptionCount("$.counter");

// Event subscriptions
app.subscriptionCount("some-event-name");

Get all subscriptions

If you want to get all subscriptions, you can use the subscriptions method. This method returns an array of all the subscription names.

app.subscriptions();

Get current app state

If you want to get the current state of the app, you can use the state method. This method returns the current state of the app.

app.state();