@noego/wood 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +31 -0
  2. package/dist/client/index.cjs +18 -11
  3. package/dist/client/index.cjs.map +1 -1
  4. package/dist/client/index.d.cts +21 -1
  5. package/dist/client/index.d.ts +21 -1
  6. package/dist/client/index.js +17 -11
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/codegen/renderer_app_generator.cjs +2 -9
  9. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  10. package/dist/codegen/renderer_app_generator.js +2 -9
  11. package/dist/codegen/renderer_app_generator.js.map +1 -1
  12. package/dist/controller/cleanup.cjs +39 -0
  13. package/dist/controller/cleanup.cjs.map +1 -0
  14. package/dist/controller/cleanup.d.cts +4 -0
  15. package/dist/controller/cleanup.d.ts +4 -0
  16. package/dist/controller/cleanup.js +15 -0
  17. package/dist/controller/cleanup.js.map +1 -0
  18. package/dist/controller/controller_lifecycle.cjs +26 -2
  19. package/dist/controller/controller_lifecycle.cjs.map +1 -1
  20. package/dist/controller/controller_lifecycle.d.cts +3 -1
  21. package/dist/controller/controller_lifecycle.d.ts +3 -1
  22. package/dist/controller/controller_lifecycle.js +24 -1
  23. package/dist/controller/controller_lifecycle.js.map +1 -1
  24. package/dist/frontend/frontend_lifecycle.cjs +5 -7
  25. package/dist/frontend/frontend_lifecycle.cjs.map +1 -1
  26. package/dist/frontend/frontend_lifecycle.d.cts +1 -14
  27. package/dist/frontend/frontend_lifecycle.d.ts +1 -14
  28. package/dist/frontend/frontend_lifecycle.js +5 -7
  29. package/dist/frontend/frontend_lifecycle.js.map +1 -1
  30. package/dist/frontend/frontend_surface.cjs +130 -104
  31. package/dist/frontend/frontend_surface.cjs.map +1 -1
  32. package/dist/frontend/frontend_surface.js +131 -105
  33. package/dist/frontend/frontend_surface.js.map +1 -1
  34. package/dist/frontend/work_registry.cjs +29 -4
  35. package/dist/frontend/work_registry.cjs.map +1 -1
  36. package/dist/frontend/work_registry.d.cts +2 -0
  37. package/dist/frontend/work_registry.d.ts +2 -0
  38. package/dist/frontend/work_registry.js +29 -4
  39. package/dist/frontend/work_registry.js.map +1 -1
  40. package/dist/index.d.cts +2 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/ipc/index.cjs +6 -2
  43. package/dist/ipc/index.cjs.map +1 -1
  44. package/dist/ipc/index.d.cts +9 -0
  45. package/dist/ipc/index.d.ts +9 -0
  46. package/dist/ipc/index.js +6 -2
  47. package/dist/ipc/index.js.map +1 -1
  48. package/dist/main/index.cjs +53 -35
  49. package/dist/main/index.cjs.map +1 -1
  50. package/dist/main/index.d.cts +11 -2
  51. package/dist/main/index.d.ts +11 -2
  52. package/dist/main/index.js +56 -40
  53. package/dist/main/index.js.map +1 -1
  54. package/dist/main/startup_failure_handler.cjs +67 -0
  55. package/dist/main/startup_failure_handler.cjs.map +1 -0
  56. package/dist/main/startup_failure_handler.d.cts +16 -0
  57. package/dist/main/startup_failure_handler.d.ts +16 -0
  58. package/dist/main/startup_failure_handler.js +43 -0
  59. package/dist/main/startup_failure_handler.js.map +1 -0
  60. package/dist/router/index.d.cts +3 -0
  61. package/dist/router/index.d.ts +3 -0
  62. package/dist/router/wood_router.cjs +204 -123
  63. package/dist/router/wood_router.cjs.map +1 -1
  64. package/dist/router/wood_router.d.cts +20 -0
  65. package/dist/router/wood_router.d.ts +20 -0
  66. package/dist/router/wood_router.js +206 -125
  67. package/dist/router/wood_router.js.map +1 -1
  68. package/dist/runtime/index.cjs +13 -0
  69. package/dist/runtime/index.cjs.map +1 -1
  70. package/dist/runtime/index.d.cts +8 -4
  71. package/dist/runtime/index.d.ts +8 -4
  72. package/dist/runtime/index.js +8 -0
  73. package/dist/runtime/index.js.map +1 -1
  74. package/dist/runtime/renderer_config_extension.cjs +1 -2
  75. package/dist/runtime/renderer_config_extension.cjs.map +1 -1
  76. package/dist/runtime/renderer_config_extension.d.cts +1 -1
  77. package/dist/runtime/renderer_config_extension.d.ts +1 -1
  78. package/dist/runtime/renderer_config_extension.js +1 -2
  79. package/dist/runtime/renderer_config_extension.js.map +1 -1
  80. package/dist/runtime/renderer_log_dispatcher.cjs +2 -0
  81. package/dist/runtime/renderer_log_dispatcher.cjs.map +1 -1
  82. package/dist/runtime/renderer_log_dispatcher.d.cts +1 -1
  83. package/dist/runtime/renderer_log_dispatcher.d.ts +1 -1
  84. package/dist/runtime/renderer_log_dispatcher.js +1 -0
  85. package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
  86. package/dist/runtime/runtime.cjs +107 -57
  87. package/dist/runtime/runtime.cjs.map +1 -1
  88. package/dist/runtime/runtime.d.cts +12 -0
  89. package/dist/runtime/runtime.d.ts +12 -0
  90. package/dist/runtime/runtime.js +110 -59
  91. package/dist/runtime/runtime.js.map +1 -1
  92. package/dist/runtime/runtime_module.cjs +58 -0
  93. package/dist/runtime/runtime_module.cjs.map +1 -0
  94. package/dist/runtime/runtime_module.d.cts +22 -0
  95. package/dist/runtime/runtime_module.d.ts +22 -0
  96. package/dist/runtime/runtime_module.js +30 -0
  97. package/dist/runtime/runtime_module.js.map +1 -0
  98. package/dist/testing/create_controller_harness.d.cts +3 -0
  99. package/dist/testing/create_controller_harness.d.ts +3 -0
  100. package/dist/testing/create_router_bridge.d.cts +3 -0
  101. package/dist/testing/create_router_bridge.d.ts +3 -0
  102. package/dist/testing/create_test_router.d.cts +3 -0
  103. package/dist/testing/create_test_router.d.ts +3 -0
  104. package/dist/testing/index.cjs +3 -0
  105. package/dist/testing/index.cjs.map +1 -1
  106. package/dist/testing/index.d.cts +6 -1
  107. package/dist/testing/index.d.ts +6 -1
  108. package/dist/testing/index.js +2 -0
  109. package/dist/testing/index.js.map +1 -1
  110. package/dist/testing/registration_only_root.cjs +168 -0
  111. package/dist/testing/registration_only_root.cjs.map +1 -0
  112. package/dist/testing/registration_only_root.d.cts +46 -0
  113. package/dist/testing/registration_only_root.d.ts +46 -0
  114. package/dist/testing/registration_only_root.js +141 -0
  115. package/dist/testing/registration_only_root.js.map +1 -0
  116. package/dist/testing/test_wood_config.cjs +382 -70
  117. package/dist/testing/test_wood_config.cjs.map +1 -1
  118. package/dist/testing/test_wood_config.d.cts +25 -1
  119. package/dist/testing/test_wood_config.d.ts +25 -1
  120. package/dist/testing/test_wood_config.js +388 -71
  121. package/dist/testing/test_wood_config.js.map +1 -1
  122. package/dist/testing/wood_dom_animation.cjs +213 -0
  123. package/dist/testing/wood_dom_animation.cjs.map +1 -0
  124. package/dist/testing/wood_dom_animation.d.cts +89 -0
  125. package/dist/testing/wood_dom_animation.d.ts +89 -0
  126. package/dist/testing/wood_dom_animation.js +185 -0
  127. package/dist/testing/wood_dom_animation.js.map +1 -0
  128. package/dist/testing/wood_env_renderer.cjs +81 -32
  129. package/dist/testing/wood_env_renderer.cjs.map +1 -1
  130. package/dist/testing/wood_env_renderer.d.cts +32 -1
  131. package/dist/testing/wood_env_renderer.d.ts +32 -1
  132. package/dist/testing/wood_env_renderer.js +84 -32
  133. package/dist/testing/wood_env_renderer.js.map +1 -1
  134. package/dist/transport/in_process_router_transport.d.cts +3 -0
  135. package/dist/transport/in_process_router_transport.d.ts +3 -0
  136. package/dist/transport/index.d.cts +3 -0
  137. package/dist/transport/index.d.ts +3 -0
  138. package/package.json +8 -6
  139. package/src/components/NavigationShell.svelte +12 -7
  140. package/src/controller/cleanup.ts +8 -0
  141. package/src/controller/controller_lifecycle.ts +24 -0
  142. package/src/navigation/layout_preservation.ts +45 -0
@@ -34,7 +34,8 @@ function describe(work) {
34
34
  return `[${work.kind}] ${work.label} (owner: ${work.owner})`;
35
35
  }
36
36
  class WorkRegistry {
37
- constructor() {
37
+ constructor(timeoutMs = 5e3) {
38
+ this.timeoutMs = timeoutMs;
38
39
  this.nextId = 1;
39
40
  /** Total registrations ever — the fixed-point signal for draining. */
40
41
  this.registrations = 0;
@@ -43,6 +44,7 @@ class WorkRegistry {
43
44
  /** How many failures a completed drain has already reported. */
44
45
  this.reportedFailures = 0;
45
46
  this.drainChain = Promise.resolve();
47
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new Error("Wood work timeoutMs must be finite and non-negative");
46
48
  }
47
49
  /**
48
50
  * Register finite owned work. Returns a promise with the work's own
@@ -57,7 +59,12 @@ class WorkRegistry {
57
59
  owner: options.owner ?? "application"
58
60
  };
59
61
  this.registrations += 1;
60
- const promise = typeof work === "function" ? work() : work;
62
+ let promise;
63
+ try {
64
+ promise = Promise.resolve(typeof work === "function" ? work() : work);
65
+ } catch (error) {
66
+ promise = Promise.reject(error);
67
+ }
61
68
  const observed = promise.then(
62
69
  () => void 0,
63
70
  (error) => {
@@ -88,14 +95,30 @@ class WorkRegistry {
88
95
  * (repeating owned work), listing pending and repeating labels.
89
96
  */
90
97
  drain(flushHooks = []) {
91
- const run = this.drainChain.then(() => this.drainOnce(flushHooks));
98
+ const run = this.drainChain.then(async () => {
99
+ let timer;
100
+ const cancelled = { value: false };
101
+ try {
102
+ await Promise.race([
103
+ this.drainOnce(flushHooks, cancelled),
104
+ new Promise((_, reject) => {
105
+ timer = setTimeout(() => {
106
+ cancelled.value = true;
107
+ reject(new FrontendWorkError("settle timed out; pending owners:\n " + this.pending().map(describe).join("\n ")));
108
+ }, this.timeoutMs);
109
+ })
110
+ ]);
111
+ } finally {
112
+ if (timer !== void 0) clearTimeout(timer);
113
+ }
114
+ });
92
115
  this.drainChain = run.then(
93
116
  () => void 0,
94
117
  () => void 0
95
118
  );
96
119
  return run;
97
120
  }
98
- async drainOnce(flushHooks) {
121
+ async drainOnce(flushHooks, cancelled) {
99
122
  const seenPerLabel = /* @__PURE__ */ new Map();
100
123
  for (let pass = 1; pass <= MAX_DRAIN_PASSES; pass++) {
101
124
  const registrationsBefore = this.registrations;
@@ -105,10 +128,12 @@ class WorkRegistry {
105
128
  }
106
129
  await Promise.all(snapshot.map((entry) => entry.observed));
107
130
  for (const hook of flushHooks) {
131
+ if (cancelled.value) return;
108
132
  await hook();
109
133
  }
110
134
  await Promise.resolve();
111
135
  await Promise.resolve();
136
+ if (cancelled.value) return;
112
137
  const stable = this.inFlight.size === 0 && this.registrations === registrationsBefore;
113
138
  if (stable) {
114
139
  this.throwNewFailures();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/frontend/work_registry.ts"],"sourcesContent":["/**\n * Owned-work registry for the frontend application slice (18A D-FAS-01).\n *\n * `act()` and `settle()` drain framework-owned finite work registered here\n * through production seams (transport invokes, event deliveries, navigation\n * transitions, tracked lifecycle tasks). The registry never patches global\n * async primitives and never guesses at event-loop emptiness: work exists\n * only because an owner registered it, and a drain is a fixed point —\n * repeated passes until one complete pass creates no new owned work.\n *\n * Floating promises and bare timers are explicitly outside the guarantee;\n * they are product lifecycle bugs, not framework blind spots to paper over.\n */\n\nexport type WorkKind =\n | 'invoke'\n | 'event-delivery'\n | 'navigation'\n | 'initialize'\n | 'tracked'\n | (string & {});\n\nexport interface WorkOptions {\n /** Human-readable diagnostic label, e.g. `invoke messages.send`. */\n label: string;\n kind?: WorkKind;\n /** Owning lifecycle identity, e.g. `page main.chat_dom` or `application`. */\n owner?: string;\n}\n\nexport interface WorkDescriptor {\n readonly id: number;\n readonly label: string;\n readonly kind: WorkKind;\n readonly owner: string;\n}\n\nexport interface WorkFailure extends WorkDescriptor {\n readonly error: unknown;\n}\n\n/** Safety bound on drain passes — a diagnostic limit, not the readiness mechanism. */\nconst MAX_DRAIN_PASSES = 100;\n\nexport class FrontendWorkError extends Error {\n constructor(message: string, readonly failures: readonly WorkFailure[] = []) {\n super(message);\n this.name = 'FrontendWorkError';\n }\n}\n\nfunction describe(work: WorkDescriptor): string {\n return `[${work.kind}] ${work.label} (owner: ${work.owner})`;\n}\n\nexport class WorkRegistry {\n private nextId = 1;\n /** Total registrations ever — the fixed-point signal for draining. */\n private registrations = 0;\n private readonly inFlight = new Map<number, { descriptor: WorkDescriptor; observed: Promise<void> }>();\n private readonly failures: WorkFailure[] = [];\n /** How many failures a completed drain has already reported. */\n private reportedFailures = 0;\n private drainChain: Promise<void> = Promise.resolve();\n\n /**\n * Register finite owned work. Returns a promise with the work's own\n * settlement semantics — callers may await it directly; the registry\n * additionally observes completion (success or failure) for draining.\n */\n track<T>(work: Promise<T> | (() => Promise<T>), options: WorkOptions): Promise<T> {\n const descriptor: WorkDescriptor = {\n id: this.nextId++,\n label: options.label,\n kind: options.kind ?? 'tracked',\n owner: options.owner ?? 'application',\n };\n\n this.registrations += 1;\n const promise = typeof work === 'function' ? work() : work;\n\n // Observe settlement without altering the caller's promise semantics.\n const observed = promise.then(\n () => undefined,\n (error) => {\n this.failures.push({ ...descriptor, error });\n },\n ).finally(() => {\n this.inFlight.delete(descriptor.id);\n });\n\n this.inFlight.set(descriptor.id, { descriptor, observed });\n return promise;\n }\n\n /** Descriptors of currently incomplete owned work. */\n pending(): WorkDescriptor[] {\n return [...this.inFlight.values()].map((entry) => entry.descriptor);\n }\n\n /** Every owned-work failure observed so far (accumulates across drains). */\n errors(): readonly WorkFailure[] {\n return [...this.failures];\n }\n\n /**\n * Fixed-point drain: await all pending owned work, run the adapter flush\n * hooks, repeat until one full pass observes no pending work and causes\n * no new registrations. Concurrent drains queue — each caller gets its\n * own complete pass over whatever work exists when its turn arrives.\n *\n * Rejects with `FrontendWorkError` naming the owners when owned work\n * failed since the previous drain, or when the pass bound is exceeded\n * (repeating owned work), listing pending and repeating labels.\n */\n drain(flushHooks: ReadonlyArray<() => void | Promise<void>> = []): Promise<void> {\n const run = this.drainChain.then(() => this.drainOnce(flushHooks));\n // Keep the chain alive regardless of this drain's outcome.\n this.drainChain = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n private async drainOnce(flushHooks: ReadonlyArray<() => void | Promise<void>>): Promise<void> {\n const seenPerLabel = new Map<string, number>();\n\n for (let pass = 1; pass <= MAX_DRAIN_PASSES; pass++) {\n const registrationsBefore = this.registrations;\n const snapshot = [...this.inFlight.values()];\n\n for (const { descriptor } of snapshot) {\n seenPerLabel.set(descriptor.label, (seenPerLabel.get(descriptor.label) ?? 0) + 1);\n }\n\n // Observed promises never reject (failures are recorded by track());\n // awaiting them cannot abort the pass mid-way.\n await Promise.all(snapshot.map((entry) => entry.observed));\n\n for (const hook of flushHooks) {\n await hook();\n }\n\n // Give completion continuations a microtask to register follow-on\n // owned work before the fixed point is tested.\n await Promise.resolve();\n await Promise.resolve();\n\n const stable = this.inFlight.size === 0 && this.registrations === registrationsBefore;\n if (stable) {\n this.throwNewFailures();\n return;\n }\n }\n\n const pendingLines = this.pending().map(describe);\n const repeating = [...seenPerLabel.entries()]\n .filter(([, count]) => count > 3)\n .map(([label, count]) => `${label} (seen in ${count} passes)`);\n\n throw new FrontendWorkError(\n `settle did not converge within ${MAX_DRAIN_PASSES} drain passes.\\n` +\n (pendingLines.length ? `Still pending:\\n ${pendingLines.join('\\n ')}\\n` : '') +\n (repeating.length ? `Repeating owned work:\\n ${repeating.join('\\n ')}\\n` : '') +\n 'Owned work keeps creating more owned work — this names the owners; it is not a timeout.',\n this.takeNewFailures(),\n );\n }\n\n private takeNewFailures(): WorkFailure[] {\n const fresh = this.failures.slice(this.reportedFailures);\n this.reportedFailures = this.failures.length;\n return fresh;\n }\n\n private throwNewFailures(): void {\n const fresh = this.takeNewFailures();\n if (!fresh.length) return;\n const lines = fresh.map(\n (failure) =>\n `${describe(failure)}: ${failure.error instanceof Error ? failure.error.message : String(failure.error)}`,\n );\n throw new FrontendWorkError(\n `owned frontend work failed:\\n ${lines.join('\\n ')}`,\n fresh,\n );\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/frontend/work_registry.ts"],"sourcesContent":["/**\n * Owned-work registry for the frontend application slice (18A D-FAS-01).\n *\n * `act()` and `settle()` drain framework-owned finite work registered here\n * through production seams (transport invokes, event deliveries, navigation\n * transitions, tracked lifecycle tasks). The registry never patches global\n * async primitives and never guesses at event-loop emptiness: work exists\n * only because an owner registered it, and a drain is a fixed point —\n * repeated passes until one complete pass creates no new owned work.\n *\n * Floating promises and bare timers are explicitly outside the guarantee;\n * they are product lifecycle bugs, not framework blind spots to paper over.\n */\n\nexport type WorkKind =\n | 'invoke'\n | 'event-delivery'\n | 'navigation'\n | 'initialize'\n | 'tracked'\n | (string & {});\n\nexport interface WorkOptions {\n /** Human-readable diagnostic label, e.g. `invoke messages.send`. */\n label: string;\n kind?: WorkKind;\n /** Owning lifecycle identity, e.g. `page main.chat_dom` or `application`. */\n owner?: string;\n}\n\nexport interface WorkDescriptor {\n readonly id: number;\n readonly label: string;\n readonly kind: WorkKind;\n readonly owner: string;\n}\n\nexport interface WorkFailure extends WorkDescriptor {\n readonly error: unknown;\n}\n\n/** Safety bound on drain passes — a diagnostic limit, not the readiness mechanism. */\nconst MAX_DRAIN_PASSES = 100;\n\nexport class FrontendWorkError extends Error {\n constructor(message: string, readonly failures: readonly WorkFailure[] = []) {\n super(message);\n this.name = 'FrontendWorkError';\n }\n}\n\nfunction describe(work: WorkDescriptor): string {\n return `[${work.kind}] ${work.label} (owner: ${work.owner})`;\n}\n\nexport class WorkRegistry {\n constructor(private readonly timeoutMs = 5000) {\n if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new Error('Wood work timeoutMs must be finite and non-negative');\n }\n private nextId = 1;\n /** Total registrations ever — the fixed-point signal for draining. */\n private registrations = 0;\n private readonly inFlight = new Map<number, { descriptor: WorkDescriptor; observed: Promise<void> }>();\n private readonly failures: WorkFailure[] = [];\n /** How many failures a completed drain has already reported. */\n private reportedFailures = 0;\n private drainChain: Promise<void> = Promise.resolve();\n\n /**\n * Register finite owned work. Returns a promise with the work's own\n * settlement semantics — callers may await it directly; the registry\n * additionally observes completion (success or failure) for draining.\n */\n track<T>(work: Promise<T> | (() => Promise<T>), options: WorkOptions): Promise<T> {\n const descriptor: WorkDescriptor = {\n id: this.nextId++,\n label: options.label,\n kind: options.kind ?? 'tracked',\n owner: options.owner ?? 'application',\n };\n\n this.registrations += 1;\n let promise: Promise<T>;\n try { promise = Promise.resolve(typeof work === 'function' ? work() : work); }\n catch (error) { promise = Promise.reject(error); }\n\n // Observe settlement without altering the caller's promise semantics.\n const observed = promise.then(\n () => undefined,\n (error) => {\n this.failures.push({ ...descriptor, error });\n },\n ).finally(() => {\n this.inFlight.delete(descriptor.id);\n });\n\n this.inFlight.set(descriptor.id, { descriptor, observed });\n return promise;\n }\n\n /** Descriptors of currently incomplete owned work. */\n pending(): WorkDescriptor[] {\n return [...this.inFlight.values()].map((entry) => entry.descriptor);\n }\n\n /** Every owned-work failure observed so far (accumulates across drains). */\n errors(): readonly WorkFailure[] {\n return [...this.failures];\n }\n\n /**\n * Fixed-point drain: await all pending owned work, run the adapter flush\n * hooks, repeat until one full pass observes no pending work and causes\n * no new registrations. Concurrent drains queue — each caller gets its\n * own complete pass over whatever work exists when its turn arrives.\n *\n * Rejects with `FrontendWorkError` naming the owners when owned work\n * failed since the previous drain, or when the pass bound is exceeded\n * (repeating owned work), listing pending and repeating labels.\n */\n drain(flushHooks: ReadonlyArray<() => void | Promise<void>> = []): Promise<void> {\n const run = this.drainChain.then(async () => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const cancelled = { value: false };\n try {\n await Promise.race([\n this.drainOnce(flushHooks, cancelled),\n new Promise<never>((_, reject) => { timer = setTimeout(() => {\n cancelled.value = true;\n reject(new FrontendWorkError('settle timed out; pending owners:\\n ' + this.pending().map(describe).join('\\n ')));\n }, this.timeoutMs); }),\n ]);\n } finally { if (timer !== undefined) clearTimeout(timer); }\n });\n // Keep the chain alive regardless of this drain's outcome.\n this.drainChain = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n private async drainOnce(flushHooks: ReadonlyArray<() => void | Promise<void>>, cancelled: { value: boolean }): Promise<void> {\n const seenPerLabel = new Map<string, number>();\n\n for (let pass = 1; pass <= MAX_DRAIN_PASSES; pass++) {\n const registrationsBefore = this.registrations;\n const snapshot = [...this.inFlight.values()];\n\n for (const { descriptor } of snapshot) {\n seenPerLabel.set(descriptor.label, (seenPerLabel.get(descriptor.label) ?? 0) + 1);\n }\n\n // Observed promises never reject (failures are recorded by track());\n // awaiting them cannot abort the pass mid-way.\n await Promise.all(snapshot.map((entry) => entry.observed));\n\n for (const hook of flushHooks) {\n if (cancelled.value) return;\n await hook();\n }\n\n // Give completion continuations a microtask to register follow-on\n // owned work before the fixed point is tested.\n await Promise.resolve();\n await Promise.resolve();\n\n if (cancelled.value) return;\n const stable = this.inFlight.size === 0 && this.registrations === registrationsBefore;\n if (stable) {\n this.throwNewFailures();\n return;\n }\n }\n\n const pendingLines = this.pending().map(describe);\n const repeating = [...seenPerLabel.entries()]\n .filter(([, count]) => count > 3)\n .map(([label, count]) => `${label} (seen in ${count} passes)`);\n\n throw new FrontendWorkError(\n `settle did not converge within ${MAX_DRAIN_PASSES} drain passes.\\n` +\n (pendingLines.length ? `Still pending:\\n ${pendingLines.join('\\n ')}\\n` : '') +\n (repeating.length ? `Repeating owned work:\\n ${repeating.join('\\n ')}\\n` : '') +\n 'Owned work keeps creating more owned work — this names the owners; it is not a timeout.',\n this.takeNewFailures(),\n );\n }\n\n private takeNewFailures(): WorkFailure[] {\n const fresh = this.failures.slice(this.reportedFailures);\n this.reportedFailures = this.failures.length;\n return fresh;\n }\n\n private throwNewFailures(): void {\n const fresh = this.takeNewFailures();\n if (!fresh.length) return;\n const lines = fresh.map(\n (failure) =>\n `${describe(failure)}: ${failure.error instanceof Error ? failure.error.message : String(failure.error)}`,\n );\n throw new FrontendWorkError(\n `owned frontend work failed:\\n ${lines.join('\\n ')}`,\n fresh,\n );\n }\n}\n"],"mappings":"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","names":[]}
@@ -33,6 +33,8 @@ declare class FrontendWorkError extends Error {
33
33
  constructor(message: string, failures?: readonly WorkFailure[]);
34
34
  }
35
35
  declare class WorkRegistry {
36
+ private readonly timeoutMs;
37
+ constructor(timeoutMs?: number);
36
38
  private nextId;
37
39
  /** Total registrations ever — the fixed-point signal for draining. */
38
40
  private registrations;
@@ -33,6 +33,8 @@ declare class FrontendWorkError extends Error {
33
33
  constructor(message: string, failures?: readonly WorkFailure[]);
34
34
  }
35
35
  declare class WorkRegistry {
36
+ private readonly timeoutMs;
37
+ constructor(timeoutMs?: number);
36
38
  private nextId;
37
39
  /** Total registrations ever — the fixed-point signal for draining. */
38
40
  private registrations;
@@ -10,7 +10,8 @@ function describe(work) {
10
10
  return `[${work.kind}] ${work.label} (owner: ${work.owner})`;
11
11
  }
12
12
  class WorkRegistry {
13
- constructor() {
13
+ constructor(timeoutMs = 5e3) {
14
+ this.timeoutMs = timeoutMs;
14
15
  this.nextId = 1;
15
16
  /** Total registrations ever — the fixed-point signal for draining. */
16
17
  this.registrations = 0;
@@ -19,6 +20,7 @@ class WorkRegistry {
19
20
  /** How many failures a completed drain has already reported. */
20
21
  this.reportedFailures = 0;
21
22
  this.drainChain = Promise.resolve();
23
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new Error("Wood work timeoutMs must be finite and non-negative");
22
24
  }
23
25
  /**
24
26
  * Register finite owned work. Returns a promise with the work's own
@@ -33,7 +35,12 @@ class WorkRegistry {
33
35
  owner: options.owner ?? "application"
34
36
  };
35
37
  this.registrations += 1;
36
- const promise = typeof work === "function" ? work() : work;
38
+ let promise;
39
+ try {
40
+ promise = Promise.resolve(typeof work === "function" ? work() : work);
41
+ } catch (error) {
42
+ promise = Promise.reject(error);
43
+ }
37
44
  const observed = promise.then(
38
45
  () => void 0,
39
46
  (error) => {
@@ -64,14 +71,30 @@ class WorkRegistry {
64
71
  * (repeating owned work), listing pending and repeating labels.
65
72
  */
66
73
  drain(flushHooks = []) {
67
- const run = this.drainChain.then(() => this.drainOnce(flushHooks));
74
+ const run = this.drainChain.then(async () => {
75
+ let timer;
76
+ const cancelled = { value: false };
77
+ try {
78
+ await Promise.race([
79
+ this.drainOnce(flushHooks, cancelled),
80
+ new Promise((_, reject) => {
81
+ timer = setTimeout(() => {
82
+ cancelled.value = true;
83
+ reject(new FrontendWorkError("settle timed out; pending owners:\n " + this.pending().map(describe).join("\n ")));
84
+ }, this.timeoutMs);
85
+ })
86
+ ]);
87
+ } finally {
88
+ if (timer !== void 0) clearTimeout(timer);
89
+ }
90
+ });
68
91
  this.drainChain = run.then(
69
92
  () => void 0,
70
93
  () => void 0
71
94
  );
72
95
  return run;
73
96
  }
74
- async drainOnce(flushHooks) {
97
+ async drainOnce(flushHooks, cancelled) {
75
98
  const seenPerLabel = /* @__PURE__ */ new Map();
76
99
  for (let pass = 1; pass <= MAX_DRAIN_PASSES; pass++) {
77
100
  const registrationsBefore = this.registrations;
@@ -81,10 +104,12 @@ class WorkRegistry {
81
104
  }
82
105
  await Promise.all(snapshot.map((entry) => entry.observed));
83
106
  for (const hook of flushHooks) {
107
+ if (cancelled.value) return;
84
108
  await hook();
85
109
  }
86
110
  await Promise.resolve();
87
111
  await Promise.resolve();
112
+ if (cancelled.value) return;
88
113
  const stable = this.inFlight.size === 0 && this.registrations === registrationsBefore;
89
114
  if (stable) {
90
115
  this.throwNewFailures();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/frontend/work_registry.ts"],"sourcesContent":["/**\n * Owned-work registry for the frontend application slice (18A D-FAS-01).\n *\n * `act()` and `settle()` drain framework-owned finite work registered here\n * through production seams (transport invokes, event deliveries, navigation\n * transitions, tracked lifecycle tasks). The registry never patches global\n * async primitives and never guesses at event-loop emptiness: work exists\n * only because an owner registered it, and a drain is a fixed point —\n * repeated passes until one complete pass creates no new owned work.\n *\n * Floating promises and bare timers are explicitly outside the guarantee;\n * they are product lifecycle bugs, not framework blind spots to paper over.\n */\n\nexport type WorkKind =\n | 'invoke'\n | 'event-delivery'\n | 'navigation'\n | 'initialize'\n | 'tracked'\n | (string & {});\n\nexport interface WorkOptions {\n /** Human-readable diagnostic label, e.g. `invoke messages.send`. */\n label: string;\n kind?: WorkKind;\n /** Owning lifecycle identity, e.g. `page main.chat_dom` or `application`. */\n owner?: string;\n}\n\nexport interface WorkDescriptor {\n readonly id: number;\n readonly label: string;\n readonly kind: WorkKind;\n readonly owner: string;\n}\n\nexport interface WorkFailure extends WorkDescriptor {\n readonly error: unknown;\n}\n\n/** Safety bound on drain passes — a diagnostic limit, not the readiness mechanism. */\nconst MAX_DRAIN_PASSES = 100;\n\nexport class FrontendWorkError extends Error {\n constructor(message: string, readonly failures: readonly WorkFailure[] = []) {\n super(message);\n this.name = 'FrontendWorkError';\n }\n}\n\nfunction describe(work: WorkDescriptor): string {\n return `[${work.kind}] ${work.label} (owner: ${work.owner})`;\n}\n\nexport class WorkRegistry {\n private nextId = 1;\n /** Total registrations ever — the fixed-point signal for draining. */\n private registrations = 0;\n private readonly inFlight = new Map<number, { descriptor: WorkDescriptor; observed: Promise<void> }>();\n private readonly failures: WorkFailure[] = [];\n /** How many failures a completed drain has already reported. */\n private reportedFailures = 0;\n private drainChain: Promise<void> = Promise.resolve();\n\n /**\n * Register finite owned work. Returns a promise with the work's own\n * settlement semantics — callers may await it directly; the registry\n * additionally observes completion (success or failure) for draining.\n */\n track<T>(work: Promise<T> | (() => Promise<T>), options: WorkOptions): Promise<T> {\n const descriptor: WorkDescriptor = {\n id: this.nextId++,\n label: options.label,\n kind: options.kind ?? 'tracked',\n owner: options.owner ?? 'application',\n };\n\n this.registrations += 1;\n const promise = typeof work === 'function' ? work() : work;\n\n // Observe settlement without altering the caller's promise semantics.\n const observed = promise.then(\n () => undefined,\n (error) => {\n this.failures.push({ ...descriptor, error });\n },\n ).finally(() => {\n this.inFlight.delete(descriptor.id);\n });\n\n this.inFlight.set(descriptor.id, { descriptor, observed });\n return promise;\n }\n\n /** Descriptors of currently incomplete owned work. */\n pending(): WorkDescriptor[] {\n return [...this.inFlight.values()].map((entry) => entry.descriptor);\n }\n\n /** Every owned-work failure observed so far (accumulates across drains). */\n errors(): readonly WorkFailure[] {\n return [...this.failures];\n }\n\n /**\n * Fixed-point drain: await all pending owned work, run the adapter flush\n * hooks, repeat until one full pass observes no pending work and causes\n * no new registrations. Concurrent drains queue — each caller gets its\n * own complete pass over whatever work exists when its turn arrives.\n *\n * Rejects with `FrontendWorkError` naming the owners when owned work\n * failed since the previous drain, or when the pass bound is exceeded\n * (repeating owned work), listing pending and repeating labels.\n */\n drain(flushHooks: ReadonlyArray<() => void | Promise<void>> = []): Promise<void> {\n const run = this.drainChain.then(() => this.drainOnce(flushHooks));\n // Keep the chain alive regardless of this drain's outcome.\n this.drainChain = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n private async drainOnce(flushHooks: ReadonlyArray<() => void | Promise<void>>): Promise<void> {\n const seenPerLabel = new Map<string, number>();\n\n for (let pass = 1; pass <= MAX_DRAIN_PASSES; pass++) {\n const registrationsBefore = this.registrations;\n const snapshot = [...this.inFlight.values()];\n\n for (const { descriptor } of snapshot) {\n seenPerLabel.set(descriptor.label, (seenPerLabel.get(descriptor.label) ?? 0) + 1);\n }\n\n // Observed promises never reject (failures are recorded by track());\n // awaiting them cannot abort the pass mid-way.\n await Promise.all(snapshot.map((entry) => entry.observed));\n\n for (const hook of flushHooks) {\n await hook();\n }\n\n // Give completion continuations a microtask to register follow-on\n // owned work before the fixed point is tested.\n await Promise.resolve();\n await Promise.resolve();\n\n const stable = this.inFlight.size === 0 && this.registrations === registrationsBefore;\n if (stable) {\n this.throwNewFailures();\n return;\n }\n }\n\n const pendingLines = this.pending().map(describe);\n const repeating = [...seenPerLabel.entries()]\n .filter(([, count]) => count > 3)\n .map(([label, count]) => `${label} (seen in ${count} passes)`);\n\n throw new FrontendWorkError(\n `settle did not converge within ${MAX_DRAIN_PASSES} drain passes.\\n` +\n (pendingLines.length ? `Still pending:\\n ${pendingLines.join('\\n ')}\\n` : '') +\n (repeating.length ? `Repeating owned work:\\n ${repeating.join('\\n ')}\\n` : '') +\n 'Owned work keeps creating more owned work — this names the owners; it is not a timeout.',\n this.takeNewFailures(),\n );\n }\n\n private takeNewFailures(): WorkFailure[] {\n const fresh = this.failures.slice(this.reportedFailures);\n this.reportedFailures = this.failures.length;\n return fresh;\n }\n\n private throwNewFailures(): void {\n const fresh = this.takeNewFailures();\n if (!fresh.length) return;\n const lines = fresh.map(\n (failure) =>\n `${describe(failure)}: ${failure.error instanceof Error ? failure.error.message : String(failure.error)}`,\n );\n throw new FrontendWorkError(\n `owned frontend work failed:\\n ${lines.join('\\n ')}`,\n fresh,\n );\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/frontend/work_registry.ts"],"sourcesContent":["/**\n * Owned-work registry for the frontend application slice (18A D-FAS-01).\n *\n * `act()` and `settle()` drain framework-owned finite work registered here\n * through production seams (transport invokes, event deliveries, navigation\n * transitions, tracked lifecycle tasks). The registry never patches global\n * async primitives and never guesses at event-loop emptiness: work exists\n * only because an owner registered it, and a drain is a fixed point —\n * repeated passes until one complete pass creates no new owned work.\n *\n * Floating promises and bare timers are explicitly outside the guarantee;\n * they are product lifecycle bugs, not framework blind spots to paper over.\n */\n\nexport type WorkKind =\n | 'invoke'\n | 'event-delivery'\n | 'navigation'\n | 'initialize'\n | 'tracked'\n | (string & {});\n\nexport interface WorkOptions {\n /** Human-readable diagnostic label, e.g. `invoke messages.send`. */\n label: string;\n kind?: WorkKind;\n /** Owning lifecycle identity, e.g. `page main.chat_dom` or `application`. */\n owner?: string;\n}\n\nexport interface WorkDescriptor {\n readonly id: number;\n readonly label: string;\n readonly kind: WorkKind;\n readonly owner: string;\n}\n\nexport interface WorkFailure extends WorkDescriptor {\n readonly error: unknown;\n}\n\n/** Safety bound on drain passes — a diagnostic limit, not the readiness mechanism. */\nconst MAX_DRAIN_PASSES = 100;\n\nexport class FrontendWorkError extends Error {\n constructor(message: string, readonly failures: readonly WorkFailure[] = []) {\n super(message);\n this.name = 'FrontendWorkError';\n }\n}\n\nfunction describe(work: WorkDescriptor): string {\n return `[${work.kind}] ${work.label} (owner: ${work.owner})`;\n}\n\nexport class WorkRegistry {\n constructor(private readonly timeoutMs = 5000) {\n if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new Error('Wood work timeoutMs must be finite and non-negative');\n }\n private nextId = 1;\n /** Total registrations ever — the fixed-point signal for draining. */\n private registrations = 0;\n private readonly inFlight = new Map<number, { descriptor: WorkDescriptor; observed: Promise<void> }>();\n private readonly failures: WorkFailure[] = [];\n /** How many failures a completed drain has already reported. */\n private reportedFailures = 0;\n private drainChain: Promise<void> = Promise.resolve();\n\n /**\n * Register finite owned work. Returns a promise with the work's own\n * settlement semantics — callers may await it directly; the registry\n * additionally observes completion (success or failure) for draining.\n */\n track<T>(work: Promise<T> | (() => Promise<T>), options: WorkOptions): Promise<T> {\n const descriptor: WorkDescriptor = {\n id: this.nextId++,\n label: options.label,\n kind: options.kind ?? 'tracked',\n owner: options.owner ?? 'application',\n };\n\n this.registrations += 1;\n let promise: Promise<T>;\n try { promise = Promise.resolve(typeof work === 'function' ? work() : work); }\n catch (error) { promise = Promise.reject(error); }\n\n // Observe settlement without altering the caller's promise semantics.\n const observed = promise.then(\n () => undefined,\n (error) => {\n this.failures.push({ ...descriptor, error });\n },\n ).finally(() => {\n this.inFlight.delete(descriptor.id);\n });\n\n this.inFlight.set(descriptor.id, { descriptor, observed });\n return promise;\n }\n\n /** Descriptors of currently incomplete owned work. */\n pending(): WorkDescriptor[] {\n return [...this.inFlight.values()].map((entry) => entry.descriptor);\n }\n\n /** Every owned-work failure observed so far (accumulates across drains). */\n errors(): readonly WorkFailure[] {\n return [...this.failures];\n }\n\n /**\n * Fixed-point drain: await all pending owned work, run the adapter flush\n * hooks, repeat until one full pass observes no pending work and causes\n * no new registrations. Concurrent drains queue — each caller gets its\n * own complete pass over whatever work exists when its turn arrives.\n *\n * Rejects with `FrontendWorkError` naming the owners when owned work\n * failed since the previous drain, or when the pass bound is exceeded\n * (repeating owned work), listing pending and repeating labels.\n */\n drain(flushHooks: ReadonlyArray<() => void | Promise<void>> = []): Promise<void> {\n const run = this.drainChain.then(async () => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const cancelled = { value: false };\n try {\n await Promise.race([\n this.drainOnce(flushHooks, cancelled),\n new Promise<never>((_, reject) => { timer = setTimeout(() => {\n cancelled.value = true;\n reject(new FrontendWorkError('settle timed out; pending owners:\\n ' + this.pending().map(describe).join('\\n ')));\n }, this.timeoutMs); }),\n ]);\n } finally { if (timer !== undefined) clearTimeout(timer); }\n });\n // Keep the chain alive regardless of this drain's outcome.\n this.drainChain = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n private async drainOnce(flushHooks: ReadonlyArray<() => void | Promise<void>>, cancelled: { value: boolean }): Promise<void> {\n const seenPerLabel = new Map<string, number>();\n\n for (let pass = 1; pass <= MAX_DRAIN_PASSES; pass++) {\n const registrationsBefore = this.registrations;\n const snapshot = [...this.inFlight.values()];\n\n for (const { descriptor } of snapshot) {\n seenPerLabel.set(descriptor.label, (seenPerLabel.get(descriptor.label) ?? 0) + 1);\n }\n\n // Observed promises never reject (failures are recorded by track());\n // awaiting them cannot abort the pass mid-way.\n await Promise.all(snapshot.map((entry) => entry.observed));\n\n for (const hook of flushHooks) {\n if (cancelled.value) return;\n await hook();\n }\n\n // Give completion continuations a microtask to register follow-on\n // owned work before the fixed point is tested.\n await Promise.resolve();\n await Promise.resolve();\n\n if (cancelled.value) return;\n const stable = this.inFlight.size === 0 && this.registrations === registrationsBefore;\n if (stable) {\n this.throwNewFailures();\n return;\n }\n }\n\n const pendingLines = this.pending().map(describe);\n const repeating = [...seenPerLabel.entries()]\n .filter(([, count]) => count > 3)\n .map(([label, count]) => `${label} (seen in ${count} passes)`);\n\n throw new FrontendWorkError(\n `settle did not converge within ${MAX_DRAIN_PASSES} drain passes.\\n` +\n (pendingLines.length ? `Still pending:\\n ${pendingLines.join('\\n ')}\\n` : '') +\n (repeating.length ? `Repeating owned work:\\n ${repeating.join('\\n ')}\\n` : '') +\n 'Owned work keeps creating more owned work — this names the owners; it is not a timeout.',\n this.takeNewFailures(),\n );\n }\n\n private takeNewFailures(): WorkFailure[] {\n const fresh = this.failures.slice(this.reportedFailures);\n this.reportedFailures = this.failures.length;\n return fresh;\n }\n\n private throwNewFailures(): void {\n const fresh = this.takeNewFailures();\n if (!fresh.length) return;\n const lines = fresh.map(\n (failure) =>\n `${describe(failure)}: ${failure.error instanceof Error ? failure.error.message : String(failure.error)}`,\n );\n throw new FrontendWorkError(\n `owned frontend work failed:\\n ${lines.join('\\n ')}`,\n fresh,\n );\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -42,6 +42,8 @@ export { Navigation, NavigationEntry, NavigationOptions, getNavigation, go, goba
42
42
  export { Ipc, getIpc } from './ipc/index.cjs';
43
43
  export { IpcPushOptions } from './ipc/shared.cjs';
44
44
  import '@noego/ioc';
45
+ import './runtime/renderer_log_dispatcher.cjs';
46
+ import './runtime/renderer_config_extension.cjs';
45
47
  import './runtime/app_env.cjs';
46
48
  import 'rxjs';
47
49
  import '@noego/trace';
package/dist/index.d.ts CHANGED
@@ -42,6 +42,8 @@ export { Navigation, NavigationEntry, NavigationOptions, getNavigation, go, goba
42
42
  export { Ipc, getIpc } from './ipc/index.js';
43
43
  export { IpcPushOptions } from './ipc/shared.js';
44
44
  import '@noego/ioc';
45
+ import './runtime/renderer_log_dispatcher.js';
46
+ import './runtime/renderer_config_extension.js';
45
47
  import './runtime/app_env.js';
46
48
  import 'rxjs';
47
49
  import '@noego/trace';
@@ -43,8 +43,12 @@ function normalizeCallbackPayload(args) {
43
43
  return args;
44
44
  }
45
45
  class Ipc {
46
+ /** Omit options only for the legacy process-global/preload adapter. */
47
+ constructor(owner) {
48
+ this.owner = owner;
49
+ }
46
50
  send(channel, payload, options) {
47
- const sender = (0, import_shared.getIpcPushSender)();
51
+ const sender = this.owner ? this.owner.send : (0, import_shared.getIpcPushSender)();
48
52
  if (!sender) {
49
53
  return false;
50
54
  }
@@ -55,7 +59,7 @@ class Ipc {
55
59
  return this.send(channel, payload, options);
56
60
  }
57
61
  on(channel, listener) {
58
- const bridge = resolveBridge();
62
+ const bridge = this.owner ? this.owner.bridge : resolveBridge();
59
63
  if (!bridge || typeof bridge.on !== "function") {
60
64
  return () => {
61
65
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/ipc/index.ts"],"sourcesContent":["import {\n getIpcPushSender,\n type IpcPushOptions,\n} from './shared.js';\n\nconst IPC_SINGLETON_SYMBOL = Symbol.for('@noego/wood.ipc.singleton');\n\ntype IpcListener = (payload: unknown) => void;\ntype IpcUnsubscribe = () => void;\n\ntype WoodBridgeLike = {\n on?: (channel: string, callback: (...args: unknown[]) => void) => IpcUnsubscribe;\n};\n\nconst INTERNAL_BRIDGE_GLOBAL_KEY = '__NOEGO_WOOD_BRIDGE__';\n\ntype GlobalWithIpc = typeof globalThis & {\n [IPC_SINGLETON_SYMBOL]?: Ipc;\n [INTERNAL_BRIDGE_GLOBAL_KEY]?: unknown;\n wood?: unknown;\n window?: {\n [key: string]: unknown;\n wood?: unknown;\n };\n};\n\nfunction getGlobal(): GlobalWithIpc {\n return globalThis as GlobalWithIpc;\n}\n\nfunction resolveBridge(): WoodBridgeLike | null {\n const globalObj = getGlobal();\n const bridge = globalObj[INTERNAL_BRIDGE_GLOBAL_KEY]\n ?? globalObj.window?.[INTERNAL_BRIDGE_GLOBAL_KEY]\n ?? globalObj.wood\n ?? globalObj.window?.wood;\n if (!bridge || typeof bridge !== 'object') {\n return null;\n }\n return bridge as WoodBridgeLike;\n}\n\nfunction normalizeCallbackPayload(args: unknown[]): unknown {\n if (args.length <= 1) {\n return args[0];\n }\n return args;\n}\n\nexport class Ipc {\n send(channel: string, payload: unknown, options?: IpcPushOptions): boolean {\n const sender = getIpcPushSender();\n if (!sender) {\n return false;\n }\n sender(channel, payload, options);\n return true;\n }\n\n emit(channel: string, payload: unknown, options?: IpcPushOptions): boolean {\n return this.send(channel, payload, options);\n }\n\n on(channel: string, listener: IpcListener): IpcUnsubscribe {\n const bridge = resolveBridge();\n if (!bridge || typeof bridge.on !== 'function') {\n return () => {};\n }\n return bridge.on(channel, (...args: unknown[]) => {\n listener(normalizeCallbackPayload(args));\n });\n }\n}\n\nexport function getIpc(): Ipc {\n const globalObj = getGlobal();\n if (!globalObj[IPC_SINGLETON_SYMBOL]) {\n globalObj[IPC_SINGLETON_SYMBOL] = new Ipc();\n }\n return globalObj[IPC_SINGLETON_SYMBOL] as Ipc;\n}\n\nexport type { IpcPushOptions };\n\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAGO;AAEP,MAAM,uBAAuB,uBAAO,IAAI,2BAA2B;AASnE,MAAM,6BAA6B;AAYnC,SAAS,YAA2B;AAClC,SAAO;AACT;AAEA,SAAS,gBAAuC;AAC9C,QAAM,YAAY,UAAU;AAC5B,QAAM,SAAS,UAAU,0BAA0B,KAC9C,UAAU,SAAS,0BAA0B,KAC7C,UAAU,QACV,UAAU,QAAQ;AACvB,MAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,yBAAyB,MAA0B;AAC1D,MAAI,KAAK,UAAU,GAAG;AACpB,WAAO,KAAK,CAAC;AAAA,EACf;AACA,SAAO;AACT;AAEO,MAAM,IAAI;AAAA,EACf,KAAK,SAAiB,SAAkB,SAAmC;AACzE,UAAM,aAAS,gCAAiB;AAChC,QAAI,CAAC,QAAQ;AACX,aAAO;AAAA,IACT;AACA,WAAO,SAAS,SAAS,OAAO;AAChC,WAAO;AAAA,EACT;AAAA,EAEA,KAAK,SAAiB,SAAkB,SAAmC;AACzE,WAAO,KAAK,KAAK,SAAS,SAAS,OAAO;AAAA,EAC5C;AAAA,EAEA,GAAG,SAAiB,UAAuC;AACzD,UAAM,SAAS,cAAc;AAC7B,QAAI,CAAC,UAAU,OAAO,OAAO,OAAO,YAAY;AAC9C,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AACA,WAAO,OAAO,GAAG,SAAS,IAAI,SAAoB;AAChD,eAAS,yBAAyB,IAAI,CAAC;AAAA,IACzC,CAAC;AAAA,EACH;AACF;AAEO,SAAS,SAAc;AAC5B,QAAM,YAAY,UAAU;AAC5B,MAAI,CAAC,UAAU,oBAAoB,GAAG;AACpC,cAAU,oBAAoB,IAAI,IAAI,IAAI;AAAA,EAC5C;AACA,SAAO,UAAU,oBAAoB;AACvC;","names":[]}
1
+ {"version":3,"sources":["../../src/ipc/index.ts"],"sourcesContent":["import {\n getIpcPushSender,\n type IpcPushOptions,\n} from './shared.js';\n\nconst IPC_SINGLETON_SYMBOL = Symbol.for('@noego/wood.ipc.singleton');\n\ntype IpcListener = (payload: unknown) => void;\ntype IpcUnsubscribe = () => void;\n\ntype WoodBridgeLike = {\n on?: (channel: string, callback: (...args: unknown[]) => void) => IpcUnsubscribe;\n};\n\nconst INTERNAL_BRIDGE_GLOBAL_KEY = '__NOEGO_WOOD_BRIDGE__';\n\ntype GlobalWithIpc = typeof globalThis & {\n [IPC_SINGLETON_SYMBOL]?: Ipc;\n [INTERNAL_BRIDGE_GLOBAL_KEY]?: unknown;\n wood?: unknown;\n window?: {\n [key: string]: unknown;\n wood?: unknown;\n };\n};\n\nfunction getGlobal(): GlobalWithIpc {\n return globalThis as GlobalWithIpc;\n}\n\nfunction resolveBridge(): WoodBridgeLike | null {\n const globalObj = getGlobal();\n const bridge = globalObj[INTERNAL_BRIDGE_GLOBAL_KEY]\n ?? globalObj.window?.[INTERNAL_BRIDGE_GLOBAL_KEY]\n ?? globalObj.wood\n ?? globalObj.window?.wood;\n if (!bridge || typeof bridge !== 'object') {\n return null;\n }\n return bridge as WoodBridgeLike;\n}\n\nfunction normalizeCallbackPayload(args: unknown[]): unknown {\n if (args.length <= 1) {\n return args[0];\n }\n return args;\n}\n\nexport class Ipc {\n /** Omit options only for the legacy process-global/preload adapter. */\n constructor(private readonly owner?: {\n send?: (channel: string, payload: unknown, options?: IpcPushOptions) => void;\n bridge?: WoodBridgeLike;\n }) {}\n send(channel: string, payload: unknown, options?: IpcPushOptions): boolean {\n const sender = this.owner ? this.owner.send : getIpcPushSender();\n if (!sender) {\n return false;\n }\n sender(channel, payload, options);\n return true;\n }\n\n emit(channel: string, payload: unknown, options?: IpcPushOptions): boolean {\n return this.send(channel, payload, options);\n }\n\n on(channel: string, listener: IpcListener): IpcUnsubscribe {\n const bridge = this.owner ? this.owner.bridge : resolveBridge();\n if (!bridge || typeof bridge.on !== 'function') {\n return () => {};\n }\n return bridge.on(channel, (...args: unknown[]) => {\n listener(normalizeCallbackPayload(args));\n });\n }\n}\n\nexport function getIpc(): Ipc {\n const globalObj = getGlobal();\n if (!globalObj[IPC_SINGLETON_SYMBOL]) {\n globalObj[IPC_SINGLETON_SYMBOL] = new Ipc();\n }\n return globalObj[IPC_SINGLETON_SYMBOL] as Ipc;\n}\n\nexport type { IpcPushOptions };\n\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAGO;AAEP,MAAM,uBAAuB,uBAAO,IAAI,2BAA2B;AASnE,MAAM,6BAA6B;AAYnC,SAAS,YAA2B;AAClC,SAAO;AACT;AAEA,SAAS,gBAAuC;AAC9C,QAAM,YAAY,UAAU;AAC5B,QAAM,SAAS,UAAU,0BAA0B,KAC9C,UAAU,SAAS,0BAA0B,KAC7C,UAAU,QACV,UAAU,QAAQ;AACvB,MAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,yBAAyB,MAA0B;AAC1D,MAAI,KAAK,UAAU,GAAG;AACpB,WAAO,KAAK,CAAC;AAAA,EACf;AACA,SAAO;AACT;AAEO,MAAM,IAAI;AAAA;AAAA,EAEf,YAA6B,OAG1B;AAH0B;AAAA,EAGzB;AAAA,EACJ,KAAK,SAAiB,SAAkB,SAAmC;AACzE,UAAM,SAAS,KAAK,QAAQ,KAAK,MAAM,WAAO,gCAAiB;AAC/D,QAAI,CAAC,QAAQ;AACX,aAAO;AAAA,IACT;AACA,WAAO,SAAS,SAAS,OAAO;AAChC,WAAO;AAAA,EACT;AAAA,EAEA,KAAK,SAAiB,SAAkB,SAAmC;AACzE,WAAO,KAAK,KAAK,SAAS,SAAS,OAAO;AAAA,EAC5C;AAAA,EAEA,GAAG,SAAiB,UAAuC;AACzD,UAAM,SAAS,KAAK,QAAQ,KAAK,MAAM,SAAS,cAAc;AAC9D,QAAI,CAAC,UAAU,OAAO,OAAO,OAAO,YAAY;AAC9C,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AACA,WAAO,OAAO,GAAG,SAAS,IAAI,SAAoB;AAChD,eAAS,yBAAyB,IAAI,CAAC;AAAA,IACzC,CAAC;AAAA,EACH;AACF;AAEO,SAAS,SAAc;AAC5B,QAAM,YAAY,UAAU;AAC5B,MAAI,CAAC,UAAU,oBAAoB,GAAG;AACpC,cAAU,oBAAoB,IAAI,IAAI,IAAI;AAAA,EAC5C;AACA,SAAO,UAAU,oBAAoB;AACvC;","names":[]}
@@ -2,7 +2,16 @@ import { IpcPushOptions } from './shared.cjs';
2
2
 
3
3
  type IpcListener = (payload: unknown) => void;
4
4
  type IpcUnsubscribe = () => void;
5
+ type WoodBridgeLike = {
6
+ on?: (channel: string, callback: (...args: unknown[]) => void) => IpcUnsubscribe;
7
+ };
5
8
  declare class Ipc {
9
+ private readonly owner?;
10
+ /** Omit options only for the legacy process-global/preload adapter. */
11
+ constructor(owner?: {
12
+ send?: (channel: string, payload: unknown, options?: IpcPushOptions) => void;
13
+ bridge?: WoodBridgeLike;
14
+ } | undefined);
6
15
  send(channel: string, payload: unknown, options?: IpcPushOptions): boolean;
7
16
  emit(channel: string, payload: unknown, options?: IpcPushOptions): boolean;
8
17
  on(channel: string, listener: IpcListener): IpcUnsubscribe;
@@ -2,7 +2,16 @@ import { IpcPushOptions } from './shared.js';
2
2
 
3
3
  type IpcListener = (payload: unknown) => void;
4
4
  type IpcUnsubscribe = () => void;
5
+ type WoodBridgeLike = {
6
+ on?: (channel: string, callback: (...args: unknown[]) => void) => IpcUnsubscribe;
7
+ };
5
8
  declare class Ipc {
9
+ private readonly owner?;
10
+ /** Omit options only for the legacy process-global/preload adapter. */
11
+ constructor(owner?: {
12
+ send?: (channel: string, payload: unknown, options?: IpcPushOptions) => void;
13
+ bridge?: WoodBridgeLike;
14
+ } | undefined);
6
15
  send(channel: string, payload: unknown, options?: IpcPushOptions): boolean;
7
16
  emit(channel: string, payload: unknown, options?: IpcPushOptions): boolean;
8
17
  on(channel: string, listener: IpcListener): IpcUnsubscribe;
package/dist/ipc/index.js CHANGED
@@ -21,8 +21,12 @@ function normalizeCallbackPayload(args) {
21
21
  return args;
22
22
  }
23
23
  class Ipc {
24
+ /** Omit options only for the legacy process-global/preload adapter. */
25
+ constructor(owner) {
26
+ this.owner = owner;
27
+ }
24
28
  send(channel, payload, options) {
25
- const sender = getIpcPushSender();
29
+ const sender = this.owner ? this.owner.send : getIpcPushSender();
26
30
  if (!sender) {
27
31
  return false;
28
32
  }
@@ -33,7 +37,7 @@ class Ipc {
33
37
  return this.send(channel, payload, options);
34
38
  }
35
39
  on(channel, listener) {
36
- const bridge = resolveBridge();
40
+ const bridge = this.owner ? this.owner.bridge : resolveBridge();
37
41
  if (!bridge || typeof bridge.on !== "function") {
38
42
  return () => {
39
43
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/ipc/index.ts"],"sourcesContent":["import {\n getIpcPushSender,\n type IpcPushOptions,\n} from './shared.js';\n\nconst IPC_SINGLETON_SYMBOL = Symbol.for('@noego/wood.ipc.singleton');\n\ntype IpcListener = (payload: unknown) => void;\ntype IpcUnsubscribe = () => void;\n\ntype WoodBridgeLike = {\n on?: (channel: string, callback: (...args: unknown[]) => void) => IpcUnsubscribe;\n};\n\nconst INTERNAL_BRIDGE_GLOBAL_KEY = '__NOEGO_WOOD_BRIDGE__';\n\ntype GlobalWithIpc = typeof globalThis & {\n [IPC_SINGLETON_SYMBOL]?: Ipc;\n [INTERNAL_BRIDGE_GLOBAL_KEY]?: unknown;\n wood?: unknown;\n window?: {\n [key: string]: unknown;\n wood?: unknown;\n };\n};\n\nfunction getGlobal(): GlobalWithIpc {\n return globalThis as GlobalWithIpc;\n}\n\nfunction resolveBridge(): WoodBridgeLike | null {\n const globalObj = getGlobal();\n const bridge = globalObj[INTERNAL_BRIDGE_GLOBAL_KEY]\n ?? globalObj.window?.[INTERNAL_BRIDGE_GLOBAL_KEY]\n ?? globalObj.wood\n ?? globalObj.window?.wood;\n if (!bridge || typeof bridge !== 'object') {\n return null;\n }\n return bridge as WoodBridgeLike;\n}\n\nfunction normalizeCallbackPayload(args: unknown[]): unknown {\n if (args.length <= 1) {\n return args[0];\n }\n return args;\n}\n\nexport class Ipc {\n send(channel: string, payload: unknown, options?: IpcPushOptions): boolean {\n const sender = getIpcPushSender();\n if (!sender) {\n return false;\n }\n sender(channel, payload, options);\n return true;\n }\n\n emit(channel: string, payload: unknown, options?: IpcPushOptions): boolean {\n return this.send(channel, payload, options);\n }\n\n on(channel: string, listener: IpcListener): IpcUnsubscribe {\n const bridge = resolveBridge();\n if (!bridge || typeof bridge.on !== 'function') {\n return () => {};\n }\n return bridge.on(channel, (...args: unknown[]) => {\n listener(normalizeCallbackPayload(args));\n });\n }\n}\n\nexport function getIpc(): Ipc {\n const globalObj = getGlobal();\n if (!globalObj[IPC_SINGLETON_SYMBOL]) {\n globalObj[IPC_SINGLETON_SYMBOL] = new Ipc();\n }\n return globalObj[IPC_SINGLETON_SYMBOL] as Ipc;\n}\n\nexport type { IpcPushOptions };\n\n"],"mappings":"AAAA;AAAA,EACE;AAAA,OAEK;AAEP,MAAM,uBAAuB,uBAAO,IAAI,2BAA2B;AASnE,MAAM,6BAA6B;AAYnC,SAAS,YAA2B;AAClC,SAAO;AACT;AAEA,SAAS,gBAAuC;AAC9C,QAAM,YAAY,UAAU;AAC5B,QAAM,SAAS,UAAU,0BAA0B,KAC9C,UAAU,SAAS,0BAA0B,KAC7C,UAAU,QACV,UAAU,QAAQ;AACvB,MAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,yBAAyB,MAA0B;AAC1D,MAAI,KAAK,UAAU,GAAG;AACpB,WAAO,KAAK,CAAC;AAAA,EACf;AACA,SAAO;AACT;AAEO,MAAM,IAAI;AAAA,EACf,KAAK,SAAiB,SAAkB,SAAmC;AACzE,UAAM,SAAS,iBAAiB;AAChC,QAAI,CAAC,QAAQ;AACX,aAAO;AAAA,IACT;AACA,WAAO,SAAS,SAAS,OAAO;AAChC,WAAO;AAAA,EACT;AAAA,EAEA,KAAK,SAAiB,SAAkB,SAAmC;AACzE,WAAO,KAAK,KAAK,SAAS,SAAS,OAAO;AAAA,EAC5C;AAAA,EAEA,GAAG,SAAiB,UAAuC;AACzD,UAAM,SAAS,cAAc;AAC7B,QAAI,CAAC,UAAU,OAAO,OAAO,OAAO,YAAY;AAC9C,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AACA,WAAO,OAAO,GAAG,SAAS,IAAI,SAAoB;AAChD,eAAS,yBAAyB,IAAI,CAAC;AAAA,IACzC,CAAC;AAAA,EACH;AACF;AAEO,SAAS,SAAc;AAC5B,QAAM,YAAY,UAAU;AAC5B,MAAI,CAAC,UAAU,oBAAoB,GAAG;AACpC,cAAU,oBAAoB,IAAI,IAAI,IAAI;AAAA,EAC5C;AACA,SAAO,UAAU,oBAAoB;AACvC;","names":[]}
1
+ {"version":3,"sources":["../../src/ipc/index.ts"],"sourcesContent":["import {\n getIpcPushSender,\n type IpcPushOptions,\n} from './shared.js';\n\nconst IPC_SINGLETON_SYMBOL = Symbol.for('@noego/wood.ipc.singleton');\n\ntype IpcListener = (payload: unknown) => void;\ntype IpcUnsubscribe = () => void;\n\ntype WoodBridgeLike = {\n on?: (channel: string, callback: (...args: unknown[]) => void) => IpcUnsubscribe;\n};\n\nconst INTERNAL_BRIDGE_GLOBAL_KEY = '__NOEGO_WOOD_BRIDGE__';\n\ntype GlobalWithIpc = typeof globalThis & {\n [IPC_SINGLETON_SYMBOL]?: Ipc;\n [INTERNAL_BRIDGE_GLOBAL_KEY]?: unknown;\n wood?: unknown;\n window?: {\n [key: string]: unknown;\n wood?: unknown;\n };\n};\n\nfunction getGlobal(): GlobalWithIpc {\n return globalThis as GlobalWithIpc;\n}\n\nfunction resolveBridge(): WoodBridgeLike | null {\n const globalObj = getGlobal();\n const bridge = globalObj[INTERNAL_BRIDGE_GLOBAL_KEY]\n ?? globalObj.window?.[INTERNAL_BRIDGE_GLOBAL_KEY]\n ?? globalObj.wood\n ?? globalObj.window?.wood;\n if (!bridge || typeof bridge !== 'object') {\n return null;\n }\n return bridge as WoodBridgeLike;\n}\n\nfunction normalizeCallbackPayload(args: unknown[]): unknown {\n if (args.length <= 1) {\n return args[0];\n }\n return args;\n}\n\nexport class Ipc {\n /** Omit options only for the legacy process-global/preload adapter. */\n constructor(private readonly owner?: {\n send?: (channel: string, payload: unknown, options?: IpcPushOptions) => void;\n bridge?: WoodBridgeLike;\n }) {}\n send(channel: string, payload: unknown, options?: IpcPushOptions): boolean {\n const sender = this.owner ? this.owner.send : getIpcPushSender();\n if (!sender) {\n return false;\n }\n sender(channel, payload, options);\n return true;\n }\n\n emit(channel: string, payload: unknown, options?: IpcPushOptions): boolean {\n return this.send(channel, payload, options);\n }\n\n on(channel: string, listener: IpcListener): IpcUnsubscribe {\n const bridge = this.owner ? this.owner.bridge : resolveBridge();\n if (!bridge || typeof bridge.on !== 'function') {\n return () => {};\n }\n return bridge.on(channel, (...args: unknown[]) => {\n listener(normalizeCallbackPayload(args));\n });\n }\n}\n\nexport function getIpc(): Ipc {\n const globalObj = getGlobal();\n if (!globalObj[IPC_SINGLETON_SYMBOL]) {\n globalObj[IPC_SINGLETON_SYMBOL] = new Ipc();\n }\n return globalObj[IPC_SINGLETON_SYMBOL] as Ipc;\n}\n\nexport type { IpcPushOptions };\n\n"],"mappings":"AAAA;AAAA,EACE;AAAA,OAEK;AAEP,MAAM,uBAAuB,uBAAO,IAAI,2BAA2B;AASnE,MAAM,6BAA6B;AAYnC,SAAS,YAA2B;AAClC,SAAO;AACT;AAEA,SAAS,gBAAuC;AAC9C,QAAM,YAAY,UAAU;AAC5B,QAAM,SAAS,UAAU,0BAA0B,KAC9C,UAAU,SAAS,0BAA0B,KAC7C,UAAU,QACV,UAAU,QAAQ;AACvB,MAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,yBAAyB,MAA0B;AAC1D,MAAI,KAAK,UAAU,GAAG;AACpB,WAAO,KAAK,CAAC;AAAA,EACf;AACA,SAAO;AACT;AAEO,MAAM,IAAI;AAAA;AAAA,EAEf,YAA6B,OAG1B;AAH0B;AAAA,EAGzB;AAAA,EACJ,KAAK,SAAiB,SAAkB,SAAmC;AACzE,UAAM,SAAS,KAAK,QAAQ,KAAK,MAAM,OAAO,iBAAiB;AAC/D,QAAI,CAAC,QAAQ;AACX,aAAO;AAAA,IACT;AACA,WAAO,SAAS,SAAS,OAAO;AAChC,WAAO;AAAA,EACT;AAAA,EAEA,KAAK,SAAiB,SAAkB,SAAmC;AACzE,WAAO,KAAK,KAAK,SAAS,SAAS,OAAO;AAAA,EAC5C;AAAA,EAEA,GAAG,SAAiB,UAAuC;AACzD,UAAM,SAAS,KAAK,QAAQ,KAAK,MAAM,SAAS,cAAc;AAC9D,QAAI,CAAC,UAAU,OAAO,OAAO,OAAO,YAAY;AAC9C,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AACA,WAAO,OAAO,GAAG,SAAS,IAAI,SAAoB;AAChD,eAAS,yBAAyB,IAAI,CAAC;AAAA,IACzC,CAAC;AAAA,EACH;AACF;AAEO,SAAS,SAAc;AAC5B,QAAM,YAAY,UAAU;AAC5B,MAAI,CAAC,UAAU,oBAAoB,GAAG;AACpC,cAAU,oBAAoB,IAAI,IAAI,IAAI;AAAA,EAC5C;AACA,SAAO,UAAU,oBAAoB;AACvC;","names":[]}
@@ -37,17 +37,19 @@ __export(main_exports, {
37
37
  });
38
38
  module.exports = __toCommonJS(main_exports);
39
39
  var import_path = __toESM(require("path"), 1);
40
+ var import_startup_failure_handler = require("./startup_failure_handler.cjs");
40
41
  var import_electron = require("electron");
41
42
  var import_logger = require("@noego/logger");
42
43
  var import_window_state_store = require("../window/window_state_store.js");
43
44
  var import_get_container = require("../runtime/get_container.js");
45
+ var import_cleanup = require("../controller/cleanup.js");
46
+ var import_ioc = require("@noego/ioc");
44
47
  var import_runtime = require("../runtime/runtime.js");
45
48
  var import_tracing = require("../tracing/index.js");
46
49
  var import_application_root = require("../runtime/application_root.js");
47
50
  var import_renderer_log_dispatcher = require("../runtime/renderer_log_dispatcher.js");
48
51
  var import_renderer_log_batch_receiver = require("../runtime/renderer_log_batch_receiver.js");
49
52
  var import_renderer_log_batch_receiver2 = require("../runtime/renderer_log_batch_receiver.js");
50
- var import_renderer_config_extension = require("../runtime/renderer_config_extension.js");
51
53
  var import_renderer_config_extension2 = require("../runtime/renderer_config_extension.js");
52
54
  var import_ipc = require("../ipc/index.js");
53
55
  var import_shared = require("../ipc/shared.js");
@@ -119,6 +121,13 @@ function toErrorMessage(error) {
119
121
  function startWoodMain(options) {
120
122
  const logger = (0, import_logger.getLogger)("wood:main").named("bootstrap");
121
123
  const runtimeLogger = (0, import_logger.getLogger)("wood:main").named("runtime");
124
+ const startupFailure = new import_startup_failure_handler.StartupFailureHandler(
125
+ logger,
126
+ (message) => {
127
+ process.stderr.write(message);
128
+ },
129
+ (code) => import_electron.app.exit(code)
130
+ );
122
131
  const debugConfig = resolveWoodDebugMetadataConfiguration();
123
132
  const windowState = new import_window_state_store.WindowStateStore();
124
133
  const windows = /* @__PURE__ */ new Map();
@@ -128,6 +137,7 @@ function startWoodMain(options) {
128
137
  let beforeQuitCleanupPromise = null;
129
138
  let runtimeContext = null;
130
139
  let mainBootCleanup;
140
+ let runtimeCleanup;
131
141
  let nativeQuitFallbackInstalled = false;
132
142
  const rendererTraceDrain = new import_renderer_trace_drain_coordinator.RendererTraceDrainCoordinator(import_electron.ipcMain);
133
143
  rendererTraceDrain.start();
@@ -163,7 +173,6 @@ function startWoodMain(options) {
163
173
  const initializeRuntime = async () => {
164
174
  const appBootResult = options.appBoot ? await options.appBoot(bootContext) : void 0;
165
175
  const bootOverrides = appBootResult ?? {};
166
- const scope = bootOverrides.scope ?? options.scope ?? process.cwd();
167
176
  const configureContainer = bootOverrides.configureContainer ?? options.configureContainer;
168
177
  const privilegedSchemes = Array.isArray(bootOverrides.registerSchemesAsPrivileged) ? bootOverrides.registerSchemesAsPrivileged : [];
169
178
  if (privilegedSchemes.length > 0) {
@@ -176,13 +185,14 @@ function startWoodMain(options) {
176
185
  const outDir = options.outDir ?? "out";
177
186
  const preloadPath = import_path.default.join(import_electron.app.getAppPath(), outDir, "preload", "index.js");
178
187
  const rendererFile = import_path.default.join(import_electron.app.getAppPath(), outDir, "renderer", "index.html");
179
- const container = (0, import_get_container.getContainer)({
180
- scope,
188
+ const rendererLogDispatcher = new import_renderer_log_dispatcher.RendererLogDispatcher();
189
+ let rendererConfigExtension = null;
190
+ const container = (0, import_get_container.createWoodRoot)({
181
191
  ...configureContainer ? { configure: configureContainer } : {}
182
192
  });
183
193
  const registrationContainer = container;
184
194
  if (typeof registrationContainer.registerFunction === "function") {
185
- registrationContainer.registerFunction(import_ipc.Ipc, () => (0, import_ipc.getIpc)());
195
+ registrationContainer.registerFunction(import_ipc.Ipc, () => new import_ipc.Ipc({ send: dispatchPushEvent }));
186
196
  }
187
197
  const getWindow = (windowId) => {
188
198
  const existing = windows.get(windowId);
@@ -413,7 +423,11 @@ function startWoodMain(options) {
413
423
  webPreferences: {
414
424
  preload: preloadPath,
415
425
  contextIsolation: true,
416
- nodeIntegration: false
426
+ nodeIntegration: false,
427
+ // Packaged builds never expose DevTools: this disables the default
428
+ // menu's "Toggle Developer Tools" shortcut (Cmd/Ctrl+Alt+I, F12)
429
+ // and any openDevTools() call for this window.
430
+ devTools: !import_electron.app.isPackaged
417
431
  }
418
432
  };
419
433
  const win = new import_electron.BrowserWindow(windowOptions);
@@ -555,7 +569,7 @@ function startWoodMain(options) {
555
569
  safeSend(windowId, win, eventChannel, data);
556
570
  }
557
571
  };
558
- (0, import_shared.setIpcPushSender)(dispatchPushEvent);
572
+ if (options.legacyGlobals) (0, import_shared.setIpcPushSender)(dispatchPushEvent);
559
573
  const broadcast = (channel, data) => {
560
574
  for (const [windowId, win] of windows.entries()) {
561
575
  if (win.isDestroyed()) {
@@ -575,11 +589,13 @@ function startWoodMain(options) {
575
589
  broadcast,
576
590
  getRendererHealthState,
577
591
  setRendererLogCallback: (callback) => {
578
- (0, import_renderer_log_dispatcher.getRendererLogDispatcher)().setCallback(callback);
592
+ rendererLogDispatcher.setCallback(callback);
579
593
  },
580
- getRendererLogCallback: () => (0, import_renderer_log_dispatcher.getRendererLogDispatcher)().getCallback(),
594
+ getRendererLogCallback: () => rendererLogDispatcher.getCallback(),
581
595
  setRendererLogRecordFilter: import_renderer_log_batch_receiver.setRendererLogRecordFilter,
582
- setRendererConfigExtension: import_renderer_config_extension.setRendererConfigExtension,
596
+ setRendererConfigExtension: (extension) => {
597
+ rendererConfigExtension = extension;
598
+ },
583
599
  getDebugMetadata,
584
600
  // Late-bound after bootRuntime constructs the router (see bootstrap).
585
601
  dispatch: async () => {
@@ -597,10 +613,14 @@ function startWoodMain(options) {
597
613
  mainBootCleanup = bootResult;
598
614
  }
599
615
  }
600
- const { router: runtimeRouter } = await (0, import_runtime.runtime)({
616
+ const { router: runtimeRouter, dispose: closeRuntime } = await (0, import_runtime.runtime)({
601
617
  container,
602
618
  ipcMain: import_electron.ipcMain,
603
619
  registerRuntime: options.registerRuntime,
620
+ rendererTraceBatchReceiver: options.rendererTraceBatchReceiver,
621
+ legacyGlobals: options.legacyGlobals,
622
+ rendererLogDispatcher,
623
+ rendererConfigExtension: () => rendererConfigExtension?.() ?? {},
604
624
  controllersDir: resolveRuntimePath(options.controllersDir),
605
625
  middlewareDir: resolveRuntimePath(options.middlewareDir),
606
626
  ...options.traceMinLevel ? { traceMinLevel: options.traceMinLevel } : {},
@@ -694,14 +714,15 @@ function startWoodMain(options) {
694
714
  }
695
715
  });
696
716
  runtimeContext.dispatch = (channel, payload) => runtimeRouter.dispatch(channel, payload ?? {});
717
+ runtimeCleanup = closeRuntime;
697
718
  bootstrapActivationGate.ready();
698
719
  logger.info("bootstrap complete");
699
720
  };
700
- const handleBootstrapFailure = (error) => {
721
+ const handleBootstrapFailure = async (error) => {
701
722
  bootstrapActivationGate.failed();
702
- logger.error("bootstrap failed", {
703
- message: toErrorMessage(error),
704
- stack: error instanceof Error ? error.stack : void 0
723
+ await startupFailure.handle("bootstrap failed", error, async () => {
724
+ rendererTraceDrain.stop();
725
+ await (0, import_cleanup.cleanupAll)([() => runtimeCleanup?.(), () => mainBootCleanup?.(), () => container.dispose()], "Wood bootstrap rollback failed");
705
726
  });
706
727
  };
707
728
  void import_electron.app.whenReady().then(bootstrap).catch(handleBootstrapFailure);
@@ -735,19 +756,16 @@ function startWoodMain(options) {
735
756
  windows.clear();
736
757
  };
737
758
  const runBeforeQuitCleanup = async () => {
738
- await (0, import_renderer_trace_drain_coordinator.drainRendererTracesBeforeQuit)(
739
- rendererTraceDrain,
740
- windows.values(),
741
- logger,
742
- Date.now() + 1e3
743
- );
744
- (0, import_shared.clearIpcPushSender)();
745
- if (mainBootCleanup) {
746
- await mainBootCleanup();
747
- }
748
- if (options.onBeforeQuit) {
749
- await options.onBeforeQuit(runtimeContext);
750
- }
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");
751
769
  };
752
770
  import_electron.app.on("before-quit", (event) => {
753
771
  if (!runtimeContext) {
@@ -783,13 +801,13 @@ function startWoodMain(options) {
783
801
  };
784
802
  registerApplicationLifecycle();
785
803
  };
786
- void initializeRuntime().catch((error) => {
787
- rendererTraceDrain.stop();
788
- logger.error("startup failed before ready", {
789
- message: toErrorMessage(error),
790
- stack: error instanceof Error ? error.stack : void 0
791
- });
792
- });
804
+ void initializeRuntime().catch((error) => startupFailure.handle(
805
+ "startup failed before ready",
806
+ error,
807
+ async () => {
808
+ rendererTraceDrain.stop();
809
+ }
810
+ ));
793
811
  }
794
812
  // Annotate the CommonJS export names for ESM import in node:
795
813
  0 && (module.exports = {