commit 34d09bf786776f2e1bdc91cddf3c41aae11439d7
parent a64232b210714ac96724eef3165db3aac6857cd6
Author: Sylvia Ivory <git@sivory.net>
Date: Thu, 5 Feb 2026 14:59:24 -0800
Add basic Publisher
Diffstat:
2 files changed, 130 insertions(+), 0 deletions(-)
diff --git a/shared/pubsub.zig b/shared/pubsub.zig
@@ -0,0 +1,127 @@
+const std = @import("std");
+
+const ListenerFn = *const fn (*const anyopaque) void;
+const Listener = struct {
+ callback: ListenerFn,
+ node: std.SinglyLinkedList.Node,
+};
+
+fn validate_listener_fn(comptime ptr: anytype) type {
+ const P = @TypeOf(ptr);
+ const ptr_info = @typeInfo(P);
+ if (ptr_info != .pointer) {
+ @compileError("expected *fn, found " ++ @typeName(ptr_info));
+ }
+ const L = ptr_info.pointer.child;
+
+ const listener_info = @typeInfo(L);
+ if (listener_info != .@"fn") {
+ @compileError("expected *fn, found " ++ @typeName(P));
+ }
+
+ const listener_fn = listener_info.@"fn";
+ if (listener_fn.params.len != 1) {
+ @compileError("expected 1 parameter, found " ++ listener_fn.params.len);
+ }
+ const msg_type = listener_fn.params[0].type orelse unreachable;
+ const msg_type_info = @typeInfo(msg_type);
+ if (msg_type_info != .pointer) {
+ @compileError("expected *const, found " ++ @typeName(msg_type));
+ }
+ if (!msg_type_info.pointer.is_const) {
+ @compileError("expected *const, found " ++ @typeName(msg_type));
+ }
+
+ // Questionable legality
+ // return @ptrCast(ptr);
+ return msg_type;
+}
+
+// pub fn create_listener_static(comptime ptr: anytype) Listener {
+// return .{
+// .node = undefined,
+// .callback = @ptrCast(ptr),
+// .callback_fn_param = validate_listener_fn(ptr),
+// };
+// }
+
+pub fn Publisher(comptime M: type, max_listeners: comptime_int) type {
+ const msg_info = @typeInfo(M);
+ if (msg_info != .@"union") {
+ @compileError("expected tagged union, found " ++ @typeName(M));
+ }
+
+ const msg_union = msg_info.@"union";
+ const msg_tag_type = msg_union.tag_type orelse @compileError("expected tagged union, found untagged union");
+
+ const Slot = struct { []const u8, std.SinglyLinkedList };
+
+ var slots: [msg_union.fields.len]Slot = undefined;
+
+ comptime for (msg_union.fields, 0..) |field, index| {
+ slots[index] = .{ field.name, undefined };
+ };
+
+ const event_map: std.StaticStringMap(std.SinglyLinkedList) = .initComptime(slots);
+
+ return struct {
+ const Self = @This();
+
+ listeners: std.StaticStringMap(std.SinglyLinkedList),
+ buffer: [max_listeners * @sizeOf(Listener)]u8,
+ fba: std.heap.FixedBufferAllocator,
+
+ pub fn init() Self {
+ const buffer = undefined;
+
+ return .{
+ .listeners = event_map,
+ .buffer = buffer,
+ .fba = .init(buffer),
+ };
+ }
+
+ pub fn listen(self: *Self, comptime event: msg_tag_type, comptime listener: anytype) !void {
+ comptime if (max_listeners == 0) @compileError("must use listen_alloc on this Publisher");
+
+ try self.listen_alloc(self.fba.allocator(), event, listener);
+ }
+
+ // TODO; this leaks
+ pub fn listen_alloc(self: *Self, gpa: std.mem.Allocator, comptime event: msg_tag_type, comptime listener: anytype) !void {
+ comptime {
+ const param_type = validate_listener_fn(listener);
+ for (msg_union.fields) |f| {
+ if (std.mem.eql(u8, f.name, @tagName(event))) {
+ // We know that param_type is a pointer so its safe to index into
+ if (f.type != @typeInfo(param_type).pointer.child) {
+ @compileError("expected " ++ @typeName(f.type) ++ ", found " ++ @typeName(param_type));
+ }
+
+ break;
+ }
+ }
+ }
+
+ var node = try gpa.create(Listener);
+ node.callback = @ptrCast(listener);
+
+ const name = @tagName(event);
+ var ev = self.listeners.get(name) orelse unreachable;
+
+ ev.prepend(&node.node);
+ }
+
+ pub fn publish(self: *Self, comptime event: msg_tag_type, data: anytype) void {
+ const name = @tagName(event);
+ const ev = self.listeners.get(name) orelse unreachable;
+
+ var head = ev.first;
+ while (head) |node| {
+ const listener: *Listener = @fieldParentPtr("node", node);
+ listener.callback(@ptrCast(&data));
+ head = node.next;
+ }
+ }
+ };
+}
diff --git a/shared/root.zig b/shared/root.zig
@@ -1,4 +1,7 @@
pub const bootloader_protocol = @import("./net.zig");
pub const lists = @import("./lists.zig");
+pub const pubsub = @import("./pubsub.zig");
pub const StackRingBuffer = lists.StackRingBuffer;
+
+pub const Publisher = pubsub.Publisher;