Skip to content

WebSockets

KickJS's @forinda/kickjs-ws: a gateway in a module's .gateways, at /ws + its path. Frames are JSON { "event": …, "data": … }; a handler's data is decoded like a request body.

pub const ChatGateway = struct {
    presence: *Presence, // injected like any service

    pub const path = "/chat"; // ws://host/ws/chat
    pub const State = struct { name: []const u8 = "" }; // per socket
    const Ctx = kzig.ws.Ctx(State);
    pub const events = .{
        .{ .event = "message:send", .handler = "send" },
        .{ .event = "*", .handler = "unknown" },
    };

    pub fn connect(self: *ChatGateway, ctx: *Ctx) !void {
        const user = try self.verify(try ctx.query("token")) orelse return ctx.close(4401, "Unauthorized");
        ctx.state.name = try ctx.conn_arena.dupe(u8, user.name);
    }

    pub fn send(_: *ChatGateway, ctx: *Ctx, data: struct { room: []const u8, text: []const u8 }) !void {
        if (!ctx.inRoom(data.room)) return ctx.send("error", .{ .message = "join it first" });
        try ctx.to(data.room).send("message", .{ .from = ctx.state.name, .text = data.text });
    }
};

ws.Ctx has send, close, join, leave, rooms, to(room).send, broadcast, broadcastAll, and the upgrade request's header and query. Optional hooks: connect, disconnect, draining (on shutdown, before every socket closes with 1001). The App's .ws = .{ .path, .heartbeat_ms, .max_payload, .write_timeout_ms } sets the mount, the ping interval (a socket that misses a pong is dropped), the message limit (1009 over it) and how long a write may wait on a client that stopped reading before its socket is cut. Both runtimes serve gateways with the same loop (http.zig hands the socket over after the handshake). task-api's chat module is the archive's ws-chat without Redis.