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; }; }