@lichenbuliren/flow-orchestrator
v0.2.0
Published
A high-cohesion, low-coupling flow orchestration engine for multi-step workflows
Maintainers
Readme
flow-orchestrator
A high-cohesion, low-coupling flow orchestration engine for multi-step workflows.
Features
- Framework-agnostic core — FlowEngine is pure logic, zero platform dependencies
- Pluggable navigation — Implement
INavigationAdapterfor any navigation system - Built-in goBack — First-class backward navigation with automatic stack tracking
- Middleware pipeline — Logging, persistence, timeout as plugins, not core code
- Declarative node config —
beforeEnterhooks replace ad-hoc handler patterns - Two node types —
page(renders UI) andaction(logic-only, auto-advances) - Type-safe — Full TypeScript support with generics for meta and node data
Install
npm install @lichenbuliren/flow-orchestrator
# or
pnpm add @lichenbuliren/flow-orchestratorReact is an optional peer dependency — only required when using the React hooks (useFlow / useFlowNode).
Quick Start
import {
FlowOrchestrator,
FlowEventName,
BeforeEnterCode,
ActionResultType,
flowInstanceManager,
type INavigationAdapter,
} from '@lichenbuliren/flow-orchestrator';
// 1. Implement your navigation adapter
const adapter: INavigationAdapter = {
push(pageName, propsData, options) {
YourNavigator.push(pageName, propsData, options);
},
pop(options) {
YourNavigator.pop(options);
},
};
// 2. Define your flow nodes
const nodes = [
{
id: 1,
name: 'TermsPage',
type: 'page' as const,
},
{
id: 2,
name: 'OTPVerify',
type: 'page' as const,
beforeEnter: async (ctx) => {
if (canSkipOTP(ctx.meta)) return { code: BeforeEnterCode.Skip };
return { code: BeforeEnterCode.Continue };
},
},
{
id: 3,
name: 'KYCCheck',
type: 'action' as const,
execute: async (ctx) => {
const result = await checkKYC();
if (!result.ok) return { type: ActionResultType.Abort, reason: 'kyc_failed' };
ctx.set('kycResult', result);
return { type: ActionResultType.Next };
},
},
{
id: 4,
name: 'SetupPIN',
type: 'page' as const,
presentation: { secure: true },
},
];
// 3. Create and start the flow
const flow = new FlowOrchestrator({
flowId: 'onboarding',
nodes,
meta: { source: 'home', userId: 123 },
adapter,
});
flowInstanceManager.register(flow);
flow.events.on(FlowEventName.End, ({ history }) => {
console.log('Flow completed', history);
});
flow.events.on(FlowEventName.Abort, ({ reason }) => {
console.log('Flow aborted', reason);
});
await flow.start();React Hooks
import { useFlow, useFlowNode } from '@lichenbuliren/flow-orchestrator';
function MyFlowPage(props) {
const { next, goBack, abort } = useFlow();
const { data, meta, isGoBack } = useFlowNode(props);
return (
<View>
<Button onPress={() => goBack()} title="Back" />
<Button onPress={() => next({ result: 'ok' })} title="Continue" />
<Button onPress={() => abort()} title="Cancel" />
</View>
);
}Middleware
import {
LoggingMiddleware,
PersistenceMiddleware,
TimeoutMiddleware,
} from '@lichenbuliren/flow-orchestrator';
const flow = new FlowOrchestrator({
// ...
middleware: [
new LoggingMiddleware(logger),
new PersistenceMiddleware(asyncStorageAdapter),
new TimeoutMiddleware(5 * 60 * 1000, (ctx) => {
console.warn('Node timed out', ctx.currentNode);
}),
],
});Architecture
FlowOrchestrator (orchestrator)
├── FlowEngine (pure state machine, no platform deps)
├── NavigationController (page stack + popCount, auto-managed)
│ ├── PageStack (tracks which pages are in the native stack)
│ └── INavigationAdapter (your platform implementation)
├── FlowContext (meta + shared state)
└── MiddlewarePipeline (logging, persistence, timeout, ...)Development
Prerequisites
- Node.js >= 16
- pnpm (recommended)
Setup
pnpm installBuild
# Production build (outputs CJS + ESM + .d.ts to dist/)
pnpm build
# Watch mode for development
pnpm devBuild is powered by tsup. Configuration lives in tsup.config.ts, outputting:
| Format | File |
|--------|------|
| CommonJS | dist/index.js |
| ES Module | dist/index.esm.js |
| Type declarations | dist/index.d.ts |
Test
pnpm testTests use Jest + ts-jest. Test files live in src/__tests__/.
Type Check
pnpm typecheckDocumentation
The documentation site is built with dumi.
Local Preview
pnpm docs:devThis starts a local dev server (usually at http://localhost:8000) with hot reload enabled.
Build Docs
pnpm docs:buildStatic files are generated to docs-dist/.
Writing a Demo
Demos live in docs/demos/ and are referenced from markdown pages. To add a new demo:
- Create a React component in
docs/demos/, e.g.docs/demos/MyDemo.tsx:
import React, { useState } from 'react';
import { FlowEngine, FlowStatus } from '@lichenbuliren/flow-orchestrator';
import type { FlowNode } from '@lichenbuliren/flow-orchestrator';
const NODES: FlowNode[] = [
{ id: 'step1', name: 'Step1', type: 'page' },
{ id: 'step2', name: 'Step2', type: 'page' },
];
export default function MyDemo() {
// your demo logic here
return <div>...</div>;
}- Create a companion markdown file
docs/demos/my-demo.md:
---
title: My Demo
order: 3
---
# My Demo
Brief description of what this demo shows.
<code src="./MyDemo.tsx"></code>- (Optional) Add the page to navigation in
.dumirc.tsif you want it in the top nav/sidebar.
The demo can import from @lichenbuliren/flow-orchestrator directly — dumi resolves it to src/index.ts via the alias configured in .dumirc.ts.
Release
Version management and publishing are handled by Changesets.
First-time Setup
# Login to npmjs.org (only needed once per machine)
npm login --registry https://registry.npmjs.org/
# Verify identity
npm whoami --registry https://registry.npmjs.org/Workflow Overview
Code change → pnpm changeset → commit → pnpm version → pnpm release1. Add a Changeset
After making changes, describe what changed and the semver impact:
pnpm changesetFollow the interactive prompts to select the version bump type (patch / minor / major) and write a change summary. This generates a markdown file in .changeset/ — commit it along with your code.
2. Version (Consume Changesets)
When ready to release, consume all pending changesets to bump package.json version and update CHANGELOG.md:
pnpm versionReview the version bump and changelog, then commit:
git add .
git commit -m "chore: release v0.2.0"
git tag v0.2.03. Publish
pnpm releaseThis runs pnpm build then changeset publish, which publishes to npm with public access.
Prerelease (Alpha / Beta)
For testing unstable versions before an official release:
Enter Prerelease Mode
# Enter alpha prerelease
pnpm changeset pre enter alpha
# Or beta
pnpm changeset pre enter betaPublish Prerelease Versions
# 1. Add changeset as usual
pnpm changeset
# 2. Version — produces e.g. 0.2.0-beta.0, 0.2.0-beta.1, ...
pnpm version
# 3. Commit and publish
git add .
git commit -m "chore: release v0.2.0-beta.0"
pnpm releaseRepeat steps 1–3 to publish additional prerelease iterations (.beta.1, .beta.2, ...).
Exit Prerelease Mode
When the prerelease is stable and ready for official release:
pnpm changeset pre exitThen follow the normal version + release flow to publish the stable version.
Install a Prerelease Version
# Users install prerelease with the tag
npm install @lichenbuliren/flow-orchestrator@beta
npm install @lichenbuliren/flow-orchestrator@alphaQuick Reference
| Command | Description |
|---------|-------------|
| pnpm changeset | Add a new changeset |
| pnpm version | Bump version & update CHANGELOG |
| pnpm release | Build & publish to npm |
| pnpm changeset pre enter beta | Enter beta prerelease mode |
| pnpm changeset pre enter alpha | Enter alpha prerelease mode |
| pnpm changeset pre exit | Exit prerelease mode |
| pnpm release --dry-run | Preview publish without uploading |
Key Points
- Project-level
.npmrcsetsregistry=https://registry.npmjs.org/, overriding any global config .changeset/config.jsonsets"access": "public"for scoped package publishingprepackscript auto-runspnpm buildbefore publishingpublishConfiginpackage.jsonremaps entry points todist/- Only
dist/,package.json,CHANGELOG.md, andREADME.mdare included in the tarball (controlled by thefilesfield)
License
MIT
