diff options
author | joffrey-bion <joffrey.bion@gmail.com> | 2019-10-23 00:43:33 +0200 |
---|---|---|
committer | joffrey-bion <joffrey.bion@gmail.com> | 2019-10-23 00:43:33 +0200 |
commit | ffe9dc9fde3b419bd7ac3e105c9c2b2d8b2c0238 (patch) | |
tree | 3b8cd4983b85262720014cabe3b643109d5fc3a1 | |
parent | WIP sagas (diff) | |
download | seven-wonders-ffe9dc9fde3b419bd7ac3e105c9c2b2d8b2c0238.tar.gz seven-wonders-ffe9dc9fde3b419bd7ac3e105c9c2b2d8b2c0238.tar.bz2 seven-wonders-ffe9dc9fde3b419bd7ac3e105c9c2b2d8b2c0238.zip |
Sagas rework
7 files changed, 95 insertions, 90 deletions
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt index fafa2333..d8dee5df 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt @@ -1,5 +1,7 @@ package org.luxons.sevenwonders.ui +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.promise import org.luxons.sevenwonders.ui.components.application import org.luxons.sevenwonders.ui.redux.SwState import org.luxons.sevenwonders.ui.redux.configureStore @@ -38,6 +40,10 @@ private fun initializeAndRender(rootElement: Element) { private fun initRedux(): Store<SwState, RAction, WrapperAction> { val sagaManager = SagaManager<SwState, RAction, WrapperAction>() val store = configureStore(sagaManager = sagaManager) - sagaManager.startSaga(rootSaga()) + GlobalScope.promise { + sagaManager.launchSaga(this) { + rootSaga() + } + } return store } diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameBrowserSagas.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameBrowserSagas.kt index cd9e0cf8..b4c99827 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameBrowserSagas.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameBrowserSagas.kt @@ -1,28 +1,29 @@ package org.luxons.sevenwonders.ui.redux.sagas -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import org.luxons.sevenwonders.client.SevenWondersSession import org.luxons.sevenwonders.model.api.LobbyDTO import org.luxons.sevenwonders.ui.redux.EnterLobbyAction import org.luxons.sevenwonders.ui.redux.RequestCreateGameAction import org.luxons.sevenwonders.ui.redux.RequestJoinGameAction -import org.luxons.sevenwonders.ui.redux.SwState import org.luxons.sevenwonders.ui.redux.UpdateGameListAction import org.luxons.sevenwonders.ui.redux.UpdateLobbyAction -import redux.RAction -import redux.WrapperAction +import kotlin.coroutines.coroutineContext -fun gameBrowserSaga(session: SevenWondersSession) = saga<SwState, RAction, WrapperAction> { - val watchGamesJob = fork(watchGames(session)) - val watchCreateGameJob = fork(watchCreateGame(session)) - val watchJoinGameJob = fork(watchJoinGame(session)) +suspend fun SwSagaContext.gameBrowserSaga(session: SevenWondersSession) { + coroutineScope { + launch { watchGames(session) } + launch { watchCreateGame(session) } + launch { watchJoinGame(session) } + } } -private fun watchGames(session: SevenWondersSession) = saga<SwState, RAction, WrapperAction> { +private suspend fun SwSagaContext.watchGames(session: SevenWondersSession) { val gamesSubscription = session.watchGames() for (lobbies in gamesSubscription.messages) { - if (!isActive) { + if (!coroutineContext.isActive) { gamesSubscription.unsubscribe() break } @@ -30,23 +31,26 @@ private fun watchGames(session: SevenWondersSession) = saga<SwState, RAction, Wr } } -private fun watchCreateGame(session: SevenWondersSession) = - actionHandlerSaga<SwState, RAction, WrapperAction, RequestCreateGameAction> { +private suspend fun SwSagaContext.watchCreateGame(session: SevenWondersSession) = + onEach<RequestCreateGameAction> { val lobby = session.createGame(it.gameName) handleGameJoined(session, lobby) } -private fun watchJoinGame(session: SevenWondersSession) = actionHandlerSaga<SwState, RAction, WrapperAction, RequestJoinGameAction> { - val lobby = session.joinGame(it.gameId) - handleGameJoined(session, lobby) -} +private suspend fun SwSagaContext.watchJoinGame(session: SevenWondersSession) = + onEach<RequestJoinGameAction> { + val lobby = session.joinGame(it.gameId) + handleGameJoined(session, lobby) + } -private fun SagaContext<SwState, RAction, WrapperAction>.handleGameJoined( +private suspend fun SwSagaContext.handleGameJoined( session: SevenWondersSession, lobby: LobbyDTO ) { dispatch(UpdateLobbyAction(lobby)) dispatch(EnterLobbyAction(lobby.id)) - fork(lobbySaga(session, lobby.id)) - // TODO push /lobby/{lobby.id} + coroutineScope { + launch { lobbySaga(session, lobby.id) } + // TODO push /lobby/{lobby.id} + } } diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt index 24a9e1b4..29242782 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt @@ -1,15 +1,19 @@ package org.luxons.sevenwonders.ui.redux.sagas +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch import org.luxons.sevenwonders.client.SevenWondersSession import org.luxons.sevenwonders.ui.redux.SwState import redux.RAction import redux.WrapperAction -fun gameSaga(session: SevenWondersSession, gameId: Long) = saga<SwState, RAction, WrapperAction> { - fork(watchPlayerReady(session, gameId)) +suspend fun SwSagaContext.gameSaga(session: SevenWondersSession, gameId: Long) { + coroutineScope { + launch { watchPlayerReady(session, gameId) } + } } -fun watchPlayerReady(session: SevenWondersSession, gameId: Long) = saga<SwState, RAction, WrapperAction> { +private suspend fun SwSagaContext.watchPlayerReady(session: SevenWondersSession, gameId: Long) { session.watchPlayerReady(gameId) } diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/LobbySagas.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/LobbySagas.kt index 89e8e890..a58aeaa9 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/LobbySagas.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/LobbySagas.kt @@ -1,33 +1,39 @@ package org.luxons.sevenwonders.ui.redux.sagas +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch import org.luxons.sevenwonders.client.SevenWondersSession import org.luxons.sevenwonders.ui.redux.EnterGameAction import org.luxons.sevenwonders.ui.redux.RequestStartGameAction -import org.luxons.sevenwonders.ui.redux.SwState import org.luxons.sevenwonders.ui.redux.UpdateLobbyAction -import redux.RAction -import redux.WrapperAction -fun lobbySaga(session: SevenWondersSession, lobbyId: Long) = saga<SwState, RAction, WrapperAction> { - fork(watchLobbyUpdates(session, lobbyId)) - fork(watchGameStart(session, lobbyId)) - fork(startGame(session)) +suspend fun SwSagaContext.lobbySaga(session: SevenWondersSession, lobbyId: Long) { + coroutineScope { + launch { watchLobbyUpdates(session, lobbyId) } + launch { handleGameStart(session, lobbyId) } + launch { watchStartGame(session) } + } } -private fun watchLobbyUpdates(session: SevenWondersSession, lobbyId: Long) = saga<SwState, RAction, WrapperAction> { +private suspend fun SwSagaContext.watchLobbyUpdates(session: SevenWondersSession, lobbyId: Long) { val lobbyUpdates = session.watchLobbyUpdates(lobbyId) for (lobby in lobbyUpdates.messages) { dispatch(UpdateLobbyAction(lobby)) } } -private fun watchGameStart(session: SevenWondersSession, lobbyId: Long) = saga<SwState, RAction, WrapperAction> { +private suspend fun SwSagaContext.handleGameStart(session: SevenWondersSession, lobbyId: Long) { val gameStartSubscription = session.watchGameStart(lobbyId) gameStartSubscription.messages.receive() dispatch(EnterGameAction(lobbyId)) - // TODO push /game/{lobby.id} + + coroutineScope { + launch { gameSaga(session, lobbyId) } + + // TODO push /game/{lobby.id} + } } -private fun startGame(session: SevenWondersSession) = actionHandlerSaga<SwState, RAction, WrapperAction, RequestStartGameAction> { +private suspend fun SwSagaContext.watchStartGame(session: SevenWondersSession) = onEach<RequestStartGameAction> { session.startGame() } diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt index 96311f98..08769202 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt @@ -1,5 +1,7 @@ package org.luxons.sevenwonders.ui.redux.sagas +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch import org.luxons.sevenwonders.client.SevenWondersClient import org.luxons.sevenwonders.model.api.SEVEN_WONDERS_WS_ENDPOINT import org.luxons.sevenwonders.ui.redux.RequestChooseName @@ -8,13 +10,17 @@ import org.luxons.sevenwonders.ui.redux.SwState import redux.RAction import redux.WrapperAction -fun rootSaga() = saga<SwState, RAction, WrapperAction> { +typealias SwSagaContext = SagaContext<SwState, RAction, WrapperAction> + +suspend fun SwSagaContext.rootSaga() { val action = next<RequestChooseName>() val session = SevenWondersClient().connect(SEVEN_WONDERS_WS_ENDPOINT) - fork(gameBrowserSaga(session)) + coroutineScope { + launch { gameBrowserSaga(session) } - val player = session.chooseName(action.playerName) - dispatch(SetCurrentPlayerAction(player)) - // push /games + val player = session.chooseName(action.playerName) + dispatch(SetCurrentPlayerAction(player)) + // push /games + } } diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt index 2aa677fe..c7184cea 100644 --- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt +++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt @@ -1,5 +1,6 @@ package org.luxons.sevenwonders.ui.redux.sagas +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview @@ -12,7 +13,7 @@ import kotlinx.coroutines.promise import redux.Middleware import redux.MiddlewareApi import redux.RAction -import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.coroutineContext @UseExperimental(ExperimentalCoroutinesApi::class) class SagaManager<S, A : RAction, R>( @@ -44,61 +45,43 @@ class SagaManager<S, A : RAction, R>( GlobalScope.promise { actions.send(action) } } - fun startSaga(saga: Saga<S, A, R>): Job { - check(::context.isInitialized) { - "Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware" - } - return context.launch { - saga.execute(context) + fun launchSaga(coroutineScope: CoroutineScope, saga: suspend SagaContext<S, A, R>.() -> Unit): Job { + checkMiddlewareApplied() + return coroutineScope.launch { + context.saga() } } - suspend fun runSaga(saga: Saga<S, A, R>) { + suspend fun runSaga(saga: suspend SagaContext<S, A, R>.() -> Unit) { + checkMiddlewareApplied() + context.saga() + } + + private fun checkMiddlewareApplied() { check(::context.isInitialized) { "Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware" } - saga.execute(context) } } -fun <S, A : RAction, R> saga(block: suspend SagaContext<S, A, R>.() -> Unit) = Saga(block) - -inline fun <S, A : RAction, R, reified AT : A> actionHandlerSaga( - noinline block: suspend SagaContext<S, A, R>.(AT) -> Unit -) = saga<S, A, R> { - onEach<AT> { - block(it) - } -} - -class Saga<S, A : RAction, R>(private val body: suspend SagaContext<S, A, R>.() -> Unit) { - - internal suspend fun execute(context: SagaContext<S, A, R>) = context.body() -} - @UseExperimental(FlowPreview::class, ExperimentalCoroutinesApi::class) class SagaContext<S, A : RAction, R>( - private val reduxApi: MiddlewareApi<S, A, R>, - private val actions: BroadcastChannel<A> -): CoroutineScope { - - private val job = Job() - - override val coroutineContext: CoroutineContext - get() = job - + private val reduxApi: MiddlewareApi<S, A, R>, private val actions: BroadcastChannel<A> +) { + /** + * Dispatches the given redux [action]. + */ fun dispatch(action: A) { reduxApi.dispatch(action) } /** * Starts a concurrent coroutine that executes [handle] on every action dispatched. - * Returns an "unsubscribe" function. */ suspend fun onEach(handle: suspend SagaContext<S, A, R>.(A) -> Unit) { val channel = actions.openSubscription() for (a in channel) { - if (!isActive) { + if (!coroutineContext.isActive) { channel.cancel() break } @@ -107,13 +90,12 @@ class SagaContext<S, A : RAction, R>( } /** - * Starts a concurrent coroutine that executes [handle] on every action dispatched of the given type. - * Returns an "unsubscribe" function. + * Starts a concurrent coroutine that executes [handle] on every action dispatched of the type [T]. */ - suspend inline fun <reified AT : A> onEach( - crossinline handle: suspend SagaContext<S, A, R>.(AT) -> Unit + suspend inline fun <reified T : A> onEach( + crossinline handle: suspend SagaContext<S, A, R>.(T) -> Unit ) = onEach { - if (it is AT) { + if (it is T) { handle(it) } } @@ -124,6 +106,10 @@ class SagaContext<S, A : RAction, R>( suspend fun next(predicate: (A) -> Boolean): A { val channel = actions.openSubscription() for (a in channel) { + if (!coroutineContext.isActive) { + channel.cancel() + throw CancellationException("The expected action was not received before cancellation") + } if (predicate(a)) { channel.cancel() return a @@ -136,8 +122,4 @@ class SagaContext<S, A : RAction, R>( * Suspends until the next action of type [T] is dispatched, and returns that action. */ suspend inline fun <reified T : A> next(): T = next { it is T } as T - - fun fork(saga: Saga<S, A, R>) = SagaContext(reduxApi, actions).launch { - saga.execute(this@SagaContext) - } } diff --git a/sw-ui-kt/src/test/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFrameworkTest.kt b/sw-ui-kt/src/test/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFrameworkTest.kt index d8efcc3a..19489375 100644 --- a/sw-ui-kt/src/test/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFrameworkTest.kt +++ b/sw-ui-kt/src/test/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFrameworkTest.kt @@ -47,10 +47,9 @@ class SagaContextTest { val redux = configureTestStore(State("initial")) - val saga = saga<State, RAction, WrapperAction> { + redux.sagas.runSaga { dispatch(UpdateData("Bob")) } - redux.sagas.runSaga(saga) assertEquals(State("Bob"), redux.store.getState(), "state is not as expected") } @@ -59,16 +58,15 @@ class SagaContextTest { fun next(): dynamic = GlobalScope.promise { val redux = configureTestStore(State("initial")) - val saga = saga<State, RAction, WrapperAction> { + val job = redux.sagas.launchSaga(this) { val action = next<SideEffectAction>() dispatch(UpdateData("effect-${action.data}")) } - redux.sagas.startSaga(saga) assertEquals(State("initial"), redux.store.getState()) redux.store.dispatch(SideEffectAction("data")) - delay(50) + job.join() assertEquals(State("effect-data"), redux.store.getState()) } @@ -77,12 +75,11 @@ class SagaContextTest { val redux = configureTestStore(State("initial")) - val saga = saga<State, RAction, WrapperAction> { + val job = redux.sagas.launchSaga(this) { onEach<SideEffectAction> { dispatch(UpdateData("effect-${it.data}")) } } - val job = redux.sagas.startSaga(saga) assertEquals(State("initial"), redux.store.getState()) |