Did I get async/await in Zig right?

Here is a simple implementation of an event loop with async/await with the two popular functions in JavaScript. setInterval and setTimeout.

Can I get a code review of the same?

const std = @import("std");

const Data = struct {
    val: u64,
    frame: ?anyframe = null,
};

const Node = struct {
    data: Data = undefined,
    next: ?*Node = null,
    prev: ?*Node = null,

    pub fn print(self: *Node) void {
        std.debug.print("Node{{ {d}, ", .{self.data.val});
        if (self.data.frame != null) {
            std.debug.print("{s} }}", .{"    <frame>"});
        } else {
            std.debug.print("{s} }}", .{"<nullframe>"});
        }
    }
};

const RingBuffer = struct {
    head: ?*Node = null,
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator) !RingBuffer {
        return RingBuffer{ .allocator = allocator };
    }

    pub fn deinit(self: *RingBuffer) void {
        var node: ?*Node = self.head;

        while (node) |n| {
            var next = n.next;
            self.allocator.destroy(n);
            node = next;
            if (node == self.head) break;
        }
    }

    pub fn hasSingleNode(self: *RingBuffer) bool {
        return if (self.head) |h| h.next == h else false;
    }

    pub fn add(self: *RingBuffer, val: u64, frame: ?anyframe) !*Node {
        var node = try self.allocator.create(Node);
        node.* = Node{
            .data = Data{ .val = val, .frame = frame },
            .next = self.head,
        };

        if (self.head) |head| {
            node.prev = head.prev;
            head.prev.?.next = node;
            head.prev = node;
        } else {
            node.next = node;
            node.prev = node;
        }
        self.head = node;
        return node;
    }

    pub fn getHead(self: *RingBuffer) ?Data {
        return if (self.head) |head| head.data else null;
    }

    pub fn remove(self: *RingBuffer, node: *Node) !Data {
        var data: Data = undefined;
        if (node.next == node and node.prev == node) {
            data = node.data;
            self.head = null;
            self.allocator.destroy(node);
            return data;
        }

        data = node.data;
        node.prev.?.next = node.next;
        node.next.?.prev = node.prev;
        if (self.head == node) {
            self.head = node.next;
        }
        self.allocator.destroy(node);
        return data;
    }

    pub fn print(self: *RingBuffer) void {
        var head = self.head;
        var node: ?*Node = head;
        std.debug.print("\n", .{});
        var i: u64 = 0;
        while (node) |n| : (i += 1) {
            n.print();
            if (n.next == head) break;
            std.debug.print(" <-> ", .{});
            node = n.next;
        }
        std.debug.print("\n", .{});
    }
};

const EventLoop = struct {
    allocator: std.mem.Allocator,
    ringBuffer: RingBuffer,

    current: ?*Node = null,

    fn init(allocator: std.mem.Allocator) !*EventLoop {
        var loop = try allocator.create(EventLoop);
        loop.allocator = allocator;
        loop.ringBuffer = try RingBuffer.init(allocator);
        return loop;
    }

    fn deinit(self: *EventLoop) void {
        self.ringBuffer.deinit();
        self.allocator.destroy(self);
    }

    fn push(self: *EventLoop, val: u64, frame: ?anyframe) !*Node {
        return self.ringBuffer.add(val, frame);
    }

    fn pop(self: *EventLoop, node: *Node) !Data {
        return try self.ringBuffer.remove(node);
    }

    fn advance(self: *EventLoop) void {
        if (self.current) |c| self.current = c.next;
    }

    fn run(self: *EventLoop) !void {
        self.current = self.ringBuffer.head;

        while (self.current) |current| {
            if (self.ringBuffer.head == null) break;

            if (current.data.frame) |f| {
                self.advance();
                resume f;
            } else {
                self.advance();
            }
        }
    }
};

const SetTimeoutCallbackType = fn (loop: *EventLoop) void;

fn setTimeout(loop: *EventLoop, callback: SetTimeoutCallbackType, timeout: i64) !void {
    const curr_time = std.time.milliTimestamp();
    var node = try loop.push(@intCast(u64, timeout), null);

    while (true) {
        const new_time = std.time.milliTimestamp();
        if (new_time - curr_time >= timeout) {
            break;
        } else {
            suspend {
                node.data.frame = @frame();
            }
        }
    }

    _ = try loop.pop(node);
    callback(loop);
}

fn setInterval(loop: *EventLoop, callback: SetTimeoutCallbackType, interval: i64) !void {
    var _time: u64 = @intCast(u64, std.time.milliTimestamp());
    var node = try loop.push(_time, null);

    while (true) {
        var curr_time = @intCast(u64, std.time.milliTimestamp());
        const prev_time = node.data.val;
        if (curr_time - prev_time >= interval) {
            node.data.val = @intCast(u64, curr_time);
            callback(loop);
        }
        suspend {
            node.data.frame = @frame();
        }
    }
    return node;
}

fn setTimeoutCallback1(_: *EventLoop) void {
    std.debug.print("setTimeoutCallback 100!!\n", .{});
}

fn setTimeoutCallback2(_: *EventLoop) void {
    std.debug.print("setTimeoutCallback 200!!\n", .{});
}

fn setIntervalCallback(_: *EventLoop) void {
    std.debug.print("setIntervalCallback 1000!!\n", .{});
}

fn asyncMain(loop: *EventLoop) !void {
    _ = async setTimeout(loop, setTimeoutCallback1, 100);
    _ = async setTimeout(loop, setTimeoutCallback2, 200);

    _ = async setInterval(loop, setIntervalCallback, 2000);
    suspend {}
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    defer _ = gpa.deinit();

    var loop = try EventLoop.init(allocator);
    defer loop.deinit();

    _ = async asyncMain(loop);

    std.debug.print("start\n", .{});

    defer std.debug.print("end\n", .{});

    try loop.run();
}

“async” word is very popular and seems to be like a kind of fetish these days.
under the hood it is (usually) a co-routines, which are (more or less smartly)
made-up by a compiler.

but there are some other ways to make your application to do many things concurrently,
see my examples of doing concurrency with state machines here

and here