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
8c2d6806b5
commit
e9802bf546
@ -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",
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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 => {
|
||||
|
@ -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.*,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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 = ¶ms };
|
||||
} 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)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
3
src/jetzig/websockets.zig
Normal file
3
src/jetzig/websockets.zig
Normal 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;
|
136
src/jetzig/websockets/Websocket.zig
Normal file
136
src/jetzig/websockets/Websocket.zig
Normal 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,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user