@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.
- package/README.md +31 -0
- package/dist/client/index.cjs +18 -11
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +21 -1
- package/dist/client/index.d.ts +21 -1
- package/dist/client/index.js +17 -11
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/renderer_app_generator.cjs +2 -9
- package/dist/codegen/renderer_app_generator.cjs.map +1 -1
- package/dist/codegen/renderer_app_generator.js +2 -9
- package/dist/codegen/renderer_app_generator.js.map +1 -1
- package/dist/controller/cleanup.cjs +39 -0
- package/dist/controller/cleanup.cjs.map +1 -0
- package/dist/controller/cleanup.d.cts +4 -0
- package/dist/controller/cleanup.d.ts +4 -0
- package/dist/controller/cleanup.js +15 -0
- package/dist/controller/cleanup.js.map +1 -0
- package/dist/controller/controller_lifecycle.cjs +26 -2
- package/dist/controller/controller_lifecycle.cjs.map +1 -1
- package/dist/controller/controller_lifecycle.d.cts +3 -1
- package/dist/controller/controller_lifecycle.d.ts +3 -1
- package/dist/controller/controller_lifecycle.js +24 -1
- package/dist/controller/controller_lifecycle.js.map +1 -1
- package/dist/frontend/frontend_lifecycle.cjs +5 -7
- package/dist/frontend/frontend_lifecycle.cjs.map +1 -1
- package/dist/frontend/frontend_lifecycle.d.cts +1 -14
- package/dist/frontend/frontend_lifecycle.d.ts +1 -14
- package/dist/frontend/frontend_lifecycle.js +5 -7
- package/dist/frontend/frontend_lifecycle.js.map +1 -1
- package/dist/frontend/frontend_surface.cjs +130 -104
- package/dist/frontend/frontend_surface.cjs.map +1 -1
- package/dist/frontend/frontend_surface.js +131 -105
- package/dist/frontend/frontend_surface.js.map +1 -1
- package/dist/frontend/work_registry.cjs +29 -4
- package/dist/frontend/work_registry.cjs.map +1 -1
- package/dist/frontend/work_registry.d.cts +2 -0
- package/dist/frontend/work_registry.d.ts +2 -0
- package/dist/frontend/work_registry.js +29 -4
- package/dist/frontend/work_registry.js.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/ipc/index.cjs +6 -2
- package/dist/ipc/index.cjs.map +1 -1
- package/dist/ipc/index.d.cts +9 -0
- package/dist/ipc/index.d.ts +9 -0
- package/dist/ipc/index.js +6 -2
- package/dist/ipc/index.js.map +1 -1
- package/dist/main/index.cjs +53 -35
- package/dist/main/index.cjs.map +1 -1
- package/dist/main/index.d.cts +11 -2
- package/dist/main/index.d.ts +11 -2
- package/dist/main/index.js +56 -40
- package/dist/main/index.js.map +1 -1
- package/dist/main/startup_failure_handler.cjs +67 -0
- package/dist/main/startup_failure_handler.cjs.map +1 -0
- package/dist/main/startup_failure_handler.d.cts +16 -0
- package/dist/main/startup_failure_handler.d.ts +16 -0
- package/dist/main/startup_failure_handler.js +43 -0
- package/dist/main/startup_failure_handler.js.map +1 -0
- package/dist/router/index.d.cts +3 -0
- package/dist/router/index.d.ts +3 -0
- package/dist/router/wood_router.cjs +204 -123
- package/dist/router/wood_router.cjs.map +1 -1
- package/dist/router/wood_router.d.cts +20 -0
- package/dist/router/wood_router.d.ts +20 -0
- package/dist/router/wood_router.js +206 -125
- package/dist/router/wood_router.js.map +1 -1
- package/dist/runtime/index.cjs +13 -0
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +8 -4
- package/dist/runtime/index.d.ts +8 -4
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderer_config_extension.cjs +1 -2
- package/dist/runtime/renderer_config_extension.cjs.map +1 -1
- package/dist/runtime/renderer_config_extension.d.cts +1 -1
- package/dist/runtime/renderer_config_extension.d.ts +1 -1
- package/dist/runtime/renderer_config_extension.js +1 -2
- package/dist/runtime/renderer_config_extension.js.map +1 -1
- package/dist/runtime/renderer_log_dispatcher.cjs +2 -0
- package/dist/runtime/renderer_log_dispatcher.cjs.map +1 -1
- package/dist/runtime/renderer_log_dispatcher.d.cts +1 -1
- package/dist/runtime/renderer_log_dispatcher.d.ts +1 -1
- package/dist/runtime/renderer_log_dispatcher.js +1 -0
- package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
- package/dist/runtime/runtime.cjs +107 -57
- package/dist/runtime/runtime.cjs.map +1 -1
- package/dist/runtime/runtime.d.cts +12 -0
- package/dist/runtime/runtime.d.ts +12 -0
- package/dist/runtime/runtime.js +110 -59
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/runtime_module.cjs +58 -0
- package/dist/runtime/runtime_module.cjs.map +1 -0
- package/dist/runtime/runtime_module.d.cts +22 -0
- package/dist/runtime/runtime_module.d.ts +22 -0
- package/dist/runtime/runtime_module.js +30 -0
- package/dist/runtime/runtime_module.js.map +1 -0
- package/dist/testing/create_controller_harness.d.cts +3 -0
- package/dist/testing/create_controller_harness.d.ts +3 -0
- package/dist/testing/create_router_bridge.d.cts +3 -0
- package/dist/testing/create_router_bridge.d.ts +3 -0
- package/dist/testing/create_test_router.d.cts +3 -0
- package/dist/testing/create_test_router.d.ts +3 -0
- package/dist/testing/index.cjs +3 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +6 -1
- package/dist/testing/index.d.ts +6 -1
- package/dist/testing/index.js +2 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/registration_only_root.cjs +168 -0
- package/dist/testing/registration_only_root.cjs.map +1 -0
- package/dist/testing/registration_only_root.d.cts +46 -0
- package/dist/testing/registration_only_root.d.ts +46 -0
- package/dist/testing/registration_only_root.js +141 -0
- package/dist/testing/registration_only_root.js.map +1 -0
- package/dist/testing/test_wood_config.cjs +382 -70
- package/dist/testing/test_wood_config.cjs.map +1 -1
- package/dist/testing/test_wood_config.d.cts +25 -1
- package/dist/testing/test_wood_config.d.ts +25 -1
- package/dist/testing/test_wood_config.js +388 -71
- package/dist/testing/test_wood_config.js.map +1 -1
- package/dist/testing/wood_dom_animation.cjs +213 -0
- package/dist/testing/wood_dom_animation.cjs.map +1 -0
- package/dist/testing/wood_dom_animation.d.cts +89 -0
- package/dist/testing/wood_dom_animation.d.ts +89 -0
- package/dist/testing/wood_dom_animation.js +185 -0
- package/dist/testing/wood_dom_animation.js.map +1 -0
- package/dist/testing/wood_env_renderer.cjs +81 -32
- package/dist/testing/wood_env_renderer.cjs.map +1 -1
- package/dist/testing/wood_env_renderer.d.cts +32 -1
- package/dist/testing/wood_env_renderer.d.ts +32 -1
- package/dist/testing/wood_env_renderer.js +84 -32
- package/dist/testing/wood_env_renderer.js.map +1 -1
- package/dist/transport/in_process_router_transport.d.cts +3 -0
- package/dist/transport/in_process_router_transport.d.ts +3 -0
- package/dist/transport/index.d.cts +3 -0
- package/dist/transport/index.d.ts +3 -0
- package/package.json +8 -6
- package/src/components/NavigationShell.svelte +12 -7
- package/src/controller/cleanup.ts +8 -0
- package/src/controller/controller_lifecycle.ts +24 -0
- 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
|
-
|
|
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(() =>
|
|
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
|
-
|
|
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(() =>
|
|
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';
|
package/dist/ipc/index.cjs
CHANGED
|
@@ -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
|
};
|
package/dist/ipc/index.cjs.map
CHANGED
|
@@ -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,
|
|
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":[]}
|
package/dist/ipc/index.d.cts
CHANGED
|
@@ -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;
|
package/dist/ipc/index.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/ipc/index.js.map
CHANGED
|
@@ -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,
|
|
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":[]}
|
package/dist/main/index.cjs
CHANGED
|
@@ -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
|
|
180
|
-
|
|
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, () =>
|
|
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
|
-
|
|
592
|
+
rendererLogDispatcher.setCallback(callback);
|
|
579
593
|
},
|
|
580
|
-
getRendererLogCallback: () =>
|
|
594
|
+
getRendererLogCallback: () => rendererLogDispatcher.getCallback(),
|
|
581
595
|
setRendererLogRecordFilter: import_renderer_log_batch_receiver.setRendererLogRecordFilter,
|
|
582
|
-
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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,
|
|
739
|
-
rendererTraceDrain,
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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 = {
|