From a7f47a4137598ad293169191ca001aa4ef70f005 Mon Sep 17 00:00:00 2001 From: "Nurul Huda (Apon)" Date: Mon, 22 Jun 2026 13:49:53 +0600 Subject: [PATCH] refactor: cleanup event handler dispatcher --- pkg/ziex/src/wasm/index.ts | 4 +- src/runtime/client/window/extern.zig | 42 +++----- src/runtime/core/EventHandler.zig | 15 ++- src/runtime/server/dispatch.zig | 138 ++++++++++----------------- 4 files changed, 78 insertions(+), 121 deletions(-) diff --git a/pkg/ziex/src/wasm/index.ts b/pkg/ziex/src/wasm/index.ts index 3cec761e..cbd3b711 100644 --- a/pkg/ziex/src/wasm/index.ts +++ b/pkg/ziex/src/wasm/index.ts @@ -83,7 +83,7 @@ export class ZxBridge extends ZxBridgeCore { fetch(window.location.href, { method: 'POST', // `>>> 0` reinterprets the i32 from wasm as u32 (ids can exceed 2^31). - headers: { 'X-ZX-Action': String(actionId >>> 0) }, + headers: { 'x-action': String(actionId >>> 0) }, body: formData, }) .then(async (response) => { @@ -366,7 +366,7 @@ export class ZxBridge extends ZxBridgeCore { fetch(window.location.href, { method: 'POST', // `>>> 0` reinterprets the i32 from wasm as u32 (ids can exceed 2^31). - headers: { 'X-ZX-Action': String(actionId >>> 0) }, + headers: { 'x-action': String(actionId >>> 0) }, body: formData, }).catch(() => {}); }, diff --git a/src/runtime/client/window/extern.zig b/src/runtime/client/window/extern.zig index 598d86bb..ae197324 100644 --- a/src/runtime/client/window/extern.zig +++ b/src/runtime/client/window/extern.zig @@ -1,20 +1,14 @@ -/// Create an HTML/SVG element by tag enum id, register it in the JS domNodes -/// registry under vnode_id, and set __zx_ref = vnode_id on the element. -/// Returns the jsz ref (needed to construct the root HTMLElement for CommentMarker). +// DOM +/// Create element pub extern "__zx" fn _ce(id: usize, vnode_id: u64) u64; -/// Create a text node with the given content, register it in the JS domNodes -/// registry under vnode_id, and set __zx_ref = vnode_id on the node. +/// Create text pub extern "__zx" fn _ct(ptr: [*]const u8, len: usize, vnode_id: u64) u64; -// Attribute / property mutation - /// setAttribute on the element identified by vnode_id. pub extern "__zx" fn _sa(vnode_id: u64, name_ptr: [*]const u8, name_len: usize, val_ptr: [*]const u8, val_len: usize) void; -/// Set a DOM property (not attribute) on the element identified by vnode_id. -/// Used for properties like checked, value, selected, muted where -/// setAttribute does not reflect the current state after user interaction. +/// Set property pub extern "__zx" fn _sp(vnode_id: u64, name_ptr: [*]const u8, name_len: usize, val_ptr: [*]const u8, val_len: usize) void; /// removeAttribute on the element identified by vnode_id. @@ -23,24 +17,19 @@ pub extern "__zx" fn _ra(vnode_id: u64, name_ptr: [*]const u8, name_len: usize) /// Set nodeValue on the text node identified by vnode_id. pub extern "__zx" fn _snv(vnode_id: u64, ptr: [*]const u8, len: usize) void; -/// Set innerHTML (raw, unescaped HTML) on the element identified by vnode_id. -/// Used for elements with @escaping={.none}. +/// Set innerHTML pub extern "__zx" fn _srh(vnode_id: u64, ptr: [*]const u8, len: usize) void; -// DOM tree mutation - -/// parent.appendChild(child) - both nodes looked up by vnode_id. +/// parent.appendChild(child) pub extern "__zx" fn _ac(parent_id: u64, child_id: u64) void; -/// parent.insertBefore(child, ref) - all nodes looked up by vnode_id. +/// parent.insertBefore(child, ref) pub extern "__zx" fn _ib(parent_id: u64, child_id: u64, ref_id: u64) void; -/// parent.removeChild(child) - looked up by vnode_id. -/// Also recursively removes all descendants from the JS domNodes registry. +/// parent.removeChild(child) pub extern "__zx" fn _rc(parent_id: u64, child_id: u64) void; -/// parent.replaceChild(new_child, old_child) - looked up by vnode_id. -/// Also removes old_child subtree from the JS domNodes registry. +/// parent.replaceChild(new_child, old_child) pub extern "__zx" fn _rpc(parent_id: u64, new_id: u64, old_id: u64) void; // Async / timer @@ -57,20 +46,13 @@ pub extern "__zx" fn _wsClose(ws_id: u64, code: u16, reason_ptr: [*]const u8, re /// Write window.location.href into buf. Returns the number of bytes written. pub extern "__zx" fn _getLocationHref(buf: [*]u8, buf_len: usize) usize; -/// Serialize the form data of the form DOM element identified by vnode_id as a -/// URL-encoded string (application/x-www-form-urlencoded). Returns the number -/// of bytes written to buf (0 if the element is not a form or not found). +/// Get serialized form data pub extern "__zx" fn _getFormData(vnode_id: u64, buf_ptr: [*]u8, buf_len: usize) usize; -/// Submit a form action: reads the DOM form identified by vnode_id, builds a -/// multipart/form-data request with the X-ZX-Action header set to action_id, -/// and fires it via the JS fetch API. The browser handles multipart -/// serialization (including file inputs) so no WASM-side encoding is required. +/// Submits a form with action identity pub extern "__zx" fn _submitFormAction(vnode_id: u64, action_id: u32) void; -/// Like _submitFormAction but stateful: injects the serialised bound-state JSON -/// as a `__$states` multipart field and calls __zx_fetch_complete(fetch_id,…) -/// when the response arrives so WASM can apply the returned state updates. +/// Submits a form with component states in __$states field pub extern "__zx" fn _submitFormActionAsync( vnode_id: u64, action_id: u32, diff --git a/src/runtime/core/EventHandler.zig b/src/runtime/core/EventHandler.zig index b81fa69e..3aeb6d66 100644 --- a/src/runtime/core/EventHandler.zig +++ b/src/runtime/core/EventHandler.zig @@ -10,6 +10,13 @@ fn getGlobalAllocator() std.mem.Allocator { return zx.allocator; } +pub const Constants = struct { + pub const action_form_name = "__$action"; + pub const states_form_name = "__$states"; + pub const action_header_name = "x-action"; + pub const event_header_name = "x-event"; +}; + pub const Bound = struct { state_ptr: *anyopaque, /// Serialize the current state value to its positional JSON representation. @@ -394,7 +401,7 @@ pub fn actionHandler(ctx: *anyopaque, event: zx.client.Event) void { const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{handler_id}) catch "0"; const headers = [_]CoreFetch.RequestInit.Header{ .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "X-ZX-Action", .value = id_str }, + .{ .name = Constants.action_header_name, .value = id_str }, }; if (bound_states.len > 0) { @@ -432,7 +439,7 @@ pub fn actionHandler(ctx: *anyopaque, event: zx.client.Event) void { } fn eventHandler(ctx: *anyopaque, event: zx.client.Event) void { - if (!is_wasm) return; + if (comptime (!is_wasm)) return; event.preventDefault(); const client_fetch = @import("../client/fetch.zig"); @@ -458,9 +465,11 @@ fn eventHandler(ctx: *anyopaque, event: zx.client.Event) void { state_jsons.append(getGlobalAllocator(), json) catch {}; } + var id_buf: [16]u8 = undefined; + const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{handler_id}) catch "0"; const headers = [_]CoreFetch.RequestInit.Header{ .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "X-ZX-Server-Event", .value = "1" }, + .{ .name = Constants.event_header_name, .value = id_str }, }; const payload = Payload{ diff --git a/src/runtime/server/dispatch.zig b/src/runtime/server/dispatch.zig index ad50c6ac..deb04b45 100644 --- a/src/runtime/server/dispatch.zig +++ b/src/runtime/server/dispatch.zig @@ -3,6 +3,9 @@ const zx = @import("../../root.zig"); const registry = @import("registry.zig"); const render = @import("render.zig"); +const EventHandler = zx.EventHandler; +const Constants = EventHandler.Constants; + pub const PageFn = *const fn (zx.PageContext, ?*const anyopaque, ?*const anyopaque) anyerror!zx.Component; pub const DispatchResult = union(enum) { @@ -18,33 +21,27 @@ pub const DispatchResult = union(enum) { page_error: anyerror, }; -/// Returns true if the request is a server action request. -/// Checks for the x-zx-action header (JS fetch) or __$action in the form body (no-JS form POST). -pub fn isActionRequest(request: zx.server.Request) bool { - if (request.headers.has("x-zx-action")) return true; - const body = request.text() orelse return false; - const ct = request.headers.get("content-type") orelse ""; - return std.mem.indexOf(u8, body, "__$action=") != null or - (std.mem.indexOf(u8, ct, "multipart/form-data") != null and - std.mem.indexOf(u8, body, "name=\"__$action\"") != null); -} - -fn parseActionId(request: zx.server.Request) u32 { +fn parseActionId(request: zx.server.Request) !u32 { const content_type = request.headers.get("content-type") orelse ""; - if (std.mem.indexOf(u8, content_type, "multipart/form-data") != null) { - if (request.multiFormData().getValue("__$action")) |raw| { - return std.fmt.parseInt(u32, raw, 10) catch 1; - } - } else if (request.formData().get("__$action")) |raw| { - return std.fmt.parseInt(u32, raw, 10) catch 1; - } + const is_multipart = std.mem.indexOf(u8, content_type, "multipart/form-data") != null; - // Fall back to the header (JS fetches with no form body). - if (request.headers.get("x-zx-action")) |raw| { - return std.fmt.parseInt(u32, raw, 10) catch 1; - } + const action_id = blk: { + // Multi-part Form + if (is_multipart) if (request.multiFormData().getValue(Constants.action_form_name)) |raw| + break :blk try std.fmt.parseInt(u32, raw, 10); - return 1; + // Form + if (request.formData().get(Constants.action_form_name)) |raw| + break :blk try std.fmt.parseInt(u32, raw, 10); + + // Client Form + if (request.headers.get(Constants.action_header_name)) |raw| + break :blk try std.fmt.parseInt(u32, raw, 10); + + return error.NotServerAction; + }; + + return action_id; } fn serializeStateOutputs(sc: anytype, allocator: std.mem.Allocator) !?[]u8 { @@ -53,6 +50,8 @@ fn serializeStateOutputs(sc: anytype, allocator: std.mem.Allocator) !?[]u8 { return aw.written(); } +/// it is used to register the action handler in cases where they are not in the memory, +/// this is always true for statless hosting platform like Cloudflare worker fn slowPathRender( page_fn: PageFn, pagectx: zx.PageContext, @@ -85,61 +84,39 @@ pub fn dispatchAction( state_ptr: ?*const anyopaque, base_path: ?[]const u8, ) !DispatchResult { - if (!isActionRequest(request)) return .not_triggered; + const action_id = parseActionId(request) catch return .not_triggered; + const is_progressive = request.headers.has(Constants.action_header_name); + const mfd = request.multiFormData(); - const is_js = request.headers.has("x-zx-action"); - const action_id = parseActionId(request); - - // TODO: cleanup - // const sr = request.multiFormData().getValue("__$states") orelse "null"; - - // std.debug.print("IsJS: {}\nStates: {s}", .{ is_js, sr }); - - // Parse state inputs for stateful actions. - // JS path (X-ZX-Action header): states are the entire JSON body. - // Form path (_submitFormActionAsync): states are in the __$states multipart field. - const action_inputs: ?[]const []const u8 = if (false) blk: { - const body_text = request.text() orelse break :blk null; - break :blk zx.util.zxon.parse([]const []const u8, arena, body_text, .{}) catch null; - } else blk: { - const states_raw = request.multiFormData().getValue("__$states") orelse break :blk null; - break :blk zx.util.zxon.parse([]const []const u8, arena, states_raw, .{}) catch null; + const action_states: ?[]const []const u8 = blk: { + break :blk zx.util.zxon.parse( + []const []const u8, + arena, + mfd.getValue(Constants.states_form_name) orelse break :blk null, + .{}, + ) catch null; }; - if (registry.get(route_path, action_id)) |action_fn| { - var action_ctx = zx.server.Action{ - .request = request, - .response = response, - .allocator = allocator, - .arena = arena, - ._internal = .{ .inputs = action_inputs }, - }; - action_fn(&action_ctx); - const body = if (action_ctx._internal.state_ctx) |sc| try serializeStateOutputs(sc, allocator) else null; - return if (is_js) .{ .ok = .{ .body = body } } else .ok_native; - } - - // Slow path: render the page to populate the registry, then retry. - if (page_fn) |pfn| { + var action_fn = registry.get(route_path, action_id); + if (action_fn == null) if (page_fn) |pfn| { if (slowPathRender(pfn, pagectx, route_path, arena, app_ptr, state_ptr, base_path)) |err| { return .{ .page_error = err }; } - } + }; + action_fn = registry.get(route_path, action_id); - if (registry.get(route_path, action_id)) |action_fn| { + if (action_fn) |af| { var action_ctx = zx.server.Action{ .request = request, .response = response, .allocator = allocator, .arena = arena, - ._internal = .{ .inputs = action_inputs }, + ._internal = .{ .inputs = action_states }, }; - action_fn(&action_ctx); + af(&action_ctx); const body = if (action_ctx._internal.state_ctx) |sc| try serializeStateOutputs(sc, allocator) else null; - return if (is_js) .{ .ok = .{ .body = body } } else .ok_native; - } - - return .not_found; + return if (is_progressive) .{ .ok = .{ .body = body } } else .ok_native; + } else return .not_found; } /// Dispatches a server event request. Performs a fast-path registry lookup and falls back @@ -156,38 +133,27 @@ pub fn dispatchServerEvent( state_ptr: ?*const anyopaque, base_path: ?[]const u8, ) !DispatchResult { - if (!request.headers.has("x-zx-server-event")) return .not_triggered; - + if (!request.headers.has(Constants.event_header_name)) return .not_triggered; const payload = zx.util.zxon.parse(zx.EventHandler.Payload, arena, request.text() orelse return .not_found, .{}) catch return .not_found; - if (registry.getEvent(route_path, payload.handler_id)) |event_fn| { - var event_ctx = zx.server.Event{ - .allocator = allocator, - .arena = arena, - ._internal = .{ .payload = payload }, - }; - event_fn(&event_ctx); - const body = if (event_ctx._internal.state_ctx) |sc| try serializeStateOutputs(sc, allocator) else null; - return .{ .ok = .{ .body = body } }; - } - - // Slow path: render the page to populate the registry, then retry. - if (page_fn) |pfn| { + // TODO: get the handler_id from header instead, currently the id in header is not accurate + const handler_id = payload.handler_id; + var event_fn = registry.getEvent(route_path, handler_id); + if (event_fn == null) if (page_fn) |pfn| { if (slowPathRender(pfn, pagectx, route_path, arena, app_ptr, state_ptr, base_path)) |err| { return .{ .page_error = err }; } - } + }; + event_fn = registry.getEvent(route_path, handler_id); - if (registry.getEvent(route_path, payload.handler_id)) |event_fn| { + if (event_fn) |ef| { var event_ctx = zx.server.Event{ .allocator = allocator, .arena = arena, ._internal = .{ .payload = payload }, }; - event_fn(&event_ctx); + ef(&event_ctx); const body = if (event_ctx._internal.state_ctx) |sc| try serializeStateOutputs(sc, allocator) else null; return .{ .ok = .{ .body = body } }; - } - - return .not_found; + } else return .not_found; }