0

I am working on a web application to visualize maze generation and solving algorithms based on Websockets. The generation algorithms implement the interface IMazeGenerator and return a Flow.

interface IMazeGenerator {

    fun generate(maze: Maze): Flow<Maze>

}

After each step in the generator algorithm, the updated Maze is emitted. The collector then sends the intermediate steps to the client after a short delay.

generatorFlow
            .map { it.toDto() }
            .delay(150)
            .onEach {
                client.send(UpdateMaze(it))
            }
            .launchIn(this.scope)

And now to my problem: I want to offer the client the possibility to skip the intermediate steps and then send only the last result, i.e. the final Maze. To do this, I first need to be able to determine if the emitter is already done (i.e. the final maze was already emitted) and if so, send only the last result when a skip command comes.

Unfortunately I don't know at all at this point, I'm not even sure if this works with flows. Any help is welcome.

tihmels
  • 11
  • 1
  • 2

2 Answers2

1

Thank you marstran, unfortunately this has not yet worked directly, but you have given me the necessary inspiration. Let me first explain the problem, then my solution.

The flow is started as soon as a generation is triggered. If the client wants to skip all further intermediate steps after a certain point, I need to transmit the last emission from exactly this flow. With your solution, the problem was that I would restart the flow in this case, and get a different result, because some of the algorithms are non deterministic. Another problem was that I first have to make sure if the emitter has already calculated the last result, since some algorithms do take some time. At this point, the flow would be skippable. Therefore I also had to use a buffer to omit the backpressure.

I now created an extension function which receives a callback function on the last emitted value.

fun <T> Flow<T>.finalValueCallback(block: suspend (T?) -> Unit) = flow {
    var finalValue: T? = null
    collect {
        emit(it)
        finalValue = it
    }
    block(finalValue)
}

I then used this callback to save the last result from the current flow. If a skip command comes in, I interrupt the flow and send only the last result.

generatorJob = generatorFlow
            .onStart { client.send(UpdateGeneratorState(GeneratorState.RUNNING)) }
            .map { it.toDto() }
            .finalValueCallback {
                finalMaze = it
                client.send(UpdateGeneratorState(GeneratorState.SKIPPABLE))
            }
            .buffer()
            .delay(150)
            .onEach {
                client.send(UpdateMaze(it))
            }
            .onCompletion { cause ->
                finalValue = null
                if (cause == null || cause is FlowSkippedException) {
                    client.send(UpdateGeneratorState(GeneratorState.COMPLETED))
                } else {
                    client.send(UpdateGeneratorState(GeneratorState.CANCELLED))
                }
            }
            .launchIn(this.scope)
tihmels
  • 11
  • 1
  • 2
0

You can create an extension function that collects the flow and returns the last element. Be aware that it would suspend indefinitely if the input flow is infinite.

Something like this:

suspend fun <T> Flow<T>.lastOrNull(): T? {
    var elem: T? = null
    collect { elem = it }
    return elem
}

Now the client can just do val finalMaze = mazeSteps.lastOrNull() to get the final maze.

If you want to transform the input flow to a flow that only emits the final maze, you can just do this:

fun <T> Flow<T>.skipUntilLast(): Flow<T> = flow { 
    val last = lastOrNull()
    if (last != null) emit(last)
}
marstran
  • 26,413
  • 5
  • 61
  • 67