Skip to content

Transformer

A Transformer is the core component that processes transmissions and contains your business logic. Transformers receive Signals and Effects, process them, and produce Data or additional Effects.

Basic Structure

class MyTransformer : Transformer() {

    override val handlers: Handlers = handlers {
        // Define signal and effect handlers
    }

    override val computations: Computations = computations {
        // Define computations for inter-transformer communication
    }

    override val executions: Executions = executions {
        // Define executions for fire-and-forget operations
    }
}

Constructor Parameters

open class Transformer(
    identity: Contract.Identity = Contract.identity(),
    dispatcher: CoroutineDispatcher = Dispatchers.Default,
    private val capacity: Capacity = Capacity.Default
)
  • identity: Unique identifier for the transformer (auto-generated if not provided)
  • dispatcher: Coroutine dispatcher for processing (defaults to Dispatchers.Default)
  • capacity: Buffer capacity for internal channels

Handlers

Handlers define how your Transformer responds to incoming Signals and Effects.

Signal Handlers

override val handlers: Handlers = handlers {
    onSignal<UserLoginSignal> { signal ->
        // Process the login signal
        val result = authenticateUser(signal.credentials)

        if (result.isSuccess) {
            send(UserData(result.user))
            publish(NavigationEffect.GoToHome)
        } else {
            send(ErrorData("Login failed"))
        }
    }
}

Effect Handlers

override val handlers: Handlers = handlers {
    onEffect<RefreshDataEffect> { effect ->
        // Handle refresh effect
        val freshData = fetchFreshData()
        send(DataRefreshedData(freshData))
    }
}

Complete Example from Counter Sample

class Worker(val id: String) : Transformer() {

    override val handlers: Handlers = handlers {
        onSignal<CounterSignal.Lookup> {
            send(CounterData("Transformer $id updated data to ${compute(lookUpAndReturn, id)}"))
        }
    }
}

Communication Scope

Within handler lambdas, you have access to CommunicationScope which provides:

Sending Data

// Send data to the router's data stream
send(UserData(user))

Publishing Effects

// Publish effect to other transformers
publish(LoggingEffect("User logged in"))

// Publish effect to specific transformer
publish(
    effect = NotificationEffect("Welcome!"),
    identity = notificationTransformerIdentity
)

Query Other Transformers

// Compute value from another transformer
val userData = compute(userDataContract)

// Compute with arguments
val validationResult = compute(validationContract, inputData)

Execute Operations

// Fire-and-forget execution
execute(cleanupContract)

// Execute with arguments
execute(logContract, "User action performed")

Data Holders

Data Holders provide state management within Transformers:

class UserTransformer : Transformer() {

    // Create a data holder with initial state
    private val userHolder = dataHolder(
        initialValue = UserState(),
        contract = userStateContract
    )

    override val handlers: Handlers = handlers {
        onSignal<UpdateUserSignal> { signal ->
            // Update the held state
            userHolder.update { currentState ->
                currentState.copy(name = signal.newName)
            }

            // The updated state is automatically sent as Data
        }

        onSignal<GetUserSignal> {
            // Access current state
            val currentUser = userHolder.getValue()
            send(CurrentUserData(currentUser))
        }
    }
}

Data Holder Features

  • Automatic Data Publishing: Updates are automatically sent to the data stream
  • Thread-Safe: Updates are synchronized
  • State Tracking: Previous states are tracked for debugging

Creating Data Holders

// Basic data holder
val holder = dataHolder(initialValue = MyState(), contract = myContract)

// Data holder without auto-publishing  
val holder = dataHolder(
    initialValue = MyState(), 
    contract = myContract,
    publishUpdates = false
)

Computations

Computations enable inter-transformer communication for retrieving data:

class CalculatorTransformer : Transformer() {

    override val computations: Computations = computations {
        // Simple computation
        register(currentValueContract) {
            getCurrentCalculatorValue()
        }

        // Computation with arguments
        register(calculateContract) { input: CalculationInput ->
            performCalculation(input)
        }

        // Cached computation (result is cached)
        register(expensiveCalculationContract) {
            expensiveOperation()
        }
    }
}

Using Computations

override val handlers: Handlers = handlers {
    onSignal<DisplayResultSignal> {
        // Query another transformer's computation
        val currentValue = compute(currentValueContract)
        send(DisplayData(currentValue))
    }

    onSignal<PerformCalculationSignal> { signal ->
        // Query with arguments
        val result = compute(calculateContract, signal.input)
        send(CalculationResultData(result))
    }
}

Executions

Executions are fire-and-forget operations for side effects:

class LoggingTransformer : Transformer() {

    override val executions: Executions = executions {
        // Simple execution
        register(logMessageContract) {
            writeToLogFile("Message logged")
        }

        // Execution with arguments
        register(logWithLevelContract) { logEntry: LogEntry ->
            writeToLogFile(logEntry.level, logEntry.message)
        }
    }
}

Using Executions

override val handlers: Handlers = handlers {
    onSignal<UserActionSignal> { signal ->
        // Fire-and-forget logging
        execute(logMessageContract)

        // With arguments
        execute(logWithLevelContract, LogEntry(LogLevel.INFO, "User action: ${signal.action}"))

        // Continue with main logic
        send(ActionCompletedData(signal.action))
    }
}

Advanced Features

Checkpoint Tracking (Experimental)

Checkpoints enable debugging and flow control:

@OptIn(ExperimentalTransmissionApi::class)
class ComplexTransformer : Transformer() {

    override val handlers: Handlers = handlers {
        onSignal<ProcessDataSignal> { signal ->
            // Pause execution at checkpoint
            val validatedData = pauseOn(validationCheckpoint)

            // Continue processing
            send(ProcessedData(validatedData))
        }

        onEffect<ValidationCompleteEffect> { effect ->
            // Validate checkpoint with result
            validate(validationCheckpoint, effect.validatedData)
        }
    }

    companion object {
        val validationCheckpoint = Contract.checkpointWithArgs<ValidatedData>()
    }
}

Custom Error Handling

class SafeTransformer : Transformer() {

    override fun onError(throwable: Throwable) {
        // Handle errors that occur in this transformer
        publish(ErrorEffect("Transformer error: ${throwable.message}"))
    }

    override val handlers: Handlers = handlers {
        onSignal<RiskyOperationSignal> { signal ->
            try {
                val result = riskyOperation(signal.data)
                send(SuccessData(result))
            } catch (e: Exception) {
                send(ErrorData(e.message))
            }
        }
    }
}

Lifecycle Management

class ResourceTransformer : Transformer() {

    private val database = openDatabase()

    override fun onCleared() {
        // Cleanup when transformer is cleared
        database.close()
        super.onCleared()
    }
}

Complete Example from Components Sample

class InputTransformer(
    private val defaultDispatcher: CoroutineDispatcher
) : Transformer(dispatcher = defaultDispatcher) {

    private val holder = dataHolder(InputUiState(), holderContract)

    override val computations: Computations = computations {
        register(writtenInputContract) {
            delay(1.seconds)
            WrittenInput(holder.getValue().writtenText)
        }
        register(writtenInputWithArgs) {
            WrittenInput(it)
        }
    }

    @OptIn(ExperimentalTransmissionApi::class)
    override val handlers: Handlers = handlers {
        onSignal<InputSignal.InputUpdate> { signal ->
            holder.update { it.copy(writtenText = signal.value) }
            val color = pauseOn(colorCheckpoint)
            send(
                effect = ColorPickerEffect.SelectedColorUpdate(color),
                identity = multiOutputTransformerIdentity
            )
            publish(effect = InputEffect.InputUpdate(signal.value))
        }
        onEffect<ColorPickerEffect.BackgroundColorUpdate> { effect ->
            validate(colorCheckpoint, effect.color)
            holder.update { it.copy(backgroundColor = effect.color) }
        }
    }

    companion object {
        val writtenInputWithArgs = Contract.computationWithArgs<String, WrittenInput>()
        val writtenInputContract = Contract.computation<WrittenInput>()
        val holderContract = Contract.dataHolder<InputUiState>()
        val colorCheckpoint = Contract.checkpointWithArgs<Color>()
    }
}

Best Practices

1. Single Responsibility

Keep each Transformer focused on a specific domain:

// Good - focused on user authentication
class AuthTransformer : Transformer()

// Good - focused on data caching  
class CacheTransformer : Transformer()

// Avoid - mixed responsibilities
class AuthAndCacheTransformer : Transformer()

2. Immutable State

Use immutable data classes for state:

data class UserState(
    val user: User? = null,
    val isLoading: Boolean = false,
    val error: String? = null
)

3. Error Handling

Handle errors gracefully:

onSignal<LoadDataSignal> { signal ->
    try {
        val data = loadData(signal.id)
        send(DataLoadedData(data))
    } catch (e: Exception) {
        send(ErrorData("Failed to load data: ${e.message}"))
    }
}

4. Use Contracts for Communication

Define contracts for inter-transformer communication:

companion object {
    val userDataContract = Contract.computation<UserData>()
    val validateUserContract = Contract.computationWithArgs<User, Boolean>()
}