╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │172 } │ │173 │ │174 // commit the seed events and consume them into the database │ │176 - try evt.commitAndConsume(repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); │ │175 + try evt.commitAndConsume(evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume);│ │176 │ │178 - session = try ui.Session.init(repo_opts, &page_arena, &repo, .{}); │ │179 - break :blk .{ .home = try .init(repo_opts, &page_arena, session.haxy_moment orelse unreachable) }; │ │177 + session = try ui.Session.init(&page_arena, &repo, .{}); │ │178 + break :blk .{ .home = try .init(&page_arena, session.haxy_moment orelse unreachable) }; │ │179 }; │ │180 │ │181 // start the server │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────────────────────────────────────────────────────╮ │ 9 const Grid = xitui.grid.Grid; │ │10 const Focus = xitui.focus.Focus; │ │11 const evt = @import("./event.zig"); │ │12 - │ │13 -pub const canonical_repo_opts: rp.RepoOpts(.xit) = .{};│ │14 -pub const DB = rp.Repo(.xit, canonical_repo_opts).DB; │ │12 │ │13 pub const Home = @import("./ui/Home.zig"); │ │14 pub const Title = @import("./ui/Title.zig"); │ ╰───────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │15 │ │16 pub const Page = union(enum) { │ │17 home: Home, │ │18 +}; │ │19 + │ │20 +// what the server hands to the client (and what main_wasm parses on _start).│ │21 +// keeps Page free of any per-request session state. │ │22 +pub const Snapshot = struct { │ │23 + page: Page, │ │24 + session: Session.Data = .{}, │ │25 }; │ │26 │ │27 // a top-level "page" the user can navigate to │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │53 }; │ │54 │ │55 // per-connection mutable state. each SSH session / web session / local TUI │ │52 -// run gets its own │ │56 +// run gets its own. │ │57 pub const Session = struct { │ │54 - data: Data = .{}, // serializable data sent down to web client │ │58 + data: Data = .{}, │ │59 arena: *std.heap.ArenaAllocator, │ │56 - haxy_moment: ?DB.HashMap(.read_only) = null, // db cursor (null on the wasm side) │ │60 + haxy_moment: ?evt.AdminDB.HashMap(.read_only) = null, // db cursor (null on the wasm side)│ │61 pending: std.ArrayList(Action) = .empty, // actions queued by widgets this frame │ │62 + │ │63 + const Self = @This(); │ │64 │ │65 + // serializable data sent down to web client │ │66 pub const Data = struct { │ │67 user_id: ?[]const u8 = null, │ │68 // a transient outcome to surface from the last /login POST attempt │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │70 current_page: RoutablePage = .default, │ │71 // whether to render the ANSI art backdrop │ │72 enable_ansi: bool = false, │ │73 + }; │ │74 + │ │75 + // a user-initiated state change. widgets enqueue these on the session during │ │76 + // input instead of mutating state or touching the DB themselves; the host │ │77 + // drains them each frame (see applyAndWritePending / applyPending). this keeps DB│ │78 + // side effects out of the widget tree and gives every render path one place to │ │79 + // turn a UI action into a state change + event. │ │80 + pub const Action = union(enum) { │ │81 + toggle_ansi, │ │82 }; │ │83 │ │84 pub fn init( │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │69 - comptime repo_opts: rp.RepoOpts(.xit), │ │85 arena: *std.heap.ArenaAllocator, │ │71 - repo: *rp.Repo(.xit, repo_opts), │ │86 + repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │87 data: Data, │ │73 - ) !Session { │ │74 - var session = Session{ │ │88 + ) !Self { │ │89 + var session = Self{ │ │90 .data = data, │ │91 .arena = arena, │ │77 - .haxy_moment = try evt.currentMoment(repo_opts, repo), │ │92 + .haxy_moment = try evt.currentMoment(evt.admin_repo_opts, repo),│ │93 }; │ │94 try session.loadUserPrefs(); │ │95 return session; │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 96 } │ │ 97 │ │ 98 // load the logged-in user's persisted preferences from the db │ │ 84 - pub fn loadUserPrefs(self: *Session) !void { │ │ 99 + pub fn loadUserPrefs(self: *Self) !void { │ │100 const user_id = self.data.user_id orelse return; │ │101 const moment = self.haxy_moment orelse return; │ │ 87 - if (try evt.User.readById(DB, canonical_repo_opts.hash, moment, self.arena, user_id)) |user| { │ │102 + if (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, self.arena, user_id)) |user| {│ │103 self.data.enable_ansi = user.enable_ansi; │ │104 } │ │105 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │106 │ │107 // queue an action for the host to drain this frame. │ │ 93 - pub fn push(self: *Session, action: Action) !void { │ │108 + pub fn push(self: *Self, action: Action) !void { │ │109 try self.pending.append(self.arena.allocator(), action);│ │110 } │ │111 │ ╰─────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │112 // apply a single action's in-memory effect only (no persistence). │ │ 98 - pub fn apply(self: *Session, action: Action) void { │ │113 + fn apply(self: *Self, action: Action) void { │ │114 switch (action) { │ │115 .toggle_ansi => self.data.enable_ansi = !self.data.enable_ansi,│ │116 } │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────╮ │117 } │ │118 │ │104 - // drain queued actions, applying only their in-memory effect. used on the │ │105 - // wasm path, which has no repo to persist to (and only anonymous toggles │ │106 - // reach it anyway). │ │107 - pub fn applyPending(self: *Session) void { │ │119 + // drain the session's queued actions by applying them to the session. │ │120 + // used on the wasm path, which has no repo to persist to. │ │121 + pub fn applyPending(self: *Self) void { │ │122 for (self.pending.items) |action| self.apply(action); │ │123 self.pending.clearRetainingCapacity(); │ │110 - } │ │111 -}; │ │112 - │ │113 -// a user-initiated state change. widgets enqueue these on the Session during │ │114 -// input instead of mutating state or touching the DB themselves; the host │ │115 -// drains them each frame (see dispatchPending / applyPending). this keeps DB │ │116 -// side effects out of the widget tree and gives every render path one place to │ │117 -// turn a UI action into a state change + event. │ │118 -pub const Action = union(enum) { │ │119 - toggle_ansi, │ │120 - │ │121 - // apply this action and, when it maps to persistent state and the session │ │122 - // is logged in, emit the corresponding event. the single place that knows │ │123 - // how a UI action becomes a DB write. │ │124 - pub fn dispatch( │ │125 - self: Action, │ │126 - comptime repo_opts: rp.RepoOpts(.xit), │ │127 - io: std.Io, │ │128 - allocator: std.mem.Allocator, │ │129 - repo: *rp.Repo(.xit, repo_opts), │ │130 - session: *Session, │ │131 - ) !void { │ │132 - session.apply(self); │ │133 - switch (self) { │ │134 - .toggle_ansi => if (session.data.user_id) |user_id| { │ │135 - try evt.User.toggleAnsi(repo_opts, io, allocator, repo, user_id); │ │136 - }, │ │137 - } │ │124 } │ │125 │ │140 - // drain the session's queued actions through dispatch. always clears the │ │141 - // queue, even if a dispatch fails, so a single bad write isn't retried │ │142 - // every frame. │ │143 - pub fn dispatchPending( │ │144 - comptime repo_opts: rp.RepoOpts(.xit), │ │126 + // drain the session's queued actions by applying them to the session and │ │127 + // writing them to the db │ │128 + pub fn applyAndWritePending( │ │129 + self: *Self, │ │130 io: std.Io, │ │131 allocator: std.mem.Allocator, │ │147 - repo: *rp.Repo(.xit, repo_opts), │ │148 - session: *Session, │ │132 + repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │133 ) !void { │ │150 - defer session.pending.clearRetainingCapacity(); │ │151 - for (session.pending.items) |action| { │ │152 - try action.dispatch(repo_opts, io, allocator, repo, session); │ │134 + defer self.pending.clearRetainingCapacity(); │ │135 + for (self.pending.items) |action| { │ │136 + self.apply(action); │ │137 + switch (action) { │ │138 + .toggle_ansi => if (self.data.user_id) |user_id| { │ │139 + try evt.User.toggleAnsi(evt.admin_repo_opts, io, allocator, repo, user_id);│ │140 + }, │ │141 + } │ │142 } │ │143 } │ │144 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯