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",
},
.zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/cfbbc1263c4c62fa91579280c08c5a935c579563.tar.gz",
.hash = "zmpl-0.0.1-SYFGBmJsAwCUsj-noN2QEWHY1paouyj0naGNQ2uTIcYw",
.url = "https://github.com/jetzig-framework/zmpl/archive/9c54edd62b47aabdcb0e5f17beb45637b9de6a33.tar.gz",
.hash = "zmpl-0.0.1-SYFGBl5sAwDu45IUKKH3TQRc4qZ8A1P2nNaU4iO3Zf-6",
},
.httpz = .{
.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 {
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 message.channel.sync();
return;
}
const cell: usize = if (value.getT(.integer, "cell")) |integer|
const cell: usize = if (params.getT(.integer, "cell")) |integer|
@intCast(integer)
else
return;
@ -62,8 +62,8 @@ pub const Channel = struct {
pub const Actions = struct {
pub fn reset(channel: jetzig.channels.Channel) !void {
_ = channel;
std.debug.print("here\n", .{});
try resetGame(channel);
try channel.sync();
}
};

View File

@ -1,6 +1,7 @@
<script>
const channel = {
websocket: null,
actions: {},
stateChangedCallbacks: [],
messageCallbacks: [],
onStateChanged: function(callback) { this.stateChangedCallbacks.push(callback); },
@ -20,11 +21,35 @@
channel.websocket = new WebSocket('ws://{{host}}{{request.path.base_path}}');
channel.websocket.addEventListener("message", (event) => {
const state_tag = "__jetzig_channel_state__:";
const actions_tag = "__jetzig_actions__:";
if (event.data.startsWith(state_tag)) {
const state = JSON.parse(event.data.slice(state_tag.length));
channel.stateChangedCallbacks.forEach((callback) => {
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 {
const data = JSON.parse(event.data);
channel.messageCallbacks.forEach((callback) => {
@ -76,6 +101,7 @@
<script>
channel.onStateChanged(state => {
console.log(state);
document.querySelector("#player-wins").innerText = state.results.player;
document.querySelector("#cpu-wins").innerText = state.results.cpu;
document.querySelector("#ties").innerText = state.results.ties;
@ -97,7 +123,7 @@
});
document.querySelector("#reset-button").addEventListener("click", () => {
channel.publish({ reset: true });
channel.actions.reset();
});
document.querySelectorAll("#board div.cell").forEach(element => {

View File

@ -189,12 +189,12 @@ pub fn generateRoutes(self: *Routes) ![]const u8 {
try writer.writeAll(
\\
\\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(
\\};
\\});
\\
);
@ -411,7 +411,7 @@ fn writeChannelRoutes(self: *Routes, writer: anytype) !void {
const view_name = chompExtension(relative_path);
try writer.print(
\\.{{ "{s}", jetzig.channels.Route.initComptime(@import("{s}")) }}
\\.{{ "{0s}", jetzig.channels.Route.initComptime(@import("{1s}"), "{0s}") }}
\\
, .{ view_name, module_path });
}
@ -893,14 +893,17 @@ fn writeJobs(self: Routes, writer: anytype) !void {
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();
while (it.next()) |path| {
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 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;

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 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,9 +1,109 @@
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 {
var len: usize = 0;
for (T.views) |view| {
for (Routes.views.values()) |view| {
if (!@hasDecl(view.module, "Channel")) continue;
if (!@hasDecl(view.module.Channel, "Actions")) continue;
@ -14,32 +114,110 @@ pub fn initComptime(T: type) ActionRouter {
}
var actions: [len]Action = undefined;
var index: usize = 0;
for (T.views) |view| {
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;
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] = .{
.view = view.name,
.name = decl.name,
.params = &.{}, //@typeInfo(@TypeOf(@field(view.module.Channel.Actions, decl.name))).@"fn".params,
.params = params,
};
index += 1;
}
}
const encoded_params = try encodeParams(Routes);
const result = actions;
return .{ .actions = &result };
return .{ .actions = &result, .routes = Routes, .encoded_params = encoded_params };
}
}
pub const Action = struct {
view: []const u8,
name: []const u8,
params: []const u8,
};
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 u8,
};
};
const Tuple = std.meta.Tuple(&.{ []const u8, []const u8 });
var map: [Routes.views.keys().len]Tuple = undefined;
pub const ActionRouter = struct {
actions: []const Action,
};
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);
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 Channel = @This();
pub fn RoutedChannel(Routes: type) type {
return struct {
const Channel = @This();
allocator: std.mem.Allocator,
websocket: *jetzig.http.Websocket,
state: *jetzig.data.Value,
data: *jetzig.data.Data,
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();
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();
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(
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 sync(channel: Channel) !void {
try channel.websocket.syncState(channel);
const writer = write_buffer.writer();
try std.json.stringify(data, .{}, writer);
try write_buffer.flush();
}
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 sync(channel: Channel) !void {
try channel.websocket.syncState(channel);
}
};
}

View File

@ -2,16 +2,18 @@ const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const Channel = @import("Channel.zig");
const Message = @This();
allocator: std.mem.Allocator,
payload: []const u8,
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 .{
.allocator = allocator,
.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);
d.* = jetzig.data.Data.init(message.allocator);
try d.fromJson(message.payload);
return d.value.?;
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,
Channel{
jetzig.channels.Channel{
.websocket = undefined,
.state = undefined,
.allocator = undefined,

View File

@ -4,12 +4,13 @@ const Route = @This();
receiveMessageFn: ?*const fn (jetzig.channels.Message) 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 {
if (route.receiveMessageFn) |func| try func(message);
}
pub fn initComptime(T: type) Route {
pub fn initComptime(T: type, path: []const u8) Route {
comptime {
if (!@hasDecl(T, "Channel")) return .{};
const openConnectionFn = if (@hasDecl(T.Channel, "open")) T.Channel.open else null;
@ -18,6 +19,7 @@ pub fn initComptime(T: type) Route {
return .{
.openConnectionFn = openConnectionFn,
.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 Cookies = @import("http/Cookies.zig");
pub const Headers = @import("http/Headers.zig");
pub const Websocket = @import("http/Websocket.zig");
pub const Query = @import("http/Query.zig");
pub const MultipartQuery = @import("http/MultipartQuery.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 fn RoutedServer(Routes: type) type {
_ = Routes;
return struct {
allocator: std.mem.Allocator,
logger: jetzig.loggers.Logger,
@ -75,7 +74,7 @@ pub fn RoutedServer(Routes: type) type {
const HttpzHandler = struct {
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 {
self.server.processNextRequest(request, response) catch |err| {
@ -215,14 +214,15 @@ pub fn RoutedServer(Routes: type) type {
};
return try httpz.upgradeWebsocket(
jetzig.http.Websocket,
jetzig.websockets.RoutedWebsocket(Routes),
httpz_request,
httpz_response,
jetzig.http.Websocket.Context{
jetzig.websockets.Context{
.allocator = self.allocator,
.route = route,
.session_id = session_id,
.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,
);
}
};
}