Changes
9 changed files (+339/-85)
-
-
@@ -1,7 +1,4 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { ListResponse } from "@yamori/proto/yamori/workspace/v1/list_response_pb.js"; export function encode_list_request(): Uint8Array; export function decode_list_response(binary: Uint8Array): ListResponse; export function main(container: Element): void;
-
-
-
@@ -1,17 +1,89 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import backend import dom import gleam/json import gleam/string import protobuf pub fn encode_list_request() -> protobuf.Binary { fn encode_list_request() -> protobuf.Binary { json.object([]) |> protobuf.encode(protobuf.list_request()) } pub fn decode_list_response( fn decode_list_response( binary: protobuf.Binary, ) -> protobuf.Message(protobuf.ListResponse) { binary |> protobuf.decode(protobuf.list_response()) } pub fn main(container: dom.Node(dom.Element)) -> Nil { let loading = dom.html("p") |> dom.append_child(dom.text("Loading...")) container |> dom.append_child(loading) use worker_result <- backend.load_worker() dom.detach_node(loading) case worker_result { Ok(worker) -> { let button = dom.html("button") |> dom.append_child(dom.text("List Workspaces")) |> dom.set_attribute("type", "button") let pre = dom.html("pre") let output = dom.html("output") container |> dom.append_child(button) |> dom.append_child(dom.append_child(pre, output)) { use _event <- dom.add_event_listener(button, "click") button |> dom.set_attribute("disabled", "") use response <- backend.request( worker, "yamori.workspace.v1.KeyValueStorageBasedWorkspaceService", "List", encode_list_request(), ) button |> dom.remove_attribute("disabled") let text = decode_list_response(response) |> protobuf.to_json |> string.append("\n") output |> dom.append_child(dom.text(text)) Nil } } Error(error) -> { let feedback = dom.html("p") |> dom.append_child(dom.text("Failed to load worker: ")) |> dom.append_child( dom.text(case error { backend.DecodeError(_errors) -> "Detected broken inner system message." backend.WorkerInitializeError(text) -> text }), ) container |> dom.append_child(feedback) Nil } } }
-
-
-
@@ -0,0 +1,97 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { isValidMessage, type Message } from "@/worker/message.ts"; type BackendResult<T, E> = | { ok: true; data: T; } | { ok: false; error: E; }; export function loadWorker( onLoaded: (worker: BackendResult<Worker, string>) => void, ): void { const worker = new Worker(new URL("./worker/main.ts", import.meta.url), { type: "module", }); const eventListener = (event: ErrorEvent | MessageEvent) => { worker.removeEventListener("error", eventListener); worker.removeEventListener("message", eventListener); if (event instanceof ErrorEvent) { onLoaded({ ok: false, error: String(event.error), }); return; } if (event.data !== "ready") { onLoaded({ ok: false, error: `Unexpected initial message received: ${event.data}`, }); return; } onLoaded({ ok: true, data: worker, }); }; worker.addEventListener("error", eventListener); worker.addEventListener("message", eventListener); } export function request( worker: Worker, service: string, method: string, request: Uint8Array, onResponse: (response: Uint8Array) => void, ): void { const requestID = crypto.randomUUID(); const handler = (event: MessageEvent) => { if (!isValidMessage(event.data) || event.data.id !== requestID) { return; } worker.removeEventListener("message", handler); if (import.meta.env.NODE_ENV === "development" && event.data.service !== service) { console.warn( `Worker backend returned mismatching service: sent=${service}, received=${event.data.service}`, ); } if (import.meta.env.NODE_ENV === "development" && event.data.method !== method) { console.warn( `Worker backend returned mismatching service: sent=${method}, received=${event.data.method}`, ); } onResponse(event.data.data); }; worker.addEventListener("message", handler); worker.postMessage( { id: requestID, service, method, data: request, } satisfies Message, { transfer: [request.buffer], }, ); }
-
-
-
@@ -0,0 +1,74 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import gleam/dynamic import gleam/result import protobuf fn decode_backend_result( input: dynamic.Dynamic, data: dynamic.Decoder(a), error: dynamic.Decoder(b), ) -> Result(Result(a, b), dynamic.DecodeErrors) { input |> dynamic.decode2( fn(ok, obj) { case ok { True -> obj |> dynamic.decode1(Ok, dynamic.field("data", data)) False -> obj |> dynamic.decode1(Error, dynamic.field("error", error)) } }, dynamic.field("ok", dynamic.bool), dynamic.dynamic, ) |> result.flatten } @external(javascript, "@/backend.ffi.ts", "loadWorker") fn load_worker_internal(on_loaded: fn(dynamic.Dynamic) -> Nil) -> Nil pub opaque type Worker { Worker(ref: dynamic.Dynamic) } pub type WorkerLoadError { DecodeError(dynamic.DecodeErrors) WorkerInitializeError(String) } pub fn load_worker(on_loaded: fn(Result(Worker, WorkerLoadError)) -> Nil) -> Nil { use backend_result <- load_worker_internal() backend_result |> decode_backend_result( dynamic.decode1(Worker, dynamic.dynamic), dynamic.decode1(WorkerInitializeError, dynamic.string), ) |> result.map_error(DecodeError) |> result.flatten |> on_loaded } @external(javascript, "@/backend.ffi.ts", "request") fn request_internal( worker: dynamic.Dynamic, service: String, method: String, request: protobuf.Binary, on_response: fn(protobuf.Binary) -> Nil, ) -> Nil pub fn request( worker: Worker, service: String, method: String, request: protobuf.Binary, on_response: fn(protobuf.Binary) -> Nil, ) -> Nil { request_internal(worker.ref, service, method, request, on_response) }
-
-
-
@@ -0,0 +1,42 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only export function createTextNode(text: string): Text { return document.createTextNode(text); } export function createHTMLElement(tagName: string): HTMLElement { return document.createElement(tagName); } export function setAttribute<T extends Element>( element: T, name: string, value: string, ): T { element.setAttribute(name, value); return element; } export function removeAttribute<T extends Element>(element: T, name: string): T { element.removeAttribute(name); return element; } export function appendChild<T extends Element>(parent: T, node: Node): T { parent.appendChild(node); return parent; } export function removeNode<T extends Node>(node: T): void { node.parentNode?.removeChild(node); } export function addEventListener<T extends Element>( element: T, eventName: string, listener: (event: Event) => void, ): T { element.addEventListener(eventName, listener); return element; }
-
-
-
@@ -0,0 +1,39 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import gleam/dynamic.{type Dynamic} pub type TextNode pub type Element pub type Node(a) @external(javascript, "@/dom.ffi.ts", "createHTMLElement") pub fn html(tag_name: String) -> Node(Element) @external(javascript, "@/dom.ffi.ts", "createTextNode") pub fn text(content: String) -> Node(TextNode) @external(javascript, "@/dom.ffi.ts", "setAttribute") pub fn set_attribute( element: Node(Element), name: String, value: String, ) -> Node(Element) @external(javascript, "@/dom.ffi.ts", "removeAttribute") pub fn remove_attribute(element: Node(Element), name: String) -> Node(Element) @external(javascript, "@/dom.ffi.ts", "appendChild") pub fn append_child(parent: Node(Element), child: Node(a)) -> Node(Element) @external(javascript, "@/dom.ffi.ts", "removeNode") pub fn detach_node(node: Node(a)) -> Nil @external(javascript, "@/dom.ffi.ts", "addEventListener") pub fn add_event_listener( element: Node(Element), event_name: String, callback: fn(Dynamic) -> Nil, ) -> Nil
-
-
-
@@ -1,83 +1,6 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { isValidMessage, type Message } from "./worker/message.ts"; import { encode_list_request, decode_list_response } from "./app.gleam"; import { main } from "./app.gleam"; const button = document.createElement("button"); button.textContent = "Send Message"; button.disabled = true; document.body.appendChild(button); const pre = document.createElement("pre"); const output = document.createElement("output"); pre.appendChild(output); document.body.appendChild(pre); const worker = new Worker(new URL("./worker/main.ts", import.meta.url), { type: "module", }); interface RequestParameters { service: string; method: string; request: Uint8Array; } function request({ service, method, request }: RequestParameters): Promise<Uint8Array> { const id = crypto.randomUUID(); return new Promise((resolve) => { const handler = (event: MessageEvent) => { if (!isValidMessage(event.data) || event.data.id !== id) { return; } worker.removeEventListener("message", handler); if (event.data.service !== service || event.data.method !== method) { throw new Error( `Received response with incorrect service/method: expected=${service}.${method}, received=${event.data.service}/${event.data.method}`, ); } resolve(event.data.data); }; worker.addEventListener("message", handler); worker.postMessage( { id, service, method, data: request, } satisfies Message, { transfer: [request.buffer], }, ); }); } worker.addEventListener( "message", (ev) => { if (ev.data === "ready") { button.disabled = false; button.addEventListener("click", async (ev) => { ev.preventDefault(); const resp = await request({ service: "yamori.workspace.v1.KeyValueStorageBasedWorkspaceService", method: "List", request: encode_list_request(), }); output.textContent += "\n" + JSON.stringify(decode_list_response(resp), null, 2); }); } }, { once: true }, ); main(document.body);
-
-
-
@@ -7,6 +7,7 @@ fromBinary,toBinary, type DescMessage, type MessageInitShape, type MessageShape, } from "@bufbuild/protobuf"; // TODO: Move these out to services/ or similar.
-
@@ -31,3 +32,9 @@ schema: Message,) { return toBinary(schema, create(schema, payload)); } export function toJSON<Message extends DescMessage>( message: MessageShape<Message>, ): string { return JSON.stringify(message, null, 2); }
-
-
-
@@ -24,3 +24,6 @@ pub type ListResponse@external(javascript, "@/protobuf.ffi.ts", "ListResponse") pub fn list_response() -> Schema(ListResponse) @external(javascript, "@/protobuf.ffi.ts", "toJSON") pub fn to_json(message: Message(a)) -> String
-