Compare commits

..

No commits in common. "websockets" and "main" have entirely different histories.

47 changed files with 886 additions and 2596 deletions

View File

@ -12,22 +12,11 @@ const use_llvm_default = builtin.os.tag != .linux;
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
var jetzig_templates_path = std.ArrayList([]const u8).init(b.allocator);
try jetzig_templates_path.append("/");
var it = std.mem.splitSequence(
u8,
try b.path("src/jetzig/templates").getPath3(b, null).toString(b.allocator),
std.fs.path.sep_str,
);
while (it.next()) |segment| {
try jetzig_templates_path.append(segment);
}
const templates_paths = try zmpl_build.templatesPaths(
b.allocator,
&.{
.{ .prefix = "views", .path = &.{ "src", "app", "views" } },
.{ .prefix = "mailers", .path = &.{ "src", "app", "mailers" } },
.{ .prefix = "jetzig", .path = jetzig_templates_path.items },
},
);

View File

@ -25,9 +25,8 @@
.hash = "jetkv-0.0.0-zCv0fmCGAgCyYqwHjk0P5KrYVRew1MJAtbtAcIO-WPpT",
},
.zmpl = .{
// .url = "https://github.com/jetzig-framework/zmpl/archive/89ee0ce9b4c96c316cc0575266fb66c864f24a49.tar.gz",
// .hash = "zmpl-0.0.1-SYFGBtuNAwCj2YbqnoEJt3bk1iFIZjGK6JwMc72toZBR",
.path = "../zmpl",
.url = "https://github.com/jetzig-framework/zmpl/archive/c57fc9b83027e8c1459d9625c3509f59f0fb89f3.tar.gz",
.hash = "zmpl-0.0.1-SYFGBgdqAwDeA6xm4KAhpKoNrWs5CMQK6x447zhWclCs",
},
.httpz = .{
.url = "https://github.com/karlseguin/http.zig/archive/37d7cb9819b804ade5f4b974b82f8dd0622225ed.tar.gz",

View File

@ -1,139 +0,0 @@
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;
display: inline;
}
#reset-button:hover {
background-color: #45a049;
}
#reset-wrapper {
text-align: center;
}
#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;
}
#victor {
margin: 3rem;
font-size: 3.5rem;
text-align: center;
vertical-align: center;
}
#grid {
transition: background-color 0.5s ease;
}
.flash-animation {
animation: flashRed 0.5s ease;
}
@keyframes flashRed {
0% { background-color: #e55; }
100% { background-color: white; }
}

View File

@ -1,47 +0,0 @@
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

@ -1,83 +0,0 @@
const std = @import("std");
const jetzig = @import("jetzig");
grid: Grid,
victor: ?State = null,
pub const Grid = [9]State;
pub const State = enum { empty, player, cpu, tie };
const Game = @This();
pub fn gridFromValues(values: []*jetzig.data.Value) Grid {
var grid: [9]Game.State = undefined;
for (0..9) |id| {
if (values[id].* != .null) {
grid[id] = if (values[id].eql("player")) .player else .cpu;
} else {
grid[id] = .empty;
}
}
return grid;
}
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.victor == 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;
}
pub fn evaluate(game: *Game) void {
var full = true;
for (game.grid) |cell| {
if (cell == .empty) full = false;
}
if (full) {
game.victor = .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_victor = true;
var player_victor = true;
for (pattern) |cell_index| {
if (game.grid[cell_index] != .cpu) cpu_victor = false;
if (game.grid[cell_index] != .player) player_victor = false;
}
std.debug.assert(!(cpu_victor and player_victor));
if (cpu_victor) game.victor = .cpu;
if (player_victor) game.victor = .player;
}
}

View File

@ -1 +0,0 @@
<title>My Inertia App</title>

View File

@ -6,12 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/prism.css" />
{{context.middleware.renderHeader()}}
</head>
<body>
<main>{{zmpl.content}}</main>
<script src="/prism.js"></script>
{{context.middleware.renderFooter()}}
</body>
</html>

View File

@ -16,7 +16,7 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
}
pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
try request.logger.INFO("id: {s}", .{id});
try request.server.logger.INFO("id: {s}", .{id});
return request.render(.ok);
}

View File

@ -1,77 +0,0 @@
const std = @import("std");
const jetzig = @import("jetzig");
const Game = @import("../lib/Game.zig");
pub const layout = "application";
pub fn index(request: *jetzig.Request) !jetzig.View {
return request.render(.ok);
}
pub const Channel = struct {
pub fn open(channel: jetzig.channels.Channel) !void {
if (channel.get("cells") == null) try initGame(channel);
try channel.sync();
}
pub const Actions = struct {
pub fn move(channel: jetzig.channels.Channel, cell: usize) !void {
const cells = channel.getT(.array, "cells") orelse return;
const grid = Game.gridFromValues(cells.items());
var game = Game{ .grid = grid };
game.evaluate();
if (game.victor != null) {
try channel.invoke(.game_over, .{});
return;
} else {
try movePlayer(channel, &game, cells, cell);
try channel.sync();
}
}
pub fn reset(channel: jetzig.channels.Channel) !void {
try resetGame(channel);
try channel.sync();
}
};
fn resetGame(channel: jetzig.channels.Channel) !void {
try channel.put("victor", 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("tie", 0);
try resetGame(channel);
}
fn movePlayer(
channel: jetzig.channels.Channel,
game: *Game,
cells: *const jetzig.data.Array,
cell: usize,
) !void {
const values = cells.items();
if (game.movePlayer(cell)) {
values[cell] = channel.data.string("player");
if (game.victor == null) {
values[game.moveCpu()] = channel.data.string("cpu");
}
if (game.victor) |victor| try setVictor(channel, victor);
}
}
fn setVictor(channel: jetzig.channels.Channel, victor: Game.State) !void {
try channel.put("victor", @tagName(victor));
var results = channel.getT(.object, "results") orelse return;
const count = results.getT(.integer, @tagName(victor)) orelse return;
try results.put(@tagName(victor), count + 1);
try channel.invoke(.victor, .{ .type = @tagName(victor) });
}
};

View File

@ -1,64 +0,0 @@
<link rel="stylesheet" href="/party.css" />
<div id="party-container"></div>
<div id="results-wrapper">
<span class="trophy">&#127942;</span>
<div id="results">
<div>Player</div>
<div id="player-wins" jetzig-connect="$.results.player"></div>
<div>CPU</div>
<div id="cpu-wins" jetzig-connect="$.results.cpu"></div>
<div>Tie</div>
<div id="ties" jetzig-connect="$.results.tie"></div>
</div>
<span class="trophy">&#127942;</span>
</div>
<div class="board" id="board">
@for (0..9) |index| {
<div
class="cell"
jetzig-connect="$.cells.{{index}}"
jetzig-transform="{ player: '&#9992;&#65039;', cpu: '&#129422;', tie: '&#129309;' }[value] || ''"
jetzig-click="move"
id="tic-tac-toe-cell-{{index}}"
data-cell="{{index}}"
>
</div>
}
</div>
<div id="reset-wrapper">
<button jetzig-click="reset" id="reset-button">Reset Game</button>
</div>
<div jetzig-style="{ visibility: $.victor === null ? 'hidden' : 'visible' }" id="victor">
<span>&#127942;</span>
<span
jetzig-connect="$.victor"
jetzig-transform="{ player: '&#9992;&#65039;', cpu: '&#129422;', tie: '&#129309;' }[value] || ''"
></span>
<span>&#127942;</span>
</div>
<script>
@// jetzig.channel.onStateChanged(state => {
@// });
@//
@// jetzig.channel.onMessage(data => {
@// });
@//
@// jetzig.channel.receive("victor", data => {
@// triggerPartyAnimation();
@// });
@//
@// jetzig.channel.receive("game_over", data => {
@// const element = document.querySelector("#board");
@// element.classList.remove('flash-animation');
@// void element.offsetWidth;
@// element.classList.add('flash-animation');
@// });
@//
</script>

View File

@ -7,8 +7,6 @@ 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
@ -16,11 +14,9 @@ pub const jetzig_options = struct {
pub const middleware: []const type = &.{
// jetzig.middleware.AuthMiddleware,
// jetzig.middleware.AntiCsrfMiddleware,
jetzig.middleware.HtmxMiddleware,
jetzig.middleware.ChannelsMiddleware,
// jetzig.middleware.InertiaMiddleware,
// jetzig.middleware.CompressionMiddleware,
// @import("app/middleware/DemoMiddleware.zig"),
// jetzig.middleware.HtmxMiddleware,
// jetzig.middleware.CompressionMiddleware,
// @import("app/middleware/DemoMiddleware.zig"),
};
// Maximum bytes to allow in request body.

View File

@ -11,13 +11,9 @@ mailers_path: []const u8,
buffer: std.ArrayList(u8),
dynamic_routes: std.ArrayList(Function),
static_routes: std.ArrayList(Function),
channel_routes: std.ArrayList([]const u8),
channel_actions: std.StringHashMap(std.StringHashMap([]const []const u8)),
module_paths: std.StringHashMap(void),
module_paths: std.ArrayList([]const u8),
data: *jetzig.data.Data,
const receive_message = "receiveMessage";
const Routes = @This();
const Function = struct {
@ -124,9 +120,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([]const u8).init(allocator),
.channel_actions = std.StringHashMap(std.StringHashMap([]const []const u8)).init(allocator),
.module_paths = std.StringHashMap(void).init(allocator),
.module_paths = std.ArrayList([]const u8).init(allocator),
.data = data,
};
}
@ -136,7 +130,6 @@ pub fn deinit(self: *Routes) void {
self.buffer.deinit();
self.static_routes.deinit();
self.dynamic_routes.deinit();
self.channel_routes.deinit();
}
/// Generates the complete route set for the application
@ -144,7 +137,6 @@ pub fn generateRoutes(self: *Routes) ![]const u8 {
const writer = self.buffer.writer();
try writer.writeAll(
\\const std = @import("std");
\\const jetzig = @import("jetzig");
\\
\\pub const routes = [_]jetzig.Route{
@ -156,16 +148,6 @@ pub fn generateRoutes(self: *Routes) ![]const u8 {
\\
);
try writer.writeAll(
\\pub const channel_routes = std.StaticStringMap(jetzig.channels.Route).initComptime(.{
\\
);
try self.writeChannelRoutes(writer);
try writer.writeAll(
\\});
\\
);
try writer.writeAll(
\\
\\pub const mailers = [_]jetzig.MailerDefinition{
@ -189,29 +171,16 @@ pub fn generateRoutes(self: *Routes) ![]const u8 {
\\
);
try writer.writeAll(
\\
\\pub const View = struct { name: []const u8, module: type };
\\pub const views = std.StaticStringMap(View).initComptime(.{
\\
);
try self.writeViewsMap(writer);
try writer.writeAll(
\\});
\\
);
try writer.writeAll(
\\test {
\\
);
var it = self.module_paths.keyIterator();
while (it.next()) |module_path| {
for (self.module_paths.items) |module_path| {
try writer.print(
\\ _ = @import("{s}");
\\
, .{module_path.*});
, .{module_path});
}
try writer.writeAll(
@ -285,10 +254,6 @@ fn writeRoutes(self: *Routes, writer: anytype) !void {
for (view_routes.dynamic) |view_route| {
try self.dynamic_routes.append(view_route);
}
for (view_routes.channel) |view_route| {
try self.channel_routes.append(view_route);
}
}
std.sort.pdq(Function, self.static_routes.items, {}, Function.lessThanFn);
@ -378,7 +343,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
unreachable;
std.mem.replaceScalar(u8, module_path, '\\', '/');
try self.module_paths.put(try self.allocator.dupe(u8, module_path), {});
try self.module_paths.append(try self.allocator.dupe(u8, module_path));
var buf: [32]u8 = undefined;
const id = jetzig.util.generateVariableName(&buf);
@ -402,49 +367,8 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
const RouteSet = struct {
dynamic: []Function,
static: []Function,
channel: [][]const u8,
};
fn writeChannelRoutes(self: *Routes, writer: anytype) !void {
for (self.channel_routes.items) |path| {
const module_path = try self.relativePathFrom(.root, path, .posix);
defer self.allocator.free(module_path);
const relative_path = try self.relativePathFrom(.views, path, .posix);
defer self.allocator.free(relative_path);
const view_name = chompExtension(relative_path);
var actions_buf = std.ArrayList(u8).init(self.allocator);
const actions_writer = actions_buf.writer();
if (self.channel_actions.get(path)) |actions| {
var it = actions.iterator();
while (it.next()) |entry| {
try actions_writer.print(
\\.{{ .name = "{s}", .params = &.{{
, .{entry.key_ptr.*});
for (entry.value_ptr.*, 0..) |param, index| {
if (index == 0) continue; // Skip `self` argument.
try actions_writer.print(
\\.{{ .name = "{s}" }},
, .{param});
}
try actions_writer.writeAll("},},");
}
}
try writer.print(
\\.{{
\\ "{0s}",
\\ jetzig.channels.Route.initComptime(
\\ @import("{1s}"),
\\ "{0s}",
\\ &.{{{2s}}}
\\ ),
\\}}
\\
, .{ view_name, module_path, actions_buf.items });
}
}
fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !RouteSet {
const stat = try dir.statFile(path);
const source = try dir.readFileAllocOptions(
@ -461,37 +385,30 @@ 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([]const u8).init(self.allocator);
var static_params: ?*jetzig.data.Value = null;
for (self.ast.nodes.items(.tag), 0..) |tag, index| {
switch (tag) {
.fn_proto_multi, .fn_proto_one, .fn_proto_simple => |function_tag| {
var maybe_function = try self.parseFunction(
function_tag,
@enumFromInt(index),
path,
source,
);
if (maybe_function) |*function| {
if (!std.mem.eql(u8, function.name, receive_message) and function.args.len == 0) {
var function = try self.parseFunction(function_tag, @enumFromInt(index), path, source);
if (function) |*capture| {
if (capture.args.len == 0) {
std.debug.print(
"Expected at least 1 argument for view function `{s}` in `{s}`",
.{ function.name, path },
.{ capture.name, path },
);
return error.JetzigMissingViewArgument;
}
for (function.args, 0..) |arg, arg_index| {
for (capture.args, 0..) |arg, arg_index| {
if (std.mem.eql(u8, try arg.typeBasename(), "StaticRequest")) {
function.static = jetzig.build_options.build_static;
function.legacy = arg_index + 1 < function.args.len;
try static_routes.append(function.*);
capture.static = jetzig.build_options.build_static;
capture.legacy = arg_index + 1 < capture.args.len;
try static_routes.append(capture.*);
} else if (std.mem.eql(u8, try arg.typeBasename(), "Request")) {
function.static = false;
function.legacy = arg_index + 1 < function.args.len;
try dynamic_routes.append(function.*);
capture.static = false;
capture.legacy = arg_index + 1 < capture.args.len;
try dynamic_routes.append(capture.*);
}
}
}
@ -505,28 +422,6 @@ 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);
try self.parseChannel(container, path);
}
},
else => {},
}
}
@ -552,87 +447,9 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
return .{
.dynamic = dynamic_routes.items,
.static = static_routes.items,
.channel = channel_routes.items,
};
}
// Although we mostly evaluate channel routes at comptime, we need to parse the function
// signatures in `Actions` to get argument names (Zig only reflects the types).
fn parseChannel(self: *Routes, channel: std.zig.Ast.full.ContainerDecl, path: []const u8) !void {
for (channel.ast.members) |member| {
const tag = self.ast.nodeTag(member);
switch (tag) {
.simple_var_decl => {
const var_decl = self.ast.simpleVarDecl(member);
const var_name = self.ast.tokenSlice(self.ast.nodeMainToken(member) + 1);
if (std.mem.eql(u8, var_name, "Actions")) {
const init_node = var_decl.ast.init_node.unwrap() orelse continue;
switch (self.ast.nodeTag(init_node)) {
.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, init_node),
.container_decl,
.container_decl_trailing,
=> self.ast.containerDecl(init_node),
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, "Actions")) {
try self.parseChannelActions(container, path);
}
},
else => continue,
}
}
},
else => {},
}
}
}
fn parseChannelActions(self: *Routes, actions: std.zig.Ast.full.ContainerDecl, path: []const u8) !void {
for (actions.ast.members) |member| {
const tag = self.ast.nodes.items(.tag)[@intFromEnum(member)];
switch (tag) {
.fn_proto,
.fn_proto_multi,
.fn_proto_one,
.fn_proto_simple,
.fn_decl,
=> {
var buf: [1]std.zig.Ast.Node.Index = undefined;
const func = self.ast.fullFnProto(&buf, member).?;
const visib_token = func.visib_token orelse continue;
if (!std.mem.eql(u8, self.ast.tokenSlice(visib_token), "pub")) continue;
const func_name_token = func.name_token orelse continue;
const func_name = self.ast.tokenSlice(func_name_token);
var params_buf = std.ArrayList([]const u8).init(self.allocator);
var params_it = func.iterate(&self.ast);
while (params_it.next()) |param| {
try params_buf.append(
try self.allocator.dupe(u8, self.ast.tokenSlice(param.name_token.?)),
);
}
const result = try self.channel_actions.getOrPut(path);
if (!result.found_existing) {
result.value_ptr.* = std.StringHashMap([]const []const u8).init(self.allocator);
}
try result.value_ptr.put(try self.allocator.dupe(u8, func_name), try params_buf.toOwnedSlice());
},
else => {},
}
}
}
// Parse the `pub const static_params` definition and into a `jetzig.data.Value`.
fn parseStaticParamsDecl(self: *Routes, decl: std.zig.Ast.full.VarDecl, params: *jetzig.data.Value) !void {
const init_node = decl.ast.init_node.unwrap() orelse return;
@ -801,10 +618,6 @@ fn parseFunction(
var it = fn_proto.iterate(&self.ast);
while (it.next()) |arg| {
// We don't need to resolve args for `receiveMessage` as it only has one form (it was
// added after the removal of the `data` arg from view functions).
if (std.mem.eql(u8, receive_message, function_name)) continue;
if (arg.name_token) |arg_token| {
const arg_name = self.ast.tokenSlice(arg_token);
const node = self.ast.nodes.get(@intFromEnum(arg.type_expr.?));
@ -832,7 +645,7 @@ fn parseFunction(
fn parseTypeExpr(self: *Routes, node: std.zig.Ast.Node) ![]const u8 {
switch (node.tag) {
// Currently all expected params are pointers, keeping this here in case that changes in future:
.identifier => return self.ast.tokenSlice(@as(u32, @intCast(node.main_token))),
.identifier => {},
.ptr_type_aligned => {
var buf = std.ArrayList([]const u8).init(self.allocator);
defer buf.deinit();
@ -850,14 +663,6 @@ fn parseTypeExpr(self: *Routes, node: std.zig.Ast.Node) ![]const u8 {
else => {},
}
// TODO: Output source line
std.log.err(
"Unexpected token type `{s}` in `{s}`",
.{
@tagName(node.tag),
self.ast.tokenSlice(@as(u32, @intCast(node.main_token))),
},
);
return error.JetzigAstParserError;
}
@ -866,7 +671,7 @@ fn isActionFunctionName(name: []const u8) bool {
if (std.mem.eql(u8, field.name, name)) return true;
}
return std.mem.eql(u8, receive_message, name);
return false;
}
inline fn chompExtension(path: []const u8) []const u8 {
@ -998,18 +803,3 @@ fn writeJobs(self: Routes, writer: anytype) !void {
std.debug.print("[jetzig] Imported {} job(s)\n", .{count});
}
fn writeViewsMap(self: Routes, writer: anytype) !void {
var it = self.module_paths.keyIterator();
while (it.next()) |path| {
try writer.print(
\\.{{ "{0s}", View{{ .name = "{0s}", .module = @import("{1s}") }} }},
\\
,
.{
chompExtension(try self.relativePathFrom(.views, path.*, .posix)),
path.*,
},
);
}
}

View File

@ -147,7 +147,7 @@ fn renderMarkdown(
if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
view.data.content = .{ .data = content };
return try layout.render(view.data, jetzig.TemplateContext, .{}, &.{}, .{});
return try layout.render(view.data, jetzig.TemplateContext, .{}, .{});
} else {
std.debug.print("Unknown layout: {s}\n", .{layout_name});
return content;
@ -174,7 +174,6 @@ fn renderZmplTemplate(
view.data,
jetzig.TemplateContext,
.{},
&.{},
.{ .layout = layout },
);
} else {
@ -182,7 +181,7 @@ fn renderZmplTemplate(
return try allocator.dupe(u8, "");
}
} else {
return try template.render(view.data, jetzig.TemplateContext, .{}, &.{}, .{});
return try template.render(view.data, jetzig.TemplateContext, .{}, .{});
}
} else return null;
}

View File

@ -25,8 +25,6 @@ pub const auth = @import("jetzig/auth.zig");
pub const callbacks = @import("jetzig/callbacks.zig");
pub const debug = @import("jetzig/debug.zig");
pub const TemplateContext = @import("jetzig/TemplateContext.zig");
pub const websockets = @import("jetzig/websockets.zig");
pub const channels = @import("jetzig/channels.zig");
pub const DateTime = jetcommon.types.DateTime;
pub const Time = jetcommon.types.Time;
@ -40,20 +38,6 @@ 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

@ -11,7 +11,7 @@ env: jetzig.Environment,
allocator: std.mem.Allocator,
custom_routes: std.ArrayList(jetzig.views.Route),
initHook: ?*const fn (*App) anyerror!void,
server: *anyopaque = undefined,
server: *jetzig.http.Server = undefined,
pub fn deinit(self: *const App) void {
@constCast(self).custom_routes.deinit();
@ -34,15 +34,7 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
defer mime_map.deinit();
try mime_map.build();
inline for (jetzig.http.middleware.middlewares) |middleware| {
if (@hasDecl(middleware, "setup")) try middleware.setup(@constCast(self));
}
const routes = try createRoutes(self.allocator, if (@hasDecl(routes_module, "routes"))
&routes_module.routes
else
&.{});
const routes = try createRoutes(self.allocator, if (@hasDecl(routes_module, "routes")) &routes_module.routes else &.{});
defer {
for (routes) |var_route| {
var_route.deinitParams();
@ -55,11 +47,6 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
self.allocator.free(custom_route.template);
};
const channel_routes = if (@hasDecl(routes_module, "channel_routes"))
routes_module.channel_routes
else
std.StaticStringMap(jetzig.channels.Route).initComptime(.{});
var store = try jetzig.kv.Store.GeneralStore.init(self.allocator, self.env.logger, .general);
defer store.deinit();
@ -69,9 +56,6 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
var cache = try jetzig.kv.Store.CacheStore.init(self.allocator, self.env.logger, .cache);
defer cache.deinit();
var channels = try jetzig.kv.Store.CacheStore.init(self.allocator, self.env.logger, .channels);
defer channels.deinit();
var repo = try jetzig.database.repo(self.allocator, self);
defer repo.deinit();
@ -97,11 +81,10 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
std.process.exit(0);
}
var server = jetzig.http.Server.RoutedServer(routes_module).init(
var server = jetzig.http.Server.init(
self.allocator,
self.env,
routes,
channel_routes,
self.custom_routes.items,
if (@hasDecl(routes_module, "jobs")) &routes_module.jobs else &.{},
if (@hasDecl(routes_module, "jobs")) &routes_module.mailers else &.{},
@ -109,7 +92,6 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
&store,
&job_queue,
&cache,
&channels,
&repo,
options.global,
);

View File

@ -1,43 +1,13 @@
const std = @import("std");
pub const http = @import("http.zig");
pub const views = @import("views.zig");
pub const config = @import("config.zig");
/// Context available in every Zmpl template as `context`.
pub const TemplateContext = @This();
request: ?*http.Request = null,
route: ?views.Route = null,
middleware: Middleware = .{},
pub const Middleware = struct {
context: *TemplateContext = undefined,
pub inline fn renderHeader(middleware: Middleware) ![]const u8 {
return try middleware.render("header");
}
pub inline fn renderFooter(middleware: Middleware) ![]const u8 {
return try middleware.render("footer");
}
fn render(middleware: Middleware, comptime section: []const u8) ![]const u8 {
var buf = std.ArrayList(u8).init(middleware.context.*.request.?.allocator);
const writer = buf.writer();
inline for (http.middleware.middlewares) |middleware_type| {
if (@hasDecl(middleware_type, "Blocks") and @hasDecl(middleware_type.Blocks, section)) {
const renderFn = @field(middleware_type.Blocks, section);
try renderFn(middleware.context.*, writer);
}
}
return try buf.toOwnedSlice();
}
};
/// Return an authenticity token stored in the current request's session. If no token exists,
/// generate and store before returning.
/// Use to create a form element which can be verified by `AntiCsrfMiddleware`.
pub fn authenticityToken(self: TemplateContext) !?[]const u8 {
return if (self.request) |request|
try request.authenticityToken()
@ -45,8 +15,6 @@ pub fn authenticityToken(self: TemplateContext) !?[]const u8 {
null;
}
/// Generate a hidden form element containing an authenticity token provided by
/// `authenticityToken`. Use as `{{context.authenticityFormElement()}}` in a Zmpl template.
pub fn authenticityFormElement(self: TemplateContext) !?[]const u8 {
return if (self.request) |request| blk: {
const token = try request.authenticityToken();
@ -55,10 +23,3 @@ pub fn authenticityFormElement(self: TemplateContext) !?[]const u8 {
, .{ config.get([]const u8, "authenticity_token_name"), token });
} else null;
}
pub fn path(self: TemplateContext) ?[]const u8 {
return if (self.request) |request|
request.path.path
else
null;
}

View File

@ -1,7 +0,0 @@
pub const RoutedChannel = @import("channels/Channel.zig").RoutedChannel;
pub const Message = @import("channels/Message.zig");
pub const Route = @import("channels/Route.zig");
pub const ActionRouter = @import("channels/ActionRouter.zig");
// For convenience in channel callback functions implemented by users.
pub const Channel = RoutedChannel(@import("root").routes);

View File

@ -1,235 +0,0 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
pub const Action = struct {
view: []const u8,
name: []const u8,
params: []const std.builtin.Type.Fn.Param,
};
pub const ActionRouter = struct {
actions: []const Action,
routes: type,
encoded_params: std.StaticStringMap([]const u8),
pub fn invoke(
comptime router: ActionRouter,
allocator: std.mem.Allocator,
path: []const u8,
data: []const u8,
Channel: type,
channel: Channel,
) !?[]const u8 {
inline for (router.actions) |action| {
if (match(action, path, data)) {
var d = jetzig.data.Data.init(allocator);
defer d.deinit();
// Format should be at least e.g.: `_invoke:foo:[]`
if (data.len < prefix(action).len + 2) return error.InvalidChannelActionArguments;
try d.fromJson(data[prefix(action).len..]);
const received_args = switch (d.value.?.*) {
.array => |array| array.array.items,
else => return error.InvalidChannelActionArguments,
};
const view = router.routes.views.get(action.view).?;
const func = @field(view.module.Channel.Actions, action.name);
const Args = std.meta.ArgsTuple(@TypeOf(func));
var args: Args = undefined;
const expected_args = std.meta.fields(Args);
if (expected_args.len < 1 or received_args.len != expected_args.len - 1) {
return error.InvalidChannelActionArguments;
}
args[0] = channel;
if (comptime action.params.len > 1) {
inline for (action.params[1..], 0..) |param, index| {
args[index + 1] = try coerce(param.type.?, received_args[index].*);
}
}
try @call(.auto, func, args);
return action.name;
}
}
return null;
}
pub fn encodedParams(comptime router: ActionRouter, route: jetzig.channels.Route) ?[]const u8 {
if (router.routes.channel_routes.get(route.path)) |matched_route| {
_ = matched_route;
}
}
fn match(comptime action: Action, path: []const u8, data: []const u8) bool {
return (std.mem.eql(u8, action.view, path)) and std.mem.startsWith(
u8,
data,
prefix(action),
);
}
inline fn prefix(comptime action: Action) []const u8 {
return "_invoke:" ++ action.name ++ ":";
}
fn coerce(T: type, value: jetzig.data.Value) !T {
return switch (T) {
[]const u8 => switch (value) {
.string => |v| v.value,
else => error.InvalidChannelActionArguments,
},
else => switch (@typeInfo(T)) {
.int => switch (value) {
.integer => |v| @intCast(v.value),
else => error.InvalidChannelActionArguments,
},
.float => switch (value) {
.float => |v| @floatCast(v.value),
else => error.InvalidChannelActionArguments,
},
.bool => switch (value) {
.boolean => |v| v.value,
else => error.InvalidChannelActionArguments,
},
else => error.InvalidChannelActionArguments,
},
};
}
};
pub fn initComptime(Routes: type) ActionRouter {
comptime {
var len: usize = 0;
for (Routes.views.values()) |view| {
if (!@hasDecl(view.module, "Channel")) continue;
if (!@hasDecl(view.module.Channel, "Actions")) continue;
const actions = view.module.Channel.Actions;
for (std.meta.declarations(actions)) |_| {
len += 1;
}
}
var actions: [len]Action = undefined;
var index: usize = 0;
for (Routes.views.values()) |view| {
if (!@hasDecl(view.module, "Channel")) continue;
if (!@hasDecl(view.module.Channel, "Actions")) continue;
const channel_actions = view.module.Channel.Actions;
const decls = std.meta.declarations(channel_actions);
for (decls) |decl| {
const params = @typeInfo(
@TypeOf(@field(view.module.Channel.Actions, decl.name)),
).@"fn".params;
actions[index] = .{
.view = view.name,
.name = decl.name,
.params = params,
};
index += 1;
}
}
const encoded_params = try encodeParams(Routes);
const result = actions;
return .{ .actions = &result, .routes = Routes, .encoded_params = encoded_params };
}
}
fn encodeParams(Routes: type) !std.StaticStringMap([]const u8) {
// We do a bit of awkward encoding here to ensure that we have a pre-compiled JSON string
// that we can send to the websocket after intialization to give the Jetzig Javascript code a
// spec for all available actions.
comptime {
const Spec = struct {
actions: []ActionSpec,
pub const ActionSpec = struct {
name: []const u8,
params: []const ParamSpec,
pub const ParamSpec = struct {
type: []const u8,
name: []const u8,
};
};
};
const Tuple = std.meta.Tuple(&.{ []const u8, []const u8 });
var map: [Routes.views.keys().len]Tuple = undefined;
for (Routes.views.values(), 0..) |view, view_index| {
const has_actions = @hasDecl(view.module, "Channel") and
@hasDecl(view.module.Channel, "Actions");
const channel_actions = if (has_actions) view.module.Channel.Actions else struct {};
const decls = std.meta.declarations(channel_actions);
var channel_params: Spec = undefined;
var actions: [decls.len]Spec.ActionSpec = undefined;
for (decls, 0..) |decl, decl_index| {
switch (@typeInfo(@TypeOf(@field(view.module.Channel.Actions, decl.name)))) {
.@"fn" => |info| {
verifyParams(info.params, view.name, decl.name);
const route = Routes.channel_routes.get(view.name).?;
const action = for (route.actions) |action| {
if (std.mem.eql(u8, action.name, decl.name)) break action;
} else unreachable;
if (info.params.len > 1) {
var params: [info.params.len - 1]Spec.ActionSpec.ParamSpec = undefined;
for (info.params[1..], 0..) |param, param_index| {
params[param_index] = .{
.type = jsonTypeName(param.type.?),
.name = action.params[param_index].name,
};
}
actions[decl_index] = .{ .name = decl.name, .params = &params };
} else {
actions[decl_index] = .{ .name = decl.name, .params = &.{} };
}
},
else => {},
}
}
channel_params.actions = &actions;
var counting_stream = std.io.countingWriter(std.io.null_writer);
try std.json.stringify(channel_params, .{}, counting_stream.writer());
var buf: [counting_stream.bytes_written]u8 = undefined;
var stream = std.io.fixedBufferStream(&buf);
try std.json.stringify(channel_params, .{}, stream.writer());
const written = buf;
map[view_index] = .{ view.name, &written };
}
return std.StaticStringMap([]const u8).initComptime(map);
}
}
fn verifyParams(
params: []const std.builtin.Type.Fn.Param,
view: []const u8,
action: []const u8,
) void {
const humanized = std.fmt.comptimePrint("Channel Action {s}:{s}", .{ view, action });
const too_few_params = "Expected at least 1 parameter for " ++ humanized;
const missing_param = "Incorrect first argument (must be jetzig.channels.Channel) for " ++ humanized;
if (params.len < 1) @compileError(too_few_params);
if (params[0].type.? != jetzig.channels.Channel) @compileError(missing_param);
}
fn jsonTypeName(T: type) []const u8 {
return switch (T) {
[]const u8 => "string",
else => switch (@typeInfo(T)) {
.float, .comptime_float => "float",
.int, .comptime_int => "integer",
.bool => "bool",
else => @compileError("Unsupported Channel Action argument type: " ++ @typeName(T)),
},
};
}

View File

@ -1,82 +0,0 @@
const std = @import("std");
const httpz = @import("httpz");
const jetzig = @import("../../jetzig.zig");
pub fn RoutedChannel(Routes: type) type {
return struct {
const Channel = @This();
allocator: std.mem.Allocator,
websocket: *jetzig.websockets.RoutedWebsocket(Routes),
state: *jetzig.data.Value,
data: *jetzig.data.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();
channel.websocket.logger.DEBUG(
"Published Channel message for `{s}`",
.{channel.websocket.route.path},
) catch {};
}
pub fn invoke(
channel: Channel,
comptime method: @TypeOf(.enum_literal),
args: anytype,
) !void {
// TODO: DRY
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 writer.writeAll("__jetzig_event__:");
try std.json.stringify(.{ .method = method, .params = args }, .{}, writer);
try write_buffer.flush();
channel.websocket.logger.DEBUG(
"Invoked Javascript function `{s}` for `{s}`",
.{ @tagName(method), channel.websocket.route.path },
) catch {};
}
pub fn getT(
channel: Channel,
comptime T: jetzig.data.Data.ValueType,
key: []const u8,
) @TypeOf(channel.state.getT(T, key)) {
return channel.state.getT(T, key);
}
pub fn get(channel: Channel, key: []const u8) ?*jetzig.data.Value {
return channel.state.get(key);
}
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 remove(channel: Channel, key: []const u8) bool {
return channel.state.remove(key);
}
pub fn sync(channel: Channel) !void {
try channel.websocket.syncState(channel);
}
};
}

View File

@ -1,54 +0,0 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const Message = @This();
allocator: std.mem.Allocator,
payload: []const u8,
data: *jetzig.data.Data,
channel: jetzig.channels.Channel,
pub fn init(
allocator: std.mem.Allocator,
channel: jetzig.channels.Channel,
payload: []const u8,
) Message {
return .{
.allocator = allocator,
.channel = channel,
.data = channel.data,
.payload = payload,
};
}
pub fn params(message: Message) !?*jetzig.data.Value {
var d = try message.allocator.create(jetzig.data.Data);
d.* = jetzig.data.Data.init(message.allocator);
d.fromJson(message.payload) catch |err| {
switch (err) {
error.SyntaxError => {
message.channel.websocket.logger.ERROR("Invalid JSON received in Channel message.", .{}) catch {};
},
else => {
message.channel.websocket.logger.logError(@errorReturnTrace(), err) catch {};
},
}
return null;
};
return d.value;
}
test "message with payload" {
const message = Message.init(
std.testing.allocator,
jetzig.channels.Channel{
.websocket = undefined,
.state = undefined,
.allocator = undefined,
.data = undefined,
},
"foo",
);
try std.testing.expectEqualStrings(message.payload, "foo");
}

View File

@ -1,36 +0,0 @@
const jetzig = @import("../../jetzig.zig");
const Route = @This();
receiveMessageFn: ?*const fn (jetzig.channels.Message) anyerror!void = null,
openConnectionFn: ?*const fn (jetzig.channels.Channel) anyerror!void = null,
path: []const u8,
actions: []const Action,
pub const Action = struct {
name: []const u8,
params: []const Param,
pub const Param = struct {
name: []const u8,
};
};
pub fn receiveMessage(route: Route, message: jetzig.channels.Message) !void {
if (route.receiveMessageFn) |func| try func(message);
}
pub fn initComptime(T: type, path: []const u8, actions: []const Action) 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,
.path = path,
.actions = actions,
};
}
}

View File

@ -145,19 +145,6 @@ pub const cache: kv.Store.Options = .{
// },
};
/// Channels. Identical to `store` options, but allows using different
/// backends (e.g. `.memory` for key-value store, `.file` for cache.
/// Channel state data is stored here.
pub const channels: kv.Store.Options = .{
.backend = .memory,
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-channels.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.addressSpace(4096),
// },
};
/// SMTP configuration for Jetzig Mail.
pub const smtp: mail.SMTPConfig = .{
.port = 25,

View File

@ -39,10 +39,9 @@ pub fn repo(allocator: std.mem.Allocator, app: anytype) !Repo {
}
fn eventCallback(event: jetzig.jetquery.events.Event, app: anytype) !void {
var server: *jetzig.http.Server.RoutedServer(@import("root").routes) = @ptrCast(@alignCast(app.server));
try server.logger.logSql(event);
try app.server.logger.logSql(event);
if (event.err) |err| {
try server.logger.ERROR("[database] {?s}", .{err.message});
try app.server.logger.ERROR("[database] {?s}", .{err.message});
}
}

View File

@ -41,12 +41,6 @@ pub fn get(self: Headers, name: []const u8) ?[]const u8 {
return self.httpz_headers.get(lower);
}
/// Get the first value for a given header identified by `name`, which is assumed to be lower case.
pub fn getLower(self: Headers, name: []const u8) ?[]const u8 {
std.debug.assert(name.len <= max_bytes_header_name);
return self.httpz_headers.get(name);
}
/// Get all values for a given header identified by `name`. Names are case insensitive.
pub fn getAll(self: Headers, name: []const u8) []const []const u8 {
var headers = std.ArrayList([]const u8).init(self.allocator);

View File

@ -13,7 +13,6 @@ 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,
@ -30,7 +29,6 @@ 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),
@ -416,8 +414,3 @@ 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

@ -15,9 +15,7 @@ pub const RequestState = enum {
initial, // No processing has taken place
processed, // Request headers have been processed
after_request, // Initial middleware processing
after_view, // View returned, response data ready for full response render
rendered, // Rendered by middleware or view
rendered_content, // Rendered a plain string by middleware or view
redirected, // Redirected by middleware or view
failed, // Failed by middleware or view
before_response, // Post middleware processing
@ -28,7 +26,7 @@ allocator: std.mem.Allocator,
path: jetzig.http.Path,
method: Method,
headers: jetzig.http.Headers,
host: []const u8,
server: *jetzig.http.Server,
httpz_request: *httpz.Request,
httpz_response: *httpz.Response,
response: *jetzig.http.Response,
@ -54,14 +52,8 @@ rendered_view: ?jetzig.views.View = null,
start_time: i128,
store: RequestStore(jetzig.kv.Store.GeneralStore),
cache: RequestStore(jetzig.kv.Store.CacheStore),
job_queue: RequestStore(jetzig.kv.Store.JobQueueStore),
job_definitions: []const jetzig.JobDefinition,
mailer_definitions: []const jetzig.MailerDefinition,
repo: *jetzig.database.Repo,
global: *jetzig.Global,
env: jetzig.Environment,
routes: []const *const jetzig.views.Route,
logger: jetzig.loggers.Logger,
/// Wrapper for KV store that uses the request's arena allocator for fetching values.
pub fn RequestStore(T: type) type {
@ -130,20 +122,12 @@ pub fn RequestStore(T: type) type {
pub fn init(
allocator: std.mem.Allocator,
server: *jetzig.http.Server,
start_time: i128,
httpz_request: *httpz.Request,
httpz_response: *httpz.Response,
response: *jetzig.http.Response,
repo: *jetzig.database.Repo,
env: jetzig.Environment,
routes: []const *const jetzig.views.Route,
logger: jetzig.loggers.Logger,
store: *jetzig.kv.Store.GeneralStore,
cache: *jetzig.kv.Store.CacheStore,
job_queue: *jetzig.kv.Store.JobQueueStore,
job_definitions: []const jetzig.JobDefinition,
mailer_definitions: []const jetzig.MailerDefinition,
global: *anyopaque,
) !Request {
const path = jetzig.http.Path.init(httpz_request.url.raw);
@ -162,33 +146,25 @@ pub fn init(
const response_data = try allocator.create(jetzig.data.Data);
response_data.* = jetzig.data.Data.init(allocator);
const headers = jetzig.http.Headers.init(allocator, httpz_request.headers);
const host = headers.getLower("host") orelse "";
return .{
.allocator = allocator,
.path = path,
.method = method,
.headers = headers,
.host = host,
.headers = jetzig.http.Headers.init(allocator, httpz_request.headers),
.server = server,
.response = response,
.response_data = response_data,
.httpz_request = httpz_request,
.httpz_response = httpz_response,
.start_time = start_time,
.store = .{ .store = store, .allocator = allocator },
.cache = .{ .store = cache, .allocator = allocator },
.job_queue = .{ .store = job_queue, .allocator = allocator },
.job_definitions = job_definitions,
.mailer_definitions = mailer_definitions,
.env = env,
.routes = routes,
.logger = logger,
.store = .{ .store = server.store, .allocator = allocator },
.cache = .{ .store = server.cache, .allocator = allocator },
.repo = repo,
.global = if (@hasField(jetzig.Global, "__jetzig_default"))
undefined
else
@ptrCast(@alignCast(global)),
@ptrCast(@alignCast(server.global)),
};
}
@ -241,20 +217,6 @@ pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode)
return self.rendered_view.?;
}
/// Render a response with pre-rendered content. This function can only be called once per
/// request (repeat calls will trigger an error).
pub fn renderContent(
self: *Request,
status_code: jetzig.http.status_codes.StatusCode,
content: []const u8,
) jetzig.views.View {
if (self.isRendered()) self.rendered_multiple = true;
self.rendered_view = .{ .data = self.response_data, .status_code = status_code, .content = content };
self.state = .rendered_content;
return self.rendered_view.?;
}
/// Render an error. This function can only be called once per request (repeat calls will
/// trigger an error).
pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
@ -268,7 +230,7 @@ pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) je
pub inline fn isRendered(self: *const Request) bool {
return switch (self.state) {
.initial, .processed, .after_request, .before_response => false,
.after_view, .rendered, .rendered_content, .redirected, .failed, .finalized => true,
.rendered, .redirected, .failed, .finalized => true,
};
}
@ -336,9 +298,6 @@ pub fn renderRedirect(self: *Request, state: RedirectState) !void {
var root = try self.response_data.root(.object);
try root.put("location", self.response_data.string(state.location));
var template_context = jetzig.TemplateContext{ .request = self };
template_context.middleware.context = &template_context;
const content = switch (self.requestFormat()) {
.HTML, .UNKNOWN => if (maybe_template) |template| blk: {
try view.data.addConst("jetzig_view", view.data.string("internal"));
@ -346,8 +305,7 @@ pub fn renderRedirect(self: *Request, state: RedirectState) !void {
break :blk try template.render(
self.response_data,
jetzig.TemplateContext,
template_context,
&.{},
.{ .request = self },
.{},
);
} else try std.fmt.allocPrint(self.allocator, "Redirecting to {s}", .{state.location}),
@ -539,19 +497,19 @@ pub fn cookies(self: *Request) !*jetzig.http.Cookies {
/// `jetzig.http.Session`.
pub fn session(self: *Request) !*jetzig.http.Session {
if (self._session) |capture| return capture;
const cookie_name = self.env.vars.get("JETZIG_SESSION_COOKIE") orelse
const cookie_name = self.server.env.vars.get("JETZIG_SESSION_COOKIE") orelse
jetzig.http.Session.default_cookie_name;
const local_session = try self.allocator.create(jetzig.http.Session);
local_session.* = jetzig.http.Session.init(
self.allocator,
try self.cookies(),
self.env.secret,
self.server.env.secret,
.{ .cookie_name = cookie_name },
);
local_session.parse() catch |err| {
switch (err) {
error.JetzigInvalidSessionCookie => {
try self.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{});
try self.server.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{});
try local_session.reset();
},
else => return err,
@ -603,11 +561,11 @@ pub fn job(self: *Request, job_name: []const u8) !*jetzig.Job {
const background_job = try self.allocator.create(jetzig.Job);
background_job.* = jetzig.Job.init(
self.allocator,
self.store.store,
self.job_queue.store,
self.cache.store,
self.logger,
self.job_definitions,
self.server.store,
self.server.job_queue,
self.server.cache,
self.server.logger,
self.server.job_definitions,
job_name,
);
return background_job;
@ -649,14 +607,14 @@ const RequestMail = struct {
self.request.allocator,
mail_job.params,
jetzig.jobs.JobEnv{
.vars = self.request.env.vars,
.environment = self.request.env.environment,
.logger = self.request.logger,
.routes = self.request.routes,
.mailers = self.request.mailer_definitions,
.jobs = self.request.job_definitions,
.store = self.request.store.store,
.cache = self.request.cache.store,
.vars = self.request.server.env.vars,
.environment = self.request.server.env.environment,
.logger = self.request.server.logger,
.routes = self.request.server.routes,
.mailers = self.request.server.mailer_definitions,
.jobs = self.request.server.job_definitions,
.store = self.request.server.store,
.cache = self.request.server.cache,
.mutex = undefined,
.repo = self.request.repo,
},

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,6 @@ cookie_name: []const u8,
initialized: bool = false,
data: jetzig.data.Data,
state: enum { parsed, pending } = .pending,
id: [32]u8 = undefined,
const Self = @This();
@ -49,11 +48,7 @@ pub fn parse(self: *Self) !void {
/// Reset session to an empty state.
pub fn reset(self: *Self) !void {
self.data.reset();
var object = try self.data.object();
_ = jetzig.util.generateRandomString(&self.id);
try object.put("_id", &self.id);
_ = try self.data.object();
self.state = .parsed;
try self.save();
}
@ -75,7 +70,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

@ -48,7 +48,7 @@ pub fn Type(comptime name: MiddlewareEnum()) type {
}
}
pub fn afterLaunch(server: *jetzig.http.Server.RoutedServer(@import("root").routes)) !void {
pub fn afterLaunch(server: *jetzig.http.Server) !void {
inline for (middlewares) |middleware| {
if (comptime @hasDecl(middleware, "afterLaunch")) {
try middleware.afterLaunch(server);
@ -94,38 +94,6 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
return middleware_data;
}
pub fn afterView(middleware_data: *MiddlewareData, request: *jetzig.http.Request, route: jetzig.views.Route) !void {
if (request.state != .failed) request.state = .after_view;
inline for (middlewares, 0..) |middleware, index| {
if (comptime !@hasDecl(middleware, "afterView")) continue;
if (request.state == .after_view) {
if (comptime @hasDecl(middleware, "init")) {
const data = middleware_data.get(index).?;
try @call(
.always_inline,
middleware.afterView,
.{ @as(*middleware, @ptrCast(@alignCast(data))), request, route },
);
} else {
try @call(
.always_inline,
middleware.afterView,
.{ request, route },
);
}
}
if (request.state != .after_view) {
request.middleware_rendered = .{
.name = @typeName(middleware),
.action = "afterView",
};
break;
}
}
}
pub fn beforeResponse(
middleware_data: *MiddlewareData,
request: *jetzig.http.Request,

View File

@ -22,9 +22,6 @@ pub const Store = struct {
/// Store ephemeral data.
pub const CacheStore = @import("kv/Store.zig").Store(config.get(Store.Options, "cache"));
/// Store channel data.
pub const ChannelStore = @import("kv/Store.zig").Store(config.get(Store.Options, "channels"));
/// Background job storage.
pub const JobQueueStore = @import("kv/Store.zig").Store(config.get(Store.Options, "job_queue"));

View File

@ -53,7 +53,7 @@ fn jetKVOptions(options: KVOptions) jetzig.jetkv.Options {
}
/// Role a given store fills. Used in log outputs.
pub const Role = enum { jobs, cache, general, channels, custom };
pub const Role = enum { jobs, cache, general, custom };
pub fn Store(comptime options: KVOptions) type {
return struct {

View File

@ -148,7 +148,7 @@ fn defaultHtml(
try data.addConst("jetzig_view", data.string(""));
try data.addConst("jetzig_action", data.string(""));
return if (jetzig.zmpl.findPrefixed("mailers", mailer.html_template)) |template|
try template.render(&data, jetzig.TemplateContext, .{}, &.{}, .{})
try template.render(&data, jetzig.TemplateContext, .{}, .{})
else
null;
}
@ -166,7 +166,7 @@ fn defaultText(
try data.addConst("jetzig_view", data.string(""));
try data.addConst("jetzig_action", data.string(""));
return if (jetzig.zmpl.findPrefixed("mailers", mailer.text_template)) |template|
try template.render(&data, jetzig.TemplateContext, .{}, &.{}, .{})
try template.render(&data, jetzig.TemplateContext, .{}, .{})
else
null;
}

View File

@ -5,8 +5,6 @@ pub const HtmxMiddleware = @import("middleware/HtmxMiddleware.zig");
pub const CompressionMiddleware = @import("middleware/CompressionMiddleware.zig");
pub const AuthMiddleware = @import("middleware/AuthMiddleware.zig");
pub const AntiCsrfMiddleware = @import("middleware/AntiCsrfMiddleware.zig");
pub const InertiaMiddleware = @import("middleware/InertiaMiddleware.zig");
pub const ChannelsMiddleware = @import("middleware/ChannelsMiddleware.zig");
const RouteOptions = struct {
content: ?[]const u8 = null,

View File

@ -29,7 +29,7 @@ pub fn beforeRender(request: *jetzig.http.Request, route: jetzig.views.Route) !v
fn logFailure(request: *jetzig.http.Request) !void {
_ = request.fail(.forbidden);
try request.logger.DEBUG("Anti-CSRF token validation failed. Request aborted.", .{});
try request.server.logger.DEBUG("Anti-CSRF token validation failed. Request aborted.", .{});
}
fn verifyCsrfToken(request: *jetzig.http.Request) !void {

View File

@ -1,35 +0,0 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const ChannelsMiddleware = @This();
pub fn setup(app: *jetzig.App) !void {
app.route(.GET, "/_channels.js", ChannelsMiddleware, .renderChannels);
}
pub const Blocks = struct {
pub fn header(_: jetzig.TemplateContext, writer: anytype) !void {
try writer.writeAll(
\\<script src="/_channels.js"></script>
);
}
pub fn footer(context: jetzig.TemplateContext, writer: anytype) !void {
const request = context.request orelse return;
const host = request.headers.getLower("host") orelse return;
try writer.print(
\\<script>
\\ (() => {{
\\ window.addEventListener('DOMContentLoaded', () => {{
\\ jetzig.channel.init("{s}", "{s}");
\\ }});
\\ }})();
\\</script>
\\
, .{ host, request.path.base_path });
}
};
pub fn renderChannels(request: *jetzig.Request) !jetzig.View {
return request.renderContent(.ok, @embedFile("channels/channels.js"));
}

View File

@ -9,7 +9,7 @@ const HtmxMiddleware = @This();
/// content rendered directly by the view function.
pub fn afterRequest(request: *jetzig.http.Request) !void {
if (request.headers.get("HX-Request")) |_| {
try request.logger.DEBUG(
try request.server.logger.DEBUG(
"[middleware-htmx] HX-Request header, disabling layout.",
.{},
);

View File

@ -1,6 +0,0 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
// WIP
const InertiaMiddleware = @This();

View File

@ -1,176 +0,0 @@
window.jetzig = window.jetzig ? window.jetzig : {}
jetzig = window.jetzig;
(() => {
const transform = (value, state, element) => {
const id = element.getAttribute('jetzig-id');
const transformer = id && jetzig.channel.transformers[id];
if (transformer) {
return transformer(value, state, element);
} else {
return value === undefined || value == null ? '' : `${value}`
}
};
jetzig.channel = {
websocket: null,
actions: {},
action_specs: {},
stateChangedCallbacks: [],
messageCallbacks: [],
invokeCallbacks: {},
elementMap: {},
transformers: {},
onStateChanged: function(callback) { this.stateChangedCallbacks.push(callback); },
onMessage: function(callback) { this.messageCallbacks.push(callback); },
init: function(host, path) {
this.websocket = new WebSocket(`ws://${host}${path}`);
this.websocket.addEventListener("message", (event) => {
const state_tag = "__jetzig_channel_state__:";
const actions_tag = "__jetzig_actions__:";
const event_tag = "__jetzig_event__:";
if (event.data.startsWith(state_tag)) {
const state = JSON.parse(event.data.slice(state_tag.length));
Object.entries(this.elementMap).forEach(([ref, elements]) => {
const value = reduceState(ref, state);
elements.forEach(element => element.innerHTML = transform(value, state, element));
});
this.stateChangedCallbacks.forEach((callback) => {
callback(state);
});
} else if (event.data.startsWith(event_tag)) {
const data = JSON.parse(event.data.slice(event_tag.length));
if (Object.hasOwn(this.invokeCallbacks, data.method)) {
this.invokeCallbacks[data.method].forEach(callback => {
callback(data);
});
}
} else if (event.data.startsWith(actions_tag)) {
const data = JSON.parse(event.data.slice(actions_tag.length));
data.actions.forEach(action => {
this.action_specs[action.name] = {
callback: (...params) => {
if (action.params.length != params.length) {
throw new Error(`Invalid params for action '${action.name}'. Expected ${action.params.length} params, found ${params.length}`);
}
[...action.params].forEach((param, index) => {
if (param.type !== typeof params[index]) {
const err = `Incorrect argument type for argument ${index} in '${action.name}'. Expected: ${param.type}, found ${typeof params[index]}`;
switch (param.type) {
case "string":
params[index] = `${params[index]}`;
break;
case "integer":
try { params[index] = parseInt(params[index]) } catch {
throw new Error(err);
};
break;
case "float":
try { params[index] = parseFloat(params[index]) } catch {
throw new Error(err);
};
case "boolean":
params[index] = ["true", "y", "1", "yes", "t"].includes(params[index]);
break;
default:
throw new Error(err);
}
}
});
this.websocket.send(`_invoke:${action.name}:${JSON.stringify(params)}`);
},
spec: { ...action },
};
this.actions[action.name] = this.action_specs[action.name].callback;
});
document.querySelectorAll('[jetzig-click]').forEach(element => {
const ref = element.getAttribute('jetzig-click');
const action = this.action_specs[ref];
if (action) {
element.addEventListener('click', () => {
const args = [];
action.spec.params.forEach(param => {
const arg = element.dataset[param.name];
if (arg === undefined) {
throw new Error(`Expected 'data-${param.name}' attribute for '${action.name}' click handler.`);
} else {
args.push(element.dataset[param.name]);
}
});
action.callback(...args);
});
} else {
throw new Error(`Unknown click handler: '${ref}'`);
}
});
} else {
const data = JSON.parse(event.data);
this.messageCallbacks.forEach((callback) => {
callback(data);
});
}
});
const reduceState = (ref, state) => {
if (!ref.startsWith('$.')) throw new Error(`Unexpected ref format: ${ref}`);
const args = ref.split('.');
args.shift();
const isNumeric = (string) => [...string].every(char => '0123456789'.includes(char));
const isObject = (object) => object && typeof object === 'object';
return args.reduce((acc, arg) => {
if (isNumeric(arg)) {
if (acc && Array.isArray(acc) && acc.length > arg) return acc[parseInt(arg)];
return null;
} else {
if (acc && isObject(acc)) return acc[arg];
return null;
}
}, state);
};
document.querySelectorAll('[jetzig-connect]').forEach(element => {
const ref = element.getAttribute('jetzig-connect');
if (!this.elementMap[ref]) this.elementMap[ref] = [];
const id = `jetzig-${crypto.randomUUID()}`;
element.setAttribute('jetzig-id', id);
this.elementMap[ref].push(element);
const transformer = element.getAttribute('jetzig-transform');
if (transformer) {
this.transformers[id] = new Function("value", "$", "element", `return ${transformer};`);
}
});
const styled_elements = document.querySelectorAll('[jetzig-style]');
this.onStateChanged(state => {
styled_elements.forEach(element => {
const func = new Function("$", `return ${element.getAttribute('jetzig-style')};`)
const styles = func(state);
Object.entries(styles).forEach(([key, value]) => {
element.style.setProperty(key, value);
});
});
});
// this.websocket.addEventListener("open", (event) => {
// // TODO
// this.publish("websockets", {});
// });
},
receive: function(ref, callback) {
if (Object.hasOwn(this.invokeCallbacks, ref)) {
this.invokeCallbacks[ref].push(callback);
} else {
this.invokeCallbacks[ref] = [callback];
}
},
publish: function(data) {
if (this.websocket) {
const json = JSON.stringify(data);
this.websocket.send(json);
}
},
};
})();

View File

@ -1,3 +0,0 @@
@block head {
<script>console.log("hello");</script>
}

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
@partial views:inertia/head
</head>
<body>
<div
id="app"
data-page='{"component":"{{jetzig_view}}","props":{{zmpl.toJson()}},"url":"{{context.path()}}","version":"c32b8e4965f418ad16eaebba1d4e960f"}'
>
</div>
</body>
</html>

View File

@ -11,11 +11,10 @@ routes: []const jetzig.views.Route,
arena: *std.heap.ArenaAllocator,
store: *MemoryStore,
cache: *MemoryStore,
channels: *MemoryStore,
job_queue: *MemoryStore,
multipart_boundary: ?[]const u8 = null,
logger: jetzig.loggers.Logger,
server: *jetzig.http.Server.RoutedServer(@import("root").routes),
server: Server,
repo: *jetzig.database.Repo,
cookies: *jetzig.http.Cookies,
session: *jetzig.http.Session,
@ -58,38 +57,15 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
const session = try alloc.create(jetzig.http.Session);
session.* = jetzig.http.Session.init(alloc, cookies, jetzig.testing.secret, .{});
const server = try alloc.create(jetzig.http.Server.RoutedServer(@import("root").routes));
// This server is only used by database logging - we create a more lifelike server when we
// process the actual test request.
server.* = .{
.logger = logger,
.allocator = undefined,
.env = undefined,
.routes = undefined,
.channel_routes = undefined,
.custom_routes = undefined,
.job_definitions = undefined,
.mailer_definitions = undefined,
.mime_map = undefined,
.initialized = undefined,
.store = undefined,
.job_queue = undefined,
.cache = undefined,
.channels = undefined,
.repo = undefined,
.global = undefined,
};
app.* = App{
.arena = arena,
.allocator = allocator,
.routes = &routes_module.routes,
.store = try createStore(arena.allocator(), logger, .general),
.cache = try createStore(arena.allocator(), logger, .cache),
.channels = try createStore(arena.allocator(), logger, .channels),
.job_queue = try createStore(arena.allocator(), logger, .jobs),
.logger = logger,
.server = server,
.server = .{ .logger = logger },
.repo = repo,
.cookies = cookies,
.session = session,
@ -155,7 +131,7 @@ pub fn request(
.env_map = std.process.EnvMap.init(allocator),
.env_file = null,
};
var server = jetzig.http.Server.RoutedServer(@import("root").routes){
var server = jetzig.http.Server{
.allocator = allocator,
.logger = self.logger,
.env = .{
@ -172,14 +148,12 @@ pub fn request(
.secret = jetzig.testing.secret,
},
.routes = routes,
.channel_routes = std.StaticStringMap(jetzig.channels.Route).initComptime(.{}),
.custom_routes = &.{},
.mailer_definitions = &.{},
.job_definitions = &.{},
.mime_map = jetzig.testing.mime_map,
.store = self.store,
.cache = self.cache,
.channels = self.channels,
.job_queue = self.job_queue,
.global = undefined,
.repo = self.repo,

View File

@ -92,8 +92,6 @@ pub fn format(self: Route, _: []const u8, _: anytype, writer: anytype) !void {
pub fn match(self: Route, request: *const jetzig.http.Request) bool {
if (self.method != request.method) return false;
if (std.mem.eql(u8, request.path.file_path, self.uri_path)) return true;
var request_path_it = std.mem.splitScalar(u8, request.path.base_path, '/');
var uri_path_it = std.mem.splitScalar(u8, self.uri_path, '/');

View File

@ -6,7 +6,6 @@ const jetzig = @import("../../jetzig.zig");
data: *jetzig.data.Data,
status_code: jetzig.http.status_codes.StatusCode = .ok,
content: ?[]const u8 = null,
pub fn deinit(self: Self) void {
_ = self;

View File

@ -1,3 +0,0 @@
pub const RoutedWebsocket = @import("websockets/Websocket.zig").RoutedWebsocket;
pub const Websocket = RoutedWebsocket(@import("root").routes);
pub const Context = @import("websockets/Websocket.zig").Context;

View File

@ -1,136 +0,0 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const httpz = @import("httpz");
pub const Context = struct {
allocator: std.mem.Allocator,
route: jetzig.channels.Route,
session_id: []const u8,
channels: *jetzig.kv.Store.ChannelStore,
logger: jetzig.loggers.Logger,
};
pub fn RoutedWebsocket(Routes: type) type {
return struct {
allocator: std.mem.Allocator,
connection: *httpz.websocket.Conn,
channels: *jetzig.kv.Store.ChannelStore,
route: jetzig.channels.Route,
data: *jetzig.Data,
session_id: []const u8,
logger: jetzig.loggers.Logger,
const Websocket = @This();
const router = jetzig.channels.ActionRouter.initComptime(Routes);
pub fn init(connection: *httpz.websocket.Conn, context: Context) !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,
.channels = context.channels,
.logger = context.logger,
.data = data,
};
}
pub fn afterInit(websocket: *Websocket, context: Context) !void {
_ = context;
if (router.encoded_params.get(websocket.route.path)) |params| {
var stack_fallback = std.heap.stackFallback(4096, websocket.allocator);
const allocator = stack_fallback.get();
var write_buffer = websocket.connection.writeBuffer(allocator, .text);
defer write_buffer.deinit();
const writer = write_buffer.writer();
try writer.print("__jetzig_actions__:{s}", .{params});
try write_buffer.flush();
}
const func = websocket.route.openConnectionFn orelse return;
const channel = jetzig.channels.Channel{
.allocator = websocket.allocator,
.websocket = websocket,
.state = try websocket.getState(),
.data = websocket.data,
};
try func(channel);
}
pub fn clientMessage(websocket: *Websocket, allocator: std.mem.Allocator, data: []const u8) !void {
const channel = jetzig.channels.RoutedChannel(Routes){
.allocator = allocator,
.websocket = websocket,
.state = try websocket.getState(),
.data = websocket.data,
};
if (websocket.invoke(channel, data)) |maybe_action| {
if (maybe_action) |action| {
websocket.logger.DEBUG(
"Invoked Channel Action `{s}:{?s}`",
.{ websocket.route.path, action },
) catch {};
return;
}
} else |err| {
websocket.logger.logError(@errorReturnTrace(), err) catch {};
return;
}
const message = jetzig.channels.Message.init(allocator, channel, data);
websocket.route.receiveMessage(message) catch |err| {
websocket.logger.logError(@errorReturnTrace(), err) catch {};
};
websocket.logger.DEBUG("Routed Channel message for `{s}`", .{websocket.route.path}) catch {};
}
pub fn syncState(websocket: *Websocket, channel: jetzig.channels.RoutedChannel(Routes)) !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 websocket.channels.put(websocket.session_id, channel.state);
try writer.print("__jetzig_channel_state__:{s}", .{try websocket.data.toJson()});
try write_buffer.flush();
websocket.logger.DEBUG("Synchronized Channel state for `{s}`", .{websocket.route.path}) catch {};
}
pub fn getState(websocket: *Websocket) !*jetzig.data.Value {
return try websocket.channels.get(websocket.data, websocket.session_id) orelse blk: {
const root = try websocket.data.root(.object);
try websocket.channels.put(websocket.session_id, root);
break :blk try websocket.channels.get(websocket.data, websocket.session_id) orelse error.JetzigInvalidChannel;
};
}
fn invoke(
websocket: *Websocket,
channel: jetzig.channels.RoutedChannel(Routes),
data: []const u8,
) !?[]const u8 {
return router.invoke(
websocket.allocator,
websocket.route.path,
data,
@TypeOf(channel),
channel,
);
}
};
}

View File

@ -8,7 +8,6 @@ pub const std_options = std.Options{
};
pub const jetzig_options = @import("main").jetzig_options;
pub const routes = @import("main").routes;
pub fn log(
comptime message_level: std.log.Level,

View File

@ -11,7 +11,5 @@ test {
_ = @import("jetzig/http/Path.zig");
_ = @import("jetzig/jobs/Job.zig");
_ = @import("jetzig/mail/Mail.zig");
_ = @import("jetzig/channels/Channel.zig");
_ = @import("jetzig/channels/Message.zig");
_ = @import("jetzig/loggers/LogQueue.zig");
}