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

commonMain.MobiusLoop.kt Maven / Gradle / Ivy

package kt.mobius

import kt.mobius.disposables.Disposable
import kt.mobius.functions.Consumer
import kt.mobius.functions.Producer
import kt.mobius.runners.Runnable
import kt.mobius.runners.WorkRunner
import kotlin.js.JsName
import kotlin.jvm.JvmStatic
import kotlin.jvm.Synchronized
import kotlin.jvm.Volatile

/**
 * This is the main loop for Mobius.
 *
 *
 * It hooks up all the different parts of the main Mobius loop, and dispatches messages
 * internally on the appropriate executors.
 */
class MobiusLoop private constructor(
    eventProcessorFactory: EventProcessor.Factory,
    effectHandler: Connectable,
    eventSource: EventSource,
    eventRunner: WorkRunner,
    effectRunner: WorkRunner
) : Disposable {

    companion object {

        @JvmStatic
        @JsName("create")
        fun  create(
            store: MobiusStore,
            effectHandler: Connectable,
            eventSource: EventSource,
            eventRunner: WorkRunner,
            effectRunner: WorkRunner
        ): MobiusLoop {

            return MobiusLoop(
                EventProcessor.Factory(store),
                effectHandler,
                eventSource,
                eventRunner,
                effectRunner
            )
        }
    }

    private val eventDispatcher = MessageDispatcher(eventRunner, Consumer { event ->
        eventProcessor.update(event)
    })
    private val effectDispatcher = MessageDispatcher(effectRunner, Consumer { effect ->
        try {
            effectConsumer.accept(effect)
        } catch (t: Throwable) {
            throw ConnectionException(effect!!, t)
        }
    })

    private val eventProcessor = eventProcessorFactory.create(effectDispatcher) { model ->
        mpp.synchronized(modelObservers) {
            mostRecentModel = model
            for (observer in modelObservers) {
                observer.accept(model)
            }
        }
    }
    private val effectConsumer: Connection
    private val eventSourceDisposable: Disposable

    private val modelObservers = arrayListOf>()

    @Volatile
    var mostRecentModel: M? = null
        private set

    @Volatile
    private var disposed: Boolean = false

    init {
        val eventConsumer: Consumer = object : Consumer {
            override fun accept(value: E) {
                dispatchEvent(value)
            }
        }

        this.effectConsumer = effectHandler.connect(eventConsumer)
        this.eventSourceDisposable = eventSource.subscribe(eventConsumer)

        eventRunner.post(
            object : Runnable {
                override fun run() {
                    eventProcessor.init()
                }
            })
    }

    fun dispatchEvent(event: E) {
        if (disposed)
            throw IllegalStateException(
                "This loop has already been disposed. You cannot dispatch events after disposal"
            )
        eventDispatcher.accept(event)
    }

    /**
     * Add an observer of model changes to this loop. If [.getMostRecentModel] is non-null,
     * the observer will immediately be notified of the most recent model. The observer will be
     * notified of future changes to the model until the loop or the returned [Disposable] is
     * disposed.
     *
     * @param observer a non-null observer of model changes
     * @return a [Disposable] that can be used to stop further notifications to the observer
     * @throws NullPointerException if the observer is null
     * @throws IllegalStateException if the loop has been disposed
     */
    fun observe(observer: Consumer): Disposable {
        mpp.synchronized(modelObservers) {
            if (disposed) {
                error("This loop has already been disposed. You cannot observe a disposed loop")
            }

            val currentModel = mostRecentModel
            if (currentModel != null) {
                // Start by emitting the most recently received model.
                observer.accept(currentModel)
            }

            modelObservers.add(observer)
        }

        return Disposable {
            mpp.synchronized(modelObservers) {
                modelObservers.remove(observer)
            }
        }
    }

    @Synchronized
    override fun dispose() {
        mpp.synchronized(modelObservers) {
            modelObservers.clear()
        }

        eventDispatcher.disable()
        effectDispatcher.disable()

        eventSourceDisposable.dispose()
        effectConsumer.dispose()

        eventDispatcher.dispose()
        effectDispatcher.dispose()

        disposed = true
    }

    /**
     * Defines a fluent API for configuring a [MobiusLoop]. Implementations must be immutable,
     * making them safe to share between threads.
     *
     * @param M the model type
     * @param E the event type
     * @param F the effect type
     */
    interface Builder : Factory {

        /**
         * @return a new [Builder] with the supplied [Init], and the same values as the
         * current one for the other fields.
         */
        @JsName("init")
        fun init(init: Init): Builder

        /**
         * @return a new [Builder] with the supplied [EventSource], and the same values as
         * the current one for the other fields. NOTE: Invoking this method will replace the current
         * [EventSource] with the supplied one. If you want to pass multiple event sources,
         * please use [.eventSources].
         */
        @JsName("eventSource")
        fun eventSource(eventSource: EventSource): Builder

        /**
         * @return a new [Builder] with an [EventSource] that merges the supplied event
         * sources, and the same values as the current one for the other fields.
         */
        @JsName("eventSources")
        fun eventSources(vararg eventSources: EventSource): Builder

        /**
         * @return a new [Builder] with the supplied logger, and the same values as the current
         * one for the other fields.
         */
        @JsName("logger")
        fun logger(logger: Logger): Builder

        /**
         * @return a new [Builder] with the supplied event runner, and the same values as the
         * current one for the other fields.
         */
        @JsName("eventRunner")
        fun eventRunner(eventRunner: Producer): Builder

        /**
         * @return a new [Builder] with the supplied effect runner, and the same values as the
         * current one for the other fields.
         */
        @JsName("effectRunner")
        fun effectRunner(effectRunner: Producer): Builder
    }

    interface Factory {
        /**
         * Start a [MobiusLoop] using this factory.
         *
         * @param startModel the model that the loop should start from
         * @return the started [MobiusLoop]
         */
        @JsName("startFrom")
        fun startFrom(startModel: M): MobiusLoop
    }

    /**
     * Defines a controller that can be used to start and stop MobiusLoops.
     *
     * If a loop is stopped and then started again, the new loop will continue from where the last
     * one left off.
     */
    interface Controller {
        /**
         * Indicates whether this controller is running.
         *
         * @return true if the controller is running
         */
        val isRunning: Boolean

        /**
         * Get the current model of the loop that this controller is running, or the most recent model
         * if it's not running.
         *
         * @return a model with the state of the controller
         */
        val model: M

        /**
         * Connect a view to this controller.
         *
         * Must be called before [.start].
         *
         * The [Connectable] will be given an event consumer, which the view should use to send
         * events to the MobiusLoop. The view should also return a [Connection] that accepts
         * models and renders them. Disposing the connection should make the view stop emitting events.
         *
         * The view Connectable is guaranteed to only be connected once, so you don't have to check
         * for multiple connections or throw [ConnectionLimitExceededException].
         *
         * @throws IllegalStateException if the loop is running or if the controller already is
         * connected
         */
        @JsName("connect")
        fun connect(view: Connectable)

        /**
         * Disconnect UI from this controller.
         *
         * @throws IllegalStateException if the loop is running or if there isn't anything to disconnect
         */
        fun disconnect()

        /**
         * Start a MobiusLoop from the current model.
         *
         * @throws IllegalStateException if the loop already is running or no view has been connected
         */
        fun start()

        /**
         * Stop the currently running MobiusLoop.
         *
         *
         * When the loop is stopped, the last model of the loop will be remembered and used as the
         * first model the next time the loop is started.
         *
         * @throws IllegalStateException if the loop isn't running
         */
        fun stop()

        /**
         * Replace which model the controller should start from.
         *
         * @param model the model with the state the controller should start from
         * @throws IllegalStateException if the loop is running
         */
        @JsName("replaceModel")
        fun replaceModel(model: M)
    }

    /** Interface for logging init and update calls.  */
    interface Logger {
        /**
         * Called right before the [Init.init] function is called.
         *
         *
         * This method mustn't block, as it'll hinder the loop from running. It will be called on the
         * same thread as the init function.
         *
         * @param model the model that will be passed to the init function
         */
        @JsName("beforeInit")
        fun beforeInit(model: M)

        /**
         * Called right after the [Init.init] function is called.
         *
         *
         * This method mustn't block, as it'll hinder the loop from running. It will be called on the
         * same thread as the init function.
         *
         * @param model the model that was passed to init
         * @param result the [First] that init returned
         */
        @JsName("afterInit")
        fun afterInit(model: M, result: First)

        /**
         * Called if the [Init.init] invocation throws an exception. This is a programmer
         * error; Mobius is in an undefined state if it happens.
         *
         * @param model the model object that led to the exception
         * @param exception the thrown exception
         */
        @JsName("exceptionDuringInit")
        fun exceptionDuringInit(model: M, exception: Throwable)

        /**
         * Called right before the [Update.update] function is called.
         *
         *
         * This method mustn't block, as it'll hinder the loop from running. It will be called on the
         * same thread as the update function.
         *
         * @param model the model that will be passed to the update function
         * @param event the event that will be passed to the update function
         */
        @JsName("beforeUpdate")
        fun beforeUpdate(model: M, event: E)

        /**
         * Called right after the [Update.update] function is called.
         *
         *
         * This method mustn't block, as it'll hinder the loop from running. It will be called on the
         * same thread as the update function.
         *
         * @param model the model that was passed to update
         * @param event the event that was passed to update
         * @param result the [Next] that update returned
         */
        @JsName("afterUpdate")
        fun afterUpdate(model: M, event: E, result: Next)

        /**
         * Called if the [Update.update] invocation throws an exception. This is a
         * programmer error; Mobius is in an undefined state if it happens.
         *
         * @param model the model object that led to the exception
         * @param exception the thrown exception
         */
        @JsName("exceptionDuringUpdate")
        fun exceptionDuringUpdate(model: M, event: E, exception: Throwable)
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy