diff --git a/examples/example_dusty.zig b/examples/example_dusty.zig index 90bb228..3aa9b52 100644 --- a/examples/example_dusty.zig +++ b/examples/example_dusty.zig @@ -254,10 +254,10 @@ const snippets = [_][]const u8{ fn executeScript(req: *dusty.Request, res: *dusty.Response) !void { const sample = paramInt(u8, req, "sample") orelse 0; - var attribs = datastar.ScriptAttributes.init(req.arena); - try attribs.put("type", "text/javascript"); - try attribs.put("trace", "true"); - try attribs.put("aardvark", "should appear last, not first"); + var attribs: datastar.ScriptAttributes = .empty; + try attribs.put(req.arena, "type", "text/javascript"); + try attribs.put(req.arena, "trace", "true"); + try attribs.put(req.arena, "aardvark", "should appear last, not first"); try beginSseBatch(res); res.body = switch (sample) { diff --git a/examples/example_httpz.zig b/examples/example_httpz.zig index 68c3526..d5a7681 100644 --- a/examples/example_httpz.zig +++ b/examples/example_httpz.zig @@ -276,10 +276,10 @@ const snippets = [_][]const u8{ fn executeScript(req: *httpz.Request, res: *httpz.Response) !void { const sample = paramInt(u8, req, "sample") orelse 0; - var attribs = datastar.ScriptAttributes.init(res.arena); - try attribs.put("type", "text/javascript"); - try attribs.put("trace", "true"); - try attribs.put("aardvark", "should appear last, not first"); + var attribs: datastar.ScriptAttributes = .empty; + try attribs.put(res.arena, "type", "text/javascript"); + try attribs.put(res.arena, "trace", "true"); + try attribs.put(res.arena, "aardvark", "should appear last, not first"); beginSse(res); res.body = switch (sample) { diff --git a/examples/example_stdlib.zig b/examples/example_stdlib.zig index 4639c0a..ebb4523 100644 --- a/examples/example_stdlib.zig +++ b/examples/example_stdlib.zig @@ -11,20 +11,14 @@ const PORT = 8081; pub const std_options = std.Options{ .log_level = .debug }; -var update_count: usize = 1; -var update_mutex: Io.Mutex = .init; +var update_count: std.atomic.Value(usize) = .init(1); var prng: std.Random.DefaultPrng = .init(0); var shared_io: Io = undefined; -fn getCountAndIncrement(io: Io) !usize { - try update_mutex.lock(io); - defer { - update_count += 1; - update_mutex.unlock(io); - } - return update_count; +fn getCountAndIncrement() usize { + return update_count.fetchAdd(1, .acq_rel); } var hotreload_id: u64 = 0; @@ -183,9 +177,9 @@ fn respondJson(arena: std.mem.Allocator, request: *std.http.Server.Request, valu }, }, }); - defer body.end() catch {}; const jf = std.json.fmt(value, .{}); try jf.format(&body.writer); + try body.end(); } // ----- Handlers ----- @@ -193,7 +187,7 @@ fn respondJson(arena: std.mem.Allocator, request: *std.http.Server.Request, valu fn textHtml(arena: std.mem.Allocator, request: *std.http.Server.Request) !void { const body = try std.fmt.allocPrint(arena, \\
This is update number {d}
- , .{try getCountAndIncrement(shared_io)}); + , .{getCountAndIncrement()}); try request.respond(body, .{ .extra_headers = &.{.{ .name = "content-type", .value = "text/html; charset=UTF-8" }}, }); @@ -202,7 +196,7 @@ fn textHtml(arena: std.mem.Allocator, request: *std.http.Server.Request) !void { fn patchElements(arena: std.mem.Allocator, request: *std.http.Server.Request) !void { const block = try datastar.patchElementsFmt(arena, \\This is update number {d}
- , .{try getCountAndIncrement(shared_io)}, .{}); + , .{getCountAndIncrement()}, .{}); try respondSse(arena, request, block); } @@ -226,7 +220,7 @@ fn patchElementsOpts(arena: std.mem.Allocator, request: *std.http.Server.Request , opts), else => try datastar.patchElementsFmt(arena, \\This is update number {d}
- , .{try getCountAndIncrement(shared_io)}, opts), + , .{getCountAndIncrement()}, opts), }; try respondSse(arena, request, body); } @@ -282,10 +276,10 @@ fn patchSignalsRemove(arena: std.mem.Allocator, request: *std.http.Server.Reques } fn executeScript(arena: std.mem.Allocator, request: *std.http.Server.Request, sample: u8) !void { - var attribs = datastar.ScriptAttributes.init(arena); - try attribs.put("type", "text/javascript"); - try attribs.put("trace", "true"); - try attribs.put("aardvark", "should appear last, not first"); + var attribs: datastar.ScriptAttributes = .empty; + try attribs.put(arena, "type", "text/javascript"); + try attribs.put(arena, "trace", "true"); + try attribs.put(arena, "aardvark", "should appear last, not first"); const body = switch (sample) { 1 => try datastar.executeScript( @@ -316,9 +310,8 @@ fn executeScript(arena: std.mem.Allocator, request: *std.http.Server.Request, sa // ----- Long-lived streaming SSE ----- -fn beginStream(request: *std.http.Server.Request) !std.http.BodyWriter { - var buf: [4096]u8 = undefined; - var body = try request.respondStreaming(&buf, .{ +fn beginStream(buffer: []u8, request: *std.http.Server.Request) !std.http.BodyWriter { + var body = try request.respondStreaming(buffer, .{ .respond_options = .{ .extra_headers = &.{ .{ .name = "content-type", .value = "text/event-stream; charset=UTF-8" }, @@ -333,8 +326,8 @@ fn beginStream(request: *std.http.Server.Request) !std.http.BodyWriter { fn svgMorph(arena: std.mem.Allocator, request: *std.http.Server.Request) !void { const opt = try datastar.readSignals(struct { svgMorph: usize = 1 }, arena, request); - var body = try beginStream(request); - defer body.end() catch {}; + var body_buffer: [4096]u8 = undefined; + var body = try beginStream(&body_buffer, request); var frame_buf: [4096]u8 = undefined; var fba: std.heap.FixedBufferAllocator = .init(&frame_buf); @@ -359,6 +352,8 @@ fn svgMorph(arena: std.mem.Allocator, request: *std.http.Server.Request) !void { }); try shared_io.sleep(.fromMilliseconds(100), .real); } + + try body.end(); } fn emitSvgFrame(body: *std.http.BodyWriter, fba: *std.heap.FixedBufferAllocator, comptime fmt: []const u8, args: anytype) !void { @@ -391,8 +386,8 @@ fn mathMorph(arena: std.mem.Allocator, request: *std.http.Server.Request) !void return; } - var stream = try beginStream(request); - defer stream.end() catch {}; + var stream_buffer: [4096]u8 = undefined; + var stream = try beginStream(&stream_buffer, request); var frame_buf: [4096]u8 = undefined; var fba: std.heap.FixedBufferAllocator = .init(&frame_buf); @@ -417,8 +412,7 @@ fn mathMorph(arena: std.mem.Allocator, request: *std.http.Server.Request) !void fba.reset(); const reset_block = try datastar.patchSignals(fba.allocator(), .{ .mathmlMorph = 1 }, .{}); try stream.writer.writeAll(reset_block); - try stream.writer.flush(); - try stream.flush(); + try stream.end(); } const snippets = [_][]const u8{ @@ -469,8 +463,8 @@ fn mimeTest(arena: std.mem.Allocator, request: *std.http.Server.Request, filenam } fn hotreload(request: *std.http.Server.Request, id: u64) !void { - var stream = try beginStream(request); - defer stream.end() catch {}; + var stream_buffer: [4096]u8 = undefined; + var stream = try beginStream(&stream_buffer, request); var frame_buf: [1024]u8 = undefined; var fba: std.heap.FixedBufferAllocator = .init(&frame_buf); @@ -498,4 +492,6 @@ fn hotreload(request: *std.http.Server.Request, id: u64) !void { try stream.writer.flush(); try stream.flush(); } + + try stream.end(); } diff --git a/hello_world/main.zig b/hello_world/main.zig index cbdc492..758cb55 100644 --- a/hello_world/main.zig +++ b/hello_world/main.zig @@ -101,7 +101,6 @@ fn streamHello(io: Io, arena: std.mem.Allocator, request: *std.http.Server.Reque }, }, }); - defer body.end() catch {}; try body.flush(); // push headers to the wire before first SSE event for (0..MESSAGE.len) |i| { @@ -118,4 +117,5 @@ fn streamHello(io: Io, arena: std.mem.Allocator, request: *std.http.Server.Reque } } std.debug.print("\n", .{}); + try body.end(); } diff --git a/src/datastar.zig b/src/datastar.zig index 02e0c52..70e5092 100644 --- a/src/datastar.zig +++ b/src/datastar.zig @@ -3,9 +3,9 @@ const Io = std.Io; const Allocator = std.mem.Allocator; pub const Command = enum { - patchElements, - patchSignals, - executeScript, + patch_elements, + patch_signals, + execute_script, }; pub const PatchMode = enum { @@ -41,49 +41,17 @@ pub const PatchSignalsOptions = struct { retry_duration: ?i64 = null, }; -pub const ScriptAttributes = struct { - const Map = std.array_hash_map.String([]const u8); - - map: Map = .empty, - allocator: Allocator, - - pub fn init(allocator: Allocator) ScriptAttributes { - return .{ .allocator = allocator }; - } - - pub fn deinit(self: *ScriptAttributes) void { - self.map.deinit(self.allocator); - } - - pub fn put(self: *ScriptAttributes, key: []const u8, value: []const u8) !void { - try self.map.put(self.allocator, key, value); - } - - pub fn count(self: ScriptAttributes) usize { - return self.map.count(); - } - - pub fn get(self: ScriptAttributes, key: []const u8) ?[]const u8 { - return self.map.get(key); - } - - pub fn keys(self: ScriptAttributes) [][]const u8 { - return self.map.keys(); - } - - pub fn values(self: ScriptAttributes) [][]const u8 { - return self.map.values(); - } -}; +pub const ScriptAttributes = std.array_hash_map.String([]const u8); pub const ExecuteScriptOptions = struct { - auto_remove: bool = true, // by default remove the script after use, otherwise explicity set this to false if you want to keep the script loaded + /// by default remove the script after use, otherwise explicity set this to false if you want to keep the script loaded + auto_remove: bool = true, attributes: ?ScriptAttributes = null, event_id: ?[]const u8 = null, retry_duration: ?i64 = null, }; -pub const DEFAULT_BUFFER_SIZE = 8 * 1024; +pub const default_buffer_size = 8 * 1024; // Framework-agnostic transformer functions. // @@ -95,8 +63,8 @@ pub const DEFAULT_BUFFER_SIZE = 8 * 1024; pub fn patchElements(arena: Allocator, elements: []const u8, opt: PatchElementsOptions) ![]const u8 { var buf: Io.Writer.Allocating = .init(arena); - var msg: Message = .{}; - msg.init(.patchElements, opt, &buf.writer); + var msg_buffer: [default_buffer_size]u8 = undefined; + var msg: Message = .patchElements(opt, &msg_buffer, &buf.writer); try msg.header(); try msg.interface.writeAll(elements); try msg.end(); @@ -105,8 +73,8 @@ pub fn patchElements(arena: Allocator, elements: []const u8, opt: PatchElementsO pub fn patchElementsFmt(arena: Allocator, comptime elements: []const u8, args: anytype, opt: PatchElementsOptions) ![]const u8 { var buf: Io.Writer.Allocating = .init(arena); - var msg: Message = .{}; - msg.init(.patchElements, opt, &buf.writer); + var msg_buffer: [default_buffer_size]u8 = undefined; + var msg: Message = .patchElements(opt, &msg_buffer, &buf.writer); try msg.header(); try msg.interface.print(elements, args); try msg.end(); @@ -115,8 +83,8 @@ pub fn patchElementsFmt(arena: Allocator, comptime elements: []const u8, args: a pub fn patchSignals(arena: Allocator, signals: anytype, opt: PatchSignalsOptions) ![]const u8 { var buf: Io.Writer.Allocating = .init(arena); - var msg: Message = .{}; - msg.init(.patchSignals, opt, &buf.writer); + var msg_buffer: [default_buffer_size]u8 = undefined; + var msg: Message = .patchSignals(opt, &msg_buffer, &buf.writer); try msg.header(); const json_formatter = std.json.fmt(signals, .{}); try json_formatter.format(&msg.interface); @@ -126,8 +94,8 @@ pub fn patchSignals(arena: Allocator, signals: anytype, opt: PatchSignalsOptions pub fn executeScript(arena: Allocator, script: []const u8, opt: ExecuteScriptOptions) ![]const u8 { var buf: Io.Writer.Allocating = .init(arena); - var msg: Message = .{}; - msg.init(.executeScript, opt, &buf.writer); + var msg_buffer: [default_buffer_size]u8 = undefined; + var msg: Message = .executeScript(opt, &msg_buffer, &buf.writer); try msg.header(); try msg.interface.writeAll(script); try msg.end(); @@ -136,8 +104,9 @@ pub fn executeScript(arena: Allocator, script: []const u8, opt: ExecuteScriptOpt pub fn executeScriptFmt(arena: Allocator, comptime script: []const u8, args: anytype, opt: ExecuteScriptOptions) ![]const u8 { var buf: Io.Writer.Allocating = .init(arena); - var msg: Message = .{}; - msg.init(.executeScript, opt, &buf.writer); + var msg_buffer: [default_buffer_size]u8 = undefined; + var msg: Message = .executeScript(opt, &msg_buffer, &buf.writer); + try msg.header(); try msg.interface.print(script, args); try msg.end(); @@ -185,64 +154,71 @@ pub fn readSignals(comptime T: type, arena: std.mem.Allocator, req: *std.http.Se } } -fn CommandOptions(comptime command: Command) type { - return switch (command) { - .patchElements => PatchElementsOptions, - .patchSignals => PatchSignalsOptions, - .executeScript => ExecuteScriptOptions, - }; -} +const CommandOptions = union(Command) { + patch_elements: PatchElementsOptions, + patch_signals: PatchSignalsOptions, + execute_script: ExecuteScriptOptions, +}; pub const Message = struct { - out_buffer: *Io.Writer = undefined, // an intermediate buffer to write the expanded Datastar event stream to - input_buffer: [8 * 1024]u8 = undefined, - started: bool = false, - command: Command = .patchElements, - - patch_element_options: PatchElementsOptions = .{}, - patch_signal_options: PatchSignalsOptions = .{}, - execute_script_options: ExecuteScriptOptions = .{}, - - line_in_progress: bool = false, - interface: Io.Writer = undefined, - - fn init(m: *Message, comptime command: Command, opt: CommandOptions(command), out_buffer: *Io.Writer) void { - m.out_buffer = out_buffer; - m.command = command; - m.interface = .{ - .buffer = &m.input_buffer, - .vtable = &.{ - .drain = &drain, + /// buffer to write the expanded Datastar event stream to + out_writer: *Io.Writer, + started: bool, + + command: CommandOptions, + + line_in_progress: bool, + interface: Io.Writer, + + pub fn patchElements(options: PatchElementsOptions, input_buffer: []u8, out_writer: *Io.Writer) Message { + return .{ + .started = false, + .line_in_progress = false, + .out_writer = out_writer, + .command = .{ .patch_elements = options }, + .interface = .{ + .buffer = input_buffer, + .vtable = &.{ + .drain = &drain, + }, }, }; - switch (command) { - .patchElements => { - m.patch_element_options = opt; - }, - .patchSignals => { - m.patch_signal_options = opt; + } + + pub fn patchSignals(options: PatchSignalsOptions, input_buffer: []u8, out_writer: *Io.Writer) Message { + return .{ + .started = false, + .line_in_progress = false, + .out_writer = out_writer, + .command = .{ .patch_signals = options }, + .interface = .{ + .buffer = input_buffer, + .vtable = &.{ + .drain = &drain, + }, }, - .executeScript => { - m.execute_script_options = opt; + }; + } + + pub fn executeScript(options: ExecuteScriptOptions, input_buffer: []u8, out_writer: *Io.Writer) Message { + return .{ + .started = false, + .line_in_progress = false, + .out_writer = out_writer, + .command = .{ .execute_script = options }, + .interface = .{ + .buffer = input_buffer, + .vtable = &.{ + .drain = &drain, + }, }, - } + }; } - pub fn swapTo(self: *Message, comptime command: Command, opt: CommandOptions(command)) void { - // always just swap to new command - self.end() catch {}; + pub fn swapTo(self: *Message, command: CommandOptions) !void { + // can't always just swap to new command because it would swallow cancelation error. + try self.end(); self.command = command; - switch (command) { - .patchElements => { - self.patch_element_options = opt; - }, - .patchSignals => { - self.patch_signal_options = opt; - }, - .executeScript => { - self.execute_script_options = opt; - }, - } } pub fn end(self: *Message) !void { @@ -254,11 +230,11 @@ pub const Message = struct { self.line_in_progress = false; // const w = self.stream_writer; - const w = self.out_buffer; + const w = self.out_writer; switch (self.command) { else => {}, - .executeScript => { + .execute_script => { // need to close off the script tag !! try w.writeAll(""); }, @@ -270,66 +246,66 @@ pub const Message = struct { pub fn header(self: *Message) !void { // var w = self.stream_writer; - var w = self.out_buffer; + var w = self.out_writer; switch (self.command) { - .patchElements => { + .patch_elements => |*patch_elements| { try w.writeAll("event: datastar-patch-elements\n"); - if (self.patch_element_options.event_id) |event_id| { + if (patch_elements.event_id) |event_id| { try w.print("id: {s}\n", .{event_id}); } - if (self.patch_element_options.retry_duration) |retry| { + if (patch_elements.retry_duration) |retry| { try w.print("retry: {}\n", .{retry}); } - if (self.patch_element_options.selector) |s| { + if (patch_elements.selector) |s| { try w.print("data: selector {s}\n", .{s}); } - if (self.patch_element_options.view_transition) { + if (patch_elements.view_transition) { try w.print("data: useViewTransition true\n", .{}); } - if (self.patch_element_options.view_transition_selector) |s| { + if (patch_elements.view_transition_selector) |s| { try w.print("data: viewTransitionSelector {s}\n", .{s}); } - const mt = self.patch_element_options.mode; + const mt = patch_elements.mode; switch (mt) { .outer => {}, else => try w.print("data: mode {t}\n", .{mt}), } - switch (self.patch_element_options.namespace) { + switch (patch_elements.namespace) { .html => {}, .svg => try w.writeAll("data: namespace svg\n"), .mathml => try w.writeAll("data: namespace mathml\n"), } }, - .patchSignals => { + .patch_signals => |patch_signals| { try w.writeAll("event: datastar-patch-signals\n"); - if (self.patch_signal_options.event_id) |event_id| { + if (patch_signals.event_id) |event_id| { try w.print("id: {s}\n", .{event_id}); } - if (self.patch_signal_options.retry_duration) |retry| { + if (patch_signals.retry_duration) |retry| { try w.print("retry: {}\n", .{retry}); } - if (self.patch_signal_options.only_if_missing) { + if (patch_signals.only_if_missing) { try w.writeAll("data: onlyIfMissing true\n"); } }, - .executeScript => { + .execute_script => |execute_script| { try w.writeAll("event: datastar-patch-elements\n"); - if (self.execute_script_options.event_id) |event_id| { + if (execute_script.event_id) |event_id| { try w.print("id: {s}\n", .{event_id}); } - if (self.execute_script_options.retry_duration) |retry| { + if (execute_script.retry_duration) |retry| { try w.print("retry: {}\n", .{retry}); } try w.writeAll("data: mode append\ndata: selector body\ndata: elements