This repository has been archived by the owner on Mar 6, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The `CoroutineScope.actor()` API from `kotlinx.coroutines.cannels` might be a better fit even though it is marked as `@ObsoleteCoroutinesApi`. Kotlin might get a new actors API at some point. It is unclear though, when that might happen. Maybe it is worth rolling with the current one and porting onto the new API when and if it comes out. Kotlin/kotlinx.coroutines#87
- Loading branch information
1 parent
b1a959a
commit c210533
Showing
8 changed files
with
180 additions
and
121 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
import { setup } from "./live"; | ||
|
||
setup("/live"); | ||
setup("/live", { name: ["Firefox"] }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,22 @@ | ||
import resolve from "./resolve"; | ||
import Socket from "./Socket"; | ||
import type { Params } from "./types"; | ||
import View from "./View"; | ||
|
||
export function setup(endpoint: string) { | ||
export function setup(endpoint: string, params: Params) { | ||
if (["complete", "interactive", "loaded"].includes(document.readyState)) { | ||
connect(endpoint); | ||
connect(endpoint, params); | ||
} else { | ||
document.addEventListener("DOMContentLoaded", () => connect(endpoint)); | ||
const deferred = () => connect(endpoint, params); | ||
document.addEventListener("DOMContentLoaded", deferred); | ||
} | ||
} | ||
|
||
export function connect(endpoint: string) { | ||
export function connect(endpoint: string, params: Params) { | ||
const socket = new Socket(resolve(endpoint)); | ||
const view = new View(socket, document.documentElement); | ||
const view = new View(socket, document.documentElement, params); | ||
|
||
view.join(); | ||
|
||
socket.connect(); | ||
|
||
return socket; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
export type Params = Record<string, string[]>; | ||
|
||
export type TypedArray = | ||
| Int8Array | ||
| Uint8Array | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package io.mnhrdt.plugins.live | ||
|
||
import io.ktor.http.* | ||
import io.ktor.http.content.* | ||
import io.ktor.server.application.* | ||
import io.ktor.server.response.* | ||
import io.ktor.server.routing.* | ||
import io.ktor.server.websocket.* | ||
import kotlinx.coroutines.* | ||
import kotlinx.coroutines.channels.* | ||
import kotlinx.html.* | ||
import kotlinx.html.stream.* | ||
import kotlinx.serialization.* | ||
|
||
class LiveViewScope private constructor(options: Options) { | ||
internal val handlers = mutableMapOf<String, LiveViewContext.() -> LiveView>() | ||
internal var installed = false | ||
|
||
val endpoint: String | ||
|
||
init { | ||
endpoint = options.endpoint | ||
} | ||
|
||
constructor(configure: Options.() -> Unit) : this(Options().apply(configure)) | ||
|
||
class Options { | ||
var endpoint: String = "/live" | ||
} | ||
} | ||
|
||
class LiveViewContext(val application: Application, val parameters: Parameters) | ||
|
||
interface LiveEvent // TODO: Implementing classes need to be serializable (fun serializer(): KSerializer<…>) | ||
|
||
@Serializable | ||
data class LiveUpdate(val html: String) : LiveEvent | ||
|
||
class LiveViewSession(private val session: DefaultWebSocketServerSession) : CoroutineScope by session { | ||
suspend fun send(event: LiveEvent) = session.sendSerialized(event) | ||
|
||
suspend fun flush() = session.flush() | ||
|
||
suspend fun receive() = session.receiveDeserialized<LiveEvent>() | ||
} | ||
|
||
@Serializable | ||
data class LiveConnect(val path: String, val parameters: Map<String, List<String>>) | ||
|
||
class LiveRouting(private val routing: Routing, private val scope: LiveViewScope) { | ||
init { | ||
routing.application.plugin(WebSockets) // require plugin to be installed | ||
|
||
if (!scope.installed) { | ||
routing.webSocket(scope.endpoint) { | ||
val connect = receiveDeserialized<LiveConnect>() | ||
val init = scope.handlers[connect.path] | ||
|
||
checkNotNull(init) { "No view registered for path ${connect.path}" } | ||
|
||
val context = LiveViewContext(application, parametersOf(connect.parameters)) | ||
val session = LiveViewSession(this) | ||
|
||
val view = init(context) | ||
view.join(session) | ||
} | ||
|
||
scope.installed = true | ||
} | ||
} | ||
|
||
fun view(path: String, params: Parameters.() -> Parameters = { this }, init: LiveViewContext.() -> LiveView) { | ||
routing.get(path) { | ||
val context = LiveViewContext(call.application, params(call.parameters)) | ||
val view = init(context) | ||
view.mount() | ||
|
||
val content = view.render() | ||
|
||
val type = ContentType.Text.Html.withCharset(Charsets.UTF_8) | ||
val ok = HttpStatusCode.OK | ||
|
||
call.respond(TextContent(content, type, ok)) | ||
} | ||
|
||
scope.handlers[path] = init | ||
} | ||
} | ||
|
||
fun Routing.live(scope: LiveViewScope, block: LiveRouting.() -> Unit) = LiveRouting(this, scope).apply(block) | ||
|
||
abstract class LiveView : CoroutineScope { | ||
override val coroutineContext get() = checkNotNull(session) { "View is not connected" }.coroutineContext | ||
|
||
private var session: LiveViewSession? = null | ||
val connected: Boolean get() = session != null | ||
|
||
private val state = mutableMapOf<String, Any>() | ||
val assigns = state as Map<String, Any> | ||
|
||
private var pending: Deferred<*>? = null | ||
|
||
abstract fun mount() | ||
|
||
abstract fun render(): String | ||
|
||
fun assign(key: String, value: Any) { | ||
pending?.cancel() | ||
state[key] = value | ||
pending = session?.async { send(LiveUpdate(render())) } | ||
} | ||
|
||
private suspend fun handle(event: LiveEvent) {} | ||
|
||
private suspend fun send(event: LiveEvent) { | ||
val session = checkNotNull(session) { "View is not connected" } | ||
session.send(event) | ||
} | ||
|
||
internal suspend fun join(session: LiveViewSession) { | ||
check(this.session == null) { "View is joined to another session already" } | ||
this.session = session | ||
|
||
try { | ||
mount() | ||
|
||
while (session.isActive) { | ||
val event = session.receive() | ||
println("Received LiveEvent: $event") | ||
// TODO: handle events | ||
} | ||
} catch (e: ClosedReceiveChannelException) { | ||
// … | ||
} finally { | ||
this.session = null | ||
} | ||
} | ||
} | ||
|
||
fun LiveView.html(block: HTML.() -> Unit): String = | ||
buildString { append("<!DOCTYPE html>\n").appendHTML().html(block = block) } |