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",
|
.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",
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 => {
|
||||||
|
@ -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.*,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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 = ¶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 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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