╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │112 const jsonBytes = Uint8Array.from(atob(pageJsonBase64), (c) => c.charCodeAt(0)); │ │113 const ptr = wasmInstance.exports._alloc(jsonBytes.length); │ │114 new Uint8Array(wasmInstance.exports.memory.buffer, ptr, jsonBytes.length).set(jsonBytes);│ │115 - wasmInstance.exports._start(ptr, jsonBytes.length, minRows(), maxCols()); │ │115 + wasmInstance.exports._init(ptr, jsonBytes.length, minRows(), maxCols()); │ │116 │ │117 document.addEventListener("keydown", (event) => { │ │118 // when a form element (text input or submit button) owns focus, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/main_wasm.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ 9 const allocator = std.heap.wasm_allocator; │ │10 │ │11 var root: ?ui.Widget = null; │ │12 - │ │13 -fn updateHtml() !void { │ │14 - const root_ptr = if (root) |*root_value| root_value else return error.NotStarted; │ │15 - const html = try web.generateHtml(allocator, root_ptr, &session); │ │16 - defer allocator.free(html); │ │17 - setHtml(html); │ │18 - │ │19 - // emit the overlay (form + inputs + button) on every tick so the wasm │ │20 - // layout drives positions, not just the server's initial render. JS │ │21 - // diffs the result against the previous overlay; an unchanged overlay │ │22 - // leaves the live <form> alone — crucial since wiping it mid-click │ │23 - // would detach the submit button before the browser can dispatch the │ │24 - // form submission. │ │25 - const overlay = try web.generateOverlay(allocator, root_ptr, &session); │ │26 - defer allocator.free(overlay); │ │27 - setOverlay(overlay); │ │28 -} │ │29 - │ │30 -// the snapshot is allocated into this arena via parseFromSliceLeaky. it │ │31 -// has to outlive `root`, because `root` holds slices into the parsed strings. │ │32 -var page_arena: ?std.heap.ArenaAllocator = null; │ │33 -var snapshot: ?ui.Snapshot = null; │ │34 -var session: ui.Session = .{}; │ │35 -// last current_page we told JS about. used to avoid pushing the URL on │ │36 -// every tick — we only call history.pushState when the tab actually │ │37 -// changes. │ │38 -var last_pushed_page: ?ui.RoutablePage = null; │ │12 +var page_arena = std.heap.ArenaAllocator.init(allocator); │ │13 +var snapshot: ui.Snapshot = undefined; │ │14 +var session: ui.Session = undefined; │ │15 +var last_pushed_page_maybe: ?ui.RoutablePage = null; // last current_page we told JS about│ │16 │ │40 -fn start(json: []const u8, min_height: u32, max_width: u32) !void { │ │41 - if (page_arena) |*a| a.deinit(); │ │42 - page_arena = std.heap.ArenaAllocator.init(allocator); │ │17 +fn init(json: []const u8, min_height: u32, max_width: u32) !void { │ │18 + _ = page_arena.reset(.free_all); │ │19 │ │20 // alloc_always so the parsed strings live entirely inside the arena │ │21 // and don't borrow from the caller's json buffer. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │46 - snapshot = try std.json.parseFromSliceLeaky(ui.Snapshot, (page_arena orelse unreachable).allocator(), json, .{│ │22 + snapshot = try std.json.parseFromSliceLeaky(ui.Snapshot, page_arena.allocator(), json, .{ │ │23 .allocate = .alloc_always, │ │24 }); │ │25 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │26 // adopt the server's session view wholesale — one assignment instead of │ │27 // hand-restoring each field by reaching into the page's data tree. │ │52 - session.data = (snapshot orelse unreachable).session; │ │28 + session = .{ │ │29 + .data = snapshot.session, │ │30 + .arena = &page_arena, │ │31 + }; │ │32 │ │54 - var next_root = try ui.initRoot(allocator, &(snapshot orelse unreachable).page, &session);│ │33 + var next_root = try ui.initRoot(allocator, &snapshot.page, &session); │ │34 errdefer next_root.deinit(allocator); │ │35 │ │36 if (root) |*old_root| old_root.deinit(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │51 │ │52 // mirror the page Header settled on into the browser URL. _pushState │ │53 // is idempotent on the JS side (it bails when pathname already matches) │ │75 - // so the first tick after _start is a no-op even though we always send │ │54 + // so the first tick after _init is a no-op even though we always send │ │55 // something through. │ │56 const current_page = session.data.current_page; │ │78 - if (last_pushed_page == null or last_pushed_page.? != current_page) { │ │79 - last_pushed_page = current_page; │ │57 + const should_push = if (last_pushed_page_maybe) |lp| lp != current_page else true;│ │58 + if (should_push) { │ │59 + last_pushed_page_maybe = current_page; │ │60 const url = current_page.url(); │ │61 _pushState(url.ptr, @intCast(url.len)); │ │62 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │63 │ │84 - try updateHtml(); │ │64 + const html = try web.generateHtml(allocator, root_ptr, &session); │ │65 + defer allocator.free(html); │ │66 + setHtml(html); │ │67 + │ │68 + // emit the overlay (form + inputs + button) on every tick so the wasm │ │69 + // layout drives positions, not just the server's initial render. JS │ │70 + // diffs the result against the previous overlay; an unchanged overlay │ │71 + // leaves the live <form> alone — crucial since wiping it mid-click │ │72 + // would detach the submit button before the browser can dispatch the │ │73 + // form submission. │ │74 + const overlay = try web.generateOverlay(allocator, root_ptr, &session);│ │75 + defer allocator.free(overlay); │ │76 + setOverlay(overlay); │ │77 } │ │78 │ │79 fn onKeyDown(key_code: u32) !void { │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │121 return slice.ptr; │ │122 } │ │123 │ │132 -export fn _start(json_ptr: [*]u8, json_len: u32, min_height: u32, max_width: u32) void {│ │124 +export fn _init(json_ptr: [*]u8, json_len: u32, min_height: u32, max_width: u32) void { │ │125 const json = json_ptr[0..json_len]; │ │126 defer allocator.free(json); │ │135 - start(json, min_height, max_width) catch |err| { │ │127 + init(json, min_height, max_width) catch |err| { │ │128 var buf: [256]u8 = undefined; │ │137 - const str = std.fmt.bufPrint(&buf, "start: {}", .{err}) catch unreachable; │ │129 + const str = std.fmt.bufPrint(&buf, "init: {}", .{err}) catch unreachable; │ │130 consoleLog(str); │ │131 }; │ │132 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │57 const Repo = rp.Repo(.xit, repo_opts); │ │58 var repo = try Repo.open(io, allocator, .{ .path = handler.admin_repo_path }); │ │59 defer repo.deinit(io, allocator); │ │60 - var ui_session: ui.Session = .{}; │ │60 + var ui_session = try ui.Session.init(repo_opts, &page_arena, &repo, .{}); │ │61 │ │62 - const page: ui.Page = .{ .home = try .init(repo_opts, &page_arena, &repo, &ui_session) }; │ │62 + const page: ui.Page = .{ .home = try .init(repo_opts, &page_arena, ui_session.haxy_moment orelse unreachable) };│ │63 │ │64 var root = try ui.initRoot(allocator, &page, &ui_session); │ │65 defer root.deinit(allocator); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │48 var repo = try Repo.init(io, allocator, .{ .path = work_path });│ │49 defer repo.deinit(io, allocator); │ │50 │ │51 - var session: ui.Session = .{}; │ │51 + var session: ui.Session = undefined; │ │52 │ │53 const page: ui.Page = blk: { │ │54 var arena = std.heap.ArenaAllocator.init(allocator); │ ╰────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │187 // consume events into the database │ │188 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); │ │189 │ │190 - break :blk .{ .home = try .init(repo_opts, &page_arena, &repo, &session) }; │ │190 + session = try ui.Session.init(repo_opts, &page_arena, &repo, .{}); │ │191 + break :blk .{ .home = try .init(repo_opts, &page_arena, session.haxy_moment orelse unreachable) };│ │192 }; │ │193 │ │194 // start the server │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯