Skip to content

Qard/execution-flow

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ExecutionFlow — Cross-Language Context & Pub/Sub

ExecutionFlow is a cross-language library that connects structured pub/sub (Topics) with scoped context propagation (Contexts). It is implemented idiomatically in eight languages while preserving the same conceptual contract across all of them.

Why ExecutionFlow?

Most application frameworks either give you global state (bad) or require you to thread a context object through every function call (tedious). ExecutionFlow offers a third path:

  1. Topic: A named publish/subscribe channel. Publish an event once; all interested parties receive it.
  2. Context: Scoped key-value storage that follows the execution flow — whether that's an async task, a goroutine's context.Context, or a thread-local stack frame.
  3. Binding: Declare that when a Topic fires, a Context value is automatically derived from the event payload and injected into the current execution scope. Every subscriber (and all code that runs within the scope) sees that value without needing it passed explicitly.

This pattern is ideal for cross-cutting concerns: request IDs, user identities, trace spans, feature flags — anything that describes what is executing right now rather than what to compute.

Core Model

Topic<Msg>  ──bind──>  Context<T>  (via transform: Msg → T)
     │
  publish(msg)
     │
  subscribers notified with enriched context
     └── Context<T>.get() returns transform(msg) during the call

Key Methods

Topic

Method Description
topic(name) / NewTopic[T](name) Get or create a Topic by name
topic.subscribe(fn) Register a subscriber; returns a handle to unsubscribe
topic.publish(msg) Fire all subscribers; panics/exceptions are isolated per-subscriber
topic.withValue(msg) Populate bound Contexts, publish, return scope handle + enriched ctx
topic.runWithValue(msg, fn) Sugar: withValue + call fn + close scope
topic.bindContext(ctx, transform) Declare that publishing msg should set context to transform(msg)

Context

Method Description
new Context<T>(name) Create a named context store
context.withValue(val) Push val onto the scope stack for the current execution
context.runWithValue(val, fn) Sugar: withValue + call fn + pop
context.clear() Push a "no value" sentinel — hides any outer value
context.runClear(fn) Sugar: clear + call fn + pop
context.get() Return the current value, or empty/nil if not set
context.snapshot() Capture current state as a Snapshot object for re-entry later

Snapshot

Method Description
context.snapshot() Capture the current context state into a Snapshot
snapshot.withValue() Re-enter the snapshotted scope; returns a scope handle
snapshot.runWithValue(fn) Sugar: withValue + call fn + close scope

Execution Order for topic.withValue(msg)

  1. For each bound Context (in registration order): context.withValue(transform(msg))
  2. publish(msg) — subscribers fire inside the enriched scope
  3. Returns control to caller — scope remains active until closed

Quick Start

# Run all tests
make test-all

# Per language
make test-typescript
make test-python
make test-go
make test-java
make test-csharp
make test-ruby
make test-rust
make test-swift

Language Packages

Language Package README
TypeScript / Node.js packages/typescript README
Python packages/python README
Go packages/go README
Java packages/java README
C# packages/csharp README
Ruby packages/ruby README
Rust packages/rust README
Swift packages/swift README

Cross-Language Async Propagation

ExecutionFlow uses each language's idiomatic async-context mechanism:

  • TypeScriptAsyncLocalStorage automatically propagates across await and Promise.then()
  • Pythoncontextvars.ContextVar automatically copies into asyncio.create_task() children
  • Go — uses explicit context.Context threading (Go has no implicit ambient context); pass the enriched context.Context to downstream functions and goroutines
  • JavaInheritableThreadLocal automatically copies the context stack into new Thread instances at creation time; use Snapshot for thread-pool scenarios where threads are reused
  • C#AsyncLocal<T> automatically propagates into async/await continuations
  • RubyFiber.prepend copies the context hash into new Fiber instances at initialization; use Snapshot to transfer context into pre-existing fibers
  • Rusttokio::task_local! ensures context follows the task across thread migrations on multi-threaded executors; use Snapshot::run_with_value for explicit cross-task propagation via tokio::spawn
  • Swift — uses @TaskLocal; automatically propagates to child Task { } closures

Requirements

Language Minimum version
TypeScript / Node.js Node.js 22+, TypeScript 5.2+
Python 3.11+
Go 1.21+
Java 21+ (JDK)
C# .NET 8+
Ruby 3.2+
Rust 1.75+ (with tokio)
Swift 5.9+ (structured concurrency)

Reference Docs

About

Cross-language implementations of diagnostics_channel and AsyncLocalStorage

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors