This commit is contained in:
Bob Farrell 2025-04-22 19:42:20 +01:00
parent 9847efdf4a
commit d3b3ae63cf
15 changed files with 557 additions and 160 deletions

View File

@ -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",

116
demo/public/party.css Normal file
View File

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

47
demo/public/party.js Normal file
View File

@ -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 = '&#127881;';
// 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);
}
}

View File

@ -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();
} 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);
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;
}
}
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"));

View File

@ -1,12 +1,14 @@
<script>
const channel = {
websocket: null,
callbacks: [],
onStateChanged: function(callback) { this.callbacks.push(callback); },
publish: function(path, data) {
stateChangedCallbacks: [],
messageCallbacks: [],
onStateChanged: function(callback) { this.stateChangedCallbacks.push(callback); },
onMessage: function(callback) { this.messageCallbacks.push(callback); },
publish: function(data) {
if (this.websocket) {
const json = JSON.stringify(data);
this.websocket.send(`${path}:${json}`);
this.websocket.send(json);
}
},
};
@ -15,12 +17,21 @@
@if (context.request) |request|
@if (request.headers.get("host")) |host|
<script>
channel.websocket = new WebSocket('ws://{{host}}');
channel.websocket = new WebSocket('ws://{{host}}{{request.path.base_path}}');
channel.websocket.addEventListener("message", (event) => {
const state = JSON.parse(event.data);
channel.callbacks.forEach((callback) => {
const state_tag = "__jetzig_channel_state__:";
if (event.data.startsWith(state_tag)) {
const state = JSON.parse(event.data.slice(state_tag.length));
channel.stateChangedCallbacks.forEach((callback) => {
callback(state);
});
} else {
const data = JSON.parse(event.data);
channel.messageCallbacks.forEach((callback) => {
callback(data);
});
}
});
@// channel.websocket.addEventListener("open", (event) => {
@// // TODO
@ -30,49 +41,68 @@
@end
@end
<style>
#tic-tac-toe-grid td {
min-width: 5rem;
width: 5rem;
height: 5rem;
border: 1px dotted black;
font-size: 3rem;
font-family: monospace;
}
</style>
<div id="results-wrapper">
<span class="trophy">&#127942;</span>
<div id="results">
<div>Player</div>
<div id="player-wins"></div>
<div>CPU</div>
<div id="cpu-wins"></div>
<div>Tie</div>
<div id="ties"></div>
</div>
<span class="trophy">&#127942;</span>
</div>
<table id="tic-tac-toe-grid">
<tbody>
<tr>
<td id="tic-tac-toe-cell-1" data-cell="1"></td>
<td id="tic-tac-toe-cell-2" data-cell="2"></td>
<td id="tic-tac-toe-cell-3" data-cell="3"></td>
</tr>
<tr>
<td id="tic-tac-toe-cell-4" data-cell="4"></td>
<td id="tic-tac-toe-cell-5" data-cell="5"></td>
<td id="tic-tac-toe-cell-6" data-cell="6"></td>
</tr>
<tr>
<td id="tic-tac-toe-cell-7" data-cell="7"></td>
<td id="tic-tac-toe-cell-8" data-cell="8"></td>
<td id="tic-tac-toe-cell-9" data-cell="9"></td>
</tr>
</tbody>
</table>
<div id="party-container"></div>
<div class="board" id="board">
<div class="cell" id="tic-tac-toe-cell-0" data-cell="0"></div>
<div class="cell" id="tic-tac-toe-cell-1" data-cell="1"></div>
<div class="cell" id="tic-tac-toe-cell-2" data-cell="2"></div>
<div class="cell" id="tic-tac-toe-cell-3" data-cell="3"></div>
<div class="cell" id="tic-tac-toe-cell-4" data-cell="4"></div>
<div class="cell" id="tic-tac-toe-cell-5" data-cell="5"></div>
<div class="cell" id="tic-tac-toe-cell-6" data-cell="6"></div>
<div class="cell" id="tic-tac-toe-cell-7" data-cell="7"></div>
<div class="cell" id="tic-tac-toe-cell-8" data-cell="8"></div>
</div>
<button id="reset-button">Reset Game</button>
<script src="/party.js"></script>
<link rel="stylesheet" href="/party.css" />
<script>
channel.onStateChanged(state => {
console.log(state);
Object.entries(state.cells).forEach(([cell, toggle]) => {
document.querySelector("#player-wins").innerText = state.results.player;
document.querySelector("#cpu-wins").innerText = state.results.cpu;
document.querySelector("#ties").innerText = state.results.ties;
if (state.winner) {
triggerPartyAnimation();
}
Object.entries(state.cells).forEach(([cell, state]) => {
const element = document.querySelector(`#tic-tac-toe-cell-${cell}`);
element.innerHTML = toggle ? "&#9992;" : "&#129422;"
element.innerHTML = { player: "&#9992;", cpu: "&#129422;" }[state] || "";
});
});
document.querySelectorAll("#tic-tac-toe-grid td").forEach(element => {
channel.onMessage(message => {
if (message.err) {
console.log(message.err);
}
});
document.querySelector("#reset-button").addEventListener("click", () => {
channel.publish({ reset: true });
});
document.querySelectorAll("#board div.cell").forEach(element => {
element.addEventListener("click", () => {
channel.publish("websockets", { toggle: element.dataset.cell });
channel.publish({ cell: parseInt(element.dataset.cell) });
});
});
</script>

View File

@ -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

View File

@ -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 => {},
}
}

View File

@ -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");

View File

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

View File

@ -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");
}

View File

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

View File

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

View File

@ -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,
},
);
}

View File

@ -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)) {

View File

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