diff --git a/build.zig.zon b/build.zig.zon
index 0061b20..290a42e 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -25,8 +25,8 @@
.hash = "jetkv-0.0.0-zCv0fmCGAgCyYqwHjk0P5KrYVRew1MJAtbtAcIO-WPpT",
},
.zmpl = .{
- .url = "https://github.com/jetzig-framework/zmpl/archive/c57fc9b83027e8c1459d9625c3509f59f0fb89f3.tar.gz",
- .hash = "zmpl-0.0.1-SYFGBgdqAwDeA6xm4KAhpKoNrWs5CMQK6x447zhWclCs",
+ .url = "https://github.com/jetzig-framework/zmpl/archive/cfbbc1263c4c62fa91579280c08c5a935c579563.tar.gz",
+ .hash = "zmpl-0.0.1-SYFGBmJsAwCUsj-noN2QEWHY1paouyj0naGNQ2uTIcYw",
},
.httpz = .{
.url = "https://github.com/karlseguin/http.zig/archive/37d7cb9819b804ade5f4b974b82f8dd0622225ed.tar.gz",
diff --git a/demo/public/party.css b/demo/public/party.css
new file mode 100644
index 0000000..9f3c769
--- /dev/null
+++ b/demo/public/party.css
@@ -0,0 +1,116 @@
+body {
+ font-family: Arial, sans-serif;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background-color: #f0f0f0;
+ overflow-x: hidden;
+ position: relative;
+ padding-top: 5rem;
+}
+#board {
+ display: grid;
+ grid-template-columns: repeat(3, 100px);
+ grid-gap: 5px;
+ margin: 20px;
+}
+.cell {
+ width: 100px;
+ height: 100px;
+ background: white;
+ border: 2px solid #333;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 40px;
+ cursor: pointer;
+}
+.cell:hover {
+ background: #e0e0e0;
+}
+#status {
+ font-size: 24px;
+ margin-bottom: 20px;
+}
+#reset-button {
+ padding: 10px 20px;
+ font-size: 16px;
+ cursor: pointer;
+ background-color: #4CAF50;
+ color: white;
+ border: none;
+ border-radius: 5px;
+}
+#reset-button:hover {
+ background-color: #45a049;
+}
+#party-container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 10;
+}
+.animal {
+ position: absolute;
+ font-size: 50px;
+ opacity: 0;
+}
+.run-dog {
+ animation: runAcross 2s linear forwards;
+}
+.run-cat {
+ animation: runAcross 2s linear forwards;
+}
+.run-lizard {
+ animation: runAcross 2s linear forwards;
+}
+.run-jet {
+ animation: runAcross 2s linear forwards;
+}
+@keyframes runAcross {
+ 0% {
+ left: -100px;
+ opacity: 1;
+ }
+ 100% {
+ left: 100%;
+ opacity: 1;
+ }
+}
+.confetti {
+ position: absolute;
+ font-size: 30px;
+ opacity: 0.8;
+}
+.fall {
+ animation: fall 3s linear forwards;
+}
+@keyframes fall {
+ 0% {
+ top: -50px;
+ opacity: 1;
+ transform: rotate(0deg);
+ }
+ 100% {
+ top: 100%;
+ opacity: 0.5;
+ transform: rotate(360deg);
+ }
+}
+#results {
+ display: grid;
+ grid-template-columns: repeat(2, 5rem);
+ text-align: center;
+ font-weight: bold;
+ font-family: monospace;
+}
+#results-wrapper {
+ display: flex;
+ justify-content: space-between;
+}
+.trophy {
+ font-size: 5rem;
+}
diff --git a/demo/public/party.js b/demo/public/party.js
new file mode 100644
index 0000000..5b6a6b5
--- /dev/null
+++ b/demo/public/party.js
@@ -0,0 +1,47 @@
+function triggerPartyAnimation() {
+ const container = document.getElementById('party-container');
+ container.innerHTML = ''; // Clear previous animations
+
+ // Define entities
+ const entities = [
+ { type: 'dog', emoji: '🐶' },
+ { type: 'cat', emoji: '🐱' },
+ { type: 'lizard', emoji: '🦎' },
+ { type: 'jet', emoji: '✈' }
+ ];
+
+ // Create random number of each entity (2-5 per type)
+ entities.forEach(entity => {
+ const count = Math.floor(Math.random() * 4) + 2; // Random 2-5
+ for (let i = 0; i < count; i++) {
+ const div = document.createElement('div');
+ div.className = 'animal';
+ div.innerHTML = entity.emoji;
+ // Random vertical position (between 20% and 80% of screen height)
+ div.style.top = `${20 + Math.random() * 60}%`;
+ // Random delay (0 to 1.5s)
+ div.style.animationDelay = `${Math.random() * 1.5}s`;
+ container.appendChild(div);
+ // Trigger animation
+ setTimeout(() => {
+ div.classList.add(`run-${entity.type}`);
+ }, 10);
+ }
+ });
+
+ // Create confetti (20 pieces)
+ for (let i = 0; i < 20; i++) {
+ const div = document.createElement('div');
+ div.className = 'confetti';
+ div.innerHTML = '🎉';
+ // Random horizontal position
+ div.style.left = `${Math.random() * 100}%`;
+ // Random delay (0 to 2s)
+ div.style.animationDelay = `${Math.random() * 2}s`;
+ container.appendChild(div);
+ // Trigger fall animation
+ setTimeout(() => {
+ div.classList.add('fall');
+ }, 10);
+ }
+}
diff --git a/demo/src/app/views/websockets.zig b/demo/src/app/views/websockets.zig
index 4194833..3aa1e4f 100644
--- a/demo/src/app/views/websockets.zig
+++ b/demo/src/app/views/websockets.zig
@@ -35,34 +35,143 @@ pub fn delete(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jet
return request.render(.ok);
}
-pub fn receiveMessage(message: jetzig.channels.Message) !void {
- const data = try message.data();
- if (data.getT(.string, "toggle")) |toggle| {
- if (message.channel.get("cells")) |cells| {
- const is_taken = cells.getT(.boolean, toggle);
- if (is_taken == null or is_taken.? == false) {
- try cells.put(toggle, true);
- }
- } else {
- var cells = try message.channel.put("cells", .object);
- for (1..10) |cell| {
- var buf: [1]u8 = undefined;
- const key = try std.fmt.bufPrint(&buf, "{d}", .{cell});
- try cells.put(key, std.mem.eql(u8, key, toggle));
+pub const Channel = struct {
+ pub fn open(channel: jetzig.channels.Channel) !void {
+ if (channel.get("cells") == null) try initGame(channel);
+ try channel.sync();
+ }
+
+ pub fn receive(message: jetzig.channels.Message) !void {
+ const value = try message.value();
+
+ if (value.getT(.boolean, "reset") == true) {
+ try resetGame(message.channel);
+ try message.channel.sync();
+ return;
+ }
+
+ const cell: usize = if (value.getT(.integer, "cell")) |integer|
+ @intCast(integer)
+ else
+ return;
+ const cells = message.channel.getT(.array, "cells") orelse return;
+ const items = cells.items();
+
+ var grid: [9]Game.State = undefined;
+ for (0..9) |id| {
+ if (items[id].* != .null) {
+ grid[id] = if (items[id].eql("player")) .player else .cpu;
+ } else {
+ grid[id] = .empty;
}
}
- try message.channel.sync();
- } else {
- var cells = try message.channel.put("cells", .object);
- for (1..10) |cell| {
- var buf: [1]u8 = undefined;
- const key = try std.fmt.bufPrint(&buf, "{d}", .{cell});
- try cells.put(key, false);
+
+ var game = Game{ .grid = grid };
+ game.evaluate();
+
+ if (game.winner != null) {
+ try message.channel.publish(.{ .err = "Game is already over." });
+ return;
}
+
+ if (game.movePlayer(cell)) {
+ items[cell] = message.data.string("player");
+ if (game.winner == null) {
+ items[game.moveCpu()] = message.data.string("cpu");
+ }
+ if (game.winner) |winner| {
+ try message.channel.put("winner", @tagName(winner));
+ var results = message.channel.getT(.object, "results") orelse return;
+ const count = results.getT(.integer, @tagName(winner)) orelse return;
+ try results.put(@tagName(winner), count + 1);
+ }
+ }
+
try message.channel.sync();
}
- // try message.channel.publish("hello");
-}
+
+ fn resetGame(channel: jetzig.channels.Channel) !void {
+ try channel.put("winner", null);
+ var cells = try channel.put("cells", .array);
+ for (0..9) |_| try cells.append(null);
+ }
+
+ fn initGame(channel: jetzig.channels.Channel) !void {
+ var results = try channel.put("results", .object);
+ try results.put("cpu", 0);
+ try results.put("player", 0);
+ try results.put("ties", 0);
+ try resetGame(channel);
+ }
+};
+
+const Game = struct {
+ grid: [9]State,
+ winner: ?State = null,
+
+ pub const State = enum { empty, player, cpu, tie };
+
+ pub fn movePlayer(game: *Game, cell: usize) bool {
+ if (cell >= game.grid.len) return false;
+ if (game.grid[cell] != .empty) return false;
+
+ game.grid[cell] = .player;
+ game.evaluate();
+ return true;
+ }
+
+ pub fn moveCpu(game: *Game) usize {
+ std.debug.assert(game.winner == null);
+ var available: [9]usize = undefined;
+ var available_len: usize = 0;
+ for (game.grid, 0..) |cell, cell_index| {
+ if (cell == .empty) {
+ available[available_len] = cell_index;
+ available_len += 1;
+ }
+ }
+ std.debug.assert(available_len > 0);
+ const choice = available[std.crypto.random.intRangeAtMost(usize, 0, available_len - 1)];
+ game.grid[choice] = .cpu;
+ game.evaluate();
+ return choice;
+ }
+
+ fn evaluate(game: *Game) void {
+ var full = true;
+ for (game.grid) |cell| {
+ if (cell == .empty) full = false;
+ }
+ if (full) {
+ game.winner = .tie;
+ return;
+ }
+
+ const patterns = [_][3]usize{
+ .{ 0, 1, 2 },
+ .{ 3, 4, 5 },
+ .{ 6, 7, 8 },
+ .{ 0, 3, 6 },
+ .{ 1, 4, 7 },
+ .{ 2, 5, 8 },
+ .{ 0, 4, 8 },
+ .{ 2, 4, 6 },
+ };
+ for (patterns) |pattern| {
+ var cpu_winner = true;
+ var player_winner = true;
+
+ for (pattern) |cell_index| {
+ if (game.grid[cell_index] != .cpu) cpu_winner = false;
+ if (game.grid[cell_index] != .player) player_winner = false;
+ }
+
+ std.debug.assert(!(cpu_winner and player_winner));
+ if (cpu_winner) game.winner = .cpu;
+ if (player_winner) game.winner = .player;
+ }
+ }
+};
test "index" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
diff --git a/demo/src/app/views/websockets/index.zmpl b/demo/src/app/views/websockets/index.zmpl
index 17f920c..55f869f 100644
--- a/demo/src/app/views/websockets/index.zmpl
+++ b/demo/src/app/views/websockets/index.zmpl
@@ -1,12 +1,14 @@
+
diff --git a/demo/src/main.zig b/demo/src/main.zig
index b306e70..c76ab16 100644
--- a/demo/src/main.zig
+++ b/demo/src/main.zig
@@ -7,6 +7,8 @@ const zmd = @import("zmd");
pub const routes = @import("routes");
pub const static = @import("static");
+pub const std_options = jetzig.std_options;
+
// Override default settings in `jetzig.config` here:
pub const jetzig_options = struct {
/// Middleware chain. Add any custom middleware here, or use middleware provided in
diff --git a/src/Routes.zig b/src/Routes.zig
index 243e64e..61cc13e 100644
--- a/src/Routes.zig
+++ b/src/Routes.zig
@@ -11,7 +11,7 @@ mailers_path: []const u8,
buffer: std.ArrayList(u8),
dynamic_routes: std.ArrayList(Function),
static_routes: std.ArrayList(Function),
-channel_routes: std.ArrayList(Function),
+channel_routes: std.ArrayList([]const u8),
module_paths: std.ArrayList([]const u8),
data: *jetzig.data.Data,
@@ -123,7 +123,7 @@ pub fn init(
.buffer = std.ArrayList(u8).init(allocator),
.static_routes = std.ArrayList(Function).init(allocator),
.dynamic_routes = std.ArrayList(Function).init(allocator),
- .channel_routes = std.ArrayList(Function).init(allocator),
+ .channel_routes = std.ArrayList([]const u8).init(allocator),
.module_paths = std.ArrayList([]const u8).init(allocator),
.data = data,
};
@@ -386,18 +386,19 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
const RouteSet = struct {
dynamic: []Function,
static: []Function,
- channel: []Function,
+ channel: [][]const u8,
};
fn writeChannelRoutes(self: *Routes, writer: anytype) !void {
- for (self.channel_routes.items) |route| {
- const module_path = try self.relativePathFrom(.root, route.path, .posix);
+ for (self.channel_routes.items) |path| {
+ const module_path = try self.relativePathFrom(.root, path, .posix);
defer self.allocator.free(module_path);
- const view_name = try route.viewName();
- defer self.allocator.free(view_name);
+ const relative_path = try self.relativePathFrom(.views, path, .posix);
+ defer self.allocator.free(relative_path);
+ const view_name = chompExtension(relative_path);
try writer.print(
- \\.{{ "{s}", jetzig.channels.Route{{ .receiveMessageFn = @import("{s}").receiveMessage }} }},
+ \\.{{ "{s}", jetzig.channels.Route.initComptime(@import("{s}")) }}
\\
, .{ view_name, module_path });
}
@@ -419,7 +420,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
var static_routes = std.ArrayList(Function).init(self.allocator);
var dynamic_routes = std.ArrayList(Function).init(self.allocator);
- var channel_routes = std.ArrayList(Function).init(self.allocator);
+ var channel_routes = std.ArrayList([]const u8).init(self.allocator);
var static_params: ?*jetzig.data.Value = null;
@@ -433,10 +434,6 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
source,
);
if (maybe_function) |*function| {
- if (std.mem.eql(u8, function.name, receive_message)) {
- try channel_routes.append(function.*);
- }
-
if (!std.mem.eql(u8, function.name, receive_message) and function.args.len == 0) {
std.debug.print(
"Expected at least 1 argument for view function `{s}` in `{s}`",
@@ -467,6 +464,27 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
static_params = self.data.value;
}
},
+ .container_decl_two,
+ .container_decl_two_trailing,
+ .container_decl,
+ .container_decl_trailing,
+ => |container_tag| {
+ var buf: [2]std.zig.Ast.Node.Index = undefined;
+ const container = switch (container_tag) {
+ .container_decl_two,
+ .container_decl_two_trailing,
+ => self.ast.containerDeclTwo(&buf, @enumFromInt(index)),
+ .container_decl,
+ .container_decl_trailing,
+ => self.ast.containerDecl(@enumFromInt(index)),
+ else => unreachable,
+ };
+ const container_token = container.ast.main_token;
+ const decl_name = self.ast.tokenSlice(container_token - 2);
+ if (std.mem.eql(u8, decl_name, "Channel")) {
+ try channel_routes.append(path);
+ }
+ },
else => {},
}
}
diff --git a/src/jetzig.zig b/src/jetzig.zig
index 5267b94..96fc2dc 100644
--- a/src/jetzig.zig
+++ b/src/jetzig.zig
@@ -39,6 +39,20 @@ pub const environment = @field(
@tagName(build_options.environment),
);
+pub fn logFn(
+ comptime level: std.log.Level,
+ comptime scope: @Type(.enum_literal),
+ comptime format: []const u8,
+ args: anytype,
+) void {
+ if (scope == .websocket) return; // We handle our own websocket event logging.
+ std.log.defaultLog(level, scope, format, args);
+}
+
+pub const std_options: std.Options = .{
+ .logFn = logFn,
+};
+
/// The primary interface for a Jetzig application. Create an `App` in your application's
/// `src/main.zig` and call `start` to launch the application.
pub const App = @import("jetzig/App.zig");
diff --git a/src/jetzig/channels/Channel.zig b/src/jetzig/channels/Channel.zig
index 6b35f0f..02f5993 100644
--- a/src/jetzig/channels/Channel.zig
+++ b/src/jetzig/channels/Channel.zig
@@ -6,29 +6,43 @@ const jetzig = @import("../../jetzig.zig");
const Channel = @This();
+allocator: std.mem.Allocator,
websocket: *jetzig.http.Websocket,
state: *jetzig.data.Value,
+data: *jetzig.data.Data,
-pub fn publish(self: Channel, data: []const u8) !void {
- try self.connection.write(data);
+pub fn publish(channel: Channel, data: anytype) !void {
+ var stack_fallback = std.heap.stackFallback(4096, channel.allocator);
+ const allocator = stack_fallback.get();
+
+ var write_buffer = channel.websocket.connection.writeBuffer(allocator, .text);
+ defer write_buffer.deinit();
+
+ const writer = write_buffer.writer();
+ try std.json.stringify(data, .{}, writer);
+ try write_buffer.flush();
}
pub fn getT(
- self: Channel,
+ channel: Channel,
comptime T: jetzig.data.Data.ValueType,
key: []const u8,
-) @TypeOf(self.state.getT(T, key)) {
- return self.state.getT(T, key);
+) @TypeOf(channel.state.getT(T, key)) {
+ return channel.state.getT(T, key);
}
-pub fn get(self: Channel, key: []const u8) ?*jetzig.data.Value {
- return self.state.get(key);
+pub fn get(channel: Channel, key: []const u8) ?*jetzig.data.Value {
+ return channel.state.get(key);
}
-pub fn put(self: Channel, key: []const u8, value: anytype) @TypeOf(self.state.put(key, value)) {
- return try self.state.put(key, value);
+pub fn put(
+ channel: Channel,
+ key: []const u8,
+ value: anytype,
+) @TypeOf(channel.state.put(key, value)) {
+ return try channel.state.put(key, value);
}
-pub fn sync(self: Channel) !void {
- try self.websocket.syncState(self);
+pub fn sync(channel: Channel) !void {
+ try channel.websocket.syncState(channel);
}
diff --git a/src/jetzig/channels/Message.zig b/src/jetzig/channels/Message.zig
index d729c84..5b55a9f 100644
--- a/src/jetzig/channels/Message.zig
+++ b/src/jetzig/channels/Message.zig
@@ -7,52 +7,31 @@ const Channel = @import("Channel.zig");
const Message = @This();
allocator: std.mem.Allocator,
-raw_data: []const u8,
-channel_name: ?[]const u8,
payload: []const u8,
+data: *jetzig.data.Data,
channel: Channel,
-pub fn init(allocator: std.mem.Allocator, channel: Channel, raw_data: []const u8) Message {
- const channel_name = parseChannelName(raw_data);
- const payload = parsePayload(raw_data, channel_name);
+pub fn init(allocator: std.mem.Allocator, channel: Channel, payload: []const u8) Message {
return .{
.allocator = allocator,
- .raw_data = raw_data,
.channel = channel,
- .channel_name = channel_name,
+ .data = channel.data,
.payload = payload,
};
}
-pub fn data(message: Message) !*jetzig.data.Value {
+pub fn value(message: Message) !*jetzig.data.Value {
var d = try message.allocator.create(jetzig.data.Data);
d.* = jetzig.data.Data.init(message.allocator);
try d.fromJson(message.payload);
return d.value.?;
}
-fn parseChannelName(raw_data: []const u8) ?[]const u8 {
- return if (std.mem.indexOfScalar(u8, raw_data, ':')) |index|
- if (index > 1) raw_data[0..index] else null
- else
- null;
-}
-
-fn parsePayload(raw_data: []const u8, maybe_channel_name: ?[]const u8) []const u8 {
- return if (maybe_channel_name) |channel_name|
- raw_data[channel_name.len + 1 ..]
- else
- raw_data;
-}
-
-test "message with channel and payload" {
- const message = Message.init("foo:bar");
- try std.testing.expectEqualStrings(message.channel_name.?, "foo");
- try std.testing.expectEqualStrings(message.payload, "bar");
-}
-
-test "message with payload only" {
- const message = Message.init("bar");
- try std.testing.expectEqual(message.channel_name, null);
- try std.testing.expectEqualStrings(message.payload, "bar");
+test "message with payload" {
+ const message = Message.init(
+ std.testing.allocator,
+ Channel{ .websocket = undefined, .state = undefined },
+ "foo",
+ );
+ try std.testing.expectEqualStrings(message.payload, "foo");
}
diff --git a/src/jetzig/channels/Route.zig b/src/jetzig/channels/Route.zig
index 64269ab..38f884b 100644
--- a/src/jetzig/channels/Route.zig
+++ b/src/jetzig/channels/Route.zig
@@ -2,8 +2,22 @@ const jetzig = @import("../../jetzig.zig");
const Route = @This();
-receiveMessageFn: *const fn (jetzig.channels.Message) anyerror!void,
+receiveMessageFn: ?*const fn (jetzig.channels.Message) anyerror!void = null,
+openConnectionFn: ?*const fn (jetzig.channels.Channel) anyerror!void = null,
pub fn receiveMessage(route: Route, message: jetzig.channels.Message) !void {
- try route.receiveMessageFn(message);
+ if (route.receiveMessageFn) |func| try func(message);
+}
+
+pub fn initComptime(T: type) Route {
+ comptime {
+ if (!@hasDecl(T, "Channel")) return .{};
+ const openConnectionFn = if (@hasDecl(T.Channel, "open")) T.Channel.open else null;
+ const receiveMessageFn = if (@hasDecl(T.Channel, "receive")) T.Channel.receive else null;
+
+ return .{
+ .openConnectionFn = openConnectionFn,
+ .receiveMessageFn = receiveMessageFn,
+ };
+ }
}
diff --git a/src/jetzig/http/Path.zig b/src/jetzig/http/Path.zig
index d774d2f..2e3f64d 100644
--- a/src/jetzig/http/Path.zig
+++ b/src/jetzig/http/Path.zig
@@ -13,6 +13,7 @@ path: []const u8,
base_path: []const u8,
directory: []const u8,
file_path: []const u8,
+view_name: []const u8,
resource_id: []const u8,
extension: ?[]const u8,
query: ?[]const u8,
@@ -29,6 +30,7 @@ pub fn init(path: []const u8) Path {
.base_path = base_path,
.directory = getDirectory(base_path),
.file_path = getFilePath(path),
+ .view_name = std.mem.trimLeft(u8, base_path, "/"),
.resource_id = getResourceId(base_path),
.extension = getExtension(path),
.query = getQuery(path),
@@ -414,3 +416,8 @@ test ".method (/foo/bar/1/_PATCH" {
const path = Path.init("/foo/bar/1/_PATCH");
try std.testing.expect(path.method.? == .PATCH);
}
+
+test ".view_name" {
+ const path = Path.init("/foo/bar");
+ try std.testing.expectEqualStrings("foo/bar", path.view_name);
+}
diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig
index c47f45b..ee09120 100644
--- a/src/jetzig/http/Server.zig
+++ b/src/jetzig/http/Server.zig
@@ -147,11 +147,6 @@ pub fn processNextRequest(
var repo = try self.repo.bindConnect(.{ .allocator = httpz_response.arena });
defer repo.release();
- if (try self.upgradeWebsocket(httpz_request, httpz_response)) {
- try self.logger.DEBUG("Websocket upgrade request successful.", .{});
- return;
- }
-
var response = try jetzig.http.Response.init(httpz_response.arena, httpz_response);
var request = try jetzig.http.Request.init(
httpz_response.arena,
@@ -163,6 +158,11 @@ pub fn processNextRequest(
&repo,
);
+ if (try self.upgradeWebsocket(httpz_request, httpz_response, &request)) {
+ try self.logger.DEBUG("Websocket upgrade request successful.", .{});
+ return;
+ }
+
try request.process();
var middleware_data = try jetzig.http.middleware.afterRequest(&request);
@@ -187,12 +187,29 @@ pub fn matchChannelRoute(self: *const Server, channel_name: []const u8) ?jetzig.
return self.channel_routes.get(channel_name);
}
-fn upgradeWebsocket(self: *const Server, httpz_request: *httpz.Request, httpz_response: *httpz.Response) !bool {
+fn upgradeWebsocket(
+ self: *const Server,
+ httpz_request: *httpz.Request,
+ httpz_response: *httpz.Response,
+ request: *jetzig.http.Request,
+) !bool {
+ const route = self.matchChannelRoute(request.path.view_name) orelse return false;
+ const session = try request.session();
+ const session_id = session.getT(.string, "_id") orelse {
+ try self.logger.ERROR("Error fetching session ID for websocket, aborting.", .{});
+ return false;
+ };
+
return try httpz.upgradeWebsocket(
jetzig.http.Websocket,
httpz_request,
httpz_response,
- jetzig.http.Websocket.Context{ .allocator = self.allocator, .server = self },
+ jetzig.http.Websocket.Context{
+ .allocator = self.allocator,
+ .route = route,
+ .session_id = session_id,
+ .server = self,
+ },
);
}
diff --git a/src/jetzig/http/Session.zig b/src/jetzig/http/Session.zig
index 384745b..d051133 100644
--- a/src/jetzig/http/Session.zig
+++ b/src/jetzig/http/Session.zig
@@ -12,6 +12,7 @@ cookie_name: []const u8,
initialized: bool = false,
data: jetzig.data.Data,
state: enum { parsed, pending } = .pending,
+id: [32]u8 = undefined,
const Self = @This();
@@ -48,7 +49,11 @@ pub fn parse(self: *Self) !void {
/// Reset session to an empty state.
pub fn reset(self: *Self) !void {
self.data.reset();
- _ = try self.data.object();
+ var object = try self.data.object();
+
+ _ = jetzig.util.generateRandomString(&self.id);
+ try object.put("_id", &self.id);
+
self.state = .parsed;
try self.save();
}
@@ -70,7 +75,7 @@ pub fn get(self: *Self, key: []const u8) ?*jetzig.data.Value {
/// Get a typed value from the session.
pub fn getT(
- self: *Self,
+ self: Self,
comptime T: jetzig.data.ValueType,
key: []const u8,
) @TypeOf(self.data.value.?.object.getT(T, key)) {
diff --git a/src/jetzig/http/Websocket.zig b/src/jetzig/http/Websocket.zig
index 2f17478..42ecbc0 100644
--- a/src/jetzig/http/Websocket.zig
+++ b/src/jetzig/http/Websocket.zig
@@ -6,6 +6,8 @@ const httpz = @import("httpz");
pub const Context = struct {
allocator: std.mem.Allocator,
+ route: jetzig.channels.Route,
+ session_id: []const u8,
server: *const jetzig.http.Server,
};
@@ -14,46 +16,69 @@ const Websocket = @This();
allocator: std.mem.Allocator,
connection: *httpz.websocket.Conn,
server: *const jetzig.http.Server,
+route: jetzig.channels.Route,
data: *jetzig.Data,
-id: [32]u8 = undefined,
+session_id: []const u8,
pub fn init(connection: *httpz.websocket.Conn, context: Context) !Websocket {
- var websocket = Websocket{
+ const data = try context.allocator.create(jetzig.Data);
+ data.* = jetzig.Data.init(context.allocator);
+
+ return Websocket{
.allocator = context.allocator,
.connection = connection,
+ .route = context.route,
+ .session_id = context.session_id,
.server = context.server,
- .data = try context.allocator.create(jetzig.Data),
+ .data = data,
};
- websocket.data.* = jetzig.Data.init(context.allocator);
- _ = jetzig.util.generateRandomString(&websocket.id);
-
- return websocket;
}
-pub fn clientMessage(self: *Websocket, data: []const u8) !void {
+pub fn afterInit(websocket: *Websocket, context: Context) !void {
+ _ = context;
+
+ const func = websocket.route.openConnectionFn orelse return;
+
const channel = jetzig.channels.Channel{
- .websocket = self,
- .state = try self.getState(),
+ .allocator = websocket.allocator,
+ .websocket = websocket,
+ .state = try websocket.getState(),
+ .data = websocket.data,
};
- const message = jetzig.channels.Message.init(self.allocator, channel, data);
-
- if (message.channel_name) |target_channel_name| {
- if (self.server.matchChannelRoute(target_channel_name)) |route| {
- try route.receiveMessage(message);
- } else try self.server.logger.WARN("Unrecognized channel: {s}", .{target_channel_name});
- } else try self.server.logger.WARN("Invalid channel message format.", .{});
+ try func(channel);
}
-pub fn syncState(self: *Websocket, channel: jetzig.channels.Channel) !void {
+pub fn clientMessage(websocket: *Websocket, allocator: std.mem.Allocator, data: []const u8) !void {
+ const channel = jetzig.channels.Channel{
+ .allocator = allocator,
+ .websocket = websocket,
+ .state = try websocket.getState(),
+ .data = websocket.data,
+ };
+ const message = jetzig.channels.Message.init(allocator, channel, data);
+
+ try websocket.route.receiveMessage(message);
+}
+
+pub fn syncState(websocket: *Websocket, channel: jetzig.channels.Channel) !void {
+ var stack_fallback = std.heap.stackFallback(4096, channel.allocator);
+ const allocator = stack_fallback.get();
+
+ var write_buffer = channel.websocket.connection.writeBuffer(allocator, .text);
+ defer write_buffer.deinit();
+
+ const writer = write_buffer.writer();
+
// TODO: Make this really fast.
- try self.server.channels.put(&self.id, channel.state);
- try self.connection.write(try self.data.toJson());
+ try websocket.server.channels.put(websocket.session_id, channel.state);
+ try writer.print("__jetzig_channel_state__:{s}", .{try websocket.data.toJson()});
+ try write_buffer.flush();
}
-fn getState(self: *Websocket) !*jetzig.data.Value {
- return try self.server.channels.get(self.data, &self.id) orelse blk: {
- const root = try self.data.root(.object);
- try self.server.channels.put(&self.id, root);
- break :blk try self.server.channels.get(self.data, &self.id) orelse error.JetzigInvalidChannel;
+pub fn getState(websocket: *Websocket) !*jetzig.data.Value {
+ return try websocket.server.channels.get(websocket.data, websocket.session_id) orelse blk: {
+ const root = try websocket.data.root(.object);
+ try websocket.server.channels.put(websocket.session_id, root);
+ break :blk try websocket.server.channels.get(websocket.data, websocket.session_id) orelse error.JetzigInvalidChannel;
};
}