Skip to content

Commit 75eac4e

Browse files
committed
Overhaul profile path API
1 parent 742a8ab commit 75eac4e

8 files changed

Lines changed: 67 additions & 71 deletions

File tree

MIGRATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ Profile-path notes:
7676

7777
Repository cleanup notes:
7878
- Active example sources are under `examples/` (used directly by `zig build run`/`zig build examples`).
79-
- Legacy bridge asset compatibility arguments were removed from the build-time JS asset generator.
79+
- Bridge asset generation uses a single strict build-time path.
8080

8181
## Pinned Struct Move Safety
8282

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ zig build -l
6565
zig build -h
6666
```
6767

68-
## Cleanup Notes
68+
## Repository Hygiene
6969

70-
- Removed stale compatibility handling from JS asset generation (`js_asset_gen` no longer accepts legacy extra args).
71-
- Collapsed duplicated `Service` move-invariant guard calls to a single guard boundary (`Service.window()` + `App` entrypoints).
72-
- Async RPC jobs remain push-first; fallback polling now starts only when needed (or after a short grace window) to reduce extra requests.
70+
- Removed duplicate render-path logic in `Window.showHtml` / `Window.showFile`.
71+
- Removed unnecessary lifecycle unload signaling to reduce extra runtime traffic.
72+
- Profile launch semantics are strict and rule-based (`profile_rules`, first-match wins).
7373

7474
## Add To Your Project
7575

docs/migration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ Typed control APIs:
213213
- Existing warning log strings may still appear, but typed diagnostics are the authoritative integration surface.
214214
- For Linux packaging, validate helper/runtime presence through `listRuntimeRequirements`.
215215
- Active examples are tracked under `examples/` and built from those paths directly.
216-
- Legacy JS asset generator compatibility arguments were removed; the build path is now strict and deterministic.
216+
- JS asset generation is strict and deterministic in the active build path.
217217

218218
## Pinned Struct Move Safety
219219

examples/shared/demo_runner.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ fn hintFor(comptime kind: ExampleKind) []const u8 {
419419
.public_network => "Public listen policy enabled for LAN testing.",
420420
.multi_client => "Open multiple clients and verify independent RPC sessions.",
421421
.chatgpt_api => "RPC transport pattern suitable for chat-like workflows.",
422-
.custom_web_server => "Custom server integration shape with bridge-compatible routes.",
422+
.custom_web_server => "Custom server integration shape with bridge RPC routes.",
423423
.react => "Component-style RPC calls using the generated bridge contract.",
424424
else => "Window style/control and transport demo.",
425425
};

src/bridge/generated/runtime_helpers.embed.js

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -579,18 +579,11 @@ async function __webuiExecuteScriptTask(task) {
579579
})();
580580

581581
(function __webuiInstallLifecycleHooks() {
582-
if (typeof globalThis === "undefined" || typeof globalThis.addEventListener !== "function") return;
583-
globalThis.addEventListener("beforeunload", () => {
584-
// beforeunload also fires on reload/navigation, so do not signal hard-close here.
585-
__webuiNotifyLifecycle("window_unloading");
586-
});
587-
588-
if (typeof globalThis !== "undefined") {
589-
globalThis.__webuiNotifyLifecycle = __webuiNotifyLifecycle;
590-
globalThis.__webuiWindowControl = __webuiWindowControl;
591-
globalThis.__webuiWindowStyle = __webuiWindowStyle;
592-
globalThis.__webuiGetWindowStyle = __webuiGetWindowStyle;
593-
globalThis.__webuiGetWindowCapabilities = __webuiGetWindowCapabilities;
594-
}
582+
if (typeof globalThis === "undefined") return;
583+
globalThis.__webuiNotifyLifecycle = __webuiNotifyLifecycle;
584+
globalThis.__webuiWindowControl = __webuiWindowControl;
585+
globalThis.__webuiWindowStyle = __webuiWindowStyle;
586+
globalThis.__webuiGetWindowStyle = __webuiGetWindowStyle;
587+
globalThis.__webuiGetWindowCapabilities = __webuiGetWindowCapabilities;
595588
})();
596589

src/bridge/generated/runtime_helpers.written.js

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -579,17 +579,10 @@ async function __webuiExecuteScriptTask(task) {
579579
})();
580580

581581
(function __webuiInstallLifecycleHooks() {
582-
if (typeof globalThis === "undefined" || typeof globalThis.addEventListener !== "function") return;
583-
globalThis.addEventListener("beforeunload", () => {
584-
// beforeunload also fires on reload/navigation, so do not signal hard-close here.
585-
__webuiNotifyLifecycle("window_unloading");
586-
});
587-
588-
if (typeof globalThis !== "undefined") {
589-
globalThis.__webuiNotifyLifecycle = __webuiNotifyLifecycle;
590-
globalThis.__webuiWindowControl = __webuiWindowControl;
591-
globalThis.__webuiWindowStyle = __webuiWindowStyle;
592-
globalThis.__webuiGetWindowStyle = __webuiGetWindowStyle;
593-
globalThis.__webuiGetWindowCapabilities = __webuiGetWindowCapabilities;
594-
}
582+
if (typeof globalThis === "undefined") return;
583+
globalThis.__webuiNotifyLifecycle = __webuiNotifyLifecycle;
584+
globalThis.__webuiWindowControl = __webuiWindowControl;
585+
globalThis.__webuiWindowStyle = __webuiWindowStyle;
586+
globalThis.__webuiGetWindowStyle = __webuiGetWindowStyle;
587+
globalThis.__webuiGetWindowCapabilities = __webuiGetWindowCapabilities;
595588
})();

src/bridge/runtime_helpers.source.js

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -579,17 +579,10 @@ async function __webuiExecuteScriptTask(task) {
579579
})();
580580

581581
(function __webuiInstallLifecycleHooks() {
582-
if (typeof globalThis === "undefined" || typeof globalThis.addEventListener !== "function") return;
583-
globalThis.addEventListener("beforeunload", () => {
584-
// beforeunload also fires on reload/navigation, so do not signal hard-close here.
585-
__webuiNotifyLifecycle("window_unloading");
586-
});
587-
588-
if (typeof globalThis !== "undefined") {
589-
globalThis.__webuiNotifyLifecycle = __webuiNotifyLifecycle;
590-
globalThis.__webuiWindowControl = __webuiWindowControl;
591-
globalThis.__webuiWindowStyle = __webuiWindowStyle;
592-
globalThis.__webuiGetWindowStyle = __webuiGetWindowStyle;
593-
globalThis.__webuiGetWindowCapabilities = __webuiGetWindowCapabilities;
594-
}
582+
if (typeof globalThis === "undefined") return;
583+
globalThis.__webuiNotifyLifecycle = __webuiNotifyLifecycle;
584+
globalThis.__webuiWindowControl = __webuiWindowControl;
585+
globalThis.__webuiWindowStyle = __webuiWindowStyle;
586+
globalThis.__webuiGetWindowStyle = __webuiGetWindowStyle;
587+
globalThis.__webuiGetWindowCapabilities = __webuiGetWindowCapabilities;
595588
})();

src/root.zig

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,13 +1606,30 @@ const WindowState = struct {
16061606
fn reconcileChildExit(self: *WindowState, allocator: std.mem.Allocator) void {
16071607
const pid = self.launched_browser_pid orelse return;
16081608

1609+
// IMPORTANT:
1610+
// In browser/web modes, the PID we track can be a short-lived launcher process
1611+
// (or helper) rather than the long-lived browser tab/window process.
1612+
// We must not close the backend just because that PID exits.
1613+
// Only native-webview-first mode treats tracked process death as a terminal close.
1614+
const should_close_on_pid_exit = self.launch_policy.first == .native_webview;
1615+
16091616
if (self.launched_browser_lifecycle_linked and core_runtime.linkedChildExited(pid)) {
1610-
self.markClosedFromTrackedBrowserExit(allocator, "child-exited");
1617+
if (should_close_on_pid_exit) {
1618+
self.markClosedFromTrackedBrowserExit(allocator, "child-exited");
1619+
} else {
1620+
self.clearTrackedBrowserState(allocator);
1621+
self.emit(.window_state, "browser-detached", "child-exited");
1622+
}
16111623
return;
16121624
}
16131625

16141626
if (!core_runtime.isProcessAlive(pid)) {
1615-
self.markClosedFromTrackedBrowserExit(allocator, "browser-exited");
1627+
if (should_close_on_pid_exit) {
1628+
self.markClosedFromTrackedBrowserExit(allocator, "browser-exited");
1629+
} else {
1630+
self.clearTrackedBrowserState(allocator);
1631+
self.emit(.window_state, "browser-detached", "browser-exited");
1632+
}
16161633
}
16171634
}
16181635

@@ -2548,6 +2565,15 @@ pub const Window = struct {
25482565
index: usize,
25492566
id: usize,
25502567

2568+
fn refreshRenderedContentLocked(self: *Window, win_state: *WindowState) !void {
2569+
try win_state.ensureBrowserRenderState(self.app.allocator, self.app.options);
2570+
if (win_state.isNativeWindowActive()) {
2571+
if (win_state.last_url) |url| {
2572+
_ = win_state.backend.showContent(.{ .url = url }) catch {};
2573+
}
2574+
}
2575+
}
2576+
25512577
pub fn showHtml(self: *Window, html: []const u8) !void {
25522578
self.app.enforcePinnedMoveInvariant(.app);
25532579
if (html.len == 0) return error.EmptyHtml;
@@ -2564,12 +2590,7 @@ pub const Window = struct {
25642590
}
25652591
win_state.shown = true;
25662592

2567-
try win_state.ensureBrowserRenderState(self.app.allocator, self.app.options);
2568-
if (win_state.isNativeWindowActive()) {
2569-
if (win_state.last_url) |url| {
2570-
_ = win_state.backend.showContent(.{ .url = url }) catch {};
2571-
}
2572-
}
2593+
try self.refreshRenderedContentLocked(win_state);
25732594

25742595
self.emitRuntimeDiagnostics();
25752596
self.emit(.navigation, "show-html", html);
@@ -2601,12 +2622,7 @@ pub const Window = struct {
26012622
}
26022623
win_state.shown = true;
26032624

2604-
try win_state.ensureBrowserRenderState(self.app.allocator, self.app.options);
2605-
if (win_state.isNativeWindowActive()) {
2606-
if (win_state.last_url) |url| {
2607-
_ = win_state.backend.showContent(.{ .url = url }) catch {};
2608-
}
2609-
}
2625+
try self.refreshRenderedContentLocked(win_state);
26102626

26112627
self.emitRuntimeDiagnostics();
26122628
self.emit(.navigation, "show-file", path);
@@ -3020,6 +3036,9 @@ pub const Service = struct {
30203036
const state = win.state();
30213037
state.state_mutex.lock();
30223038
defer state.state_mutex.unlock();
3039+
// Reconcile tracked process state every loop.
3040+
// `reconcileChildExit()` is mode-aware and only turns PID exit into app close
3041+
// for native-webview-first mode. Browser/web modes only detach PID tracking.
30233042
state.reconcileChildExit(self.app.allocator);
30243043
return state.close_requested.load(.acquire);
30253044
}
@@ -3755,7 +3774,6 @@ fn handleLifecycleRoute(
37553774
if (!std.mem.eql(u8, path_only, "/webui/lifecycle")) return false;
37563775

37573776
var parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch null;
3758-
var logged_event = false;
37593777
const client_token = httpHeaderValue(headers, "x-webui-client-id") orelse default_client_token;
37603778
state.state_mutex.lock();
37613779
_ = state.findOrCreateClientSessionLocked(client_token) catch null;
@@ -3770,19 +3788,11 @@ fn handleLifecycleRoute(
37703788
state.requestLifecycleCloseFromFrontend();
37713789
state.state_mutex.unlock();
37723790
}
3773-
if (state.rpc_state.log_enabled) {
3774-
std.debug.print("[webui.lifecycle] event={s}\n", .{event_value.string});
3775-
logged_event = true;
3776-
}
37773791
}
37783792
}
37793793
}
37803794
}
37813795

3782-
if (state.rpc_state.log_enabled and !logged_event) {
3783-
std.debug.print("[webui.lifecycle] body={s}\n", .{body});
3784-
}
3785-
37863796
try writeHttpResponse(stream, 200, "application/json; charset=utf-8", "{\"ok\":true}");
37873797
return true;
37883798
}
@@ -5120,7 +5130,7 @@ test "window_closing lifecycle event is ignored while tracked browser pid is ali
51205130
try std.testing.expect(!should_close);
51215131
}
51225132

5123-
test "non-linked tracked browser pid death requests close" {
5133+
test "non-linked tracked browser pid death detaches without close in web mode" {
51245134
if (builtin.os.tag != .linux) return error.SkipZigTest;
51255135

51265136
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
@@ -5152,20 +5162,27 @@ test "non-linked tracked browser pid death requests close" {
51525162
win.state().state_mutex.unlock();
51535163

51545164
var closed = false;
5165+
var detached = false;
51555166
var attempts: usize = 0;
51565167
while (attempts < 120) : (attempts += 1) {
51575168
win.state().state_mutex.lock();
51585169
win.state().reconcileChildExit(gpa.allocator());
51595170
const requested = win.state().close_requested.load(.acquire);
5171+
const tracked_pid = win.state().launched_browser_pid;
51605172
win.state().state_mutex.unlock();
51615173
if (requested) {
51625174
closed = true;
51635175
break;
51645176
}
5177+
if (tracked_pid == null) {
5178+
detached = true;
5179+
break;
5180+
}
51655181
std.Thread.sleep(5 * std.time.ns_per_ms);
51665182
}
51675183

5168-
try std.testing.expect(closed);
5184+
try std.testing.expect(!closed);
5185+
try std.testing.expect(detached);
51695186
}
51705187

51715188
test "window style apply updates persisted state" {

0 commit comments

Comments
 (0)