Jerelo

Jerelo is a Dart library for building cold, lazy, reusable computations using continuation-passing style. It provides a composable Cont type with three outcome channels — success, typed errors, and crashes — plus operations for chaining, transforming, branching, merging, and error handling.

The name "Jerelo" is Ukrainian for "source" or "spring" - each Cont is a source of results that can branch, merge, filter, and transform data through your workflows.

What Makes Jerelo Different?

Unlike Dart's Future which starts executing immediately upon creation, Cont is:

  • Cold: Doesn't run until you explicitly call run
  • Pure: No side effects during construction
  • Lazy: Evaluation is deferred
  • Reusable: Can be safely executed multiple times with different configurations
  • Composable: Build complex flows from simple, testable pieces
  • Cancellable: run returns a token whose cancel() is forwarded to the running computations, which decide how to stop

This makes Cont ideal for defining complex business logic that needs to be tested, reused across different contexts, or configured at runtime.

Quick Example

import 'package:jerelo/jerelo.dart';

// Define a computation (doesn't execute yet)
final getUserData = Cont.of<AppConfig, String, String>(userId)
  .thenDo((id) => fetchUserFromApi(id))
  .thenDo<User>((user) => user.isActive ? Cont.of(user) : Cont.error('User is inactive'))
  .elseDo((_) => loadUserFromCache(userId))
  .thenMap((user) {
    logAccess(user);
    return user;
  });

// Execute it multiple times with different configs
getUserData.run(prodConfig, onElse: handleError, onThen: handleSuccess);
getUserData.run(testConfig, onElse: handleError, onThen: handleSuccess);

What is Cont?

final class Cont<E, F, A>

A continuation monad representing a computation that will eventually produce a value of type A, terminate with a typed error of type F, or crash with an unexpected exception.

Type Parameters:

  • E: The environment type providing context for the continuation execution
  • F: The error type for expected, typed failures on the else channel
  • A: The value type that the continuation produces upon success

Design Goals

  • Pure Dart - No platform dependencies
  • Zero dependencies - Minimal footprint
  • Minimal API - Learn once, use everywhere
  • Modular boundaries - Swap HTTP/DB/cache/logging without rewriting flows
  • Execution agnostic - Same flow works sync, async, and in tests
  • Explicit failures - Unexpected throws become explicit termination
  • Extensible - Write your own operators on the public API

Documentation

User Guide

A comprehensive guide to understanding and using Jerelo:

  1. Introduction & Core Concepts - Understanding computations and continuation-passing style
  2. Fundamentals: Construct & Run - Learn to create and execute computations
  3. Composing Computations - Master mapping, chaining, branching, and error handling
  4. Racing and Merging - Parallel execution, racing, and complex workflows
  5. Environment Management - Configuration and dependency injection patterns
  6. Extending Jerelo - Create custom operators and computations
  7. Complete Examples - Real-world patterns and use cases

API Reference

Complete reference for all public APIs:

  • Types - ContCrash, CrashOr, BracketOutcome, ContCancelToken, ContObserver
  • Construction - Creating continuations: fromRun, of, error, crash, askThen, askElse, fromDeferred, bracket, decorate
  • Execution - Running continuations: run, runWith, cancellation; extensions: flatten, thenAbsurd, elseAbsurd, thenUnit, elseUnit
  • Then Channel - Success path operations: mapping, chaining, tapping, zipping, forking, demotion, conditionals, loops
  • Else Channel - Error path operations: fallback, transformation, tapping, zipping, forking, promotion, conditionals, retry loops
  • Crash Channel - Crash recovery operations: recovery, side effects, zipping, forking, recovery to then or else, conditionals, retry loops
  • Combining - Parallel execution: OkPolicy, CrashPolicy, both, all, either, any, coalesce, converge, and, or, coalesceWith
  • Environment - Environment management: local, local0, withEnv, thenInject, injectedByThen, elseInject, injectedByElse

Most operators come with variants that ignore the value (thenMap0), read the environment (thenDoWithEnv, thenDoWithEnv0) or use a constant (thenMapTo). You can write your own operators on the public API in the same way (see Extending Jerelo).

Full Example

Here's a complete example demonstrating key Jerelo features:

import 'dart:async';
import 'package:jerelo/jerelo.dart';

// Configuration injected as environment
class AppConfig {
  final String apiUrl;
  final Duration timeout;
  final bool enableCache;

  AppConfig(this.apiUrl, this.timeout, this.enableCache);
}

// Domain model
class User {
  final String id;
  final String name;
  final bool isActive;

  User(this.id, this.name, this.isActive);
}

// Simulated API call
Cont<AppConfig, String, User> fetchUserFromApi(String userId) {
  return Cont.askThen<AppConfig, String>().thenDo((config) {
    return Cont.fromRun((env, observer) {
      // Simulate async API call
      final timer = Timer(config.timeout, () {
        // Simulate successful response
        observer.onThen(User(userId, 'John Doe', true));
      });
      // Cancelling stops the call and reports it as an error
      return () {
        timer.cancel();
        observer.onElse('Request cancelled');
      };
    });
  });
}

// Simulated cache fallback
Cont<AppConfig, String, User> loadUserFromCache(String userId) {
  return Cont.of(User(userId, 'Cached User', true));
}

// Simulated logging side effect
void logAccess(User user) {
  print('[LOG] User accessed: ${user.name}');
}

// Build a computation pipeline
Cont<AppConfig, String, User> getUserData(String userId) {
  return fetchUserFromApi(userId)
    // Filter: only proceed if user is active, with custom error
    .thenDo<User>((user) {
      return user.isActive
        ? Cont.of(user)
        : Cont.error('User account is not active');
    })
    // Fallback: if API fails or user inactive, try cache
    .elseDo((error) {
      return Cont.askThen<AppConfig, String>().thenDo<User>((config) {
        return config.enableCache
          ? loadUserFromCache(userId)
          : Cont.error(error);
      });
    })
    // Side effect: log access and pass the value through
    .thenMap((user) {
      logAccess(user);
      return user;
    })
    // Recover unexpected exceptions into typed errors
    .crashDo((crash) => Cont.error('Unexpected error: $crash'));
}

// Fetch multiple users in parallel
Cont<AppConfig, String, List<User>> getMultipleUsers(List<String> userIds) {
  final computations = userIds.map(getUserData).toList();

  return Cont.all(
    computations,
    policy: OkPolicy.quitFast(), // Fail fast on first error
  );
}

void main() {
  // Production configuration
  final prodConfig = AppConfig(
    'https://api.example.com',
    Duration(seconds: 5),
    true,
  );

  // Test configuration
  final testConfig = AppConfig(
    'http://localhost:3000',
    Duration(milliseconds: 100),
    false,
  );

  print('=== Single User Example ===');

  // Define the computation once
  final singleUserFlow = getUserData('user123');

  // Execute with production config
  singleUserFlow.run(
    prodConfig,
    onCrash: (crash) => print('Production crash: $crash'),
    onElse: (error) => print('Production failed: $error'),
    onThen: (user) => print('Production success: ${user.name}'),
  );

  // Execute the same computation with test config
  singleUserFlow.run(
    testConfig,
    onElse: (error) => print('Test failed: $error'),
    onThen: (user) => print('Test success: ${user.name}'),
  );

  print('\n=== Multiple Users Example ===');

  // Fetch multiple users in parallel
  final multiUserFlow = getMultipleUsers(['user1', 'user2', 'user3']);

  multiUserFlow.run(
    prodConfig,
    onElse: (error) => print('Failed to fetch users: $error'),
    onThen: (users) => print('Fetched ${users.length} users: ${users.map((u) => u.name).join(', ')}'),
  );

  print('\n=== Advanced: Racing API vs Cache ===');

  // Race API against cache, return whichever succeeds first
  // (a failure waits for the other source)
  final racingFlow = Cont.either(
    fetchUserFromApi('user456'),
    loadUserFromCache('user456'),
    (e1, e2) => '$e1; $e2', // Combine errors if both fail
    policy: OkPolicy.quitFast(), // Return first success
  );

  racingFlow.run(
    prodConfig,
    onElse: (error) => print('Both sources failed: $error'),
    onThen: (user) => print('Got user from fastest source: ${user.name}'),
  );
}

Key Features Demonstrated

The example above showcases:

  • Three-channel design: Success (then), typed errors (else), and crashes for unexpected exceptions
  • Environment Management: Configuration threaded through computations via AppConfig
  • Chaining: Sequential operations with thenDo
  • Error Handling: Fallbacks with elseDo, conditional caching that reads the environment with Cont.askThen
  • Crash Recovery: Converting unexpected exceptions into typed errors with crashDo
  • Conditional Logic: Filtering with thenDo that returns Cont.of or a typed Cont.error
  • Side Effects: Logging inside thenMap while passing the original value through
  • Parallel Execution: Multiple computations with Cont.all and OkPolicy
  • Racing: Competing computations with Cont.either and error combining
  • Cancellation: fetchUserFromApi returns a canceller that stops its timer, so the quitFast race cancels the API call once the cache wins
  • Reusability: Same computation executed with different configurations

License

See LICENSE file for details.

Libraries

jerelo
A continuation monad library for Dart.