import { Asset, Resource } from "@noya-app/noya-schemas"; import { uuid } from "@noya-app/noya-utils"; import { Observable } from "@noya-app/observable"; import { ActivityEventsManager } from "./ActivityEventsManager"; import { AIManager } from "./AIManager"; import { AssetManager } from "./AssetManager"; import { ConnectionEventManager } from "./ConnectionEventManager"; import { registerDocumentAITools } from "./documentAITools"; import { EvalManager, SafeEval } from "./EvalManager"; import { FilePropertyManager } from "./FilePropertyManager"; import { HistoryEntries } from "./historyEntries"; import { HostStorageManager } from "./HostStorageManager"; import { ImagePickerManager } from "./ImagePickerManager"; import { IOManager } from "./IOManager"; import { LogManager } from "./LogManager"; import { MenuManager } from "./MenuManager"; import { MultiplayerPatchMetadata, MultiplayerStateManager, MultiplayerStateManagerError, MultiplayerStateManagerOptions, SharedConnectionDataManager, } from "./multiplayer"; import { PipelineManager } from "./PipelineManager"; import { PublishingManager } from "./PublishingManager"; import { GitManager } from "./GitManager"; import { ResourceManager } from "./ResourceManager"; import { SandboxManager } from "./SandboxManager"; import { RPCManager } from "./rpcManager"; import { ScriptEvaluatorManager } from "./ScriptEvaluatorManager"; import { Secret, SecretManager } from "./SecretManager"; import { HistoryEntry } from "./stateManager"; import type { AvoidRect } from "./sync/parentFrameMessages"; import { TaskManager } from "./TaskManager"; import { TranscriptionManager } from "./TranscriptionManager"; import { UserManager } from "./UserManager"; export type NoyaManagerOptions = MultiplayerStateManagerOptions & { initialAssets?: Asset[]; initialSecrets?: Secret[]; initialResources?: Resource[]; resourceUrlBase?: string; initialFileName?: string; registerDefaultAITools?: boolean; safeEval?: SafeEval; /** Whether git operations should include base64-encoded file contents in responses */ gitIncludeContent?: boolean; }; export function defaultMergeHistoryEntries< S, M extends MultiplayerPatchMetadata, >({ previous, next, }: { previous: HistoryEntry; next: HistoryEntry; }) { if ( previous.metadata.name !== undefined && previous.metadata.name === next.metadata.name && previous.metadata.timestamp + 500 > next.metadata.timestamp ) { return HistoryEntries.merge({ previous, next }); } } export class NoyaManager< S, M extends object = object, E extends object = object, MenuT extends string = string, I extends Record = Record, > { id = uuid(); multiplayerStateManager: MultiplayerStateManager; rpcManager: RPCManager; pipelineManager: PipelineManager; scriptEvaluatorManager: ScriptEvaluatorManager; secretManager: SecretManager; aiManager: AIManager; ioManager: IOManager; imagePickerManager = new ImagePickerManager(); hostStorageManager = new HostStorageManager(); sharedConnectionDataManager = new SharedConnectionDataManager(); assetManager: AssetManager; userManager: UserManager; taskManager = new TaskManager(); connectionEventManager = new ConnectionEventManager(); menuManager = new MenuManager(); evalManager: EvalManager; publishingManager = new PublishingManager(); resourceManager: ResourceManager; activityEventsManager: ActivityEventsManager; filePropertyManager: FilePropertyManager; logManager: LogManager; transcriptionManager: TranscriptionManager; sandboxManager: SandboxManager; gitManager: GitManager; isProcessing$: Observable; // Rectangles to avoid when rendering UI in embedded mode (set by parent) avoidRects$ = new Observable([]); initialState: S | (() => S); options: NoyaManagerOptions; unrecoverableError = new Observable( undefined ); constructor( initialState: S | (() => S), options: NoyaManagerOptions = {} ) { options = { ...options, getSharedConnectionData: options.getSharedConnectionData ?? (() => this.sharedConnectionDataManager.data$.get() ?? {}), mergeHistoryEntries: options.mergeHistoryEntries === undefined ? defaultMergeHistoryEntries : options.mergeHistoryEntries, }; this.multiplayerStateManager = new MultiplayerStateManager( initialState, options ); this.initialState = initialState; this.options = options; this.assetManager = new AssetManager({ initialAssets: options.initialAssets, }); this.rpcManager = new RPCManager({ debug: this.options.debug }); this.userManager = new UserManager(this.rpcManager); this.pipelineManager = new PipelineManager(this.rpcManager); this.scriptEvaluatorManager = new ScriptEvaluatorManager(this.rpcManager); this.secretManager = new SecretManager(this.rpcManager, { initialSecrets: options.initialSecrets, }); this.aiManager = new AIManager(this.rpcManager); this.ioManager = new IOManager(this.rpcManager); this.evalManager = new EvalManager({ safeEval: this.options.safeEval, }); this.resourceManager = new ResourceManager(this.rpcManager, { initialResources: options.initialResources, resourceUrlBase: options.resourceUrlBase, sendMessage: (message) => this.multiplayerStateManager.sendMessage(message), subscribeConnectionEvents: (handler) => this.connectionEventManager.events$.subscribe((events) => { // Forward only receive events const last = events.at(-1); if (!last) return; handler(last); }), resolveAssetId: (idOrStableId) => this.assetManager._getServerAssetId(idOrStableId) ?? idOrStableId, }); this.sandboxManager = new SandboxManager(this.rpcManager); this.gitManager = new GitManager(this.rpcManager, this.assetManager, { sendMessage: (message) => this.multiplayerStateManager.sendMessage(message), subscribeConnectionEvents: (handler) => this.connectionEventManager.events$.subscribe((events) => { const last = events.at(-1); if (!last) return; handler(last); }), includeContent: options.gitIncludeContent, }); this.filePropertyManager = new FilePropertyManager(this.rpcManager, { initialName: options.initialFileName, }); this.logManager = new LogManager(); this.transcriptionManager = new TranscriptionManager( this.rpcManager, this.assetManager ); this.activityEventsManager = new ActivityEventsManager((message) => this.multiplayerStateManager.sendMessage(message as any) ); const resourceProcessing$ = this.resourceManager.patches$.map( (patches) => patches.length > 0 ); this.isProcessing$ = Observable.combine( [resourceProcessing$, this.filePropertyManager.isUpdating$], ([resourceProcessing, isRenaming]) => resourceProcessing || isRenaming ); this.multiplayerStateManager.errorEmitter.addListener((error) => { switch (error.reason) { case "invalidDataType": case "schemaMismatch": case "outdatedSchema": case "dataMigrationFailure": case "schemaMigration": this.unrecoverableError.set(error); break; } }); if (this.options.registerDefaultAITools === true) { registerDocumentAITools(this); } } enqueueInput = >( queue: K, payload: I[K] ) => { const connectionId = this.userManager.currentConnectionId$.get(); this.multiplayerStateManager.sendMessage({ type: "enqueueInput", queue, payload, ...(connectionId && { connectionId }), }); }; forceInit = () => { const initialState = this.initialState instanceof Function ? this.initialState() : this.initialState; const schema = this.options?.schema; this.multiplayerStateManager.sm.clearHistory(); this.multiplayerStateManager.sendInit({ force: true, state: initialState, schema, }); }; undo = () => this.multiplayerStateManager.undo(); redo = () => this.multiplayerStateManager.redo(); canUndo = () => this.multiplayerStateManager.canUndo(); canRedo = () => this.multiplayerStateManager.canRedo(); restoreSnapshot = async (options: { fileId: string; fileVersionId: string; }) => { const response = await this.rpcManager.requestRoute( "POST /api/snapshots/restore", { body: JSON.stringify(options), headers: { "Content-Type": "application/json", }, } ); return JSON.parse(response.body) as unknown; }; }