Skip to content

Latest commit

 

History

History
239 lines (191 loc) · 6.43 KB

File metadata and controls

239 lines (191 loc) · 6.43 KB

Layer Guide

@async/flow is built in layers. Each layer keeps the lower layer available and adds only the authoring shape needed for that level of workflow structure.

Use the lowest layer that keeps the code clear. Move up when repeated patterns become part of the application design instead of local state mechanics.

L1: Primitives And Store

L1 is the live state layer. It is useful for adapters, framework integrations, tests, and small state units that do not need named events yet.

Signals And Computed Values

Signals are writable values with get, set, update, subscribe, and snapshot. Writable values also expose restore. Computed values are read-only values derived from signals or other store values.

import { createComputed, createSignal } from "@async/flow";

const count = createSignal(1);
const doubled = createComputed(() => count.value * 2);

count.set(2);
doubled.value; // 4

Async Signals

Async signals are async value controllers with load, reload, cancel, set, lifecycle status, snapshots, and subscriptions.

import { createAsyncSignal } from "@async/flow";

const greeting = createAsyncSignal(async function (input) {
  return `Hello ${input.name}`;
});

await greeting.load({ name: "Ada" });

greeting.status; // "ready"
greeting.value; // "Hello Ada"

Store Proxy

Stores wrap signals, computed values, async signals, and plain writable values in one author-facing proxy while keeping intentionally internal async signal controllers available.

import { asyncSignal, computed, createStore, signal } from "@async/flow";

const state = createStore({
  count: 0,
  settings: signal({ currency: "USD" }),
  doubled: computed(function () {
    return this.count * 2;
  }),
  _greeting: asyncSignal(async function () {
    const currency = this.store.settings.currency;
    return `Hello ${currency}`;
  }),
  get greeting() {
    return this._greeting.get();
  }
});

state.store.count += 1;
state.store.doubled; // 2
state.store.count; // 1
await state.store._greeting.load();
state.store.greeting; // "Hello USD"

Choose L1 when:

  • You are integrating Flow state into another runtime or framework.
  • You need async signal controllers.
  • There is no useful event vocabulary yet.
  • Tests or adapters need small state units without a full Flow instance.

L2: Flow Events And Status

L2 adds named events, handler batching, snapshots, receiver capabilities, and finite status values. Handlers are still just functions.

import { dispatch, flow, status } from "@async/flow";

const counter = flow({
  store: {
    count: 0,
    phase: status("idle", ["idle", "active"])
  },

  on: {
    increment(store, input = {}) {
      store.count += input.by ?? 1;
      store.phase = "active";
    },

    reset(store) {
      store.count = 0;
      store.phase = "idle";
    }
  }
});

counter.increment({ by: 2 });
dispatch(counter, "reset");

Choose L2 when:

  • State changes should be named actions such as increment, fetch, or submit.
  • Subscribers should see batched handler changes.
  • Author code should call known events directly, such as checkout.submit(input).
  • Adapters need target-first dispatch(target, eventName, input?) for dynamic event routing.
  • Handlers need this.dispatch(...), this.after(...), internal controllers, or injected runtime context.
  • UI controls or adapters need imported can(...), explain(...), or inspect(...) without dispatching events.

L2.5: Composition And Parallel Effects

L2.5 keeps plain functions but lets one handler read as ordered work. Use compose(...) for steps and parallel(...) for fan-out/fan-in effects. This layer does not require guards, branches, store-write helpers, or scheduling helpers.

import { compose, flow, parallel, status } from "@async/flow";

const checkout = flow({
  store: {
    step: status("review", ["review", "submitted"]),
    loading: false,
    orderId: null
  },

  on: {
    submit: compose([
      (store) => {
        store.loading = true;
      },
      parallel({
        inventory(_store, input) {
          return reserveInventory(input.form);
        },
        tax(_store, input) {
          return calculateTax(input.form);
        }
      }),
      async (_store, input) => {
        const order = await submitOrder(input.form);
        return order.id;
      },
      (store, _input, orderId) => {
        store.orderId = orderId;
        store.step = "submitted";
        store.loading = false;
      }
    ])
  }
});

Choose L2.5 when:

  • A handler has ordered synchronous and async segments.
  • Independent effects should start at the same ordered point.
  • You want step-level previous values without introducing the L3 helper vocabulary.

L3: Step Helpers

L3 adds reusable step helpers. These helpers are still ordinary Flow handler functions, but common workflow wiring reads declaratively.

import { after, branch, compose, dispatch, flow, set, status, when } from "@async/flow";

const job = flow({
  store: {
    step: status("SubmitJob", [
      "SubmitJob",
      "WaitForCompletion",
      "GetJobStatus",
      "JobSucceeded",
      "JobError"
    ]),
    jobStatus: undefined
  },

  on: {
    determineCompletion: compose([
      when((store) => store.step === "GetJobStatus"),
      branch([
        [(store) => store.jobStatus === "SUCCEEDED", dispatch("reportJobSucceeded")],
        [(store) => store.jobStatus === "ERROR", dispatch("reportJobError")],
        compose([
          set("step", "WaitForCompletion"),
          after(5000, "checkJobStatus")
        ])
      ])
    ])
  }
});

Choose L3 when:

  • Several handlers share store-write, gate, branch, dispatch, or scheduling patterns.
  • You want workflow code to read as reusable steps instead of one long handler.
  • You need set(...) projections from dispatch input or previous compose results.
  • You need after(...) to schedule follow-up events without writing a custom receiver function.

Moving Up The Layers

Start with L1 for primitives, move to L2 when state changes have event names, use L2.5 when one event has ordered or parallel work, and move to L3 when the same workflow wiring repeats.

The only half-step is L2.5 because composition changes handler structure without adding a new domain vocabulary. L1 does not need a half-step: definitions and runtime primitives are part of the same primitive/store layer. L3 does not need a half-step: new helpers should either stay as reusable steps or become a separate domain package.