This commit is contained in:
Bob Farrell 2025-05-03 16:21:54 +01:00
parent b795c6184e
commit d3113d12fe
6 changed files with 149 additions and 136 deletions

View File

@ -2,33 +2,36 @@
<div id="party-container"></div>
<div id="results-wrapper">
<span class="trophy">&#127942;</span>
<div id="results">
<div>Player</div>
<div id="player-wins" jetzig-connect="$.results.player"></div>
<div>CPU</div>
<div id="cpu-wins" jetzig-connect="$.results.cpu"></div>
<div>Tie</div>
<div id="ties" jetzig-connect="$.results.tie"></div>
</div>
<span class="trophy">&#127942;</span>
</div>
<div class="board" id="board">
@for (0..9) |index| {
<div
class="cell"
jetzig-connect="$.cells.{{index}}"
jetzig-transform="{ player: '&#9992;&#65039;', cpu: '&#129422;', tie: '&#129309;' }[value] || ''"
jetzig-click="move"
id="tic-tac-toe-cell-{{index}}"
data-cell="{{index}}"
>
<jetzig-scope name="game">
<div id="results-wrapper">
<span class="trophy">&#127942;</span>
<div id="results">
<div>Player</div>
<div id="player-wins" jetzig-scope="game" jetzig-connect="$.results.player"></div>
<div>CPU</div>
<div id="cpu-wins" jetzig-scope="game" jetzig-connect="$.results.cpu"></div>
<div>Tie</div>
<div id="ties" jetzig-scope="game" jetzig-connect="$.results.tie"></div>
</div>
}
</div>
<span class="trophy">&#127942;</span>
</div>
<div class="board" id="board">
@for (0..9) |index| {
<div
class="cell"
jetzig-connect="$.cells.{{index}}"
jetzig-transform="{ player: '&#9992;&#65039;', cpu: '&#129422;', tie: '&#129309;' }[value] || ''"
jetzig-scope="game"
jetzig-click="move"
id="tic-tac-toe-cell-{{index}}"
data-cell="{{index}}"
>
</div>
}
</div>
</jetzig-scope>
<div id="reset-wrapper">
<button jetzig-click="reset" id="reset-button">Reset Game</button>
@ -38,23 +41,24 @@
<span>&#127942;</span>
<span
jetzig-connect="$.victor"
jetzig-scope="game"
jetzig-transform="{ player: '&#9992;&#65039;', cpu: '&#129422;', tie: '&#129309;' }[value] || ''"
></span>
<span>&#127942;</span>
</div>
<script>
@// jetzig.channel.onStateChanged(state => {
@// Jetzig.channel.onStateChanged((scope, state) => {
@// });
@//
@// jetzig.channel.onMessage(data => {
@// Jetzig.channel.onMessage(data => {
@// });
@//
@// jetzig.channel.receive("victor", data => {
@// Jetzig.channel.receive("victor", data => {
@// triggerPartyAnimation();
@// });
@//
@// jetzig.channel.receive("game_over", data => {
@// Jetzig.channel.receive("game_over", data => {
@// const element = document.querySelector("#board");
@// element.classList.remove('flash-animation');
@// void element.offsetWidth;

View File

@ -23,8 +23,8 @@ pub fn RoutedChannel(Routes: type) type {
env: Env,
const Connection = struct {
object: *jetzig.data.Value,
data: *jetzig.Data,
state: *jetzig.data.Value,
key: []const u8,
};
pub fn init(allocator: std.mem.Allocator, websocket: *jetzig.websockets.Websocket) !Channel {
@ -86,7 +86,9 @@ pub fn RoutedChannel(Routes: type) type {
pub fn connect(channel: Channel, comptime scope: []const u8) !*jetzig.data.Value {
if (channel._connections.get(scope)) |cached| {
return cached.object;
// Ensure an identical value is returned for each invocation of `connect` for a
// given scope.
return cached.state;
}
if (channel.websocket.session_id.len != 32) return error.JetzigInvalidSessionIdLength;
@ -105,19 +107,14 @@ pub fn RoutedChannel(Routes: type) type {
break :blk id;
};
var buf: [32 + ":".len + 32]u8 = undefined;
const connection_key = try std.fmt.bufPrint(&buf, "{s}:{s}", .{ channel.websocket.session_id, connection_id });
return try channel.websocket.channels.get(channel.data, connection_key) orelse blk: {
const data = try channel.allocator.create(jetzig.Data);
data.* = jetzig.Data.init(channel.allocator);
const object = try data.root(.object);
const duped_connection_key = try channel.allocator.dupe(u8, connection_key);
try channel.websocket.channels.put(duped_connection_key, object);
try channel._connections.put(scope, .{ .data = data, .object = object });
const connections_state = channel.get("_connections_state") orelse try channel.put("_connections_state", .object);
try connections_state.put(scope, object);
break :blk object;
const connection_key = try std.fmt.allocPrint(channel.allocator, "{s}:{s}", .{ channel.websocket.session_id, connection_id });
const state = try channel.websocket.channels.get(channel.data, connection_key) orelse blk: {
const state = try channel.data.object();
try channel.websocket.channels.put(connection_key, state);
break :blk state;
};
try channel._connections.put(scope, .{ .key = connection_key, .state = state });
return state;
}
pub fn getT(
@ -145,12 +142,13 @@ pub fn RoutedChannel(Routes: type) type {
}
pub fn sync(channel: Channel) !void {
try channel.websocket.syncState(channel.data, "__root__");
try channel.websocket.syncState(channel.state, "__root__", channel.websocket.session_id);
var it = channel._connections.iterator();
while (it.next()) |entry| {
const data = entry.value_ptr.*.data;
const connection = entry.value_ptr.*;
const scope = entry.key_ptr.*;
try channel.websocket.syncState(data, scope);
try channel.websocket.syncState(connection.state, scope, connection.key);
}
}
};

View File

@ -155,8 +155,7 @@ pub fn Store(comptime options: KVOptions) type {
fn parseValue(data: *jetzig.data.Data, maybe_json: ?[]const u8) !?*jetzig.data.Value {
if (maybe_json) |json| {
try data.fromJson(json);
return data.value.?;
return try data.parseJsonSlice(json);
} else {
return null;
}

View File

@ -21,7 +21,7 @@ pub const Blocks = struct {
\\<script>
\\ (() => {{
\\ window.addEventListener('DOMContentLoaded', () => {{
\\ jetzig.channel.init("{s}", "{s}");
\\ Jetzig.channel.init("{s}", "{s}");
\\ }});
\\ }})();
\\</script>

View File

@ -1,5 +1,5 @@
window.jetzig = window.jetzig ? window.jetzig : {}
jetzig = window.jetzig;
window.Jetzig = window.Jetzig ? window.Jetzig : {}
const Jetzig = window.Jetzig;
(() => {
const state_tag = "__jetzig_channel_state__:";
@ -8,7 +8,7 @@ jetzig = window.jetzig;
const transform = (value, state, element) => {
const id = element.getAttribute('jetzig-id');
const transformer = id && jetzig.channel.transformers[id];
const transformer = id && Jetzig.channel.transformers[id];
if (transformer) {
return transformer(value, state, element);
} else {
@ -38,12 +38,14 @@ jetzig = window.jetzig;
const detagged = event.data.slice(state_tag.length);
const scope = detagged.split(':', 1)[0];
const state = JSON.parse(detagged.slice(scope.length + 1));
Object.entries(channel.elementMap).forEach(([ref, elements]) => {
console.log(scope, state);
Object.entries(channel.scopedElements(scope)).forEach(([ref, elements]) => {
const value = reduceState(ref, state);
console.log(ref, state);
elements.forEach(element => element.innerHTML = transform(value, state, element));
});
channel.stateChangedCallbacks.forEach((callback) => {
callback(state);
callback(scope, state);
});
};
@ -61,40 +63,40 @@ jetzig = window.jetzig;
data.actions.forEach(action => {
channel.action_specs[action.name] = {
callback: (...params) => {
if (action.params.length != params.length) {
throw new Error(`Invalid params for action '${action.name}'. Expected ${action.params.length} params, found ${params.length}`);
}
[...action.params].forEach((param, index) => {
if (param.type !== typeof params[index]) {
const err = `Incorrect argument type for argument ${index} in '${action.name}'. Expected: ${param.type}, found ${typeof params[index]}`;
switch (param.type) {
case "string":
params[index] = `${params[index]}`;
break;
case "integer":
try { params[index] = parseInt(params[index]) } catch {
throw new Error(err);
};
break;
case "float":
try { params[index] = parseFloat(params[index]) } catch {
throw new Error(err);
};
case "boolean":
params[index] = ["true", "y", "1", "yes", "t"].includes(params[index]);
break;
default:
throw new Error(err);
}
if (action.params.length != params.length) {
throw new Error(`Invalid params for action '${action.name}'. Expected ${action.params.length} params, found ${params.length}`);
}
});
channel.websocket.send(`_invoke:${action.name}:${JSON.stringify(params)}`);
[...action.params].forEach((param, index) => {
if (param.type !== typeof params[index]) {
const err = `Incorrect argument type for argument ${index} in '${action.name}'. Expected: ${param.type}, found ${typeof params[index]}`;
switch (param.type) {
case "string":
params[index] = `${params[index]}`;
break;
case "integer":
try { params[index] = parseInt(params[index]) } catch {
throw new Error(err);
};
break;
case "float":
try { params[index] = parseFloat(params[index]) } catch {
throw new Error(err);
};
case "boolean":
params[index] = ["true", "y", "1", "yes", "t", true].includes(params[index]);
break;
default:
throw new Error(err);
}
}
});
channel.websocket.send(`_invoke:${action.name}:${JSON.stringify(params)}`);
},
spec: { ...action },
};
channel.actions[action.name] = channel.action_specs[action.name].callback;
spec: { ...action },
};
channel.actions[action.name] = channel.action_specs[action.name].callback;
});
document.querySelectorAll('[jetzig-click]').forEach(element => {
const ref = element.getAttribute('jetzig-click');
const action = channel.action_specs[ref];
@ -117,7 +119,58 @@ jetzig = window.jetzig;
});
};
jetzig.channel = {
const initElementConnections = (channel) => {
document.querySelectorAll('[jetzig-connect]').forEach(element => {
const ref = element.getAttribute('jetzig-connect');
const id = `jetzig-${crypto.randomUUID()}`;
element.setAttribute('jetzig-id', id);
const scope = element.getAttribute('jetzig-scope') || '__root__';
if (!channel.elementMap[scope]) channel.elementMap[scope] = {};
if (!channel.elementMap[scope][ref]) channel.elementMap[scope][ref] = [];
channel.elementMap[scope][ref].push(element);
const transformer = element.getAttribute('jetzig-transform');
if (transformer) {
channel.transformers[id] = new Function("value", "$", "element", `return ${transformer};`);
}
});
};
const initStyledElements = (channel) => {
const styled_elements = document.querySelectorAll('[jetzig-style]');
channel.onStateChanged(state => {
styled_elements.forEach(element => {
const func = new Function("$", `return ${element.getAttribute('jetzig-style')};`)
const styles = func(state);
Object.entries(styles).forEach(([key, value]) => {
element.style.setProperty(key, value);
});
});
});
};
const initWebsocket = (channel, host, path) => {
channel.websocket = new WebSocket(`ws://${host}${path}`);
channel.websocket.addEventListener("message", (event) => {
if (event.data.startsWith(state_tag)) {
handleState(event, channel);
} else if (event.data.startsWith(event_tag)) {
handleEvent(event, channel);
} else if (event.data.startsWith(actions_tag)) {
handleAction(event, channel);
} else {
const data = JSON.parse(event.data);
channel.messageCallbacks.forEach((callback) => {
callback(data);
});
}
});
channel.websocket.addEventListener("open", (event) => {
// TODO
channel.publish("websockets", {});
});
};
Jetzig.channel = {
websocket: null,
actions: {},
action_specs: {},
@ -128,50 +181,11 @@ jetzig = window.jetzig;
transformers: {},
onStateChanged: function(callback) { this.stateChangedCallbacks.push(callback); },
onMessage: function(callback) { this.messageCallbacks.push(callback); },
scopedElements: function(scope) { return this.elementMap[scope] || {}; },
init: function(host, path) {
this.websocket = new WebSocket(`ws://${host}${path}`);
this.websocket.addEventListener("message", (event) => {
if (event.data.startsWith(state_tag)) {
handleState(event, this);
} else if (event.data.startsWith(event_tag)) {
handleEvent(event, this);
} else if (event.data.startsWith(actions_tag)) {
handleAction(event, this);
} else {
const data = JSON.parse(event.data);
this.messageCallbacks.forEach((callback) => {
callback(data);
});
}
});
document.querySelectorAll('[jetzig-connect]').forEach(element => {
const ref = element.getAttribute('jetzig-connect');
if (!this.elementMap[ref]) this.elementMap[ref] = [];
const id = `jetzig-${crypto.randomUUID()}`;
element.setAttribute('jetzig-id', id);
this.elementMap[ref].push(element);
const transformer = element.getAttribute('jetzig-transform');
if (transformer) {
this.transformers[id] = new Function("value", "$", "element", `return ${transformer};`);
}
});
const styled_elements = document.querySelectorAll('[jetzig-style]');
this.onStateChanged(state => {
styled_elements.forEach(element => {
const func = new Function("$", `return ${element.getAttribute('jetzig-style')};`)
const styles = func(state);
Object.entries(styles).forEach(([key, value]) => {
element.style.setProperty(key, value);
});
});
});
// this.websocket.addEventListener("open", (event) => {
// // TODO
// this.publish("websockets", {});
// });
initWebsocket(this, host, path);
initElementConnections(this);
initStyledElements(this);
},
receive: function(ref, callback) {
if (Object.hasOwn(this.invokeCallbacks, ref)) {

View File

@ -93,9 +93,7 @@ pub fn RoutedWebsocket(Routes: type) type {
websocket.logger.DEBUG("Routed Channel message for `{s}`", .{websocket.route.path}) catch {};
}
pub fn syncState(websocket: *Websocket, data: *jetzig.Data, scope: []const u8) !void {
const value = data.value orelse return;
pub fn syncState(websocket: *Websocket, value: *jetzig.data.Value, scope: []const u8, state_key: []const u8) !void {
var stack_fallback = std.heap.stackFallback(4096, websocket.allocator);
const allocator = stack_fallback.get();
@ -105,8 +103,8 @@ pub fn RoutedWebsocket(Routes: type) type {
const writer = write_buffer.writer();
// TODO: Make this really fast.
try websocket.channels.put(websocket.session_id, value);
try writer.print("__jetzig_channel_state__:{s}:{s}", .{ scope, try data.toJson() });
try websocket.channels.put(state_key, value);
try writer.print("__jetzig_channel_state__:{s}:{s}", .{ scope, try value.toJson() });
try write_buffer.flush();
websocket.logger.DEBUG("Synchronized Channel state for `{s}`", .{websocket.route.path}) catch {};