@noego/wood 0.4.5 → 0.4.7

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 (248) hide show
  1. package/bin/wood.js +9 -1
  2. package/dist/client/app_env.cjs +39 -0
  3. package/dist/client/app_env.cjs.map +1 -0
  4. package/dist/client/app_env.d.cts +7 -0
  5. package/dist/client/app_env.d.ts +7 -0
  6. package/dist/client/app_env.js +14 -0
  7. package/dist/client/app_env.js.map +1 -0
  8. package/dist/client/index.cjs +33 -9
  9. package/dist/client/index.cjs.map +1 -1
  10. package/dist/client/index.d.cts +5 -1
  11. package/dist/client/index.d.ts +5 -1
  12. package/dist/client/index.js +32 -9
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/codegen/channel_map_generator.cjs +102 -0
  15. package/dist/codegen/channel_map_generator.cjs.map +1 -0
  16. package/dist/codegen/channel_map_generator.d.cts +6 -0
  17. package/dist/codegen/channel_map_generator.d.ts +6 -0
  18. package/dist/codegen/channel_map_generator.js +78 -0
  19. package/dist/codegen/channel_map_generator.js.map +1 -0
  20. package/dist/codegen/controller_registration_generator.cjs +6 -1
  21. package/dist/codegen/controller_registration_generator.cjs.map +1 -1
  22. package/dist/codegen/controller_registration_generator.js +6 -1
  23. package/dist/codegen/controller_registration_generator.js.map +1 -1
  24. package/dist/codegen/generated_file_notice.cjs +40 -0
  25. package/dist/codegen/generated_file_notice.cjs.map +1 -0
  26. package/dist/codegen/generated_file_notice.d.cts +3 -0
  27. package/dist/codegen/generated_file_notice.d.ts +3 -0
  28. package/dist/codegen/generated_file_notice.js +16 -0
  29. package/dist/codegen/generated_file_notice.js.map +1 -0
  30. package/dist/codegen/index.cjs +3 -0
  31. package/dist/codegen/index.cjs.map +1 -1
  32. package/dist/codegen/index.d.cts +1 -0
  33. package/dist/codegen/index.d.ts +1 -0
  34. package/dist/codegen/index.js +2 -0
  35. package/dist/codegen/index.js.map +1 -1
  36. package/dist/codegen/preload_generator.cjs +97 -1
  37. package/dist/codegen/preload_generator.cjs.map +1 -1
  38. package/dist/codegen/preload_generator.js +97 -1
  39. package/dist/codegen/preload_generator.js.map +1 -1
  40. package/dist/codegen/renderer_app_generator.cjs +6 -1
  41. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  42. package/dist/codegen/renderer_app_generator.js +6 -1
  43. package/dist/codegen/renderer_app_generator.js.map +1 -1
  44. package/dist/codegen/renderer_controller_registration_generator.cjs +6 -1
  45. package/dist/codegen/renderer_controller_registration_generator.cjs.map +1 -1
  46. package/dist/codegen/renderer_controller_registration_generator.js +6 -1
  47. package/dist/codegen/renderer_controller_registration_generator.js.map +1 -1
  48. package/dist/codegen/renderer_view_manifest_generator.cjs +6 -1
  49. package/dist/codegen/renderer_view_manifest_generator.cjs.map +1 -1
  50. package/dist/codegen/renderer_view_manifest_generator.js +6 -1
  51. package/dist/codegen/renderer_view_manifest_generator.js.map +1 -1
  52. package/dist/codegen/runtime_generator.cjs +6 -1
  53. package/dist/codegen/runtime_generator.cjs.map +1 -1
  54. package/dist/codegen/runtime_generator.js +6 -1
  55. package/dist/codegen/runtime_generator.js.map +1 -1
  56. package/dist/codegen/test_mock_generator.cjs +5 -1
  57. package/dist/codegen/test_mock_generator.cjs.map +1 -1
  58. package/dist/codegen/test_mock_generator.js +5 -1
  59. package/dist/codegen/test_mock_generator.js.map +1 -1
  60. package/dist/codegen/type_generator.cjs +7 -1
  61. package/dist/codegen/type_generator.cjs.map +1 -1
  62. package/dist/codegen/type_generator.js +7 -1
  63. package/dist/codegen/type_generator.js.map +1 -1
  64. package/dist/index.cjs +19 -6
  65. package/dist/index.cjs.map +1 -1
  66. package/dist/index.d.cts +7 -1
  67. package/dist/index.d.ts +7 -1
  68. package/dist/index.js +13 -1
  69. package/dist/index.js.map +1 -1
  70. package/dist/main/index.cjs +287 -62
  71. package/dist/main/index.cjs.map +1 -1
  72. package/dist/main/index.d.cts +57 -3
  73. package/dist/main/index.d.ts +57 -3
  74. package/dist/main/index.js +289 -62
  75. package/dist/main/index.js.map +1 -1
  76. package/dist/main/renderer_trace_drain_coordinator.cjs +127 -0
  77. package/dist/main/renderer_trace_drain_coordinator.cjs.map +1 -0
  78. package/dist/main/renderer_trace_drain_coordinator.d.cts +46 -0
  79. package/dist/main/renderer_trace_drain_coordinator.d.ts +46 -0
  80. package/dist/main/renderer_trace_drain_coordinator.js +101 -0
  81. package/dist/main/renderer_trace_drain_coordinator.js.map +1 -0
  82. package/dist/router/wood_router.cjs +9 -1
  83. package/dist/router/wood_router.cjs.map +1 -1
  84. package/dist/router/wood_router.d.cts +1 -1
  85. package/dist/router/wood_router.d.ts +1 -1
  86. package/dist/router/wood_router.js +9 -1
  87. package/dist/router/wood_router.js.map +1 -1
  88. package/dist/runtime/app_env.cjs +52 -0
  89. package/dist/runtime/app_env.cjs.map +1 -0
  90. package/dist/runtime/app_env.d.cts +10 -0
  91. package/dist/runtime/app_env.d.ts +10 -0
  92. package/dist/runtime/app_env.js +18 -0
  93. package/dist/runtime/app_env.js.map +1 -0
  94. package/dist/runtime/index.cjs +9 -2
  95. package/dist/runtime/index.cjs.map +1 -1
  96. package/dist/runtime/index.d.cts +5 -1
  97. package/dist/runtime/index.d.ts +5 -1
  98. package/dist/runtime/index.js +9 -1
  99. package/dist/runtime/index.js.map +1 -1
  100. package/dist/runtime/renderer_log_dispatcher.cjs.map +1 -1
  101. package/dist/runtime/renderer_log_dispatcher.d.cts +1 -1
  102. package/dist/runtime/renderer_log_dispatcher.d.ts +1 -1
  103. package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
  104. package/dist/runtime/renderer_trace_batch_receiver_registry.cjs +50 -0
  105. package/dist/runtime/renderer_trace_batch_receiver_registry.cjs.map +1 -0
  106. package/dist/runtime/renderer_trace_batch_receiver_registry.d.cts +10 -0
  107. package/dist/runtime/renderer_trace_batch_receiver_registry.d.ts +10 -0
  108. package/dist/runtime/renderer_trace_batch_receiver_registry.js +24 -0
  109. package/dist/runtime/renderer_trace_batch_receiver_registry.js.map +1 -0
  110. package/dist/runtime/runtime.cjs +75 -57
  111. package/dist/runtime/runtime.cjs.map +1 -1
  112. package/dist/runtime/runtime.d.cts +8 -1
  113. package/dist/runtime/runtime.d.ts +8 -1
  114. package/dist/runtime/runtime.js +80 -58
  115. package/dist/runtime/runtime.js.map +1 -1
  116. package/dist/testing/browser.cjs +540 -11
  117. package/dist/testing/browser.cjs.map +1 -1
  118. package/dist/testing/browser.d.cts +219 -3
  119. package/dist/testing/browser.d.ts +219 -3
  120. package/dist/testing/browser.js +540 -11
  121. package/dist/testing/browser.js.map +1 -1
  122. package/dist/testing/create_controller_harness.d.cts +1 -1
  123. package/dist/testing/create_controller_harness.d.ts +1 -1
  124. package/dist/testing/index.cjs +13 -0
  125. package/dist/testing/index.cjs.map +1 -1
  126. package/dist/testing/index.d.cts +7 -2
  127. package/dist/testing/index.d.ts +7 -2
  128. package/dist/testing/index.js +14 -0
  129. package/dist/testing/index.js.map +1 -1
  130. package/dist/trace-testing/index.d.cts +1 -1
  131. package/dist/trace-testing/index.d.ts +1 -1
  132. package/dist/trace-testing/trace_recorder.d.cts +1 -1
  133. package/dist/trace-testing/trace_recorder.d.ts +1 -1
  134. package/dist/trace-transport/index.cjs +32 -0
  135. package/dist/trace-transport/index.cjs.map +1 -0
  136. package/dist/trace-transport/index.d.cts +6 -0
  137. package/dist/trace-transport/index.d.ts +6 -0
  138. package/dist/trace-transport/index.js +7 -0
  139. package/dist/trace-transport/index.js.map +1 -0
  140. package/dist/trace_hub-Cob54qRx.d.cts +56 -0
  141. package/dist/trace_hub-Cob54qRx.d.ts +56 -0
  142. package/dist/tracing/index.cjs +14 -0
  143. package/dist/tracing/index.cjs.map +1 -1
  144. package/dist/tracing/index.d.cts +7 -1
  145. package/dist/tracing/index.d.ts +7 -1
  146. package/dist/tracing/index.js +12 -0
  147. package/dist/tracing/index.js.map +1 -1
  148. package/dist/tracing/renderer_trace_batch_host.cjs +80 -0
  149. package/dist/tracing/renderer_trace_batch_host.cjs.map +1 -0
  150. package/dist/tracing/renderer_trace_batch_host.d.cts +16 -0
  151. package/dist/tracing/renderer_trace_batch_host.d.ts +16 -0
  152. package/dist/tracing/renderer_trace_batch_host.js +56 -0
  153. package/dist/tracing/renderer_trace_batch_host.js.map +1 -0
  154. package/dist/tracing/renderer_trace_batch_transport.cjs +313 -0
  155. package/dist/tracing/renderer_trace_batch_transport.cjs.map +1 -0
  156. package/dist/tracing/renderer_trace_batch_transport.d.cts +53 -0
  157. package/dist/tracing/renderer_trace_batch_transport.d.ts +53 -0
  158. package/dist/tracing/renderer_trace_batch_transport.js +292 -0
  159. package/dist/tracing/renderer_trace_batch_transport.js.map +1 -0
  160. package/dist/tracing/trace_commit_writer.cjs +159 -0
  161. package/dist/tracing/trace_commit_writer.cjs.map +1 -0
  162. package/dist/tracing/trace_commit_writer.d.cts +97 -0
  163. package/dist/tracing/trace_commit_writer.d.ts +97 -0
  164. package/dist/tracing/trace_commit_writer.js +135 -0
  165. package/dist/tracing/trace_commit_writer.js.map +1 -0
  166. package/dist/tracing/trace_hub.cjs +58 -3
  167. package/dist/tracing/trace_hub.cjs.map +1 -1
  168. package/dist/tracing/trace_hub.d.cts +2 -46
  169. package/dist/tracing/trace_hub.d.ts +2 -46
  170. package/dist/tracing/trace_hub.js +56 -3
  171. package/dist/tracing/trace_hub.js.map +1 -1
  172. package/dist/tracing/trace_provider.cjs +42 -12
  173. package/dist/tracing/trace_provider.cjs.map +1 -1
  174. package/dist/tracing/trace_provider.d.cts +15 -1
  175. package/dist/tracing/trace_provider.d.ts +15 -1
  176. package/dist/tracing/trace_provider.js +43 -12
  177. package/dist/tracing/trace_provider.js.map +1 -1
  178. package/dist/tracing/types/wood_trace_batch.types.cjs +17 -0
  179. package/dist/tracing/types/wood_trace_batch.types.cjs.map +1 -0
  180. package/dist/tracing/types/wood_trace_batch.types.d.cts +124 -0
  181. package/dist/tracing/types/wood_trace_batch.types.d.ts +124 -0
  182. package/dist/tracing/types/wood_trace_batch.types.js +1 -0
  183. package/dist/tracing/types/wood_trace_batch.types.js.map +1 -0
  184. package/dist/tracing/types/wood_trace_sink.types.cjs +17 -0
  185. package/dist/tracing/types/wood_trace_sink.types.cjs.map +1 -0
  186. package/dist/tracing/types/wood_trace_sink.types.d.cts +2 -0
  187. package/dist/tracing/types/wood_trace_sink.types.d.ts +2 -0
  188. package/dist/tracing/types/wood_trace_sink.types.js +1 -0
  189. package/dist/tracing/types/wood_trace_sink.types.js.map +1 -0
  190. package/dist/tracing/wood_trace_batch_contract.cjs +229 -0
  191. package/dist/tracing/wood_trace_batch_contract.cjs.map +1 -0
  192. package/dist/tracing/wood_trace_batch_contract.d.cts +23 -0
  193. package/dist/tracing/wood_trace_batch_contract.d.ts +23 -0
  194. package/dist/tracing/wood_trace_batch_contract.js +205 -0
  195. package/dist/tracing/wood_trace_batch_contract.js.map +1 -0
  196. package/dist/transport/build_transport_bridge.cjs +62 -0
  197. package/dist/transport/build_transport_bridge.cjs.map +1 -0
  198. package/dist/transport/build_transport_bridge.d.cts +29 -0
  199. package/dist/transport/build_transport_bridge.d.ts +29 -0
  200. package/dist/transport/build_transport_bridge.js +38 -0
  201. package/dist/transport/build_transport_bridge.js.map +1 -0
  202. package/dist/transport/in_process_router_transport.cjs +41 -0
  203. package/dist/transport/in_process_router_transport.cjs.map +1 -0
  204. package/dist/transport/in_process_router_transport.d.cts +31 -0
  205. package/dist/transport/in_process_router_transport.d.ts +31 -0
  206. package/dist/transport/in_process_router_transport.js +17 -0
  207. package/dist/transport/in_process_router_transport.js.map +1 -0
  208. package/dist/transport/index.cjs +42 -0
  209. package/dist/transport/index.cjs.map +1 -0
  210. package/dist/transport/index.d.cts +8 -0
  211. package/dist/transport/index.d.ts +8 -0
  212. package/dist/transport/index.js +19 -0
  213. package/dist/transport/index.js.map +1 -0
  214. package/dist/transport/loopback_remote_transport.cjs +270 -0
  215. package/dist/transport/loopback_remote_transport.cjs.map +1 -0
  216. package/dist/transport/loopback_remote_transport.d.cts +49 -0
  217. package/dist/transport/loopback_remote_transport.d.ts +49 -0
  218. package/dist/transport/loopback_remote_transport.js +237 -0
  219. package/dist/transport/loopback_remote_transport.js.map +1 -0
  220. package/dist/transport/loopback_serialization.cjs +116 -0
  221. package/dist/transport/loopback_serialization.cjs.map +1 -0
  222. package/dist/transport/loopback_serialization.d.cts +28 -0
  223. package/dist/transport/loopback_serialization.d.ts +28 -0
  224. package/dist/transport/loopback_serialization.js +91 -0
  225. package/dist/transport/loopback_serialization.js.map +1 -0
  226. package/dist/transport/transport_contract_suite.cjs +110 -0
  227. package/dist/transport/transport_contract_suite.cjs.map +1 -0
  228. package/dist/transport/transport_contract_suite.d.cts +23 -0
  229. package/dist/transport/transport_contract_suite.d.ts +23 -0
  230. package/dist/transport/transport_contract_suite.js +85 -0
  231. package/dist/transport/transport_contract_suite.js.map +1 -0
  232. package/dist/transport/wood_rpc_transport.cjs +17 -0
  233. package/dist/transport/wood_rpc_transport.cjs.map +1 -0
  234. package/dist/transport/wood_rpc_transport.d.cts +44 -0
  235. package/dist/transport/wood_rpc_transport.d.ts +44 -0
  236. package/dist/transport/wood_rpc_transport.js +1 -0
  237. package/dist/transport/wood_rpc_transport.js.map +1 -0
  238. package/dist/types/config.cjs.map +1 -1
  239. package/dist/types/config.d.cts +12 -0
  240. package/dist/types/config.d.ts +12 -0
  241. package/dist/window/window_manager.cjs +3 -0
  242. package/dist/window/window_manager.cjs.map +1 -1
  243. package/dist/window/window_manager.js +3 -0
  244. package/dist/window/window_manager.js.map +1 -1
  245. package/docs/browser-testing.md +139 -1
  246. package/package.json +22 -3
  247. package/src/components/NavigationShell.browser.test.ts +236 -0
  248. package/src/components/NavigationShell.svelte +162 -49
@@ -4,6 +4,7 @@ import {
4
4
  BrowserWindow,
5
5
  ipcMain,
6
6
  MessageChannelMain,
7
+ Menu,
7
8
  protocol,
8
9
  net,
9
10
  shell,
@@ -20,6 +21,40 @@ import {
20
21
  } from "../runtime/renderer_log_dispatcher.js";
21
22
  import { Ipc, getIpc } from "../ipc/index.js";
22
23
  import { setIpcPushSender, clearIpcPushSender } from "../ipc/shared.js";
24
+ import {
25
+ drainRendererTracesBeforeQuit,
26
+ RendererTraceDrainCoordinator
27
+ } from "./renderer_trace_drain_coordinator.js";
28
+ class BootstrapActivationGate {
29
+ constructor(hasWindow, createMainWindow) {
30
+ this.hasWindow = hasWindow;
31
+ this.createMainWindow = createMainWindow;
32
+ this.currentState = "pending";
33
+ }
34
+ get state() {
35
+ return this.currentState;
36
+ }
37
+ activate() {
38
+ if (this.currentState === "ready" && !this.hasWindow()) {
39
+ this.createMainWindow();
40
+ }
41
+ }
42
+ ready() {
43
+ if (this.currentState !== "pending") {
44
+ return;
45
+ }
46
+ this.createMainWindow();
47
+ this.currentState = "ready";
48
+ }
49
+ failed() {
50
+ if (this.currentState === "pending") {
51
+ this.currentState = "failed";
52
+ }
53
+ }
54
+ }
55
+ function shouldDeferLastWindowCloseForCleanup(trackedWindowCount, cleanupComplete) {
56
+ return trackedWindowCount === 1 && !cleanupComplete;
57
+ }
23
58
  const DEFAULT_WOOD_REMOTE_DEBUGGING_PORT = 9333;
24
59
  function resolveWoodDebugMetadataConfiguration(env = process.env) {
25
60
  const isDevelopment = env.WOOD_MODE === "development";
@@ -60,11 +95,15 @@ function startWoodMain(options) {
60
95
  const debugConfig = resolveWoodDebugMetadataConfiguration();
61
96
  const windowState = new WindowStateStore();
62
97
  const windows = /* @__PURE__ */ new Map();
98
+ const rendererHealth = /* @__PURE__ */ new Map();
63
99
  let mainWindow = null;
64
100
  let beforeQuitCleanupComplete = false;
65
101
  let beforeQuitCleanupPromise = null;
66
102
  let runtimeContext = null;
67
103
  let mainBootCleanup;
104
+ let nativeQuitFallbackInstalled = false;
105
+ const rendererTraceDrain = new RendererTraceDrainCoordinator(ipcMain);
106
+ rendererTraceDrain.start();
68
107
  for (const stream of [process.stdout, process.stderr]) {
69
108
  stream.on("error", (error) => {
70
109
  if (error.code === "EPIPE") {
@@ -89,11 +128,12 @@ function startWoodMain(options) {
89
128
  protocol,
90
129
  net,
91
130
  shell,
92
- screen
131
+ screen,
132
+ Menu
93
133
  },
94
134
  logger
95
135
  };
96
- void (async () => {
136
+ const initializeRuntime = async () => {
97
137
  const appBootResult = options.appBoot ? await options.appBoot(bootContext) : void 0;
98
138
  const bootOverrides = appBootResult ?? {};
99
139
  const scope = bootOverrides.scope ?? options.scope ?? process.cwd();
@@ -140,6 +180,129 @@ function startWoodMain(options) {
140
180
  }
141
181
  return null;
142
182
  };
183
+ const getRendererHealthState = (windowId) => {
184
+ return rendererHealth.get(windowId) ?? null;
185
+ };
186
+ const notifyRendererHealthChanged = (event) => {
187
+ if (!options.onRendererHealthChanged) {
188
+ return;
189
+ }
190
+ if (!runtimeContext) {
191
+ runtimeLogger.warn("renderer health hook skipped before runtime context", {
192
+ windowId: event.windowId,
193
+ state: event.state
194
+ });
195
+ return;
196
+ }
197
+ Promise.resolve(options.onRendererHealthChanged(event, runtimeContext)).catch((error) => {
198
+ runtimeLogger.warn("renderer health hook failed", {
199
+ windowId: event.windowId,
200
+ state: event.state,
201
+ error: toErrorMessage(error)
202
+ });
203
+ });
204
+ };
205
+ const setRendererHealth = (windowId, state, details = {}) => {
206
+ const previousState = rendererHealth.get(windowId) ?? null;
207
+ rendererHealth.set(windowId, state);
208
+ notifyRendererHealthChanged({
209
+ windowId,
210
+ state,
211
+ previousState,
212
+ ...details
213
+ });
214
+ };
215
+ const attachRendererHealth = (windowId, win) => {
216
+ const healthLogger = getLogger("wood:main").named(`renderer-health:${windowId}`);
217
+ const webContentsId = win.webContents.id;
218
+ setRendererHealth(windowId, "loading", { webContentsId });
219
+ win.webContents.on("did-start-loading", () => {
220
+ setRendererHealth(windowId, "loading", { webContentsId });
221
+ healthLogger.info("renderer loading", { windowId, webContentsId });
222
+ });
223
+ win.webContents.on("did-finish-load", () => {
224
+ setRendererHealth(windowId, "ready", { webContentsId });
225
+ healthLogger.info("renderer ready", { windowId, webContentsId });
226
+ });
227
+ win.webContents.on("unresponsive", () => {
228
+ setRendererHealth(windowId, "unresponsive", { webContentsId });
229
+ healthLogger.error("renderer unresponsive", { windowId, webContentsId });
230
+ });
231
+ win.webContents.on("responsive", () => {
232
+ setRendererHealth(windowId, "ready", { webContentsId });
233
+ healthLogger.info("renderer responsive", { windowId, webContentsId });
234
+ });
235
+ win.webContents.on("render-process-gone", (_event, details) => {
236
+ setRendererHealth(windowId, "gone", {
237
+ webContentsId,
238
+ reason: details.reason,
239
+ exitCode: details.exitCode
240
+ });
241
+ healthLogger.error("renderer process gone", {
242
+ windowId,
243
+ webContentsId,
244
+ reason: details.reason,
245
+ exitCode: details.exitCode
246
+ });
247
+ });
248
+ win.webContents.on("destroyed", () => {
249
+ setRendererHealth(windowId, "destroyed");
250
+ healthLogger.warn("renderer destroyed", { windowId, webContentsId });
251
+ });
252
+ win.on("closed", () => {
253
+ setRendererHealth(windowId, "destroyed");
254
+ rendererHealth.delete(windowId);
255
+ });
256
+ };
257
+ const canSendToWindow = (win) => {
258
+ if (win.isDestroyed()) {
259
+ return false;
260
+ }
261
+ if (win.webContents.isDestroyed()) {
262
+ return false;
263
+ }
264
+ if (typeof win.webContents.isCrashed === "function" && win.webContents.isCrashed()) {
265
+ return false;
266
+ }
267
+ return true;
268
+ };
269
+ const safeSend = (windowId, win, channel, data) => {
270
+ if (!canSendToWindow(win)) {
271
+ runtimeLogger.warn("skipping send to unhealthy renderer", {
272
+ windowId,
273
+ channel,
274
+ rendererHealth: getRendererHealthState(windowId)
275
+ });
276
+ return false;
277
+ }
278
+ try {
279
+ win.webContents.send(channel, data);
280
+ return true;
281
+ } catch (error) {
282
+ runtimeLogger.warn("send to renderer failed", {
283
+ windowId,
284
+ channel,
285
+ rendererHealth: getRendererHealthState(windowId),
286
+ error: toErrorMessage(error)
287
+ });
288
+ return false;
289
+ }
290
+ };
291
+ const installNativeQuitFallback = () => {
292
+ if (nativeQuitFallbackInstalled) {
293
+ return;
294
+ }
295
+ Menu.setApplicationMenu(Menu.buildFromTemplate([
296
+ {
297
+ label: electronApp.name,
298
+ submenu: [
299
+ { role: "quit" }
300
+ ]
301
+ }
302
+ ]));
303
+ nativeQuitFallbackInstalled = true;
304
+ logger.info("native quit fallback installed");
305
+ };
143
306
  const waitForWindowReady = async (win) => {
144
307
  const webContents = win.webContents;
145
308
  const isLoading = typeof webContents.isLoadingMainFrame === "function" ? webContents.isLoadingMainFrame() : webContents.isLoading();
@@ -162,7 +325,8 @@ function startWoodMain(options) {
162
325
  title: win.getTitle(),
163
326
  webContentsId: win.webContents.id,
164
327
  url: win.webContents.getURL(),
165
- focused: win.isFocused()
328
+ focused: win.isFocused(),
329
+ rendererHealth: getRendererHealthState(windowId) ?? void 0
166
330
  });
167
331
  }
168
332
  return {
@@ -183,6 +347,9 @@ function startWoodMain(options) {
183
347
  const windowDef = resolveWindowDef(options.windowDefs, windowId);
184
348
  const defaultShow = windowDef.show ?? true;
185
349
  const parentWindow = windowDef.parent ? getWindow(windowDef.parent) : null;
350
+ if (windowDef.nativeQuitFallback === true) {
351
+ installNativeQuitFallback();
352
+ }
186
353
  windowState.setScreen(screen);
187
354
  const savedBounds = windowDef.rememberBounds ? windowState.getBounds(windowId) : null;
188
355
  const windowOptions = {
@@ -209,6 +376,12 @@ function startWoodMain(options) {
209
376
  backgroundMaterial: windowDef.backgroundMaterial
210
377
  } : {},
211
378
  ...typeof windowDef.acceptFirstMouse === "boolean" ? { acceptFirstMouse: windowDef.acceptFirstMouse } : {},
379
+ // Hidden title bar with native window controls: macOS draws the
380
+ // traffic lights (positionable), Windows draws min/max/close via the
381
+ // Window Controls Overlay (always top-right; styled via titleBarOverlay).
382
+ ...typeof windowDef.titleBarStyle === "string" ? { titleBarStyle: windowDef.titleBarStyle } : {},
383
+ ...process.platform === "darwin" && windowDef.trafficLightPosition ? { trafficLightPosition: windowDef.trafficLightPosition } : {},
384
+ ...process.platform === "win32" && windowDef.titleBarOverlay != null ? { titleBarOverlay: windowDef.titleBarOverlay } : {},
212
385
  show: savedBounds ? false : defaultShow,
213
386
  webPreferences: {
214
387
  preload: preloadPath,
@@ -218,6 +391,7 @@ function startWoodMain(options) {
218
391
  };
219
392
  const win = new BrowserWindow(windowOptions);
220
393
  windows.set(windowId, win);
394
+ attachRendererHealth(windowId, win);
221
395
  if (windowId === "main") {
222
396
  mainWindow = win;
223
397
  }
@@ -305,6 +479,7 @@ function startWoodMain(options) {
305
479
  }
306
480
  win.on("closed", () => {
307
481
  windows.delete(windowId);
482
+ rendererHealth.delete(windowId);
308
483
  if (windowId === "main") {
309
484
  mainWindow = null;
310
485
  }
@@ -312,13 +487,27 @@ function startWoodMain(options) {
312
487
  electronApp.quit();
313
488
  }
314
489
  });
490
+ if (options.onWindowCreated) {
491
+ Promise.resolve(options.onWindowCreated(windowId, win, runtimeContext)).catch((error) => {
492
+ runtimeLogger.warn("window created hook failed", {
493
+ windowId,
494
+ error: toErrorMessage(error)
495
+ });
496
+ });
497
+ }
315
498
  logger.info("window created", { windowId });
316
499
  return win;
317
500
  };
501
+ const bootstrapActivationGate = new BootstrapActivationGate(
502
+ () => BrowserWindow.getAllWindows().length > 0,
503
+ () => {
504
+ createWindow("main");
505
+ }
506
+ );
318
507
  const navigateWindow = async (windowId, route) => {
319
508
  const win = createWindow(windowId);
320
509
  await waitForWindowReady(win);
321
- win.webContents.send("__event:__navigate", { route });
510
+ safeSend(windowId, win, "__event:__navigate", { route });
322
511
  win.show();
323
512
  win.focus();
324
513
  };
@@ -326,8 +515,8 @@ function startWoodMain(options) {
326
515
  const eventChannel = channel.startsWith("__event:") ? channel : `__event:${channel}`;
327
516
  if (options2?.windowId) {
328
517
  const target = getWindow(options2.windowId);
329
- if (target && !target.isDestroyed()) {
330
- target.webContents.send(eventChannel, data);
518
+ if (target) {
519
+ safeSend(options2.windowId, target, eventChannel, data);
331
520
  }
332
521
  return;
333
522
  }
@@ -336,7 +525,7 @@ function startWoodMain(options) {
336
525
  windows.delete(windowId);
337
526
  continue;
338
527
  }
339
- win.webContents.send(eventChannel, data);
528
+ safeSend(windowId, win, eventChannel, data);
340
529
  }
341
530
  };
342
531
  setIpcPushSender(dispatchPushEvent);
@@ -346,7 +535,7 @@ function startWoodMain(options) {
346
535
  windows.delete(windowId);
347
536
  continue;
348
537
  }
349
- win.webContents.send(channel, data);
538
+ safeSend(windowId, win, channel, data);
350
539
  }
351
540
  };
352
541
  runtimeContext = {
@@ -357,11 +546,16 @@ function startWoodMain(options) {
357
546
  createWindow,
358
547
  navigateWindow,
359
548
  broadcast,
549
+ getRendererHealthState,
360
550
  setRendererLogCallback: (callback) => {
361
551
  getRendererLogDispatcher().setCallback(callback);
362
552
  },
363
553
  getRendererLogCallback: () => getRendererLogDispatcher().getCallback(),
364
- getDebugMetadata
554
+ getDebugMetadata,
555
+ // Late-bound after bootRuntime constructs the router (see bootstrap).
556
+ dispatch: async () => {
557
+ throw new Error("WoodMainRuntimeContext.dispatch called before the runtime router was constructed.");
558
+ }
365
559
  };
366
560
  const bootstrap = async () => {
367
561
  logger.info("bootstrap starting");
@@ -374,7 +568,7 @@ function startWoodMain(options) {
374
568
  mainBootCleanup = bootResult;
375
569
  }
376
570
  }
377
- await bootRuntime({
571
+ const { router: runtimeRouter } = await bootRuntime({
378
572
  container,
379
573
  ipcMain,
380
574
  registerRuntime: options.registerRuntime,
@@ -470,67 +664,98 @@ function startWoodMain(options) {
470
664
  };
471
665
  }
472
666
  });
473
- createWindow("main");
667
+ runtimeContext.dispatch = (channel, payload) => runtimeRouter.dispatch(channel, payload ?? {});
668
+ bootstrapActivationGate.ready();
474
669
  logger.info("bootstrap complete");
475
670
  };
476
- void electronApp.whenReady().then(bootstrap).catch((error) => {
671
+ const handleBootstrapFailure = (error) => {
672
+ bootstrapActivationGate.failed();
477
673
  logger.error("bootstrap failed", {
478
674
  message: toErrorMessage(error),
479
675
  stack: error instanceof Error ? error.stack : void 0
480
676
  });
481
- });
482
- const destroyWindowsForQuit = () => {
483
- for (const [id, win] of windows) {
484
- if (!win.isDestroyed()) {
485
- win.destroy();
486
- logger.debug("destroyed window on quit", { windowId: id });
487
- }
488
- }
489
- windows.clear();
490
677
  };
491
- const runBeforeQuitCleanup = async () => {
492
- clearIpcPushSender();
493
- if (mainBootCleanup) {
494
- await mainBootCleanup();
495
- }
496
- if (options.onBeforeQuit) {
497
- await options.onBeforeQuit(runtimeContext);
498
- }
499
- };
500
- electronApp.on("before-quit", (event) => {
501
- if (!runtimeContext) {
502
- return;
503
- }
504
- if (beforeQuitCleanupComplete) {
505
- destroyWindowsForQuit();
506
- return;
507
- }
508
- event.preventDefault();
509
- if (beforeQuitCleanupPromise) {
510
- return;
511
- }
512
- beforeQuitCleanupPromise = (async () => {
513
- try {
514
- await runBeforeQuitCleanup();
515
- } catch (error) {
516
- logger.warn("before-quit handler failed", { error: toErrorMessage(error) });
517
- } finally {
518
- beforeQuitCleanupComplete = true;
519
- beforeQuitCleanupPromise = null;
678
+ void electronApp.whenReady().then(bootstrap).catch(handleBootstrapFailure);
679
+ const registerApplicationLifecycle = () => {
680
+ electronApp.on(
681
+ "browser-window-created",
682
+ (_event, window) => {
683
+ window.on("close", (event) => {
684
+ const isTrackedWindow = [...windows.values()].includes(window);
685
+ if (!isTrackedWindow || !shouldDeferLastWindowCloseForCleanup(
686
+ windows.size,
687
+ beforeQuitCleanupComplete
688
+ )) {
689
+ return;
690
+ }
691
+ event.preventDefault();
692
+ if (!beforeQuitCleanupPromise) {
693
+ logger.debug("deferring last window close for renderer trace drain");
694
+ electronApp.quit();
695
+ }
696
+ });
520
697
  }
521
- destroyWindowsForQuit();
698
+ );
699
+ const destroyWindowsForQuit = () => {
700
+ for (const [id, win] of windows) {
701
+ if (!win.isDestroyed()) {
702
+ win.destroy();
703
+ logger.debug("destroyed window on quit", { windowId: id });
704
+ }
705
+ }
706
+ windows.clear();
707
+ };
708
+ const runBeforeQuitCleanup = async () => {
709
+ await drainRendererTracesBeforeQuit(
710
+ rendererTraceDrain,
711
+ windows.values(),
712
+ logger,
713
+ Date.now() + 1e3
714
+ );
715
+ clearIpcPushSender();
716
+ if (mainBootCleanup) {
717
+ await mainBootCleanup();
718
+ }
719
+ if (options.onBeforeQuit) {
720
+ await options.onBeforeQuit(runtimeContext);
721
+ }
722
+ };
723
+ electronApp.on("before-quit", (event) => {
724
+ if (!runtimeContext) {
725
+ return;
726
+ }
727
+ if (beforeQuitCleanupComplete) {
728
+ destroyWindowsForQuit();
729
+ return;
730
+ }
731
+ event.preventDefault();
732
+ if (beforeQuitCleanupPromise) {
733
+ return;
734
+ }
735
+ beforeQuitCleanupPromise = (async () => {
736
+ try {
737
+ await runBeforeQuitCleanup();
738
+ } catch (error) {
739
+ logger.warn("before-quit handler failed", { error: toErrorMessage(error) });
740
+ } finally {
741
+ beforeQuitCleanupComplete = true;
742
+ beforeQuitCleanupPromise = null;
743
+ }
744
+ destroyWindowsForQuit();
745
+ electronApp.quit();
746
+ })();
747
+ });
748
+ electronApp.on("window-all-closed", () => {
522
749
  electronApp.quit();
523
- })();
524
- });
525
- electronApp.on("window-all-closed", () => {
526
- electronApp.quit();
527
- });
528
- electronApp.on("activate", () => {
529
- if (BrowserWindow.getAllWindows().length === 0) {
530
- createWindow("main");
531
- }
532
- });
533
- })().catch((error) => {
750
+ });
751
+ electronApp.on("activate", () => {
752
+ bootstrapActivationGate.activate();
753
+ });
754
+ };
755
+ registerApplicationLifecycle();
756
+ };
757
+ void initializeRuntime().catch((error) => {
758
+ rendererTraceDrain.stop();
534
759
  logger.error("startup failed before ready", {
535
760
  message: toErrorMessage(error),
536
761
  stack: error instanceof Error ? error.stack : void 0
@@ -538,7 +763,9 @@ function startWoodMain(options) {
538
763
  });
539
764
  }
540
765
  export {
766
+ BootstrapActivationGate,
541
767
  resolveWoodDebugMetadataConfiguration,
768
+ shouldDeferLastWindowCloseForCleanup,
542
769
  startWoodMain
543
770
  };
544
771
  //# sourceMappingURL=index.js.map