All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.io.github.xlopec.tea.time.travel.component.Component.kt Maven / Gradle / Ivy

Go to download

TEA Bag is simple implementation of TEA written in Kotlin. Tea-time-travel is part of this project

The newest version!
/*
 * MIT License
 *
 * Copyright (c) 2022. Maksym Oliinyk.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

@file:Suppress("FunctionName", "KDocUnresolvedReference")

package io.github.xlopec.tea.time.travel.component

import io.github.xlopec.tea.core.*
import io.github.xlopec.tea.data.RandomUUID
import io.github.xlopec.tea.time.travel.component.internal.mergeWith
import io.github.xlopec.tea.time.travel.protocol.*
import io.github.xlopec.tea.time.travel.session.*
import io.ktor.http.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.RENDEZVOUS
import kotlinx.coroutines.flow.*

/**
 * Creates new debuggable [component][Component]
 *
 * @param id component identifier
 * @param initializer initializer that provides initial values
 * @param resolver resolver that resolves messages to commands and performs side effects
 * @param updater updater that computes new states and commands to be executed
 * @param jsonSerializer json converter
 * @param scope scope in which the sharing coroutine is started
 * @param url url used to connect to debug server
 * @param shareOptions sharing options, see [shareIn][kotlinx.coroutines.flow.shareIn] for more info
 * @param sessionFactory function that for a given server settings creates a new connection
 * to a debug server
 * @param M incoming messages
 * @param S state of the application
 * @param C commands to be executed
 */
public inline fun  Component(
    id: ComponentId,
    noinline initializer: Initializer,
    noinline resolver: Resolver,
    noinline updater: Updater,
    scope: CoroutineScope,
    // todo: group to reduce number of arguments
    url: Url = Localhost,
    jsonSerializer: JsonSerializer,
    // see https://youtrack.jetbrains.com/issue/KT-47195
    // see https://github.com/Kotlin/kotlinx.coroutines/issues/3005#issuecomment-1014577573
    noinline sessionFactory: SessionFactory = { settings, block -> HttpClient.session(settings, block) },
    shareOptions: ShareOptions = ShareStateWhileSubscribed,
): Component =
    Component(
        DebugEnv(
            Env(initializer, resolver, updater, scope, shareOptions),
            Settings(id, jsonSerializer, url, sessionFactory)
        )
    )

/**
 * Creates new component using preconfigured debug environment
 *
 * @param debugEnv environment to be used
 * @param M incoming messages
 * @param S state of the application
 * @param C commands to be executed
 */
public fun  Component(
    debugEnv: DebugEnv,
): Component {

    val input = Channel(RENDEZVOUS)
    val upstream = debugEnv.computeSnapshots(input)
        .shareIn(debugEnv.env.scope, debugEnv.env.shareOptions)

    return { messages -> upstream.attachMessageCollector(messages, input::send) }
}

private fun  DebugEnv.computeSnapshots(
    input: Channel,
): Flow> =
    debugSession { sink ->
        env.computeSnapshots(mergeInitialSnapshots(states), input::send, mergeMessages(input.receiveAsFlow(), messages))
            .onEach { snapshot -> notifyServer(this@computeSnapshots, snapshot) }
            .collect(sink::invoke)
    }

private fun  DebugEnv.mergeInitialSnapshots(
    debugStates: Flow,
) = env.initial().mergeWith(debugStates.toInitialSnapshots())

private fun  DebugEnv.mergeMessages(
    originalInput: Flow,
    debugInput: Flow,
): (Initial) -> Flow = { initial ->
    env.resolveAsFlow(initial)
        .mergeWith(originalInput)
        .mergeWith(debugInput)
}

private fun  DebugEnv.debugSession(
    block: suspend DebugSession.(input: Sink>) -> Unit,
): Flow> =
    channelFlow { settings.sessionFactory(settings) { block(channel::send) } }

private fun  Flow.toInitialSnapshots(): Flow> =
    // TODO what if we want to start from Regular snapshot?
    map { s -> Initial(s, setOf()) }

/**
 * Notifies server about state changes
 */
// TODO context(DebugSession, DebugEnv)
private suspend fun  DebugSession.notifyServer(
    env: DebugEnv,
    snapshot: Snapshot,
) = with(env.settings) {
    invoke(NotifyServer(RandomUUID(), id, serializer.toServerMessage(snapshot)))
}

private fun  JsonSerializer.toServerMessage(
    snapshot: Snapshot,
) = when (snapshot) {
    is Initial -> NotifyComponentAttached(toJsonTree(snapshot.currentState), toCommandsSet(snapshot.commands))
    is Regular -> NotifyComponentSnapshot(
        toJsonTree(snapshot.message),
        toJsonTree(snapshot.previousState),
        toJsonTree(snapshot.currentState),
        toCommandsSet(snapshot.commands),
    )
}

private fun  JsonSerializer.toCommandsSet(
    s: Set,
): Set = s.mapTo(HashSet(s.size), ::toJsonTree)