This commit is contained in:
Bob Farrell 2025-04-23 19:11:02 +01:00
parent 8c2d6806b5
commit e9802bf546
15 changed files with 448 additions and 165 deletions

View File

@ -25,8 +25,8 @@
.hash = "jetkv-0.0.0-zCv0fmCGAgCyYqwHjk0P5KrYVRew1MJAtbtAcIO-WPpT", .hash = "jetkv-0.0.0-zCv0fmCGAgCyYqwHjk0P5KrYVRew1MJAtbtAcIO-WPpT",
}, },
.zmpl = .{ .zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/cfbbc1263c4c62fa91579280c08c5a935c579563.tar.gz", .url = "https://github.com/jetzig-framework/zmpl/archive/9c54edd62b47aabdcb0e5f17beb45637b9de6a33.tar.gz",
.hash = "zmpl-0.0.1-SYFGBmJsAwCUsj-noN2QEWHY1paouyj0naGNQ2uTIcYw", .hash = "zmpl-0.0.1-SYFGBl5sAwDu45IUKKH3TQRc4qZ8A1P2nNaU4iO3Zf-6",
}, },
.httpz = .{ .httpz = .{
.url = "https://github.com/karlseguin/http.zig/archive/37d7cb9819b804ade5f4b974b82f8dd0622225ed.tar.gz", .url = "https://github.com/karlseguin/http.zig/archive/37d7cb9819b804ade5f4b974b82f8dd0622225ed.tar.gz",

View File

@ -12,15 +12,15 @@ pub const Channel = struct {
} }
pub fn receive(message: jetzig.channels.Message) !void { pub fn receive(message: jetzig.channels.Message) !void {
const value = try message.value(); const params = try message.params() orelse return;
if (value.getT(.boolean, "reset") == true) { if (params.remove("reset")) {
try resetGame(message.channel); try resetGame(message.channel);
try message.channel.sync(); try message.channel.sync();
return; return;
} }
const cell: usize = if (value.getT(.integer, "cell")) |integer| const cell: usize = if (params.getT(.integer, "cell")) |integer|
@intCast(integer) @intCast(integer)
else else
return; return;
@ -62,8 +62,8 @@ pub const Channel = struct {
pub const Actions = struct { pub const Actions = struct {
pub fn reset(channel: jetzig.channels.Channel) !void { pub fn reset(channel: jetzig.channels.Channel) !void {
_ = channel; try resetGame(channel);
std.debug.print("here\n", .{}); try channel.sync();
} }
}; };

View File

@ -1,6 +1,7 @@
<script> <script>
const channel = { const channel = {
websocket: null, websocket: null,
actions: {},
stateChangedCallbacks: [], stateChangedCallbacks: [],
messageCallbacks: [], messageCallbacks: [],
onStateChanged: function(callback) { this.stateChangedCallbacks.push(callback); }, onStateChanged: function(callback) { this.stateChangedCallbacks.push(callback); },
@ -20,11 +21,35 @@
channel.websocket = new WebSocket('ws://{{host}}{{request.path.base_path}}'); channel.websocket = new WebSocket('ws://{{host}}{{request.path.base_path}}');
channel.websocket.addEventListener("message", (event) => { channel.websocket.addEventListener("message", (event) => {
const state_tag = "__jetzig_channel_state__:"; const state_tag = "__jetzig_channel_state__:";
const actions_tag = "__jetzig_actions__:";
if (event.data.startsWith(state_tag)) { if (event.data.startsWith(state_tag)) {
const state = JSON.parse(event.data.slice(state_tag.length)); const state = JSON.parse(event.data.slice(state_tag.length));
channel.stateChangedCallbacks.forEach((callback) => { channel.stateChangedCallbacks.forEach((callback) => {
callback(state); callback(state);
}); });
} else if (event.data.startsWith(actions_tag)) {
const data = JSON.parse(event.data.slice(actions_tag.length));
data.actions.forEach(action => {
channel.actions[action.name] = (...params) => {
if (action.params.length != params.length) {
throw new Error(`Invalid params for action '${action.name}'`);
}
[...action.params].forEach((param, index) => {
const map = {
s: "string",
b: "boolean",
i: "number",
f: "number",
};
if (map[param] !== typeof params[index]) {
throw new Error(`Incorrect argument type for argument ${index} in '${action.name}'. Expected: ${map[param]}, found ${typeof params[index]}`);
}
});
channel.websocket.send(`_invoke:${action.name}:${JSON.stringify(params)}`);
};
});
} else { } else {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
channel.messageCallbacks.forEach((callback) => { channel.messageCallbacks.forEach((callback) => {
@ -76,6 +101,7 @@
<script> <script>
channel.onStateChanged(state => { channel.onStateChanged(state => {
console.log(state);
document.querySelector("#player-wins").innerText = state.results.player; document.querySelector("#player-wins").innerText = state.results.player;
document.querySelector("#cpu-wins").innerText = state.results.cpu; document.querySelector("#cpu-wins").innerText = state.results.cpu;
document.querySelector("#ties").innerText = state.results.ties; document.querySelector("#ties").innerText = state.results.ties;
@ -97,7 +123,7 @@
}); });
document.querySelector("#reset-button").addEventListener("click", () => { document.querySelector("#reset-button").addEventListener("click", () => {
channel.publish({ reset: true }); channel.actions.reset();
}); });
document.querySelectorAll("#board div.cell").forEach(element => { document.querySelectorAll("#board div.cell").forEach(element => {

View File

@ -189,12 +189,12 @@ pub fn generateRoutes(self: *Routes) ![]const u8 {
try writer.writeAll( try writer.writeAll(
\\ \\
\\pub const View = struct { name: []const u8, module: type }; \\pub const View = struct { name: []const u8, module: type };
\\pub const views = [_]View{ \\pub const views = std.StaticStringMap(View).initComptime(.{
\\ \\
); );
try self.writeViewsArray(writer); try self.writeViewsMap(writer);
try writer.writeAll( try writer.writeAll(
\\}; \\});
\\ \\
); );
@ -411,7 +411,7 @@ fn writeChannelRoutes(self: *Routes, writer: anytype) !void {
const view_name = chompExtension(relative_path); const view_name = chompExtension(relative_path);
try writer.print( try writer.print(
\\.{{ "{s}", jetzig.channels.Route.initComptime(@import("{s}")) }} \\.{{ "{0s}", jetzig.channels.Route.initComptime(@import("{1s}"), "{0s}") }}
\\ \\
, .{ view_name, module_path }); , .{ view_name, module_path });
} }
@ -893,14 +893,17 @@ fn writeJobs(self: Routes, writer: anytype) !void {
std.debug.print("[jetzig] Imported {} job(s)\n", .{count}); std.debug.print("[jetzig] Imported {} job(s)\n", .{count});
} }
fn writeViewsArray(self: Routes, writer: anytype) !void { fn writeViewsMap(self: Routes, writer: anytype) !void {
var it = self.module_paths.keyIterator(); var it = self.module_paths.keyIterator();
while (it.next()) |path| { while (it.next()) |path| {
try writer.print( try writer.print(
\\.{{ .name = "{s}", .module = @import("{s}") }}, \\.{{ "{0s}", View{{ .name = "{0s}", .module = @import("{1s}") }} }},
\\ \\
, ,
.{ chompExtension(try self.relativePathFrom(.views, path.*, .posix)), path.* }, .{
chompExtension(try self.relativePathFrom(.views, path.*, .posix)),
path.*,
},
); );
} }
} }

View File

@ -25,6 +25,7 @@ pub const auth = @import("jetzig/auth.zig");
pub const callbacks = @import("jetzig/callbacks.zig"); pub const callbacks = @import("jetzig/callbacks.zig");
pub const debug = @import("jetzig/debug.zig"); pub const debug = @import("jetzig/debug.zig");
pub const TemplateContext = @import("jetzig/TemplateContext.zig"); pub const TemplateContext = @import("jetzig/TemplateContext.zig");
pub const websockets = @import("jetzig/websockets.zig");
pub const channels = @import("jetzig/channels.zig"); pub const channels = @import("jetzig/channels.zig");
pub const DateTime = jetcommon.types.DateTime; pub const DateTime = jetcommon.types.DateTime;

View File

@ -1,4 +1,7 @@
pub const Channel = @import("channels/Channel.zig"); pub const RoutedChannel = @import("channels/Channel.zig").RoutedChannel;
pub const Message = @import("channels/Message.zig"); pub const Message = @import("channels/Message.zig");
pub const Route = @import("channels/Route.zig"); pub const Route = @import("channels/Route.zig");
pub const ActionRouter = @import("channels/ActionRouter.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,9 +1,109 @@
const std = @import("std"); const std = @import("std");
pub fn initComptime(T: type) ActionRouter { 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 { comptime {
var len: usize = 0; var len: usize = 0;
for (T.views) |view| { for (Routes.views.values()) |view| {
if (!@hasDecl(view.module, "Channel")) continue; if (!@hasDecl(view.module, "Channel")) continue;
if (!@hasDecl(view.module.Channel, "Actions")) continue; if (!@hasDecl(view.module.Channel, "Actions")) continue;
@ -14,32 +114,110 @@ pub fn initComptime(T: type) ActionRouter {
} }
var actions: [len]Action = undefined; var actions: [len]Action = undefined;
var index: usize = 0; var index: usize = 0;
for (T.views) |view| { for (Routes.views.values()) |view| {
if (!@hasDecl(view.module, "Channel")) continue; if (!@hasDecl(view.module, "Channel")) continue;
if (!@hasDecl(view.module.Channel, "Actions")) continue; if (!@hasDecl(view.module.Channel, "Actions")) continue;
const channel_actions = view.module.Channel.Actions; const channel_actions = view.module.Channel.Actions;
for (std.meta.declarations(channel_actions)) |decl| { 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] = .{ actions[index] = .{
.view = view.name, .view = view.name,
.name = decl.name, .name = decl.name,
.params = &.{}, //@typeInfo(@TypeOf(@field(view.module.Channel.Actions, decl.name))).@"fn".params, .params = params,
}; };
index += 1; index += 1;
} }
} }
const encoded_params = try encodeParams(Routes);
const result = actions; const result = actions;
return .{ .actions = &result }; return .{ .actions = &result, .routes = Routes, .encoded_params = encoded_params };
} }
} }
pub const Action = struct { fn encodeParams(Routes: type) !std.StaticStringMap([]const u8) {
view: []const u8, // We do a bit of awkward encoding here to ensure that we have a pre-compiled JSON string
name: []const u8, // that we can send to the websocket after intialization to give the Jetzig Javascript code a
params: []const u8, // spec for all available actions.
}; comptime {
const Spec = struct {
actions: []ActionSpec,
pub const ActionSpec = struct {
name: []const u8,
params: []const u8,
};
};
const Tuple = std.meta.Tuple(&.{ []const u8, []const u8 });
var map: [Routes.views.keys().len]Tuple = undefined;
pub const ActionRouter = struct { for (Routes.views.values(), 0..) |view, view_index| {
actions: []const Action, 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);
if (info.params.len > 1) {
var params: [info.params.len - 1]u8 = undefined;
for (info.params[1..], 0..) |param, param_index| {
params[param_index] = jsonTypeName(param.type.?);
}
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) u8 {
return switch (T) {
[]const u8 => 's',
else => switch (@typeInfo(T)) {
.float, .comptime_float => 'f',
.bool => 'b',
.int, .comptime_int => 'i',
else => @compileError("Unsupported Channel Action argument type: " ++ @typeName(T)),
},
};
}

View File

@ -4,45 +4,49 @@ const httpz = @import("httpz");
const jetzig = @import("../../jetzig.zig"); const jetzig = @import("../../jetzig.zig");
const Channel = @This(); pub fn RoutedChannel(Routes: type) type {
return struct {
const Channel = @This();
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
websocket: *jetzig.http.Websocket, websocket: *jetzig.websockets.RoutedWebsocket(Routes),
state: *jetzig.data.Value, state: *jetzig.data.Value,
data: *jetzig.data.Data, data: *jetzig.data.Data,
pub fn publish(channel: Channel, data: anytype) !void { pub fn publish(channel: Channel, data: anytype) !void {
var stack_fallback = std.heap.stackFallback(4096, channel.allocator); var stack_fallback = std.heap.stackFallback(4096, channel.allocator);
const allocator = stack_fallback.get(); const allocator = stack_fallback.get();
var write_buffer = channel.websocket.connection.writeBuffer(allocator, .text); var write_buffer = channel.websocket.connection.writeBuffer(allocator, .text);
defer write_buffer.deinit(); defer write_buffer.deinit();
const writer = write_buffer.writer(); const writer = write_buffer.writer();
try std.json.stringify(data, .{}, writer); try std.json.stringify(data, .{}, writer);
try write_buffer.flush(); try write_buffer.flush();
} }
pub fn getT( pub fn getT(
channel: Channel, channel: Channel,
comptime T: jetzig.data.Data.ValueType, comptime T: jetzig.data.Data.ValueType,
key: []const u8, key: []const u8,
) @TypeOf(channel.state.getT(T, key)) { ) @TypeOf(channel.state.getT(T, key)) {
return channel.state.getT(T, key); return channel.state.getT(T, key);
} }
pub fn get(channel: Channel, key: []const u8) ?*jetzig.data.Value { pub fn get(channel: Channel, key: []const u8) ?*jetzig.data.Value {
return channel.state.get(key); return channel.state.get(key);
} }
pub fn put( pub fn put(
channel: Channel, channel: Channel,
key: []const u8, key: []const u8,
value: anytype, value: anytype,
) @TypeOf(channel.state.put(key, value)) { ) @TypeOf(channel.state.put(key, value)) {
return try channel.state.put(key, value); return try channel.state.put(key, value);
} }
pub fn sync(channel: Channel) !void { pub fn sync(channel: Channel) !void {
try channel.websocket.syncState(channel); try channel.websocket.syncState(channel);
}
};
} }

View File

@ -2,16 +2,18 @@ const std = @import("std");
const jetzig = @import("../../jetzig.zig"); const jetzig = @import("../../jetzig.zig");
const Channel = @import("Channel.zig");
const Message = @This(); const Message = @This();
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
payload: []const u8, payload: []const u8,
data: *jetzig.data.Data, data: *jetzig.data.Data,
channel: Channel, channel: jetzig.channels.Channel,
pub fn init(allocator: std.mem.Allocator, channel: Channel, payload: []const u8) Message { pub fn init(
allocator: std.mem.Allocator,
channel: jetzig.channels.Channel,
payload: []const u8,
) Message {
return .{ return .{
.allocator = allocator, .allocator = allocator,
.channel = channel, .channel = channel,
@ -20,17 +22,27 @@ pub fn init(allocator: std.mem.Allocator, channel: Channel, payload: []const u8)
}; };
} }
pub fn value(message: Message) !*jetzig.data.Value { pub fn params(message: Message) !?*jetzig.data.Value {
var d = try message.allocator.create(jetzig.data.Data); var d = try message.allocator.create(jetzig.data.Data);
d.* = jetzig.data.Data.init(message.allocator); d.* = jetzig.data.Data.init(message.allocator);
try d.fromJson(message.payload); d.fromJson(message.payload) catch |err| {
return d.value.?; 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" { test "message with payload" {
const message = Message.init( const message = Message.init(
std.testing.allocator, std.testing.allocator,
Channel{ jetzig.channels.Channel{
.websocket = undefined, .websocket = undefined,
.state = undefined, .state = undefined,
.allocator = undefined, .allocator = undefined,

View File

@ -4,12 +4,13 @@ const Route = @This();
receiveMessageFn: ?*const fn (jetzig.channels.Message) anyerror!void = null, receiveMessageFn: ?*const fn (jetzig.channels.Message) anyerror!void = null,
openConnectionFn: ?*const fn (jetzig.channels.Channel) anyerror!void = null, openConnectionFn: ?*const fn (jetzig.channels.Channel) anyerror!void = null,
path: []const u8,
pub fn receiveMessage(route: Route, message: jetzig.channels.Message) !void { pub fn receiveMessage(route: Route, message: jetzig.channels.Message) !void {
if (route.receiveMessageFn) |func| try func(message); if (route.receiveMessageFn) |func| try func(message);
} }
pub fn initComptime(T: type) Route { pub fn initComptime(T: type, path: []const u8) Route {
comptime { comptime {
if (!@hasDecl(T, "Channel")) return .{}; if (!@hasDecl(T, "Channel")) return .{};
const openConnectionFn = if (@hasDecl(T.Channel, "open")) T.Channel.open else null; const openConnectionFn = if (@hasDecl(T.Channel, "open")) T.Channel.open else null;
@ -18,6 +19,7 @@ pub fn initComptime(T: type) Route {
return .{ return .{
.openConnectionFn = openConnectionFn, .openConnectionFn = openConnectionFn,
.receiveMessageFn = receiveMessageFn, .receiveMessageFn = receiveMessageFn,
.path = path,
}; };
} }
} }

View File

@ -13,7 +13,6 @@ pub const Response = @import("http/Response.zig");
pub const Session = @import("http/Session.zig"); pub const Session = @import("http/Session.zig");
pub const Cookies = @import("http/Cookies.zig"); pub const Cookies = @import("http/Cookies.zig");
pub const Headers = @import("http/Headers.zig"); pub const Headers = @import("http/Headers.zig");
pub const Websocket = @import("http/Websocket.zig");
pub const Query = @import("http/Query.zig"); pub const Query = @import("http/Query.zig");
pub const MultipartQuery = @import("http/MultipartQuery.zig"); pub const MultipartQuery = @import("http/MultipartQuery.zig");
pub const File = @import("http/File.zig"); pub const File = @import("http/File.zig");

View File

@ -9,7 +9,6 @@ const httpz = @import("httpz");
pub const RenderedView = struct { view: jetzig.views.View, content: []const u8 }; pub const RenderedView = struct { view: jetzig.views.View, content: []const u8 };
pub fn RoutedServer(Routes: type) type { pub fn RoutedServer(Routes: type) type {
_ = Routes;
return struct { return struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
logger: jetzig.loggers.Logger, logger: jetzig.loggers.Logger,
@ -75,7 +74,7 @@ pub fn RoutedServer(Routes: type) type {
const HttpzHandler = struct { const HttpzHandler = struct {
server: *Server, server: *Server,
pub const WebsocketHandler = jetzig.http.Websocket; pub const WebsocketHandler = jetzig.websockets.RoutedWebsocket(Routes);
pub fn handle(self: HttpzHandler, request: *httpz.Request, response: *httpz.Response) void { pub fn handle(self: HttpzHandler, request: *httpz.Request, response: *httpz.Response) void {
self.server.processNextRequest(request, response) catch |err| { self.server.processNextRequest(request, response) catch |err| {
@ -215,14 +214,15 @@ pub fn RoutedServer(Routes: type) type {
}; };
return try httpz.upgradeWebsocket( return try httpz.upgradeWebsocket(
jetzig.http.Websocket, jetzig.websockets.RoutedWebsocket(Routes),
httpz_request, httpz_request,
httpz_response, httpz_response,
jetzig.http.Websocket.Context{ jetzig.websockets.Context{
.allocator = self.allocator, .allocator = self.allocator,
.route = route, .route = route,
.session_id = session_id, .session_id = session_id,
.channels = self.channels, .channels = self.channels,
.logger = self.logger,
}, },
); );
} }

View File

@ -1,84 +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,
};
const Websocket = @This();
allocator: std.mem.Allocator,
connection: *httpz.websocket.Conn,
channels: *jetzig.kv.Store.ChannelStore,
route: jetzig.channels.Route,
data: *jetzig.Data,
session_id: []const u8,
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,
.data = data,
};
}
pub fn afterInit(websocket: *Websocket, context: Context) !void {
_ = context;
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.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 websocket.channels.put(websocket.session_id, channel.state);
try writer.print("__jetzig_channel_state__:{s}", .{try websocket.data.toJson()});
try write_buffer.flush();
}
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;
};
}

View File

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

@ -0,0 +1,136 @@
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,
);
}
};
}