summaryrefslogtreecommitdiffhomepage
path: root/src/app/store.svelte.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-06 22:08:16 +0900
committerAdam Malczewski <[email protected]>2026-06-06 22:08:16 +0900
commite1c8cf3257cb33457aa882c548f5195ecc0f9854 (patch)
treed355147cdab8eb77917ad02caedf26b3d8d0be57 /src/app/store.svelte.ts
downloaddispatch-web-e1c8cf3257cb33457aa882c548f5195ecc0f9854.tar.gz
dispatch-web-e1c8cf3257cb33457aa882c548f5195ecc0f9854.zip
Slice 1: surface system + WS transport + composition root
Pure-core feature libraries assembled at the composition root: - core/protocol: pure reducer over surface catalog/spec/error messages - features/surface-host: generic field-kind interpreter (toggle/progress/ selector/stat/button) + pure plan logic; no surface-id special-casing - adapters/ws: injected WebSocket client (effects at the edge) - app: composition root store (Svelte 5 runes over the pure reducer), host-relative surface WS URL resolution (resolveWsUrl), root App.svelte Verified green: svelte-check 0/0, vitest 84 passed, biome clean, vite build ok.
Diffstat (limited to 'src/app/store.svelte.ts')
-rw-r--r--src/app/store.svelte.ts103
1 files changed, 103 insertions, 0 deletions
diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts
new file mode 100644
index 0000000..6b7a910
--- /dev/null
+++ b/src/app/store.svelte.ts
@@ -0,0 +1,103 @@
+import type { SurfaceServerMessage, SurfaceSpec } from "@dispatch/ui-contract";
+import type { WebSocketLike } from "../adapters/ws";
+import { createSurfaceSocket, type SurfaceSocketOptions } from "../adapters/ws";
+import {
+ applyServerMessage,
+ initialState,
+ type ProtocolState,
+ invoke as protocolInvoke,
+ subscribe as protocolSubscribe,
+ unsubscribe as protocolUnsubscribe,
+} from "../core/protocol";
+import { resolveWsUrl } from "./resolve-ws-url";
+
+export interface AppStore {
+ readonly catalog: ProtocolState["catalog"];
+ readonly selectedId: string | null;
+ readonly selectedSpec: SurfaceSpec | null;
+ readonly lastError: ProtocolState["lastError"];
+ select(surfaceId: string): void;
+ invoke(surfaceId: string, actionId: string, payload?: unknown): void;
+ dispose(): void;
+}
+
+export function createAppStore(opts?: {
+ url?: string;
+ socketFactory?: (url: string) => WebSocketLike;
+}): AppStore {
+ let protocol = $state<ProtocolState>(initialState());
+ let selectedId = $state<string | null>(null);
+
+ let socket: ReturnType<typeof createSurfaceSocket> | null = null;
+
+ function handleServerMessage(msg: SurfaceServerMessage): void {
+ protocol = applyServerMessage(protocol, msg);
+ }
+
+ const wsLocation = typeof location !== "undefined" ? location : undefined;
+ const url =
+ opts?.url ??
+ resolveWsUrl(
+ { VITE_WS_URL: import.meta.env.VITE_WS_URL, VITE_WS_PORT: import.meta.env.VITE_WS_PORT },
+ wsLocation,
+ );
+ const socketOpts: SurfaceSocketOptions = {
+ url,
+ onMessage: handleServerMessage,
+ onReopen() {
+ if (selectedId !== null) {
+ const result = protocolSubscribe(protocol, selectedId);
+ protocol = result.state;
+ for (const msg of result.outgoing) {
+ socket?.send(msg);
+ }
+ }
+ },
+ };
+ if (opts?.socketFactory !== undefined) {
+ socketOpts.socketFactory = opts.socketFactory;
+ }
+ socket = createSurfaceSocket(socketOpts);
+
+ return {
+ get catalog() {
+ return protocol.catalog;
+ },
+ get selectedId() {
+ return selectedId;
+ },
+ get selectedSpec() {
+ if (selectedId === null) return null;
+ return protocol.subscriptions.get(selectedId) ?? null;
+ },
+ get lastError() {
+ return protocol.lastError;
+ },
+ select(surfaceId: string): void {
+ if (selectedId !== null && selectedId !== surfaceId) {
+ const unsub = protocolUnsubscribe(protocol, selectedId);
+ protocol = unsub.state;
+ for (const msg of unsub.outgoing) {
+ socket?.send(msg);
+ }
+ }
+ selectedId = surfaceId;
+ const sub = protocolSubscribe(protocol, surfaceId);
+ protocol = sub.state;
+ for (const msg of sub.outgoing) {
+ socket?.send(msg);
+ }
+ },
+ invoke(surfaceId: string, actionId: string, payload?: unknown): void {
+ const result = protocolInvoke(protocol, surfaceId, actionId, payload);
+ protocol = result.state;
+ for (const msg of result.outgoing) {
+ socket?.send(msg);
+ }
+ },
+ dispose(): void {
+ socket?.close();
+ socket = null;
+ },
+ };
+}