Trading card city builder game?

attempting to integrate the new message scheme to client side code, but calling gleam from js is hellish

+199 -133
+1
api/gleam.toml
··· 21 21 gleam_stdlib = ">= 0.44.0 and < 2.0.0" 22 22 gleam_json = ">= 3.1.0 and < 4.0.0" 23 23 squirtle = ">= 1.4.0 and < 2.0.0" 24 + youid = ">= 1.5.1 and < 2.0.0" 24 25 25 26 [dev-dependencies] 26 27 gleeunit = ">= 1.0.0 and < 2.0.0"
+4
api/manifest.toml
··· 2 2 # You typically do not need to edit this file 3 3 4 4 packages = [ 5 + { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 5 6 { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 6 7 { name = "gleam_stdlib", version = "0.68.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F7FAEBD8EF260664E86A46C8DBA23508D1D11BB3BCC6EE1B89B3BC3E5C83FF1E" }, 8 + { name = "gleam_time", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "56DB0EF9433826D3B99DB0B4AF7A2BFED13D09755EC64B1DAAB46F804A9AD47D" }, 7 9 { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 8 10 { name = "squirtle", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "squirtle", source = "hex", outer_checksum = "B4DCF7ED3E829E1DB6163F3E18677EF0862DF3F4CE076D98628EBB8D9BC974BC" }, 11 + { name = "youid", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_stdlib", "gleam_time"], otp_app = "youid", source = "hex", outer_checksum = "580E909FD704DB16416D5CB080618EDC2DA0F1BE4D21B490C0683335E3FFC5AF" }, 9 12 ] 10 13 11 14 [requirements] ··· 13 16 gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 14 17 gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 15 18 squirtle = { version = ">= 1.4.0 and < 2.0.0" } 19 + youid = { version = ">= 1.5.1 and < 2.0.0" }
+15 -3
api/package.json
··· 9 9 "exports": { 10 10 "./coder": { 11 11 "import": "./build/dev/javascript/cartography_api/coder.mjs", 12 - "types": "./build/dev/javascript/cartography_api/coder.d.ts" 12 + "types": "./build/dev/javascript/cartography_api/coder.d.mts" 13 13 }, 14 14 "./request": { 15 15 "import": "./build/dev/javascript/cartography_api/request.mjs", 16 - "types": "./build/dev/javascript/cartography_api/request.d.ts" 16 + "types": "./build/dev/javascript/cartography_api/request.d.mts" 17 17 }, 18 18 "./response": { 19 19 "import": "./build/dev/javascript/cartography_api/response.mjs", 20 - "types": "./build/dev/javascript/cartography_api/response.d.ts" 20 + "types": "./build/dev/javascript/cartography_api/response.d.mts" 21 + }, 22 + "./account": { 23 + "import": "./build/dev/javascript/cartography_api/account.mjs", 24 + "types": "./build/dev/javascript/cartography_api/account.d.mts" 25 + }, 26 + "./game_state": { 27 + "import": "./build/dev/javascript/cartography_api/game_state.mjs", 28 + "types": "./build/dev/javascript/cartography_api/game_state.d.mts" 29 + }, 30 + "./prelude": { 31 + "import": "./build/dev/javascript/prelude.mjs", 32 + "types": "./build/dev/javascript/prelude.d.mts" 21 33 } 22 34 }, 23 35 "author": "Cameron Eldridge <cameldridge@gmail.com>",
+25
api/src/account.gleam
··· 1 + import gleam/dynamic/decode 2 + import gleam/json.{type Json} 3 + 4 + pub type Account { 5 + Account(id: String) 6 + } 7 + 8 + pub fn to_json(account: Account) -> Json { 9 + json.object([#("id", json.string(account.id))]) 10 + } 11 + 12 + pub fn to_string(account: Account) -> String { 13 + account 14 + |> to_json() 15 + |> json.to_string() 16 + } 17 + 18 + pub fn decoder() -> decode.Decoder(Account) { 19 + use id <- decode.field("id", decode.string) 20 + decode.success(Account(id:)) 21 + } 22 + 23 + pub fn from_string(string: String) -> Result(Account, json.DecodeError) { 24 + json.parse(string, decoder()) 25 + }
+9
api/src/repr.gleam
··· 1 1 import gleam/dynamic/decode.{type Decoder} 2 2 import gleam/json.{type Json} 3 + import youid/uuid.{type Uuid} 3 4 4 5 pub fn struct(payload: Json, tag: String) -> Json { 5 6 json.object([ ··· 28 29 ) -> Decoder(t) { 29 30 decode.field("#payload", decoder, cb) 30 31 } 32 + 33 + pub fn uuid() -> Decoder(Uuid) { 34 + use str <- decode.then(decode.string) 35 + case uuid.from_string(str) { 36 + Ok(uuid) -> decode.success(uuid) 37 + Error(Nil) -> decode.failure(uuid.nil, "uuid") 38 + } 39 + }
+47 -17
api/src/request.gleam
··· 1 1 import gleam/dynamic/decode 2 2 import gleam/json 3 3 import repr 4 + import youid/uuid.{type Uuid} 5 + 6 + pub opaque type Message { 7 + Message(id: Uuid, request: Request) 8 + } 9 + 10 + pub fn message(request: Request, id: String) -> Message { 11 + let assert Ok(id) = uuid.from_string(id) 12 + Message(id:, request:) 13 + } 14 + 15 + pub fn id(message: Message) -> String { 16 + uuid.to_string(message.id) 17 + } 18 + 19 + pub fn request(message: Message) -> Request { 20 + message.request 21 + } 4 22 5 23 /// A request is sent from the client to the server. 6 24 /// ··· 10 28 DebugAddCard(card_id: String) 11 29 } 12 30 13 - pub fn to_string(request: Request) -> String { 14 - json.to_string(case request { 31 + pub fn to_json(message: Message) -> json.Json { 32 + let Message(id, request) = message 33 + let request = case request { 15 34 Authenticate(auth_token) -> 16 35 json.string(auth_token) 17 36 |> repr.struct("Authenticate") 18 37 DebugAddCard(card_id) -> 19 38 json.string(card_id) 20 39 |> repr.struct("DebugAddCard") 21 - }) 40 + } 41 + json.object([#("id", json.string(uuid.to_string(id))), #("request", request)]) 22 42 } 23 43 24 - pub fn from_string(string: String) -> Result(Request, json.DecodeError) { 44 + pub fn to_string(message: Message) -> String { 45 + message 46 + |> to_json() 47 + |> json.to_string() 48 + } 49 + 50 + pub fn from_string(string: String) -> Result(Message, json.DecodeError) { 25 51 json.parse(string, { 26 - use tag <- repr.struct_tag(Authenticate("")) 27 - case tag { 28 - "Authenticate" -> { 29 - use payload <- repr.struct_payload(decode.string) 30 - decode.success(Authenticate(payload)) 31 - } 32 - "DebugAddCard" -> { 33 - use payload <- repr.struct_payload(decode.string) 34 - decode.success(DebugAddCard(payload)) 35 - } 36 - _ -> { 37 - decode.failure(Authenticate(""), "valid #tag") 52 + use id <- decode.field("id", repr.uuid()) 53 + use request <- decode.field("request", { 54 + use tag <- repr.struct_tag(Authenticate("")) 55 + case tag { 56 + "Authenticate" -> { 57 + use payload <- repr.struct_payload(decode.string) 58 + decode.success(Authenticate(payload)) 59 + } 60 + "DebugAddCard" -> { 61 + use payload <- repr.struct_payload(decode.string) 62 + decode.success(DebugAddCard(payload)) 63 + } 64 + _ -> { 65 + decode.failure(Authenticate(""), "valid #tag") 66 + } 38 67 } 39 - } 68 + }) 69 + decode.success(Message(id:, request:)) 40 70 }) 41 71 } 42 72
+65 -19
api/src/response.gleam
··· 1 + import account.{type Account} 1 2 import game_state.{type GameState} 2 3 import gleam/dynamic/decode 3 4 import gleam/json 4 5 import gleam/list 5 6 import repr 6 7 import squirtle.{type Patch} 8 + import youid/uuid.{type Uuid} 9 + 10 + pub opaque type Message { 11 + Message(id: Uuid, response: Response) 12 + } 13 + 14 + pub fn message(response: Response, id: String) -> Message { 15 + let assert Ok(id) = uuid.from_string(id) 16 + Message(id:, response:) 17 + } 18 + 19 + pub fn id(message: Message) -> String { 20 + uuid.to_string(message.id) 21 + } 22 + 23 + pub fn response(message: Message) -> Response { 24 + message.response 25 + } 7 26 8 27 /// A response is sent from the server to the client. 9 28 pub opaque type Response { 29 + Authenticated(Account) 10 30 PutData(GameState) 11 31 PatchData(List(Patch)) 12 32 } 13 33 14 - pub fn to_string(response: Response) -> String { 15 - json.to_string(case response { 34 + pub fn to_json(message: Message) -> json.Json { 35 + let Message(id, response) = message 36 + let response = case response { 37 + Authenticated(account) -> 38 + account.to_json(account) 39 + |> repr.struct("Authenticated") 16 40 PutData(game_state) -> 17 41 game_state.to_json(game_state) 18 42 |> repr.struct("PutData") ··· 22 46 |> list.map(squirtle.json_value_to_gleam_json) 23 47 |> json.preprocessed_array() 24 48 |> repr.struct("PatchData") 25 - }) 49 + } 50 + json.object([ 51 + #("id", json.string(uuid.to_string(id))), 52 + #("response", response), 53 + ]) 26 54 } 27 55 28 - pub fn from_string(string: String) -> Result(Response, json.DecodeError) { 56 + pub fn to_string(message: Message) -> String { 57 + message 58 + |> to_json() 59 + |> json.to_string() 60 + } 61 + 62 + pub fn from_string(string: String) -> Result(Message, json.DecodeError) { 29 63 json.parse(string, { 30 - use tag <- repr.struct_tag(PatchData([])) 31 - case tag { 32 - "PutData" -> { 33 - use payload <- repr.struct_payload(game_state.decoder()) 34 - decode.success(PutData(payload)) 35 - } 36 - "PatchData" -> { 37 - use payload <- repr.struct_payload( 38 - decode.list(squirtle.patch_decoder()), 39 - ) 40 - decode.success(PatchData(payload)) 41 - } 42 - _ -> { 43 - decode.failure(PatchData([]), "valid #tag") 64 + use id <- decode.field("id", repr.uuid()) 65 + use response <- decode.field("response", { 66 + use tag <- repr.struct_tag(PatchData([])) 67 + case tag { 68 + "Authenticated" -> { 69 + use payload <- repr.struct_payload(account.decoder()) 70 + decode.success(Authenticated(payload)) 71 + } 72 + "PutData" -> { 73 + use payload <- repr.struct_payload(game_state.decoder()) 74 + decode.success(PutData(payload)) 75 + } 76 + "PatchData" -> { 77 + use payload <- repr.struct_payload( 78 + decode.list(squirtle.patch_decoder()), 79 + ) 80 + decode.success(PatchData(payload)) 81 + } 82 + _ -> { 83 + decode.failure(PatchData([]), "valid #tag") 84 + } 44 85 } 45 - } 86 + }) 87 + decode.success(Message(id:, response:)) 46 88 }) 89 + } 90 + 91 + pub fn authenticated(account: Account) -> Response { 92 + Authenticated(account) 47 93 } 48 94 49 95 pub fn put_data(game_state: GameState) -> Response {
+1
server/gleam.toml
··· 26 26 squirrel = ">= 4.6.0 and < 5.0.0" 27 27 cartography_api = { path = "../api" } 28 28 squirtle = ">= 1.4.0 and < 2.0.0" 29 + youid = ">= 1.5.1 and < 2.0.0" 29 30 30 31 [dev-dependencies] 31 32 gleeunit = ">= 1.0.0 and < 2.0.0"
+2 -2
server/manifest.toml
··· 4 4 packages = [ 5 5 { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 6 { name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" }, 7 - { name = "cartography_api", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "squirtle"], source = "local", path = "../api" }, 7 + { name = "cartography_api", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "squirtle", "youid"], source = "local", path = "../api" }, 8 8 { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, 9 9 { name = "eval", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "eval", source = "hex", outer_checksum = "264DAF4B49DF807F303CA4A4E4EBC012070429E40BE384C58FE094C4958F9BDA" }, 10 10 { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, ··· 22 22 { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" }, 23 23 { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, 24 24 { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 25 - { name = "glepack", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glepack", source = "hex", outer_checksum = "37865B894E3745A215E334E2DC48E8AE0EC745ADB29242F883DC895DF3680706" }, 26 25 { name = "glexer", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "splitter"], otp_app = "glexer", source = "hex", outer_checksum = "40A1FB0919FA080AD6C5809B4C7DBA545841CAAC8168FACDFA0B0667C22475CC" }, 27 26 { name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" }, 28 27 { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, ··· 63 62 pog = { version = ">= 4.1.0 and < 5.0.0" } 64 63 squirrel = { version = ">= 4.6.0 and < 5.0.0" } 65 64 squirtle = { version = ">= 1.4.0 and < 2.0.0" } 65 + youid = { version = ">= 1.5.1 and < 2.0.0" }
+1 -1
src/lib/appserver/socket/AuthEvent.ts
··· 1 - import type { Account } from "../Account"; 1 + import type { Account } from "cartography-api/account"; 2 2 3 3 export class AuthEvent extends Event { 4 4 account: Account;
+7 -50
src/lib/appserver/socket/Message.ts
··· 1 1 /* eslint-disable @typescript-eslint/naming-convention -- this is a server owned field */ 2 2 3 - import type { Account } from "../Account"; 4 - import type { Field } from "../Field"; 5 - import type { FieldTile } from "../FieldTile"; 6 - import type { Card } from "../Card"; 3 + import type { Authenticate, DebugAddCard } from "cartography-api/request"; 4 + import type { Authenticated, PatchData } from "cartography-api/response"; 7 5 8 - export interface MessageReplyMap { 9 - auth: AccountMessage; 10 - get_fields: FieldsMessage; 11 - get_field: FieldMessage; 12 - subscribe: never; 13 - unsubscribe: never; 14 - } 15 - 16 - export interface AnyMessage { 17 - type: string; 18 - id: string; 19 - data: unknown; 20 - } 21 - 22 - export interface AccountMessage extends AnyMessage { 23 - type: "account"; 24 - data: { account: Account }; 25 - } 26 - 27 - export interface FieldMessage extends AnyMessage { 28 - type: "field"; 29 - data: { field: Field; field_tiles: FieldTile[] }; 30 - } 31 - 32 - export interface FieldsMessage extends AnyMessage { 33 - type: "field"; 34 - data: { fields: Field[] }; 35 - } 36 - 37 - export interface FieldTileMessage extends AnyMessage { 38 - type: "field_tile"; 39 - 40 - data: { field_tile: FieldTile }; 41 - } 42 - 43 - export interface CardMessage extends AnyMessage { 44 - type: "card"; 45 - data: { card: Card }; 46 - } 47 - 48 - export type Message = 49 - | AccountMessage 50 - | FieldMessage 51 - | CardMessage 52 - | FieldTileMessage 53 - | FieldsMessage; 6 + export type MessageReply<T> = T extends Authenticate 7 + ? Authenticated 8 + : T extends DebugAddCard 9 + ? PatchData 10 + : void;
+3 -3
src/lib/appserver/socket/MessageEvent.ts
··· 1 - import type { Message } from "./Message"; 1 + import type { Message$ } from "cartography-api/response"; 2 2 3 3 export class MessageEvent extends Event { 4 - message: Message; 4 + message: Message$; 5 5 6 - constructor(message: Message) { 6 + constructor(message: Message$) { 7 7 super("message"); 8 8 this.message = message; 9 9 }
+6 -6
src/lib/appserver/socket/OneOff.svelte.ts
··· 1 - import type { MessageReplyMap } from "./Message"; 1 + import * as Response from "cartography-api/response"; 2 2 import type { MessageEvent } from "./MessageEvent"; 3 3 import { type SocketV1 } from "./SocketV1"; 4 4 5 - export class OneOff<T extends keyof MessageReplyMap> extends EventTarget { 5 + export class OneOff<T> extends EventTarget { 6 6 #socket: SocketV1; 7 7 id: string; 8 8 ··· 13 13 } 14 14 15 15 async reply(abort?: AbortSignal) { 16 - return new Promise<MessageReplyMap[T]>((resolve, reject) => { 16 + return new Promise<T>((resolve, reject) => { 17 17 abort?.throwIfAborted(); 18 18 19 19 const handler = (event: MessageEvent) => { 20 - if (event.message.id === this.id) { 21 - resolve(event.message as MessageReplyMap[T]); 20 + if (Response.id(event.message) === this.id) { 21 + resolve(Response.response(event.message) as T); 22 22 this.#socket.removeEventListener("message", handler); 23 23 abort?.removeEventListener("abort", onabort); 24 24 } ··· 34 34 }); 35 35 } 36 36 37 - $then(callback: (event: MessageReplyMap[T]) => void): void { 37 + $then(callback: (event: T) => void): void { 38 38 $effect(() => { 39 39 const abort = new AbortController(); 40 40
+13 -32
src/lib/appserver/socket/SocketV1.ts
··· 2 2 import { AuthEvent } from "./AuthEvent"; 3 3 import { OneOff } from "./OneOff.svelte"; 4 4 import { Subscription, type Channel } from "./Subscription"; 5 - import type { Message, MessageReplyMap } from "./Message"; 5 + import type { MessageReply } from "./Message"; 6 6 import { ReactiveEventTarget } from "$lib/ReactiveEventTarget.svelte"; 7 7 import type { FieldId } from "../Field"; 8 + import { Result$isOk, Result$Ok$0 } from "cartography-api/prelude"; 9 + import * as Request from "cartography-api/request"; 10 + import * as Response from "cartography-api/response"; 8 11 9 12 interface SocketV1EventMap { 10 13 message: MessageEvent; ··· 40 43 this.#socket.close(1003, "Only text messages are supported"); 41 44 this.dispatchEvent(new Event("error")); 42 45 } 43 - try { 44 - const message = JSON.parse(data) as Message; 45 - this.dispatchEvent(new MessageEvent(message)); 46 - } catch { 46 + const message = Response.from_string(data); 47 + if (Result$isOk(message)) { 48 + this.dispatchEvent(new MessageEvent(Result$Ok$0(message)!)); 49 + } else { 47 50 this.#socket.close(4000, "Invalid JSON received"); 48 51 this.dispatchEvent(new Event("error")); 49 52 } ··· 74 77 this.#socket.addEventListener("close", onClose); 75 78 } 76 79 77 - #sendMessage<T extends keyof MessageReplyMap>( 78 - type: T, 79 - data: unknown = {}, 80 - id: string = window.crypto.randomUUID(), 81 - ) { 82 - this.#socket.send(JSON.stringify({ type, data, id })); 83 - return new OneOff<T>(this, id); 80 + #sendMessage<T extends Request.Request$>(request: T, id: string = window.crypto.randomUUID()) { 81 + this.#socket.send(Request.to_string(Request.message(request, id))); 82 + return new OneOff<MessageReply<T>>(this, id); 84 83 } 85 84 86 85 auth(data: { id: string }) { 87 - this.#sendMessage("auth", data) 86 + this.#sendMessage<Request.Authenticate>(Request.authenticate(data.id) as Request.Authenticate) 88 87 .reply() 89 88 .then((event) => { 90 - this.dispatchEvent(new AuthEvent(event.data.account)); 89 + this.dispatchEvent(new AuthEvent(event[0])); 91 90 }); 92 - } 93 - 94 - getFields() { 95 - return this.#sendMessage("get_fields"); 96 - } 97 - 98 - getField(id: FieldId) { 99 - return this.#sendMessage("get_field", { field_id: id }); 100 - } 101 - 102 - unsubscribe(id: string) { 103 - this.#sendMessage("unsubscribe", {}, id); 104 - } 105 - 106 - subscribe<C extends Channel>(channel: C) { 107 - const id = window.crypto.randomUUID(); 108 - this.#sendMessage("subscribe", { channel }, id); 109 - return new Subscription<C>(this, id); 110 91 } 111 92 112 93 close(code?: number, reason?: string) {