A unified, type-safe, metadata-driven pipeline for capabilities (effects) and resources (leases) in modern TypeScript.
Built with Light Functional Programming principles — no classes, no decorators, no exceptions in core domain logic — just pure functions, immutable types, and a tiny executor with six phases:
validate → resolve → before → run → onError → after
This monorepo combines three ideas under one engine:
- fix (capability injection): declare what you need in
meta
; get typed ports inctx
. - effectfx (side-effects): HTTP, KV, DB, Queue, Time, Crypto, Log as Ports, host-agnostic.
- resourcefx (lifetimes): bracket + Lease<T, Scope> to guarantee allocation/release without leaks.
You only see the capabilities you declare in
meta
. The compiler enforces this at call sites with full type safety and branded domain types.
Inspired by @saltyAom's https://github.com/saltyaom/elysia-typegen-example.
Requirements: Deno 2+
deno task demo
You should see:
GET /users/123 → { id: "123", name: "Ada" }
fix/
packages/
core/ # executor, types, policy weaver
ports/ # type-only port surfaces
std/ # built-in macros & policies (http/kv/db/cache/schema/telemetry + retry/timeout/log/idempotency)
resources/ # bracket + Lease<T,Scope> + temp dir + simple pool
std/env.ts # Default in-memory host bindings for demos and tests
testing/ # fakes, chaos, leak detection helpers
examples/
deno/ # Deno.serve demo endpoint
docs/ # comprehensive documentation
deno.json # tasks
- One
meta
, oneCapsOf<M>
for both effects and resources. - One policy weaver applies
retry/timeout/idempotency/log
uniformly across all ports/openers. - Host-agnostic: extend the provided in-memory env (
packages/std/env.ts
) or supply your own factories for real hosts. - Testable: pass
@fix/testing
fakes into the executor; everything is functions.
Built with functional programming best practices for maintainable, type-safe code:
import { createEmail, createUserId, type Result } from "@fix/core";
// Smart constructors with branded types
const emailResult = createEmail("[email protected]");
const userIdResult = createUserId("user123");
// Result types for error handling (no exceptions)
if (emailResult.ok && userIdResult.ok) {
const email: Email = emailResult.value; // Branded type
const userId: UserId = userIdResult.value; // Different branded type
// email and userId are different types at compile time
}
// All data types are immutable with readonly properties
type User = {
readonly id: UserId;
readonly email: Email;
readonly createdAt: Date;
};
Core Principles:
- ✅ No classes in domain logic — Pure functions and immutable data
- ✅ Result types for errors — No exceptions, all errors are values
- ✅ Branded types — Prevent mixing incompatible values (Email ≠ UserId)
- ✅ Immutable data — All types use
readonly
properties - ✅ Smart constructors — Validation at type boundaries
- ✅ Ports pattern — Dependency injection through function parameters
Build meta declaratively with type-safe chaining:
import { meta } from "@fix/core";
const myMeta = meta()
.withDb("ro")
.withKv("users")
.withRetry(3, 100, true)
.withTimeout({ ms: 5000 })
.withLog("debug");
// Call .build() if you prefer to strip helper methods explicitly.
Compose steps into pipelines, parallel execution, and branches:
import { allSteps, branch, pipe, race } from "@fix/core";
// Sequential pipeline
const pipeline = pipe<Base>()(fetchUser, enrichProfile, cacheResult);
// Parallel execution
const parallel = allSteps<Base>()(fetchData, fetchOrders, fetchRecs);
// Pattern-matched branching with ts-pattern
const tiered = branch<"free" | "pro" | "enterprise", Base>(plan)
.with("free", freeTierStep)
.with("pro", proTierStep)
.otherwise(enterpriseTierStep);
Type-safe error handling without exceptions, with comprehensive error types:
import { err, map, matchResult, ok, type Result, withResult } from "@fix/core";
import { createStdEngine } from "@fix/std";
// Domain validation with specific error types
type ValidationError =
| { readonly type: "REQUIRED"; readonly field: string }
| { readonly type: "INVALID_EMAIL"; readonly field: string }
| {
readonly type: "TOO_SHORT";
readonly field: string;
readonly minLength: number;
};
// All operations return Result types
const safeStep = withResult<Base>()(riskyStep);
const engine = createStdEngine<Base>();
const base: Base = {/* ...request scoped data... */};
const result = await engine.run(safeStep, base);
// Pattern match on results
matchResult(
result,
(data) => console.log("Success:", data),
(error) => console.error("Error:", error),
);
// Schema macro now returns Result types too
const schemaResult = ctx.schema; // Result<{name: string, data: T}, SchemaError>
if (schemaResult.ok) {
const data = schemaResult.value.data; // Type-safe access
}
async run(ctx) {
// Telemetry spans
return await ctx.span("fetch-user", async (ctx) => {
return await ctx.kv.get(key);
});
// Request-scoped memoization
const cached = await ctx.memo("expensive", () => compute());
// Spawn child steps
const result = await ctx.child({ http: { baseUrl: "..." } }, apiStep);
}
Prevent invalid states with compile-time type safety:
import {
createEmail,
createStepName,
createUserId,
type DomainValidationError,
type Email,
type StepName,
type UserId,
} from "@fix/core";
// Smart constructors validate and return branded types
const emailResult = createEmail("[email protected]");
const userIdResult = createUserId("user123");
const stepNameResult = createStepName("fetch-user-data");
if (emailResult.ok && userIdResult.ok && stepNameResult.ok) {
const email: Email = emailResult.value;
const userId: UserId = userIdResult.value;
const stepName: StepName = stepNameResult.value;
// These are different types at compile time!
// email = userId; // ❌ Type error - cannot assign UserId to Email
// Available smart constructors:
// Email, UserId, StepName, MacroKey, CircuitName,
// LockKey, CacheKey, KvNamespace, QueueName
}
// Validation errors are typed and descriptive
if (!emailResult.ok) {
const error: DomainValidationError = emailResult.error;
console.log(error.type); // "INVALID_EMAIL" | "REQUIRED" | "TOO_LONG"
console.log(error.message); // "Email must contain @ symbol"
}
import { assertValidStep, validateStep } from "@fix/core";
// Helpful errors with suggestions
// [UNKNOWN_CAPABILITY] Step declares 'redis' but no matching macro registered
// 💡 Did you mean 'kv'? Add the corresponding macro to your macros array
- Opt into structured step/macro telemetry with
meta().withTelemetry()
. Combine it with the shipped console logger or your own collector. - Layer in-memory result caching via
meta().withCache(key, ttlMs)
and manage entries withclearCache()
/getCacheStats()
. - Validate upstream data declaratively using
createSchemaMacro()
paired withmeta().withSchema(...)
to inject typed results intoctx.schema
.
examples/deno/api.ts
— HTTP-like example with cache + db + retry + timeoutexamples/advanced/multi-resource.ts
— Nested leases with finalisersexamples/advanced/policy-combo.ts
— Retry + timeout + circuit breakerexamples/testing/with-fakes.test.ts
— Using@fix/testing
fakes
examples/data/cache-hit.ts
— In-memory result caching viameta().withCache()
plus access toclearCache()
/getCacheStats()
.examples/data/telemetry-console.ts
— Structured step/macro telemetry withmeta().withTelemetry()
and the built-in console logger.examples/data/schema-validate.ts
— Declarative fetch + validation usingcreateSchemaMacro()
andmeta().withSchema()
.
examples/composition/pipeline.ts
— Sequential pipeline withpipe()
examples/composition/parallel.ts
— Parallel execution withallSteps()
examples/composition/branching.ts
— Pattern-matched branching with ts-patternexamples/result-based/error-handling.ts
— Result type with stepsexamples/result-based/chaining.ts
— Result combinatorsexamples/builder/fluent-meta.ts
— Meta builder usageexamples/builder/meta-composition.ts
— Composing meta objects
fakeLogger()
captures structured logs for assertionsfakeKv()
/fakeHttp()
/fakeTime()
provide deterministic portswithChaos()
wraps ports with configurable failure/latency injectioncreatePolicyTracker()
tracks and asserts on policy executionsnapshotPort()
records all port interactions for testing
- Overview - High-level concepts and architecture
- Concepts - Core terminology and patterns
- API Reference - Detailed API documentation
- Architecture - System design and execution flow
- Light FP Guide - Functional programming principles and implementation
- Testing Guide - Testing strategies and utilities
- Policies - Policy configuration and composition
- Examples Guide - Detailed example walkthroughs
- Ergonomic Enhancements - New features guide
This codebase follows Light Functional Programming principles for maximum maintainability and type safety:
- ✅ A- Grade (92/100) Light FP compliance
- ✅ Zero classes in core domain logic
- ✅ Zero exceptions in business logic
- ✅ 100% immutable data types with
readonly
properties - ✅ Comprehensive Result types for error handling
- ✅ Smart constructors with branded types for domain safety
- ✅ 60+ tests covering core functionality
- Keep code here as a mono-source of truth.
- Publish packages as:
@fix/core
,@fix/std
,@fix/ports
,@fix/resources
,@fix/testing
.
MIT — see LICENSE.