{"id":51339000,"url":"https://github.com/async/flow","last_synced_at":"2026-07-02T06:00:18.412Z","repository":{"id":366735876,"uuid":"1276572006","full_name":"async/flow","owner":"async","description":"Portable store, async signal, and handler runtime for Async packages.","archived":false,"fork":false,"pushed_at":"2026-06-23T04:25:40.000Z","size":84,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-23T06:15:36.072Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://async.github.io/flow/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/async.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-22T05:22:18.000Z","updated_at":"2026-06-23T06:14:13.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/async/flow","commit_stats":null,"previous_names":["async/flow"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/async/flow","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fflow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fflow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fflow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fflow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/async","download_url":"https://codeload.github.com/async/flow/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fflow/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35034985,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-02T02:00:06.368Z","response_time":173,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-02T06:00:16.145Z","updated_at":"2026-07-02T06:00:18.313Z","avatar_url":"https://github.com/async.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @async/flow\n\nPortable store, async signal, and handler runtime for Async packages.\n\nFlow is useful when an app needs signal-like state, event handlers, async\nsignals, and small workflow helpers without adopting a full statechart engine.\n\nPick the smallest layer that solves the problem:\n\n- L1 primitives: use `createSignal`, `createComputed`, `createAsyncSignal`, and\n  `createStore` when an adapter or library needs explicit values and controllers.\n- L2 Flow: use `flow(...)` when state changes should run through named events\n  and batched plain functions.\n- L2.5 composition: use `compose(...)` and `parallel(...)` when a Flow handler\n  needs ordered or fan-out/fan-in work without a full helper vocabulary.\n- L3 steps: use `set(...)`, `when(...)`, `branch(...)`, `dispatch(...)`, and\n  `after(...)` when repeated workflow wiring should read as reusable steps.\n- Protocol brands: use `@async/flow/protocol` when two layers need to recognize\n  Flow objects without importing each other.\n\n## Install\n\n```bash\npnpm add @async/flow\n```\n\n## Quick Start\n\n```js\nimport { dispatch, flow, status } from \"@async/flow\";\n\nconst counter = flow({\n  store: {\n    count: 0,\n    phase: status(\"idle\", [\"idle\", \"active\"])\n  },\n\n  on: {\n    increment(store, input = {}) {\n      store.count += input.by ?? 1;\n      store.phase = \"active\";\n    },\n\n    reset(store) {\n      store.count = 0;\n      store.phase = \"idle\";\n    }\n  }\n});\n\ncounter.increment({ by: 2 });\n\ncounter.count; // 2\ncounter.phase; // \"active\"\n\ndispatch(counter, \"reset\");\n\ncounter.count; // 0\ncounter.phase; // \"idle\"\n```\n\nA Flow instance combines:\n\n- `store`: author-facing values with getter/setter behavior.\n- `_`: non-enumerable internal controller namespace for `_` store fields.\n- Handler methods from `on`, such as `counter.increment(input)`.\n- `dispatch(name, input?)`: dynamic event execution on this Flow instance.\n- `subscribe(fn)`: whole-flow change records with `{ name, input, store }`.\n- `explain(name, input?)`: structured blocked-event reasons.\n\nThe package also provides `compose(...)`, `parallel(...)`, and `remember(...)`\nfor ordered handler steps. Use imported `can(...)` for event availability and\nimported `inspect(...)` for public metadata snapshots.\n\n## Store-Style Events\n\nEvery `on` handler is projected onto the Flow instance as a method. Whole-flow\nsubscribers receive one batched change record after each handler dispatch, and\n`change.store` contains the public store snapshot after the handler completes.\n\n```js\nimport { dispatch, flow } from \"@async/flow\";\n\nfunction createDonutStore() {\n  return flow({\n    store: {\n      donuts: 0,\n      favoriteFlavor: \"chocolate\"\n    },\n\n    on: {\n      addDonut(store) {\n        store.donuts += 1;\n      },\n\n      changeFlavor(store, event) {\n        store.favoriteFlavor = event.flavor;\n      },\n\n      eatAllDonuts(store) {\n        store.donuts = 0;\n      }\n    }\n  });\n}\n\nconst donutStore = createDonutStore();\n\ndonutStore.subscribe((change) =\u003e {\n  console.log(change.store);\n});\n\ndonutStore.addDonut();\n// logs { donuts: 1, favoriteFlavor: \"chocolate\" }\n\ndonutStore.changeFlavor({ flavor: \"strawberry\" });\n// logs { donuts: 1, favoriteFlavor: \"strawberry\" }\n\nconst routedDonutStore = createDonutStore();\n\nroutedDonutStore.subscribe((change) =\u003e {\n  console.log(change.store);\n});\n\ndispatch(routedDonutStore, \"addDonut\");\n// logs { donuts: 1, favoriteFlavor: \"chocolate\" }\n\ndispatch(routedDonutStore, \"changeFlavor\", { flavor: \"strawberry\" });\n// logs { donuts: 1, favoriteFlavor: \"strawberry\" }\n```\n\nUse direct methods when the event is known at author time. Use target-first\n`dispatch(target, eventName, input?)` when an adapter receives the target or\nevent name dynamically, or when the same sender should work with different event\nsinks.\n\n## Store Values\n\nPlain primitives and arrays become writable store values. Computed values are\nread-only. Plain record values stay explicit; use `signal(value)` when an object\nshould be a single writable value.\n\n```js\nimport { computed, flow, signal, status } from \"@async/flow\";\n\nconst cart = flow({\n  store: {\n    items: [],\n    settings: signal({ currency: \"USD\" }),\n    count: computed(function () {\n      return this.items.length;\n    }),\n    isEmpty: computed(function () {\n      return this.count === 0;\n    }),\n    phase: status(\"idle\", [\"idle\", \"ready\"])\n  },\n\n  on: {\n    add(store, input) {\n      store.items = [...store.items, input.item];\n      store.phase = \"ready\";\n    }\n  }\n});\n\ncart.add({ item: { id: \"sku_123\" } });\n\ncart.count; // 1\ncart.items; // [{ id: \"sku_123\" }]\ncart.settings = { currency: \"EUR\" };\n```\n\nComputed function callbacks read store values directly from `this`.\n\n## Async Signals\n\n`asyncSignal(loader)` declares a lazy async value with lifecycle state and\nexplicit controls. Loaders read Flow store data through `this.store`; lifecycle\ntools are available through the function receiver.\n\n```js\nimport { asyncSignal, flow } from \"@async/flow\";\n\nconst greeting = flow({\n  store: {\n    name: \"World\",\n    _request: asyncSignal(async function () {\n      const response = await fetch(`/api/greeting/${this.store.name}`, {\n        signal: this.signal\n      });\n      return response.text();\n    }),\n    get status() {\n      return this._request.status;\n    },\n    get value() {\n      return this._request.get();\n    }\n  },\n\n  on: {\n    fetch() {\n      return this.store._request.load();\n    },\n\n    reload() {\n      return this.store._request.reload();\n    },\n\n    cancel(_store, reason) {\n      return this.store._request.cancel(reason);\n    }\n  }\n});\n\nawait greeting.fetch();\n\ngreeting.value; // loaded text\ngreeting.status; // \"ready\"\n```\n\nLazy and immediate async signals can both use internal fields starting with `_` for\ncontroller methods while exposing public getters as normal Flow values.\n\n```js\nconst profile = flow({\n  store: {\n    _user: asyncSignal({ immediate: true }, async function () {\n      const response = await fetch(\"/api/user\", { signal: this.signal });\n      return response.json();\n    }),\n    get user() {\n      return this._user.get();\n    },\n    get status() {\n      return this._user.status;\n    }\n  },\n\n  on: {\n    reloadUser() {\n      return this.store._user.reload();\n    }\n  }\n});\n\nprofile.user; // current value\nprofile.status; // \"loading\", \"ready\", or \"error\"\n```\n\nMore detail: [Async Signal Lifecycle](docs/async-signals.md).\n\n## Compose And Step Workflows\n\nUse `compose(...)` for ordered steps that should share one Flow handler input.\nEach step receives `(store, input, previous)`. Use `parallel(...)` when one\nordered step should run independent effects before continuing. Use root-exported\nstep helpers when the repeated parts are store writes, gates, branches, event\ndispatches, or scheduled follow-up events.\n\n```js\nimport { compose, dispatch, every, flow, matches, not, parallel, set, status, when } from \"@async/flow\";\n\nconst checkout = flow({\n  store: {\n    step: status(\"shipping\", [\"shipping\", \"payment\", \"review\"]),\n    canSubmit: true,\n    readyToSubmit: every(matches(\"step\", \"review\"), (store) =\u003e store.canSubmit),\n    blocked: not((store) =\u003e store.readyToSubmit),\n    loading: false,\n    orderId: null\n  },\n\n  on: {\n    submit: compose([\n      when((store) =\u003e store.readyToSubmit, {\n        availability: true,\n        reason: \"not_ready\",\n        label: \"Submit order\"\n      }),\n      set(\"loading\", true),\n      parallel({\n        inventory(_store, input) {\n          return reserveInventory(input.form);\n        },\n        tax(_store, input) {\n          return calculateTax(input.form);\n        }\n      }),\n      async (_store, input) =\u003e {\n        const order = await submitOrder(input.form);\n        return order.id;\n      },\n      (store, _input, orderId) =\u003e {\n        store.orderId = orderId;\n      },\n      set(\"loading\", false)\n    ])\n  }\n});\n```\n\n`compose` stays synchronous until a step returns a promise-like value. Flow then\nflushes the current synchronous batch and resumes later steps in a fresh batch.\nThat lets `loading = true` render before async work settles.\n\nMore detail: [Compose And Status Helpers](docs/compose-and-status.md).\n\n`dispatch(\"event\", payload?)` creates a reusable deferred sender. In a composed\nFlow handler it dispatches to the current Flow receiver; outside Flow it can be\nsent to any supported event sink. When the target is already a Flow instance and\nthe event is known, `checkout.ready(input)` is the direct equivalent of\n`dispatch(checkout, \"ready\", input)`.\n\n```js\nconst ready = dispatch(\"ready\", { id: 1 });\n\nready.call(checkout);\nready.call(element);\nready.emit(emitter);\nready.send(sender);\n\ndispatch(checkout, \"ready\", { id: 1 });\ndispatch(element, \"ready\", { id: 1 });\ndispatch(emitter, \"ready\", { id: 1 });\ndispatch(sender, \"ready\", { id: 1 });\n```\n\n## Event Availability And Inspection\n\nFlow can answer whether an event is registered and whether Flow-visible guards,\ntransitions, or explicit leading availability gates currently allow it without\ndispatching the event.\n\n```js\nimport { can, inspect } from \"@async/flow\";\n\ncan(checkout, \"submit\").get(); // false while the leading availability gate is blocked\ncheckout.explain(\"submit\");\n// { event: \"submit\", allowed: false, reason: \"not_ready\", source: \"guard\", label: \"Submit order\" }\n\ncheckout.explain(\"missing\");\n// { event: \"missing\", allowed: false, reason: \"unknown_event\" }\n```\n\nUse `inspect(...)` when adapters need stable public metadata:\n\n```js\nconst description = inspect(checkout);\n\ndescription.handlers; // [\"submit\"]\ndescription.store.step.type; // \"status\"\n```\n\nInspections expose names, current values, lifecycle state, and safe metadata.\nThey do not expose raw handlers or predicates.\n\nUse `inspect(...)` for standalone status refs, computed refs, transition\nhelpers, and timer helpers without depending on a Flow instance:\n\n```js\nimport { after, inspect, status } from \"@async/flow\";\n\nconst phase = status(\"idle\", [\"idle\", \"active\"]);\nconst description = inspect(phase);\n\ndescription.type; // \"status\"\ndescription.value; // \"idle\"\n```\n\n`after(ms, callback, input?)` also works without a Flow instance. It returns a\ncancellable timer helper.\n\n```js\nconst markReady = after(100, (next) =\u003e {\n  phase.set(next);\n}, \"ready\");\n\nconst cancel = markReady();\ncancel();\n```\n\n## Runtime Options\n\nThe top-level authoring helper accepts either config or options plus config.\n\n```js\nflow(config);\nflow({ scheduler, context }, config);\n```\n\nWith two arguments, the first object is always runtime options and the second is\nalways Flow config.\n\nHandlers receive `(store, input)`. Runtime capabilities are available through\nmethod syntax or normal functions:\n\n```js\nconst appFlow = flow(\n  {\n    context() {\n      return { logger: console };\n    }\n  },\n  {\n    store: {\n      count: 0\n    },\n\n    on: {\n      increment(store, input) {\n        store.count += input.by;\n        this.logger.log(store.count);\n        return this.dispatch(\"read\");\n      },\n\n      read(store) {\n        return store.count;\n      }\n    }\n  }\n);\n```\n\nReceiver capabilities include `this.store`, `this.refs`, `this.asyncSignals`,\n`this.dispatch(name, input)`, `this.explain(name, input)`,\n`this.after(ms, eventName, input)`, and `this.dispose(cleanup)`. Imported\n`dispatch(...)`, `can(...)`, and `inspect(...)` can also receive a Flow handler\nreceiver.\n\n## Root And Subpaths\n\nThe root package exports the complete opinionated Flow surface. Use subpaths\nwhen a consumer wants a narrower entrypoint.\n\n```js\nimport {\n  after,\n  asyncSignal,\n  bool,\n  branch,\n  compose,\n  computed,\n  createAsyncSignal,\n  createFlow,\n  createSignal,\n  createStore,\n  defineAsyncSignal,\n  defineFlow,\n  dispatch,\n  every,\n  flow,\n  matches,\n  not,\n  parallel,\n  remember,\n  set,\n  signal,\n  some,\n  status,\n  when\n} from \"@async/flow\";\n```\n\nGraph helpers live in an opt-in subpath and are not re-exported from the root\nentrypoint. They consume Flow instances through the shared inspection symbol\nrather than importing runtime helpers:\n\n```js\nimport { toGraph, toMermaid } from \"@async/flow/graph\";\n```\n\nProtocol symbols live in a tiny shared subpath:\n\n```js\nimport { FLOW_INSPECT, ASYNC_SIGNAL } from \"@async/flow/protocol\";\n```\n\nDefinition helpers, runtime primitives, composition primitives, and scheduler\ncontrols are also available as narrow subpaths:\n\n```js\nimport { defineFlow, defineSignal } from \"@async/flow/define\";\nimport { createFlow } from \"@async/flow/runtime\";\nimport { compose, parallel, remember } from \"@async/flow/compose\";\nimport { set, update, when } from \"@async/flow/helpers\";\nimport { asyncSignal, createAsyncSignal } from \"@async/flow/async-signal\";\nimport { createDefaultScheduler } from \"@async/flow/scheduler\";\n```\n\nFramework integrations that provide their own scheduler can use the\nscheduler-free runtime and helper subpaths:\n\n```js\nimport { createFlow } from \"@async/flow/framework-runtime\";\nimport { set, update, when, onError } from \"@async/flow/helpers/core\";\n```\n\nThe `@async/flow/steps` subpath mirrors the step helpers for consumers that want\na step-oriented import name.\n\nBuilder helpers also live in an opt-in subpath. Use them when a graph\ndeclaration should compile into ordinary Flow config while implementation\ndetails come from handler and signal registries:\n\n```js\nimport { flow } from \"@async/flow\";\nimport { toFlowConfig } from \"@async/flow/builder\";\n\nconst payment = flow(toFlowConfig(paymentGraph, {\n  handlers: {\n    canSubmit,\n    chargePayment\n  },\n  signals: {\n    isOnline\n  }\n}));\n```\n\n## Docs\n\n- [Docs Index](docs/README.md)\n- [Layer Guide](docs/layers.md)\n- [Signals, Computed, Async Signals, And Store](docs/state-and-store.md)\n- [Async Signal Lifecycle](docs/async-signals.md)\n- [Compose And Status Helpers](docs/compose-and-status.md)\n\n## Package Checks\n\n```bash\npnpm test\npnpm run typecheck\npnpm run pack:check\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasync%2Fflow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fasync%2Fflow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasync%2Fflow/lists"}