Logo Xantham

Signal Architecture in Xantham.Fable

The signal architecture in Xantham.Fable is a core component that enables reactive data flow throughout the TypeScript AST processing pipeline. This architecture is crucial for managing the complex dependencies and computations that occur during type extraction and transformation.

Overview

Xantham.Fable uses a sophisticated signal-based reactive system that provides automatic dependency tracking, lazy evaluation, and efficient invalidation propagation. The system is designed to handle the complex data dependencies that naturally arise when processing large TypeScript ASTs.

Signal Type Definition

The core Signal<'a> type is defined in Types/Signal.fs and provides:

flowchart TD subgraph Signal_System signal[Signal<'a>] --> source[Source Signal] signal --> computed[Computed Signal] signal --> auto[Auto Signal] end subgraph Signal_Operations source --> set[Set Value] source --> get[Get Value] computed --> deps[Dependency Tracking] computed --> recalc[Recompute] auto --> track[Automatic Tracking] auto --> invalidation[Invalidation] get --> cache[Value Caching] end subgraph Dependency_Management deps --> collector[Dependency Collector] collector --> deps2[Dependency Discovery] deps2 --> invalidation invalidation --> propagation[Propagation] end subgraph Reactive_Flow input[Input Data] --> source source --> cache cache --> computed computed --> output[Output Data] end

Key Features

  1. Dual Nature:
    • Source signals: hold plain mutable values, updated imperatively
    • Computed signals: values derived from other signals via thunks
  2. Dependency Management:
    • Explicit dependency tracking (computed signals)
    • Automatic dependency tracking (auto signals)
    • Transitive dirty propagation
  3. Lazy Evaluation:
    • Computed values are cached and only re-computed when needed
    • Dirty state determines when recomputation is required

Signal Constructors

Source Signals

static member Source(value: 'a) : Signal<'a>

Creates a source signal with an initial value. These are the "roots" of the reactive system.

Computed Signals

static member Computed(compute: unit -> 'a, deps: IEvent<unit> list) : Signal<'a>

Creates a computed signal that re-evaluates when any provided dependencies fire.

Auto Signals

static member Auto(thunk: unit -> 'a) : Signal<'a>

Creates a computed signal with automatic dependency discovery through a tracking scope.

Core Operations

Value Access

member this.Value : 'a

Returns the current value, re-computing and caching it if dirty. This operation also registers dependencies in auto-mode.

Setting Values

member _.Set(v: 'a)

Sets the value of a source signal and fires invalidation events.

Fulfillment

member this.FulfillWith(thunk: unit -> 'a) : unit

Retrofits a source signal with a reactive thunk, converting it into a self-updating signal.

The Tracking Scope System

The key to automatic dependency tracking is the module-level collector that allows signals to register as dependencies:

let mutable internal collector: (IEvent<unit> -> unit) option = None

Auto Dependency Tracking Workflow

  1. Prepare scope: collector is set to a tracking function
  2. Execute thunk: Thunk runs within the tracking scope
  3. Collect dependencies: Any .Value access is recorded
  4. Restore scope: Original collector value is restored
  5. Create signal: Computed signal created with discovered dependencies

Practical Usage in Xantham

In Type Processing

The signal system is extensively used during type declaration processing where complex type relationships need to be tracked:

// Example from Read.fs
let rec healthCheckType (typKey: TypeKey) (node: TsType) =
    // Various type checking logic that might access other signals
    match node with
    | TsType.TypeReference tsTypeReference ->
        if typKey = tsTypeReference.Type then Log.healthCheckError typKey tsTypeReference tsTypeReference

In Duplicate Handling

Signals are used to manage and resolve type duplicates during processing:

let private finaliseAssembly (results: IRResult<'T> array) =
    // Uses signal-based tracking for duplicate handling
    let duplicates, nonDuplicates =
        results
        |> Array.groupBy _.Key
        |> Array.map (fun (key, values) -> key, values |> Array.distinctBy _.Node)
        |> Array.partition (snd >> _.Length >> (<) 1)

Benefits of This Approach

1. Efficient Memory Management

2. Clean Separation of Concerns

3. Complex Dependency Resolution

4. Extensibility

Advanced Patterns

Pending Signals

let pending<'a>(): Signal<'a voption>

Used for signals that are initialized with ValueNone and later filled with actual values.

Signal Effects

let effect (action: unit -> unit) (deps: IEvent<unit> list) : System.IDisposable

Allows running side-effect operations when signals change, useful for logging or debugging.

Signal Mapping

let map (f: 'a -> 'b) (s: Signal<'a>) : Signal<'b>
let map2 (f: 'a -> 'b -> 'c) (a: Signal<'a>) (b: Signal<'b>) : Signal<'c>

Transformation functions that create new computed signals from existing ones.

Integration with AST Processing

The signal architecture integrates seamlessly with the AST processing pipeline:

  1. Tag creation: Each AST node gets associated with a XanthamTag that can hold signal data
  2. Type evaluation: Complex type resolution happens through computed signals
  3. Cross-referencing: Type dependencies are tracked through signal relationships
  4. Duplication handling: Duplicate type resolution uses signal dependency tracking
  5. Final output: All processed types are assembled through signal-computed values

This signal architecture provides the foundation for Xantham.Fable's robust handling of complex TypeScript constructs and ensures that type relationships are correctly managed even in large, complex codebases.

type unit = Unit
type IEvent<'T> = IEvent<Handler<'T>,'T>
type 'T list = List<'T>
Multiple items
module Set from Microsoft.FSharp.Collections

--------------------
type Set<'T (requires comparison)> = interface IReadOnlyCollection<'T> interface IStructuralEquatable interface IComparable interface IEnumerable interface IEnumerable<'T> interface ICollection<'T> new: elements: 'T seq -> Set<'T> member Add: value: 'T -> Set<'T> member Contains: value: 'T -> bool member IsProperSubsetOf: otherSet: Set<'T> -> bool ...

--------------------
new: elements: 'T seq -> Set<'T>
type 'T option = Option<'T>
union case Option.None: Option<'T>
type 'T array = 'T array
module Array from Microsoft.FSharp.Collections
val groupBy: projection: ('T -> 'Key) -> array: 'T array -> ('Key * 'T array) array (requires equality)
val map: mapping: ('T -> 'U) -> array: 'T array -> 'U array
val distinctBy: projection: ('T -> 'Key) -> array: 'T array -> 'T array (requires equality)
val partition: predicate: ('T -> bool) -> array: 'T array -> 'T array * 'T array
val snd: tuple: ('T1 * 'T2) -> 'T2
type 'T voption = ValueOption<'T>
namespace System
type IDisposable = override Dispose: unit -> unit
<summary>Provides a mechanism for releasing unmanaged resources.</summary>

Type something to start searching.