Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add polling #136

Closed
wants to merge 17 commits into from
10 changes: 10 additions & 0 deletions docs/config/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const example = `event MyEvent = {
bar: u8,
},
}`

const pollingExample = `-- Example of using a Zap polling event on the server.
for player, payload in zap.my_event do
print(player, payload)
end`
</script>

# Events
Expand Down Expand Up @@ -43,6 +48,11 @@ This field determines how the event is listened to on the receiving side.
- `ManySync` events can be listened to by many functions, and they are called synchronously.
- `SingleAsync` events can be listened to by one function, and they are called asynchronously.
- `SingleSync` events can be listened to by one function, and they are called synchronously.
- `Polling` events are received by iterating through the event.

### Polling example

<CodeBlock :code="pollingExample" />

::: danger
Synchronous events are not recommended, and should only be used when performance is critical.
Expand Down
12 changes: 12 additions & 0 deletions docs/config/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ The paths are relative to the configuration file and should point to a lua(u) fi

<CodeBlock :code="outputExample" />

## `call_default`

Determines the default `call` field for events. See [here](events.html#call) for possible options.

### Default

None, all event declarations will need a `call` field.

### Example

<CodeBlock code = 'opt call_default = "ManySync"' />

## `remote_scope`

This option changes the name of the remotes generated by Zap.
Expand Down
7 changes: 7 additions & 0 deletions zap/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ impl<'src> Config<'src> {
}
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EventHandling {
Polling,
Signal,
}

sasial-dev marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Debug, Clone, Copy)]
pub enum Casing {
Pascal,
Expand Down Expand Up @@ -115,6 +121,7 @@ pub enum EvCall {
SingleAsync,
ManySync,
ManyAsync,
Polling,
}

#[derive(Debug, Clone)]
Expand Down
4 changes: 4 additions & 0 deletions zap/src/output/luau/base.luau
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local polling_payload_queues = {}
local polling_queue_cursors = {}
local polling_iterators = {}

local outgoing_buff: buffer
local outgoing_used: number
local outgoing_size: number
Expand Down
217 changes: 167 additions & 50 deletions zap/src/output/luau/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ impl<'src> ClientOutput<'src> {

let fire = self.config.casing.with("Fire", "fire", "fire");
let set_callback = self.config.casing.with("SetCallback", "setCallback", "set_callback");
let iter = self.config.casing.with("Iter", "iter", "iter");
let on = self.config.casing.with("On", "on", "on");
let call = self.config.casing.with("Call", "call", "call");

Expand All @@ -59,19 +60,30 @@ impl<'src> ClientOutput<'src> {
self.push_line(&format!("{send_events} = noop,"));

for ev in self.config.evdecls.iter() {
self.push_line(&format!("{name} = table.freeze({{", name = ev.name));
if ev.call == EvCall::Polling && ev.from == EvSource::Server {
self.push_line(&format!("{name} = table.freeze(setmetatable({{", name = ev.name));
} else {
self.push_line(&format!("{name} = table.freeze({{", name = ev.name));
}
self.indent();

if ev.from == EvSource::Client {
self.push_line(&format!("{fire} = noop"));
self.push_line(&format!("{fire} = noop,"));
} else {
match ev.call {
EvCall::SingleSync | EvCall::SingleAsync => self.push_line(&format!("{set_callback} = noop")),
EvCall::ManySync | EvCall::ManyAsync => self.push_line(&format!("{on} = noop")),
EvCall::SingleSync | EvCall::SingleAsync => self.push_line(&format!("{set_callback} = noop,")),
EvCall::ManySync | EvCall::ManyAsync => self.push_line(&format!("{on} = noop,")),
EvCall::Polling => self.push_line(&format!("{iter} = noop,")),
}
}

self.dedent();
if ev.call == EvCall::Polling && ev.from == EvSource::Server {
self.push_line("}), {");
self.indent();
self.push_line("__iter = noop,");
self.dedent();
}
self.push_line("}),");
}

Expand Down Expand Up @@ -199,62 +211,73 @@ impl<'src> ClientOutput<'src> {
self.push_stmts(&des::gen(data, "value", true));
}

if ev.call == EvCall::SingleSync || ev.call == EvCall::SingleAsync {
self.push_line(&format!("if events[{id}] then"));
} else {
self.push_line(&format!("if events[{id}][1] then"));
}
match ev.call {
EvCall::Polling => {
// Event types without data use `true` as a placeholder.
self.push_line(&format!(
"table.insert(polling_payload_queues[{id}], if value == nil then true else value)"
));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if true is being used as a placeholder, then what happens to real true values? I recommend we use some "null" userdata to represent nil

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe we use a counter for events w/o data instead of an array - sorta like how it works already for queuing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe we use a counter for events w/o data instead of an array - sorta like how it works already for queuing

Something needs to be returned to the loop or iteration will end, there's no player to return on the client.

Copy link
Contributor Author

@Ketasaja Ketasaja Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if true is being used as a placeholder, then what happens to real true values? I recommend we use some "null" userdata to represent nil

I don't see much benefit to this unless you assign a metatable that errors on everything, but you don't like metatables. I could make this change though.

for meaningless_value in event do
    if meaningless_value then
        -- Same behavior either way.
    end
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd also need to lie to the type system because newproxy(true) returns any, which would be worse than true.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to do something because if we use true then boolean? becomes impossible

}
_ => {
if ev.call == EvCall::SingleSync || ev.call == EvCall::SingleAsync {
self.push_line(&format!("if events[{id}] then"));
} else {
self.push_line(&format!("if events[{id}][1] then"));
}

self.indent();
self.indent();

if ev.call == EvCall::ManySync || ev.call == EvCall::ManyAsync {
self.push_line(&format!("for _, cb in events[{id}] do"));
self.indent();
}
if ev.call == EvCall::ManySync || ev.call == EvCall::ManyAsync {
self.push_line(&format!("for _, cb in events[{id}] do"));
self.indent();
}

match ev.call {
EvCall::SingleSync => self.push_line(&format!("events[{id}](value)")),
EvCall::SingleAsync => self.push_line(&format!("task.spawn(events[{id}], value)")),
EvCall::ManySync => self.push_line("cb(value)"),
EvCall::ManyAsync => self.push_line("task.spawn(cb, value)"),
}
match ev.call {
EvCall::SingleSync => self.push_line(&format!("events[{id}](value)")),
EvCall::SingleAsync => self.push_line(&format!("task.spawn(events[{id}], value)")),
EvCall::ManySync => self.push_line("cb(value)"),
EvCall::ManyAsync => self.push_line("task.spawn(cb, value)"),
_ => unreachable!(),
}

if ev.call == EvCall::ManySync || ev.call == EvCall::ManyAsync {
self.dedent();
self.push_line("end");
}
if ev.call == EvCall::ManySync || ev.call == EvCall::ManyAsync {
self.dedent();
self.push_line("end");
}

self.dedent();
self.push_line("else");
self.indent();
self.dedent();
self.push_line("else");
self.indent();

if ev.data.is_some() {
self.push_line(&format!("table.insert(event_queue[{id}], value)"));
self.push_line(&format!("if #event_queue[{id}] > 64 then"));
} else {
self.push_line(&format!("event_queue[{id}] += 1"));
self.push_line(&format!("if event_queue[{id}] > 16 then"));
}
if ev.data.is_some() {
self.push_line(&format!("table.insert(event_queue[{id}], value)"));
self.push_line(&format!("if #event_queue[{id}] > 64 then"));
} else {
self.push_line(&format!("event_queue[{id}] += 1"));
self.push_line(&format!("if event_queue[{id}] > 16 then"));
}

self.indent();
self.push_indent();
self.indent();
self.push_indent();

self.push("warn(`[ZAP] {");
self.push("warn(`[ZAP] {");

if ev.data.is_some() {
self.push("#")
}
if ev.data.is_some() {
self.push("#")
}

self.push(&format!(
"event_queue[{id}]}} events in queue for {}. Did you forget to attach a listener?`)\n",
ev.name
));
self.push(&format!(
"event_queue[{id}]}} events in queue for {}. Did you forget to attach a listener?`)\n",
ev.name
));

self.dedent();
self.push_line("end");
self.dedent();
self.push_line("end");

self.dedent();
self.push_line("end");
self.dedent();
self.push_line("end");
}
}

self.dedent();
}
Expand Down Expand Up @@ -393,6 +416,7 @@ impl<'src> ClientOutput<'src> {
EvCall::SingleAsync => self.push_line(&format!("task.spawn(events[{id}], value)")),
EvCall::ManySync => self.push_line("cb(value)"),
EvCall::ManyAsync => self.push_line("task.spawn(cb, value)"),
_ => (),
}

if ev.call == EvCall::ManySync || ev.call == EvCall::ManyAsync {
Expand Down Expand Up @@ -691,23 +715,66 @@ impl<'src> ClientOutput<'src> {
self.push_line("end,");
}

fn push_explicit_iter(&mut self, ev: &EvDecl) {
let iter = self.config.casing.with("Iter", "iter", "iter");
let id = ev.id;
self.push_indent();
self.push(&format!("{iter} = polling_iterators[{id}] :: () -> "));
if let Some(data) = &ev.data {
self.push("(() -> ");
self.push_ty(&data);
self.push(")");
} else {
self.push("(() -> true)")
Ketasaja marked this conversation as resolved.
Show resolved Hide resolved
}
self.push(",\n");
}

pub fn push_return_listen(&mut self) {
for ev in self
.config
.evdecls
.iter()
.filter(|ev_decl| ev_decl.from == EvSource::Server)
{
self.push_line(&format!("{name} = {{", name = ev.name));
if let EvCall::Polling = ev.call {
self.push_line(&format!("{name} = setmetatable({{", name = ev.name));
} else {
self.push_line(&format!("{name} = {{", name = ev.name));
}
self.indent();

if let EvCall::Polling = ev.call {
self.push_explicit_iter(ev);
}

match ev.call {
EvCall::SingleSync | EvCall::SingleAsync => self.push_return_setcallback(ev),
EvCall::ManySync | EvCall::ManyAsync => self.push_return_on(ev),
_ => (),
}
Ketasaja marked this conversation as resolved.
Show resolved Hide resolved

self.dedent();
self.push_line("},");
match ev.call {
EvCall::Polling => {
self.push_indent();
self.push("}, {\n");
self.indent();
let id = ev.id;
self.push_indent();
self.push(&format!("__iter = polling_iterators[{id}] :: () -> "));
if let Some(data) = &ev.data {
self.push("(() -> ");
self.push_ty(&data);
self.push("),\n");
} else {
self.push("(() -> true),\n")
}
self.dedent();
self.push_line("}),");
}
_ => self.push_line("},"),
}
}
}

Expand Down Expand Up @@ -807,6 +874,54 @@ impl<'src> ClientOutput<'src> {
}
}

pub fn push_polling(&mut self) {
let filtered_evdecls = self
.config
.evdecls
.iter()
.filter(|evdecl| evdecl.from == EvSource::Server)
.filter(|evdecl| evdecl.call == EvCall::Polling);

let is_polling_used = filtered_evdecls.clone().next().is_some();
if is_polling_used {
self.push_line("");
}

for evdecl in filtered_evdecls {
let id = evdecl.id;
self.push_line(&format!("polling_payload_queues[{id}] = {{}}"));
self.push_line(&format!("polling_queue_cursors[{id}] = 0"));

self.push_line(&format!("polling_iterators[{id}] = function()"));
self.indent();
self.push_line(&format!("return function()"));
self.indent();
self.push_line(&format!("local payload_queue = polling_payload_queues[{id}]"));
self.push_line(&format!("local cursor = polling_queue_cursors[{id}] + 1"));
self.push_line(&format!("local value = payload_queue[cursor]"));
self.push_line(&format!("if value then"));
self.indent();
self.push_line(&format!("polling_queue_cursors[{id}] = cursor"));
self.push_line("payload_queue[cursor] = nil");
self.push_line("return value");
self.dedent();
self.push_line("else");
self.indent();
self.push_line(&format!("polling_queue_cursors[{id}] = 0"));
self.push_line("return nil");
self.dedent();
self.push_line("end");
self.dedent();
self.push_line("end");
self.dedent();
self.push_line("end");
}
self.push_line("");
self.push_line("table.freeze(polling_payload_queues)");
self.push_line("table.freeze(polling_iterators)");
self.push_line("");
}

pub fn push_return(&mut self) {
self.push_line("local returns = {");
self.indent();
Expand Down Expand Up @@ -896,6 +1011,8 @@ impl<'src> ClientOutput<'src> {
self.push_unreliable();
}

self.push_polling();

self.push_return();

self.buf
Expand Down
Loading
Loading