@noego/wood 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +9 -0
  2. package/bin/wood.js +2 -1
  3. package/dist/client/controller_contract.cjs.map +1 -1
  4. package/dist/client/controller_contract.d.cts +5 -3
  5. package/dist/client/controller_contract.d.ts +5 -3
  6. package/dist/client/index.cjs +10 -0
  7. package/dist/client/index.cjs.map +1 -1
  8. package/dist/client/index.d.cts +2 -1
  9. package/dist/client/index.d.ts +2 -1
  10. package/dist/client/index.js +11 -1
  11. package/dist/client/index.js.map +1 -1
  12. package/dist/codegen/preload_generator.cjs +26 -0
  13. package/dist/codegen/preload_generator.cjs.map +1 -1
  14. package/dist/codegen/preload_generator.js +26 -0
  15. package/dist/codegen/preload_generator.js.map +1 -1
  16. package/dist/controller/controller_lifecycle.cjs +37 -0
  17. package/dist/controller/controller_lifecycle.cjs.map +1 -0
  18. package/dist/controller/controller_lifecycle.d.cts +10 -0
  19. package/dist/controller/controller_lifecycle.d.ts +10 -0
  20. package/dist/controller/controller_lifecycle.js +13 -0
  21. package/dist/controller/controller_lifecycle.js.map +1 -0
  22. package/dist/controller/index.cjs +5 -2
  23. package/dist/controller/index.cjs.map +1 -1
  24. package/dist/controller/index.d.cts +1 -0
  25. package/dist/controller/index.d.ts +1 -0
  26. package/dist/controller/index.js +3 -1
  27. package/dist/controller/index.js.map +1 -1
  28. package/dist/index.cjs +4 -0
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +2 -1
  31. package/dist/index.d.ts +2 -1
  32. package/dist/index.js +5 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/main/index.cjs +20 -0
  35. package/dist/main/index.cjs.map +1 -1
  36. package/dist/main/index.d.cts +1 -0
  37. package/dist/main/index.d.ts +1 -0
  38. package/dist/main/index.js +21 -0
  39. package/dist/main/index.js.map +1 -1
  40. package/dist/router/wood_router.cjs +36 -11
  41. package/dist/router/wood_router.cjs.map +1 -1
  42. package/dist/router/wood_router.js +36 -11
  43. package/dist/router/wood_router.js.map +1 -1
  44. package/dist/testing/create_controller_harness.cjs +3 -4
  45. package/dist/testing/create_controller_harness.cjs.map +1 -1
  46. package/dist/testing/create_controller_harness.d.cts +1 -1
  47. package/dist/testing/create_controller_harness.d.ts +1 -1
  48. package/dist/testing/create_controller_harness.js +3 -4
  49. package/dist/testing/create_controller_harness.js.map +1 -1
  50. package/dist/tracing/index.cjs +4 -2
  51. package/dist/tracing/index.cjs.map +1 -1
  52. package/dist/tracing/index.d.cts +1 -1
  53. package/dist/tracing/index.d.ts +1 -1
  54. package/dist/tracing/index.js +3 -1
  55. package/dist/tracing/index.js.map +1 -1
  56. package/dist/tracing/trace_hub.cjs +18 -6
  57. package/dist/tracing/trace_hub.cjs.map +1 -1
  58. package/dist/tracing/trace_hub.d.cts +2 -1
  59. package/dist/tracing/trace_hub.d.ts +2 -1
  60. package/dist/tracing/trace_hub.js +16 -5
  61. package/dist/tracing/trace_hub.js.map +1 -1
  62. package/package.json +4 -4
  63. package/src/components/WoodRecursiveRender.svelte +6 -6
  64. package/src/stick/WoodRecursiveRender.svelte +4 -4
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/main/index.ts"],"sourcesContent":["import path from 'path';\nimport {\n app as electronApp,\n BrowserWindow,\n ipcMain,\n protocol,\n net,\n shell,\n screen,\n} from 'electron';\nimport type {\n BrowserWindowConstructorOptions,\n BrowserWindow as ElectronBrowserWindow,\n IpcMainInvokeEvent,\n Privileges,\n} from 'electron';\nimport { getLogger } from '@noego/logger';\nimport { WindowStateStore } from '../window/window_state_store.js';\nimport { getContainer } from '../runtime/get_container.js';\nimport { runtime as bootRuntime } from '../runtime/runtime.js';\nimport type { TraceLevel } from '../tracing/index.js';\nimport { setApplicationRoot } from '../runtime/application_root.js';\nimport type { RegisterRuntimeRouter } from '../runtime/runtime.js';\nimport {\n getRendererLogDispatcher,\n type RendererLogCallback,\n} from '../runtime/renderer_log_dispatcher.js';\nimport { Ipc, getIpc } from '../ipc/index.js';\nimport { setIpcPushSender, clearIpcPushSender, type IpcPushOptions } from '../ipc/shared.js';\n\ntype CleanupHandler = () => void | Promise<void>;\n\nexport type WoodWindowConfig = {\n title: string;\n width: number;\n height: number;\n defaultRoute?: string;\n frame?: boolean;\n transparent?: boolean;\n resizable?: boolean;\n alwaysOnTop?: boolean;\n minWidth?: number;\n minHeight?: number;\n maxWidth?: number;\n maxHeight?: number;\n show?: boolean;\n parent?: string;\n modal?: boolean;\n vibrancy?: string;\n backgroundMaterial?: string;\n backgroundColor?: string;\n rememberBounds?: boolean;\n contextMenu?: 'native' | 'custom';\n acceptFirstMouse?: boolean;\n};\n\nexport type PrivilegedSchemeRegistration = {\n scheme: string;\n privileges: Privileges;\n};\n\nexport interface WoodMainAppBootResult {\n scope?: string;\n configureContainer?: (container: any) => void;\n registerSchemesAsPrivileged?: PrivilegedSchemeRegistration[];\n}\n\nexport interface WoodMainBootContext {\n electron: {\n app: typeof electronApp;\n BrowserWindow: typeof BrowserWindow;\n ipcMain: typeof ipcMain;\n protocol: typeof protocol;\n net: typeof net;\n shell: typeof shell;\n screen: typeof screen;\n };\n logger: ReturnType<typeof getLogger>;\n}\n\nexport interface WoodMainRuntimeContext extends WoodMainBootContext {\n container: any;\n applicationRoot: string;\n getWindow: (windowId: string) => ElectronBrowserWindow | null;\n createWindow: (windowId?: string) => ElectronBrowserWindow;\n navigateWindow: (windowId: string, route: string) => Promise<void>;\n broadcast: (channel: string, data: unknown) => void;\n setRendererLogCallback: (callback: RendererLogCallback | null) => void;\n getRendererLogCallback: () => RendererLogCallback | null;\n getDebugMetadata: () => WoodDebugMetadata;\n}\n\nexport interface StartWoodMainOptions {\n registerRuntime: (router: RegisterRuntimeRouter) => void;\n windowDefs: Record<string, WoodWindowConfig>;\n controllersDir: string;\n middlewareDir: string;\n outDir?: string;\n scope?: string;\n configureContainer?: (container: any) => void;\n appBoot?: (\n context: WoodMainBootContext,\n ) => void | WoodMainAppBootResult | Promise<void | WoodMainAppBootResult>;\n mainBoot?: (\n appBootResult: unknown,\n context: WoodMainRuntimeContext,\n ) => void | CleanupHandler | Promise<void | CleanupHandler>;\n onBeforeQuit?: (context: WoodMainRuntimeContext) => void | Promise<void>;\n onLoad?: (data: unknown, event?: unknown) => Promise<unknown>;\n onContextMenu?: (data: unknown, event?: unknown) => Promise<unknown>;\n traceMinLevel?: TraceLevel;\n}\n\nexport interface WoodDebugWindowMetadata {\n windowId: string;\n title: string;\n webContentsId: number;\n url: string;\n focused: boolean;\n}\n\nexport interface WoodDebugMetadata {\n ok: boolean;\n enabled: boolean;\n mode: 'development' | 'production';\n port?: number;\n windows: WoodDebugWindowMetadata[];\n}\n\nconst DEFAULT_WOOD_REMOTE_DEBUGGING_PORT = 9333;\n\nexport function resolveWoodDebugMetadataConfiguration(env: NodeJS.ProcessEnv = process.env): {\n enabled: boolean;\n mode: 'development' | 'production';\n port?: number;\n} {\n const isDevelopment = env.WOOD_MODE === 'development';\n if (!isDevelopment) {\n return {\n enabled: false,\n mode: 'production',\n };\n }\n\n const rawPort = env.WOOD_REMOTE_DEBUGGING_PORT;\n const parsedPort = rawPort ? Number.parseInt(rawPort, 10) : DEFAULT_WOOD_REMOTE_DEBUGGING_PORT;\n const port = Number.isFinite(parsedPort) && parsedPort > 0\n ? parsedPort\n : DEFAULT_WOOD_REMOTE_DEBUGGING_PORT;\n\n return {\n enabled: true,\n mode: 'development',\n port,\n };\n}\n\nfunction resolveWindowDef(\n windowDefs: Record<string, WoodWindowConfig>,\n windowId = 'main',\n): WoodWindowConfig {\n const def = windowDefs[windowId];\n if (!def) {\n throw new Error(`Window \"${windowId}\" not defined in generated window defs.`);\n }\n return def;\n}\n\nfunction resolveRuntimePath(relativeOrAbsolutePath: string): string {\n if (path.isAbsolute(relativeOrAbsolutePath)) {\n return relativeOrAbsolutePath;\n }\n return path.join(electronApp.getAppPath(), relativeOrAbsolutePath);\n}\n\nfunction toErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nexport function startWoodMain(options: StartWoodMainOptions): void {\n const logger = getLogger('wood:main').named('bootstrap');\n const runtimeLogger = getLogger('wood:main').named('runtime');\n const debugConfig = resolveWoodDebugMetadataConfiguration();\n const windowState = new WindowStateStore();\n const windows = new Map<string, ElectronBrowserWindow>();\n let mainWindow: ElectronBrowserWindow | null = null;\n let runtimeContext: WoodMainRuntimeContext | null = null;\n let mainBootCleanup: CleanupHandler | undefined;\n\n for (const stream of [process.stdout, process.stderr]) {\n stream.on('error', (error: NodeJS.ErrnoException) => {\n if (error.code === 'EPIPE') {\n return;\n }\n throw error;\n });\n }\n\n if (debugConfig.enabled && debugConfig.port) {\n electronApp.commandLine.appendSwitch('remote-debugging-port', String(debugConfig.port));\n electronApp.commandLine.appendSwitch('remote-debugging-address', '127.0.0.1');\n logger.info('devtools remote debugging enabled', {\n port: debugConfig.port,\n mode: debugConfig.mode,\n });\n }\n\n const bootContext: WoodMainBootContext = {\n electron: {\n app: electronApp,\n BrowserWindow,\n ipcMain,\n protocol,\n net,\n shell,\n screen,\n },\n logger,\n };\n\n void (async () => {\n const appBootResult = options.appBoot\n ? await options.appBoot(bootContext)\n : undefined;\n\n const bootOverrides = (appBootResult ?? {}) as WoodMainAppBootResult;\n const scope = bootOverrides.scope ?? options.scope ?? process.cwd();\n const configureContainer = bootOverrides.configureContainer ?? options.configureContainer;\n const privilegedSchemes = Array.isArray(bootOverrides.registerSchemesAsPrivileged)\n ? bootOverrides.registerSchemesAsPrivileged\n : [];\n\n if (privilegedSchemes.length > 0) {\n protocol.registerSchemesAsPrivileged(privilegedSchemes);\n logger.info('registered privileged schemes', {\n count: privilegedSchemes.length,\n schemes: privilegedSchemes.map((entry) => entry.scheme),\n });\n }\n\n const outDir = options.outDir ?? 'out';\n const preloadPath = path.join(electronApp.getAppPath(), outDir, 'preload', 'index.js');\n const rendererFile = path.join(electronApp.getAppPath(), outDir, 'renderer', 'index.html');\n\n const container = getContainer({\n scope,\n ...(configureContainer ? { configure: configureContainer } : {}),\n });\n\n const registrationContainer = container as {\n registerFunction?: (\n token: any,\n resolver: (...args: any[]) => unknown,\n options?: Record<string, unknown>,\n ) => void;\n };\n\n if (typeof registrationContainer.registerFunction === 'function') {\n registrationContainer.registerFunction(Ipc, () => getIpc());\n }\n\n const getWindow = (windowId: string): ElectronBrowserWindow | null => {\n const existing = windows.get(windowId);\n if (!existing) {\n return null;\n }\n if (existing.isDestroyed()) {\n windows.delete(windowId);\n return null;\n }\n return existing;\n };\n\n const findWindowIdByWebContents = (webContentsId: number): string | null => {\n for (const [windowId, win] of windows.entries()) {\n if (win.isDestroyed()) {\n windows.delete(windowId);\n continue;\n }\n if (win.webContents.id === webContentsId) {\n return windowId;\n }\n }\n return null;\n };\n\n const waitForWindowReady = async (win: ElectronBrowserWindow): Promise<void> => {\n const webContents = win.webContents as ElectronBrowserWindow['webContents'] & {\n isLoadingMainFrame?: () => boolean;\n };\n const isLoading = typeof webContents.isLoadingMainFrame === 'function'\n ? webContents.isLoadingMainFrame()\n : webContents.isLoading();\n if (!isLoading) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n webContents.once('did-finish-load', () => resolve());\n });\n };\n\n const getDebugMetadata = (): WoodDebugMetadata => {\n const debugWindows: WoodDebugWindowMetadata[] = [];\n\n for (const [windowId, win] of windows.entries()) {\n if (win.isDestroyed()) {\n windows.delete(windowId);\n continue;\n }\n\n debugWindows.push({\n windowId,\n title: win.getTitle(),\n webContentsId: win.webContents.id,\n url: win.webContents.getURL(),\n focused: win.isFocused(),\n });\n }\n\n return {\n ok: true,\n enabled: debugConfig.enabled,\n mode: debugConfig.mode,\n ...(debugConfig.port ? { port: debugConfig.port } : {}),\n windows: debugWindows,\n };\n };\n\n const createWindow = (windowId = 'main'): ElectronBrowserWindow => {\n const existing = getWindow(windowId);\n if (existing) {\n existing.show();\n existing.focus();\n return existing;\n }\n\n const windowDef = resolveWindowDef(options.windowDefs, windowId);\n const defaultShow = windowDef.show ?? true;\n const parentWindow = windowDef.parent ? getWindow(windowDef.parent) : null;\n\n windowState.setScreen(screen);\n const savedBounds = windowDef.rememberBounds ? windowState.getBounds(windowId) : null;\n\n const windowOptions: BrowserWindowConstructorOptions = {\n width: savedBounds?.width ?? windowDef.width,\n height: savedBounds?.height ?? windowDef.height,\n ...(savedBounds ? { x: savedBounds.x, y: savedBounds.y } : {}),\n ...(windowDef.minWidth != null ? { minWidth: windowDef.minWidth } : {}),\n ...(windowDef.minHeight != null ? { minHeight: windowDef.minHeight } : {}),\n ...(windowDef.maxWidth != null ? { maxWidth: windowDef.maxWidth } : {}),\n ...(windowDef.maxHeight != null ? { maxHeight: windowDef.maxHeight } : {}),\n ...(typeof windowDef.modal === 'boolean' ? { modal: windowDef.modal } : {}),\n ...(parentWindow ? { parent: parentWindow } : {}),\n title: windowDef.title,\n frame: windowDef.frame ?? true,\n transparent: windowDef.transparent ?? false,\n resizable: windowDef.resizable ?? true,\n alwaysOnTop: windowDef.alwaysOnTop ?? false,\n ...(typeof windowDef.backgroundColor === 'string' ? { backgroundColor: windowDef.backgroundColor } : {}),\n ...(process.platform === 'darwin' && typeof windowDef.vibrancy === 'string'\n ? {\n vibrancy: windowDef.vibrancy as BrowserWindowConstructorOptions['vibrancy'],\n visualEffectState: 'active' as const,\n }\n : {}),\n ...(process.platform === 'win32' && typeof windowDef.backgroundMaterial === 'string'\n ? {\n backgroundMaterial: windowDef.backgroundMaterial as BrowserWindowConstructorOptions['backgroundMaterial'],\n }\n : {}),\n ...(typeof windowDef.acceptFirstMouse === 'boolean' ? { acceptFirstMouse: windowDef.acceptFirstMouse } : {}),\n show: savedBounds ? false : defaultShow,\n webPreferences: {\n preload: preloadPath,\n contextIsolation: true,\n nodeIntegration: false,\n },\n };\n\n const win = new BrowserWindow(windowOptions);\n windows.set(windowId, win);\n if (windowId === 'main') {\n mainWindow = win;\n }\n\n if (savedBounds?.isMaximized) {\n win.maximize();\n } else if (savedBounds?.isFullScreen) {\n win.setFullScreen(true);\n }\n\n if (savedBounds && defaultShow) {\n win.show();\n }\n\n if (windowDef.rememberBounds) {\n const captureNormalBounds = () => {\n if (!win.isMaximized() && !win.isFullScreen()) {\n const rect = win.getBounds();\n windowState.updateBoundsDebounced(windowId, {\n ...rect,\n isMaximized: false,\n isFullScreen: false,\n });\n }\n };\n win.on('move', captureNormalBounds);\n win.on('resize', captureNormalBounds);\n win.on('close', () => {\n const rect = win.getBounds();\n windowState.saveBoundsSync(windowId, {\n ...rect,\n isMaximized: win.isMaximized(),\n isFullScreen: win.isFullScreen(),\n });\n });\n }\n\n win.webContents.setWindowOpenHandler(({ url }) => {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n void shell.openExternal(url);\n }\n return { action: 'deny' };\n });\n\n win.webContents.on('will-navigate', (event, url) => {\n if (process.env.ELECTRON_RENDERER_URL && url.startsWith(process.env.ELECTRON_RENDERER_URL)) {\n return;\n }\n if (url.startsWith('http://') || url.startsWith('https://')) {\n event.preventDefault();\n void shell.openExternal(url);\n }\n });\n\n const rendererLogger = getLogger('wood:main').named(`renderer:${windowId}`);\n const levelMap: Record<number, 'debug' | 'info' | 'warn' | 'error'> = {\n 0: 'debug',\n 1: 'info',\n 2: 'warn',\n 3: 'error',\n };\n\n win.webContents.on('console-message', (_event, level, message, line, sourceId) => {\n const method = levelMap[level] ?? 'info';\n rendererLogger[method](message, { source: sourceId, line, windowId });\n });\n\n if (process.env.ELECTRON_RENDERER_URL) {\n void win.loadURL(process.env.ELECTRON_RENDERER_URL);\n } else {\n void win.loadFile(rendererFile);\n }\n\n win.on('closed', () => {\n windows.delete(windowId);\n if (windowId === 'main') {\n mainWindow = null;\n }\n });\n\n logger.info('window created', { windowId });\n return win;\n };\n\n const navigateWindow = async (windowId: string, route: string): Promise<void> => {\n const win = createWindow(windowId);\n await waitForWindowReady(win);\n win.webContents.send('__event:__navigate', { route });\n win.show();\n win.focus();\n };\n\n const dispatchPushEvent = (\n channel: string,\n data: unknown,\n options?: IpcPushOptions,\n ): void => {\n const eventChannel = channel.startsWith('__event:')\n ? channel\n : `__event:${channel}`;\n\n if (options?.windowId) {\n const target = getWindow(options.windowId);\n if (target && !target.isDestroyed()) {\n target.webContents.send(eventChannel, data);\n }\n return;\n }\n\n for (const [windowId, win] of windows.entries()) {\n if (win.isDestroyed()) {\n windows.delete(windowId);\n continue;\n }\n win.webContents.send(eventChannel, data);\n }\n };\n\n setIpcPushSender(dispatchPushEvent);\n\n const broadcast = (channel: string, data: unknown): void => {\n for (const [windowId, win] of windows.entries()) {\n if (win.isDestroyed()) {\n windows.delete(windowId);\n continue;\n }\n win.webContents.send(channel, data);\n }\n };\n\n runtimeContext = {\n ...bootContext,\n container,\n applicationRoot: '',\n getWindow,\n createWindow,\n navigateWindow,\n broadcast,\n setRendererLogCallback: (callback) => {\n getRendererLogDispatcher().setCallback(callback);\n },\n getRendererLogCallback: () => getRendererLogDispatcher().getCallback(),\n getDebugMetadata,\n };\n\n const bootstrap = async () => {\n logger.info('bootstrap starting');\n const applicationRoot = setApplicationRoot(path.join(electronApp.getPath('userData'), 'wood'));\n runtimeContext!.applicationRoot = applicationRoot;\n logger.info('application root configured', { applicationRoot });\n\n if (options.mainBoot) {\n const bootResult = await options.mainBoot(appBootResult, runtimeContext!);\n if (typeof bootResult === 'function') {\n mainBootCleanup = bootResult;\n }\n }\n\n await bootRuntime({\n container,\n ipcMain,\n registerRuntime: options.registerRuntime,\n controllersDir: resolveRuntimePath(options.controllersDir),\n middlewareDir: resolveRuntimePath(options.middlewareDir),\n ...(options.traceMinLevel ? { traceMinLevel: options.traceMinLevel } : {}),\n onLoad: options.onLoad ?? (async () => {\n throw new Error('Reserved channel \"__load\" is not configured for this app.');\n }),\n onNavigate: async (rawData) => {\n const data = rawData as { windowId?: string; route?: string };\n if (!data.windowId) {\n throw new Error('__navigate requires windowId');\n }\n if (!data.route) {\n throw new Error('__navigate requires route');\n }\n await navigateWindow(data.windowId, data.route);\n return { ok: true };\n },\n onWindow: async (rawData, event) => {\n const data = rawData as { action?: string; windowId?: string };\n const action = data.action ?? '';\n const windowId = data.windowId;\n\n if (action === 'current') {\n const ipcEvent = event as IpcMainInvokeEvent | undefined;\n const senderWindowId = ipcEvent?.sender ? findWindowIdByWebContents(ipcEvent.sender.id) : null;\n if (!senderWindowId) {\n return { ok: false };\n }\n const senderWindowDef = resolveWindowDef(options.windowDefs, senderWindowId);\n return {\n ok: true,\n windowId: senderWindowId,\n defaultRoute: senderWindowDef.defaultRoute,\n };\n }\n\n if (action === 'open') {\n if (!windowId) {\n throw new Error('__window.open requires windowId');\n }\n createWindow(windowId);\n return { ok: true };\n }\n\n const win = windowId ? getWindow(windowId) : mainWindow;\n if (!win) {\n return { ok: false };\n }\n\n switch (action) {\n case 'close':\n win.close();\n return { ok: true };\n case 'focus':\n win.show();\n win.focus();\n return { ok: true };\n case 'minimize':\n win.minimize();\n return { ok: true };\n case 'maximize':\n win.isMaximized() ? win.unmaximize() : win.maximize();\n return { ok: true };\n default:\n return { ok: false };\n }\n },\n onDebug: async (rawData) => {\n const data = rawData as { action?: string } | undefined;\n const action = data?.action ?? '';\n\n if (action === 'current') {\n return getDebugMetadata();\n }\n\n return { ok: false };\n },\n onContextMenu: options.onContextMenu ?? (async (_rawData: unknown, event?: unknown) => {\n const ipcEvent = event as IpcMainInvokeEvent | undefined;\n const senderId = ipcEvent?.sender?.id;\n runtimeLogger.debug('context menu requested', { senderId });\n return { ok: true };\n }),\n });\n\n createWindow('main');\n logger.info('bootstrap complete');\n };\n\n void electronApp.whenReady().then(bootstrap).catch((error) => {\n logger.error('bootstrap failed', {\n message: toErrorMessage(error),\n stack: error instanceof Error ? error.stack : undefined,\n });\n });\n\n electronApp.on('before-quit', () => {\n if (!runtimeContext) {\n return;\n }\n\n void (async () => {\n try {\n clearIpcPushSender();\n if (mainBootCleanup) {\n await mainBootCleanup();\n }\n if (options.onBeforeQuit) {\n await options.onBeforeQuit(runtimeContext!);\n }\n } catch (error) {\n logger.warn('before-quit handler failed', { error: toErrorMessage(error) });\n }\n })();\n\n for (const [id, win] of windows) {\n if (!win.isDestroyed()) {\n win.destroy();\n logger.debug('destroyed window on quit', { windowId: id });\n }\n }\n windows.clear();\n });\n\n electronApp.on('window-all-closed', () => {\n electronApp.quit();\n });\n\n electronApp.on('activate', () => {\n if (BrowserWindow.getAllWindows().length === 0) {\n createWindow('main');\n }\n });\n })().catch((error) => {\n logger.error('startup failed before ready', {\n message: toErrorMessage(error),\n stack: error instanceof Error ? error.stack : undefined,\n });\n });\n}\n"],"mappings":"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","names":["options"]}
1
+ {"version":3,"sources":["../../src/main/index.ts"],"sourcesContent":["import path from 'path';\nimport {\n app as electronApp,\n BrowserWindow,\n ipcMain,\n MessageChannelMain,\n protocol,\n net,\n shell,\n screen,\n} from 'electron';\nimport type {\n BrowserWindowConstructorOptions,\n BrowserWindow as ElectronBrowserWindow,\n IpcMainInvokeEvent,\n Privileges,\n} from 'electron';\nimport { getLogger } from '@noego/logger';\nimport { WindowStateStore } from '../window/window_state_store.js';\nimport { getContainer } from '../runtime/get_container.js';\nimport { runtime as bootRuntime } from '../runtime/runtime.js';\nimport { dispatchRendererTracePayload, type TraceLevel } from '../tracing/index.js';\nimport { setApplicationRoot } from '../runtime/application_root.js';\nimport type { RegisterRuntimeRouter } from '../runtime/runtime.js';\nimport {\n getRendererLogDispatcher,\n type RendererLogCallback,\n} from '../runtime/renderer_log_dispatcher.js';\nimport { Ipc, getIpc } from '../ipc/index.js';\nimport { setIpcPushSender, clearIpcPushSender, type IpcPushOptions } from '../ipc/shared.js';\n\ntype CleanupHandler = () => void | Promise<void>;\n\nexport type WoodWindowConfig = {\n title: string;\n width: number;\n height: number;\n defaultRoute?: string;\n frame?: boolean;\n transparent?: boolean;\n resizable?: boolean;\n alwaysOnTop?: boolean;\n minWidth?: number;\n minHeight?: number;\n maxWidth?: number;\n maxHeight?: number;\n show?: boolean;\n parent?: string;\n modal?: boolean;\n vibrancy?: string;\n backgroundMaterial?: string;\n backgroundColor?: string;\n rememberBounds?: boolean;\n contextMenu?: 'native' | 'custom';\n acceptFirstMouse?: boolean;\n};\n\nexport type PrivilegedSchemeRegistration = {\n scheme: string;\n privileges: Privileges;\n};\n\nexport interface WoodMainAppBootResult {\n scope?: string;\n configureContainer?: (container: any) => void;\n registerSchemesAsPrivileged?: PrivilegedSchemeRegistration[];\n}\n\nexport interface WoodMainBootContext {\n electron: {\n app: typeof electronApp;\n BrowserWindow: typeof BrowserWindow;\n ipcMain: typeof ipcMain;\n protocol: typeof protocol;\n net: typeof net;\n shell: typeof shell;\n screen: typeof screen;\n };\n logger: ReturnType<typeof getLogger>;\n}\n\nexport interface WoodMainRuntimeContext extends WoodMainBootContext {\n container: any;\n applicationRoot: string;\n getWindow: (windowId: string) => ElectronBrowserWindow | null;\n createWindow: (windowId?: string) => ElectronBrowserWindow;\n navigateWindow: (windowId: string, route: string) => Promise<void>;\n broadcast: (channel: string, data: unknown) => void;\n setRendererLogCallback: (callback: RendererLogCallback | null) => void;\n getRendererLogCallback: () => RendererLogCallback | null;\n getDebugMetadata: () => WoodDebugMetadata;\n}\n\nexport interface StartWoodMainOptions {\n registerRuntime: (router: RegisterRuntimeRouter) => void;\n windowDefs: Record<string, WoodWindowConfig>;\n controllersDir: string;\n middlewareDir: string;\n outDir?: string;\n scope?: string;\n configureContainer?: (container: any) => void;\n appBoot?: (\n context: WoodMainBootContext,\n ) => void | WoodMainAppBootResult | Promise<void | WoodMainAppBootResult>;\n mainBoot?: (\n appBootResult: unknown,\n context: WoodMainRuntimeContext,\n ) => void | CleanupHandler | Promise<void | CleanupHandler>;\n onBeforeQuit?: (context: WoodMainRuntimeContext) => void | Promise<void>;\n onLoad?: (data: unknown, event?: unknown) => Promise<unknown>;\n onContextMenu?: (data: unknown, event?: unknown) => Promise<unknown>;\n traceMinLevel?: TraceLevel;\n traceTransport?: 'ipc' | 'messageport';\n}\n\nexport interface WoodDebugWindowMetadata {\n windowId: string;\n title: string;\n webContentsId: number;\n url: string;\n focused: boolean;\n}\n\nexport interface WoodDebugMetadata {\n ok: boolean;\n enabled: boolean;\n mode: 'development' | 'production';\n port?: number;\n windows: WoodDebugWindowMetadata[];\n}\n\nconst DEFAULT_WOOD_REMOTE_DEBUGGING_PORT = 9333;\n\nexport function resolveWoodDebugMetadataConfiguration(env: NodeJS.ProcessEnv = process.env): {\n enabled: boolean;\n mode: 'development' | 'production';\n port?: number;\n} {\n const isDevelopment = env.WOOD_MODE === 'development';\n if (!isDevelopment) {\n return {\n enabled: false,\n mode: 'production',\n };\n }\n\n const rawPort = env.WOOD_REMOTE_DEBUGGING_PORT;\n const parsedPort = rawPort ? Number.parseInt(rawPort, 10) : DEFAULT_WOOD_REMOTE_DEBUGGING_PORT;\n const port = Number.isFinite(parsedPort) && parsedPort > 0\n ? parsedPort\n : DEFAULT_WOOD_REMOTE_DEBUGGING_PORT;\n\n return {\n enabled: true,\n mode: 'development',\n port,\n };\n}\n\nfunction resolveWindowDef(\n windowDefs: Record<string, WoodWindowConfig>,\n windowId = 'main',\n): WoodWindowConfig {\n const def = windowDefs[windowId];\n if (!def) {\n throw new Error(`Window \"${windowId}\" not defined in generated window defs.`);\n }\n return def;\n}\n\nfunction resolveRuntimePath(relativeOrAbsolutePath: string): string {\n if (path.isAbsolute(relativeOrAbsolutePath)) {\n return relativeOrAbsolutePath;\n }\n return path.join(electronApp.getAppPath(), relativeOrAbsolutePath);\n}\n\nfunction toErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nexport function startWoodMain(options: StartWoodMainOptions): void {\n const logger = getLogger('wood:main').named('bootstrap');\n const runtimeLogger = getLogger('wood:main').named('runtime');\n const debugConfig = resolveWoodDebugMetadataConfiguration();\n const windowState = new WindowStateStore();\n const windows = new Map<string, ElectronBrowserWindow>();\n let mainWindow: ElectronBrowserWindow | null = null;\n let runtimeContext: WoodMainRuntimeContext | null = null;\n let mainBootCleanup: CleanupHandler | undefined;\n\n for (const stream of [process.stdout, process.stderr]) {\n stream.on('error', (error: NodeJS.ErrnoException) => {\n if (error.code === 'EPIPE') {\n return;\n }\n throw error;\n });\n }\n\n if (debugConfig.enabled && debugConfig.port) {\n electronApp.commandLine.appendSwitch('remote-debugging-port', String(debugConfig.port));\n electronApp.commandLine.appendSwitch('remote-debugging-address', '127.0.0.1');\n logger.info('devtools remote debugging enabled', {\n port: debugConfig.port,\n mode: debugConfig.mode,\n });\n }\n\n const bootContext: WoodMainBootContext = {\n electron: {\n app: electronApp,\n BrowserWindow,\n ipcMain,\n protocol,\n net,\n shell,\n screen,\n },\n logger,\n };\n\n void (async () => {\n const appBootResult = options.appBoot\n ? await options.appBoot(bootContext)\n : undefined;\n\n const bootOverrides = (appBootResult ?? {}) as WoodMainAppBootResult;\n const scope = bootOverrides.scope ?? options.scope ?? process.cwd();\n const configureContainer = bootOverrides.configureContainer ?? options.configureContainer;\n const privilegedSchemes = Array.isArray(bootOverrides.registerSchemesAsPrivileged)\n ? bootOverrides.registerSchemesAsPrivileged\n : [];\n\n if (privilegedSchemes.length > 0) {\n protocol.registerSchemesAsPrivileged(privilegedSchemes);\n logger.info('registered privileged schemes', {\n count: privilegedSchemes.length,\n schemes: privilegedSchemes.map((entry) => entry.scheme),\n });\n }\n\n const outDir = options.outDir ?? 'out';\n const preloadPath = path.join(electronApp.getAppPath(), outDir, 'preload', 'index.js');\n const rendererFile = path.join(electronApp.getAppPath(), outDir, 'renderer', 'index.html');\n\n const container = getContainer({\n scope,\n ...(configureContainer ? { configure: configureContainer } : {}),\n });\n\n const registrationContainer = container as {\n registerFunction?: (\n token: any,\n resolver: (...args: any[]) => unknown,\n options?: Record<string, unknown>,\n ) => void;\n };\n\n if (typeof registrationContainer.registerFunction === 'function') {\n registrationContainer.registerFunction(Ipc, () => getIpc());\n }\n\n const getWindow = (windowId: string): ElectronBrowserWindow | null => {\n const existing = windows.get(windowId);\n if (!existing) {\n return null;\n }\n if (existing.isDestroyed()) {\n windows.delete(windowId);\n return null;\n }\n return existing;\n };\n\n const findWindowIdByWebContents = (webContentsId: number): string | null => {\n for (const [windowId, win] of windows.entries()) {\n if (win.isDestroyed()) {\n windows.delete(windowId);\n continue;\n }\n if (win.webContents.id === webContentsId) {\n return windowId;\n }\n }\n return null;\n };\n\n const waitForWindowReady = async (win: ElectronBrowserWindow): Promise<void> => {\n const webContents = win.webContents as ElectronBrowserWindow['webContents'] & {\n isLoadingMainFrame?: () => boolean;\n };\n const isLoading = typeof webContents.isLoadingMainFrame === 'function'\n ? webContents.isLoadingMainFrame()\n : webContents.isLoading();\n if (!isLoading) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n webContents.once('did-finish-load', () => resolve());\n });\n };\n\n const getDebugMetadata = (): WoodDebugMetadata => {\n const debugWindows: WoodDebugWindowMetadata[] = [];\n\n for (const [windowId, win] of windows.entries()) {\n if (win.isDestroyed()) {\n windows.delete(windowId);\n continue;\n }\n\n debugWindows.push({\n windowId,\n title: win.getTitle(),\n webContentsId: win.webContents.id,\n url: win.webContents.getURL(),\n focused: win.isFocused(),\n });\n }\n\n return {\n ok: true,\n enabled: debugConfig.enabled,\n mode: debugConfig.mode,\n ...(debugConfig.port ? { port: debugConfig.port } : {}),\n windows: debugWindows,\n };\n };\n\n const createWindow = (windowId = 'main'): ElectronBrowserWindow => {\n const existing = getWindow(windowId);\n if (existing) {\n existing.show();\n existing.focus();\n return existing;\n }\n\n const windowDef = resolveWindowDef(options.windowDefs, windowId);\n const defaultShow = windowDef.show ?? true;\n const parentWindow = windowDef.parent ? getWindow(windowDef.parent) : null;\n\n windowState.setScreen(screen);\n const savedBounds = windowDef.rememberBounds ? windowState.getBounds(windowId) : null;\n\n const windowOptions: BrowserWindowConstructorOptions = {\n width: savedBounds?.width ?? windowDef.width,\n height: savedBounds?.height ?? windowDef.height,\n ...(savedBounds ? { x: savedBounds.x, y: savedBounds.y } : {}),\n ...(windowDef.minWidth != null ? { minWidth: windowDef.minWidth } : {}),\n ...(windowDef.minHeight != null ? { minHeight: windowDef.minHeight } : {}),\n ...(windowDef.maxWidth != null ? { maxWidth: windowDef.maxWidth } : {}),\n ...(windowDef.maxHeight != null ? { maxHeight: windowDef.maxHeight } : {}),\n ...(typeof windowDef.modal === 'boolean' ? { modal: windowDef.modal } : {}),\n ...(parentWindow ? { parent: parentWindow } : {}),\n title: windowDef.title,\n frame: windowDef.frame ?? true,\n transparent: windowDef.transparent ?? false,\n resizable: windowDef.resizable ?? true,\n alwaysOnTop: windowDef.alwaysOnTop ?? false,\n ...(typeof windowDef.backgroundColor === 'string' ? { backgroundColor: windowDef.backgroundColor } : {}),\n ...(process.platform === 'darwin' && typeof windowDef.vibrancy === 'string'\n ? {\n vibrancy: windowDef.vibrancy as BrowserWindowConstructorOptions['vibrancy'],\n visualEffectState: 'active' as const,\n }\n : {}),\n ...(process.platform === 'win32' && typeof windowDef.backgroundMaterial === 'string'\n ? {\n backgroundMaterial: windowDef.backgroundMaterial as BrowserWindowConstructorOptions['backgroundMaterial'],\n }\n : {}),\n ...(typeof windowDef.acceptFirstMouse === 'boolean' ? { acceptFirstMouse: windowDef.acceptFirstMouse } : {}),\n show: savedBounds ? false : defaultShow,\n webPreferences: {\n preload: preloadPath,\n contextIsolation: true,\n nodeIntegration: false,\n },\n };\n\n const win = new BrowserWindow(windowOptions);\n windows.set(windowId, win);\n if (windowId === 'main') {\n mainWindow = win;\n }\n\n if (savedBounds?.isMaximized) {\n win.maximize();\n } else if (savedBounds?.isFullScreen) {\n win.setFullScreen(true);\n }\n\n if (savedBounds && defaultShow) {\n win.show();\n }\n\n if (windowDef.rememberBounds) {\n const captureNormalBounds = () => {\n if (!win.isMaximized() && !win.isFullScreen()) {\n const rect = win.getBounds();\n windowState.updateBoundsDebounced(windowId, {\n ...rect,\n isMaximized: false,\n isFullScreen: false,\n });\n }\n };\n win.on('move', captureNormalBounds);\n win.on('resize', captureNormalBounds);\n win.on('close', () => {\n const rect = win.getBounds();\n windowState.saveBoundsSync(windowId, {\n ...rect,\n isMaximized: win.isMaximized(),\n isFullScreen: win.isFullScreen(),\n });\n });\n }\n\n win.webContents.setWindowOpenHandler(({ url }) => {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n void shell.openExternal(url);\n }\n return { action: 'deny' };\n });\n\n win.webContents.on('will-navigate', (event, url) => {\n if (process.env.ELECTRON_RENDERER_URL && url.startsWith(process.env.ELECTRON_RENDERER_URL)) {\n return;\n }\n if (url.startsWith('http://') || url.startsWith('https://')) {\n event.preventDefault();\n void shell.openExternal(url);\n }\n });\n\n const rendererLogger = getLogger('wood:main').named(`renderer:${windowId}`);\n const levelMap: Record<number, 'debug' | 'info' | 'warn' | 'error'> = {\n 0: 'debug',\n 1: 'info',\n 2: 'warn',\n 3: 'error',\n };\n\n win.webContents.on('console-message', (_event, level, message, line, sourceId) => {\n const method = levelMap[level] ?? 'info';\n rendererLogger[method](message, { source: sourceId, line, windowId });\n });\n\n if (process.env.ELECTRON_RENDERER_URL) {\n void win.loadURL(process.env.ELECTRON_RENDERER_URL);\n } else {\n void win.loadFile(rendererFile);\n }\n\n if (options.traceTransport === 'messageport') {\n const { port1, port2 } = new MessageChannelMain();\n\n const portHandler = (event: Electron.MessageEvent) => {\n dispatchRendererTracePayload(event.data);\n };\n port1.on('message', portHandler);\n port1.start();\n\n win.webContents.once('did-finish-load', () => {\n if (!win.isDestroyed()) {\n win.webContents.postMessage('__trace-port', null, [port2]);\n logger.info('trace messageport sent to renderer', { windowId });\n }\n });\n\n win.on('closed', () => {\n port1.off('message', portHandler);\n port1.close();\n });\n }\n\n win.on('closed', () => {\n windows.delete(windowId);\n if (windowId === 'main') {\n mainWindow = null;\n }\n });\n\n logger.info('window created', { windowId });\n return win;\n };\n\n const navigateWindow = async (windowId: string, route: string): Promise<void> => {\n const win = createWindow(windowId);\n await waitForWindowReady(win);\n win.webContents.send('__event:__navigate', { route });\n win.show();\n win.focus();\n };\n\n const dispatchPushEvent = (\n channel: string,\n data: unknown,\n options?: IpcPushOptions,\n ): void => {\n const eventChannel = channel.startsWith('__event:')\n ? channel\n : `__event:${channel}`;\n\n if (options?.windowId) {\n const target = getWindow(options.windowId);\n if (target && !target.isDestroyed()) {\n target.webContents.send(eventChannel, data);\n }\n return;\n }\n\n for (const [windowId, win] of windows.entries()) {\n if (win.isDestroyed()) {\n windows.delete(windowId);\n continue;\n }\n win.webContents.send(eventChannel, data);\n }\n };\n\n setIpcPushSender(dispatchPushEvent);\n\n const broadcast = (channel: string, data: unknown): void => {\n for (const [windowId, win] of windows.entries()) {\n if (win.isDestroyed()) {\n windows.delete(windowId);\n continue;\n }\n win.webContents.send(channel, data);\n }\n };\n\n runtimeContext = {\n ...bootContext,\n container,\n applicationRoot: '',\n getWindow,\n createWindow,\n navigateWindow,\n broadcast,\n setRendererLogCallback: (callback) => {\n getRendererLogDispatcher().setCallback(callback);\n },\n getRendererLogCallback: () => getRendererLogDispatcher().getCallback(),\n getDebugMetadata,\n };\n\n const bootstrap = async () => {\n logger.info('bootstrap starting');\n const applicationRoot = setApplicationRoot(path.join(electronApp.getPath('userData'), 'wood'));\n runtimeContext!.applicationRoot = applicationRoot;\n logger.info('application root configured', { applicationRoot });\n\n if (options.mainBoot) {\n const bootResult = await options.mainBoot(appBootResult, runtimeContext!);\n if (typeof bootResult === 'function') {\n mainBootCleanup = bootResult;\n }\n }\n\n await bootRuntime({\n container,\n ipcMain,\n registerRuntime: options.registerRuntime,\n controllersDir: resolveRuntimePath(options.controllersDir),\n middlewareDir: resolveRuntimePath(options.middlewareDir),\n ...(options.traceMinLevel ? { traceMinLevel: options.traceMinLevel } : {}),\n ...(options.traceTransport === 'messageport' ? { enableRendererTraceBridge: false } : {}),\n onLoad: options.onLoad ?? (async () => {\n throw new Error('Reserved channel \"__load\" is not configured for this app.');\n }),\n onNavigate: async (rawData) => {\n const data = rawData as { windowId?: string; route?: string };\n if (!data.windowId) {\n throw new Error('__navigate requires windowId');\n }\n if (!data.route) {\n throw new Error('__navigate requires route');\n }\n await navigateWindow(data.windowId, data.route);\n return { ok: true };\n },\n onWindow: async (rawData, event) => {\n const data = rawData as { action?: string; windowId?: string };\n const action = data.action ?? '';\n const windowId = data.windowId;\n\n if (action === 'current') {\n const ipcEvent = event as IpcMainInvokeEvent | undefined;\n const senderWindowId = ipcEvent?.sender ? findWindowIdByWebContents(ipcEvent.sender.id) : null;\n if (!senderWindowId) {\n return { ok: false };\n }\n const senderWindowDef = resolveWindowDef(options.windowDefs, senderWindowId);\n return {\n ok: true,\n windowId: senderWindowId,\n defaultRoute: senderWindowDef.defaultRoute,\n };\n }\n\n if (action === 'open') {\n if (!windowId) {\n throw new Error('__window.open requires windowId');\n }\n createWindow(windowId);\n return { ok: true };\n }\n\n const win = windowId ? getWindow(windowId) : mainWindow;\n if (!win) {\n return { ok: false };\n }\n\n switch (action) {\n case 'close':\n win.close();\n return { ok: true };\n case 'focus':\n win.show();\n win.focus();\n return { ok: true };\n case 'minimize':\n win.minimize();\n return { ok: true };\n case 'maximize':\n win.isMaximized() ? win.unmaximize() : win.maximize();\n return { ok: true };\n default:\n return { ok: false };\n }\n },\n onDebug: async (rawData) => {\n const data = rawData as { action?: string } | undefined;\n const action = data?.action ?? '';\n\n if (action === 'current') {\n return getDebugMetadata();\n }\n\n return { ok: false };\n },\n onContextMenu: options.onContextMenu ?? (async (_rawData: unknown, event?: unknown) => {\n const ipcEvent = event as IpcMainInvokeEvent | undefined;\n const senderId = ipcEvent?.sender?.id;\n runtimeLogger.debug('context menu requested', { senderId });\n return { ok: true };\n }),\n });\n\n createWindow('main');\n logger.info('bootstrap complete');\n };\n\n void electronApp.whenReady().then(bootstrap).catch((error) => {\n logger.error('bootstrap failed', {\n message: toErrorMessage(error),\n stack: error instanceof Error ? error.stack : undefined,\n });\n });\n\n electronApp.on('before-quit', () => {\n if (!runtimeContext) {\n return;\n }\n\n void (async () => {\n try {\n clearIpcPushSender();\n if (mainBootCleanup) {\n await mainBootCleanup();\n }\n if (options.onBeforeQuit) {\n await options.onBeforeQuit(runtimeContext!);\n }\n } catch (error) {\n logger.warn('before-quit handler failed', { error: toErrorMessage(error) });\n }\n })();\n\n for (const [id, win] of windows) {\n if (!win.isDestroyed()) {\n win.destroy();\n logger.debug('destroyed window on quit', { windowId: id });\n }\n }\n windows.clear();\n });\n\n electronApp.on('window-all-closed', () => {\n electronApp.quit();\n });\n\n electronApp.on('activate', () => {\n if (BrowserWindow.getAllWindows().length === 0) {\n createWindow('main');\n }\n });\n })().catch((error) => {\n logger.error('startup failed before ready', {\n message: toErrorMessage(error),\n stack: error instanceof Error ? error.stack : undefined,\n });\n });\n}\n"],"mappings":"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","names":["options"]}
@@ -26,6 +26,7 @@ var import_schema_validator = require("../validation/schema_validator.cjs");
26
26
  var import_middleware_resolver = require("../middleware/middleware_resolver.cjs");
27
27
  var import_middleware_pipeline = require("../middleware/middleware_pipeline.cjs");
28
28
  var import_controller_resolver = require("../controller/controller_resolver.cjs");
29
+ var import_controller_lifecycle = require("../controller/controller_lifecycle.cjs");
29
30
  var import_logger = require("@noego/logger");
30
31
  var import_tracing = require("../tracing/index.js");
31
32
  const logger = (0, import_logger.getLogger)("wood:router");
@@ -135,16 +136,16 @@ class WoodRouter {
135
136
  await (0, import_middleware_pipeline.executeMiddleware)(middlewareFns, mwContext);
136
137
  }
137
138
  if (opDef.inputSchema) {
138
- const result = this.validator.validate(
139
+ const result2 = this.validator.validate(
139
140
  channel,
140
141
  opDef.inputSchema,
141
142
  ipcContext.body
142
143
  );
143
- if (!result.valid) {
144
+ if (!result2.valid) {
144
145
  return {
145
146
  error: "VALIDATION_ERROR",
146
147
  message: "Request validation failed",
147
- details: result.errors
148
+ details: result2.errors
148
149
  };
149
150
  }
150
151
  }
@@ -153,13 +154,15 @@ class WoodRouter {
153
154
  const instance = await scopedContainer.instance(ControllerClass);
154
155
  const args = (0, import_controller_resolver.buildControllerArgs)(ipcContext, scopedContainer);
155
156
  const action = instance[opDef.action];
156
- if (typeof action !== "function") {
157
- throw new Error(
158
- `Controller "${opDef.controller}" has no action "${opDef.action}"`
159
- );
160
- }
157
+ let result;
158
+ let actionError;
161
159
  try {
162
- const result = await action.call(instance, args);
160
+ if (typeof action !== "function") {
161
+ throw new Error(
162
+ `Controller "${opDef.controller}" has no action "${opDef.action}"`
163
+ );
164
+ }
165
+ result = await action.call(instance, args);
163
166
  (0, import_tracing.publishMainTrace)("debug", {
164
167
  source: "wood.router",
165
168
  type: "dispatch.success",
@@ -170,8 +173,8 @@ class WoodRouter {
170
173
  action: opDef.action
171
174
  }
172
175
  });
173
- return result;
174
176
  } catch (error) {
177
+ actionError = error;
175
178
  (0, import_tracing.publishMainTrace)("error", {
176
179
  source: "wood.router",
177
180
  type: "dispatch.error",
@@ -183,8 +186,30 @@ class WoodRouter {
183
186
  action: opDef.action
184
187
  }
185
188
  });
186
- throw error;
189
+ } finally {
190
+ try {
191
+ await (0, import_controller_lifecycle.disposeControllerLifecycle)(instance);
192
+ } catch (disposeError) {
193
+ (0, import_tracing.publishMainTrace)("error", {
194
+ source: "wood.router",
195
+ type: "dispatch.dispose.error",
196
+ message: disposeError instanceof Error ? disposeError.message : String(disposeError),
197
+ ...conversationId !== void 0 ? { conversationId } : {},
198
+ payload: {
199
+ channel,
200
+ controller: opDef.controller,
201
+ action: opDef.action
202
+ }
203
+ });
204
+ if (!actionError) {
205
+ actionError = disposeError;
206
+ }
207
+ }
208
+ }
209
+ if (actionError) {
210
+ throw actionError;
187
211
  }
212
+ return result;
188
213
  }
189
214
  /**
190
215
  * Pre-register a controller class by name so it is resolved from
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/router/wood_router.ts"],"sourcesContent":["import type { OperationDef, IpcContext, WoodMiddlewareContext } from '../types/index.cjs';\nimport { SchemaValidator } from '../validation/schema_validator.cjs';\nimport { MiddlewareResolver } from '../middleware/middleware_resolver.cjs';\nimport { buildMiddlewareContext, executeMiddleware } from '../middleware/middleware_pipeline.cjs';\nimport { ControllerResolver, buildControllerArgs } from '../controller/controller_resolver.cjs';\nimport { getLogger } from '@noego/logger';\nimport { publishMainTrace } from '../tracing/index.js';\n\nconst logger = getLogger('wood:router');\n\nfunction extractTraceConversationId(value: unknown): number | string | undefined {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return undefined;\n }\n\n const record = value as Record<string, unknown>;\n if (typeof record.conversationId === 'number' || typeof record.conversationId === 'string') {\n return record.conversationId;\n }\n if (typeof record.threadId === 'number' || typeof record.threadId === 'string') {\n return record.threadId;\n }\n\n return undefined;\n}\n\n/** Reserved internal channels prefixed with __ to avoid collisions with user operations. */\nexport const RESERVED_CHANNELS = ['__load', '__navigate', '__window', '__debug', '__contextMenu', '__woodConfig'] as const;\nexport type ReservedChannel = (typeof RESERVED_CHANNELS)[number];\n\n/** Handler signature for reserved channels. */\nexport type ReservedChannelHandler = (data: unknown, event?: any) => Promise<unknown>;\n\nexport class WoodRouter {\n private operations = new Map<string, OperationDef>();\n private reservedHandlers = new Map<ReservedChannel, ReservedChannelHandler>();\n private validator: SchemaValidator;\n private middlewareResolver: MiddlewareResolver;\n private controllerResolver: ControllerResolver;\n private container: any; // IoC Container\n\n constructor(options: {\n container: any;\n controllersDir: string;\n middlewareDir: string;\n mode?: 'bundled' | 'dev';\n }) {\n this.container = options.container;\n this.validator = new SchemaValidator();\n this.middlewareResolver = new MiddlewareResolver(options.middlewareDir);\n this.controllerResolver = new ControllerResolver(options.controllersDir, {\n mode: options.mode,\n });\n }\n\n /**\n * Load operations from a parsed operations map.\n */\n loadOperations(operations: Map<string, OperationDef>): void {\n for (const [channel, opDef] of operations) {\n this.operations.set(channel, opDef);\n }\n logger.debug(`Loaded ${operations.size} operations`);\n }\n\n /**\n * Register a handler for a reserved channel (__load, __navigate, __window).\n * These channels are internal to Wood and serve as extension points for\n * the loader runner, navigation system, and window manager.\n */\n registerReservedHandler(channel: ReservedChannel, handler: ReservedChannelHandler): void {\n this.reservedHandlers.set(channel, handler);\n logger.debug(`Registered reserved handler for \"${channel}\"`);\n }\n\n /**\n * Register IPC handlers for all operations and reserved channels.\n * In production: registers with ipcMain.handle\n * In testing/bridge mode: handlers are callable via dispatch()\n */\n registerIpcHandlers(ipcMain: any): void {\n for (const channel of this.operations.keys()) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n return this.dispatch(channel, data, _event);\n });\n }\n\n // Register reserved channels\n for (const channel of RESERVED_CHANNELS) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n const handler = this.reservedHandlers.get(channel);\n if (!handler) {\n throw new Error(`No handler registered for reserved channel \"${channel}\"`);\n }\n return handler(data, _event);\n });\n }\n\n logger.debug(\n `Registered ${this.operations.size} operation handlers and ${RESERVED_CHANNELS.length} reserved channels`\n );\n }\n\n /**\n * The request lifecycle -- mirrors Dinner's pipeline.\n *\n * channel -> build context -> middleware -> validate -> resolve controller\n * -> create instance -> build args -> call action -> return result\n */\n async dispatch(channel: string, data: unknown, event?: any): Promise<unknown> {\n const opDef = this.operations.get(channel);\n if (!opDef) {\n throw new Error(`Unknown operation: \"${channel}\"`);\n }\n\n const conversationId = extractTraceConversationId(data);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.start',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n // 1. Build IPC context\n const ipcContext: IpcContext = {\n body: data ?? {},\n params: (data as any) ?? {},\n query: {},\n context: {},\n sender: event?.sender,\n windowId: undefined,\n };\n\n // 2. Execute middleware sequentially\n if (opDef.middleware.length > 0) {\n const mwContext = buildMiddlewareContext(ipcContext, channel);\n const middlewareFns = await Promise.all(\n opDef.middleware.map(spec => this.middlewareResolver.resolve(spec))\n );\n await executeMiddleware(middlewareFns, mwContext);\n }\n\n // 3. Validate input (if schema defined)\n if (opDef.inputSchema) {\n const result = this.validator.validate(\n channel,\n opDef.inputSchema as Record<string, unknown>,\n ipcContext.body\n );\n if (!result.valid) {\n return {\n error: 'VALIDATION_ERROR',\n message: 'Request validation failed',\n details: result.errors,\n };\n }\n }\n\n // 4. Resolve controller class (cached)\n const ControllerClass = await this.controllerResolver.resolveClass(opDef.controller);\n\n // 5. Create instance via IoC (per-request scoped container)\n const scopedContainer = this.container.extend();\n const instance = await scopedContainer.instance(ControllerClass);\n\n // 6. Build controller args (fake req/res)\n const args = buildControllerArgs(ipcContext, scopedContainer);\n\n // 7. Call action, return result\n const action = instance[opDef.action];\n if (typeof action !== 'function') {\n throw new Error(\n `Controller \"${opDef.controller}\" has no action \"${opDef.action}\"`\n );\n }\n\n try {\n const result = await action.call(instance, args);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.success',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n return result;\n } catch (error) {\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.error',\n message: error instanceof Error ? error.message : String(error),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n throw error;\n }\n }\n\n /**\n * Pre-register a controller class by name so it is resolved from\n * the in-memory cache instead of the filesystem.\n * This is the public API used by the generated registration file.\n */\n registerController(name: string, ControllerClass: any): void {\n this.controllerResolver.register(name, ControllerClass);\n }\n\n /**\n * Register an import path for a controller. The controller will be lazily\n * imported from this path when first needed, avoiding circular dependencies.\n */\n registerControllerPath(name: string, importPath: string): void {\n this.controllerResolver.registerPath(name, importPath);\n }\n\n /**\n * Validate that every controller referenced in loaded operations is\n * pre-registered in the controller resolver. Call this after\n * loadOperations() + registerControllers() but before registerIpcHandlers().\n *\n * Throws with a clear error listing all missing controllers if any\n * operation references a controller that was not pre-registered.\n */\n validateControllers(): void {\n const missing: Array<{ channel: string; controller: string }> = [];\n\n for (const [channel, opDef] of this.operations) {\n if (!this.controllerResolver.has(opDef.controller)) {\n missing.push({ channel, controller: opDef.controller });\n }\n }\n\n if (missing.length > 0) {\n const details = missing\n .map(m => ` - channel \"${m.channel}\" requires controller \"${m.controller}\"`)\n .join('\\n');\n\n throw new Error(\n `WoodRouter: ${missing.length} operation(s) reference controllers that are not registered.\\n` +\n `This usually means controllers.generated.ts is missing or out of date.\\n` +\n `Run \"npx wood build\" or \"npx wood dev\" to regenerate it.\\n\\n` +\n `Missing controllers:\\n${details}`\n );\n }\n\n logger.info(`All ${this.operations.size} operations have registered controllers`);\n }\n\n /** Get all registered operation channels. */\n getChannels(): string[] {\n return Array.from(this.operations.keys());\n }\n\n /** Get a specific operation definition. */\n getOperation(channel: string): OperationDef | undefined {\n return this.operations.get(channel);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/router/wood_router.ts"],"sourcesContent":["import type { OperationDef, IpcContext, WoodMiddlewareContext } from '../types/index.cjs';\nimport { SchemaValidator } from '../validation/schema_validator.cjs';\nimport { MiddlewareResolver } from '../middleware/middleware_resolver.cjs';\nimport { buildMiddlewareContext, executeMiddleware } from '../middleware/middleware_pipeline.cjs';\nimport { ControllerResolver, buildControllerArgs } from '../controller/controller_resolver.cjs';\nimport { disposeControllerLifecycle } from '../controller/controller_lifecycle.cjs';\nimport { getLogger } from '@noego/logger';\nimport { publishMainTrace } from '../tracing/index.js';\n\nconst logger = getLogger('wood:router');\n\nfunction extractTraceConversationId(value: unknown): number | string | undefined {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return undefined;\n }\n\n const record = value as Record<string, unknown>;\n if (typeof record.conversationId === 'number' || typeof record.conversationId === 'string') {\n return record.conversationId;\n }\n if (typeof record.threadId === 'number' || typeof record.threadId === 'string') {\n return record.threadId;\n }\n\n return undefined;\n}\n\n/** Reserved internal channels prefixed with __ to avoid collisions with user operations. */\nexport const RESERVED_CHANNELS = ['__load', '__navigate', '__window', '__debug', '__contextMenu', '__woodConfig'] as const;\nexport type ReservedChannel = (typeof RESERVED_CHANNELS)[number];\n\n/** Handler signature for reserved channels. */\nexport type ReservedChannelHandler = (data: unknown, event?: any) => Promise<unknown>;\n\nexport class WoodRouter {\n private operations = new Map<string, OperationDef>();\n private reservedHandlers = new Map<ReservedChannel, ReservedChannelHandler>();\n private validator: SchemaValidator;\n private middlewareResolver: MiddlewareResolver;\n private controllerResolver: ControllerResolver;\n private container: any; // IoC Container\n\n constructor(options: {\n container: any;\n controllersDir: string;\n middlewareDir: string;\n mode?: 'bundled' | 'dev';\n }) {\n this.container = options.container;\n this.validator = new SchemaValidator();\n this.middlewareResolver = new MiddlewareResolver(options.middlewareDir);\n this.controllerResolver = new ControllerResolver(options.controllersDir, {\n mode: options.mode,\n });\n }\n\n /**\n * Load operations from a parsed operations map.\n */\n loadOperations(operations: Map<string, OperationDef>): void {\n for (const [channel, opDef] of operations) {\n this.operations.set(channel, opDef);\n }\n logger.debug(`Loaded ${operations.size} operations`);\n }\n\n /**\n * Register a handler for a reserved channel (__load, __navigate, __window).\n * These channels are internal to Wood and serve as extension points for\n * the loader runner, navigation system, and window manager.\n */\n registerReservedHandler(channel: ReservedChannel, handler: ReservedChannelHandler): void {\n this.reservedHandlers.set(channel, handler);\n logger.debug(`Registered reserved handler for \"${channel}\"`);\n }\n\n /**\n * Register IPC handlers for all operations and reserved channels.\n * In production: registers with ipcMain.handle\n * In testing/bridge mode: handlers are callable via dispatch()\n */\n registerIpcHandlers(ipcMain: any): void {\n for (const channel of this.operations.keys()) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n return this.dispatch(channel, data, _event);\n });\n }\n\n // Register reserved channels\n for (const channel of RESERVED_CHANNELS) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n const handler = this.reservedHandlers.get(channel);\n if (!handler) {\n throw new Error(`No handler registered for reserved channel \"${channel}\"`);\n }\n return handler(data, _event);\n });\n }\n\n logger.debug(\n `Registered ${this.operations.size} operation handlers and ${RESERVED_CHANNELS.length} reserved channels`\n );\n }\n\n /**\n * The request lifecycle -- mirrors Dinner's pipeline.\n *\n * channel -> build context -> middleware -> validate -> resolve controller\n * -> create instance -> build args -> call action -> return result\n */\n async dispatch(channel: string, data: unknown, event?: any): Promise<unknown> {\n const opDef = this.operations.get(channel);\n if (!opDef) {\n throw new Error(`Unknown operation: \"${channel}\"`);\n }\n\n const conversationId = extractTraceConversationId(data);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.start',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n // 1. Build IPC context\n const ipcContext: IpcContext = {\n body: data ?? {},\n params: (data as any) ?? {},\n query: {},\n context: {},\n sender: event?.sender,\n windowId: undefined,\n };\n\n // 2. Execute middleware sequentially\n if (opDef.middleware.length > 0) {\n const mwContext = buildMiddlewareContext(ipcContext, channel);\n const middlewareFns = await Promise.all(\n opDef.middleware.map(spec => this.middlewareResolver.resolve(spec))\n );\n await executeMiddleware(middlewareFns, mwContext);\n }\n\n // 3. Validate input (if schema defined)\n if (opDef.inputSchema) {\n const result = this.validator.validate(\n channel,\n opDef.inputSchema as Record<string, unknown>,\n ipcContext.body\n );\n if (!result.valid) {\n return {\n error: 'VALIDATION_ERROR',\n message: 'Request validation failed',\n details: result.errors,\n };\n }\n }\n\n // 4. Resolve controller class (cached)\n const ControllerClass = await this.controllerResolver.resolveClass(opDef.controller);\n\n // 5. Create instance via IoC (per-request scoped container)\n const scopedContainer = this.container.extend();\n const instance = await scopedContainer.instance(ControllerClass);\n\n // 6. Build controller args (fake req/res)\n const args = buildControllerArgs(ipcContext, scopedContainer);\n\n // 7. Call action, always dispose controller afterward\n const action = instance[opDef.action];\n let result: unknown;\n let actionError: unknown;\n\n try {\n if (typeof action !== 'function') {\n throw new Error(\n `Controller \"${opDef.controller}\" has no action \"${opDef.action}\"`\n );\n }\n\n result = await action.call(instance, args);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.success',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n } catch (error) {\n actionError = error;\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.error',\n message: error instanceof Error ? error.message : String(error),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n } finally {\n try {\n await disposeControllerLifecycle(instance);\n } catch (disposeError) {\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.dispose.error',\n message: disposeError instanceof Error ? disposeError.message : String(disposeError),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n if (!actionError) {\n actionError = disposeError;\n }\n }\n }\n\n if (actionError) {\n throw actionError;\n }\n\n return result;\n }\n\n /**\n * Pre-register a controller class by name so it is resolved from\n * the in-memory cache instead of the filesystem.\n * This is the public API used by the generated registration file.\n */\n registerController(name: string, ControllerClass: any): void {\n this.controllerResolver.register(name, ControllerClass);\n }\n\n /**\n * Register an import path for a controller. The controller will be lazily\n * imported from this path when first needed, avoiding circular dependencies.\n */\n registerControllerPath(name: string, importPath: string): void {\n this.controllerResolver.registerPath(name, importPath);\n }\n\n /**\n * Validate that every controller referenced in loaded operations is\n * pre-registered in the controller resolver. Call this after\n * loadOperations() + registerControllers() but before registerIpcHandlers().\n *\n * Throws with a clear error listing all missing controllers if any\n * operation references a controller that was not pre-registered.\n */\n validateControllers(): void {\n const missing: Array<{ channel: string; controller: string }> = [];\n\n for (const [channel, opDef] of this.operations) {\n if (!this.controllerResolver.has(opDef.controller)) {\n missing.push({ channel, controller: opDef.controller });\n }\n }\n\n if (missing.length > 0) {\n const details = missing\n .map(m => ` - channel \"${m.channel}\" requires controller \"${m.controller}\"`)\n .join('\\n');\n\n throw new Error(\n `WoodRouter: ${missing.length} operation(s) reference controllers that are not registered.\\n` +\n `This usually means controllers.generated.ts is missing or out of date.\\n` +\n `Run \"npx wood build\" or \"npx wood dev\" to regenerate it.\\n\\n` +\n `Missing controllers:\\n${details}`\n );\n }\n\n logger.info(`All ${this.operations.size} operations have registered controllers`);\n }\n\n /** Get all registered operation channels. */\n getChannels(): string[] {\n return Array.from(this.operations.keys());\n }\n\n /** Get a specific operation definition. */\n getOperation(channel: string): OperationDef | undefined {\n return this.operations.get(channel);\n }\n}\n"],"mappings":"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","names":["result"]}
@@ -2,6 +2,7 @@ import { SchemaValidator } from "../validation/schema_validator.js";
2
2
  import { MiddlewareResolver } from "../middleware/middleware_resolver.js";
3
3
  import { buildMiddlewareContext, executeMiddleware } from "../middleware/middleware_pipeline.js";
4
4
  import { ControllerResolver, buildControllerArgs } from "../controller/controller_resolver.js";
5
+ import { disposeControllerLifecycle } from "../controller/controller_lifecycle.js";
5
6
  import { getLogger } from "@noego/logger";
6
7
  import { publishMainTrace } from "../tracing/index.js";
7
8
  const logger = getLogger("wood:router");
@@ -111,16 +112,16 @@ class WoodRouter {
111
112
  await executeMiddleware(middlewareFns, mwContext);
112
113
  }
113
114
  if (opDef.inputSchema) {
114
- const result = this.validator.validate(
115
+ const result2 = this.validator.validate(
115
116
  channel,
116
117
  opDef.inputSchema,
117
118
  ipcContext.body
118
119
  );
119
- if (!result.valid) {
120
+ if (!result2.valid) {
120
121
  return {
121
122
  error: "VALIDATION_ERROR",
122
123
  message: "Request validation failed",
123
- details: result.errors
124
+ details: result2.errors
124
125
  };
125
126
  }
126
127
  }
@@ -129,13 +130,15 @@ class WoodRouter {
129
130
  const instance = await scopedContainer.instance(ControllerClass);
130
131
  const args = buildControllerArgs(ipcContext, scopedContainer);
131
132
  const action = instance[opDef.action];
132
- if (typeof action !== "function") {
133
- throw new Error(
134
- `Controller "${opDef.controller}" has no action "${opDef.action}"`
135
- );
136
- }
133
+ let result;
134
+ let actionError;
137
135
  try {
138
- const result = await action.call(instance, args);
136
+ if (typeof action !== "function") {
137
+ throw new Error(
138
+ `Controller "${opDef.controller}" has no action "${opDef.action}"`
139
+ );
140
+ }
141
+ result = await action.call(instance, args);
139
142
  publishMainTrace("debug", {
140
143
  source: "wood.router",
141
144
  type: "dispatch.success",
@@ -146,8 +149,8 @@ class WoodRouter {
146
149
  action: opDef.action
147
150
  }
148
151
  });
149
- return result;
150
152
  } catch (error) {
153
+ actionError = error;
151
154
  publishMainTrace("error", {
152
155
  source: "wood.router",
153
156
  type: "dispatch.error",
@@ -159,8 +162,30 @@ class WoodRouter {
159
162
  action: opDef.action
160
163
  }
161
164
  });
162
- throw error;
165
+ } finally {
166
+ try {
167
+ await disposeControllerLifecycle(instance);
168
+ } catch (disposeError) {
169
+ publishMainTrace("error", {
170
+ source: "wood.router",
171
+ type: "dispatch.dispose.error",
172
+ message: disposeError instanceof Error ? disposeError.message : String(disposeError),
173
+ ...conversationId !== void 0 ? { conversationId } : {},
174
+ payload: {
175
+ channel,
176
+ controller: opDef.controller,
177
+ action: opDef.action
178
+ }
179
+ });
180
+ if (!actionError) {
181
+ actionError = disposeError;
182
+ }
183
+ }
184
+ }
185
+ if (actionError) {
186
+ throw actionError;
163
187
  }
188
+ return result;
164
189
  }
165
190
  /**
166
191
  * Pre-register a controller class by name so it is resolved from
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/router/wood_router.ts"],"sourcesContent":["import type { OperationDef, IpcContext, WoodMiddlewareContext } from '../types/index.js';\nimport { SchemaValidator } from '../validation/schema_validator.js';\nimport { MiddlewareResolver } from '../middleware/middleware_resolver.js';\nimport { buildMiddlewareContext, executeMiddleware } from '../middleware/middleware_pipeline.js';\nimport { ControllerResolver, buildControllerArgs } from '../controller/controller_resolver.js';\nimport { getLogger } from '@noego/logger';\nimport { publishMainTrace } from '../tracing/index.js';\n\nconst logger = getLogger('wood:router');\n\nfunction extractTraceConversationId(value: unknown): number | string | undefined {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return undefined;\n }\n\n const record = value as Record<string, unknown>;\n if (typeof record.conversationId === 'number' || typeof record.conversationId === 'string') {\n return record.conversationId;\n }\n if (typeof record.threadId === 'number' || typeof record.threadId === 'string') {\n return record.threadId;\n }\n\n return undefined;\n}\n\n/** Reserved internal channels prefixed with __ to avoid collisions with user operations. */\nexport const RESERVED_CHANNELS = ['__load', '__navigate', '__window', '__debug', '__contextMenu', '__woodConfig'] as const;\nexport type ReservedChannel = (typeof RESERVED_CHANNELS)[number];\n\n/** Handler signature for reserved channels. */\nexport type ReservedChannelHandler = (data: unknown, event?: any) => Promise<unknown>;\n\nexport class WoodRouter {\n private operations = new Map<string, OperationDef>();\n private reservedHandlers = new Map<ReservedChannel, ReservedChannelHandler>();\n private validator: SchemaValidator;\n private middlewareResolver: MiddlewareResolver;\n private controllerResolver: ControllerResolver;\n private container: any; // IoC Container\n\n constructor(options: {\n container: any;\n controllersDir: string;\n middlewareDir: string;\n mode?: 'bundled' | 'dev';\n }) {\n this.container = options.container;\n this.validator = new SchemaValidator();\n this.middlewareResolver = new MiddlewareResolver(options.middlewareDir);\n this.controllerResolver = new ControllerResolver(options.controllersDir, {\n mode: options.mode,\n });\n }\n\n /**\n * Load operations from a parsed operations map.\n */\n loadOperations(operations: Map<string, OperationDef>): void {\n for (const [channel, opDef] of operations) {\n this.operations.set(channel, opDef);\n }\n logger.debug(`Loaded ${operations.size} operations`);\n }\n\n /**\n * Register a handler for a reserved channel (__load, __navigate, __window).\n * These channels are internal to Wood and serve as extension points for\n * the loader runner, navigation system, and window manager.\n */\n registerReservedHandler(channel: ReservedChannel, handler: ReservedChannelHandler): void {\n this.reservedHandlers.set(channel, handler);\n logger.debug(`Registered reserved handler for \"${channel}\"`);\n }\n\n /**\n * Register IPC handlers for all operations and reserved channels.\n * In production: registers with ipcMain.handle\n * In testing/bridge mode: handlers are callable via dispatch()\n */\n registerIpcHandlers(ipcMain: any): void {\n for (const channel of this.operations.keys()) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n return this.dispatch(channel, data, _event);\n });\n }\n\n // Register reserved channels\n for (const channel of RESERVED_CHANNELS) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n const handler = this.reservedHandlers.get(channel);\n if (!handler) {\n throw new Error(`No handler registered for reserved channel \"${channel}\"`);\n }\n return handler(data, _event);\n });\n }\n\n logger.debug(\n `Registered ${this.operations.size} operation handlers and ${RESERVED_CHANNELS.length} reserved channels`\n );\n }\n\n /**\n * The request lifecycle -- mirrors Dinner's pipeline.\n *\n * channel -> build context -> middleware -> validate -> resolve controller\n * -> create instance -> build args -> call action -> return result\n */\n async dispatch(channel: string, data: unknown, event?: any): Promise<unknown> {\n const opDef = this.operations.get(channel);\n if (!opDef) {\n throw new Error(`Unknown operation: \"${channel}\"`);\n }\n\n const conversationId = extractTraceConversationId(data);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.start',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n // 1. Build IPC context\n const ipcContext: IpcContext = {\n body: data ?? {},\n params: (data as any) ?? {},\n query: {},\n context: {},\n sender: event?.sender,\n windowId: undefined,\n };\n\n // 2. Execute middleware sequentially\n if (opDef.middleware.length > 0) {\n const mwContext = buildMiddlewareContext(ipcContext, channel);\n const middlewareFns = await Promise.all(\n opDef.middleware.map(spec => this.middlewareResolver.resolve(spec))\n );\n await executeMiddleware(middlewareFns, mwContext);\n }\n\n // 3. Validate input (if schema defined)\n if (opDef.inputSchema) {\n const result = this.validator.validate(\n channel,\n opDef.inputSchema as Record<string, unknown>,\n ipcContext.body\n );\n if (!result.valid) {\n return {\n error: 'VALIDATION_ERROR',\n message: 'Request validation failed',\n details: result.errors,\n };\n }\n }\n\n // 4. Resolve controller class (cached)\n const ControllerClass = await this.controllerResolver.resolveClass(opDef.controller);\n\n // 5. Create instance via IoC (per-request scoped container)\n const scopedContainer = this.container.extend();\n const instance = await scopedContainer.instance(ControllerClass);\n\n // 6. Build controller args (fake req/res)\n const args = buildControllerArgs(ipcContext, scopedContainer);\n\n // 7. Call action, return result\n const action = instance[opDef.action];\n if (typeof action !== 'function') {\n throw new Error(\n `Controller \"${opDef.controller}\" has no action \"${opDef.action}\"`\n );\n }\n\n try {\n const result = await action.call(instance, args);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.success',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n return result;\n } catch (error) {\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.error',\n message: error instanceof Error ? error.message : String(error),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n throw error;\n }\n }\n\n /**\n * Pre-register a controller class by name so it is resolved from\n * the in-memory cache instead of the filesystem.\n * This is the public API used by the generated registration file.\n */\n registerController(name: string, ControllerClass: any): void {\n this.controllerResolver.register(name, ControllerClass);\n }\n\n /**\n * Register an import path for a controller. The controller will be lazily\n * imported from this path when first needed, avoiding circular dependencies.\n */\n registerControllerPath(name: string, importPath: string): void {\n this.controllerResolver.registerPath(name, importPath);\n }\n\n /**\n * Validate that every controller referenced in loaded operations is\n * pre-registered in the controller resolver. Call this after\n * loadOperations() + registerControllers() but before registerIpcHandlers().\n *\n * Throws with a clear error listing all missing controllers if any\n * operation references a controller that was not pre-registered.\n */\n validateControllers(): void {\n const missing: Array<{ channel: string; controller: string }> = [];\n\n for (const [channel, opDef] of this.operations) {\n if (!this.controllerResolver.has(opDef.controller)) {\n missing.push({ channel, controller: opDef.controller });\n }\n }\n\n if (missing.length > 0) {\n const details = missing\n .map(m => ` - channel \"${m.channel}\" requires controller \"${m.controller}\"`)\n .join('\\n');\n\n throw new Error(\n `WoodRouter: ${missing.length} operation(s) reference controllers that are not registered.\\n` +\n `This usually means controllers.generated.ts is missing or out of date.\\n` +\n `Run \"npx wood build\" or \"npx wood dev\" to regenerate it.\\n\\n` +\n `Missing controllers:\\n${details}`\n );\n }\n\n logger.info(`All ${this.operations.size} operations have registered controllers`);\n }\n\n /** Get all registered operation channels. */\n getChannels(): string[] {\n return Array.from(this.operations.keys());\n }\n\n /** Get a specific operation definition. */\n getOperation(channel: string): OperationDef | undefined {\n return this.operations.get(channel);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/router/wood_router.ts"],"sourcesContent":["import type { OperationDef, IpcContext, WoodMiddlewareContext } from '../types/index.js';\nimport { SchemaValidator } from '../validation/schema_validator.js';\nimport { MiddlewareResolver } from '../middleware/middleware_resolver.js';\nimport { buildMiddlewareContext, executeMiddleware } from '../middleware/middleware_pipeline.js';\nimport { ControllerResolver, buildControllerArgs } from '../controller/controller_resolver.js';\nimport { disposeControllerLifecycle } from '../controller/controller_lifecycle.js';\nimport { getLogger } from '@noego/logger';\nimport { publishMainTrace } from '../tracing/index.js';\n\nconst logger = getLogger('wood:router');\n\nfunction extractTraceConversationId(value: unknown): number | string | undefined {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return undefined;\n }\n\n const record = value as Record<string, unknown>;\n if (typeof record.conversationId === 'number' || typeof record.conversationId === 'string') {\n return record.conversationId;\n }\n if (typeof record.threadId === 'number' || typeof record.threadId === 'string') {\n return record.threadId;\n }\n\n return undefined;\n}\n\n/** Reserved internal channels prefixed with __ to avoid collisions with user operations. */\nexport const RESERVED_CHANNELS = ['__load', '__navigate', '__window', '__debug', '__contextMenu', '__woodConfig'] as const;\nexport type ReservedChannel = (typeof RESERVED_CHANNELS)[number];\n\n/** Handler signature for reserved channels. */\nexport type ReservedChannelHandler = (data: unknown, event?: any) => Promise<unknown>;\n\nexport class WoodRouter {\n private operations = new Map<string, OperationDef>();\n private reservedHandlers = new Map<ReservedChannel, ReservedChannelHandler>();\n private validator: SchemaValidator;\n private middlewareResolver: MiddlewareResolver;\n private controllerResolver: ControllerResolver;\n private container: any; // IoC Container\n\n constructor(options: {\n container: any;\n controllersDir: string;\n middlewareDir: string;\n mode?: 'bundled' | 'dev';\n }) {\n this.container = options.container;\n this.validator = new SchemaValidator();\n this.middlewareResolver = new MiddlewareResolver(options.middlewareDir);\n this.controllerResolver = new ControllerResolver(options.controllersDir, {\n mode: options.mode,\n });\n }\n\n /**\n * Load operations from a parsed operations map.\n */\n loadOperations(operations: Map<string, OperationDef>): void {\n for (const [channel, opDef] of operations) {\n this.operations.set(channel, opDef);\n }\n logger.debug(`Loaded ${operations.size} operations`);\n }\n\n /**\n * Register a handler for a reserved channel (__load, __navigate, __window).\n * These channels are internal to Wood and serve as extension points for\n * the loader runner, navigation system, and window manager.\n */\n registerReservedHandler(channel: ReservedChannel, handler: ReservedChannelHandler): void {\n this.reservedHandlers.set(channel, handler);\n logger.debug(`Registered reserved handler for \"${channel}\"`);\n }\n\n /**\n * Register IPC handlers for all operations and reserved channels.\n * In production: registers with ipcMain.handle\n * In testing/bridge mode: handlers are callable via dispatch()\n */\n registerIpcHandlers(ipcMain: any): void {\n for (const channel of this.operations.keys()) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n return this.dispatch(channel, data, _event);\n });\n }\n\n // Register reserved channels\n for (const channel of RESERVED_CHANNELS) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n const handler = this.reservedHandlers.get(channel);\n if (!handler) {\n throw new Error(`No handler registered for reserved channel \"${channel}\"`);\n }\n return handler(data, _event);\n });\n }\n\n logger.debug(\n `Registered ${this.operations.size} operation handlers and ${RESERVED_CHANNELS.length} reserved channels`\n );\n }\n\n /**\n * The request lifecycle -- mirrors Dinner's pipeline.\n *\n * channel -> build context -> middleware -> validate -> resolve controller\n * -> create instance -> build args -> call action -> return result\n */\n async dispatch(channel: string, data: unknown, event?: any): Promise<unknown> {\n const opDef = this.operations.get(channel);\n if (!opDef) {\n throw new Error(`Unknown operation: \"${channel}\"`);\n }\n\n const conversationId = extractTraceConversationId(data);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.start',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n // 1. Build IPC context\n const ipcContext: IpcContext = {\n body: data ?? {},\n params: (data as any) ?? {},\n query: {},\n context: {},\n sender: event?.sender,\n windowId: undefined,\n };\n\n // 2. Execute middleware sequentially\n if (opDef.middleware.length > 0) {\n const mwContext = buildMiddlewareContext(ipcContext, channel);\n const middlewareFns = await Promise.all(\n opDef.middleware.map(spec => this.middlewareResolver.resolve(spec))\n );\n await executeMiddleware(middlewareFns, mwContext);\n }\n\n // 3. Validate input (if schema defined)\n if (opDef.inputSchema) {\n const result = this.validator.validate(\n channel,\n opDef.inputSchema as Record<string, unknown>,\n ipcContext.body\n );\n if (!result.valid) {\n return {\n error: 'VALIDATION_ERROR',\n message: 'Request validation failed',\n details: result.errors,\n };\n }\n }\n\n // 4. Resolve controller class (cached)\n const ControllerClass = await this.controllerResolver.resolveClass(opDef.controller);\n\n // 5. Create instance via IoC (per-request scoped container)\n const scopedContainer = this.container.extend();\n const instance = await scopedContainer.instance(ControllerClass);\n\n // 6. Build controller args (fake req/res)\n const args = buildControllerArgs(ipcContext, scopedContainer);\n\n // 7. Call action, always dispose controller afterward\n const action = instance[opDef.action];\n let result: unknown;\n let actionError: unknown;\n\n try {\n if (typeof action !== 'function') {\n throw new Error(\n `Controller \"${opDef.controller}\" has no action \"${opDef.action}\"`\n );\n }\n\n result = await action.call(instance, args);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.success',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n } catch (error) {\n actionError = error;\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.error',\n message: error instanceof Error ? error.message : String(error),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n } finally {\n try {\n await disposeControllerLifecycle(instance);\n } catch (disposeError) {\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.dispose.error',\n message: disposeError instanceof Error ? disposeError.message : String(disposeError),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n if (!actionError) {\n actionError = disposeError;\n }\n }\n }\n\n if (actionError) {\n throw actionError;\n }\n\n return result;\n }\n\n /**\n * Pre-register a controller class by name so it is resolved from\n * the in-memory cache instead of the filesystem.\n * This is the public API used by the generated registration file.\n */\n registerController(name: string, ControllerClass: any): void {\n this.controllerResolver.register(name, ControllerClass);\n }\n\n /**\n * Register an import path for a controller. The controller will be lazily\n * imported from this path when first needed, avoiding circular dependencies.\n */\n registerControllerPath(name: string, importPath: string): void {\n this.controllerResolver.registerPath(name, importPath);\n }\n\n /**\n * Validate that every controller referenced in loaded operations is\n * pre-registered in the controller resolver. Call this after\n * loadOperations() + registerControllers() but before registerIpcHandlers().\n *\n * Throws with a clear error listing all missing controllers if any\n * operation references a controller that was not pre-registered.\n */\n validateControllers(): void {\n const missing: Array<{ channel: string; controller: string }> = [];\n\n for (const [channel, opDef] of this.operations) {\n if (!this.controllerResolver.has(opDef.controller)) {\n missing.push({ channel, controller: opDef.controller });\n }\n }\n\n if (missing.length > 0) {\n const details = missing\n .map(m => ` - channel \"${m.channel}\" requires controller \"${m.controller}\"`)\n .join('\\n');\n\n throw new Error(\n `WoodRouter: ${missing.length} operation(s) reference controllers that are not registered.\\n` +\n `This usually means controllers.generated.ts is missing or out of date.\\n` +\n `Run \"npx wood build\" or \"npx wood dev\" to regenerate it.\\n\\n` +\n `Missing controllers:\\n${details}`\n );\n }\n\n logger.info(`All ${this.operations.size} operations have registered controllers`);\n }\n\n /** Get all registered operation channels. */\n getChannels(): string[] {\n return Array.from(this.operations.keys());\n }\n\n /** Get a specific operation definition. */\n getOperation(channel: string): OperationDef | undefined {\n return this.operations.get(channel);\n }\n}\n"],"mappings":"AACA,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,wBAAwB,yBAAyB;AAC1D,SAAS,oBAAoB,2BAA2B;AACxD,SAAS,kCAAkC;AAC3C,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB;AAEjC,MAAM,SAAS,UAAU,aAAa;AAEtC,SAAS,2BAA2B,OAA6C;AAC/E,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AAC/D,WAAO;AAAA,EACT;AAEA,QAAM,SAAS;AACf,MAAI,OAAO,OAAO,mBAAmB,YAAY,OAAO,OAAO,mBAAmB,UAAU;AAC1F,WAAO,OAAO;AAAA,EAChB;AACA,MAAI,OAAO,OAAO,aAAa,YAAY,OAAO,OAAO,aAAa,UAAU;AAC9E,WAAO,OAAO;AAAA,EAChB;AAEA,SAAO;AACT;AAGO,MAAM,oBAAoB,CAAC,UAAU,cAAc,YAAY,WAAW,iBAAiB,cAAc;AAMzG,MAAM,WAAW;AAAA;AAAA,EAQtB,YAAY,SAKT;AAZH,SAAQ,aAAa,oBAAI,IAA0B;AACnD,SAAQ,mBAAmB,oBAAI,IAA6C;AAY1E,SAAK,YAAY,QAAQ;AACzB,SAAK,YAAY,IAAI,gBAAgB;AACrC,SAAK,qBAAqB,IAAI,mBAAmB,QAAQ,aAAa;AACtE,SAAK,qBAAqB,IAAI,mBAAmB,QAAQ,gBAAgB;AAAA,MACvE,MAAM,QAAQ;AAAA,IAChB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,YAA6C;AAC1D,eAAW,CAAC,SAAS,KAAK,KAAK,YAAY;AACzC,WAAK,WAAW,IAAI,SAAS,KAAK;AAAA,IACpC;AACA,WAAO,MAAM,UAAU,WAAW,IAAI,aAAa;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,wBAAwB,SAA0B,SAAuC;AACvF,SAAK,iBAAiB,IAAI,SAAS,OAAO;AAC1C,WAAO,MAAM,oCAAoC,OAAO,GAAG;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,oBAAoB,SAAoB;AACtC,eAAW,WAAW,KAAK,WAAW,KAAK,GAAG;AAC5C,cAAQ,OAAO,SAAS,OAAO,QAAa,SAAkB;AAC5D,eAAO,KAAK,SAAS,SAAS,MAAM,MAAM;AAAA,MAC5C,CAAC;AAAA,IACH;AAGA,eAAW,WAAW,mBAAmB;AACvC,cAAQ,OAAO,SAAS,OAAO,QAAa,SAAkB;AAC5D,cAAM,UAAU,KAAK,iBAAiB,IAAI,OAAO;AACjD,YAAI,CAAC,SAAS;AACZ,gBAAM,IAAI,MAAM,+CAA+C,OAAO,GAAG;AAAA,QAC3E;AACA,eAAO,QAAQ,MAAM,MAAM;AAAA,MAC7B,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,MACL,cAAc,KAAK,WAAW,IAAI,2BAA2B,kBAAkB,MAAM;AAAA,IACvF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,SAAS,SAAiB,MAAe,OAA+B;AAC5E,UAAM,QAAQ,KAAK,WAAW,IAAI,OAAO;AACzC,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,uBAAuB,OAAO,GAAG;AAAA,IACnD;AAEA,UAAM,iBAAiB,2BAA2B,IAAI;AACtD,qBAAiB,SAAS;AAAA,MACxB,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,GAAI,mBAAmB,SAAY,EAAE,eAAe,IAAI,CAAC;AAAA,MACzD,SAAS;AAAA,QACP;AAAA,QACA,YAAY,MAAM;AAAA,QAClB,QAAQ,MAAM;AAAA,MAChB;AAAA,IACF,CAAC;AAGD,UAAM,aAAyB;AAAA,MAC7B,MAAM,QAAQ,CAAC;AAAA,MACf,QAAS,QAAgB,CAAC;AAAA,MAC1B,OAAO,CAAC;AAAA,MACR,SAAS,CAAC;AAAA,MACV,QAAQ,OAAO;AAAA,MACf,UAAU;AAAA,IACZ;AAGA,QAAI,MAAM,WAAW,SAAS,GAAG;AAC/B,YAAM,YAAY,uBAAuB,YAAY,OAAO;AAC5D,YAAM,gBAAgB,MAAM,QAAQ;AAAA,QAClC,MAAM,WAAW,IAAI,UAAQ,KAAK,mBAAmB,QAAQ,IAAI,CAAC;AAAA,MACpE;AACA,YAAM,kBAAkB,eAAe,SAAS;AAAA,IAClD;AAGA,QAAI,MAAM,aAAa;AACrB,YAAMA,UAAS,KAAK,UAAU;AAAA,QAC5B;AAAA,QACA,MAAM;AAAA,QACN,WAAW;AAAA,MACb;AACA,UAAI,CAACA,QAAO,OAAO;AACjB,eAAO;AAAA,UACL,OAAO;AAAA,UACP,SAAS;AAAA,UACT,SAASA,QAAO;AAAA,QAClB;AAAA,MACF;AAAA,IACF;AAGA,UAAM,kBAAkB,MAAM,KAAK,mBAAmB,aAAa,MAAM,UAAU;AAGnF,UAAM,kBAAkB,KAAK,UAAU,OAAO;AAC9C,UAAM,WAAW,MAAM,gBAAgB,SAAS,eAAe;AAG/D,UAAM,OAAO,oBAAoB,YAAY,eAAe;AAG5D,UAAM,SAAS,SAAS,MAAM,MAAM;AACpC,QAAI;AACJ,QAAI;AAEJ,QAAI;AACF,UAAI,OAAO,WAAW,YAAY;AAChC,cAAM,IAAI;AAAA,UACR,eAAe,MAAM,UAAU,oBAAoB,MAAM,MAAM;AAAA,QACjE;AAAA,MACF;AAEA,eAAS,MAAM,OAAO,KAAK,UAAU,IAAI;AACzC,uBAAiB,SAAS;AAAA,QACxB,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,GAAI,mBAAmB,SAAY,EAAE,eAAe,IAAI,CAAC;AAAA,QACzD,SAAS;AAAA,UACP;AAAA,UACA,YAAY,MAAM;AAAA,UAClB,QAAQ,MAAM;AAAA,QAChB;AAAA,MACF,CAAC;AAAA,IACH,SAAS,OAAO;AACd,oBAAc;AACd,uBAAiB,SAAS;AAAA,QACxB,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAC9D,GAAI,mBAAmB,SAAY,EAAE,eAAe,IAAI,CAAC;AAAA,QACzD,SAAS;AAAA,UACP;AAAA,UACA,YAAY,MAAM;AAAA,UAClB,QAAQ,MAAM;AAAA,QAChB;AAAA,MACF,CAAC;AAAA,IACH,UAAE;AACA,UAAI;AACF,cAAM,2BAA2B,QAAQ;AAAA,MAC3C,SAAS,cAAc;AACrB,yBAAiB,SAAS;AAAA,UACxB,QAAQ;AAAA,UACR,MAAM;AAAA,UACN,SAAS,wBAAwB,QAAQ,aAAa,UAAU,OAAO,YAAY;AAAA,UACnF,GAAI,mBAAmB,SAAY,EAAE,eAAe,IAAI,CAAC;AAAA,UACzD,SAAS;AAAA,YACP;AAAA,YACA,YAAY,MAAM;AAAA,YAClB,QAAQ,MAAM;AAAA,UAChB;AAAA,QACF,CAAC;AAED,YAAI,CAAC,aAAa;AAChB,wBAAc;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAEA,QAAI,aAAa;AACf,YAAM;AAAA,IACR;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,mBAAmB,MAAc,iBAA4B;AAC3D,SAAK,mBAAmB,SAAS,MAAM,eAAe;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,uBAAuB,MAAc,YAA0B;AAC7D,SAAK,mBAAmB,aAAa,MAAM,UAAU;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,sBAA4B;AAC1B,UAAM,UAA0D,CAAC;AAEjE,eAAW,CAAC,SAAS,KAAK,KAAK,KAAK,YAAY;AAC9C,UAAI,CAAC,KAAK,mBAAmB,IAAI,MAAM,UAAU,GAAG;AAClD,gBAAQ,KAAK,EAAE,SAAS,YAAY,MAAM,WAAW,CAAC;AAAA,MACxD;AAAA,IACF;AAEA,QAAI,QAAQ,SAAS,GAAG;AACtB,YAAM,UAAU,QACb,IAAI,OAAK,gBAAgB,EAAE,OAAO,0BAA0B,EAAE,UAAU,GAAG,EAC3E,KAAK,IAAI;AAEZ,YAAM,IAAI;AAAA,QACR,eAAe,QAAQ,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAGJ,OAAO;AAAA,MAClC;AAAA,IACF;AAEA,WAAO,KAAK,OAAO,KAAK,WAAW,IAAI,yCAAyC;AAAA,EAClF;AAAA;AAAA,EAGA,cAAwB;AACtB,WAAO,MAAM,KAAK,KAAK,WAAW,KAAK,CAAC;AAAA,EAC1C;AAAA;AAAA,EAGA,aAAa,SAA2C;AACtD,WAAO,KAAK,WAAW,IAAI,OAAO;AAAA,EACpC;AACF;","names":["result"]}
@@ -27,6 +27,7 @@ var import_ipc = require("../ipc/index.js");
27
27
  var import_navigation = require("../navigation/index.js");
28
28
  var import_tracing = require("../tracing/index.js");
29
29
  var import_operations_parser = require("../parser/operations_parser.cjs");
30
+ var import_controller_lifecycle = require("../controller/controller_lifecycle.cjs");
30
31
  var import_create_router_bridge = require("./create_router_bridge.cjs");
31
32
  var import_create_test_router = require("./create_test_router.cjs");
32
33
  function readOperations(input) {
@@ -141,14 +142,12 @@ async function createControllerHarness(options) {
141
142
  container: scopedContainer,
142
143
  navigation,
143
144
  trace,
144
- destroy: () => {
145
+ destroy: async () => {
145
146
  trace.info({
146
147
  source: "wood.testing.controller-harness",
147
148
  type: "harness.destroyed"
148
149
  });
149
- if (typeof controller.destroy === "function") {
150
- controller.destroy();
151
- }
150
+ await (0, import_controller_lifecycle.disposeControllerLifecycle)(controller);
152
151
  events.clear();
153
152
  (0, import_client.clearClientContainerForTests)();
154
153
  (0, import_client.clearClientAppForTests)();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/testing/create_controller_harness.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport {\n App,\n clearClientAppForTests,\n clearClientContainerForTests,\n clearClientLoggersForTests,\n clearClientNavigationForTests,\n clearClientWindowControlsForTests,\n getApp,\n getWindowControls,\n Ipc,\n WOOD_INTERNAL_BRIDGE_KEY,\n} from '../client/index.js';\nimport { getIpc } from '../ipc/index.js';\nimport { Navigation, getNavigation } from '../navigation/index.js';\nimport { TraceProvider, TraceProviderConfiguration } from '../tracing/index.js';\nimport { parseOperations } from '../parser/operations_parser.cjs';\nimport type { OperationDef } from '../types/operations.cjs';\nimport { createRouterBridge, type RouterBridgeResult } from './create_router_bridge.cjs';\nimport { createTestRouter } from './create_test_router.cjs';\n\nexport interface CreateControllerHarnessOptions<T> {\n controllerClass: new (...args: any[]) => T;\n container: any;\n operations: string | string[];\n controllersDir: string;\n middlewareDir?: string;\n configureContainer?: (container: any) => void | Promise<void>;\n configureRouter?: (router: ReturnType<typeof createTestRouter>['router']) => void | Promise<void>;\n prepareClient?: (context: {\n bridge: Record<string, any>;\n events: RouterBridgeResult['events'];\n navigation: Navigation;\n container: any;\n }) => void | Promise<void>;\n initialize?: boolean;\n}\n\nexport interface ControllerHarnessResult<T> extends RouterBridgeResult {\n controller: T;\n container: any;\n router: ReturnType<typeof createTestRouter>['router'];\n navigation: Navigation;\n trace: TraceProvider;\n destroy: () => void;\n}\n\ntype MutableGlobal = typeof globalThis & {\n [WOOD_INTERNAL_BRIDGE_KEY]?: unknown;\n wood?: unknown;\n};\n\nfunction readOperations(input: string): Map<string, OperationDef> {\n if (input.includes('\\n') || input.includes(':')) {\n return parseOperations(input);\n }\n\n return parseOperations(readFileSync(input, 'utf-8'));\n}\n\nfunction mergeOperations(inputs: string | string[]): Map<string, OperationDef> {\n const merged = new Map<string, OperationDef>();\n\n for (const input of Array.isArray(inputs) ? inputs : [inputs]) {\n for (const [channel, operation] of readOperations(input)) {\n merged.set(channel, operation);\n }\n }\n\n return merged;\n}\n\nfunction createRpcManifest(operations: Map<string, OperationDef>) {\n return Array.from(operations.values()).map((operation) => ({\n channel: operation.channel,\n controller: operation.controller,\n action: operation.action,\n path: operation.channel.split('.'),\n hasInput: !!operation.inputSchema,\n }));\n}\n\nfunction registerClientBuiltins(container: any): void {\n if (typeof container.registerFunction !== 'function') {\n return;\n }\n\n const traceConfiguration = new TraceProviderConfiguration();\n traceConfiguration.configureMode('test');\n traceConfiguration.configureRuntime('renderer');\n\n container.registerFunction(Navigation, () => getNavigation());\n container.registerFunction(App, () => getApp());\n container.registerFunction(Ipc, () => getIpc());\n container.registerFunction(WindowControls, () => getWindowControls());\n container.registerFunction(TraceProviderConfiguration, () => traceConfiguration);\n}\n\nclass WindowControls {\n async open(_windowId: string, _options?: Record<string, unknown>): Promise<void> {}\n async close(_windowId?: string): Promise<void> {}\n async focus(_windowId: string): Promise<void> {}\n async minimize(_windowId?: string): Promise<void> {}\n async maximize(_windowId?: string): Promise<void> {}\n}\n\nexport async function createControllerHarness<T>(\n options: CreateControllerHarnessOptions<T>,\n): Promise<ControllerHarnessResult<T>> {\n await options.configureContainer?.(options.container);\n\n if (typeof options.container.instance === 'function') {\n const rootTraceConfiguration = await options.container.instance(TraceProviderConfiguration);\n rootTraceConfiguration.configureMode('test');\n rootTraceConfiguration.configureRuntime('main');\n }\n\n const operations = mergeOperations(options.operations);\n const { router } = createTestRouter('', {\n container: options.container,\n controllersDir: options.controllersDir,\n middlewareDir: options.middlewareDir,\n });\n router.loadOperations(operations);\n await options.configureRouter?.(router);\n\n const { bridge, events } = createRouterBridge(router);\n const harnessBridge = Object.assign(bridge, {\n __rpcManifest: createRpcManifest(operations),\n });\n\n const globalObj = globalThis as MutableGlobal;\n const previousInternalBridge = globalObj[WOOD_INTERNAL_BRIDGE_KEY];\n const previousWoodBridge = globalObj.wood;\n globalObj[WOOD_INTERNAL_BRIDGE_KEY] = harnessBridge;\n globalObj.wood = harnessBridge;\n\n clearClientContainerForTests();\n clearClientAppForTests();\n clearClientNavigationForTests();\n clearClientWindowControlsForTests();\n clearClientLoggersForTests();\n\n const scopedContainer = options.container.extend();\n registerClientBuiltins(scopedContainer);\n if (typeof scopedContainer.registerClass === 'function') {\n scopedContainer.registerClass(options.controllerClass);\n }\n const navigation = getNavigation();\n await options.prepareClient?.({\n bridge: harnessBridge,\n events,\n navigation,\n container: scopedContainer,\n });\n\n const controller = await scopedContainer.instance(options.controllerClass);\n const trace = await scopedContainer.instance(TraceProvider);\n if (options.initialize && typeof (controller as { initialize?: () => void }).initialize === 'function') {\n await Promise.resolve(\n (controller as { initialize: () => void | Promise<void> }).initialize(),\n );\n }\n\n trace.info({\n source: 'wood.testing.controller-harness',\n type: 'harness.created',\n payload: {\n controllerClass: options.controllerClass.name,\n },\n });\n\n return {\n controller,\n bridge: harnessBridge,\n events,\n router,\n container: scopedContainer,\n navigation,\n trace,\n destroy: () => {\n trace.info({\n source: 'wood.testing.controller-harness',\n type: 'harness.destroyed',\n });\n if (typeof (controller as { destroy?: () => void }).destroy === 'function') {\n (controller as { destroy: () => void }).destroy();\n }\n events.clear();\n clearClientContainerForTests();\n clearClientAppForTests();\n clearClientNavigationForTests();\n clearClientWindowControlsForTests();\n clearClientLoggersForTests();\n\n if (previousInternalBridge === undefined) {\n delete globalObj[WOOD_INTERNAL_BRIDGE_KEY];\n } else {\n globalObj[WOOD_INTERNAL_BRIDGE_KEY] = previousInternalBridge;\n }\n\n if (previousWoodBridge === undefined) {\n delete globalObj.wood;\n } else {\n globalObj.wood = previousWoodBridge;\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA6B;AAC7B,oBAWO;AACP,iBAAuB;AACvB,wBAA0C;AAC1C,qBAA0D;AAC1D,+BAAgC;AAEhC,kCAA4D;AAC5D,gCAAiC;AAiCjC,SAAS,eAAe,OAA0C;AAChE,MAAI,MAAM,SAAS,IAAI,KAAK,MAAM,SAAS,GAAG,GAAG;AAC/C,eAAO,0CAAgB,KAAK;AAAA,EAC9B;AAEA,aAAO,8CAAgB,6BAAa,OAAO,OAAO,CAAC;AACrD;AAEA,SAAS,gBAAgB,QAAsD;AAC7E,QAAM,SAAS,oBAAI,IAA0B;AAE7C,aAAW,SAAS,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM,GAAG;AAC7D,eAAW,CAAC,SAAS,SAAS,KAAK,eAAe,KAAK,GAAG;AACxD,aAAO,IAAI,SAAS,SAAS;AAAA,IAC/B;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,kBAAkB,YAAuC;AAChE,SAAO,MAAM,KAAK,WAAW,OAAO,CAAC,EAAE,IAAI,CAAC,eAAe;AAAA,IACzD,SAAS,UAAU;AAAA,IACnB,YAAY,UAAU;AAAA,IACtB,QAAQ,UAAU;AAAA,IAClB,MAAM,UAAU,QAAQ,MAAM,GAAG;AAAA,IACjC,UAAU,CAAC,CAAC,UAAU;AAAA,EACxB,EAAE;AACJ;AAEA,SAAS,uBAAuB,WAAsB;AACpD,MAAI,OAAO,UAAU,qBAAqB,YAAY;AACpD;AAAA,EACF;AAEA,QAAM,qBAAqB,IAAI,0CAA2B;AAC1D,qBAAmB,cAAc,MAAM;AACvC,qBAAmB,iBAAiB,UAAU;AAE9C,YAAU,iBAAiB,8BAAY,UAAM,iCAAc,CAAC;AAC5D,YAAU,iBAAiB,mBAAK,UAAM,sBAAO,CAAC;AAC9C,YAAU,iBAAiB,mBAAK,UAAM,mBAAO,CAAC;AAC9C,YAAU,iBAAiB,gBAAgB,UAAM,iCAAkB,CAAC;AACpE,YAAU,iBAAiB,2CAA4B,MAAM,kBAAkB;AACjF;AAEA,MAAM,eAAe;AAAA,EACnB,MAAM,KAAK,WAAmB,UAAmD;AAAA,EAAC;AAAA,EAClF,MAAM,MAAM,WAAmC;AAAA,EAAC;AAAA,EAChD,MAAM,MAAM,WAAkC;AAAA,EAAC;AAAA,EAC/C,MAAM,SAAS,WAAmC;AAAA,EAAC;AAAA,EACnD,MAAM,SAAS,WAAmC;AAAA,EAAC;AACrD;AAEA,eAAsB,wBACpB,SACqC;AACrC,QAAM,QAAQ,qBAAqB,QAAQ,SAAS;AAEpD,MAAI,OAAO,QAAQ,UAAU,aAAa,YAAY;AACpD,UAAM,yBAAyB,MAAM,QAAQ,UAAU,SAAS,yCAA0B;AAC1F,2BAAuB,cAAc,MAAM;AAC3C,2BAAuB,iBAAiB,MAAM;AAAA,EAChD;AAEA,QAAM,aAAa,gBAAgB,QAAQ,UAAU;AACrD,QAAM,EAAE,OAAO,QAAI,4CAAiB,IAAI;AAAA,IACtC,WAAW,QAAQ;AAAA,IACnB,gBAAgB,QAAQ;AAAA,IACxB,eAAe,QAAQ;AAAA,EACzB,CAAC;AACD,SAAO,eAAe,UAAU;AAChC,QAAM,QAAQ,kBAAkB,MAAM;AAEtC,QAAM,EAAE,QAAQ,OAAO,QAAI,gDAAmB,MAAM;AACpD,QAAM,gBAAgB,OAAO,OAAO,QAAQ;AAAA,IAC1C,eAAe,kBAAkB,UAAU;AAAA,EAC7C,CAAC;AAED,QAAM,YAAY;AAClB,QAAM,yBAAyB,UAAU,sCAAwB;AACjE,QAAM,qBAAqB,UAAU;AACrC,YAAU,sCAAwB,IAAI;AACtC,YAAU,OAAO;AAEjB,kDAA6B;AAC7B,4CAAuB;AACvB,mDAA8B;AAC9B,uDAAkC;AAClC,gDAA2B;AAE3B,QAAM,kBAAkB,QAAQ,UAAU,OAAO;AACjD,yBAAuB,eAAe;AACtC,MAAI,OAAO,gBAAgB,kBAAkB,YAAY;AACvD,oBAAgB,cAAc,QAAQ,eAAe;AAAA,EACvD;AACA,QAAM,iBAAa,iCAAc;AACjC,QAAM,QAAQ,gBAAgB;AAAA,IAC5B,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACb,CAAC;AAED,QAAM,aAAa,MAAM,gBAAgB,SAAS,QAAQ,eAAe;AACzE,QAAM,QAAQ,MAAM,gBAAgB,SAAS,4BAAa;AAC1D,MAAI,QAAQ,cAAc,OAAQ,WAA2C,eAAe,YAAY;AACtG,UAAM,QAAQ;AAAA,MACX,WAA0D,WAAW;AAAA,IACxE;AAAA,EACF;AAEA,QAAM,KAAK;AAAA,IACT,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,SAAS;AAAA,MACP,iBAAiB,QAAQ,gBAAgB;AAAA,IAC3C;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA,SAAS,MAAM;AACb,YAAM,KAAK;AAAA,QACT,QAAQ;AAAA,QACR,MAAM;AAAA,MACR,CAAC;AACD,UAAI,OAAQ,WAAwC,YAAY,YAAY;AAC1E,QAAC,WAAuC,QAAQ;AAAA,MAClD;AACA,aAAO,MAAM;AACb,sDAA6B;AAC7B,gDAAuB;AACvB,uDAA8B;AAC9B,2DAAkC;AAClC,oDAA2B;AAE3B,UAAI,2BAA2B,QAAW;AACxC,eAAO,UAAU,sCAAwB;AAAA,MAC3C,OAAO;AACL,kBAAU,sCAAwB,IAAI;AAAA,MACxC;AAEA,UAAI,uBAAuB,QAAW;AACpC,eAAO,UAAU;AAAA,MACnB,OAAO;AACL,kBAAU,OAAO;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/testing/create_controller_harness.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport {\n App,\n clearClientAppForTests,\n clearClientContainerForTests,\n clearClientLoggersForTests,\n clearClientNavigationForTests,\n clearClientWindowControlsForTests,\n getApp,\n getWindowControls,\n Ipc,\n WOOD_INTERNAL_BRIDGE_KEY,\n} from '../client/index.js';\nimport { getIpc } from '../ipc/index.js';\nimport { Navigation, getNavigation } from '../navigation/index.js';\nimport { TraceProvider, TraceProviderConfiguration } from '../tracing/index.js';\nimport { parseOperations } from '../parser/operations_parser.cjs';\nimport type { OperationDef } from '../types/operations.cjs';\nimport { disposeControllerLifecycle } from '../controller/controller_lifecycle.cjs';\nimport { createRouterBridge, type RouterBridgeResult } from './create_router_bridge.cjs';\nimport { createTestRouter } from './create_test_router.cjs';\n\nexport interface CreateControllerHarnessOptions<T> {\n controllerClass: new (...args: any[]) => T;\n container: any;\n operations: string | string[];\n controllersDir: string;\n middlewareDir?: string;\n configureContainer?: (container: any) => void | Promise<void>;\n configureRouter?: (router: ReturnType<typeof createTestRouter>['router']) => void | Promise<void>;\n prepareClient?: (context: {\n bridge: Record<string, any>;\n events: RouterBridgeResult['events'];\n navigation: Navigation;\n container: any;\n }) => void | Promise<void>;\n initialize?: boolean;\n}\n\nexport interface ControllerHarnessResult<T> extends RouterBridgeResult {\n controller: T;\n container: any;\n router: ReturnType<typeof createTestRouter>['router'];\n navigation: Navigation;\n trace: TraceProvider;\n destroy: () => void | Promise<void>;\n}\n\ntype MutableGlobal = typeof globalThis & {\n [WOOD_INTERNAL_BRIDGE_KEY]?: unknown;\n wood?: unknown;\n};\n\nfunction readOperations(input: string): Map<string, OperationDef> {\n if (input.includes('\\n') || input.includes(':')) {\n return parseOperations(input);\n }\n\n return parseOperations(readFileSync(input, 'utf-8'));\n}\n\nfunction mergeOperations(inputs: string | string[]): Map<string, OperationDef> {\n const merged = new Map<string, OperationDef>();\n\n for (const input of Array.isArray(inputs) ? inputs : [inputs]) {\n for (const [channel, operation] of readOperations(input)) {\n merged.set(channel, operation);\n }\n }\n\n return merged;\n}\n\nfunction createRpcManifest(operations: Map<string, OperationDef>) {\n return Array.from(operations.values()).map((operation) => ({\n channel: operation.channel,\n controller: operation.controller,\n action: operation.action,\n path: operation.channel.split('.'),\n hasInput: !!operation.inputSchema,\n }));\n}\n\nfunction registerClientBuiltins(container: any): void {\n if (typeof container.registerFunction !== 'function') {\n return;\n }\n\n const traceConfiguration = new TraceProviderConfiguration();\n traceConfiguration.configureMode('test');\n traceConfiguration.configureRuntime('renderer');\n\n container.registerFunction(Navigation, () => getNavigation());\n container.registerFunction(App, () => getApp());\n container.registerFunction(Ipc, () => getIpc());\n container.registerFunction(WindowControls, () => getWindowControls());\n container.registerFunction(TraceProviderConfiguration, () => traceConfiguration);\n}\n\nclass WindowControls {\n async open(_windowId: string, _options?: Record<string, unknown>): Promise<void> {}\n async close(_windowId?: string): Promise<void> {}\n async focus(_windowId: string): Promise<void> {}\n async minimize(_windowId?: string): Promise<void> {}\n async maximize(_windowId?: string): Promise<void> {}\n}\n\nexport async function createControllerHarness<T>(\n options: CreateControllerHarnessOptions<T>,\n): Promise<ControllerHarnessResult<T>> {\n await options.configureContainer?.(options.container);\n\n if (typeof options.container.instance === 'function') {\n const rootTraceConfiguration = await options.container.instance(TraceProviderConfiguration);\n rootTraceConfiguration.configureMode('test');\n rootTraceConfiguration.configureRuntime('main');\n }\n\n const operations = mergeOperations(options.operations);\n const { router } = createTestRouter('', {\n container: options.container,\n controllersDir: options.controllersDir,\n middlewareDir: options.middlewareDir,\n });\n router.loadOperations(operations);\n await options.configureRouter?.(router);\n\n const { bridge, events } = createRouterBridge(router);\n const harnessBridge = Object.assign(bridge, {\n __rpcManifest: createRpcManifest(operations),\n });\n\n const globalObj = globalThis as MutableGlobal;\n const previousInternalBridge = globalObj[WOOD_INTERNAL_BRIDGE_KEY];\n const previousWoodBridge = globalObj.wood;\n globalObj[WOOD_INTERNAL_BRIDGE_KEY] = harnessBridge;\n globalObj.wood = harnessBridge;\n\n clearClientContainerForTests();\n clearClientAppForTests();\n clearClientNavigationForTests();\n clearClientWindowControlsForTests();\n clearClientLoggersForTests();\n\n const scopedContainer = options.container.extend();\n registerClientBuiltins(scopedContainer);\n if (typeof scopedContainer.registerClass === 'function') {\n scopedContainer.registerClass(options.controllerClass);\n }\n const navigation = getNavigation();\n await options.prepareClient?.({\n bridge: harnessBridge,\n events,\n navigation,\n container: scopedContainer,\n });\n\n const controller = await scopedContainer.instance(options.controllerClass);\n const trace = await scopedContainer.instance(TraceProvider);\n if (options.initialize && typeof (controller as { initialize?: () => void }).initialize === 'function') {\n await Promise.resolve(\n (controller as { initialize: () => void | Promise<void> }).initialize(),\n );\n }\n\n trace.info({\n source: 'wood.testing.controller-harness',\n type: 'harness.created',\n payload: {\n controllerClass: options.controllerClass.name,\n },\n });\n\n return {\n controller,\n bridge: harnessBridge,\n events,\n router,\n container: scopedContainer,\n navigation,\n trace,\n destroy: async () => {\n trace.info({\n source: 'wood.testing.controller-harness',\n type: 'harness.destroyed',\n });\n await disposeControllerLifecycle(controller);\n events.clear();\n clearClientContainerForTests();\n clearClientAppForTests();\n clearClientNavigationForTests();\n clearClientWindowControlsForTests();\n clearClientLoggersForTests();\n\n if (previousInternalBridge === undefined) {\n delete globalObj[WOOD_INTERNAL_BRIDGE_KEY];\n } else {\n globalObj[WOOD_INTERNAL_BRIDGE_KEY] = previousInternalBridge;\n }\n\n if (previousWoodBridge === undefined) {\n delete globalObj.wood;\n } else {\n globalObj.wood = previousWoodBridge;\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -30,7 +30,7 @@ interface ControllerHarnessResult<T> extends RouterBridgeResult {
30
30
  router: ReturnType<typeof createTestRouter>['router'];
31
31
  navigation: Navigation;
32
32
  trace: TraceProvider;
33
- destroy: () => void;
33
+ destroy: () => void | Promise<void>;
34
34
  }
35
35
  declare function createControllerHarness<T>(options: CreateControllerHarnessOptions<T>): Promise<ControllerHarnessResult<T>>;
36
36
 
@@ -30,7 +30,7 @@ interface ControllerHarnessResult<T> extends RouterBridgeResult {
30
30
  router: ReturnType<typeof createTestRouter>['router'];
31
31
  navigation: Navigation;
32
32
  trace: TraceProvider;
33
- destroy: () => void;
33
+ destroy: () => void | Promise<void>;
34
34
  }
35
35
  declare function createControllerHarness<T>(options: CreateControllerHarnessOptions<T>): Promise<ControllerHarnessResult<T>>;
36
36
 
@@ -15,6 +15,7 @@ import { getIpc } from "../ipc/index.js";
15
15
  import { Navigation, getNavigation } from "../navigation/index.js";
16
16
  import { TraceProvider, TraceProviderConfiguration } from "../tracing/index.js";
17
17
  import { parseOperations } from "../parser/operations_parser.js";
18
+ import { disposeControllerLifecycle } from "../controller/controller_lifecycle.js";
18
19
  import { createRouterBridge } from "./create_router_bridge.js";
19
20
  import { createTestRouter } from "./create_test_router.js";
20
21
  function readOperations(input) {
@@ -129,14 +130,12 @@ async function createControllerHarness(options) {
129
130
  container: scopedContainer,
130
131
  navigation,
131
132
  trace,
132
- destroy: () => {
133
+ destroy: async () => {
133
134
  trace.info({
134
135
  source: "wood.testing.controller-harness",
135
136
  type: "harness.destroyed"
136
137
  });
137
- if (typeof controller.destroy === "function") {
138
- controller.destroy();
139
- }
138
+ await disposeControllerLifecycle(controller);
140
139
  events.clear();
141
140
  clearClientContainerForTests();
142
141
  clearClientAppForTests();