···11-import type { Account } from "cartography-api/account";
11+import type { Account } from "./SocketV1Protocol";
2233export class AuthEvent extends Event {
44 account: Account;
-10
src/lib/appserver/socket/Message.ts
···11-/* eslint-disable @typescript-eslint/naming-convention -- this is a server owned field */
22-33-import type { Authenticate, DebugAddCard } from "cartography-api/request";
44-import type { Authenticated, PatchData } from "cartography-api/response";
55-66-export type MessageReply<T> = T extends Authenticate
77- ? Authenticated
88- : T extends DebugAddCard
99- ? PatchData
1010- : void;
+3-3
src/lib/appserver/socket/MessageEvent.ts
···11-import type { Message$ } from "cartography-api/response";
11+import type { ResponseMessage } from "./SocketV1Protocol";
2233export class MessageEvent extends Event {
44- message: Message$;
44+ message: ResponseMessage;
5566- constructor(message: Message$) {
66+ constructor(message: ResponseMessage) {
77 super("message");
88 this.message = message;
99 }
+4-3
src/lib/appserver/socket/OneOff.svelte.ts
···11-import * as Response from "cartography-api/response";
21import type { MessageEvent } from "./MessageEvent";
32import { type SocketV1 } from "./SocketV1";
43···1716 abort?.throwIfAborted();
18171918 const handler = (event: MessageEvent) => {
2020- if (Response.id(event.message) === this.id) {
2121- resolve(Response.response(event.message) as T);
1919+ if (event.message.id === this.id) {
2020+ // NOTE: would be nice to do a runtime assertion here, but the mapping is currently
2121+ // only defined as a type. Not hard to shift to a value, just lazy.
2222+ resolve(event.message.response as T);
2223 this.#socket.removeEventListener("message", handler);
2324 abort?.removeEventListener("abort", onabort);
2425 }
+20-15
src/lib/appserver/socket/SocketV1.ts
···11import { MessageEvent } from "./MessageEvent";
22import { AuthEvent } from "./AuthEvent";
33import { OneOff } from "./OneOff.svelte";
44-import { Subscription, type Channel } from "./Subscription";
55-import type { MessageReply } from "./Message";
64import { ReactiveEventTarget } from "$lib/ReactiveEventTarget.svelte";
77-import type { FieldId } from "../Field";
88-import { Result$isOk, Result$Ok$0 } from "cartography-api/prelude";
99-import * as Request from "cartography-api/request";
1010-import * as Response from "cartography-api/response";
55+import Value from "typebox/value";
66+import {
77+ construct,
88+ Request,
99+ RequestMessage,
1010+ ResponseMessage,
1111+ type SocketV1Protocol,
1212+} from "./SocketV1Protocol";
11131214interface SocketV1EventMap {
1315 message: MessageEvent;
···4345 this.#socket.close(1003, "Only text messages are supported");
4446 this.dispatchEvent(new Event("error"));
4547 }
4646- const message = Response.from_string(data);
4747- if (Result$isOk(message)) {
4848- this.dispatchEvent(new MessageEvent(Result$Ok$0(message)!));
4949- } else {
4848+ try {
4949+ const message = Value.Decode(ResponseMessage, data);
5050+ this.dispatchEvent(new MessageEvent(message));
5151+ } catch {
5052 this.#socket.close(4000, "Invalid JSON received");
5153 this.dispatchEvent(new Event("error"));
5254 }
···7779 this.#socket.addEventListener("close", onClose);
7880 }
79818080- #sendMessage<T extends Request.Request$>(request: T, id: string = window.crypto.randomUUID()) {
8181- this.#socket.send(Request.to_string(Request.message(request, id)));
8282- return new OneOff<MessageReply<T>>(this, id);
8282+ #sendMessage<T extends Request>(request: T, id: string = window.crypto.randomUUID()) {
8383+ const encoded = Value.Encode(RequestMessage, { id, request });
8484+ this.#socket.send(JSON.stringify(encoded));
8585+ return new OneOff<
8686+ T["#tag"] extends keyof SocketV1Protocol ? SocketV1Protocol[T["#tag"]] : never
8787+ >(this, id);
8388 }
84898590 auth(data: { id: string }) {
8686- this.#sendMessage<Request.Authenticate>(Request.authenticate(data.id) as Request.Authenticate)
9191+ this.#sendMessage(construct("Authenticate", data.id))
8792 .reply()
8893 .then((event) => {
8989- this.dispatchEvent(new AuthEvent(event[0]));
9494+ this.dispatchEvent(new AuthEvent(event["#payload"]));
9095 });
9196 }
9297
+178
src/lib/appserver/socket/SocketV1Protocol.ts
···11+/* eslint-disable @typescript-eslint/naming-convention -- TypeBox schemas are more like types than values, so can be named using type format. */
22+33+/**
44+ * This is a handwritten translation of the `cardtography_api` Gleam package's interface.
55+ * When in doubt the Gleam code is authoritative and this end should be updated.
66+ */
77+88+import Type, { type StaticDecode, type TSchema } from "typebox";
99+1010+declare const __BRAND: unique symbol;
1111+type Branded<Brand, T> = T & { [__BRAND]: Brand };
1212+1313+function Branded<Brand, T extends TSchema>(brand: Brand, value: T) {
1414+ return Type.Codec(value)
1515+ .Decode((val) => val as Branded<Brand, StaticDecode<T>>)
1616+ .Encode((val) => val as StaticDecode<T>);
1717+}
1818+1919+function Struct<Tag extends string, Payload extends TSchema>(name: Tag, payload: Payload) {
2020+ return Type.Object({
2121+ "#type": Type.Literal("struct"),
2222+ "#tag": Type.Literal(name),
2323+ "#payload": payload,
2424+ });
2525+}
2626+2727+export function construct<Tag extends string, Payload>(tag: Tag, payload: Payload) {
2828+ return { "#type": "struct", "#tag": tag, "#payload": payload } as const;
2929+}
3030+3131+export type Struct<Tag extends string, Payload extends TSchema> = StaticDecode<
3232+ typeof Struct<Tag, Payload>
3333+>;
3434+3535+const JsonPointer = Type.String({
3636+ pattern: "^(/[^/~]*(~[01][^/~]*)*)*$",
3737+});
3838+3939+/**
4040+ * Adapted from: https://github.com/fge/sample-json-schemas/blob/master/json-patch/json-patch.json
4141+ */
4242+export const JsonPatch = Type.Array(
4343+ Type.Intersect(
4444+ [
4545+ Type.Object({ path: JsonPointer }, { description: "Members common to all operations" }),
4646+ Type.Union([
4747+ Type.Object(
4848+ { op: Type.Literal("add"), value: Type.Unknown() },
4949+ { description: "add operation. Value can be any JSON value." },
5050+ ),
5151+ Type.Object(
5252+ { op: Type.Literal("remove") },
5353+ { description: "remove operation. Only a path is specified." },
5454+ ),
5555+ Type.Object(
5656+ { op: Type.Literal("replace"), value: Type.Unknown() },
5757+ { description: "replace operation. Value can be any JSON value." },
5858+ ),
5959+ Type.Object(
6060+ { op: Type.Literal("move"), from: JsonPointer },
6161+ { description: 'move operation. "from" is a JSON Pointer.' },
6262+ ),
6363+ Type.Object(
6464+ { op: Type.Literal("copy"), from: JsonPointer },
6565+ { description: 'copy operation. "from" is a JSON Pointer.' },
6666+ ),
6767+ Type.Object(
6868+ { op: Type.Literal("test"), value: Type.Unknown() },
6969+ { description: "test operation. Value can be any JSON value." },
7070+ ),
7171+ Type.Object(
7272+ { op: Type.Literal("_get"), value: Type.Unknown() },
7373+ { description: "_get operation. Value can be any JSON value." },
7474+ ),
7575+ ]),
7676+ ],
7777+ { description: "one JSON Patch operation" },
7878+ ),
7979+ {
8080+ title: "JSON Patch",
8181+ description: "A JSON Schema describing a JSON Patch",
8282+ $schema: "http://json-schema.org/draft-04/schema#",
8383+ notes: ["Only required members are accounted for, other members are ignored"],
8484+ },
8585+);
8686+8787+export const AccountId = Branded("AccountId", Type.String());
8888+export type AccountId = StaticDecode<typeof AccountId>;
8989+9090+export const TileId = Branded("TileId", Type.Integer());
9191+export type TileId = StaticDecode<typeof TileId>;
9292+9393+export const CitizenId = Branded("CitizenId", Type.Integer());
9494+export type CitizenId = StaticDecode<typeof CitizenId>;
9595+9696+export const CardId = Type.Union([CitizenId, TileId]);
9797+export type CardId = StaticDecode<typeof CardId>;
9898+9999+export const TileTypeId = Branded("TileTypeId", Type.String());
100100+export type TileTypeId = StaticDecode<typeof TileTypeId>;
101101+102102+export const SpeciesId = Branded("SpeciesId", Type.String());
103103+export type SpeciesId = StaticDecode<typeof SpeciesId>;
104104+105105+export const CardTypeId = Type.Union([TileTypeId, SpeciesId]);
106106+export type CardTypeId = StaticDecode<typeof CardTypeId>;
107107+108108+export const Account = Type.Object({ id: AccountId });
109109+export type Account = StaticDecode<typeof Account>;
110110+111111+export const Tile = Type.Object({ id: TileId, tile_type_id: TileTypeId, name: Type.String() });
112112+export type Tile = StaticDecode<typeof Tile>;
113113+114114+export const Citizen = Type.Object({
115115+ id: CitizenId,
116116+ species_id: SpeciesId,
117117+ name: Type.String(),
118118+ home_tile_id: Type.Union([TileId, Type.Null()]),
119119+});
120120+export type Citizen = StaticDecode<typeof Citizen>;
121121+122122+export const Card = Type.Union([Struct("Tile", Tile), Struct("Citizen", Citizen)]);
123123+export type Card = StaticDecode<typeof Card>;
124124+125125+export const FieldTile = Type.Object({ id: TileId, x: Type.Integer(), y: Type.Integer() });
126126+export type FieldTile = StaticDecode<typeof FieldTile>;
127127+128128+export const FieldCitizen = Type.Object({ id: CitizenId, x: Type.Integer(), y: Type.Integer() });
129129+export type FieldCitizen = StaticDecode<typeof FieldCitizen>;
130130+131131+export const Field = Type.Object({
132132+ tiles: Type.Array(FieldTile),
133133+ citizens: Type.Array(FieldCitizen),
134134+});
135135+export type Field = StaticDecode<typeof Field>;
136136+137137+export const GameState = Type.Object({
138138+ deck: Type.Array(Card),
139139+ field: Field,
140140+});
141141+export type GameState = StaticDecode<typeof GameState>;
142142+143143+export const Authenticate = Struct("Authenticate", Type.String());
144144+export type Authenticate = StaticDecode<typeof Authenticate>;
145145+146146+export const DebugAddCard = Struct("DebugAddCard", Type.String());
147147+export type DebugAddCard = StaticDecode<typeof DebugAddCard>;
148148+149149+export const Request = Type.Union([Authenticate, DebugAddCard]);
150150+export type Request = StaticDecode<typeof Request>;
151151+152152+export const Authenticated = Struct("Authenticated", Account);
153153+export type Authenticated = StaticDecode<typeof Authenticated>;
154154+155155+export const PutState = Struct("PutState", GameState);
156156+export type PutState = StaticDecode<typeof PutState>;
157157+158158+export const PatchState = Struct("PatchState", Type.Array(JsonPatch));
159159+export type PatchState = StaticDecode<typeof PatchState>;
160160+161161+export const Response = Type.Union([Authenticated, PutState, PatchState]);
162162+export type Response = StaticDecode<typeof Response>;
163163+164164+export interface SocketV1Protocol {
165165+ Authenticate: Authenticated;
166166+}
167167+168168+export const RequestMessage = Type.Object({
169169+ id: Type.String({ format: "uuid" }),
170170+ request: Request,
171171+});
172172+export type RequestMessage = StaticDecode<typeof RequestMessage>;
173173+174174+export const ResponseMessage = Type.Object({
175175+ id: Type.String({ format: "uuid" }),
176176+ response: Response,
177177+});
178178+export type ResponseMessage = StaticDecode<typeof ResponseMessage>;