/** * Embedded Host (Parent) — Overview * * This module provides `createEmbeddedHost`, a small controller that wires the * parent frame to an embedded child app. It composes a set of helpers so the * parent does not need to re-implement transport, routing, or RPC bridging. * * How it fits together * - Child uses `parentFrameSync` (auto-selected by `isEmbeddedApp`) and sends: * - multiplayer messages (init/patch/shared...) → parent * - rpc requests → parent * - receives: connection events, server-to-client messages, rpc responses * * - Parent composes these helpers: * - `webSocketSync` — activates remote sync when a multiplayer URL is present * - `forwardConnectionEventsToChild` — forwards connection state and all * server-to-client messages to the child * - `routeEmbeddedMultiplayerMessage` — routes child multiplayer messages to * either the active remote sync (MSM) or to local storage (demo/offline) * - `bridgeRpcRequestToNetwork` — proxies child RPC to HTTP/stream and emits * `chunk`, `end`, `error` messages to the child * - `processLocalStorageMessage` — offline “server” that returns server-style * messages for init/patch when operating locally * - `applyServerMessage` — applies server-to-client messages to the parent * manager and enqueues MultiplayerStateManager updates * - `getOfflineStorageKey` — computes a stable local key for demo/offline * * Typical usage * * const host = createEmbeddedHost(noyaManager, { * multiplayerUrl, // undefined for offline/demo * offlineStorageKey: getOfflineStorageKey(fileId), * sendToChild: (message) => postMessageToChild(message), * debug: false, * }); * * // From child → parent (multiplayer) * host.handleChildMultiplayer(payload); * * // From child → parent (RPC) * await host.bridgeChildRpc(request, { baseUrl, token, debug }); * * // Cleanup * host.destroy(); */ import { NoyaManager } from "../NoyaManager"; import { ClientToServerMessage } from "../multiplayer"; import { SerializableRequest } from "../rpc/types"; import { createEmbeddedRemoteConnection } from "./embeddedRemoteConnection"; import { forwardConnectionEventsToChild } from "./parentBridge"; import { ParentToEmbeddedMessage } from "./parentFrameMessages"; import { bridgeRpcRequestToLocalAssets, ensureLocalAssetStore, sendLocalInitializationMessages, } from "./parentLocalRpcBridge"; import { routeEmbeddedMultiplayerMessage, RouteEmbeddedMultiplayerOptions, } from "./parentMultiplayerRouter"; import { bridgeRpcRequestToNetwork } from "./parentRpcBridge"; type RouteFn< S, M extends object, E extends object, MenuT extends string, I extends Record, > = ( noyaManager: NoyaManager, payload: ClientToServerMessage, options: RouteEmbeddedMultiplayerOptions ) => void; type EmbeddedHostDependencies< S, M extends object, E extends object, MenuT extends string, I extends Record, > = { createRemoteConnection?: typeof createEmbeddedRemoteConnection; routeEmbeddedMultiplayerMessage?: RouteFn; }; export type EmbeddedHostOptions = { multiplayerUrl?: string; offlineStorageKey: string; /** Sends a message to the child iframe */ sendToChild: (message: ParentToEmbeddedMessage) => void; /** Optional debug flag passed to sync adapter */ debug?: boolean; }; export type EmbeddedHost = { destroy: () => void; /** Route a child multiplayer message through remote or local paths */ handleChildMultiplayer: (payload: ClientToServerMessage) => void; /** Bridge a child RPC request to the network and emit responses to child */ bridgeChildRpc: ( request: SerializableRequest, config?: { baseUrl: string; token: string; debug?: boolean } ) => Promise; }; /** * Create a minimal embedded host controller that composes the parent helpers. * * - Initializes websocket sync when a multiplayer URL is present. * - Forwards connection events and server messages to the child. * - Routes child multiplayer messages to remote or local paths. * - Bridges child RPC requests over HTTP/stream and replies to the child. */ export function createEmbeddedHost< S, M extends object, E extends object, MenuT extends string, I extends Record = Record, >( noyaManager: NoyaManager, options: EmbeddedHostOptions, dependencies: EmbeddedHostDependencies = {} ): EmbeddedHost { const { multiplayerUrl, offlineStorageKey, sendToChild, debug } = options; const { createRemoteConnection = createEmbeddedRemoteConnection, routeEmbeddedMultiplayerMessage: routeMessage = routeEmbeddedMultiplayerMessage, } = dependencies; const remoteConnection = multiplayerUrl ? createRemoteConnection(noyaManager, { url: multiplayerUrl, debug, }) : undefined; const unsubscribeConnection = forwardConnectionEventsToChild( noyaManager, sendToChild ); // Offline/demo mode: prepare local asset store and initialize child subsystems if (!multiplayerUrl) { ensureLocalAssetStore(noyaManager, offlineStorageKey); sendLocalInitializationMessages( noyaManager, sendToChild, { offlineStorageKey, } ); } return { destroy: () => { unsubscribeConnection(); remoteConnection?.destroy(); }, handleChildMultiplayer: (payload) => { routeMessage(noyaManager, payload, { isRemoteActive: Boolean(multiplayerUrl), sendRemoteMessage: remoteConnection?.sendMessage, localStorageKey: offlineStorageKey, sendToChild, }); }, bridgeChildRpc: async (request, config) => { if (config && config.baseUrl && config.token) { // Remote/network path await bridgeRpcRequestToNetwork( request, { baseUrl: config.baseUrl, token: config.token, debug: config.debug }, (m) => sendToChild(m) ); return; } // Local/offline path (RPCs handled in parent) const handled = await bridgeRpcRequestToLocalAssets( noyaManager, request, (m) => sendToChild(m), { offlineStorageKey } ); if (!handled) { sendToChild({ type: "error", id: request.id!, error: "RPC URL not configured", }); } }, }; }