@noego/wood 0.5.1 → 0.6.1

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 (151) hide show
  1. package/README.md +31 -0
  2. package/dist/client/index.cjs +30 -11
  3. package/dist/client/index.cjs.map +1 -1
  4. package/dist/client/index.d.cts +33 -1
  5. package/dist/client/index.d.ts +33 -1
  6. package/dist/client/index.js +28 -11
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/codegen/preload_generator.cjs +44 -5
  9. package/dist/codegen/preload_generator.cjs.map +1 -1
  10. package/dist/codegen/preload_generator.js +44 -5
  11. package/dist/codegen/preload_generator.js.map +1 -1
  12. package/dist/codegen/renderer_app_generator.cjs +2 -9
  13. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  14. package/dist/codegen/renderer_app_generator.js +2 -9
  15. package/dist/codegen/renderer_app_generator.js.map +1 -1
  16. package/dist/controller/cleanup.cjs +39 -0
  17. package/dist/controller/cleanup.cjs.map +1 -0
  18. package/dist/controller/cleanup.d.cts +4 -0
  19. package/dist/controller/cleanup.d.ts +4 -0
  20. package/dist/controller/cleanup.js +15 -0
  21. package/dist/controller/cleanup.js.map +1 -0
  22. package/dist/controller/controller_lifecycle.cjs +26 -2
  23. package/dist/controller/controller_lifecycle.cjs.map +1 -1
  24. package/dist/controller/controller_lifecycle.d.cts +3 -1
  25. package/dist/controller/controller_lifecycle.d.ts +3 -1
  26. package/dist/controller/controller_lifecycle.js +24 -1
  27. package/dist/controller/controller_lifecycle.js.map +1 -1
  28. package/dist/frontend/frontend_lifecycle.cjs +5 -7
  29. package/dist/frontend/frontend_lifecycle.cjs.map +1 -1
  30. package/dist/frontend/frontend_lifecycle.d.cts +1 -14
  31. package/dist/frontend/frontend_lifecycle.d.ts +1 -14
  32. package/dist/frontend/frontend_lifecycle.js +5 -7
  33. package/dist/frontend/frontend_lifecycle.js.map +1 -1
  34. package/dist/frontend/frontend_surface.cjs +130 -104
  35. package/dist/frontend/frontend_surface.cjs.map +1 -1
  36. package/dist/frontend/frontend_surface.js +131 -105
  37. package/dist/frontend/frontend_surface.js.map +1 -1
  38. package/dist/frontend/work_registry.cjs +29 -4
  39. package/dist/frontend/work_registry.cjs.map +1 -1
  40. package/dist/frontend/work_registry.d.cts +2 -0
  41. package/dist/frontend/work_registry.d.ts +2 -0
  42. package/dist/frontend/work_registry.js +29 -4
  43. package/dist/frontend/work_registry.js.map +1 -1
  44. package/dist/index.d.cts +2 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/ipc/index.cjs +6 -2
  47. package/dist/ipc/index.cjs.map +1 -1
  48. package/dist/ipc/index.d.cts +9 -0
  49. package/dist/ipc/index.d.ts +9 -0
  50. package/dist/ipc/index.js +6 -2
  51. package/dist/ipc/index.js.map +1 -1
  52. package/dist/main/index.cjs +81 -82
  53. package/dist/main/index.cjs.map +1 -1
  54. package/dist/main/index.d.cts +13 -4
  55. package/dist/main/index.d.ts +13 -4
  56. package/dist/main/index.js +89 -87
  57. package/dist/main/index.js.map +1 -1
  58. package/dist/main/shutdown_coordinator.cjs +153 -0
  59. package/dist/main/shutdown_coordinator.cjs.map +1 -0
  60. package/dist/main/shutdown_coordinator.d.cts +81 -0
  61. package/dist/main/shutdown_coordinator.d.ts +81 -0
  62. package/dist/main/shutdown_coordinator.js +127 -0
  63. package/dist/main/shutdown_coordinator.js.map +1 -0
  64. package/dist/main/startup_failure_handler.cjs +67 -0
  65. package/dist/main/startup_failure_handler.cjs.map +1 -0
  66. package/dist/main/startup_failure_handler.d.cts +16 -0
  67. package/dist/main/startup_failure_handler.d.ts +16 -0
  68. package/dist/main/startup_failure_handler.js +43 -0
  69. package/dist/main/startup_failure_handler.js.map +1 -0
  70. package/dist/router/index.d.cts +3 -0
  71. package/dist/router/index.d.ts +3 -0
  72. package/dist/router/wood_router.cjs +204 -123
  73. package/dist/router/wood_router.cjs.map +1 -1
  74. package/dist/router/wood_router.d.cts +20 -0
  75. package/dist/router/wood_router.d.ts +20 -0
  76. package/dist/router/wood_router.js +206 -125
  77. package/dist/router/wood_router.js.map +1 -1
  78. package/dist/runtime/index.cjs +13 -0
  79. package/dist/runtime/index.cjs.map +1 -1
  80. package/dist/runtime/index.d.cts +8 -4
  81. package/dist/runtime/index.d.ts +8 -4
  82. package/dist/runtime/index.js +8 -0
  83. package/dist/runtime/index.js.map +1 -1
  84. package/dist/runtime/renderer_config_extension.cjs +1 -2
  85. package/dist/runtime/renderer_config_extension.cjs.map +1 -1
  86. package/dist/runtime/renderer_config_extension.d.cts +1 -1
  87. package/dist/runtime/renderer_config_extension.d.ts +1 -1
  88. package/dist/runtime/renderer_config_extension.js +1 -2
  89. package/dist/runtime/renderer_config_extension.js.map +1 -1
  90. package/dist/runtime/renderer_log_dispatcher.cjs +2 -0
  91. package/dist/runtime/renderer_log_dispatcher.cjs.map +1 -1
  92. package/dist/runtime/renderer_log_dispatcher.d.cts +1 -1
  93. package/dist/runtime/renderer_log_dispatcher.d.ts +1 -1
  94. package/dist/runtime/renderer_log_dispatcher.js +1 -0
  95. package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
  96. package/dist/runtime/runtime.cjs +107 -57
  97. package/dist/runtime/runtime.cjs.map +1 -1
  98. package/dist/runtime/runtime.d.cts +12 -0
  99. package/dist/runtime/runtime.d.ts +12 -0
  100. package/dist/runtime/runtime.js +110 -59
  101. package/dist/runtime/runtime.js.map +1 -1
  102. package/dist/runtime/runtime_module.cjs +58 -0
  103. package/dist/runtime/runtime_module.cjs.map +1 -0
  104. package/dist/runtime/runtime_module.d.cts +22 -0
  105. package/dist/runtime/runtime_module.d.ts +22 -0
  106. package/dist/runtime/runtime_module.js +30 -0
  107. package/dist/runtime/runtime_module.js.map +1 -0
  108. package/dist/testing/create_controller_harness.d.cts +3 -0
  109. package/dist/testing/create_controller_harness.d.ts +3 -0
  110. package/dist/testing/create_router_bridge.d.cts +3 -0
  111. package/dist/testing/create_router_bridge.d.ts +3 -0
  112. package/dist/testing/create_test_router.d.cts +3 -0
  113. package/dist/testing/create_test_router.d.ts +3 -0
  114. package/dist/testing/index.cjs +3 -0
  115. package/dist/testing/index.cjs.map +1 -1
  116. package/dist/testing/index.d.cts +6 -1
  117. package/dist/testing/index.d.ts +6 -1
  118. package/dist/testing/index.js +2 -0
  119. package/dist/testing/index.js.map +1 -1
  120. package/dist/testing/registration_only_root.cjs +168 -0
  121. package/dist/testing/registration_only_root.cjs.map +1 -0
  122. package/dist/testing/registration_only_root.d.cts +46 -0
  123. package/dist/testing/registration_only_root.d.ts +46 -0
  124. package/dist/testing/registration_only_root.js +141 -0
  125. package/dist/testing/registration_only_root.js.map +1 -0
  126. package/dist/testing/test_wood_config.cjs +382 -70
  127. package/dist/testing/test_wood_config.cjs.map +1 -1
  128. package/dist/testing/test_wood_config.d.cts +25 -1
  129. package/dist/testing/test_wood_config.d.ts +25 -1
  130. package/dist/testing/test_wood_config.js +388 -71
  131. package/dist/testing/test_wood_config.js.map +1 -1
  132. package/dist/testing/wood_dom_animation.cjs +213 -0
  133. package/dist/testing/wood_dom_animation.cjs.map +1 -0
  134. package/dist/testing/wood_dom_animation.d.cts +89 -0
  135. package/dist/testing/wood_dom_animation.d.ts +89 -0
  136. package/dist/testing/wood_dom_animation.js +185 -0
  137. package/dist/testing/wood_dom_animation.js.map +1 -0
  138. package/dist/testing/wood_env_renderer.cjs +81 -32
  139. package/dist/testing/wood_env_renderer.cjs.map +1 -1
  140. package/dist/testing/wood_env_renderer.d.cts +32 -1
  141. package/dist/testing/wood_env_renderer.d.ts +32 -1
  142. package/dist/testing/wood_env_renderer.js +84 -32
  143. package/dist/testing/wood_env_renderer.js.map +1 -1
  144. package/dist/transport/in_process_router_transport.d.cts +3 -0
  145. package/dist/transport/in_process_router_transport.d.ts +3 -0
  146. package/dist/transport/index.d.cts +3 -0
  147. package/dist/transport/index.d.ts +3 -0
  148. package/package.json +7 -6
  149. package/src/components/NavigationShell.svelte +12 -7
  150. package/src/controller/cleanup.ts +8 -0
  151. package/src/controller/controller_lifecycle.ts +24 -0
@@ -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":[]}
@@ -29,29 +29,35 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  var main_exports = {};
30
30
  __export(main_exports, {
31
31
  BootstrapActivationGate: () => BootstrapActivationGate,
32
+ RENDERER_SHUTDOWN_DRAIN_TIMEOUT_MS: () => import_shutdown_coordinator2.RENDERER_SHUTDOWN_DRAIN_TIMEOUT_MS,
33
+ ShutdownCoordinator: () => import_shutdown_coordinator2.ShutdownCoordinator,
32
34
  resolveWoodDebugMetadataConfiguration: () => resolveWoodDebugMetadataConfiguration,
33
35
  setRendererConfigExtension: () => import_renderer_config_extension2.setRendererConfigExtension,
34
36
  setRendererLogRecordFilter: () => import_renderer_log_batch_receiver2.setRendererLogRecordFilter,
35
- shouldDeferLastWindowCloseForCleanup: () => shouldDeferLastWindowCloseForCleanup,
37
+ shouldDeferLastWindowCloseForCleanup: () => import_shutdown_coordinator2.shouldDeferLastWindowCloseForCleanup,
36
38
  startWoodMain: () => startWoodMain
37
39
  });
38
40
  module.exports = __toCommonJS(main_exports);
39
41
  var import_path = __toESM(require("path"), 1);
42
+ var import_startup_failure_handler = require("./startup_failure_handler.cjs");
40
43
  var import_electron = require("electron");
41
44
  var import_logger = require("@noego/logger");
42
45
  var import_window_state_store = require("../window/window_state_store.js");
43
46
  var import_get_container = require("../runtime/get_container.js");
47
+ var import_cleanup = require("../controller/cleanup.js");
48
+ var import_ioc = require("@noego/ioc");
44
49
  var import_runtime = require("../runtime/runtime.js");
45
50
  var import_tracing = require("../tracing/index.js");
46
51
  var import_application_root = require("../runtime/application_root.js");
47
52
  var import_renderer_log_dispatcher = require("../runtime/renderer_log_dispatcher.js");
48
53
  var import_renderer_log_batch_receiver = require("../runtime/renderer_log_batch_receiver.js");
49
54
  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
55
  var import_renderer_config_extension2 = require("../runtime/renderer_config_extension.js");
52
56
  var import_ipc = require("../ipc/index.js");
53
57
  var import_shared = require("../ipc/shared.js");
54
58
  var import_renderer_trace_drain_coordinator = require("./renderer_trace_drain_coordinator.js");
59
+ var import_shutdown_coordinator = require("./shutdown_coordinator.js");
60
+ var import_shutdown_coordinator2 = require("./shutdown_coordinator.js");
55
61
  class BootstrapActivationGate {
56
62
  constructor(hasWindow, createMainWindow) {
57
63
  this.hasWindow = hasWindow;
@@ -79,9 +85,6 @@ class BootstrapActivationGate {
79
85
  }
80
86
  }
81
87
  }
82
- function shouldDeferLastWindowCloseForCleanup(trackedWindowCount, cleanupComplete) {
83
- return trackedWindowCount === 1 && !cleanupComplete;
84
- }
85
88
  const DEFAULT_WOOD_REMOTE_DEBUGGING_PORT = 9333;
86
89
  function resolveWoodDebugMetadataConfiguration(env = process.env) {
87
90
  const isDevelopment = env.WOOD_MODE === "development";
@@ -119,15 +122,22 @@ function toErrorMessage(error) {
119
122
  function startWoodMain(options) {
120
123
  const logger = (0, import_logger.getLogger)("wood:main").named("bootstrap");
121
124
  const runtimeLogger = (0, import_logger.getLogger)("wood:main").named("runtime");
125
+ const startupFailure = new import_startup_failure_handler.StartupFailureHandler(
126
+ logger,
127
+ (message) => {
128
+ process.stderr.write(message);
129
+ },
130
+ (code) => import_electron.app.exit(code)
131
+ );
122
132
  const debugConfig = resolveWoodDebugMetadataConfiguration();
123
133
  const windowState = new import_window_state_store.WindowStateStore();
124
134
  const windows = /* @__PURE__ */ new Map();
125
135
  const rendererHealth = /* @__PURE__ */ new Map();
126
136
  let mainWindow = null;
127
- let beforeQuitCleanupComplete = false;
128
- let beforeQuitCleanupPromise = null;
137
+ let shutdownCoordinator = 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);
@@ -515,7 +525,11 @@ function startWoodMain(options) {
515
525
  mainWindow = null;
516
526
  }
517
527
  if (windows.size === 0) {
518
- import_electron.app.quit();
528
+ if (shutdownCoordinator) {
529
+ shutdownCoordinator.handleAllWindowsClosed();
530
+ } else {
531
+ import_electron.app.quit();
532
+ }
519
533
  }
520
534
  });
521
535
  if (options.onWindowCreated) {
@@ -559,7 +573,7 @@ function startWoodMain(options) {
559
573
  safeSend(windowId, win, eventChannel, data);
560
574
  }
561
575
  };
562
- (0, import_shared.setIpcPushSender)(dispatchPushEvent);
576
+ if (options.legacyGlobals) (0, import_shared.setIpcPushSender)(dispatchPushEvent);
563
577
  const broadcast = (channel, data) => {
564
578
  for (const [windowId, win] of windows.entries()) {
565
579
  if (win.isDestroyed()) {
@@ -579,11 +593,13 @@ function startWoodMain(options) {
579
593
  broadcast,
580
594
  getRendererHealthState,
581
595
  setRendererLogCallback: (callback) => {
582
- (0, import_renderer_log_dispatcher.getRendererLogDispatcher)().setCallback(callback);
596
+ rendererLogDispatcher.setCallback(callback);
583
597
  },
584
- getRendererLogCallback: () => (0, import_renderer_log_dispatcher.getRendererLogDispatcher)().getCallback(),
598
+ getRendererLogCallback: () => rendererLogDispatcher.getCallback(),
585
599
  setRendererLogRecordFilter: import_renderer_log_batch_receiver.setRendererLogRecordFilter,
586
- setRendererConfigExtension: import_renderer_config_extension.setRendererConfigExtension,
600
+ setRendererConfigExtension: (extension) => {
601
+ rendererConfigExtension = extension;
602
+ },
587
603
  getDebugMetadata,
588
604
  // Late-bound after bootRuntime constructs the router (see bootstrap).
589
605
  dispatch: async () => {
@@ -601,10 +617,14 @@ function startWoodMain(options) {
601
617
  mainBootCleanup = bootResult;
602
618
  }
603
619
  }
604
- const { router: runtimeRouter } = await (0, import_runtime.runtime)({
620
+ const { router: runtimeRouter, dispose: closeRuntime } = await (0, import_runtime.runtime)({
605
621
  container,
606
622
  ipcMain: import_electron.ipcMain,
607
623
  registerRuntime: options.registerRuntime,
624
+ rendererTraceBatchReceiver: options.rendererTraceBatchReceiver,
625
+ legacyGlobals: options.legacyGlobals,
626
+ rendererLogDispatcher,
627
+ rendererConfigExtension: () => rendererConfigExtension?.() ?? {},
608
628
  controllersDir: resolveRuntimePath(options.controllersDir),
609
629
  middlewareDir: resolveRuntimePath(options.middlewareDir),
610
630
  ...options.traceMinLevel ? { traceMinLevel: options.traceMinLevel } : {},
@@ -698,88 +718,65 @@ function startWoodMain(options) {
698
718
  }
699
719
  });
700
720
  runtimeContext.dispatch = (channel, payload) => runtimeRouter.dispatch(channel, payload ?? {});
721
+ runtimeCleanup = closeRuntime;
701
722
  bootstrapActivationGate.ready();
702
723
  logger.info("bootstrap complete");
703
724
  };
704
- const handleBootstrapFailure = (error) => {
725
+ const handleBootstrapFailure = async (error) => {
705
726
  bootstrapActivationGate.failed();
706
- logger.error("bootstrap failed", {
707
- message: toErrorMessage(error),
708
- stack: error instanceof Error ? error.stack : void 0
727
+ await startupFailure.handle("bootstrap failed", error, async () => {
728
+ rendererTraceDrain.stop();
729
+ await (0, import_cleanup.cleanupAll)([() => runtimeCleanup?.(), () => mainBootCleanup?.(), () => container.dispose()], "Wood bootstrap rollback failed");
709
730
  });
710
731
  };
711
732
  void import_electron.app.whenReady().then(bootstrap).catch(handleBootstrapFailure);
712
733
  const registerApplicationLifecycle = () => {
734
+ const coordinator = new import_shutdown_coordinator.ShutdownCoordinator({
735
+ trackedWindows: () => windows.entries(),
736
+ clearTrackedWindows: () => {
737
+ windows.clear();
738
+ },
739
+ drainRenderers: () => (0, import_renderer_trace_drain_coordinator.drainRendererTracesBeforeQuit)(
740
+ rendererTraceDrain,
741
+ windows.values(),
742
+ logger,
743
+ Date.now() + import_shutdown_coordinator.RENDERER_SHUTDOWN_DRAIN_TIMEOUT_MS
744
+ ),
745
+ cleanupBackend: () => (0, import_cleanup.cleanupAll)([
746
+ () => runtimeCleanup?.(),
747
+ () => {
748
+ if (options.legacyGlobals) (0, import_shared.clearIpcPushSender)();
749
+ },
750
+ () => mainBootCleanup?.(),
751
+ () => options.onBeforeQuit?.(runtimeContext),
752
+ () => import_ioc.ExecutionContext.run(container, () => container.dispose())
753
+ ], "Wood main shutdown failed"),
754
+ requestQuit: () => {
755
+ import_electron.app.quit();
756
+ },
757
+ logger
758
+ });
759
+ shutdownCoordinator = coordinator;
713
760
  import_electron.app.on(
714
761
  "browser-window-created",
715
762
  (_event, window) => {
716
763
  window.on("close", (event) => {
717
764
  const isTrackedWindow = [...windows.values()].includes(window);
718
- if (!isTrackedWindow || !shouldDeferLastWindowCloseForCleanup(
719
- windows.size,
720
- beforeQuitCleanupComplete
721
- )) {
765
+ if (!isTrackedWindow) {
722
766
  return;
723
767
  }
724
- event.preventDefault();
725
- if (!beforeQuitCleanupPromise) {
726
- logger.debug("deferring last window close for renderer trace drain");
727
- import_electron.app.quit();
728
- }
768
+ coordinator.handleTrackedWindowClose(event, windows.size);
729
769
  });
730
770
  }
731
771
  );
732
- const destroyWindowsForQuit = () => {
733
- for (const [id, win] of windows) {
734
- if (!win.isDestroyed()) {
735
- win.destroy();
736
- logger.debug("destroyed window on quit", { windowId: id });
737
- }
738
- }
739
- windows.clear();
740
- };
741
- const runBeforeQuitCleanup = async () => {
742
- await (0, import_renderer_trace_drain_coordinator.drainRendererTracesBeforeQuit)(
743
- rendererTraceDrain,
744
- windows.values(),
745
- logger,
746
- Date.now() + 1e3
747
- );
748
- (0, import_shared.clearIpcPushSender)();
749
- if (mainBootCleanup) {
750
- await mainBootCleanup();
751
- }
752
- if (options.onBeforeQuit) {
753
- await options.onBeforeQuit(runtimeContext);
754
- }
755
- };
756
772
  import_electron.app.on("before-quit", (event) => {
757
773
  if (!runtimeContext) {
758
774
  return;
759
775
  }
760
- if (beforeQuitCleanupComplete) {
761
- destroyWindowsForQuit();
762
- return;
763
- }
764
- event.preventDefault();
765
- if (beforeQuitCleanupPromise) {
766
- return;
767
- }
768
- beforeQuitCleanupPromise = (async () => {
769
- try {
770
- await runBeforeQuitCleanup();
771
- } catch (error) {
772
- logger.warn("before-quit handler failed", { error: toErrorMessage(error) });
773
- } finally {
774
- beforeQuitCleanupComplete = true;
775
- beforeQuitCleanupPromise = null;
776
- }
777
- destroyWindowsForQuit();
778
- import_electron.app.quit();
779
- })();
776
+ coordinator.handleBeforeQuit(event);
780
777
  });
781
778
  import_electron.app.on("window-all-closed", () => {
782
- import_electron.app.quit();
779
+ coordinator.handleAllWindowsClosed();
783
780
  });
784
781
  import_electron.app.on("activate", () => {
785
782
  bootstrapActivationGate.activate();
@@ -787,17 +784,19 @@ function startWoodMain(options) {
787
784
  };
788
785
  registerApplicationLifecycle();
789
786
  };
790
- void initializeRuntime().catch((error) => {
791
- rendererTraceDrain.stop();
792
- logger.error("startup failed before ready", {
793
- message: toErrorMessage(error),
794
- stack: error instanceof Error ? error.stack : void 0
795
- });
796
- });
787
+ void initializeRuntime().catch((error) => startupFailure.handle(
788
+ "startup failed before ready",
789
+ error,
790
+ async () => {
791
+ rendererTraceDrain.stop();
792
+ }
793
+ ));
797
794
  }
798
795
  // Annotate the CommonJS export names for ESM import in node:
799
796
  0 && (module.exports = {
800
797
  BootstrapActivationGate,
798
+ RENDERER_SHUTDOWN_DRAIN_TIMEOUT_MS,
799
+ ShutdownCoordinator,
801
800
  resolveWoodDebugMetadataConfiguration,
802
801
  setRendererConfigExtension,
803
802
  setRendererLogRecordFilter,