import { uuid } from "@noya-app/noya-utils"; import { jwt } from "../jwt"; import { ServerToClientMessage } from "../multiplayer"; import { NoyaManager } from "../NoyaManager"; import { clientId$, clientImage$, clientName$ } from "./clientId"; import { SyncConfig, SyncOptionsBase, SyncURLOption, TokenPayload, } from "./types"; /** * Convert various option formats into a standard options object */ export function getOptionsObject( options: T | SyncURLOption ): T { if (typeof options === "string" || options instanceof URL) { return { url: options } as T; } if (typeof options === "function") { return { url: options } as T; } return options as T; } /** * Convert various URL formats into a standard URL object */ export async function getUrl(url: SyncURLOption): Promise { const resolved = typeof url === "string" || url instanceof URL ? url : await url(); return typeof resolved === "string" ? new URL(resolved) : resolved; } /** * Get sync configuration from options */ export async function getSyncConfig( options: SyncOptionsBase | SyncURLOption ): Promise { const { url: urlOption } = getOptionsObject(options); const url = await getUrl(urlOption); const clientId = clientId$.get(); const clientName = clientName$.get(); const clientImage = clientImage$.get(); if (clientId) { url.searchParams.set("clientId", clientId); } if (clientName) { url.searchParams.set("clientName", clientName); } if (clientImage) { url.searchParams.set("clientImage", clientImage); } const tokenString = url.searchParams.get("token"); if (!tokenString) { throw new Error("No token provided in URL"); } const decoded = await jwt.decode({ token: tokenString, }); return { url, tokenString, tokenPayload: decoded.payload, }; } /** * Handle adapter messages by updating the appropriate managers */ export function handleAdapterMessage< S, M extends object, E extends object, MenuT extends string, I extends Record = Record, >( message: ServerToClientMessage, noyaManager: NoyaManager ) { switch (message.type) { case "connectedUsers": { const normalizedUsers = message.users.map((user) => ({ ...user, clientId: user.clientId ?? uuid(), inactive: user.inactive ?? false, })); noyaManager.userManager.setAllUsers(normalizedUsers); const connectedUsers = normalizedUsers.filter((user) => !user.inactive); // Prune shared connection data for users that are no longer connected const connectedIds = new Set(connectedUsers.map((u) => u.connectionId)); const currentConnectionId = noyaManager.sharedConnectionDataManager.currentConnectionId$.get(); const existing = noyaManager.sharedConnectionDataManager.data$.get(); for (const id of Object.keys(existing)) { if (!connectedIds.has(id) && id !== currentConnectionId) { noyaManager.sharedConnectionDataManager.delete(id); } } break; } case "sharedConnectionData": { // Only merge data for connected users; ignore stale entries const connectedIds = new Set( noyaManager.userManager.connectedUsers$.get().map((u) => u.connectionId) ); const incoming = message.data as Record; const filtered: Record = {}; for (const id in incoming) { if (connectedIds.has(id)) filtered[id] = incoming[id]; } if (Object.keys(filtered).length > 0) { noyaManager.sharedConnectionDataManager.merge(filtered); } break; } case "workflow": { noyaManager.pipelineManager.status$.set(message.workflow.status); noyaManager.pipelineManager.tasks$.set(message.workflow.tasks); noyaManager.pipelineManager.result$.set(message.workflow.result); break; } case "tasks": { noyaManager.taskManager.setTasks(message.tasks); break; } case "secrets": { noyaManager.secretManager.secrets$.set(message.secrets); noyaManager.secretManager.isInitialized$.set(true); break; } case "assets": { noyaManager.assetManager.set(message.assets); break; } case "inputs": { noyaManager.ioManager.inputs$.set(message.inputs); noyaManager.ioManager.inputsInitialized$.set(true); break; } case "outputTransforms": { noyaManager.ioManager.outputTransforms$.set(message.outputTransforms); noyaManager.ioManager.outputTransformsInitialized$.set(true); break; } case "resources": { noyaManager.resourceManager.setResources(message.resources); noyaManager.resourceManager.isInitialized$.set(true); break; } case "fileName": { noyaManager.filePropertyManager.applyServerUpdate({ name: message.name ?? "", changeId: message.changeId, }); break; } // Legacy activityEvents message removed in favor of stream updates case "activityEvents.update": { noyaManager.activityEventsManager.setStreamEvents( message.streamId, message.activityEvents ); break; } case "serverScript.log": { noyaManager.logManager.append(message.log); break; } case "sandbox": { noyaManager.sandboxManager.setFromServer(message.state); break; } } } /** * Apply a server-to-client message to the NoyaManager by updating adapter-managed * subsystems (assets, resources, secrets, etc.) and enqueuing the message into * the MultiplayerStateManager for state updates. */ export function applyServerMessage< S, M extends object, E extends object, MenuT extends string, I extends Record = Record, >( message: ServerToClientMessage, noyaManager: NoyaManager, options: { pushToMSM?: boolean } = {} ) { handleAdapterMessage(message, noyaManager); const shouldPush = options.pushToMSM ?? true; // Multiplayer MSM handles all non-pong messages when enabled if (shouldPush && message.type !== "pong") { noyaManager.multiplayerStateManager.incomingMessages.push(message); noyaManager.multiplayerStateManager.processIncomingMessages(); } }