Skip to content
/ fix Public

-fix- is a TypeScript library that lets you declare the effects and resources your code needs in a single meta object, then executes each step through a reusable engine.

License

Notifications You must be signed in to change notification settings

srdjan/fix

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fix

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 in ctx.
  • 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.


Quick start (Deno)

Requirements: Deno 2+

deno task demo

You should see:

GET /users/123 → { id: "123", name: "Ada" }

Monorepo layout

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, one CapsOf<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.

Key Features

🎯 Light Functional Programming

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

🔧 Ergonomic Meta Builder

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.

🔄 Step Composition

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);

✅ Result Type for Error Handling

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
}

🎯 Enhanced Context Helpers

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);
}

🏷️ Smart Constructors & Branded Types

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"
}

🛡️ Better Validation & Error Messages

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

🌐 Observability & Data Helpers

  • 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 with clearCache() / getCacheStats().
  • Validate upstream data declaratively using createSchemaMacro() paired with meta().withSchema(...) to inject typed results into ctx.schema.

Examples

Core Examples

  • examples/deno/api.ts — HTTP-like example with cache + db + retry + timeout
  • examples/advanced/multi-resource.ts — Nested leases with finalisers
  • examples/advanced/policy-combo.ts — Retry + timeout + circuit breaker
  • examples/testing/with-fakes.test.ts — Using @fix/testing fakes

Data Helpers & Telemetry

  • examples/data/cache-hit.ts — In-memory result caching via meta().withCache() plus access to clearCache() / getCacheStats().
  • examples/data/telemetry-console.ts — Structured step/macro telemetry with meta().withTelemetry() and the built-in console logger.
  • examples/data/schema-validate.ts — Declarative fetch + validation using createSchemaMacro() and meta().withSchema().

New Ergonomic Examples

  • examples/composition/pipeline.ts — Sequential pipeline with pipe()
  • examples/composition/parallel.ts — Parallel execution with allSteps()
  • examples/composition/branching.ts — Pattern-matched branching with ts-pattern
  • examples/result-based/error-handling.ts — Result type with steps
  • examples/result-based/chaining.ts — Result combinators
  • examples/builder/fluent-meta.ts — Meta builder usage
  • examples/builder/meta-composition.ts — Composing meta objects

Testing helpers

  • fakeLogger() captures structured logs for assertions
  • fakeKv() / fakeHttp() / fakeTime() provide deterministic ports
  • withChaos() wraps ports with configurable failure/latency injection
  • createPolicyTracker() tracks and asserts on policy execution
  • snapshotPort() records all port interactions for testing

Documentation

Code Quality & Best Practices

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

Publishing strategy

  • Keep code here as a mono-source of truth.
  • Publish packages as: @fix/core, @fix/std, @fix/ports, @fix/resources, @fix/testing.

License

MIT — see LICENSE.

About

-fix- is a TypeScript library that lets you declare the effects and resources your code needs in a single meta object, then executes each step through a reusable engine.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published