@noego/wood 0.6.0 → 0.6.2
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.
- package/dist/client/index.cjs +12 -0
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +14 -1
- package/dist/client/index.d.ts +14 -1
- package/dist/client/index.js +11 -0
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/preload_generator.cjs +44 -5
- package/dist/codegen/preload_generator.cjs.map +1 -1
- package/dist/codegen/preload_generator.js +44 -5
- package/dist/codegen/preload_generator.js.map +1 -1
- package/dist/frontend/frontend_surface.d.cts +1 -0
- package/dist/frontend/frontend_surface.d.ts +1 -0
- package/dist/frontend/index.d.cts +1 -0
- package/dist/frontend/index.d.ts +1 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/main/index.cjs +43 -58
- package/dist/main/index.cjs.map +1 -1
- package/dist/main/index.d.cts +2 -2
- package/dist/main/index.d.ts +2 -2
- package/dist/main/index.js +48 -58
- package/dist/main/index.js.map +1 -1
- package/dist/main/shutdown_coordinator.cjs +153 -0
- package/dist/main/shutdown_coordinator.cjs.map +1 -0
- package/dist/main/shutdown_coordinator.d.cts +81 -0
- package/dist/main/shutdown_coordinator.d.ts +81 -0
- package/dist/main/shutdown_coordinator.js +127 -0
- package/dist/main/shutdown_coordinator.js.map +1 -0
- package/dist/navigation/index.cjs.map +1 -1
- package/dist/navigation/index.d.cts +4 -28
- package/dist/navigation/index.d.ts +4 -28
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/layout_preservation.cjs.map +1 -1
- package/dist/navigation/layout_preservation.d.cts +27 -2
- package/dist/navigation/layout_preservation.d.ts +27 -2
- package/dist/navigation/layout_preservation.js.map +1 -1
- package/dist/testing/create_controller_harness.d.cts +1 -0
- package/dist/testing/create_controller_harness.d.ts +1 -0
- package/dist/testing/index.d.cts +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/test_wood_config.d.cts +1 -0
- package/dist/testing/test_wood_config.d.ts +1 -0
- package/dist/testing/wood_env_renderer.d.cts +1 -0
- package/dist/testing/wood_env_renderer.d.ts +1 -0
- package/package.json +1 -1
- package/src/navigation/layout_preservation.ts +3 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/codegen/preload_generator.ts"],"sourcesContent":["import type { OperationDef } from '../types/index.cjs';\nimport type { RendererTraceBatchingConfig } from '../types/config.cjs';\nimport { RENDERER_TRACE_BATCHING_DEFAULTS } from '../config/config_parser.cjs';\nimport { generatedFileNotice } from './generated_file_notice.cjs';\n\nexport type GeneratePreloadOptions = {\n /**\n * Renderer→main IPC trace batching policy resolved from\n * wood.config.yml `observability.trace.rendererToMain.batching`.\n * The generated preload embeds these values but does not own them.\n */\n traceBatching?: RendererTraceBatchingConfig;\n};\n\nconst PRELOAD_SOURCE_INSTRUCTIONS = [\n 'To change IPC channels or preload bridge methods, edit the operations YAML configured by',\n 'wood.config.yml main.openapi and regenerate with `wood dev` or `wood build`.',\n] as const;\n\nfunction operationHasInput(op: OperationDef): boolean {\n return Boolean(\n op.inputSchema &&\n (op.inputSchema as Record<string, unknown>).type === 'object' &&\n Object.keys((op.inputSchema as Record<string, Record<string, unknown>>).properties ?? {}).length > 0,\n );\n}\n\n/**\n * Build a nested object tree from dot-path operation channels.\n * \"chat.message.send\" -> { chat: { message: { send: ... } } }\n */\nfunction buildNestedTree(operations: Map<string, OperationDef>): Record<string, unknown> {\n const tree: Record<string, unknown> = {};\n\n for (const [channel, op] of operations) {\n const parts = channel.split('.');\n let current: Record<string, unknown> = tree;\n\n for (let i = 0; i < parts.length - 1; i++) {\n if (!current[parts[i]]) current[parts[i]] = {};\n current = current[parts[i]] as Record<string, unknown>;\n }\n\n const leaf = parts[parts.length - 1];\n current[leaf] = { __operation: op };\n }\n\n return tree;\n}\n\n/**\n * Generate TypeScript code for a nested tree node.\n */\nfunction generateTreeCode(node: Record<string, unknown>, indent: string = ' '): string {\n const lines: string[] = [];\n\n for (const [key, value] of Object.entries(node)) {\n const entry = value as Record<string, unknown>;\n if (entry.__operation) {\n const op = entry.__operation as OperationDef;\n const hasInput = operationHasInput(op);\n\n if (hasInput) {\n lines.push(`${indent}${key}: (data: unknown) =>`);\n lines.push(`${indent} ipcRenderer.invoke('${op.channel}', data),`);\n } else {\n lines.push(`${indent}${key}: () =>`);\n lines.push(`${indent} ipcRenderer.invoke('${op.channel}'),`);\n }\n } else {\n lines.push(`${indent}${key}: {`);\n lines.push(generateTreeCode(entry, indent + ' '));\n lines.push(`${indent}},`);\n }\n }\n\n return lines.join('\\n');\n}\n\nfunction generateRpcManifestCode(operations: Map<string, OperationDef>): string {\n const entries = Array.from(operations.values())\n .sort((a, b) => a.channel.localeCompare(b.channel))\n .map((op) => {\n const payload = {\n channel: op.channel,\n controller: op.controller,\n action: op.action,\n path: op.channel.split('.'),\n hasInput: operationHasInput(op),\n };\n return ` ${JSON.stringify(payload)},`;\n });\n\n return `const __WOOD_RPC_MANIFEST = [\n${entries.join('\\n')}\n];`;\n}\n\nconst INTERNAL_BRIDGE_GLOBAL_KEY = '__NOEGO_WOOD_BRIDGE__';\n\n/**\n * Generate a complete preload.ts file from operations.\n */\nexport function generatePreload(\n operations: Map<string, OperationDef>,\n options: GeneratePreloadOptions = {},\n): string {\n const tree = buildNestedTree(operations);\n const operationsCode = generateTreeCode(tree);\n const rpcManifestCode = generateRpcManifestCode(operations);\n const traceBatching = options.traceBatching ?? RENDERER_TRACE_BATCHING_DEFAULTS;\n\n return `${generatedFileNotice(PRELOAD_SOURCE_INSTRUCTIONS)}\nimport { contextBridge, ipcRenderer } from 'electron';\nimport {\n RendererTraceBatchTransport,\n type WoodRendererTraceInput,\n type WoodTraceBatch,\n type WoodTraceBatchAcknowledgement,\n type WoodTraceBatchPort,\n} from '@noego/wood/trace-transport';\n\n${rpcManifestCode}\n\nconst __WOOD_INTERNAL_BRIDGE_KEY = '${INTERNAL_BRIDGE_GLOBAL_KEY}';\n\ncontextBridge.exposeInMainWorld(__WOOD_INTERNAL_BRIDGE_KEY, {\n${operationsCode}\n\n // Internal: backend controller/action RPC map for @Inject(App)\n __rpcManifest: __WOOD_RPC_MANIFEST,\n\n // Internal: loader data requests\n __load: (route: string, params: Record<string, string>) =>\n ipcRenderer.invoke('__load', { route, params }),\n\n // Internal: cross-window navigation\n __navigate: (windowId: string, route: string) =>\n ipcRenderer.invoke('__navigate', { windowId, route }),\n\n // Internal: window management\n __window: {\n current: () =>\n ipcRenderer.invoke('__window', { action: 'current' }),\n open: (windowId: string, options?: Record<string, unknown>) =>\n ipcRenderer.invoke('__window', { action: 'open', windowId, options }),\n close: (windowId?: string) =>\n ipcRenderer.invoke('__window', { action: 'close', windowId }),\n focus: (windowId: string) =>\n ipcRenderer.invoke('__window', { action: 'focus', windowId }),\n minimize: (windowId?: string) =>\n ipcRenderer.invoke('__window', { action: 'minimize', windowId }),\n maximize: (windowId?: string) =>\n ipcRenderer.invoke('__window', { action: 'maximize', windowId }),\n },\n\n // Internal: framework config (trace min level, etc)\n __woodConfig: () =>\n ipcRenderer.invoke('__woodConfig'),\n\n // Internal: debug metadata\n __debug: {\n current: () =>\n ipcRenderer.invoke('__debug', { action: 'current' }),\n },\n\n // Internal: context menu\n __contextMenu: {\n show: (data: unknown) =>\n ipcRenderer.invoke('__contextMenu', { action: 'show', ...data as Record<string, unknown> }),\n },\n\n // Push events from main process\n on: (channel: string, callback: (...args: unknown[]) => void) => {\n const subscription = (_event: unknown, ...args: unknown[]) => callback(...args);\n ipcRenderer.on(\\`__event:\\${channel}\\`, subscription);\n return () => ipcRenderer.removeListener(\\`__event:\\${channel}\\`, subscription);\n },\n __trace: (event: unknown) => {\n ipcRenderer.send('__trace', event);\n },\n __log: (level: string, logger: string, message: string, context?: unknown) => {\n ipcRenderer.send('__log', { level, logger, message, context });\n },\n __logBatch: (batch: unknown) => {\n ipcRenderer.send('__log_batch', batch);\n },\n});\n\n// Trace transport via MessagePort (activated when main process sends a port)\nlet __tracePort: MessagePort | null = null;\nconst __traceBuffer: unknown[] = [];\nconst __TRACE_BUFFER_CAPACITY = 256;\nlet __traceDroppedCapacity = 0;\n\nfunction __traceFlush(): void {\n while (__traceBuffer.length > 0 && __tracePort) {\n __tracePort.postMessage(__traceBuffer.shift());\n }\n}\n\nipcRenderer.on('__trace-port', (event) => {\n __tracePort = event.ports[0];\n __tracePort.start();\n __traceFlush();\n});\n\ncontextBridge.exposeInMainWorld('__NOEGO_TRACE__', {\n send: (data: unknown) => {\n if (__tracePort) {\n __tracePort.postMessage(data);\n } else if (__traceBuffer.length >= __TRACE_BUFFER_CAPACITY) {\n __traceDroppedCapacity += 1;\n } else {\n __traceBuffer.push(data);\n }\n },\n status: () => ({\n queuedEvents: __traceBuffer.length,\n droppedCapacity: __traceDroppedCapacity,\n }),\n});\n\nconst __traceBatchProducerId = typeof globalThis.crypto?.randomUUID === 'function'\n ? globalThis.crypto.randomUUID()\n : \\`wood-renderer-\\${Date.now()}-\\${Math.random().toString(16).slice(2)}\\`;\nlet __traceBatchListener: ((message: unknown) => void) | null = null;\n\nconst __traceBatchPort: WoodTraceBatchPort = {\n postMessage: (batch: WoodTraceBatch) => {\n void ipcRenderer.invoke('__traceBatch', batch)\n .then((acknowledgement: WoodTraceBatchAcknowledgement) => {\n __traceBatchListener?.(acknowledgement);\n })\n .catch((error: unknown) => {\n __traceBatchListener?.({\n kind: 'retryable',\n batchId: batch.batchId,\n reason: error instanceof Error ? error.message : String(error),\n retryAfterMs: 100,\n });\n });\n },\n subscribe: (listener: (message: unknown) => void) => {\n __traceBatchListener = listener;\n return () => {\n if (__traceBatchListener === listener) {\n __traceBatchListener = null;\n }\n };\n },\n close: () => {\n __traceBatchListener = null;\n },\n};\n\n// Batching policy resolved from wood.config.yml observability configuration\n// at generation time; this file embeds but does not own it.\nconst __traceBatchTransport = new RendererTraceBatchTransport({\n producerId: __traceBatchProducerId,\n maxQueuedEvents: ${traceBatching.maxQueuedEvents},\n maxBatchEvents: ${traceBatching.maxBatchEvents},\n acknowledgementTimeoutMs: ${traceBatching.acknowledgementTimeoutMs},\n retryLimit: ${traceBatching.retryLimit},\n retryBaseDelayMs: ${traceBatching.retryBaseDelayMs},\n now: () => Date.now(),\n schedule: (work: () => void) => queueMicrotask(work),\n delay: (durationMs: number) => new Promise((resolve) => setTimeout(resolve, durationMs)),\n});\n__traceBatchTransport.attachPort(__traceBatchPort);\n\ncontextBridge.exposeInMainWorld('__NOEGO_WOOD_TRACE_BATCH__', {\n send: (event: WoodRendererTraceInput) => __traceBatchTransport.admit(event),\n status: () => __traceBatchTransport.status(),\n flush: (deadlineMs: number) => __traceBatchTransport.flush(deadlineMs),\n close: (deadlineMs: number) => __traceBatchTransport.close(deadlineMs),\n});\n\nipcRenderer.on('__woodTraceDrainRequest', (_event, payload: unknown) => {\n if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {\n return;\n }\n const requestId = Reflect.get(payload, 'requestId');\n const deadlineMs = Reflect.get(payload, 'deadlineMs');\n if (typeof requestId !== 'string'\n || typeof deadlineMs !== 'number'\n || !Number.isFinite(deadlineMs)) {\n return;\n }\n void __traceBatchTransport.close(deadlineMs).finally(() => {\n ipcRenderer.send('__woodTraceDrainAck', {\n requestId,\n status: __traceBatchTransport.status(),\n });\n });\n});\n\nglobalThis.addEventListener?.('pagehide', () => {\n void __traceBatchTransport.flush(Date.now() + 100);\n});\n`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/codegen/preload_generator.ts"],"sourcesContent":["import type { OperationDef } from '../types/index.cjs';\nimport type { RendererTraceBatchingConfig } from '../types/config.cjs';\nimport { RENDERER_TRACE_BATCHING_DEFAULTS } from '../config/config_parser.cjs';\nimport { generatedFileNotice } from './generated_file_notice.cjs';\n\nexport type GeneratePreloadOptions = {\n /**\n * Renderer→main IPC trace batching policy resolved from\n * wood.config.yml `observability.trace.rendererToMain.batching`.\n * The generated preload embeds these values but does not own them.\n */\n traceBatching?: RendererTraceBatchingConfig;\n};\n\nconst PRELOAD_SOURCE_INSTRUCTIONS = [\n 'To change IPC channels or preload bridge methods, edit the operations YAML configured by',\n 'wood.config.yml main.openapi and regenerate with `wood dev` or `wood build`.',\n] as const;\n\nfunction operationHasInput(op: OperationDef): boolean {\n return Boolean(\n op.inputSchema &&\n (op.inputSchema as Record<string, unknown>).type === 'object' &&\n Object.keys((op.inputSchema as Record<string, Record<string, unknown>>).properties ?? {}).length > 0,\n );\n}\n\n/**\n * Build a nested object tree from dot-path operation channels.\n * \"chat.message.send\" -> { chat: { message: { send: ... } } }\n */\nfunction buildNestedTree(operations: Map<string, OperationDef>): Record<string, unknown> {\n const tree: Record<string, unknown> = {};\n\n for (const [channel, op] of operations) {\n const parts = channel.split('.');\n let current: Record<string, unknown> = tree;\n\n for (let i = 0; i < parts.length - 1; i++) {\n if (!current[parts[i]]) current[parts[i]] = {};\n current = current[parts[i]] as Record<string, unknown>;\n }\n\n const leaf = parts[parts.length - 1];\n current[leaf] = { __operation: op };\n }\n\n return tree;\n}\n\n/**\n * Generate TypeScript code for a nested tree node.\n */\nfunction generateTreeCode(node: Record<string, unknown>, indent: string = ' '): string {\n const lines: string[] = [];\n\n for (const [key, value] of Object.entries(node)) {\n const entry = value as Record<string, unknown>;\n if (entry.__operation) {\n const op = entry.__operation as OperationDef;\n const hasInput = operationHasInput(op);\n\n if (hasInput) {\n lines.push(`${indent}${key}: (data: unknown) =>`);\n lines.push(`${indent} ipcRenderer.invoke('${op.channel}', data),`);\n } else {\n lines.push(`${indent}${key}: () =>`);\n lines.push(`${indent} ipcRenderer.invoke('${op.channel}'),`);\n }\n } else {\n lines.push(`${indent}${key}: {`);\n lines.push(generateTreeCode(entry, indent + ' '));\n lines.push(`${indent}},`);\n }\n }\n\n return lines.join('\\n');\n}\n\nfunction generateRpcManifestCode(operations: Map<string, OperationDef>): string {\n const entries = Array.from(operations.values())\n .sort((a, b) => a.channel.localeCompare(b.channel))\n .map((op) => {\n const payload = {\n channel: op.channel,\n controller: op.controller,\n action: op.action,\n path: op.channel.split('.'),\n hasInput: operationHasInput(op),\n };\n return ` ${JSON.stringify(payload)},`;\n });\n\n return `const __WOOD_RPC_MANIFEST = [\n${entries.join('\\n')}\n];`;\n}\n\nconst INTERNAL_BRIDGE_GLOBAL_KEY = '__NOEGO_WOOD_BRIDGE__';\n\n/**\n * Generate a complete preload.ts file from operations.\n */\nexport function generatePreload(\n operations: Map<string, OperationDef>,\n options: GeneratePreloadOptions = {},\n): string {\n const tree = buildNestedTree(operations);\n const operationsCode = generateTreeCode(tree);\n const rpcManifestCode = generateRpcManifestCode(operations);\n const traceBatching = options.traceBatching ?? RENDERER_TRACE_BATCHING_DEFAULTS;\n\n return `${generatedFileNotice(PRELOAD_SOURCE_INSTRUCTIONS)}\nimport { contextBridge, ipcRenderer } from 'electron';\nimport {\n RendererTraceBatchTransport,\n type WoodRendererTraceInput,\n type WoodTraceBatch,\n type WoodTraceBatchAcknowledgement,\n type WoodTraceBatchPort,\n} from '@noego/wood/trace-transport';\n\n${rpcManifestCode}\n\nconst __WOOD_INTERNAL_BRIDGE_KEY = '${INTERNAL_BRIDGE_GLOBAL_KEY}';\n\ncontextBridge.exposeInMainWorld(__WOOD_INTERNAL_BRIDGE_KEY, {\n${operationsCode}\n\n // Internal: backend controller/action RPC map for @Inject(App)\n __rpcManifest: __WOOD_RPC_MANIFEST,\n\n // Internal: loader data requests\n __load: (route: string, params: Record<string, string>) =>\n ipcRenderer.invoke('__load', { route, params }),\n\n // Internal: cross-window navigation\n __navigate: (windowId: string, route: string) =>\n ipcRenderer.invoke('__navigate', { windowId, route }),\n\n // Internal: window management\n __window: {\n current: () =>\n ipcRenderer.invoke('__window', { action: 'current' }),\n open: (windowId: string, options?: Record<string, unknown>) =>\n ipcRenderer.invoke('__window', { action: 'open', windowId, options }),\n close: (windowId?: string) =>\n ipcRenderer.invoke('__window', { action: 'close', windowId }),\n focus: (windowId: string) =>\n ipcRenderer.invoke('__window', { action: 'focus', windowId }),\n minimize: (windowId?: string) =>\n ipcRenderer.invoke('__window', { action: 'minimize', windowId }),\n maximize: (windowId?: string) =>\n ipcRenderer.invoke('__window', { action: 'maximize', windowId }),\n },\n\n // Internal: framework config (trace min level, etc)\n __woodConfig: () =>\n ipcRenderer.invoke('__woodConfig'),\n\n // Internal: debug metadata\n __debug: {\n current: () =>\n ipcRenderer.invoke('__debug', { action: 'current' }),\n },\n\n // Internal: context menu\n __contextMenu: {\n show: (data: unknown) =>\n ipcRenderer.invoke('__contextMenu', { action: 'show', ...data as Record<string, unknown> }),\n },\n\n // Push events from main process\n on: (channel: string, callback: (...args: unknown[]) => void) => {\n const subscription = (_event: unknown, ...args: unknown[]) => callback(...args);\n ipcRenderer.on(\\`__event:\\${channel}\\`, subscription);\n return () => ipcRenderer.removeListener(\\`__event:\\${channel}\\`, subscription);\n },\n __trace: (event: unknown) => {\n ipcRenderer.send('__trace', event);\n },\n __log: (level: string, logger: string, message: string, context?: unknown) => {\n ipcRenderer.send('__log', { level, logger, message, context });\n },\n __logBatch: (batch: unknown) => {\n ipcRenderer.send('__log_batch', batch);\n },\n});\n\n// Trace transport via MessagePort (activated when main process sends a port)\nlet __tracePort: MessagePort | null = null;\nconst __traceBuffer: unknown[] = [];\nconst __TRACE_BUFFER_CAPACITY = 256;\nlet __traceDroppedCapacity = 0;\n\nfunction __traceFlush(): void {\n while (__traceBuffer.length > 0 && __tracePort) {\n __tracePort.postMessage(__traceBuffer.shift());\n }\n}\n\nipcRenderer.on('__trace-port', (event) => {\n __tracePort = event.ports[0];\n __tracePort.start();\n __traceFlush();\n});\n\ncontextBridge.exposeInMainWorld('__NOEGO_TRACE__', {\n send: (data: unknown) => {\n if (__tracePort) {\n __tracePort.postMessage(data);\n } else if (__traceBuffer.length >= __TRACE_BUFFER_CAPACITY) {\n __traceDroppedCapacity += 1;\n } else {\n __traceBuffer.push(data);\n }\n },\n status: () => ({\n queuedEvents: __traceBuffer.length,\n droppedCapacity: __traceDroppedCapacity,\n }),\n});\n\nconst __traceBatchProducerId = typeof globalThis.crypto?.randomUUID === 'function'\n ? globalThis.crypto.randomUUID()\n : \\`wood-renderer-\\${Date.now()}-\\${Math.random().toString(16).slice(2)}\\`;\nlet __traceBatchListener: ((message: unknown) => void) | null = null;\n\nconst __traceBatchPort: WoodTraceBatchPort = {\n postMessage: (batch: WoodTraceBatch) => {\n void ipcRenderer.invoke('__traceBatch', batch)\n .then((acknowledgement: WoodTraceBatchAcknowledgement) => {\n __traceBatchListener?.(acknowledgement);\n })\n .catch((error: unknown) => {\n __traceBatchListener?.({\n kind: 'retryable',\n batchId: batch.batchId,\n reason: error instanceof Error ? error.message : String(error),\n retryAfterMs: 100,\n });\n });\n },\n subscribe: (listener: (message: unknown) => void) => {\n __traceBatchListener = listener;\n return () => {\n if (__traceBatchListener === listener) {\n __traceBatchListener = null;\n }\n };\n },\n close: () => {\n __traceBatchListener = null;\n },\n};\n\n// Batching policy resolved from wood.config.yml observability configuration\n// at generation time; this file embeds but does not own it.\nconst __traceBatchTransport = new RendererTraceBatchTransport({\n producerId: __traceBatchProducerId,\n maxQueuedEvents: ${traceBatching.maxQueuedEvents},\n maxBatchEvents: ${traceBatching.maxBatchEvents},\n acknowledgementTimeoutMs: ${traceBatching.acknowledgementTimeoutMs},\n retryLimit: ${traceBatching.retryLimit},\n retryBaseDelayMs: ${traceBatching.retryBaseDelayMs},\n now: () => Date.now(),\n schedule: (work: () => void) => queueMicrotask(work),\n delay: (durationMs: number) => new Promise((resolve) => setTimeout(resolve, durationMs)),\n});\n__traceBatchTransport.attachPort(__traceBatchPort);\n\ncontextBridge.exposeInMainWorld('__NOEGO_WOOD_TRACE_BATCH__', {\n send: (event: WoodRendererTraceInput) => __traceBatchTransport.admit(event),\n status: () => __traceBatchTransport.status(),\n flush: (deadlineMs: number) => __traceBatchTransport.flush(deadlineMs),\n close: (deadlineMs: number) => __traceBatchTransport.close(deadlineMs),\n});\n\n// App-registered shutdown drain hooks (essential user-state persistence).\n// Main waits for the drain acknowledgement, bounded by deadlineMs, before it\n// destroys this window, so hooks must flush quickly and never block forever.\ntype __WoodShutdownDrainHook = (deadlineMs: number) => unknown;\nconst __shutdownDrainHooks = new Set<__WoodShutdownDrainHook>();\n\nasync function __runShutdownDrainHooks(deadlineMs: number): Promise<{ hooks: number; settled: number; failed: number }> {\n const hooks = [...__shutdownDrainHooks];\n if (hooks.length === 0) {\n return { hooks: 0, settled: 0, failed: 0 };\n }\n const remainingMs = Math.max(0, deadlineMs - Date.now());\n const outcomes = await Promise.race([\n Promise.allSettled(hooks.map(async (hook) => hook(deadlineMs))),\n new Promise<null>((resolve) => setTimeout(() => resolve(null), remainingMs)),\n ]);\n if (outcomes === null) {\n return { hooks: hooks.length, settled: 0, failed: 0 };\n }\n const failed = outcomes.filter((outcome) => outcome.status === 'rejected').length;\n return { hooks: hooks.length, settled: outcomes.length, failed };\n}\n\ncontextBridge.exposeInMainWorld('__NOEGO_WOOD_SHUTDOWN__', {\n onDrain: (hook: __WoodShutdownDrainHook) => {\n if (typeof hook !== 'function') {\n return () => {};\n }\n __shutdownDrainHooks.add(hook);\n return () => { __shutdownDrainHooks.delete(hook); };\n },\n});\n\nipcRenderer.on('__woodTraceDrainRequest', (_event, payload: unknown) => {\n if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {\n return;\n }\n const requestId = Reflect.get(payload, 'requestId');\n const deadlineMs = Reflect.get(payload, 'deadlineMs');\n if (typeof requestId !== 'string'\n || typeof deadlineMs !== 'number'\n || !Number.isFinite(deadlineMs)) {\n return;\n }\n let persistence: { hooks: number; settled: number; failed: number } = { hooks: 0, settled: 0, failed: 0 };\n void __runShutdownDrainHooks(deadlineMs)\n .then((result) => { persistence = result; })\n .catch(() => {})\n .then(() => __traceBatchTransport.close(deadlineMs))\n .finally(() => {\n ipcRenderer.send('__woodTraceDrainAck', {\n requestId,\n status: __traceBatchTransport.status(),\n persistence,\n });\n });\n});\n\nglobalThis.addEventListener?.('pagehide', () => {\n void __traceBatchTransport.flush(Date.now() + 100);\n});\n`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -232,6 +232,39 @@ contextBridge.exposeInMainWorld('__NOEGO_WOOD_TRACE_BATCH__', {
|
|
|
232
232
|
close: (deadlineMs: number) => __traceBatchTransport.close(deadlineMs),
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
+
// App-registered shutdown drain hooks (essential user-state persistence).
|
|
236
|
+
// Main waits for the drain acknowledgement, bounded by deadlineMs, before it
|
|
237
|
+
// destroys this window, so hooks must flush quickly and never block forever.
|
|
238
|
+
type __WoodShutdownDrainHook = (deadlineMs: number) => unknown;
|
|
239
|
+
const __shutdownDrainHooks = new Set<__WoodShutdownDrainHook>();
|
|
240
|
+
|
|
241
|
+
async function __runShutdownDrainHooks(deadlineMs: number): Promise<{ hooks: number; settled: number; failed: number }> {
|
|
242
|
+
const hooks = [...__shutdownDrainHooks];
|
|
243
|
+
if (hooks.length === 0) {
|
|
244
|
+
return { hooks: 0, settled: 0, failed: 0 };
|
|
245
|
+
}
|
|
246
|
+
const remainingMs = Math.max(0, deadlineMs - Date.now());
|
|
247
|
+
const outcomes = await Promise.race([
|
|
248
|
+
Promise.allSettled(hooks.map(async (hook) => hook(deadlineMs))),
|
|
249
|
+
new Promise<null>((resolve) => setTimeout(() => resolve(null), remainingMs)),
|
|
250
|
+
]);
|
|
251
|
+
if (outcomes === null) {
|
|
252
|
+
return { hooks: hooks.length, settled: 0, failed: 0 };
|
|
253
|
+
}
|
|
254
|
+
const failed = outcomes.filter((outcome) => outcome.status === 'rejected').length;
|
|
255
|
+
return { hooks: hooks.length, settled: outcomes.length, failed };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
contextBridge.exposeInMainWorld('__NOEGO_WOOD_SHUTDOWN__', {
|
|
259
|
+
onDrain: (hook: __WoodShutdownDrainHook) => {
|
|
260
|
+
if (typeof hook !== 'function') {
|
|
261
|
+
return () => {};
|
|
262
|
+
}
|
|
263
|
+
__shutdownDrainHooks.add(hook);
|
|
264
|
+
return () => { __shutdownDrainHooks.delete(hook); };
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
235
268
|
ipcRenderer.on('__woodTraceDrainRequest', (_event, payload: unknown) => {
|
|
236
269
|
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
|
|
237
270
|
return;
|
|
@@ -243,12 +276,18 @@ ipcRenderer.on('__woodTraceDrainRequest', (_event, payload: unknown) => {
|
|
|
243
276
|
|| !Number.isFinite(deadlineMs)) {
|
|
244
277
|
return;
|
|
245
278
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
279
|
+
let persistence: { hooks: number; settled: number; failed: number } = { hooks: 0, settled: 0, failed: 0 };
|
|
280
|
+
void __runShutdownDrainHooks(deadlineMs)
|
|
281
|
+
.then((result) => { persistence = result; })
|
|
282
|
+
.catch(() => {})
|
|
283
|
+
.then(() => __traceBatchTransport.close(deadlineMs))
|
|
284
|
+
.finally(() => {
|
|
285
|
+
ipcRenderer.send('__woodTraceDrainAck', {
|
|
286
|
+
requestId,
|
|
287
|
+
status: __traceBatchTransport.status(),
|
|
288
|
+
persistence,
|
|
289
|
+
});
|
|
250
290
|
});
|
|
251
|
-
});
|
|
252
291
|
});
|
|
253
292
|
|
|
254
293
|
globalThis.addEventListener?.('pagehide', () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/codegen/preload_generator.ts"],"sourcesContent":["import type { OperationDef } from '../types/index.js';\nimport type { RendererTraceBatchingConfig } from '../types/config.js';\nimport { RENDERER_TRACE_BATCHING_DEFAULTS } from '../config/config_parser.js';\nimport { generatedFileNotice } from './generated_file_notice.js';\n\nexport type GeneratePreloadOptions = {\n /**\n * Renderer→main IPC trace batching policy resolved from\n * wood.config.yml `observability.trace.rendererToMain.batching`.\n * The generated preload embeds these values but does not own them.\n */\n traceBatching?: RendererTraceBatchingConfig;\n};\n\nconst PRELOAD_SOURCE_INSTRUCTIONS = [\n 'To change IPC channels or preload bridge methods, edit the operations YAML configured by',\n 'wood.config.yml main.openapi and regenerate with `wood dev` or `wood build`.',\n] as const;\n\nfunction operationHasInput(op: OperationDef): boolean {\n return Boolean(\n op.inputSchema &&\n (op.inputSchema as Record<string, unknown>).type === 'object' &&\n Object.keys((op.inputSchema as Record<string, Record<string, unknown>>).properties ?? {}).length > 0,\n );\n}\n\n/**\n * Build a nested object tree from dot-path operation channels.\n * \"chat.message.send\" -> { chat: { message: { send: ... } } }\n */\nfunction buildNestedTree(operations: Map<string, OperationDef>): Record<string, unknown> {\n const tree: Record<string, unknown> = {};\n\n for (const [channel, op] of operations) {\n const parts = channel.split('.');\n let current: Record<string, unknown> = tree;\n\n for (let i = 0; i < parts.length - 1; i++) {\n if (!current[parts[i]]) current[parts[i]] = {};\n current = current[parts[i]] as Record<string, unknown>;\n }\n\n const leaf = parts[parts.length - 1];\n current[leaf] = { __operation: op };\n }\n\n return tree;\n}\n\n/**\n * Generate TypeScript code for a nested tree node.\n */\nfunction generateTreeCode(node: Record<string, unknown>, indent: string = ' '): string {\n const lines: string[] = [];\n\n for (const [key, value] of Object.entries(node)) {\n const entry = value as Record<string, unknown>;\n if (entry.__operation) {\n const op = entry.__operation as OperationDef;\n const hasInput = operationHasInput(op);\n\n if (hasInput) {\n lines.push(`${indent}${key}: (data: unknown) =>`);\n lines.push(`${indent} ipcRenderer.invoke('${op.channel}', data),`);\n } else {\n lines.push(`${indent}${key}: () =>`);\n lines.push(`${indent} ipcRenderer.invoke('${op.channel}'),`);\n }\n } else {\n lines.push(`${indent}${key}: {`);\n lines.push(generateTreeCode(entry, indent + ' '));\n lines.push(`${indent}},`);\n }\n }\n\n return lines.join('\\n');\n}\n\nfunction generateRpcManifestCode(operations: Map<string, OperationDef>): string {\n const entries = Array.from(operations.values())\n .sort((a, b) => a.channel.localeCompare(b.channel))\n .map((op) => {\n const payload = {\n channel: op.channel,\n controller: op.controller,\n action: op.action,\n path: op.channel.split('.'),\n hasInput: operationHasInput(op),\n };\n return ` ${JSON.stringify(payload)},`;\n });\n\n return `const __WOOD_RPC_MANIFEST = [\n${entries.join('\\n')}\n];`;\n}\n\nconst INTERNAL_BRIDGE_GLOBAL_KEY = '__NOEGO_WOOD_BRIDGE__';\n\n/**\n * Generate a complete preload.ts file from operations.\n */\nexport function generatePreload(\n operations: Map<string, OperationDef>,\n options: GeneratePreloadOptions = {},\n): string {\n const tree = buildNestedTree(operations);\n const operationsCode = generateTreeCode(tree);\n const rpcManifestCode = generateRpcManifestCode(operations);\n const traceBatching = options.traceBatching ?? RENDERER_TRACE_BATCHING_DEFAULTS;\n\n return `${generatedFileNotice(PRELOAD_SOURCE_INSTRUCTIONS)}\nimport { contextBridge, ipcRenderer } from 'electron';\nimport {\n RendererTraceBatchTransport,\n type WoodRendererTraceInput,\n type WoodTraceBatch,\n type WoodTraceBatchAcknowledgement,\n type WoodTraceBatchPort,\n} from '@noego/wood/trace-transport';\n\n${rpcManifestCode}\n\nconst __WOOD_INTERNAL_BRIDGE_KEY = '${INTERNAL_BRIDGE_GLOBAL_KEY}';\n\ncontextBridge.exposeInMainWorld(__WOOD_INTERNAL_BRIDGE_KEY, {\n${operationsCode}\n\n // Internal: backend controller/action RPC map for @Inject(App)\n __rpcManifest: __WOOD_RPC_MANIFEST,\n\n // Internal: loader data requests\n __load: (route: string, params: Record<string, string>) =>\n ipcRenderer.invoke('__load', { route, params }),\n\n // Internal: cross-window navigation\n __navigate: (windowId: string, route: string) =>\n ipcRenderer.invoke('__navigate', { windowId, route }),\n\n // Internal: window management\n __window: {\n current: () =>\n ipcRenderer.invoke('__window', { action: 'current' }),\n open: (windowId: string, options?: Record<string, unknown>) =>\n ipcRenderer.invoke('__window', { action: 'open', windowId, options }),\n close: (windowId?: string) =>\n ipcRenderer.invoke('__window', { action: 'close', windowId }),\n focus: (windowId: string) =>\n ipcRenderer.invoke('__window', { action: 'focus', windowId }),\n minimize: (windowId?: string) =>\n ipcRenderer.invoke('__window', { action: 'minimize', windowId }),\n maximize: (windowId?: string) =>\n ipcRenderer.invoke('__window', { action: 'maximize', windowId }),\n },\n\n // Internal: framework config (trace min level, etc)\n __woodConfig: () =>\n ipcRenderer.invoke('__woodConfig'),\n\n // Internal: debug metadata\n __debug: {\n current: () =>\n ipcRenderer.invoke('__debug', { action: 'current' }),\n },\n\n // Internal: context menu\n __contextMenu: {\n show: (data: unknown) =>\n ipcRenderer.invoke('__contextMenu', { action: 'show', ...data as Record<string, unknown> }),\n },\n\n // Push events from main process\n on: (channel: string, callback: (...args: unknown[]) => void) => {\n const subscription = (_event: unknown, ...args: unknown[]) => callback(...args);\n ipcRenderer.on(\\`__event:\\${channel}\\`, subscription);\n return () => ipcRenderer.removeListener(\\`__event:\\${channel}\\`, subscription);\n },\n __trace: (event: unknown) => {\n ipcRenderer.send('__trace', event);\n },\n __log: (level: string, logger: string, message: string, context?: unknown) => {\n ipcRenderer.send('__log', { level, logger, message, context });\n },\n __logBatch: (batch: unknown) => {\n ipcRenderer.send('__log_batch', batch);\n },\n});\n\n// Trace transport via MessagePort (activated when main process sends a port)\nlet __tracePort: MessagePort | null = null;\nconst __traceBuffer: unknown[] = [];\nconst __TRACE_BUFFER_CAPACITY = 256;\nlet __traceDroppedCapacity = 0;\n\nfunction __traceFlush(): void {\n while (__traceBuffer.length > 0 && __tracePort) {\n __tracePort.postMessage(__traceBuffer.shift());\n }\n}\n\nipcRenderer.on('__trace-port', (event) => {\n __tracePort = event.ports[0];\n __tracePort.start();\n __traceFlush();\n});\n\ncontextBridge.exposeInMainWorld('__NOEGO_TRACE__', {\n send: (data: unknown) => {\n if (__tracePort) {\n __tracePort.postMessage(data);\n } else if (__traceBuffer.length >= __TRACE_BUFFER_CAPACITY) {\n __traceDroppedCapacity += 1;\n } else {\n __traceBuffer.push(data);\n }\n },\n status: () => ({\n queuedEvents: __traceBuffer.length,\n droppedCapacity: __traceDroppedCapacity,\n }),\n});\n\nconst __traceBatchProducerId = typeof globalThis.crypto?.randomUUID === 'function'\n ? globalThis.crypto.randomUUID()\n : \\`wood-renderer-\\${Date.now()}-\\${Math.random().toString(16).slice(2)}\\`;\nlet __traceBatchListener: ((message: unknown) => void) | null = null;\n\nconst __traceBatchPort: WoodTraceBatchPort = {\n postMessage: (batch: WoodTraceBatch) => {\n void ipcRenderer.invoke('__traceBatch', batch)\n .then((acknowledgement: WoodTraceBatchAcknowledgement) => {\n __traceBatchListener?.(acknowledgement);\n })\n .catch((error: unknown) => {\n __traceBatchListener?.({\n kind: 'retryable',\n batchId: batch.batchId,\n reason: error instanceof Error ? error.message : String(error),\n retryAfterMs: 100,\n });\n });\n },\n subscribe: (listener: (message: unknown) => void) => {\n __traceBatchListener = listener;\n return () => {\n if (__traceBatchListener === listener) {\n __traceBatchListener = null;\n }\n };\n },\n close: () => {\n __traceBatchListener = null;\n },\n};\n\n// Batching policy resolved from wood.config.yml observability configuration\n// at generation time; this file embeds but does not own it.\nconst __traceBatchTransport = new RendererTraceBatchTransport({\n producerId: __traceBatchProducerId,\n maxQueuedEvents: ${traceBatching.maxQueuedEvents},\n maxBatchEvents: ${traceBatching.maxBatchEvents},\n acknowledgementTimeoutMs: ${traceBatching.acknowledgementTimeoutMs},\n retryLimit: ${traceBatching.retryLimit},\n retryBaseDelayMs: ${traceBatching.retryBaseDelayMs},\n now: () => Date.now(),\n schedule: (work: () => void) => queueMicrotask(work),\n delay: (durationMs: number) => new Promise((resolve) => setTimeout(resolve, durationMs)),\n});\n__traceBatchTransport.attachPort(__traceBatchPort);\n\ncontextBridge.exposeInMainWorld('__NOEGO_WOOD_TRACE_BATCH__', {\n send: (event: WoodRendererTraceInput) => __traceBatchTransport.admit(event),\n status: () => __traceBatchTransport.status(),\n flush: (deadlineMs: number) => __traceBatchTransport.flush(deadlineMs),\n close: (deadlineMs: number) => __traceBatchTransport.close(deadlineMs),\n});\n\nipcRenderer.on('__woodTraceDrainRequest', (_event, payload: unknown) => {\n if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {\n return;\n }\n const requestId = Reflect.get(payload, 'requestId');\n const deadlineMs = Reflect.get(payload, 'deadlineMs');\n if (typeof requestId !== 'string'\n || typeof deadlineMs !== 'number'\n || !Number.isFinite(deadlineMs)) {\n return;\n }\n void __traceBatchTransport.close(deadlineMs).finally(() => {\n ipcRenderer.send('__woodTraceDrainAck', {\n requestId,\n status: __traceBatchTransport.status(),\n });\n });\n});\n\nglobalThis.addEventListener?.('pagehide', () => {\n void __traceBatchTransport.flush(Date.now() + 100);\n});\n`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/codegen/preload_generator.ts"],"sourcesContent":["import type { OperationDef } from '../types/index.js';\nimport type { RendererTraceBatchingConfig } from '../types/config.js';\nimport { RENDERER_TRACE_BATCHING_DEFAULTS } from '../config/config_parser.js';\nimport { generatedFileNotice } from './generated_file_notice.js';\n\nexport type GeneratePreloadOptions = {\n /**\n * Renderer→main IPC trace batching policy resolved from\n * wood.config.yml `observability.trace.rendererToMain.batching`.\n * The generated preload embeds these values but does not own them.\n */\n traceBatching?: RendererTraceBatchingConfig;\n};\n\nconst PRELOAD_SOURCE_INSTRUCTIONS = [\n 'To change IPC channels or preload bridge methods, edit the operations YAML configured by',\n 'wood.config.yml main.openapi and regenerate with `wood dev` or `wood build`.',\n] as const;\n\nfunction operationHasInput(op: OperationDef): boolean {\n return Boolean(\n op.inputSchema &&\n (op.inputSchema as Record<string, unknown>).type === 'object' &&\n Object.keys((op.inputSchema as Record<string, Record<string, unknown>>).properties ?? {}).length > 0,\n );\n}\n\n/**\n * Build a nested object tree from dot-path operation channels.\n * \"chat.message.send\" -> { chat: { message: { send: ... } } }\n */\nfunction buildNestedTree(operations: Map<string, OperationDef>): Record<string, unknown> {\n const tree: Record<string, unknown> = {};\n\n for (const [channel, op] of operations) {\n const parts = channel.split('.');\n let current: Record<string, unknown> = tree;\n\n for (let i = 0; i < parts.length - 1; i++) {\n if (!current[parts[i]]) current[parts[i]] = {};\n current = current[parts[i]] as Record<string, unknown>;\n }\n\n const leaf = parts[parts.length - 1];\n current[leaf] = { __operation: op };\n }\n\n return tree;\n}\n\n/**\n * Generate TypeScript code for a nested tree node.\n */\nfunction generateTreeCode(node: Record<string, unknown>, indent: string = ' '): string {\n const lines: string[] = [];\n\n for (const [key, value] of Object.entries(node)) {\n const entry = value as Record<string, unknown>;\n if (entry.__operation) {\n const op = entry.__operation as OperationDef;\n const hasInput = operationHasInput(op);\n\n if (hasInput) {\n lines.push(`${indent}${key}: (data: unknown) =>`);\n lines.push(`${indent} ipcRenderer.invoke('${op.channel}', data),`);\n } else {\n lines.push(`${indent}${key}: () =>`);\n lines.push(`${indent} ipcRenderer.invoke('${op.channel}'),`);\n }\n } else {\n lines.push(`${indent}${key}: {`);\n lines.push(generateTreeCode(entry, indent + ' '));\n lines.push(`${indent}},`);\n }\n }\n\n return lines.join('\\n');\n}\n\nfunction generateRpcManifestCode(operations: Map<string, OperationDef>): string {\n const entries = Array.from(operations.values())\n .sort((a, b) => a.channel.localeCompare(b.channel))\n .map((op) => {\n const payload = {\n channel: op.channel,\n controller: op.controller,\n action: op.action,\n path: op.channel.split('.'),\n hasInput: operationHasInput(op),\n };\n return ` ${JSON.stringify(payload)},`;\n });\n\n return `const __WOOD_RPC_MANIFEST = [\n${entries.join('\\n')}\n];`;\n}\n\nconst INTERNAL_BRIDGE_GLOBAL_KEY = '__NOEGO_WOOD_BRIDGE__';\n\n/**\n * Generate a complete preload.ts file from operations.\n */\nexport function generatePreload(\n operations: Map<string, OperationDef>,\n options: GeneratePreloadOptions = {},\n): string {\n const tree = buildNestedTree(operations);\n const operationsCode = generateTreeCode(tree);\n const rpcManifestCode = generateRpcManifestCode(operations);\n const traceBatching = options.traceBatching ?? RENDERER_TRACE_BATCHING_DEFAULTS;\n\n return `${generatedFileNotice(PRELOAD_SOURCE_INSTRUCTIONS)}\nimport { contextBridge, ipcRenderer } from 'electron';\nimport {\n RendererTraceBatchTransport,\n type WoodRendererTraceInput,\n type WoodTraceBatch,\n type WoodTraceBatchAcknowledgement,\n type WoodTraceBatchPort,\n} from '@noego/wood/trace-transport';\n\n${rpcManifestCode}\n\nconst __WOOD_INTERNAL_BRIDGE_KEY = '${INTERNAL_BRIDGE_GLOBAL_KEY}';\n\ncontextBridge.exposeInMainWorld(__WOOD_INTERNAL_BRIDGE_KEY, {\n${operationsCode}\n\n // Internal: backend controller/action RPC map for @Inject(App)\n __rpcManifest: __WOOD_RPC_MANIFEST,\n\n // Internal: loader data requests\n __load: (route: string, params: Record<string, string>) =>\n ipcRenderer.invoke('__load', { route, params }),\n\n // Internal: cross-window navigation\n __navigate: (windowId: string, route: string) =>\n ipcRenderer.invoke('__navigate', { windowId, route }),\n\n // Internal: window management\n __window: {\n current: () =>\n ipcRenderer.invoke('__window', { action: 'current' }),\n open: (windowId: string, options?: Record<string, unknown>) =>\n ipcRenderer.invoke('__window', { action: 'open', windowId, options }),\n close: (windowId?: string) =>\n ipcRenderer.invoke('__window', { action: 'close', windowId }),\n focus: (windowId: string) =>\n ipcRenderer.invoke('__window', { action: 'focus', windowId }),\n minimize: (windowId?: string) =>\n ipcRenderer.invoke('__window', { action: 'minimize', windowId }),\n maximize: (windowId?: string) =>\n ipcRenderer.invoke('__window', { action: 'maximize', windowId }),\n },\n\n // Internal: framework config (trace min level, etc)\n __woodConfig: () =>\n ipcRenderer.invoke('__woodConfig'),\n\n // Internal: debug metadata\n __debug: {\n current: () =>\n ipcRenderer.invoke('__debug', { action: 'current' }),\n },\n\n // Internal: context menu\n __contextMenu: {\n show: (data: unknown) =>\n ipcRenderer.invoke('__contextMenu', { action: 'show', ...data as Record<string, unknown> }),\n },\n\n // Push events from main process\n on: (channel: string, callback: (...args: unknown[]) => void) => {\n const subscription = (_event: unknown, ...args: unknown[]) => callback(...args);\n ipcRenderer.on(\\`__event:\\${channel}\\`, subscription);\n return () => ipcRenderer.removeListener(\\`__event:\\${channel}\\`, subscription);\n },\n __trace: (event: unknown) => {\n ipcRenderer.send('__trace', event);\n },\n __log: (level: string, logger: string, message: string, context?: unknown) => {\n ipcRenderer.send('__log', { level, logger, message, context });\n },\n __logBatch: (batch: unknown) => {\n ipcRenderer.send('__log_batch', batch);\n },\n});\n\n// Trace transport via MessagePort (activated when main process sends a port)\nlet __tracePort: MessagePort | null = null;\nconst __traceBuffer: unknown[] = [];\nconst __TRACE_BUFFER_CAPACITY = 256;\nlet __traceDroppedCapacity = 0;\n\nfunction __traceFlush(): void {\n while (__traceBuffer.length > 0 && __tracePort) {\n __tracePort.postMessage(__traceBuffer.shift());\n }\n}\n\nipcRenderer.on('__trace-port', (event) => {\n __tracePort = event.ports[0];\n __tracePort.start();\n __traceFlush();\n});\n\ncontextBridge.exposeInMainWorld('__NOEGO_TRACE__', {\n send: (data: unknown) => {\n if (__tracePort) {\n __tracePort.postMessage(data);\n } else if (__traceBuffer.length >= __TRACE_BUFFER_CAPACITY) {\n __traceDroppedCapacity += 1;\n } else {\n __traceBuffer.push(data);\n }\n },\n status: () => ({\n queuedEvents: __traceBuffer.length,\n droppedCapacity: __traceDroppedCapacity,\n }),\n});\n\nconst __traceBatchProducerId = typeof globalThis.crypto?.randomUUID === 'function'\n ? globalThis.crypto.randomUUID()\n : \\`wood-renderer-\\${Date.now()}-\\${Math.random().toString(16).slice(2)}\\`;\nlet __traceBatchListener: ((message: unknown) => void) | null = null;\n\nconst __traceBatchPort: WoodTraceBatchPort = {\n postMessage: (batch: WoodTraceBatch) => {\n void ipcRenderer.invoke('__traceBatch', batch)\n .then((acknowledgement: WoodTraceBatchAcknowledgement) => {\n __traceBatchListener?.(acknowledgement);\n })\n .catch((error: unknown) => {\n __traceBatchListener?.({\n kind: 'retryable',\n batchId: batch.batchId,\n reason: error instanceof Error ? error.message : String(error),\n retryAfterMs: 100,\n });\n });\n },\n subscribe: (listener: (message: unknown) => void) => {\n __traceBatchListener = listener;\n return () => {\n if (__traceBatchListener === listener) {\n __traceBatchListener = null;\n }\n };\n },\n close: () => {\n __traceBatchListener = null;\n },\n};\n\n// Batching policy resolved from wood.config.yml observability configuration\n// at generation time; this file embeds but does not own it.\nconst __traceBatchTransport = new RendererTraceBatchTransport({\n producerId: __traceBatchProducerId,\n maxQueuedEvents: ${traceBatching.maxQueuedEvents},\n maxBatchEvents: ${traceBatching.maxBatchEvents},\n acknowledgementTimeoutMs: ${traceBatching.acknowledgementTimeoutMs},\n retryLimit: ${traceBatching.retryLimit},\n retryBaseDelayMs: ${traceBatching.retryBaseDelayMs},\n now: () => Date.now(),\n schedule: (work: () => void) => queueMicrotask(work),\n delay: (durationMs: number) => new Promise((resolve) => setTimeout(resolve, durationMs)),\n});\n__traceBatchTransport.attachPort(__traceBatchPort);\n\ncontextBridge.exposeInMainWorld('__NOEGO_WOOD_TRACE_BATCH__', {\n send: (event: WoodRendererTraceInput) => __traceBatchTransport.admit(event),\n status: () => __traceBatchTransport.status(),\n flush: (deadlineMs: number) => __traceBatchTransport.flush(deadlineMs),\n close: (deadlineMs: number) => __traceBatchTransport.close(deadlineMs),\n});\n\n// App-registered shutdown drain hooks (essential user-state persistence).\n// Main waits for the drain acknowledgement, bounded by deadlineMs, before it\n// destroys this window, so hooks must flush quickly and never block forever.\ntype __WoodShutdownDrainHook = (deadlineMs: number) => unknown;\nconst __shutdownDrainHooks = new Set<__WoodShutdownDrainHook>();\n\nasync function __runShutdownDrainHooks(deadlineMs: number): Promise<{ hooks: number; settled: number; failed: number }> {\n const hooks = [...__shutdownDrainHooks];\n if (hooks.length === 0) {\n return { hooks: 0, settled: 0, failed: 0 };\n }\n const remainingMs = Math.max(0, deadlineMs - Date.now());\n const outcomes = await Promise.race([\n Promise.allSettled(hooks.map(async (hook) => hook(deadlineMs))),\n new Promise<null>((resolve) => setTimeout(() => resolve(null), remainingMs)),\n ]);\n if (outcomes === null) {\n return { hooks: hooks.length, settled: 0, failed: 0 };\n }\n const failed = outcomes.filter((outcome) => outcome.status === 'rejected').length;\n return { hooks: hooks.length, settled: outcomes.length, failed };\n}\n\ncontextBridge.exposeInMainWorld('__NOEGO_WOOD_SHUTDOWN__', {\n onDrain: (hook: __WoodShutdownDrainHook) => {\n if (typeof hook !== 'function') {\n return () => {};\n }\n __shutdownDrainHooks.add(hook);\n return () => { __shutdownDrainHooks.delete(hook); };\n },\n});\n\nipcRenderer.on('__woodTraceDrainRequest', (_event, payload: unknown) => {\n if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {\n return;\n }\n const requestId = Reflect.get(payload, 'requestId');\n const deadlineMs = Reflect.get(payload, 'deadlineMs');\n if (typeof requestId !== 'string'\n || typeof deadlineMs !== 'number'\n || !Number.isFinite(deadlineMs)) {\n return;\n }\n let persistence: { hooks: number; settled: number; failed: number } = { hooks: 0, settled: 0, failed: 0 };\n void __runShutdownDrainHooks(deadlineMs)\n .then((result) => { persistence = result; })\n .catch(() => {})\n .then(() => __traceBatchTransport.close(deadlineMs))\n .finally(() => {\n ipcRenderer.send('__woodTraceDrainAck', {\n requestId,\n status: __traceBatchTransport.status(),\n persistence,\n });\n });\n});\n\nglobalThis.addEventListener?.('pagehide', () => {\n void __traceBatchTransport.flush(Date.now() + 100);\n});\n`;\n}\n"],"mappings":"AAEA,SAAS,wCAAwC;AACjD,SAAS,2BAA2B;AAWpC,MAAM,8BAA8B;AAAA,EAClC;AAAA,EACA;AACF;AAEA,SAAS,kBAAkB,IAA2B;AACpD,SAAO;AAAA,IACL,GAAG,eACA,GAAG,YAAwC,SAAS,YACrD,OAAO,KAAM,GAAG,YAAwD,cAAc,CAAC,CAAC,EAAE,SAAS;AAAA,EACvG;AACF;AAMA,SAAS,gBAAgB,YAAgE;AACvF,QAAM,OAAgC,CAAC;AAEvC,aAAW,CAAC,SAAS,EAAE,KAAK,YAAY;AACtC,UAAM,QAAQ,QAAQ,MAAM,GAAG;AAC/B,QAAI,UAAmC;AAEvC,aAAS,IAAI,GAAG,IAAI,MAAM,SAAS,GAAG,KAAK;AACzC,UAAI,CAAC,QAAQ,MAAM,CAAC,CAAC,EAAG,SAAQ,MAAM,CAAC,CAAC,IAAI,CAAC;AAC7C,gBAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,IAC5B;AAEA,UAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,YAAQ,IAAI,IAAI,EAAE,aAAa,GAAG;AAAA,EACpC;AAEA,SAAO;AACT;AAKA,SAAS,iBAAiB,MAA+B,SAAiB,MAAc;AACtF,QAAM,QAAkB,CAAC;AAEzB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,UAAM,QAAQ;AACd,QAAI,MAAM,aAAa;AACrB,YAAM,KAAK,MAAM;AACjB,YAAM,WAAW,kBAAkB,EAAE;AAErC,UAAI,UAAU;AACZ,cAAM,KAAK,GAAG,MAAM,GAAG,GAAG,sBAAsB;AAChD,cAAM,KAAK,GAAG,MAAM,yBAAyB,GAAG,OAAO,WAAW;AAAA,MACpE,OAAO;AACL,cAAM,KAAK,GAAG,MAAM,GAAG,GAAG,SAAS;AACnC,cAAM,KAAK,GAAG,MAAM,yBAAyB,GAAG,OAAO,KAAK;AAAA,MAC9D;AAAA,IACF,OAAO;AACL,YAAM,KAAK,GAAG,MAAM,GAAG,GAAG,KAAK;AAC/B,YAAM,KAAK,iBAAiB,OAAO,SAAS,IAAI,CAAC;AACjD,YAAM,KAAK,GAAG,MAAM,IAAI;AAAA,IAC1B;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,wBAAwB,YAA+C;AAC9E,QAAM,UAAU,MAAM,KAAK,WAAW,OAAO,CAAC,EAC3C,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,cAAc,EAAE,OAAO,CAAC,EACjD,IAAI,CAAC,OAAO;AACX,UAAM,UAAU;AAAA,MACd,SAAS,GAAG;AAAA,MACZ,YAAY,GAAG;AAAA,MACf,QAAQ,GAAG;AAAA,MACX,MAAM,GAAG,QAAQ,MAAM,GAAG;AAAA,MAC1B,UAAU,kBAAkB,EAAE;AAAA,IAChC;AACA,WAAO,KAAK,KAAK,UAAU,OAAO,CAAC;AAAA,EACrC,CAAC;AAEH,SAAO;AAAA,EACP,QAAQ,KAAK,IAAI,CAAC;AAAA;AAEpB;AAEA,MAAM,6BAA6B;AAK5B,SAAS,gBACd,YACA,UAAkC,CAAC,GAC3B;AACR,QAAM,OAAO,gBAAgB,UAAU;AACvC,QAAM,iBAAiB,iBAAiB,IAAI;AAC5C,QAAM,kBAAkB,wBAAwB,UAAU;AAC1D,QAAM,gBAAgB,QAAQ,iBAAiB;AAE/C,SAAO,GAAG,oBAAoB,2BAA2B,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAU1D,eAAe;AAAA;AAAA,sCAEqB,0BAA0B;AAAA;AAAA;AAAA,EAG9D,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAqIK,cAAc,eAAe;AAAA,oBAC9B,cAAc,cAAc;AAAA,8BAClB,cAAc,wBAAwB;AAAA,gBACpD,cAAc,UAAU;AAAA,sBAClB,cAAc,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4EpD;","names":[]}
|
|
@@ -5,6 +5,7 @@ import { WorkRegistry, WorkFailure } from './work_registry.cjs';
|
|
|
5
5
|
export { FrontendWorkError } from './work_registry.cjs';
|
|
6
6
|
export { FrontendLifecycle } from './frontend_lifecycle.cjs';
|
|
7
7
|
import { FrontendPage } from './frontend_page.cjs';
|
|
8
|
+
import '../navigation/layout_preservation.cjs';
|
|
8
9
|
import '../navigation/automation_introspection.cjs';
|
|
9
10
|
|
|
10
11
|
interface FrontendOpenOptions {
|
|
@@ -5,6 +5,7 @@ import { WorkRegistry, WorkFailure } from './work_registry.js';
|
|
|
5
5
|
export { FrontendWorkError } from './work_registry.js';
|
|
6
6
|
export { FrontendLifecycle } from './frontend_lifecycle.js';
|
|
7
7
|
import { FrontendPage } from './frontend_page.js';
|
|
8
|
+
import '../navigation/layout_preservation.js';
|
|
8
9
|
import '../navigation/automation_introspection.js';
|
|
9
10
|
|
|
10
11
|
interface FrontendOpenOptions {
|
|
@@ -7,4 +7,5 @@ export { CreateFrontendSurfaceOptions, FrontendApplicationSurface, FrontendOpenO
|
|
|
7
7
|
import '../types/views.cjs';
|
|
8
8
|
import '../transport/wood_rpc_transport.cjs';
|
|
9
9
|
import '../navigation/index.cjs';
|
|
10
|
+
import '../navigation/layout_preservation.cjs';
|
|
10
11
|
import '../navigation/automation_introspection.cjs';
|
package/dist/frontend/index.d.ts
CHANGED
|
@@ -7,4 +7,5 @@ export { CreateFrontendSurfaceOptions, FrontendApplicationSurface, FrontendOpenO
|
|
|
7
7
|
import '../types/views.js';
|
|
8
8
|
import '../transport/wood_rpc_transport.js';
|
|
9
9
|
import '../navigation/index.js';
|
|
10
|
+
import '../navigation/layout_preservation.js';
|
|
10
11
|
import '../navigation/automation_introspection.js';
|
package/dist/index.d.cts
CHANGED
|
@@ -38,9 +38,10 @@ export { WoodRendererTraceEvent, WoodRendererTraceInput, WoodTraceBatch, WoodTra
|
|
|
38
38
|
export { WoodTraceBatchContract } from './tracing/wood_trace_batch_contract.cjs';
|
|
39
39
|
export { TraceCommitRow, TraceCommitWriter, TraceCommitWriterOptions, TraceFlushOptions, TraceFlushResult, WoodTracePersistencePort } from './tracing/trace_commit_writer.cjs';
|
|
40
40
|
export { FrameworkTracer, MainTracer, RendererTracer, TestTracer, TraceProvider, TraceProviderConfiguration, TraceProviderMode, TraceRuntime } from './tracing/trace_provider.cjs';
|
|
41
|
-
export { Navigation, NavigationEntry,
|
|
41
|
+
export { Navigation, NavigationEntry, getNavigation, go, goback, goto } from './navigation/index.cjs';
|
|
42
42
|
export { Ipc, getIpc } from './ipc/index.cjs';
|
|
43
43
|
export { IpcPushOptions } from './ipc/shared.cjs';
|
|
44
|
+
export { NavigationOptions } from './navigation/layout_preservation.cjs';
|
|
44
45
|
import '@noego/ioc';
|
|
45
46
|
import './runtime/renderer_log_dispatcher.cjs';
|
|
46
47
|
import './runtime/renderer_config_extension.cjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -38,9 +38,10 @@ export { WoodRendererTraceEvent, WoodRendererTraceInput, WoodTraceBatch, WoodTra
|
|
|
38
38
|
export { WoodTraceBatchContract } from './tracing/wood_trace_batch_contract.js';
|
|
39
39
|
export { TraceCommitRow, TraceCommitWriter, TraceCommitWriterOptions, TraceFlushOptions, TraceFlushResult, WoodTracePersistencePort } from './tracing/trace_commit_writer.js';
|
|
40
40
|
export { FrameworkTracer, MainTracer, RendererTracer, TestTracer, TraceProvider, TraceProviderConfiguration, TraceProviderMode, TraceRuntime } from './tracing/trace_provider.js';
|
|
41
|
-
export { Navigation, NavigationEntry,
|
|
41
|
+
export { Navigation, NavigationEntry, getNavigation, go, goback, goto } from './navigation/index.js';
|
|
42
42
|
export { Ipc, getIpc } from './ipc/index.js';
|
|
43
43
|
export { IpcPushOptions } from './ipc/shared.js';
|
|
44
|
+
export { NavigationOptions } from './navigation/layout_preservation.js';
|
|
44
45
|
import '@noego/ioc';
|
|
45
46
|
import './runtime/renderer_log_dispatcher.js';
|
|
46
47
|
import './runtime/renderer_config_extension.js';
|
package/dist/main/index.cjs
CHANGED
|
@@ -29,10 +29,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
var main_exports = {};
|
|
30
30
|
__export(main_exports, {
|
|
31
31
|
BootstrapActivationGate: () => BootstrapActivationGate,
|
|
32
|
+
RENDERER_SHUTDOWN_DRAIN_TIMEOUT_MS: () => import_shutdown_coordinator2.RENDERER_SHUTDOWN_DRAIN_TIMEOUT_MS,
|
|
33
|
+
ShutdownCoordinator: () => import_shutdown_coordinator2.ShutdownCoordinator,
|
|
32
34
|
resolveWoodDebugMetadataConfiguration: () => resolveWoodDebugMetadataConfiguration,
|
|
33
35
|
setRendererConfigExtension: () => import_renderer_config_extension2.setRendererConfigExtension,
|
|
34
36
|
setRendererLogRecordFilter: () => import_renderer_log_batch_receiver2.setRendererLogRecordFilter,
|
|
35
|
-
shouldDeferLastWindowCloseForCleanup: () => shouldDeferLastWindowCloseForCleanup,
|
|
37
|
+
shouldDeferLastWindowCloseForCleanup: () => import_shutdown_coordinator2.shouldDeferLastWindowCloseForCleanup,
|
|
36
38
|
startWoodMain: () => startWoodMain
|
|
37
39
|
});
|
|
38
40
|
module.exports = __toCommonJS(main_exports);
|
|
@@ -54,6 +56,8 @@ var import_renderer_config_extension2 = require("../runtime/renderer_config_exte
|
|
|
54
56
|
var import_ipc = require("../ipc/index.js");
|
|
55
57
|
var import_shared = require("../ipc/shared.js");
|
|
56
58
|
var import_renderer_trace_drain_coordinator = require("./renderer_trace_drain_coordinator.js");
|
|
59
|
+
var import_shutdown_coordinator = require("./shutdown_coordinator.js");
|
|
60
|
+
var import_shutdown_coordinator2 = require("./shutdown_coordinator.js");
|
|
57
61
|
class BootstrapActivationGate {
|
|
58
62
|
constructor(hasWindow, createMainWindow) {
|
|
59
63
|
this.hasWindow = hasWindow;
|
|
@@ -81,9 +85,6 @@ class BootstrapActivationGate {
|
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
|
-
function shouldDeferLastWindowCloseForCleanup(trackedWindowCount, cleanupComplete) {
|
|
85
|
-
return trackedWindowCount === 1 && !cleanupComplete;
|
|
86
|
-
}
|
|
87
88
|
const DEFAULT_WOOD_REMOTE_DEBUGGING_PORT = 9333;
|
|
88
89
|
function resolveWoodDebugMetadataConfiguration(env = process.env) {
|
|
89
90
|
const isDevelopment = env.WOOD_MODE === "development";
|
|
@@ -133,8 +134,7 @@ function startWoodMain(options) {
|
|
|
133
134
|
const windows = /* @__PURE__ */ new Map();
|
|
134
135
|
const rendererHealth = /* @__PURE__ */ new Map();
|
|
135
136
|
let mainWindow = null;
|
|
136
|
-
let
|
|
137
|
-
let beforeQuitCleanupPromise = null;
|
|
137
|
+
let shutdownCoordinator = null;
|
|
138
138
|
let runtimeContext = null;
|
|
139
139
|
let mainBootCleanup;
|
|
140
140
|
let runtimeCleanup;
|
|
@@ -525,7 +525,11 @@ function startWoodMain(options) {
|
|
|
525
525
|
mainWindow = null;
|
|
526
526
|
}
|
|
527
527
|
if (windows.size === 0) {
|
|
528
|
-
|
|
528
|
+
if (shutdownCoordinator) {
|
|
529
|
+
shutdownCoordinator.handleAllWindowsClosed();
|
|
530
|
+
} else {
|
|
531
|
+
import_electron.app.quit();
|
|
532
|
+
}
|
|
529
533
|
}
|
|
530
534
|
});
|
|
531
535
|
if (options.onWindowCreated) {
|
|
@@ -727,73 +731,52 @@ function startWoodMain(options) {
|
|
|
727
731
|
};
|
|
728
732
|
void import_electron.app.whenReady().then(bootstrap).catch(handleBootstrapFailure);
|
|
729
733
|
const registerApplicationLifecycle = () => {
|
|
734
|
+
const coordinator = new import_shutdown_coordinator.ShutdownCoordinator({
|
|
735
|
+
trackedWindows: () => windows.entries(),
|
|
736
|
+
clearTrackedWindows: () => {
|
|
737
|
+
windows.clear();
|
|
738
|
+
},
|
|
739
|
+
drainRenderers: () => (0, import_renderer_trace_drain_coordinator.drainRendererTracesBeforeQuit)(
|
|
740
|
+
rendererTraceDrain,
|
|
741
|
+
windows.values(),
|
|
742
|
+
logger,
|
|
743
|
+
Date.now() + import_shutdown_coordinator.RENDERER_SHUTDOWN_DRAIN_TIMEOUT_MS
|
|
744
|
+
),
|
|
745
|
+
cleanupBackend: () => (0, import_cleanup.cleanupAll)([
|
|
746
|
+
() => runtimeCleanup?.(),
|
|
747
|
+
() => {
|
|
748
|
+
if (options.legacyGlobals) (0, import_shared.clearIpcPushSender)();
|
|
749
|
+
},
|
|
750
|
+
() => mainBootCleanup?.(),
|
|
751
|
+
() => options.onBeforeQuit?.(runtimeContext),
|
|
752
|
+
() => import_ioc.ExecutionContext.run(container, () => container.dispose())
|
|
753
|
+
], "Wood main shutdown failed"),
|
|
754
|
+
requestQuit: () => {
|
|
755
|
+
import_electron.app.quit();
|
|
756
|
+
},
|
|
757
|
+
logger
|
|
758
|
+
});
|
|
759
|
+
shutdownCoordinator = coordinator;
|
|
730
760
|
import_electron.app.on(
|
|
731
761
|
"browser-window-created",
|
|
732
762
|
(_event, window) => {
|
|
733
763
|
window.on("close", (event) => {
|
|
734
764
|
const isTrackedWindow = [...windows.values()].includes(window);
|
|
735
|
-
if (!isTrackedWindow
|
|
736
|
-
windows.size,
|
|
737
|
-
beforeQuitCleanupComplete
|
|
738
|
-
)) {
|
|
765
|
+
if (!isTrackedWindow) {
|
|
739
766
|
return;
|
|
740
767
|
}
|
|
741
|
-
event.
|
|
742
|
-
if (!beforeQuitCleanupPromise) {
|
|
743
|
-
logger.debug("deferring last window close for renderer trace drain");
|
|
744
|
-
import_electron.app.quit();
|
|
745
|
-
}
|
|
768
|
+
coordinator.handleTrackedWindowClose(event, windows.size);
|
|
746
769
|
});
|
|
747
770
|
}
|
|
748
771
|
);
|
|
749
|
-
const destroyWindowsForQuit = () => {
|
|
750
|
-
for (const [id, win] of windows) {
|
|
751
|
-
if (!win.isDestroyed()) {
|
|
752
|
-
win.destroy();
|
|
753
|
-
logger.debug("destroyed window on quit", { windowId: id });
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
windows.clear();
|
|
757
|
-
};
|
|
758
|
-
const runBeforeQuitCleanup = async () => {
|
|
759
|
-
await (0, import_cleanup.cleanupAll)([
|
|
760
|
-
() => (0, import_renderer_trace_drain_coordinator.drainRendererTracesBeforeQuit)(rendererTraceDrain, windows.values(), logger, Date.now() + 1e3),
|
|
761
|
-
() => runtimeCleanup?.(),
|
|
762
|
-
() => {
|
|
763
|
-
if (options.legacyGlobals) (0, import_shared.clearIpcPushSender)();
|
|
764
|
-
},
|
|
765
|
-
() => mainBootCleanup?.(),
|
|
766
|
-
() => options.onBeforeQuit?.(runtimeContext),
|
|
767
|
-
() => import_ioc.ExecutionContext.run(container, () => container.dispose())
|
|
768
|
-
], "Wood main shutdown failed");
|
|
769
|
-
};
|
|
770
772
|
import_electron.app.on("before-quit", (event) => {
|
|
771
773
|
if (!runtimeContext) {
|
|
772
774
|
return;
|
|
773
775
|
}
|
|
774
|
-
|
|
775
|
-
destroyWindowsForQuit();
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
event.preventDefault();
|
|
779
|
-
if (beforeQuitCleanupPromise) {
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
beforeQuitCleanupPromise = (async () => {
|
|
783
|
-
try {
|
|
784
|
-
await runBeforeQuitCleanup();
|
|
785
|
-
} catch (error) {
|
|
786
|
-
logger.warn("before-quit handler failed", { error: toErrorMessage(error) });
|
|
787
|
-
} finally {
|
|
788
|
-
beforeQuitCleanupComplete = true;
|
|
789
|
-
beforeQuitCleanupPromise = null;
|
|
790
|
-
}
|
|
791
|
-
destroyWindowsForQuit();
|
|
792
|
-
import_electron.app.quit();
|
|
793
|
-
})();
|
|
776
|
+
coordinator.handleBeforeQuit(event);
|
|
794
777
|
});
|
|
795
778
|
import_electron.app.on("window-all-closed", () => {
|
|
796
|
-
|
|
779
|
+
coordinator.handleAllWindowsClosed();
|
|
797
780
|
});
|
|
798
781
|
import_electron.app.on("activate", () => {
|
|
799
782
|
bootstrapActivationGate.activate();
|
|
@@ -812,6 +795,8 @@ function startWoodMain(options) {
|
|
|
812
795
|
// Annotate the CommonJS export names for ESM import in node:
|
|
813
796
|
0 && (module.exports = {
|
|
814
797
|
BootstrapActivationGate,
|
|
798
|
+
RENDERER_SHUTDOWN_DRAIN_TIMEOUT_MS,
|
|
799
|
+
ShutdownCoordinator,
|
|
815
800
|
resolveWoodDebugMetadataConfiguration,
|
|
816
801
|
setRendererConfigExtension,
|
|
817
802
|
setRendererLogRecordFilter,
|