From 9c201c60a727106f1e3bf8c2af5f86aa7427098f Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Mon, 7 Sep 2026 19:16:08 +0900 Subject: feat(workspaces): sync the home tile layout per device orientation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Win8-Start home grid's arrangement now syncs from the backend's home-tile-layout plugin: ONE shared document per device orientation (wide = landscape/desktop, tall = portrait/mobile) — every landscape device renders the same layout, every portrait device the same other one. localStorage persistence is retired (no migration — devices start from the shared document; the first arrangement creates it). - adapter/tile-layout-http.ts: the injected HTTP edge (GET/PUT/DELETE /home/tile-layout/:mode), the features/layout adapter's pattern - tile-layout-store: server-backed, keyed by geometry mode — boot + orientation-change probes (stale in-flight responses never land), locally-applied edits + debounced full-document PUT with rollback to the last server document, WS push filtered to the active orientation, quiet ephemeral degrade when the backend is unreachable (no error UI), sync gated while a probe is in flight - WorkspacesHome locks the store's orientation to the resolved grid geometry (the landscape/portrait split IS the device-type split) - app store: the tile store lives beside the layout store; the WS dispatch feeds 'tile-layout' broadcasts into it; 'tiles' prop from App.svelte (the page entry owns the httpBase) - ws adapter: the 'tile-layout' server message (parse guard) + socket handler; wire conformance case --- src/adapters/ws/index.ts | 9 +++++++++ src/adapters/ws/logic.ts | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) (limited to 'src/adapters') diff --git a/src/adapters/ws/index.ts b/src/adapters/ws/index.ts index 3713c8c..4dc0104 100644 --- a/src/adapters/ws/index.ts +++ b/src/adapters/ws/index.ts @@ -6,6 +6,7 @@ import type { ConversationStatusChangedMessage, ExtensionsChangedMessage, LayoutChangedMessage, + TileLayoutChangedMessage, WsClientMessage, } from "@dispatch/transport-contract"; import type { SurfaceServerMessage } from "@dispatch/ui-contract"; @@ -40,6 +41,12 @@ export interface SurfaceSocketOptions { * tree for one workspace. The consumer filters by its active workspace. */ onLayout?: (msg: LayoutChangedMessage) => void; + /** + * Broadcast after every successful home tile-layout write: the FULL + * document for one device orientation (wide/tall). The consumer filters by + * its active geometry mode. + */ + onTileLayout?: (msg: TileLayoutChangedMessage) => void; onReopen?: () => void; socketFactory?: (url: string) => WebSocketLike; } @@ -92,6 +99,8 @@ export function createSurfaceSocket(opts: SurfaceSocketOptions): SurfaceSocketHa opts.onExtensionsChanged?.(msg as ExtensionsChangedMessage); } else if (msg.type === "layout") { opts.onLayout?.(msg as LayoutChangedMessage); + } else if (msg.type === "tile-layout") { + opts.onTileLayout?.(msg as TileLayoutChangedMessage); } else { opts.onMessage(msg as SurfaceServerMessage); } diff --git a/src/adapters/ws/logic.ts b/src/adapters/ws/logic.ts index 6cbc559..4dbd173 100644 --- a/src/adapters/ws/logic.ts +++ b/src/adapters/ws/logic.ts @@ -6,6 +6,7 @@ import type { ConversationStatusChangedMessage, ExtensionsChangedMessage, LayoutChangedMessage, + TileLayoutChangedMessage, WsClientMessage, WsServerMessage, } from "@dispatch/transport-contract"; @@ -28,6 +29,7 @@ const VALID_SERVER_TYPES = new Set([ "conversation.compacted", "extensions-changed", "layout", + "tile-layout", ]); /** Serialize a client message to a JSON string for the wire. */ @@ -206,6 +208,23 @@ export function parseServerMessage(data: string): WsServerMessage | null { }; return msg; } + case "tile-layout": { + // Home tile-layout broadcast (the synced home grid): the FULL document + // for ONE device orientation. Light shape checks here (the document's + // structural validation is the backend's write-time job); the store + // filters by its active geometry mode. + if (parsed.mode !== "wide" && parsed.mode !== "tall") return null; + const data = parsed.data; + if (data === null || !isRecord(data) || data.version !== 2 || !Array.isArray(data.tiles)) { + return null; + } + const msg: TileLayoutChangedMessage = { + type: "tile-layout", + mode: parsed.mode, + data: data as unknown as TileLayoutChangedMessage["data"], + }; + return msg; + } default: return null; } -- cgit v1.2.3