mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
WIP
This commit is contained in:
parent
9847efdf4a
commit
d3b3ae63cf
@ -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
116
demo/public/party.css
Normal 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
47
demo/public/party.js
Normal 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 = '🎉';
|
||||
// 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);
|
||||
}
|
||||
}
|
@ -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"));
|
||||
|
@ -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">🏆</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">🏆</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 ? "✈" : "🦎"
|
||||
element.innerHTML = { player: "✈", cpu: "🦎" }[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>
|
||||
|
@ -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
|
||||
|
@ -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 => {},
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user