From 14688ef87e54b5d01eb7ec4ca125aee9724488e3 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Fri, 5 Dec 2025 23:09:57 +0200
Subject: [PATCH 01/26] ws support
---
.gitignore | 3 +
build.zig | 55 +++++-
build.zig.zon | 14 +-
examples_ws/README.md | 39 ++++
examples_ws/cert/README.md | 114 +++++++++++
examples_ws/cert/fullchain.pem | 34 ++++
examples_ws/cert/private.crt | 34 ++++
examples_ws/cert/private.csr | 29 +++
examples_ws/cert/private.key | 52 +++++
examples_ws/cert/privkey.pem | 52 +++++
examples_ws/cert/ssl.conf | 35 ++++
examples_ws/example_ws_1.zig | 209 ++++++++++++++++++++
examples_ws/example_ws_2.zig | 233 ++++++++++++++++++++++
examples_ws/example_ws_3.zig | 179 +++++++++++++++++
src/http/context.zig | 3 +
src/http/lib.zig | 6 +
src/http/request.zig | 12 +-
src/http/server.zig | 70 +++++--
src/http/websocket.zig | 198 +++++++++++++++++++
src/lib.zig | 19 ++
src/websocket/pubsub.zig | 298 +++++++++++++++++++++++++++++
src/websocket/websocket.zig | 340 +++++++++++++++++++++++++++++++++
22 files changed, 2006 insertions(+), 22 deletions(-)
create mode 100644 examples_ws/README.md
create mode 100644 examples_ws/cert/README.md
create mode 100644 examples_ws/cert/fullchain.pem
create mode 100644 examples_ws/cert/private.crt
create mode 100644 examples_ws/cert/private.csr
create mode 100644 examples_ws/cert/private.key
create mode 100644 examples_ws/cert/privkey.pem
create mode 100644 examples_ws/cert/ssl.conf
create mode 100644 examples_ws/example_ws_1.zig
create mode 100644 examples_ws/example_ws_2.zig
create mode 100644 examples_ws/example_ws_3.zig
create mode 100644 src/http/websocket.zig
create mode 100644 src/websocket/pubsub.zig
create mode 100644 src/websocket/websocket.zig
diff --git a/.gitignore b/.gitignore
index 2c30843..87bbe59 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,6 @@ zig-out/
.zig-cache/
perf*.data*
heaptrack*
+/ex_ws_1
+/ex_ws_2
+/ex_ws_3
diff --git a/build.zig b/build.zig
index 037a9eb..b8e916c 100644
--- a/build.zig
+++ b/build.zig
@@ -2,8 +2,9 @@ const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
- const optimize = b.standardOptimizeOption(.{});
-
+ //const optimize = b.standardOptimizeOption(.{}); // -O Debug
+ const optimize = std.builtin.OptimizeMode.ReleaseFast; // -O ReleaseFast
+
const zzz = b.addModule("zzz", .{
.root_source_file = b.path("src/lib.zig"),
.target = target,
@@ -35,7 +36,55 @@ pub fn build(b: *std.Build) void {
if (target.result.os.tag != .windows) {
add_example(b, "unix", false, target, optimize, zzz);
}
-
+
+
+
+ const example1_ws_step = b.step("ex_ws_1", "Build ws example1");
+ const example2_ws_step = b.step("ex_ws_2", "Build ws example2");
+ const example3_ws_step = b.step("ex_ws_3", "Build ws example3");
+
+ const exe_ws1 = b.addExecutable(.{ // without C libs
+ .name = "ex_ws_1", // exe name
+ .root_source_file = b.path("examples_ws/example_ws_1.zig"), // b.path("src/test1.zig"), // main file
+ .target = target,
+ .optimize = optimize,
+ });
+ const exe_ws2 = b.addExecutable(.{ // without C libs
+ .name = "ex_ws_2", // exe name
+ .root_source_file = b.path("examples_ws/example_ws_2.zig"), // main file
+ .target = target,
+ .optimize = optimize,
+ });
+ const exe_ws3 = b.addExecutable(.{ // without C libs
+ .name = "ex_ws_3", // exe name
+ .root_source_file = b.path("examples_ws/example_ws_3.zig"), // main file
+ .target = target,
+ .optimize = optimize,
+ });
+
+ exe_ws1.root_module.addImport("zzz", zzz);
+ exe_ws2.root_module.addImport("zzz", zzz);
+ exe_ws3.root_module.addImport("zzz", zzz);
+
+ const install_ws1 = b.addInstallBinFile(exe_ws1.getEmittedBin(), "../../ex_ws_1"); // b.addInstallBinFile(exe_ws1.getEmittedBin(), "ex_ws_1"); // -femit-bin=ex_ws_1 // to project root
+ b.getInstallStep().dependOn(&install_ws1.step);
+ example1_ws_step.dependOn(&install_ws1.step);
+ const install_ws2 = b.addInstallBinFile(exe_ws2.getEmittedBin(), "../../ex_ws_2"); // to project root
+ b.getInstallStep().dependOn(&install_ws2.step);
+ example2_ws_step.dependOn(&install_ws2.step);
+ const install_ws3 = b.addInstallBinFile(exe_ws3.getEmittedBin(), "../../ex_ws_3"); // to project root
+ b.getInstallStep().dependOn(&install_ws3.step);
+ example3_ws_step.dependOn(&install_ws3.step);
+ b.default_step = b.getInstallStep();
+ //b.installArtifact(exe_test1); // saves to /zig-out/bin/test1
+
+ const all_ws_examples_step = b.step("examples_ws", "Build all WS examples");
+ all_ws_examples_step.dependOn(&install_ws1.step);
+ all_ws_examples_step.dependOn(&install_ws2.step);
+ all_ws_examples_step.dependOn(&install_ws3.step);
+
+
+
const tests = b.addTest(.{
.name = "tests",
.root_source_file = b.path("./src/tests.zig"),
diff --git a/build.zig.zon b/build.zig.zon
index 2200eb4..ae04fa1 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -2,15 +2,19 @@
.name = .zzz,
.fingerprint = 0xc3273dca261a7ae0,
.version = "0.3.0",
- .minimum_zig_version = "0.14.0",
+ .minimum_zig_version = "0.14.1",
.dependencies = .{
.tardy = .{
- .url = "git+https://github.com/tardy-org/tardy?ref=v0.3.0#cd454060f3b6006368d53c05ab96cd16c73c34de",
- .hash = "tardy-0.3.0-69wrgi7PAwDFhO7m0aXae6N15s2b28VIOrnRrSHHake6",
+ //.url = "git+https://github.com/tardy-org/tardy?ref=v0.3.0#cd454060f3b6006368d53c05ab96cd16c73c34de",
+ //.hash = "tardy-0.3.0-69wrgi7PAwDFhO7m0aXae6N15s2b28VIOrnRrSHHake6",
+ .url = "git+https://github.com/221V/tardy?ref=zig-0.14#d0364f3142cb859474cd23ccc9da1c97d9dc1e05",
+ .hash = "tardy-0.3.0-zwolxXHNAwA6tH6Xr1qgUIAyHy6o4-hgOSVOlGBt6jjL",
},
.secsock = .{
- .url = "git+https://github.com/tardy-org/secsock?ref=v0.1.0#263dcd630e32c7a5c7a0522a8d1fd04e39b75c24",
- .hash = "secsock-0.0.0-p0qurf09AQD95s1NQF2MGpBqMmFz7cKZWibsgv_SQBAr",
+ //.url = "git+https://github.com/tardy-org/secsock?ref=v0.1.0#263dcd630e32c7a5c7a0522a8d1fd04e39b75c24",
+ //.hash = "secsock-0.0.0-p0qurf09AQD95s1NQF2MGpBqMmFz7cKZWibsgv_SQBAr",
+ .url = "git+https://github.com/221V/secsock?ref=zig-0.14#625147512dcacbfe5fa35cab2f94332ba76cd563",
+ .hash = "secsock-0.0.0-p0qurcM-AQCPUYli5tuTIPncDh3bKpHE-yBEkY5Zma1E",
},
},
diff --git a/examples_ws/README.md b/examples_ws/README.md
new file mode 100644
index 0000000..90f3b8f
--- /dev/null
+++ b/examples_ws/README.md
@@ -0,0 +1,39 @@
+# zzz webserver ws examples
+
+```
+zig 0.14.1
+
+zig build
+zig build ex_ws_1
+zig build ex_ws_2
+zig build ex_ws_3
+
+# ws example
+./ex_ws_1
+// goto http://localhost:3010/ , open browser webmaster console
+ws.send(42);
+ws.send("hello world");
+
+# wss example (with cert)
+sudo ./ex_ws_2
+// goto https://test1.ls/ , open browser webmaster console
+ws.send(42);
+ws.send("hello world");
+
+
+# ws PubSub example
+sudo ./ex_ws_3
+// goto http://localhost:3010/ in one browser(or common window - non-private), open browser webmaster console
+
+// also open the same http://localhost:3010/ in second browser(or private window), open browser webmaster console
+ws.send("general:hello");
+ws.send("general:42");
+ws.send("gen:777");
+
+// check received messages in second window, also send there
+ws.send("general:hello 2");
+ws.send("general:43");
+ws.send("gen:hello");
+// and check received messages in first window
+```
+
diff --git a/examples_ws/cert/README.md b/examples_ws/cert/README.md
new file mode 100644
index 0000000..f655f2d
--- /dev/null
+++ b/examples_ws/cert/README.md
@@ -0,0 +1,114 @@
+# how to wildcard certs
+how to wildcard certs for localhost ssl-http-wss tests --
+tested on (l)ubuntu 22.04 LTS but possible on any linux
+```
+sudo vim /etc/hosts
+
+# add next lines
+127.0.0.1 test1.ls
+127.0.0.1 en.test1.ls
+127.0.0.1 test2.ls
+127.0.0.1 subdomain.test2.ls
+
+# apply changes without reboot (few commands - latest is newest)
+sudo /etc/init.d/networking restart
+# or
+sudo service networking restart
+# or
+sudo /etc/init.d/network-manager restart
+# or
+sudo service network-manager restart
+# or
+sudo systemctl restart NetworkManager.service
+
+# next lets create certs
+mkdir certs
+cd certs
+vim ssl.conf
+
+# add and save next lines
+[ req ]
+default_bits = 4096
+distinguished_name = req_distinguished_name
+req_extensions = req_extensions_section
+
+[ req_distinguished_name ]
+countryName = Country Name (2 letter code)
+countryName_min = 2
+countryName_max = 2
+countryName_default = UA
+stateOrProvinceName = State or Province Name (full name)
+stateOrProvinceName_default = Lviv
+localityName = Locality Name (eg, city)
+localityName_default = Lviv
+organizationName = Organization Name (eg, company)
+organizationName_default = Test
+organizationalUnitName = Organizational Unit Name (eg, section)
+organizationalUnitName_default = IT
+commonName = Common Name (e.g. server FQDN or YOUR name)
+commonName_max = 64
+commonName_default = localhost
+emailAddress = Email Address (eg, admin@example.com)
+emailAddress_max = 64
+emailAddress_default = info@test.com
+
+[ req_extensions_section ]
+subjectAltName = @subject_alternative_name_section
+
+[ subject_alternative_name_section ]
+DNS.1 = test1.ls
+DNS.2 = *.test1.ls
+DNS.3 = test2.ls
+DNS.4 = *.test2.ls
+
+# save file in vim and exit = CTRL + I for editing, next Esc, :w! for save and :q! for exit
+
+# next generate privkey
+openssl genrsa -out private.key 4096
+
+# generate CSR (Certificate Signing Request) - "Common name" is your project name
+openssl req -new -sha256 -out private.csr -key private.key -config ssl.conf
+
+# check CSR
+openssl req -text -noout -in private.csr
+
+# we will see something like next
+...
+X509v3 Subject Alternative Name: DNS:test1.ls
+...
+Signature Algorithm: sha256WithRSAEncryption
+...
+
+# generate cert
+openssl x509 -req -sha256 -days 3650 -in private.csr -signkey private.key -out private.crt -extensions req_extensions_section -extfile ssl.conf
+
+# install cert as trusted in system
+sudo mkdir /usr/share/ca-certificates/extra
+sudo cp private.crt /usr/share/ca-certificates/extra/private.crt
+
+sudo dpkg-reconfigure ca-certificates
+# or
+sudo update-ca-certificates
+
+# next we can use cert, for example, in nginx
+server{
+ listen 443 ssl http2;
+ ssl_certificate /home/user/certs/private.crt;
+ ssl_certificate_key /home/user/certs/private.key;
+ ssl_dhparam /home/user/certs/dhparams.pem;
+
+ ...
+ server_name www.test1.ls;
+ return 301 https://test1.ls$request_uri;
+}
+
+# or use without nginx
+# we can see cert usage in 2nd ws example
+
+# if we have private.csr, private.crt and private.key
+# but needs fullchain.pem and privkey.pem - just copy:
+
+cp private.key privkey.pem
+cp private.crt fullchain.pem
+```
+
diff --git a/examples_ws/cert/fullchain.pem b/examples_ws/cert/fullchain.pem
new file mode 100644
index 0000000..551b11e
--- /dev/null
+++ b/examples_ws/cert/fullchain.pem
@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+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
+ak3F2Cf6v7xZn39/
+-----END CERTIFICATE-----
diff --git a/examples_ws/cert/private.crt b/examples_ws/cert/private.crt
new file mode 100644
index 0000000..551b11e
--- /dev/null
+++ b/examples_ws/cert/private.crt
@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+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
+ak3F2Cf6v7xZn39/
+-----END CERTIFICATE-----
diff --git a/examples_ws/cert/private.csr b/examples_ws/cert/private.csr
new file mode 100644
index 0000000..54cd830
--- /dev/null
+++ b/examples_ws/cert/private.csr
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIFBjCCAu4CAQAweTELMAkGA1UEBhMCVUExDTALBgNVBAgMBEx2aXYxDTALBgNV
+BAcMBEx2aXYxDTALBgNVBAoMBFRlc3QxCzAJBgNVBAsMAklUMRIwEAYDVQQDDAls
+b2NhbGhvc3QxHDAaBgkqhkiG9w0BCQEWDWluZm9AdGVzdC5jb20wggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQDAH7CmRJD/q+uBFXUTIlvLUYQ0XkPdGekU
+kAhqa1i9y15om4nYNk982fk+6gh0xDHqODD9GwWz2rHrmZPyoqE8NAxPti7tTnU1
+DV/d9X8G1pBiyWiiZ0VJ6tSASn7CfngBQpREfjgLtgcs3/rZ9mzcPBQu9blGz4xZ
+690FBhJvS6iS2c8sxvX7H8V3s15+9ffszZxStNeX7UaNLCe96EQUNyt5usiGqR66
+qWshSheLKgLyRRhZ0fJdPIi1aaWSZJJX92PVFOwoKuump4ZNzkfyCxOdIcplcwrr
+nyZsFe26DVOfJA93F9zkiyYXtkCtIsGbyA1peJlOSct0P92GuIz7GB5dFwLu6qHz
+6lSNs+ph2BsQko87RIP3FUEr5VESXjrYOkgiMyiPYO230y4BuCj9gGgktqjn50GC
+OLYBcFXlUCq3n4rnlSiWXm+lr8N5i9teKJWO9qaAo07GKGphO3z8oVhg5oL+WU68
+IT6SFt34RTQ6FnN5AO/SQY6kANgnzEDdppnAHJGJIRx55pCjbTGQ2J0+wlAAHFiR
+tRJDnVkjXc6J6HaZMfD4eHCtnftkCwsDQdCAIgGrJxbBGcErXPaFwXR04YrZHmnm
+8hATrdUgJxFhf0p0xuDyLyl/dOIO4wWBwf48xM8nI9NdUXLGaJrYbdUnj4hFLSW5
+qQ1aINrYZwIDAQABoEgwRgYJKoZIhvcNAQkOMTkwNzA1BgNVHREELjAsggh0ZXN0
+MS5sc4IKKi50ZXN0MS5sc4IIdGVzdDIubHOCCioudGVzdDIubHMwDQYJKoZIhvcN
+AQELBQADggIBAJvJlcfahvKTyXNLd43MfJOkhR5iXmX+/kM6oQZrRrAuwhhUozY9
+BIn74vfSb7fupbuh1aurnW1ad8H9UgVuw2cIZnhyLJ+DAawOT92wp5r/20VY9ZyQ
+sfWp3kJ/obst1k024esBw3jvoAiVXDW+9Ukmd9P/9lfZbLxfOWuhzqYc6IPsvtd/
+bQddQhaZ7kYJ7l6QardypcxpB4cRSajF8wZ6avvNruIThF7KZO1OLrPx369tNrQK
+Sa3hV811qW4yuTTxlTQjwNNOGuB4I07R96e0Db86TeHixP4AFuoFNArL9J9SvfaL
+tMXwE1Fcsgl8+7Uv1UKb5qk2orvrFayXf3Jxj5EG3TTOjwnd9guPVDEO0bXBV4zw
+osGSvG0nNe1nePHXLUJm0GBcl4T4bMSIgPUNb+gFIQEkDNgF8m9UDNvss+9oeh6k
+e4VtgC3OJt0O7Rd1d5PJkD0hVCaBX9pv/R5+oAQnLbbhusDnMy9EhjQrpzF6fQIg
+WJhVU8D+aMz77XMU23U6neY8CQ9b4RaCFRdd6CxSja2S2R2mBD1Ut/lK0NGJ5aML
+IEno//E12Z/RlL/YzjQrpWnSbo9SYcZ7xY7OXcoaF6/cBwD0bDkQJm3MwfH1WTJN
+6sEq0ugqiLNrrqutFRXvgLnCfYzMAbkFaL81gEycd9RGu1J+vifWkK1M
+-----END CERTIFICATE REQUEST-----
diff --git a/examples_ws/cert/private.key b/examples_ws/cert/private.key
new file mode 100644
index 0000000..38dd91a
--- /dev/null
+++ b/examples_ws/cert/private.key
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+cnk/jkfKFYNPl+wF0d25ufaZl96ENw==
+-----END PRIVATE KEY-----
diff --git a/examples_ws/cert/privkey.pem b/examples_ws/cert/privkey.pem
new file mode 100644
index 0000000..38dd91a
--- /dev/null
+++ b/examples_ws/cert/privkey.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+cnk/jkfKFYNPl+wF0d25ufaZl96ENw==
+-----END PRIVATE KEY-----
diff --git a/examples_ws/cert/ssl.conf b/examples_ws/cert/ssl.conf
new file mode 100644
index 0000000..bb17517
--- /dev/null
+++ b/examples_ws/cert/ssl.conf
@@ -0,0 +1,35 @@
+[ req ]
+default_bits = 4096
+distinguished_name = req_distinguished_name
+req_extensions = req_extensions_section
+
+[ req_distinguished_name ]
+countryName = Country Name (2 letter code)
+countryName_min = 2
+countryName_max = 2
+countryName_default = UA
+stateOrProvinceName = State or Province Name (full name)
+stateOrProvinceName_default = Lviv
+localityName = Locality Name (eg, city)
+localityName_default = Lviv
+organizationName = Organization Name (eg, company)
+organizationName_default = Test
+organizationalUnitName = Organizational Unit Name (eg, section)
+organizationalUnitName_default = IT
+commonName = Common Name (e.g. server FQDN or YOUR name)
+commonName_max = 64
+commonName_default = localhost
+emailAddress = Email Address (eg, admin@example.com)
+emailAddress_max = 64
+emailAddress_default = info@test.com
+
+[ req_extensions_section ]
+subjectAltName = @subject_alternative_name_section
+
+[ subject_alternative_name_section ]
+DNS.1 = test1.ls
+DNS.2 = *.test1.ls
+DNS.3 = test2.ls
+DNS.4 = *.test2.ls
+
+
diff --git a/examples_ws/example_ws_1.zig b/examples_ws/example_ws_1.zig
new file mode 100644
index 0000000..d6c46dc
--- /dev/null
+++ b/examples_ws/example_ws_1.zig
@@ -0,0 +1,209 @@
+
+// ws example (without cert - without https-wss)
+
+const std = @import("std");
+
+const zzz = @import("zzz");
+
+//const websocket = @import("zzz/websocket/websocket.zig");
+const websocket = zzz.websocket;
+
+const Socket = zzz.tardy.Socket;
+
+
+const PORT = 3010;
+const HOST = "0.0.0.0";
+
+//const STACK_SIZE = 10 * 1024 * 1024; // DEBUG
+const STACK_SIZE = 64 * 1024; // RELEASE
+
+
+// WebSocket handlers
+fn on_ws_connect(conn: websocket.Conn) !void{
+ try conn.send("Hello from zzz WebSocket!");
+ std.log.info("WebSocket connected", .{});
+}
+
+
+fn on_ws_close(conn: websocket.Conn, code: u16, reason: []const u8) !void{
+ _ = conn;
+ std.log.info("WS closed: code={d}, reason={s}", .{ code, reason });
+}
+
+
+fn on_ws_message(conn: websocket.Conn, data: []const u8) !void{
+ std.log.info("WS Payload received: '{s}' (len: {d})", .{data, data.len});
+
+ //const msg = try std.fmt.allocPrint(conn.runtime.allocator, "Echo: {s}", .{data});
+ //defer conn.runtime.allocator.free(msg);
+ //try conn.send(msg);
+
+ try conn.send("Hello from Server!");
+ std.log.info("WS <- {s}", .{data});
+}
+
+
+fn on_ws_disconnect(conn: websocket.Conn) !void{
+ _ = conn;
+ std.log.info("WebSocket disconnected", .{});
+}
+
+
+// HTTP fallback
+fn on_request(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
+ const res = ctx.response;
+ res.status = .OK;
+ res.mime = zzz.HTTP.Mime.HTML;
+ res.body =
+ \\
+ \\
zzz + WebSocket
+ \\
+ \\WebSocket Test
+ \\
+ \\
+ \\
+ ;
+ return .standard;
+}
+
+
+// Upgrade handler
+fn on_upgrade(req: *const zzz.Request, proto: []const u8) !bool {
+ if (!std.mem.eql(u8, proto, "websocket")) return false;
+
+ const key = req.headers.get("Sec-WebSocket-Key") orelse return false;
+ const ext = req.headers.get("Sec-WebSocket-Extensions");
+
+ var header_buf = std.ArrayList(u8).init(std.heap.page_allocator);
+ defer header_buf.deinit();
+
+ //const res = try websocket.upgrade(req.socket, req.runtime, std.heap.page_allocator, key, ext, header_buf.writer() );
+ const res = try websocket.upgrade(req.socket, req.runtime, req.runtime.allocator, key, ext, header_buf.writer() );
+
+ _ = try req.socket.send_all(req.runtime, header_buf.items);
+
+ const ws_handler = websocket.Handler{
+ .on_connect = on_ws_connect,
+ .on_message = on_ws_message,
+ .on_close = on_ws_close,
+ .on_disconnect = on_ws_disconnect,
+ };
+
+ if (ws_handler.on_connect) |f| try f(res.conn);
+ try req.runtime.spawn(.{ res.conn, ws_handler, std.heap.page_allocator }, websocket.runLoop, STACK_SIZE);
+ //try req.runtime.spawn(.{ res.conn, ws_handler, req.runtime.allocator }, websocket.runLoop, STACK_SIZE);
+ return true;
+}
+
+
+fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
+ const req = ctx.request;
+
+ const upgrade = req.headers.get("Upgrade");
+ if (upgrade == null or !std.mem.eql(u8, upgrade.?, "websocket")) {
+ ctx.response.status = .@"Bad Request";
+ ctx.response.body = "Expected WebSocket Upgrade";
+ return .standard;
+ }
+
+ const key = req.headers.get("Sec-WebSocket-Key") orelse {
+ ctx.response.status = .@"Bad Request";
+ return .standard;
+ };
+ const ext = req.headers.get("Sec-WebSocket-Extensions");
+
+ var header_buf = std.ArrayList(u8).init(ctx.allocator);
+ defer header_buf.deinit();
+
+ const res = try websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer());
+
+ _ = try ctx.socket.send_all(ctx.runtime, header_buf.items);
+
+ const ws_handler = websocket.Handler{
+ .on_connect = on_ws_connect,
+ .on_message = on_ws_message,
+ .on_close = on_ws_close,
+ .on_disconnect = on_ws_disconnect,
+ };
+
+ if (ws_handler.on_connect) |f| try f(res.conn);
+
+ std.log.info("Starting WebSocket Loop...", .{});
+
+ //websocket.runLoop(res.conn, ws_handler, ctx.runtime.allocator) catch |err| { // sync loop
+ websocket.runLoop(res.conn, ws_handler, std.heap.page_allocator) catch |err| { // sync loop
+ std.log.err("WebSocket RunLoop Error: {s}", .{@errorName(err)});
+
+ if (err == error.Closed) {
+ std.log.info("Socket closed by browser", .{});
+ }
+
+ };
+
+ std.log.info("WebSocket Loop finished", .{});
+ return .close;
+}
+
+
+
+pub fn main() !void{
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ const allocator = gpa.allocator();
+ defer _ = gpa.deinit();
+
+ const socket = try Socket.init(.{ .tcp = .{ .host = HOST, .port = PORT } });
+ defer socket.close_blocking();
+ try socket.bind();
+ try socket.listen(1024); // max conn count that are waiting in accept queue
+
+ const TardyType = zzz.tardy.Tardy(.auto);
+ var tardy = try TardyType.init(allocator, .{});
+ //var tardy = try TardyType.init(allocator, .{ .threading = .single });
+ defer tardy.deinit();
+
+ try tardy.entry(&socket, struct {
+ fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void {
+ const config = zzz.ServerConfig{
+ .stack_size = STACK_SIZE,
+ };
+
+ const home_route = zzz.HTTP.Route.init("/").get({}, on_request);
+ const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint);
+ const layers = &[_]zzz.HTTP.Layer{
+ home_route.layer(),
+ ws_route.layer(),
+ };
+
+ const router = try rt.allocator.create(zzz.Router);
+ router.* = try zzz.Router.init(rt.allocator, layers, .{
+ .not_found = on_request,
+ });
+ // no defer router - lifetime per server work
+
+ const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap, not stack
+ provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); // 1024 = pool size
+
+ const byte_count = provisions.items.len * @sizeOf(zzz.Provision); // set zeros - for initialized = false
+ @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0);
+
+ const connection_count = try rt.allocator.create(usize); // use heap, not stack
+ connection_count.* = 0;
+
+ const accept_queued = try rt.allocator.create(bool);
+ accept_queued.* = false;
+
+ try rt.spawn(
+ .{ rt, config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, connection_count, accept_queued },
+ zzz.Server.main_frame,
+ config.stack_size
+ );
+
+ } // end fn entry
+ }.entry);
+}
+
diff --git a/examples_ws/example_ws_2.zig b/examples_ws/example_ws_2.zig
new file mode 100644
index 0000000..f082b05
--- /dev/null
+++ b/examples_ws/example_ws_2.zig
@@ -0,0 +1,233 @@
+
+// ws example (wss - with cert, with https)
+
+const std = @import("std");
+
+const zzz = @import("zzz");
+
+//const websocket = @import("zzz/websocket/websocket.zig");
+const websocket = zzz.websocket;
+
+const Socket = zzz.tardy.Socket;
+
+
+//const PORT = 3010;
+const PORT = 443;
+const HOST = "0.0.0.0";
+
+//const STACK_SIZE = 10 * 1024 * 1024; // DEBUG
+const STACK_SIZE = 64 * 1024; // RELEASE
+
+const FULLCHAIN_CERT = "examples_ws/cert/fullchain.pem";
+const PRIVKEY_CERT = "examples_ws/cert/privkey.pem";
+
+
+const ServerContext = struct {
+ socket: Socket,
+ cert_pem: []const u8,
+ key_pem: []const u8,
+};
+
+
+// WebSocket handlers
+fn on_ws_connect(conn: websocket.Conn) !void{
+ try conn.send("Hello from zzz WebSocket!");
+ std.log.info("WebSocket connected", .{});
+}
+
+
+fn on_ws_close(conn: websocket.Conn, code: u16, reason: []const u8) !void{
+ _ = conn;
+ std.log.info("WS closed: code={d}, reason={s}", .{ code, reason });
+}
+
+
+fn on_ws_message(conn: websocket.Conn, data: []const u8) !void{
+ std.log.info("WS Payload received: '{s}' (len: {d})", .{data, data.len});
+ try conn.send("Hello from Server!");
+ std.log.info("WS <- {s}", .{data});
+}
+
+
+fn on_ws_disconnect(conn: websocket.Conn) !void{
+ _ = conn;
+ std.log.info("WebSocket disconnected", .{});
+}
+
+
+// HTTP fallback
+fn on_request(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
+ const res = ctx.response;
+ res.status = .OK;
+ res.mime = zzz.HTTP.Mime.HTML;
+ res.body =
+ \\
+ \\zzz + WebSocket
+ \\
+ \\WebSocket Test
+ \\
+ \\
+ \\
+ ;
+ return .standard;
+}
+
+
+// Upgrade handler
+fn on_upgrade(req: *const zzz.Request, proto: []const u8) !bool {
+ if (!std.mem.eql(u8, proto, "websocket")) return false;
+
+ const key = req.headers.get("Sec-WebSocket-Key") orelse return false;
+ const ext = req.headers.get("Sec-WebSocket-Extensions");
+
+ var header_buf = std.ArrayList(u8).init(std.heap.page_allocator);
+ defer header_buf.deinit();
+
+ //const res = try websocket.upgrade(req.socket, req.runtime, std.heap.page_allocator, key, ext, header_buf.writer() );
+ const res = try websocket.upgrade(req.socket, req.runtime, req.runtime.allocator, key, ext, header_buf.writer() );
+
+ _ = try req.socket.send_all(req.runtime, header_buf.items);
+
+ const ws_handler = websocket.Handler{
+ .on_connect = on_ws_connect,
+ .on_message = on_ws_message,
+ .on_close = on_ws_close,
+ .on_disconnect = on_ws_disconnect,
+ };
+
+ if (ws_handler.on_connect) |f| try f(res.conn);
+ try req.runtime.spawn(.{ res.conn, ws_handler, std.heap.page_allocator }, websocket.runLoop, STACK_SIZE);
+ //try req.runtime.spawn(.{ res.conn, ws_handler, req.runtime.allocator }, websocket.runLoop, STACK_SIZE);
+ return true;
+}
+
+
+fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
+ const req = ctx.request;
+
+ const upgrade = req.headers.get("Upgrade");
+ if (upgrade == null or !std.mem.eql(u8, upgrade.?, "websocket")) {
+ ctx.response.status = .@"Bad Request";
+ ctx.response.body = "Expected WebSocket Upgrade";
+ return .standard;
+ }
+
+ const key = req.headers.get("Sec-WebSocket-Key") orelse {
+ ctx.response.status = .@"Bad Request";
+ return .standard;
+ };
+ const ext = req.headers.get("Sec-WebSocket-Extensions");
+
+ var header_buf = std.ArrayList(u8).init(ctx.allocator);
+ defer header_buf.deinit();
+
+ const res = try websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer());
+
+ _ = try ctx.socket.send_all(ctx.runtime, header_buf.items);
+
+ const ws_handler = websocket.Handler{
+ .on_connect = on_ws_connect,
+ .on_message = on_ws_message,
+ .on_close = on_ws_close,
+ .on_disconnect = on_ws_disconnect,
+ };
+
+ if (ws_handler.on_connect) |f| try f(res.conn);
+
+ std.log.info("Starting WebSocket Loop...", .{});
+
+ //websocket.runLoop(res.conn, ws_handler, ctx.runtime.allocator) catch |err| { // sync loop
+ websocket.runLoop(res.conn, ws_handler, std.heap.page_allocator) catch |err| { // sync loop
+ std.log.err("WebSocket RunLoop Error: {s}", .{@errorName(err)});
+
+ if (err == error.Closed) {
+ std.log.info("Socket closed by browser", .{});
+ }
+
+ };
+
+ std.log.info("WebSocket Loop finished", .{});
+ return .close;
+}
+
+
+pub fn main() !void{
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ const allocator = gpa.allocator();
+ defer _ = gpa.deinit();
+
+ const socket = try Socket.init(.{ .tcp = .{ .host = HOST, .port = PORT } });
+ defer socket.close_blocking();
+ try socket.bind();
+ try socket.listen(1024); // max conn count that are waiting in accept queue
+
+ const cert = try std.fs.cwd().readFileAlloc(allocator, FULLCHAIN_CERT, 1024 * 10);
+ defer allocator.free(cert);
+ const key = try std.fs.cwd().readFileAlloc(allocator, PRIVKEY_CERT, 1024 * 10);
+ defer allocator.free(key);
+
+ const ctx = ServerContext{
+ .socket = socket,
+ .cert_pem = cert,
+ .key_pem = key,
+ };
+
+ const TardyType = zzz.tardy.Tardy(.auto);
+ var tardy = try TardyType.init(allocator, .{});
+ //var tardy = try TardyType.init(allocator, .{ .threading = .single });
+ defer tardy.deinit();
+
+ try tardy.entry(&ctx, struct {
+ fn entry(rt: *zzz.tardy.Runtime, s_ctx: *const ServerContext) !void {
+ const bearssl = try rt.allocator.create(zzz.secsock.BearSSL);
+ bearssl.* = zzz.secsock.BearSSL.init(rt.allocator);
+ //defer bearssl.deinit();
+
+ try bearssl.add_cert_chain("CERTIFICATE", s_ctx.cert_pem, "PRIVATE KEY", s_ctx.key_pem);
+ const secure_socket = try bearssl.to_secure_socket(s_ctx.socket, .server);
+ //defer secure_socket.deinit();
+
+ const config = zzz.ServerConfig{
+ .stack_size = STACK_SIZE,
+ };
+
+ const home_route = zzz.HTTP.Route.init("/").get({}, on_request);
+ const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint);
+ const layers = &[_]zzz.HTTP.Layer{
+ home_route.layer(),
+ ws_route.layer(),
+ };
+
+ const router = try rt.allocator.create(zzz.Router);
+ router.* = try zzz.Router.init(rt.allocator, layers, .{
+ .not_found = on_request,
+ });
+ // no defer router - lifetime per server work
+
+ const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap instead of stack
+ provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); // 1024 = pool size
+
+ const byte_count = provisions.items.len * @sizeOf(zzz.Provision); // set zeros -- initialized = false
+ @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0);
+
+ const connection_count = try rt.allocator.create(usize); // use heap instead stack
+ connection_count.* = 0;
+
+ const accept_queued = try rt.allocator.create(bool);
+ accept_queued.* = false;
+
+ try rt.spawn(
+ .{ rt, config, router, secure_socket, provisions, connection_count, accept_queued },
+ zzz.Server.main_frame,
+ config.stack_size
+ );
+
+ } // end fn entry
+ }.entry);
+}
+
diff --git a/examples_ws/example_ws_3.zig b/examples_ws/example_ws_3.zig
new file mode 100644
index 0000000..5437843
--- /dev/null
+++ b/examples_ws/example_ws_3.zig
@@ -0,0 +1,179 @@
+
+// ws with Pub/Sub example
+
+const std = @import("std");
+
+const zzz = @import("zzz");
+
+//const websocket = @import("zzz/websocket/websocket.zig");
+const websocket = zzz.websocket;
+const PubSub = zzz.PubSub;
+const handle_upgrade = zzz.handle_upgrade;
+const WsSession = zzz.WsSession;
+
+const Socket = zzz.tardy.Socket;
+const Timer = zzz.tardy.Timer;
+
+
+const PORT = 3010;
+//const PORT = 443;
+const HOST = "0.0.0.0";
+
+//const STACK_SIZE = 10 * 1024 * 1024; // DEBUG
+const STACK_SIZE = 16 * 1024; // RELEASE // reader stack // 8-16 kb without ssl usage, 32 kb when use ssl
+const WS_WRITER_STACK = 8 * 1024; // stack size for writer task // 8 kb without ssl usage, 32 kb when use ssl
+
+var global_pubsub: PubSub = undefined;
+
+
+// WebSocket handlers
+fn on_ws_connect(session: *WsSession) !void{
+ try global_pubsub.subscribe("general", session);
+ try session.scheduleSend("Welcome! You are joined to 'general'. Send 'room:message' to chat.");
+
+ std.log.info("WebSocket connected and joined 'general' chat", .{});
+}
+
+
+fn on_ws_close(session: *WsSession) void{
+ global_pubsub.removeConn(session);
+ //std.log.info("WS closed: code={d}, reason={s}", .{ code, reason });
+ std.log.info("WS closed, client removed from all chats", .{ });
+}
+
+
+fn on_ws_message(session: *WsSession, data: []const u8) !void{
+ std.log.info("WS server received message: '{s}' (len: {d})", .{data, data.len});
+
+ if (std.mem.indexOfScalar(u8, data, ':')) |idx| {
+ const command_or_chat = data[0..idx];
+ const msg_content = data[idx+1..];
+
+ if (std.mem.eql(u8, command_or_chat, "join")) { // "join:chatname"
+ try global_pubsub.subscribe(msg_content, session);
+ try session.scheduleSend("Joined chat");
+ return;
+ }
+
+ if (std.mem.eql(u8, command_or_chat, "leave")) { // "leave:chatname"
+ global_pubsub.unsubscribe(msg_content, session);
+ try session.scheduleSend("Left chat");
+ return;
+ }
+
+ //const formatted_msg = try std.fmt.allocPrint(conn.runtime.allocator, "Msg in {s}: {s}", .{command_or_chat, msg_content}); // pub to chatname
+ //defer conn.runtime.allocator.free(formatted_msg);
+ const formatted_msg = try std.fmt.allocPrint(session.allocator, "Msg in {s}: {s}", .{command_or_chat, msg_content}); // pub to chatname
+ defer session.allocator.free(formatted_msg);
+
+ global_pubsub.publish(command_or_chat, formatted_msg, session); // send message everybody (but not to sender)
+ //global_pubsub.publish("general", "System: Server is shutting down", null); // "system" message without sender conn
+
+ } else {
+ try session.scheduleSend("Format: 'chatname:message' or 'join:chatname'");
+ }
+ std.log.info("WS <- {s}", .{data});
+}
+
+
+// optional -- not required
+fn on_ws_disconnect(session: *WsSession) void{
+ global_pubsub.removeConn(session);
+ std.log.info("WebSocket disconnected, client removed from all chats", .{});
+}
+
+
+// HTTP fallback
+fn on_request(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
+ const res = ctx.response;
+ res.status = .OK;
+ res.mime = zzz.HTTP.Mime.HTML;
+ res.body =
+ \\
+ \\zzz + WebSocket + PubSub
+ \\
+ \\WebSocket PubSub Test
+ \\
+ \\
+ \\
+ ;
+ return .standard;
+}
+
+
+fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
+ return handle_upgrade(ctx, .{
+ .on_connect = on_ws_connect,
+ .on_message = on_ws_message,
+ .on_close = on_ws_close,
+ //.on_disconnect = on_ws_disconnect, // optional
+ },
+ WS_WRITER_STACK // stack size for writer task
+ );
+}
+
+
+pub fn main() !void{
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ const allocator = gpa.allocator();
+ defer _ = gpa.deinit();
+
+ global_pubsub = PubSub.init(allocator);
+ defer global_pubsub.deinit();
+
+ const socket = try Socket.init(.{ .tcp = .{ .host = HOST, .port = PORT } });
+ defer socket.close_blocking();
+ try socket.bind();
+ try socket.listen(1024); // max conn count that are waiting in accept queue
+
+ const TardyType = zzz.tardy.Tardy(.auto);
+ var tardy = try TardyType.init(allocator, .{});
+ //var tardy = try TardyType.init(allocator, .{ .threading = .single });
+ defer tardy.deinit();
+
+ try tardy.entry(&socket, struct {
+ fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void {
+ const config = zzz.ServerConfig{
+ .stack_size = STACK_SIZE, // stack for http requests
+ };
+
+ const home_route = zzz.HTTP.Route.init("/").get({}, on_request);
+ const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint);
+ const layers = &[_]zzz.HTTP.Layer{
+ home_route.layer(),
+ ws_route.layer(),
+ };
+
+ const router = try rt.allocator.create(zzz.Router);
+ router.* = try zzz.Router.init(rt.allocator, layers, .{
+ .not_found = on_request,
+ });
+ // no defer router - lifetime per server work
+
+ const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap instead of stack
+ provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static);
+
+ const byte_count = provisions.items.len * @sizeOf(zzz.Provision); // set zeros -- initialized = false
+ @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0);
+
+ const connection_count = try rt.allocator.create(usize); // use heap instead stack
+ connection_count.* = 0;
+
+ const accept_queued = try rt.allocator.create(bool);
+ accept_queued.* = false;
+
+ try rt.spawn(
+ .{ rt, config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, connection_count, accept_queued },
+ zzz.Server.main_frame,
+ config.stack_size
+ );
+
+ } // end fn entry
+ }.entry);
+}
+
diff --git a/src/http/context.zig b/src/http/context.zig
index 60dee37..6074eb1 100644
--- a/src/http/context.zig
+++ b/src/http/context.zig
@@ -30,4 +30,7 @@ pub const Context = struct {
captures: []const Capture,
/// Map of the KV Query pairs in the URL
queries: *const AnyCaseStringMap,
+ /// bind WebSocket to Context (for use on_connect etc)
+ ws_user_data: ?*anyopaque = null,
};
+
diff --git a/src/http/lib.zig b/src/http/lib.zig
index 5b2b3f3..06b0b06 100644
--- a/src/http/lib.zig
+++ b/src/http/lib.zig
@@ -23,8 +23,13 @@ pub const MiddlewareFn = @import("router/middleware.zig").MiddlewareFn;
pub const Next = @import("router/middleware.zig").Next;
pub const Middlewares = @import("middlewares/lib.zig");
+pub const Provision = @import("server.zig").Provision;
+
+pub const Capture = @import("router/routing_trie.zig").Capture;
pub const FsDir = @import("router/fs_dir.zig").FsDir;
+pub const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap;
+
pub const Server = @import("server.zig").Server;
pub const ServerConfig = @import("server.zig").ServerConfig;
@@ -36,3 +41,4 @@ pub const HTTPError = error{
URITooLong,
HTTPVersionNotSupported,
};
+
diff --git a/src/http/request.zig b/src/http/request.zig
index c74e1f0..fc2ec30 100644
--- a/src/http/request.zig
+++ b/src/http/request.zig
@@ -2,6 +2,11 @@ const std = @import("std");
const log = std.log.scoped(.@"zzz/http/request");
const assert = std.debug.assert;
+//const SecureSocket = @import("secsock").SecureSocket;
+//const Runtime = @import("tardy").Runtime;
+const secsock = @import("secsock");
+const tardy = @import("tardy");
+
const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap;
const CookieMap = @import("cookie.zig").CookieMap;
const HTTPError = @import("lib.zig").HTTPError;
@@ -15,7 +20,11 @@ pub const Request = struct {
headers: AnyCaseStringMap,
cookies: CookieMap,
body: ?[]const u8 = null,
-
+ //socket: *const SecureSocket = undefined, // for WebSocket on_upgrade
+ //runtime: *Runtime = undefined,
+ socket: *const secsock.SecureSocket = undefined, // for WebSocket on_upgrade
+ runtime: *tardy.Runtime = undefined,
+
/// This is for constructing a Request.
pub fn init(allocator: std.mem.Allocator) Request {
const headers = AnyCaseStringMap.init(allocator);
@@ -243,3 +252,4 @@ test "Malformed AnyCaseStringMap" {
});
try testing.expectError(HTTPError.MalformedRequest, err);
}
+
diff --git a/src/http/server.zig b/src/http/server.zig
index d3432db..9f56e54 100644
--- a/src/http/server.zig
+++ b/src/http/server.zig
@@ -51,6 +51,11 @@ pub const TLSFileOptions = union(enum) {
},
};
+pub const TLSConfig = struct {
+ cert: TLSFileOptions,
+ key: TLSFileOptions,
+};
+
/// These are various general configuration
/// options that are important for the actual framework.
///
@@ -122,6 +127,11 @@ pub const ServerConfig = struct {
///
/// Default: 2KB
request_uri_bytes_max: u32 = 1024 * 2,
+ /// Callback for upgrade request (Websocket)
+ /// return true if upgrade processed
+ on_upgrade: ?*const fn (*const Request, []const u8) anyerror!bool = null,
+ /// we need two tls files - cert + key
+ tls: ?TLSConfig = null,
};
pub const Provision = struct {
@@ -336,18 +346,50 @@ pub const Server = struct {
assert(info.current_length <= info.content_length);
},
},
+
.handler => {
- const found = try router.get_bundle_from_host(
+
+ const request_text = provision.zc_recv_buffer.as_slice();
+ var request = Request.init(rt.allocator);
+ request.parse_headers(request_text, .{
+ .request_bytes_max = config.request_bytes_max,
+ .request_uri_bytes_max = config.request_uri_bytes_max,
+ }) catch |e| {
+ log.debug("malformed request| {}", .{e});
+ break :http_loop;
+ };
+
+
+ if (config.on_upgrade) |on_upgrade| { // check is this WebSocket upgrade
+ request.socket = &secure;
+ request.runtime = rt;
+
+ const upgrade_header = request.headers.get("Upgrade");
+ if (upgrade_header) |upgrade| {
+
+ if (std.mem.eql(u8, upgrade, "websocket")) {
+ if (try on_upgrade(&request, upgrade)) {
+ continue :http_loop;
+ }
+ }
+
+ }
+ }
+
+ const found = try router.get_bundle_from_host( // continue as common HTTP if not WebSocket upgrade
rt.allocator,
- provision.request.uri.?,
+ //provision.request.uri.?,
+ request.uri.?,
provision.captures,
&provision.queries,
);
defer rt.allocator.free(found.duped);
defer for (found.duped) |dupe| rt.allocator.free(dupe);
-
+
+
const h_with_data: HandlerWithData = found.route.get_handler(
- provision.request.method.?,
+ //provision.request.method.?,
+ request.method.?,
) orelse {
provision.response.headers.clearRetainingCapacity();
provision.response.status = .@"Method Not Allowed";
@@ -357,7 +399,8 @@ pub const Server = struct {
state = .respond;
continue;
};
-
+
+
const context: Context = .{
.runtime = rt,
.allocator = provision.arena.allocator(),
@@ -369,13 +412,14 @@ pub const Server = struct {
.captures = found.captures,
.queries = found.queries,
};
-
+
var next: Next = .{
.context = &context,
.middlewares = h_with_data.middlewares,
.handler = h_with_data,
};
-
+
+
const next_respond: Respond = next.run() catch |e| blk: {
log.warn("rt{d} - \"{s} {s}\" {} ({})", .{
rt.id,
@@ -385,8 +429,7 @@ pub const Server = struct {
secure.socket.addr,
});
- // If in Debug Mode, we will return the error name. In other modes,
- // we won't to avoid leaking implemenation details.
+ // If in Debug Mode, we will return the error name
const body = if (comptime builtin.mode == .Debug) @errorName(e) else "";
break :blk try provision.response.apply(.{
@@ -395,10 +438,10 @@ pub const Server = struct {
.body = body,
});
};
-
+
+
switch (next_respond) {
.standard => {
- // applies the respond onto the response
//try provision.response.apply(respond);
state = .respond;
},
@@ -410,15 +453,15 @@ pub const Server = struct {
log.debug("closing connection, exceeded keepalive max", .{});
break :http_loop;
}
-
keepalive_count += 1;
}
-
+
try prepare_new_request(&state, provision, config);
},
.close => break :http_loop,
}
},
+
.respond => {
const body = provision.response.body orelse "";
const content_length = body.len;
@@ -531,3 +574,4 @@ pub const Server = struct {
);
}
};
+
diff --git a/src/http/websocket.zig b/src/http/websocket.zig
new file mode 100644
index 0000000..40210ae
--- /dev/null
+++ b/src/http/websocket.zig
@@ -0,0 +1,198 @@
+
+const std = @import("std");
+
+const zzz = @import("../lib.zig");
+
+const websocket = zzz.websocket;
+const SecureSocket = zzz.secsock.SecureSocket;
+const Runtime = zzz.tardy.Runtime;
+
+const Context = @import("context.zig").Context;
+
+
+pub const WebSocketHandler = struct {
+ on_connect: ?*const fn (Conn) anyerror!void = null,
+ on_message: ?*const fn (Conn, []const u8) anyerror!void = null,
+ on_disconnect: ?*const fn (Conn) anyerror!void = null,
+ user_data: ?*anyopaque = null,
+};
+
+pub const Conn = struct {
+ socket: *const SecureSocket,
+ runtime: *Runtime,
+ user_data: ?*anyopaque,
+
+ pub fn send(self: Conn, data: []const u8) !void {
+ const frame = websocket.frameText(data);
+ _ = try self.socket.send_all(self.runtime, frame);
+ }
+
+ pub fn close(self: Conn) void {
+ self.socket.close_blocking();
+ }
+};
+
+pub fn upgrade_to_websocket(
+ ctx: *const Context,
+ handler: WebSocketHandler,
+) !bool {
+ const req = ctx.request;
+ const res = ctx.response;
+
+ // handshake
+ if (!std.mem.eql(u8, req.headers.get("Connection") orelse "", "Upgrade"))
+ return false;
+ if (!std.mem.eql(u8, req.headers.get("Upgrade") orelse "", "websocket"))
+ return false;
+ if (!std.mem.eql(u8, req.headers.get("Sec-WebSocket-Version") orelse "", "13"))
+ return false;
+
+ const key = req.headers.get("Sec-WebSocket-Key") orelse return false;
+
+ // Sec-WebSocket-Accept
+ const accept = try compute_accept(ctx.allocator, key);
+
+ // 101 Switching Protocols
+ res.clear();
+ try res.headers.put("Upgrade", "websocket");
+ try res.headers.put("Connection", "Upgrade");
+ try res.headers.put("Sec-WebSocket-Accept", accept);
+ res.status = .@"Switching Protocols";
+
+ try res.headers_into_writer(ctx.header_buffer.writer(), 0);
+ _ = try ctx.socket.send_all(ctx.runtime, ctx.header_buffer.items);
+
+ // WebSocket Conn
+ const conn = Conn{
+ .socket = &ctx.socket,
+ .runtime = ctx.runtime,
+ .user_data = handler.user_data,
+ };
+
+ // on_connect
+ if (handler.on_connect) |on_connect| {
+ try on_connect(conn);
+ }
+
+ // process
+ try ctx.runtime.spawn(.{ conn, handler }, message_loop, 64 * 1024);
+ return true;
+}
+
+// inner loop
+fn message_loop(conn: Conn, handler: WebSocketHandler) !void {
+ var buffer: [65536]u8 = undefined;
+ while (true) {
+ const n = conn.socket.recv(conn.runtime, &buffer) catch |err| {
+ if (err == error.Closed) {
+ if (handler.on_disconnect) |on_disconnect| {
+ _ = on_disconnect(conn);
+ }
+ return;
+ }
+ return err;
+ };
+
+ // RFC 6455
+ var i: usize = 0;
+ while (i < n) {
+ const op = buffer[i];
+ const fin = (op & 0x80) != 0;
+ const opcode = op & 0x0F;
+ i += 1;
+
+ if (i >= n) return error.InvalidWebSocketFrame;
+
+ const mask_flag = (buffer[i] & 0x80) != 0;
+ const payload_len_raw = buffer[i] & 0x7F;
+ i += 1;
+
+ if (i >= n) return error.InvalidWebSocketFrame;
+
+ var payload_len: usize = payload_len_raw;
+ var extra: usize = 0;
+ if (payload_len_raw == 126) {
+ if (i + 2 > n) return error.InvalidWebSocketFrame;
+ payload_len = @as(usize, @bitCast(std.mem.readIntBig(u16, buffer[i..])));
+ extra = 2;
+ } else if (payload_len_raw == 127) {
+ if (i + 8 > n) return error.InvalidWebSocketFrame;
+ payload_len = @as(usize, @bitCast(std.mem.readIntBig(u64, buffer[i..])));
+ extra = 8;
+ }
+
+ i += extra;
+ if (mask_flag) {
+ if (i + 4 > n) return error.InvalidWebSocketFrame;
+ const mask = buffer[i..][0..4].*;
+ i += 4;
+ if (i + payload_len > n) return error.InvalidWebSocketFrame;
+
+ //for (0..payload_len) |j| {
+ // buffer[i + j] ^= mask[j % 4];
+ //} // use next vector optimising instead this
+
+ var j: usize = 0;
+ const vec_len = std.simd.suggestVectorLength(u8) orelse 16;
+ const Vector = @Vector(vec_len, u8);
+
+ var mask_arr: [vec_len]u8 = undefined;
+ for (0..vec_len) |k| mask_arr[k] = mask[k % 4];
+ const mask_vec: Vector = mask_arr;
+
+ while (j + vec_len <= payload_len) {
+ const chunk: Vector = buffer[i+j..][0..vec_len].*;
+ const res = chunk ^ mask_vec;
+ buffer[i+j..][0..vec_len].* = res;
+ j += vec_len;
+ }
+
+ while (j < payload_len) : (j += 1) {
+ buffer[i + j] ^= mask[j % 4];
+ }
+
+
+ }
+
+ if (i + payload_len > n) return error.InvalidWebSocketFrame;
+
+ switch (opcode) {
+ 0x1 => { // Text
+ if (fin and handler.on_message) |on_msg| {
+ try on_msg(conn, buffer[i .. i + payload_len]);
+ }
+ },
+ 0x8 => { // Close
+ conn.close();
+ if (handler.on_disconnect) |on_disconnect| {
+ _ = on_disconnect(conn);
+ }
+ return;
+ },
+ 0x9 => { // Ping - reply Pong
+ const pong = ([2]u8{ 0x8A, @intCast(@min(payload_len, 125)) })[0..];
+ _ = try conn.socket.send_all(conn.runtime, pong);
+ if (payload_len <= 125) {
+ _ = try conn.socket.send_all(conn.runtime, buffer[i .. i + payload_len]);
+ }
+ },
+ else => {
+ // ignore other opcode (binary, pong etc)
+ },
+ }
+
+ i += payload_len;
+ }
+ }
+}
+
+// Sec-WebSocket-Accept
+fn compute_accept(allocator: std.mem.Allocator, key: []const u8) ![]const u8 {
+ const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+ var hasher = std.crypto.hash.sha1.Sha1.init();
+ hasher.update(key);
+ hasher.update(magic);
+ const hash = hasher.final();
+ return try std.base64.encode(allocator, &hash);
+}
+
diff --git a/src/lib.zig b/src/lib.zig
index 96d9dd7..703faf1 100644
--- a/src/lib.zig
+++ b/src/lib.zig
@@ -2,10 +2,29 @@ const std = @import("std");
/// Internally exposed Tardy.
pub const tardy = @import("tardy");
+//pub const Pool = tardy.Pool;
/// Internally exposed secsock.
pub const secsock = @import("secsock");
+//pub const SecureSocket = secsock.SecureSocket;
/// HyperText Transfer Protocol.
/// Supports: HTTP/1.1
pub const HTTP = @import("http/lib.zig");
+pub const ServerConfig = HTTP.ServerConfig;
+pub const Request = HTTP.Request;
+//pub const Respond = HTTP.Respond;
+pub const Router = HTTP.Router;
+pub const Context = HTTP.Context;
+pub const Provision = HTTP.Provision;
+
+pub const TypedStorage = @import("core/typed_storage.zig").TypedStorage;
+
+pub const Server = @import("http/server.zig").Server;
+
+/// websocket + PubSub
+pub const websocket = @import("websocket/websocket.zig");
+pub const PubSub = @import("websocket/pubsub.zig").PubSub;
+pub const WsSession = @import("websocket/pubsub.zig").WsSession;
+pub const handle_upgrade = @import("websocket/pubsub.zig").handle_upgrade;
+
diff --git a/src/websocket/pubsub.zig b/src/websocket/pubsub.zig
new file mode 100644
index 0000000..ad4ca85
--- /dev/null
+++ b/src/websocket/pubsub.zig
@@ -0,0 +1,298 @@
+
+// Pub/Sub for WS
+
+const std = @import("std");
+const zzz = @import("../lib.zig"); // "zzz" // for zzz.Context
+const Conn = @import("websocket.zig").Conn;
+const SecureSocket = zzz.secsock.SecureSocket;
+
+
+pub const UserWsHandler = struct {
+ on_connect: ?*const fn (session: *WsSession) anyerror!void = null,
+ on_message: ?*const fn (session: *WsSession, data: []const u8) anyerror!void = null,
+ on_close: ?*const fn (session: *WsSession) void = null,
+ on_disconnect: ?*const fn (session: *WsSession) void = null,
+};
+
+
+pub const WsSession = struct { // for safe multithread use Pub/Sub
+ conn: Conn,
+ socket_owned: SecureSocket, // owned copy of socket structure (avoid use-after-free error)
+ outbox: std.ArrayList([]const u8), // messages queue that need to send to client with conn
+ mutex: std.Thread.Mutex,
+ writer_task_id: usize = 0, // task id for send in native thread
+ active: bool = true, // flag for to stop writer_task on disconnect
+ allocator: std.mem.Allocator,
+ writer_done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), // for avoid race condition
+ handler: UserWsHandler, // for access callbacks inside session
+
+ pub fn init(allocator: std.mem.Allocator, conn: Conn, handler: UserWsHandler) WsSession {
+ return .{
+ .conn = conn,
+ .socket_owned = conn.socket.*,
+ .outbox = std.ArrayList([]const u8).init(allocator),
+ .mutex = .{},
+ .allocator = allocator,
+ .handler = handler,
+ };
+ }
+
+ pub fn deinit(self: *WsSession) void {
+ self.mutex.lock();
+ defer self.mutex.unlock();
+ for (self.outbox.items) |msg| {
+ self.allocator.free(msg);
+ }
+ self.outbox.deinit();
+ }
+
+ pub fn scheduleSend(self: *WsSession, data: []const u8) !void { // function for safe call from any thread
+ self.mutex.lock();
+ if (!self.active) {
+ self.mutex.unlock();
+ return error.Closed;
+ }
+
+ const msg_copy = try self.allocator.dupe(u8, data); // copy msg to heap
+ try self.outbox.append(msg_copy);
+ self.mutex.unlock();
+
+ try self.conn.runtime.trigger(self.writer_task_id); // conn.runtime.trigger is thread-safe // this is task-writer in target thread
+ }
+};
+
+
+pub const PubSub = struct {
+ topics: std.StringHashMap(std.ArrayList(*WsSession)), // Topic Name -> List of Connections
+ lock: std.Thread.RwLock,
+ allocator: std.mem.Allocator,
+
+ pub fn init(allocator: std.mem.Allocator) PubSub {
+ return .{
+ .topics = std.StringHashMap(std.ArrayList(*WsSession)).init(allocator),
+ .lock = .{},
+ .allocator = allocator,
+ };
+ }
+
+ pub fn deinit(self: *PubSub) void {
+ self.lock.lock();
+ defer self.lock.unlock();
+
+ var iter = self.topics.iterator();
+ while (iter.next()) |entry| {
+ entry.value_ptr.deinit();
+ self.allocator.free(entry.key_ptr.*);
+ }
+ self.topics.deinit();
+ }
+
+ pub fn subscribe(self: *PubSub, topic: []const u8, session: *WsSession) !void {
+ self.lock.lock();
+ defer self.lock.unlock();
+
+ const entry = try self.topics.getOrPut(topic);
+ if (!entry.found_existing) {
+ entry.key_ptr.* = try self.allocator.dupe(u8, topic);
+ entry.value_ptr.* = std.ArrayList(*WsSession).init(self.allocator);
+ }
+
+ for (entry.value_ptr.items) |existing| { // not add doublibgs
+ if (existing == session) return;
+ }
+
+ try entry.value_ptr.append(session);
+ std.log.debug("PubSub: Client subscribed to '{s}'", .{topic});
+ }
+
+ pub fn unsubscribe(self: *PubSub, topic: []const u8, session: *WsSession) void {
+ self.lock.lock();
+ defer self.lock.unlock();
+
+ if (self.topics.getPtr(topic)) |list| {
+ for (list.items, 0..) |s, i| {
+ if (s == session) {
+ _ = list.swapRemove(i);
+ break;
+ }
+ }
+ }
+
+ //if (list.items.len == 0) { // del group if empty
+ // list.deinit();
+ // if (self.topics.fetchRemove(topic)) |kv| {
+ // self.allocator.free(kv.key);
+ // }
+ //}
+ }
+
+
+ pub fn removeConn(self: *PubSub, session: *WsSession) void { // delete connection/client from all groups - call on_disconnect
+ self.lock.lock();
+ defer self.lock.unlock();
+
+ var iter = self.topics.iterator();
+ while (iter.next()) |entry| {
+ const list = entry.value_ptr;
+ var i: usize = 0;
+ while (i < list.items.len) {
+ if (list.items[i] == session) {
+ _ = list.swapRemove(i);
+ } else {
+ i += 1;
+ }
+ }
+ }
+ }
+
+
+ pub fn publish(self: *PubSub, topic: []const u8, message: []const u8, sender: ?*WsSession) void {
+ self.lock.lockShared(); // shared lock for read
+ defer self.lock.unlockShared();
+
+ if (self.topics.get(topic)) |list| {
+ for (list.items) |session| {
+ if (sender) |s| { // do not send for the sender
+ if (s == session) continue;
+ }
+
+ session.scheduleSend(message) catch |e| { // todo repeat send on error (one laggy/dead client not blocks all)
+ std.log.warn("PubSub send failed: {}", .{e});
+ };
+ }
+ }
+ }
+};
+
+
+// helpers
+
+fn writer_task(session: *WsSession) !void { // send message in same thread with conn
+ session.writer_task_id = session.conn.runtime.current_task.?; // save task id for PubSub
+ defer session.writer_done.store(true, .release);
+
+ while (true) {
+ session.mutex.lock();
+ if (!session.active) {
+ session.mutex.unlock();
+ break;
+ }
+
+ const batch = try session.outbox.toOwnedSlice();
+ session.mutex.unlock();
+
+ if (batch.len > 0) {
+ for (batch) |msg| {
+ session.conn.send(msg) catch |e| {
+ std.log.debug("WS Writer Error: {s}", .{ @errorName(e) });
+ };
+ session.allocator.free(msg);
+ }
+ session.allocator.free(batch);
+ }
+ try session.conn.runtime.scheduler.trigger_await();
+ }
+}
+
+
+pub fn handle_upgrade(ctx: *const zzz.Context, user_handler: UserWsHandler, stack_size_writer: usize) !zzz.HTTP.Respond {
+ const req = ctx.request;
+
+ const upgrade = req.headers.get("Upgrade");
+ if (upgrade == null or !std.mem.eql(u8, upgrade.?, "websocket")) {
+ ctx.response.status = .@"Bad Request";
+ //ctx.response.body = "Expected WebSocket Upgrade";
+ return .standard;
+ }
+
+ const key = req.headers.get("Sec-WebSocket-Key") orelse return .standard;
+ const ext = req.headers.get("Sec-WebSocket-Extensions");
+
+ var header_buf = std.ArrayList(u8).init(ctx.allocator);
+ defer header_buf.deinit();
+
+ const res = try zzz.websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer());
+ _ = try ctx.socket.send_all(ctx.runtime, header_buf.items);
+
+ const session = try std.heap.page_allocator.create(WsSession);
+ session.* = WsSession.init(std.heap.page_allocator, res.conn, user_handler);
+
+ session.conn.socket = &session.socket_owned; // use heap instead stack
+ session.conn.user_data = session; // for not use global maps that locks
+
+ try ctx.runtime.spawn(.{ session }, writer_task, stack_size_writer);
+
+ const internal_handler = zzz.websocket.Handler{
+ .on_message = struct {
+ fn wrapper(c: Conn, d: []const u8) !void {
+ const s: *WsSession = @ptrCast(@alignCast(c.user_data)); // get session from user_data
+ if (s.handler.on_message) |f| try f(s, d);
+ }
+ }.wrapper,
+
+ .on_close = struct {
+ fn wrapper(c: Conn, code: u16, reason: []const u8) !void {
+ _ = code;
+ _ = reason;
+ const s: *WsSession = @ptrCast(@alignCast(c.user_data));
+ if (s.handler.on_close) |f| f(s);
+ }
+ }.wrapper,
+
+ .on_disconnect = struct {
+ fn wrapper(c: Conn) !void {
+ const s: *WsSession = @ptrCast(@alignCast(c.user_data));
+ if (s.handler.on_close) |f| f(s);
+ }
+ }.wrapper,
+ };
+
+
+ if (user_handler.on_connect) |f| {
+ f(session) catch |e| { // clean if conn failed
+ std.log.err("WS on_connect error: {s}", .{ @errorName(e) });
+
+ session.active = false;
+ ctx.runtime.trigger(session.writer_task_id) catch {};
+
+ while(!session.writer_done.load(.acquire)){
+ try zzz.tardy.Timer.delay(ctx.runtime, .{ .nanos = 1000 });
+ }
+
+ session.deinit();
+ std.heap.page_allocator.destroy(session);
+
+ std.log.info("WebSocket Loop finished and memory freed", .{});
+ return .close;
+ };
+ }
+
+ //zzz.websocket.runLoop(session.conn, internal_handler, ctx.runtime.allocator) catch |err| { // sync loop
+ zzz.websocket.runLoop(session.conn, internal_handler, std.heap.page_allocator) catch |err| { // sync loop
+ if (err != error.Closed){
+ std.log.err("WebSocket RunLoop Error: {s}", .{ @errorName(err) });
+ }
+ //if (err == error.Closed) {
+ // std.log.info("WebSocket closed by browser", .{});
+ //}
+ };
+
+ //std.log.info("Closing session...", .{});
+
+ session.mutex.lock();
+ session.active = false;
+ session.mutex.unlock();
+
+ ctx.runtime.trigger(session.writer_task_id) catch {};
+
+ while (!session.writer_done.load(.acquire)) { // waiting while writer_task will be done
+ try zzz.tardy.Timer.delay(ctx.runtime, .{ .nanos = 1_000_000 }); // 1 ms
+ }
+
+ session.deinit();
+ std.heap.page_allocator.destroy(session);
+
+ std.log.info("WebSocket Loop finished and memory freed", .{});
+ return .close;
+}
+
diff --git a/src/websocket/websocket.zig b/src/websocket/websocket.zig
new file mode 100644
index 0000000..6f926cd
--- /dev/null
+++ b/src/websocket/websocket.zig
@@ -0,0 +1,340 @@
+
+// websocket - RFC 6455 + RFC 7692 (permessage-deflate) // todo fix last one
+
+const std = @import("std");
+
+const Allocator = std.mem.Allocator;
+
+const SecureSocket = @import("secsock").SecureSocket;
+const Runtime = @import("tardy").Runtime;
+
+const compress = std.compress;
+
+
+pub const Conn = struct {
+ socket: *const SecureSocket,
+ runtime: *Runtime,
+ user_data: ?*anyopaque = null,
+ //allocator: Allocator, // maybe todo - and pass with on_upgrade
+ //compression: bool,
+
+ pub fn send(self: Conn, data: []const u8) !void {
+ var buf = std.ArrayList(u8).init(self.runtime.allocator);
+ defer buf.deinit();
+
+ //try writeFrameHeader(buf.writer(), .text, data.len, true);
+ try writeFrameHeader(buf.writer(), .text, data.len, false);
+ try buf.appendSlice(data);
+ _ = try self.socket.send_all(self.runtime, buf.items);
+ }
+
+ pub fn sendBinary(self: Conn, binary_data: []const u8) !void {
+ var buf = std.ArrayList(u8).init(self.runtime.allocator);
+ defer buf.deinit();
+
+ try writeFrameHeader(buf.writer(), .binary, binary_data.len, false);
+ try buf.appendSlice(binary_data);
+ _ = try self.socket.send_all(self.runtime, buf.items);
+
+ }
+
+ pub fn close(self: Conn, code: u16, reason: []const u8) !void {
+ if (reason.len > 123) return error.ReasonTooLong;
+
+ var buf = std.ArrayList(u8).init(self.runtime.allocator);
+ defer buf.deinit();
+
+ const payload_len = 2 + reason.len;
+ try writeFrameHeader(buf.writer(), .close, payload_len, false); // close header
+
+ try buf.writer().writeInt(u16, code, .big); // big-endian
+
+ if (reason.len > 0) {
+ try buf.appendSlice(reason);
+ }
+
+ _ = try self.socket.send_all(self.runtime, buf.items);
+ }
+
+};
+
+
+pub const Handler = struct {
+ on_connect: ?*const fn (Conn) anyerror!void = null,
+ on_message: ?*const fn (Conn, []const u8) anyerror!void = null,
+ on_binary: ?*const fn (Conn, []const u8) anyerror!void = null,
+ on_close: ?*const fn (Conn, u16, []const u8) anyerror!void = null,
+ on_disconnect: ?*const fn (Conn) anyerror!void = null,
+};
+
+pub const HandshakeResult = struct {
+ conn: Conn,
+ //compression: bool,
+};
+
+pub fn upgrade(
+ socket: *const SecureSocket,
+ runtime: *Runtime,
+ allocator: Allocator,
+ sec_websocket_key: []const u8,
+ sec_websocket_extensions: ?[]const u8,
+ response_writer: anytype,
+) !HandshakeResult {
+ _ = sec_websocket_extensions;
+ //var compression = false;
+ //if (sec_websocket_extensions) |ext| {
+ // if (std.mem.indexOf(u8, ext, "permessage-deflate") != null) {
+ // const has_client_no_ctx = std.mem.indexOf(u8, ext, "client_no_context_takeover") != null;
+ // const has_server_no_ctx = std.mem.indexOf(u8, ext, "server_no_context_takeover") != null;
+ // if (has_client_no_ctx and has_server_no_ctx) {
+ // compression = true;
+ // }
+ // }
+ //}
+
+ const accept = try computeAccept(allocator, sec_websocket_key);
+ try response_writer.writeAll("HTTP/1.1 101 Switching Protocols\r\n");
+ try response_writer.writeAll("Upgrade: websocket\r\n");
+ try response_writer.writeAll("Connection: Upgrade\r\n");
+ try response_writer.writeAll("Sec-WebSocket-Accept: ");
+ try response_writer.writeAll(accept);
+ try response_writer.writeAll("\r\n");
+ //if (compression) {
+ // try response_writer.writeAll("Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n");
+ //}
+ try response_writer.writeAll("\r\n");
+
+ return .{
+ .conn = Conn{
+ .socket = socket,
+ .runtime = runtime,
+ .user_data = null,
+ //.compression = compression,
+ },
+ //.compression = compression,
+ };
+}
+
+pub fn runLoop(conn: Conn, handler: Handler, allocator: Allocator) !void {
+ const buffer = try allocator.alloc(u8, 65536);
+ defer allocator.free(buffer);
+
+ var fragment = std.ArrayList(u8).init(allocator);
+ defer fragment.deinit();
+
+ while (true) {
+ const n = conn.socket.recv(conn.runtime, buffer) catch |err| {
+ if (err == error.Closed) {
+ if (handler.on_disconnect) |on_disc| try on_disc(conn);
+ return;
+ }
+ return err;
+ };
+
+ var i: usize = 0;
+ while (i < n) {
+ const byte1 = buffer[i];
+ i += 1;
+ if (i >= n) return error.InvalidFrame;
+ const byte2 = buffer[i];
+ i += 1;
+
+ const fin = (byte1 & 0x80) != 0;
+ const rsv1 = (byte1 & 0x40) != 0;
+ const opcode = byte1 & 0x0F;
+ const has_mask = (byte2 & 0x80) != 0;
+ var payload_len = @as(usize, byte2 & 0x7F);
+
+ if (payload_len == 126) {
+ if (i + 2 > n) return error.InvalidFrame;
+ //payload_len = @as(usize, @bitCast(std.mem.readIntBig(u16, buffer[i..])));
+ //payload_len = @as(usize, @bitCast(std.mem.readInt(u16, buffer[i..], .big)));
+ payload_len = @as(usize, @intCast(buffer[i])) << 8 |
+ @as(usize, @intCast(buffer[i + 1]));
+ i += 2;
+ } else if (payload_len == 127) {
+ if (i + 8 > n) return error.InvalidFrame;
+ //payload_len = @as(usize, @bitCast(std.mem.readIntBig(u64, buffer[i..])));
+ //payload_len = @as(usize, @bitCast(std.mem.readInt(u64, buffer[i..], .big)));
+ payload_len =
+ @as(usize, @intCast(buffer[i + 0])) << 56 |
+ @as(usize, @intCast(buffer[i + 1])) << 48 |
+ @as(usize, @intCast(buffer[i + 2])) << 40 |
+ @as(usize, @intCast(buffer[i + 3])) << 32 |
+ @as(usize, @intCast(buffer[i + 4])) << 24 |
+ @as(usize, @intCast(buffer[i + 5])) << 16 |
+ @as(usize, @intCast(buffer[i + 6])) << 8 |
+ @as(usize, @intCast(buffer[i + 7]));
+ i += 8;
+ }
+
+ var mask: [4]u8 = undefined;
+ if (has_mask) {
+ if (i + 4 > n) return error.InvalidFrame;
+ mask = buffer[i..][0..4].*;
+ i += 4;
+ }
+ if (i + payload_len > n) return error.InvalidFrame;
+
+ const payload_start = i;
+ const payload_end = i + payload_len;
+ i = payload_end;
+
+ if (has_mask) {
+ for (payload_start..payload_end) |j| {
+ buffer[j] ^= mask[(j - payload_start) % 4];
+ }
+ }
+
+ var payload = buffer[payload_start..payload_end];
+
+ // Compression (permessage-deflate) // todo fix
+ if (rsv1) return error.CompressionNotNegotiated;
+ //var decompressed: ?[]u8 = null;
+ //if (rsv1) {
+ // if (!conn.compression) return error.CompressionNotNegotiated;
+ // const inflated_len = payload.len + 4;
+ // const inflated = try allocator.alloc(u8, inflated_len);
+ // errdefer allocator.free(inflated);
+ // @memcpy(inflated[0..payload.len], payload);
+ // inflated[payload.len + 0] = 0x00;
+ // inflated[payload.len + 1] = 0x00;
+ // inflated[payload.len + 2] = 0xFF;
+ // inflated[payload.len + 3] = 0xFF;
+ // const stream = std.io.fixedBufferStream(inflated);
+ // var inflater = compress.flate.InflateStream.init(stream.reader(), .{});
+ // defer inflater.deinit();
+ // var out = std.ArrayList(u8).init(allocator);
+ // errdefer out.deinit();
+ // try inflater.reader().readAllArrayList(&out, 1024 * 1024);
+ // allocator.free(inflated);
+ // decompressed = try out.toOwnedSlice();
+ // payload = decompressed.?;
+ //}
+
+ var is_text: ?bool = null;
+ switch (opcode) {
+ 0x1 => { // Text
+ if (is_text == null) is_text = true;
+
+ if (fin) {
+ if (handler.on_message) |on_msg| try on_msg(conn, payload);
+ //if (decompressed) |d| allocator.free(d);
+ } else {
+ try fragment.appendSlice(payload);
+ //if (decompressed) |d| allocator.free(d);
+ }
+ },
+
+ 0x2 => { // Binary
+ if (is_text == null) is_text = false;
+
+ if (fin) {
+ if (handler.on_binary) |on_bin| try on_bin(conn, payload);
+ //if (decompressed) |d| allocator.free(d);
+ } else {
+ try fragment.appendSlice(payload);
+ //if (decompressed) |d| allocator.free(d);
+ }
+ },
+
+ 0x0 => { // Continuation
+ try fragment.appendSlice(payload);
+ if (fin) {
+ const full = fragment.items;
+ if (is_text orelse true) {
+ if (handler.on_message) |f| try f(conn, full);
+ } else {
+ if (handler.on_binary) |f| try f(conn, full);
+ }
+ is_text = null;
+ fragment.clearRetainingCapacity();
+ }
+ //if (decompressed) |d| allocator.free(d);
+ },
+ 0x8 => { // Close
+ var code: u16 = 1000; // Normal closure code by default
+ var reason_slice: []const u8 = "";
+ if (payload.len >= 2) {
+ code = std.mem.readInt(u16, payload[0..2], .big);
+
+ if (payload.len > 2) {
+ reason_slice = payload[2..];
+ if (!std.unicode.utf8ValidateSlice(reason_slice)) {
+ //conn.socket.socket.close_blocking(); // that makes tardy
+ //if (decompressed) |d| allocator.free(d);
+ if (handler.on_disconnect) |on_disc| try on_disc(conn);
+ return;
+ }
+ }
+ }
+ conn.close(code, "") catch {}; // just exit
+ if (handler.on_close) |on_close| try on_close(conn, code, reason_slice);
+ if (handler.on_disconnect) |on_disc| try on_disc(conn);
+ //if (decompressed) |d| allocator.free(d);
+ return;
+ },
+ 0x9 => { // Ping
+ var buf = std.ArrayList(u8).init(allocator);
+ defer buf.deinit();
+ try writeFrameHeader(buf.writer(), .pong, payload.len, false);
+ try buf.appendSlice(payload);
+ _ = try conn.socket.send_all(conn.runtime, buf.items);
+ //if (decompressed) |d| allocator.free(d);
+ },
+ 0xA => { // Pong - ignore
+ //if (decompressed) |d| allocator.free(d);
+ },
+ else => {
+ //if (decompressed) |d| allocator.free(d);
+ return error.UnsupportedOpcode;
+ },
+ }
+ }
+ }
+}
+
+
+// helpers
+
+fn computeAccept(allocator: Allocator, key: []const u8) ![]const u8 {
+ const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+ var hasher = std.crypto.hash.Sha1.init(.{});
+ hasher.update(key);
+ hasher.update(magic);
+ var hash: [20]u8 = undefined;
+ hasher.final(&hash);
+ var buf: [28]u8 = undefined;
+ const encoded = std.base64.standard.Encoder.encode(&buf, &hash);
+ return try allocator.dupe(u8, encoded);
+}
+
+const OpCode = enum(u8) {
+ continuation = 0x0,
+ text = 0x1,
+ binary = 0x2,
+ close = 0x8,
+ ping = 0x9,
+ pong = 0xA,
+};
+
+fn writeFrameHeader(writer: anytype, opcode: OpCode, payload_len: usize, compressed: bool) !void {
+ var first_byte: u8 = @intFromEnum(opcode);
+ first_byte |= 0x80;
+
+ if (compressed) first_byte |= 0x40;
+ try writer.writeByte(first_byte);
+
+ if (payload_len < 126) {
+ try writer.writeByte(@intCast(payload_len));
+ } else if (payload_len <= 0xFFFF) {
+ var buf: [2]u8 = undefined;
+ std.mem.writeInt(u16, &buf, @intCast(payload_len), .big);
+ try writer.writeAll(&buf);
+ } else {
+ var buf: [8]u8 = undefined;
+ std.mem.writeInt(u64, &buf, payload_len, .big);
+ try writer.writeAll(&buf);
+ }
+}
+
From 1701c22a46e586ab639452296fea957212460caa Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Sat, 27 Dec 2025 13:29:54 +0200
Subject: [PATCH 02/26] ws file upload example, fix ws frame buffer
---
.gitignore | 1 +
build.zig | 14 +-
examples_ws/README.md | 10 +-
examples_ws/bert.zig | 750 +++++++++++++++++++++++++++
examples_ws/example_ws_4.zig | 546 +++++++++++++++++++
examples_ws/static/BigInteger.min.js | 1 +
examples_ws/static/bert_ftp.js | 610 ++++++++++++++++++++++
examples_ws/static/form.js | 68 +++
src/websocket/pubsub.zig | 59 ++-
src/websocket/websocket.zig | 370 +++++++------
10 files changed, 2253 insertions(+), 176 deletions(-)
create mode 100644 examples_ws/bert.zig
create mode 100644 examples_ws/example_ws_4.zig
create mode 100755 examples_ws/static/BigInteger.min.js
create mode 100644 examples_ws/static/bert_ftp.js
create mode 100644 examples_ws/static/form.js
diff --git a/.gitignore b/.gitignore
index 87bbe59..e304926 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ heaptrack*
/ex_ws_1
/ex_ws_2
/ex_ws_3
+/ex_ws_4
diff --git a/build.zig b/build.zig
index b8e916c..21b77aa 100644
--- a/build.zig
+++ b/build.zig
@@ -42,6 +42,7 @@ pub fn build(b: *std.Build) void {
const example1_ws_step = b.step("ex_ws_1", "Build ws example1");
const example2_ws_step = b.step("ex_ws_2", "Build ws example2");
const example3_ws_step = b.step("ex_ws_3", "Build ws example3");
+ const example4_ws_step = b.step("ex_ws_4", "Build ws example4");
const exe_ws1 = b.addExecutable(.{ // without C libs
.name = "ex_ws_1", // exe name
@@ -61,10 +62,17 @@ pub fn build(b: *std.Build) void {
.target = target,
.optimize = optimize,
});
+ const exe_ws4 = b.addExecutable(.{ // without C libs
+ .name = "ex_ws_4", // exe name
+ .root_source_file = b.path("examples_ws/example_ws_4.zig"), // main file
+ .target = target,
+ .optimize = optimize,
+ });
exe_ws1.root_module.addImport("zzz", zzz);
exe_ws2.root_module.addImport("zzz", zzz);
exe_ws3.root_module.addImport("zzz", zzz);
+ exe_ws4.root_module.addImport("zzz", zzz);
const install_ws1 = b.addInstallBinFile(exe_ws1.getEmittedBin(), "../../ex_ws_1"); // b.addInstallBinFile(exe_ws1.getEmittedBin(), "ex_ws_1"); // -femit-bin=ex_ws_1 // to project root
b.getInstallStep().dependOn(&install_ws1.step);
@@ -75,6 +83,9 @@ pub fn build(b: *std.Build) void {
const install_ws3 = b.addInstallBinFile(exe_ws3.getEmittedBin(), "../../ex_ws_3"); // to project root
b.getInstallStep().dependOn(&install_ws3.step);
example3_ws_step.dependOn(&install_ws3.step);
+ const install_ws4 = b.addInstallBinFile(exe_ws4.getEmittedBin(), "../../ex_ws_4"); // to project root
+ b.getInstallStep().dependOn(&install_ws4.step);
+ example4_ws_step.dependOn(&install_ws4.step);
b.default_step = b.getInstallStep();
//b.installArtifact(exe_test1); // saves to /zig-out/bin/test1
@@ -82,7 +93,7 @@ pub fn build(b: *std.Build) void {
all_ws_examples_step.dependOn(&install_ws1.step);
all_ws_examples_step.dependOn(&install_ws2.step);
all_ws_examples_step.dependOn(&install_ws3.step);
-
+ all_ws_examples_step.dependOn(&install_ws4.step);
const tests = b.addTest(.{
@@ -134,3 +145,4 @@ fn add_example(
run_step.dependOn(&install_artifact.step);
run_step.dependOn(&run_artifact.step);
}
+
diff --git a/examples_ws/README.md b/examples_ws/README.md
index 90f3b8f..cf639d3 100644
--- a/examples_ws/README.md
+++ b/examples_ws/README.md
@@ -7,14 +7,17 @@ zig build
zig build ex_ws_1
zig build ex_ws_2
zig build ex_ws_3
+zig build ex_ws_4
# ws example
./ex_ws_1
// goto http://localhost:3010/ , open browser webmaster console
ws.send(42);
ws.send("hello world");
+// in debug build you can see log of received messages
-# wss example (with cert)
+
+# wss example (with cert) // you need sudo because port 443 used
sudo ./ex_ws_2
// goto https://test1.ls/ , open browser webmaster console
ws.send(42);
@@ -35,5 +38,10 @@ ws.send("general:hello 2");
ws.send("general:43");
ws.send("gen:hello");
// and check received messages in first window
+
+
+# ws file upload example, with queue and pause-resume feature
+./ex_ws_4
+// goto http://localhost:3010/ , select files for upload
```
diff --git a/examples_ws/bert.zig b/examples_ws/bert.zig
new file mode 100644
index 0000000..130e107
--- /dev/null
+++ b/examples_ws/bert.zig
@@ -0,0 +1,750 @@
+
+// https://github.com/221V/zig_erl_bert for BERT encode-decode
+
+const std = @import("std");
+
+const Allocator = std.mem.Allocator;
+const ArrayList = std.ArrayList;
+
+const BigInt = std.math.big.int.Managed;
+const Limb = std.math.big.Limb;
+const Endian = std.builtin.Endian;
+
+const assert = std.debug.assert;
+
+
+pub const Value_Tag = enum(u8){
+ int,
+ big_int,
+ float,
+ atom,
+ binary,
+ tuple,
+ list,
+ map,
+ @"null",
+};
+
+
+const Map_Pair = struct{
+ key: Bert_Value,
+ val: Bert_Value,
+};
+
+pub const Bert_Value = union(Value_Tag){
+ int: i64,
+ big_int: BigInt,
+ float: f64,
+ atom: []const u8,
+ binary: []const u8,
+ tuple: []Bert_Value,
+ list: []Bert_Value,
+ map: []Map_Pair,
+ @"null": void,
+};
+
+
+fn readExact(reader: anytype, buf: []u8) !void{
+ var offset: usize = 0;
+ while(offset < buf.len){
+ const n = try reader.read(buf[offset..]);
+ if(n == 0){ return error.EndOfStream; }
+ offset += n;
+ }
+}
+
+
+pub const Bert = struct{
+ const Self = @This();
+ allocator: Allocator,
+
+ pub fn init(allocator: Allocator) Self{
+ return .{ .allocator = allocator };
+ }
+
+ pub fn deinit(_: *Self) void{ }
+
+ // constructors
+ pub fn int(self: Self, n: i64) Bert_Value{
+ _ = self;
+ return .{ .int = n };
+ }
+
+ pub fn bigInt(self: Self, n: BigInt) Bert_Value{
+ _ = self;
+ return .{ .big_int = n };
+ }
+
+ pub fn float(self: Self, f: f64) Bert_Value{
+ _ = self;
+ return .{ .float = f };
+ }
+
+ pub fn atom(self: Self, s: []const u8) !Bert_Value{
+ const copy = try self.allocator.dupe(u8, s);
+ return .{ .atom = copy };
+ }
+
+ pub fn binary(self: Self, data: []const u8) !Bert_Value{
+ const copy = try self.allocator.dupe(u8, data);
+ return .{ .binary = copy };
+ }
+
+ pub fn tuple(self: Self, elems: []const Bert_Value) !Bert_Value{
+ const copy = try self.allocator.dupe(Bert_Value, elems);
+ return .{ .tuple = copy };
+ }
+
+ pub fn list(self: Self, elems: []const Bert_Value) !Bert_Value{
+ const copy = try self.allocator.dupe(Bert_Value, elems);
+ return .{ .list = copy };
+ }
+
+ pub fn map(self: Self, pairs: []const Map_Pair) !Bert_Value{
+ const copy = try self.allocator.dupe(Map_Pair, pairs);
+ return .{ .map = copy };
+ }
+
+ pub fn @"null"(self: Self) Bert_Value{
+ _ = self;
+ return .{ .@"null" = {} };
+ }
+
+ // encode
+ pub fn encode(self: Self, term: Bert_Value) ![]u8{
+ var buffer = ArrayList(u8).init(self.allocator);
+ defer buffer.deinit();
+
+ try buffer.append(131); // VERSION_MAGIC
+ try encodeValue(self.allocator, &buffer, term);
+
+ return buffer.toOwnedSlice();
+ }
+
+ // decode
+ pub fn decode(self: Self, data: []const u8) !Bert_Value{
+ if(data.len == 0 or data[0] != 131){
+ return error.InvalidVersion;
+ }
+ var stream = std.io.fixedBufferStream(data[1..]);
+ return decodeValue(self.allocator, stream.reader());
+ }
+};
+
+
+// helpers
+fn encodeValue(allocator: Allocator, buffer: *ArrayList(u8), value: Bert_Value) !void{
+ switch(value){
+ .int => |n|{
+ if(n >= 0 and n <= 255){
+ try buffer.append(97); // SMALL_INTEGER_EXT
+ try buffer.append( @as(u8, @intCast(n)) );
+ }else if(n >= -2_147_483_648 and n <= 2_147_483_647){
+ try buffer.append(98); // INTEGER_EXT
+ const be = @byteSwap( @as(i32, @intCast(n)) );
+ try buffer.appendSlice(std.mem.asBytes(&be));
+ }else{
+ try encodeBigInt(allocator, buffer, n, false);
+ }
+ },
+
+ .big_int => |bi|{
+ if( bi.eqlZero() ){ // encode zero as SMALL_BIG_EXT: [110, 1, 0, 0]
+ try buffer.append(110);
+ try buffer.append(1);
+ try buffer.append(0);
+ try buffer.append(0);
+ return;
+ }
+
+ const is_neg = !bi.isPositive();
+ var abs_bi = try BigInt.init(allocator);
+ defer abs_bi.deinit();
+ try abs_bi.copy( bi.toConst() );
+ if(is_neg){
+ abs_bi.negate();
+ }
+
+ try encodeBigIntFromAbs(allocator, buffer, &abs_bi, is_neg);
+ },
+
+ .float => |f|{
+ try buffer.append(70); // NEW_FLOAT_EXT
+ const bits = @as(u64, @bitCast(f));
+ var buf: [8]u8 = undefined;
+ std.mem.writeInt(u64, &buf, bits, .big);
+ try buffer.appendSlice(&buf);
+ },
+
+ .atom => |s|{
+ if(s.len > 65535){ return error.AtomTooLong; }
+ try buffer.append(118); // ATOM_UTF8_EXT
+ const len_be = @byteSwap( @as(u16, @intCast( @as(u16, @intCast(s.len)) )) );
+ try buffer.appendSlice(std.mem.asBytes(&len_be));
+ try buffer.appendSlice(s);
+ },
+
+ .binary => |b|{
+ if(b.len > std.math.maxInt(u32)){ return error.BinaryTooLong; }
+ try buffer.append(109); // BINARY_EXT
+ const len_be = @byteSwap( @as(u32, @intCast(b.len)) );
+ try buffer.appendSlice(std.mem.asBytes(&len_be));
+ try buffer.appendSlice(b);
+ },
+
+ .tuple => |elems|{
+ if(elems.len <= 255){
+ try buffer.append(104); // SMALL_TUPLE_EXT
+ try buffer.append( @as(u8, @intCast(elems.len)) );
+ }else{
+ try buffer.append(105); // LARGE_TUPLE_EXT
+ const len_be = @byteSwap( @as(u32, @intCast(elems.len)) );
+ try buffer.appendSlice(std.mem.asBytes(&len_be));
+ }
+ for(elems) |elem|{
+ try encodeValue(allocator, buffer, elem);
+ }
+ },
+
+ .list => |elems|{
+ try buffer.append(108); // LIST_EXT
+ const len_be = @byteSwap( @as(u32, @intCast(elems.len)) );
+ try buffer.appendSlice(std.mem.asBytes(&len_be));
+ for(elems) |elem|{
+ try encodeValue(allocator, buffer, elem);
+ }
+ try buffer.append(106); // NIL_EXT
+ },
+
+ .map => |pairs|{
+ if(pairs.len > std.math.maxInt(u32)){ return error.MapTooLarge; }
+ try buffer.append(116); // MAP_EXT
+ const len_be = @byteSwap( @as(u32, @intCast(pairs.len)) );
+ try buffer.appendSlice(std.mem.asBytes(&len_be));
+ for(pairs) |pair|{
+ try encodeValue(allocator, buffer, pair.key);
+ try encodeValue(allocator, buffer, pair.val);
+ }
+ },
+
+ .@"null" => {
+ try buffer.append(106); // NIL_EXT
+ },
+ }
+}
+
+
+fn encodeBigInt(allocator: Allocator, buffer: *ArrayList(u8), n: i64, is_neg: bool) !void{
+ var abs_n: u64 = if(is_neg) @as(u64, @intCast(-n)) else @as(u64, @intCast(n));
+ var digits = std.ArrayList(u8).init(allocator);
+ defer digits.deinit();
+
+ if(abs_n == 0){
+ try digits.append(0);
+ }else{
+ while(abs_n > 0) : (abs_n >>= 8){
+ try digits.append( @as(u8, @intCast(abs_n & 0xFF)) );
+ }
+ }
+
+ const len = digits.items.len;
+ if(len > 255){ return error.BigIntTooLargeForSmallBig; }
+
+ try buffer.append(110); // SMALL_BIG_EXT
+ try buffer.append( @as(u8, @intCast(len)) );
+ try buffer.append(if (is_neg) 1 else 0);
+ try buffer.appendSlice(digits.items);
+}
+
+
+fn encodeBigIntFromAbs(allocator: Allocator, buffer: *ArrayList(u8), abs_bi: *const BigInt, is_neg: bool) !void{ // abs_bi > 0
+ var digits = std.ArrayList(u8).init(allocator);
+ defer digits.deinit();
+
+ for( abs_bi.limbs[0..abs_bi.len()] ) |limb|{
+ var val = limb;
+ var i: usize = 0;
+ while(i < @sizeOf(Limb)) : (i += 1){
+ try digits.append( @as(u8, @truncate(val)) );
+ val >>= 8;
+ }
+ }
+
+ while(digits.items.len > 1 and digits.items[digits.items.len - 1] == 0){
+ _ = digits.pop();
+ }
+
+ const len = digits.items.len;
+ if(len > 255){ return error.BigIntTooLargeForSmallBig; }
+
+ try buffer.append(110); // SMALL_BIG_EXT
+ try buffer.append( @as(u8, @intCast(len)) );
+ try buffer.append( if(is_neg) 1 else 0 );
+ try buffer.appendSlice(digits.items);
+}
+
+
+fn decodeValue(allocator: Allocator, reader: anytype) !Bert_Value{
+ const tag = try reader.readByte();
+ //std.debug.print("tag: {}\n", .{tag}); // debug
+ switch(tag){
+ 97 => { // SMALL_INTEGER_EXT
+ const b = try reader.readByte();
+ return Bert_Value{ .int = b };
+ },
+
+ 98 => { // INTEGER_EXT
+ var buf: [4]u8 = undefined;
+ try readExact(reader, &buf);
+ const n = std.mem.readInt(i32, &buf, .big);
+ return Bert_Value{ .int = n };
+ },
+
+ 70 => { // NEW_FLOAT_EXT
+ var buf: [8]u8 = undefined;
+ try readExact(reader, &buf);
+ const bits = std.mem.readInt(u64, &buf, .big);
+ return Bert_Value{ .float = @as(f64, @bitCast(bits)) };
+ },
+
+ 118 => { // ATOM_UTF8_EXT
+ var len_buf: [2]u8 = undefined;
+ try readExact(reader, &len_buf);
+ const len = std.mem.readInt(u16, &len_buf, .big);
+ const atom = try allocator.alloc(u8, len);
+ errdefer allocator.free(atom);
+ try readExact(reader, atom);
+ return Bert_Value{ .atom = atom };
+ },
+
+ 107 => { // STRING_EXT
+ var len_buf: [2]u8 = undefined;
+ try readExact(reader, &len_buf);
+ const len = std.mem.readInt(u16, &len_buf, .big);
+ const str = try allocator.alloc(u8, len);
+ errdefer allocator.free(str);
+ try readExact(reader, str);
+ return Bert_Value{ .binary = str };
+ },
+
+ 109 => { // BINARY_EXT
+ var len_buf: [4]u8 = undefined;
+ try readExact(reader, &len_buf);
+ const len = std.mem.readInt(u32, &len_buf, .big);
+ const bin = try allocator.alloc(u8, len);
+ errdefer allocator.free(bin);
+ try readExact(reader, bin);
+ return Bert_Value{ .binary = bin };
+ },
+
+ 104, 105 => { // SMALL_TUPLE_EXT, LARGE_TUPLE_EXT
+ const arity: u32 = if(tag == 104) try reader.readByte() else blk: {
+ var len_buf: [4]u8 = undefined;
+ try readExact(reader, &len_buf);
+ break :blk std.mem.readInt(u32, &len_buf, .big);
+ };
+ const elems = try allocator.alloc(Bert_Value, arity);
+ for(elems) |*elem|{
+ elem.* = try decodeValue(allocator, reader);
+ }
+ return Bert_Value{ .tuple = elems };
+ },
+
+ 108 => { // LIST_EXT
+ var len_buf: [4]u8 = undefined;
+ try readExact(reader, &len_buf);
+ const len = std.mem.readInt(u32, &len_buf, .big);
+ const elems = try allocator.alloc(Bert_Value, len);
+ errdefer allocator.free(elems);
+ for(elems) |*elem|{
+ //for(elems, 0..) |*elem, i|{
+ //std.debug.print("lets Decode list element {}: {any}\n", .{i, elem}); // debug
+ elem.* = try decodeValue(allocator, reader);
+ //std.debug.print("Decoded list element {}: {any}\n", .{i, elem}); // debug
+ }
+ const tail_tag = try reader.readByte();
+ if(tail_tag != 106){ return error.ImproperListNotSupported; }
+ return Bert_Value{ .list = elems };
+ },
+
+ 106 => { // NIL_EXT
+ return Bert_Value{ .@"null" = {} };
+ },
+
+ 110 => { // SMALL_BIG_EXT
+ const n = try reader.readByte();
+ const sign = try reader.readByte();
+ const is_neg = sign != 0;
+
+ if(n == 0){
+ return Bert_Value{ .int = 0 };
+ }
+
+ if(n <= 8){
+ var value: u64 = 0;
+ var shift: u6 = 0;
+ for(0..n) |_|{
+ const byte = try reader.readByte();
+ value |= @as(u64, byte) << @as(u6, shift);
+ shift += 8;
+ }
+
+ if(is_neg){
+ return Bert_Value{ .int = - @as(i64, @intCast(value)) };
+ }else if(value <= @as(u64, @intCast(std.math.maxInt(i64))) ){
+ return Bert_Value{ .int = @as(i64, @intCast(value)) };
+ }
+ }
+
+ var bi = try BigInt.init(allocator);
+ errdefer bi.deinit();
+
+ var base = try BigInt.initSet(allocator, 256);
+ defer base.deinit();
+ var zero = try BigInt.initSet(allocator, 0);
+ defer zero.deinit();
+
+ try bi.set(0);
+ var power = try BigInt.initSet(allocator, 1);
+ defer power.deinit();
+
+ for(0..n) |_|{
+ const byte = try reader.readByte();
+ var digit = try BigInt.initSet(allocator, byte);
+ defer digit.deinit();
+ var term = try BigInt.init(allocator);
+ defer term.deinit();
+ try term.mul(&digit, &power);
+ try bi.add(&bi, &term);
+ var next_power = try BigInt.init(allocator);
+ defer next_power.deinit();
+ try next_power.mul(&power, &base);
+ power.deinit();
+ power = next_power;
+ }
+
+ if(is_neg){
+ bi.negate();
+ }
+
+ return Bert_Value{ .big_int = bi };
+ },
+
+ 116 => { // MAP_EXT
+ var len_buf: [4]u8 = undefined;
+ try readExact(reader, &len_buf);
+ const len = std.mem.readInt(u32, &len_buf, .big);
+ const pairs = try allocator.alloc(Map_Pair, len);
+ for(pairs) |*pair|{
+ pair.key = try decodeValue(allocator, reader);
+ pair.val = try decodeValue(allocator, reader);
+ }
+ return Bert_Value{ .map = pairs };
+ },
+
+ else => return error.UnsupportedTag,
+ }
+}
+
+
+// next for matching - select values from decoded with try, without shitcode (if(..){..}else if(..){..})
+
+pub const Bert_Get_Error = error{
+ Not_List,
+ Not_Tuple,
+ Not_Map,
+ Not_Atom,
+ Not_Binary,
+ Not_Int,
+ Not_Big_Int,
+ Not_Float,
+ Not_Found,
+ Index_Out_Of_Range,
+ Value_Out_Of_Range,
+ Big_Int_Too_Large, // bigger than max u128 = 340282366920938463463374607431768211455
+};
+
+
+pub fn get_int_as_u8(val: Bert_Value) Bert_Get_Error!u8{
+ return switch(val){
+ .int => |i|{
+ if(i < 0 or i > std.math.maxInt(u8)){ return error.Value_Out_Of_Range; }
+ return @as(u8, @intCast(i));
+ },
+ else => error.Not_Int,
+ };
+}
+
+
+pub fn get_int_as_u16(val: Bert_Value) Bert_Get_Error!u16{
+ return switch(val){
+ .int => |i|{
+ if(i < 0 or i > std.math.maxInt(u16)){ return error.Value_Out_Of_Range; }
+ return @as(u16, @intCast(i));
+ },
+ else => error.Not_Int,
+ };
+}
+
+
+pub fn get_int_as_u32(val: Bert_Value) Bert_Get_Error!u32{
+ return switch(val){
+ .int => |i|{
+ if(i < 0 or i > std.math.maxInt(u32)){ return error.Value_Out_Of_Range; }
+ return @as(u32, @intCast(i));
+ },
+ else => error.Not_Int,
+ };
+}
+
+
+pub fn get_int_as_u64(val: Bert_Value) Bert_Get_Error!u64{
+ return switch(val){
+ .int => |i|{
+ if(i < 0){ return error.Value_Out_Of_Range; } // 0 - 18446744073709551615
+ return @as(u64, @intCast(i));
+ },
+ else => error.Not_Int,
+ };
+}
+
+
+pub fn get_big_int_as_u128(val: Bert_Value) Bert_Get_Error!u128{
+ const bi = switch(val){
+ .big_int => |b| b,
+ else => return error.Not_Big_Int,
+ };
+
+ if(!bi.isPositive()){ return error.Value_Out_Of_Range; } // negative value
+
+ const limb_cnt = bi.len(); // limbs count (0..2) // each limb is little‑endian u64, 2 pcs for u128 needs
+ if(limb_cnt == 0){ return @as(u128, 0); }
+ if(limb_cnt > 2){ return error.Big_Int_Too_Large; } // bigger than max u128
+
+ const low = @as(u128, @intCast(bi.limbs[0]));
+ var result = low;
+
+ if(limb_cnt == 2){
+ const high = @as(u128, @intCast(bi.limbs[1]));
+ result = (high << 64) | low; // shift and concatenate
+ }
+
+ return result;
+}
+
+
+pub fn get_int_as_i8(val: Bert_Value) Bert_Get_Error!i8{
+ return switch(val){
+ .int => |i|{
+ if(i < std.math.minInt(i8) or i > std.math.maxInt(i8)){ return error.Value_Out_Of_Range; }
+ return @as(i8, @intCast(i));
+ },
+ else => error.Not_Int,
+ };
+}
+
+
+pub fn get_int_as_i16(val: Bert_Value) Bert_Get_Error!i16{
+ return switch(val){
+ .int => |i|{
+ if(i < std.math.minInt(i16) or i > std.math.maxInt(i16)){ return error.Value_Out_Of_Range; }
+ return @as(i16, @intCast(i));
+ },
+ else => error.Not_Int,
+ };
+}
+
+
+pub fn get_int_as_i32(val: Bert_Value) Bert_Get_Error!i32{
+ return switch(val){
+ .int => |i|{
+ if(i < std.math.minInt(i32) or i > std.math.maxInt(i32)){ return error.Value_Out_Of_Range; }
+ return @as(i32, @intCast(i));
+ },
+ else => error.Not_Int,
+ };
+}
+
+
+pub fn get_int_as_i64(val: Bert_Value) Bert_Get_Error!i64{
+ return switch(val){
+ .int => |i| i, // i64
+ else => error.Not_Int,
+ };
+}
+
+
+pub fn get_float_as_f64(val: Bert_Value) Bert_Get_Error!f64{
+ return switch(val){
+ .float => |f| f, // f64
+ else => error.Not_Float,
+ };
+}
+
+
+pub fn get_atom_as_str(val: Bert_Value) Bert_Get_Error![]const u8{
+ return switch(val){
+ .atom => |at| at, // []const u8
+ else => error.Not_Atom,
+ };
+}
+
+
+pub fn get_binary_as_str(val: Bert_Value) Bert_Get_Error![]const u8{
+ return switch(val){
+ .binary => |bin| bin, // []const u8
+ else => error.Not_Binary,
+ };
+}
+
+
+pub fn get_list_elem(val: Bert_Value, idx: u32) Bert_Get_Error!Bert_Value{ // get elem from list
+ return switch(val){
+ .list => |elems|{
+ if(idx >= elems.len){ return error.Index_Out; }
+ return elems[idx];
+ },
+ else => error.Not_List,
+ };
+}
+
+
+pub fn get_tuple_elem(val: Bert_Value, idx: u32) Bert_Get_Error!Bert_Value{ // get elem from tuple
+ return switch(val){
+ .tuple => |elems|{
+ if(idx >= elems.len){ return error.Index_Out_Of_Range; }
+ return elems[idx];
+ },
+ else => error.Not_Tuple,
+ };
+}
+
+
+pub fn get_list(val: Bert_Value) Bert_Get_Error![]Bert_Value{ // get list
+ return switch(val){
+ .list => |elems| elems,
+ else => error.Not_List,
+ };
+}
+
+
+pub fn get_tuple(val: Bert_Value) Bert_Get_Error![]Bert_Value{ // get tuple
+ return switch(val){
+ .tuple => |elems| elems,
+ else => error.Not_Tuple,
+ };
+}
+
+
+pub fn map_lookup(val: Bert_Value, key: Bert_Value) Bert_Get_Error!Bert_Value{
+ return switch(val){
+ .map => |pairs|{
+ for(pairs) |pair|{
+ if(std.meta.eql(pair.key, key)){ return pair.val; } // compare type and value
+ }
+ return error.Not_Found;
+ },
+ else => error.Not_Map,
+ };
+}
+
+
+// next for pretty print in erlang style
+
+pub fn format_bert(allocator: Allocator, value: Bert_Value) ![]const u8{
+ var out = std.ArrayList(u8).init(allocator);
+ defer out.deinit();
+
+ try writeValue(&out, value);
+ return out.toOwnedSlice();
+}
+
+
+fn writeValue(buf: *std.ArrayList(u8), value: Bert_Value) !void{
+ const writer = buf.writer();
+ switch(value){
+ .int => |i|{
+ try writer.print("{}", .{i});
+ },
+
+ .big_int => |bi|{ // BigInt to decimal string
+ const s = try bi.toString(buf.allocator, 10, .lower);
+ defer buf.allocator.free(s);
+ try writer.print("{any}", .{s});
+ },
+
+ .float => |f|{
+ try writer.print("{any}", .{f});
+ },
+
+ .atom => |a|{
+ const needQuotes = !isSimpleAtom(a); // simple atoms (ascii, digits and _ symbol, begins from ascii symbol)
+ if(needQuotes) try writer.print("'{s}'", .{a}) else try writer.print("{s}", .{a});
+ },
+
+ .binary => |b|{
+ //if(isPrintableAscii(b)){ // as text when ascii, otherwise bytes
+ try writer.print("<<\"{s}\">>", .{b});
+ //}else{
+ // try writer.print("<<", .{});
+ // for(b, 0..) |byte, i|{
+ // if(i != 0){ try writer.print(",", .{}); }
+ // try writer.print("{}", .{byte});
+ // }
+ // try writer.print(">>", .{});
+ //}
+ },
+
+ .tuple => |elems|{
+ try writer.print("{{", .{});
+ for(elems, 0..) |e, i|{
+ if(i != 0){ try writer.print(", ", .{}); }
+ try writeValue(buf, e);
+ }
+ try writer.print("}}", .{});
+ },
+
+ .list => |elems|{
+ try writer.print("[", .{});
+ for(elems, 0..) |e, i|{
+ if(i != 0){ try writer.print(", ", .{}); }
+ try writeValue(buf, e);
+ }
+ try writer.print("]", .{});
+ },
+
+ .map => |pairs|{
+ try writer.print("#{{", .{});
+ for(pairs, 0..) |p, i|{
+ if(i != 0){ try writer.print(", ", .{}); }
+ try writeValue(buf, p.key);
+ try writer.print(" => ", .{});
+ try writeValue(buf, p.val);
+ }
+ try writer.print("}}", .{});
+ },
+
+ .@"null" => { // NIL is empty list in BERT
+ try writer.print("[]", .{});
+ },
+ }
+}
+
+
+fn isSimpleAtom(a: []const u8) bool{
+ if(a.len == 0){ return false; }
+ if(std.ascii.isDigit(a[0])){ return false; } // first symbol can not be digit
+ for(a) |c|{
+ if(!std.ascii.isAlphanumeric(c) and c != '_'){ return false; }
+ }
+ return true;
+}
+
+
+fn isPrintableAscii(b: []const u8) bool{
+ for(b) |c|{
+ if(c < 32 or c > 126){ return false; }
+ }
+ return true;
+}
+
diff --git a/examples_ws/example_ws_4.zig b/examples_ws/example_ws_4.zig
new file mode 100644
index 0000000..72dc732
--- /dev/null
+++ b/examples_ws/example_ws_4.zig
@@ -0,0 +1,546 @@
+const std = @import("std");
+
+const zzz = @import("zzz");
+const Socket = zzz.tardy.Socket;
+const File = zzz.tardy.File;
+const Dir = zzz.tardy.Dir;
+const Timer = zzz.tardy.Timer; // for cleaner
+
+const websocket = zzz.websocket;
+const WsSession = zzz.WsSession;
+const handle_upgrade = zzz.handle_upgrade;
+
+const Bert = @import("bert.zig").Bert; // https://github.com/221V/zig_erl_bert for BERT encode-decode
+const Bert_Value = @import("bert.zig").Bert_Value;
+
+
+const PORT = 3010;
+const HOST = "0.0.0.0";
+
+//const WS_STACK_SIZE = 1024 * 1024; // DEBUG
+const WS_STACK_SIZE = 32 * 1024; // RELEASE
+
+
+const ExtensionsLimits = struct {
+ //ext: []const u8,
+ exts: []const []const u8, // for set few filetypes with same max size
+ size: usize,
+};
+
+const UploadConfig = struct {
+ save_path_temp: []const u8 = "uploads/temp",
+ save_path_root: []const u8 = "uploads",
+ max_size_default: usize = 10 * 1024 * 1024, // default 10Mb
+
+ allow_extensions: ?[]const []const u8 = &.{ ".jpg", ".png", ".gif", ".txt", ".pdf", ".mp3", ".mp4", ".avi" },
+ deny_extensions: ?[]const []const u8 = &.{ ".exe", ".sh" },
+
+ extension_limits: []const ExtensionsLimits = &.{
+ .{ .exts = &.{ ".txt" }, .size = 1 * 1024 * 1024 }, // 1Mb max
+ .{
+ .exts = &.{ ".jpg", ".png", ".gif", ".pdf" },
+ .size = 5 * 1024 * 1024 // 5Mb max
+ },
+ .{
+ .exts = &.{ ".mp3", ".mp4" },
+ .size = 50 * 1024 * 1024 // 50Mb max
+ },
+ .{ .exts = &.{ ".avi" }, .size = 2 * 1024 * 1024 * 1024 }, // 2Gb max
+ },
+
+ ttl_seconds: i64 = 24 * 60 * 60, // 24 hours for temp files
+};
+
+const config = UploadConfig{};
+
+
+// session state for uploads
+const UploadState = struct {
+ file: File,
+ path: []const u8,
+ path_done: []const u8,
+ total_size: usize,
+ current_size: usize,
+ last_update: i64,
+};
+
+
+// global or per-session map // use per-session user_data custom struct
+const SessionContext = struct {
+ uploads: std.StringHashMap(UploadState),
+ allocator: std.mem.Allocator,
+};
+
+
+// helpers
+
+fn checkLimits(name: []const u8, size: usize) !void {
+ const raw_ext = std.fs.path.extension(name);
+
+ var ext_buf: [10]u8 = undefined; // 10 chars length for files extension
+ if (raw_ext.len >= ext_buf.len) return error.ExtensionTooLong;
+
+ const lower_ext = std.ascii.lowerString(&ext_buf, raw_ext);
+ const ext = std.mem.trim(u8, lower_ext, &[_]u8{ 0, ' ', '\t', '\r', '\n' });
+
+ std.log.info("CheckLimits: file='{s}', raw_ext='{s}', normalized_ext='{s}', size={d}", .{ name, raw_ext, ext, size });
+
+ if (config.deny_extensions) |list| { // check blacklist
+ for (list) |denied| {
+ if (std.mem.eql(u8, ext, denied)) return error.ExtensionDenied;
+ }
+ }
+
+ if (config.allow_extensions) |list| { // check whitelist
+ var found = false;
+ for (list) |allowed| {
+ if (std.mem.eql(u8, ext, allowed)) { found = true; break; }
+ }
+ if (!found){
+ std.log.err("CheckLimits: Extension '{s}' not found in allow list", .{ext});
+ return error.ExtensionNotAllowed;
+ }
+ }
+
+ var limit = config.max_size_default;
+
+ outer: for (config.extension_limits) |group| { // lets check file size limit // label :outer for exit both loops
+ for (group.exts) |group_ext| {
+ if (std.mem.eql(u8, ext, group_ext)) {
+ limit = group.size;
+ //std.log.info("CheckLimits: Matched group for '{s}', setting limit to {d}", .{group_ext, limit});
+ break :outer; // limit found, stop search
+ }
+ }
+ }
+
+ //std.log.info("CheckLimits: Checking size {d} vs limit {d}", .{size, limit});
+ if (size > limit) {
+ std.log.err("CheckLimits: FAILED. Size {d} > Limit {d}", .{size, limit});
+ return error.FileTooLarge;
+ }
+}
+
+
+fn getSavePath(allocator: std.mem.Allocator, filename: []const u8, total_size: usize) ![]const u8 {
+ //const ts = std.time.timestamp(); // construct path: root/timestamp_file
+ //const basename = std.fs.path.basename(filename);
+ //std.log.info("getSavePath: config.save_path_root = '{s}', basename = '{s}', ts = {d}", .{ config.save_path_root, basename, ts });
+
+ var hasher = std.hash.Wyhash.init(0); // construct path: root/.
+ hasher.update(filename);
+ hasher.update(std.mem.asBytes(&total_size));
+ const hash = hasher.final();
+
+ const raw_ext = std.fs.path.extension(filename);
+
+ var ext_buf: [10]u8 = undefined; // 10 chars length for files extension
+ //if (raw_ext.len >= ext_buf.len) return error.ExtensionTooLong;
+
+ const ext = if (raw_ext.len < ext_buf.len)
+ std.ascii.lowerString(&ext_buf, raw_ext)
+ else
+ raw_ext;
+
+ //std.log.info("getSavePath: config.save_path_root = '{s}', ext = '{s}', ts = {d}", .{ config.save_path_root, ext, ts });
+ std.log.info("getSavePath: config.save_path_root = '{s}', hash = '{x}', ext = {s}", .{ config.save_path_root, hash, ext });
+ return std.fmt.allocPrint(allocator, "{s}/{x}{s}", .{config.save_path_root, hash, ext}); // .{config.save_path_root, ts, ext}); // .{config.save_path_root, ts, basename});
+}
+
+
+fn getFileInfo(allocator: std.mem.Allocator, filename: []const u8, total_size: usize, is_temp: bool) !struct{ path: []const u8, id: []const u8 } {
+ var hasher = std.hash.Wyhash.init(0);
+ hasher.update(filename);
+ hasher.update(std.mem.asBytes(&total_size));
+ const hash = hasher.final();
+
+ const ext = std.fs.path.extension(filename);
+ const id_str = try std.fmt.allocPrint(allocator, "{x}", .{hash}); // id = hex hash
+
+ const root = if (is_temp) config.save_path_temp else config.save_path_root;
+ const path = try std.fmt.allocPrint(allocator, "{s}/{s}{s}", .{root, id_str, ext});
+
+ return .{ .path = path, .id = id_str };
+}
+
+
+fn cleanup_task(rt: *zzz.tardy.Runtime) !void {
+ std.log.info("Cleanup task started", .{});
+ while (true) {
+ try Timer.delay(rt, .{ .seconds = 3600 }); // sleep 1 hour
+
+ std.log.info("Cleanup task: Scanning for stale uploads...", .{});
+ var dir = std.fs.cwd().openDir(config.save_path_temp, .{ .iterate = true }) catch continue;
+ defer dir.close();
+
+ var iter = dir.iterate();
+ const now = std.time.timestamp();
+
+ while (iter.next() catch null) |entry| {
+ if (entry.kind == .file) {
+ const stat = dir.statFile(entry.name) catch continue;
+ const mtime_sec = @divFloor(stat.mtime, std.time.ns_per_s); // time of last modification, in ns
+
+ if (now - mtime_sec > config.ttl_seconds) {
+ std.log.info("Cleanup task: Deleting stale file {s} (age: {d}s)", .{entry.name, now - mtime_sec});
+ dir.deleteFile(entry.name) catch |e| std.log.err("Cleanup task delete error: {s}", .{ @errorName(e) });
+ }
+ }
+ }
+ }
+}
+
+
+// WS Handlers
+
+fn on_ws_connect(session: *WsSession) !void {
+ std.log.info("Client connected", .{});
+
+ const ctx = try session.allocator.create(SessionContext); // init session context
+ ctx.* = .{
+ .uploads = std.StringHashMap(UploadState).init(session.allocator),
+ .allocator = session.allocator,
+ };
+
+ session.context = ctx;
+}
+
+fn on_ws_message(session: *WsSession, data: []const u8) !void {
+ // we expect binary BERT data -- if text, ignore or log
+ _ = session;
+ std.log.info("Received Text (ignoring): {s}", .{data});
+}
+
+
+// helper for on_ws_binary file_upload handle
+fn handleInitUpload(allocator: std.mem.Allocator, ctx: *SessionContext, name: []const u8, total: usize) !struct { offset: usize, response_id: []const u8 } {
+ std.log.info("DEBUG: handleInitUpload start for {s}", .{name});
+
+ checkLimits(name, total) catch |e| {
+ std.log.err("Limit check failed: {s}", .{ @errorName(e) });
+ return e;
+ };
+
+ const info_temp = try getFileInfo(ctx.allocator, name, total, true);
+ const info_done = try getFileInfo(ctx.allocator, name, total, false); // for check maybe already uploaded
+
+ std.log.info("DEBUG: Generated ID: {s}", .{info_temp.id});
+
+ std.fs.cwd().makePath(config.save_path_root) catch {}; // ensure dir exists
+ std.fs.cwd().makePath(config.save_path_temp) catch {};
+
+ if (ctx.uploads.fetchRemove(info_temp.id)) |kv| { // close existing handle if resuming
+ std.log.info("Resuming active session: closing old handle for {s}", .{name});
+ kv.value.file.close_blocking();
+
+ ctx.allocator.free(kv.key);
+ ctx.allocator.free(kv.value.path);
+ ctx.allocator.free(kv.value.path_done);
+ }
+
+ if (std.fs.cwd().access(info_done.path, .{})) { // already uploaded
+ std.log.info("File already uploaded: {s}", .{info_done.path});
+ return .{ .offset = total, .response_id = info_temp.id }; // success, done
+
+ //ctx.allocator.free(info_temp.path);
+ //ctx.allocator.free(info_temp.id);
+ //ctx.allocator.free(info_done.path);
+ //ctx.allocator.free(info_done.id);
+
+ }else |_| {} // check temp (resume)
+
+ var file_exists = false;
+ var existing_size: usize = 0;
+
+ //const existing_file_result = std.fs.cwd().openFile(info_temp.path, .{ .mode = .read_write }); // try to open temp file that exists
+ //if(existing_file_result) |f| { // check if exists for resume
+ if( std.fs.cwd().openFile(info_temp.path, .{ .mode = .read_write }) ) |f| { // check if exists for resume
+ const stat = f.stat() catch |e| {
+ std.log.err("Stat failed: {s}", .{ @errorName(e) });
+ f.close();
+ return e;
+ };
+ //const stat = try f.stat();
+
+ if(stat.size < total){ // resume - not uploded yet
+ file_exists = true;
+ existing_size = stat.size;
+ f.close();
+
+ } else if(stat.size == total){ // already uploaded ok
+ f.close();
+ std.log.info("Temp file complete, returning done", .{});
+ return .{ .offset = total, .response_id = info_temp.id }; // temp file done
+
+ //ctx.allocator.free(info_temp.path);
+ //ctx.allocator.free(info_temp.id);
+ //ctx.allocator.free(info_done.path);
+ //ctx.allocator.free(info_done.id);
+
+ } else { // error? rewrite
+ f.close();
+ }
+
+ }else |err| { // file not exists or access error
+ if (err != error.FileNotFound) {
+ std.log.err("Failed to check existing file '{s}': {s}", .{ info_temp.path, @errorName(err) }); // std.log.warn("OpenFile warning (not fatal): {s}", .{ @errorName(err) });
+ return err; // return err and not rewrite
+ }
+ }
+
+ std.log.info("DEBUG: Opening file: {s}... Exists: {}", .{ info_temp.path, file_exists });
+
+ const file = if (file_exists) // open for writing
+ try std.fs.cwd().openFile(info_temp.path, .{ .mode = .read_write })
+ else
+ try std.fs.cwd().createFile(info_temp.path, .{});
+
+ if(file_exists){ // if resuming or new file
+ try file.seekTo(existing_size);
+ std.log.info("RESUMING: {s} -> {d}/{d}", .{info_temp.id, existing_size, total});
+ }else{
+ std.log.info("NEW: {s}", .{info_temp.id});
+ }
+
+ // Convert std.fs.File to tardy.File if needed, for non-blocking file I/O
+ // Better use tardy.File, but for simplicity we use std.fs.File handle wrapped
+ const tardy_file = zzz.tardy.File.from_std(file);
+ const id_key = try allocator.dupe(u8, info_temp.id);
+
+ try ctx.uploads.put(id_key, .{
+ .file = tardy_file,
+ .path = info_temp.path, // here write
+ .path_done = info_done.path, // here move when upload 100%
+ .total_size = total,
+ .current_size = if(file_exists) existing_size else 0,
+ .last_update = std.time.milliTimestamp(),
+ });
+
+ //ctx.allocator.free(info_done.id);
+ std.log.info("DEBUG: Init success, returning", .{});
+ return .{ .offset = if(file_exists) existing_size else 0, .response_id = info_temp.id };
+}
+
+fn on_ws_binary(session: *WsSession, data: []const u8) !void {
+ var arena = std.heap.ArenaAllocator.init(session.allocator); // temp arena for parsing
+ defer arena.deinit();
+ const temp_alloc = arena.allocator();
+
+ var b = Bert.init(temp_alloc);
+ const val = b.decode(data) catch |e| { // for file data Bert_Value must be Tuple{ftp, id, ..., data, status}
+ std.log.err("BERT Decode Error: {s}", .{ @errorName(e) });
+ return;
+ };
+
+ switch(val){
+ .tuple => |elems| {
+ if(elems.len != 13){ return; } // not file data // n2o ftp protocol has 13 elements
+
+ const atom_tag = elems[0];
+ switch (atom_tag) {
+ .atom => |s| if (!std.mem.eql(u8, s, "ftp")) return, // check 'ftp' atom
+ else => return,
+ }
+
+ const id = try get_binary_str(elems[1]);
+ const name = try get_binary_str(elems[3]);
+ const total = try get_int_usize(elems[8]);
+ const offset = try get_int_usize(elems[9]);
+ const bin_data = try get_binary_str(elems[11]);
+ const status = try get_binary_str(elems[12]);
+
+ const ctx_ptr = session.context orelse return; // get upload context
+ const ctx: *SessionContext = @ptrCast(@alignCast(ctx_ptr));
+
+ var reply_status: []const u8 = "send";
+ var current_offset: usize = 0;
+ var response_id: []const u8 = id; // client id by default
+
+ if(std.mem.eql(u8, status, "init")){ // start upload
+ std.log.info("Init upload: {s} ({d} bytes)", .{name, total});
+
+ if( handleInitUpload(ctx.allocator, ctx, name, total) ) |res| {
+ current_offset = res.offset;
+ response_id = res.response_id;
+ std.log.info("WS: Init OK, offset={d}", .{current_offset});
+ } else |err| {
+ std.log.err("Init failed with exception: {s}", .{ @errorName(err) });
+ reply_status = "error";
+ }
+
+
+ }else if(std.mem.eql(u8, status, "send")){ // chunk received
+ if (ctx.uploads.getPtr(id)) |state| { // here id must be server id because client has update it after init
+ if (offset != state.current_size) { // validate offset
+ current_offset = state.current_size; // client out of sync? tell him where we are
+ }else{ // write data
+ _ = try state.file.write_all(session.conn.runtime, bin_data, null);
+ state.current_size += bin_data.len;
+ state.last_update = std.time.milliTimestamp();
+ current_offset = state.current_size;
+
+ if (state.current_size >= state.total_size) {
+ std.log.info("Upload complete. Move to: {s}", .{state.path_done});
+ state.file.close_blocking();
+
+ std.fs.cwd().rename(state.path, state.path_done) catch |e| {
+ std.log.err("Failed to move file: {s}", .{ @errorName(e) }); // maybe todo send err status to client.. but file already uploaded
+ };
+
+ if(ctx.uploads.fetchRemove(id)) |kv| {
+ ctx.allocator.free(kv.key); // id
+ ctx.allocator.free(kv.value.path);
+ ctx.allocator.free(kv.value.path_done);
+ }
+
+ //_ = ctx.uploads.remove(id);
+ }
+ }
+
+ }else{
+ reply_status = "error"; // upload session not found
+ }
+ }
+
+ var reply_tuple_items = [_]Bert_Value{ // encode Reply
+ b.atom("ftp") catch unreachable,
+ b.binary(response_id) catch unreachable,
+ b.binary("") catch unreachable, // sid
+ b.binary("") catch unreachable, // name (empty for ack)
+ b.binary("") catch unreachable,
+ b.binary("") catch unreachable,
+ b.binary("") catch unreachable,
+ b.binary("") catch unreachable,
+ b.int(@intCast(total)),
+ b.int(@intCast(current_offset)),
+ b.int(0), // block
+ b.binary("") catch unreachable, // data
+ b.binary(reply_status) catch unreachable, // status
+ };
+
+ std.log.info("WS: Sending Reply status='{s}' offset={d}", .{reply_status, current_offset});
+ const encoded = try b.encode(try b.tuple(&reply_tuple_items));
+
+ session.scheduleSendBinary(encoded) catch |e| {
+ std.log.err("WS: scheduleSendBinary FAILED: {s}", .{ @errorName(e) });
+ };
+ },
+ else => {},
+ }
+}
+
+// helpers
+fn get_binary_str(v: Bert_Value) ![]const u8 {
+ return switch(v) { .binary => |b| b, else => error.NotBinary };
+}
+
+fn get_int_usize(v: Bert_Value) !usize {
+ return switch(v) {
+ //.int => |i| @intCast(i),
+ //.big_int => 0,
+ .int => |i| if (i < 0) error.NegativeValue else @intCast(i),
+ .big_int => |bi| bi.toConst().toInt(usize) catch error.IntTooLarge,
+ else => error.NotInt
+ };
+}
+
+
+fn on_ws_close(session: *WsSession) void {
+ if (session.context) |ptr| {
+ session.context = null; // clean context for avoid double free error
+
+ const ctx: *SessionContext = @ptrCast(@alignCast(ptr));
+
+ var iter = ctx.uploads.iterator();
+ while (iter.next()) |entry| { // clean up open files
+ entry.value_ptr.file.close_blocking();
+ }
+ ctx.uploads.deinit();
+
+ session.allocator.destroy(ctx);
+ }
+}
+
+
+// HTTP Handler
+fn on_request(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
+ const res = ctx.response;
+ res.status = .OK;
+ res.mime = zzz.HTTP.Mime.HTML;
+ // embed the JS client
+ res.body =
+ \\
+ \\
+ \\zzz + WS File Upload Example
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ ;
+ return .standard;
+}
+
+fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
+ return handle_upgrade(ctx, .{
+ .on_connect = on_ws_connect,
+ .on_message = on_ws_message,
+ .on_binary = on_ws_binary,
+ .on_close = on_ws_close,
+ }, WS_STACK_SIZE);
+}
+
+
+pub fn main() !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ const allocator = gpa.allocator();
+ defer _ = gpa.deinit();
+
+ const socket = try Socket.init(.{ .tcp = .{ .host = HOST, .port = PORT } });
+ defer socket.close_blocking();
+ try socket.bind();
+ try socket.listen(1024);
+
+ const TardyType = zzz.tardy.Tardy(.auto);
+ var tardy = try TardyType.init(allocator, .{});
+ defer tardy.deinit();
+
+ try tardy.entry(&socket, struct {
+ fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void {
+ const server_config = zzz.ServerConfig{ .stack_size = 64 * 1024 };
+
+ const home_route = zzz.HTTP.Route.init("/").get({}, on_request);
+ const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint);
+
+ //const js_route = zzz.HTTP.FsDir.serve("/static", zzz.tardy.Dir.cwd()); // bert_ftp.js
+ const std_static_dir = try std.fs.cwd().openDir("examples_ws/static", .{ .iterate = true });
+ const static_dir = zzz.tardy.Dir.from_std(std_static_dir);
+ const js_route = zzz.HTTP.FsDir.serve("/static", static_dir); // bert_ftp.js
+
+ const layers = &[_]zzz.HTTP.Layer{ home_route.layer(), ws_route.layer(), js_route };
+
+ const router = try rt.allocator.create(zzz.Router);
+ router.* = try zzz.Router.init(rt.allocator, layers, .{ .not_found = on_request });
+
+ const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision));
+ provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static);
+ const byte_count = provisions.items.len * @sizeOf(zzz.Provision);
+ @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0);
+
+ const connection_count = try rt.allocator.create(usize);
+ connection_count.* = 0;
+ const accept_queued = try rt.allocator.create(bool);
+ accept_queued.* = false;
+
+ try rt.spawn(
+ .{ rt, server_config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, connection_count, accept_queued },
+ zzz.Server.main_frame,
+ server_config.stack_size
+ );
+
+ //try rt.spawn(.{rt}, cleanup_task, 64 * 1024);
+ }
+ }.entry);
+}
+
diff --git a/examples_ws/static/BigInteger.min.js b/examples_ws/static/BigInteger.min.js
new file mode 100755
index 0000000..2303a07
--- /dev/null
+++ b/examples_ws/static/BigInteger.min.js
@@ -0,0 +1 @@
+var bigInt=function(t){"use strict";var e=1e7,r=7,o=9007199254740992,n=v(o),i="0123456789abcdefghijklmnopqrstuvwxyz",u="function"==typeof BigInt;function p(t,e,r,o){return void 0===t?p[0]:void 0!==e&&(10!=+e||r)?_(t,e,r,o):K(t)}function a(t,e){this.value=t,this.sign=e,this.isSmall=!1}function s(t){this.value=t,this.sign=t<0,this.isSmall=!0}function l(t){this.value=t}function f(t){return-o0?Math.floor(t):Math.ceil(t)}function m(t,r){var o,n,i=t.length,u=r.length,p=new Array(i),a=0,s=e;for(n=0;n=s?1:0,p[n]=o-a*s;for(;n0&&p.push(a),p}function d(t,e){return t.length>=e.length?m(t,e):m(e,t)}function b(t,r){var o,n,i=t.length,u=new Array(i),p=e;for(n=0;n0;)u[n++]=r%p,r=Math.floor(r/p);return u}function w(t,r){var o,n,i=t.length,u=r.length,p=new Array(i),a=0,s=e;for(o=0;o0;)u[n++]=a%p,a=Math.floor(a/p);return u}function M(t,e){for(var r=[];e-- >0;)r.push(0);return r.concat(t)}function N(t,r,o){return new a(t=0;--o)i=(u=i*s+t[o])-(n=c(u/r))*r,a[o]=0|n;return[a,0|i]}function B(t,r){var o,n=K(r);if(u)return[new l(t.value/n.value),new l(t.value%n.value)];var i,f=t.value,m=n.value;if(0===m)throw new Error("Cannot divide by zero");if(t.isSmall)return n.isSmall?[new s(c(f/m)),new s(f%m)]:[p[0],t];if(n.isSmall){if(1===m)return[t,p[0]];if(-1==m)return[t.negate(),p[0]];var d=Math.abs(m);if(d=0;n--){for(o=v-1,d[n+f]!==c&&(o=Math.floor((d[n+f]*v+d[n+f-1])/c)),i=0,u=0,a=b.length,p=0;ps&&(i=(i+1)*v),o=Math.ceil(i/u);do{if(A(p=q(r,o),f)<=0)break;o--}while(o);l.push(o),f=w(f,p)}return l.reverse(),[h(l),h(f)]}(f,m))[0];var I=t.sign!==n.sign,M=o[1],N=t.sign;return"number"==typeof i?(I&&(i=-i),i=new s(i)):i=new a(i,I),"number"==typeof M?(N&&(M=-M),M=new s(M)):M=new a(M,N),[i,M]}function A(t,e){if(t.length!==e.length)return t.length>e.length?1:-1;for(var r=t.length-1;r>=0;r--)if(t[r]!==e[r])return t[r]>e[r]?1:-1;return 0}function P(t){var e=t.abs();return!e.isUnit()&&(!!(e.equals(2)||e.equals(3)||e.equals(5))||!(e.isEven()||e.isDivisibleBy(3)||e.isDivisibleBy(5))&&(!!e.lesser(49)||void 0))}function Z(t,e){for(var r,o,n,i=t.prev(),u=i,p=0;u.isEven();)u=u.divide(2),p++;t:for(o=0;o=0?o=w(t,e):(o=w(e,t),r=!r),"number"==typeof(o=h(o))?(r&&(o=-o),new s(o)):new a(o,r)}(r,o,this.sign)},a.prototype.minus=a.prototype.subtract,s.prototype.subtract=function(t){var e=K(t),r=this.value;if(r<0!==e.sign)return this.add(e.negate());var o=e.value;return e.isSmall?new s(r-o):S(o,Math.abs(r),r>=0)},s.prototype.minus=s.prototype.subtract,l.prototype.subtract=function(t){return new l(this.value-K(t).value)},l.prototype.minus=l.prototype.subtract,a.prototype.negate=function(){return new a(this.value,!this.sign)},s.prototype.negate=function(){var t=this.sign,e=new s(-this.value);return e.sign=!t,e},l.prototype.negate=function(){return new l(-this.value)},a.prototype.abs=function(){return new a(this.value,!1)},s.prototype.abs=function(){return new s(Math.abs(this.value))},l.prototype.abs=function(){return new l(this.value>=0?this.value:-this.value)},a.prototype.multiply=function(t){var r,o,n,i=K(t),u=this.value,s=i.value,l=this.sign!==i.sign;if(i.isSmall){if(0===s)return p[0];if(1===s)return this;if(-1===s)return this.negate();if((r=Math.abs(s))0?function t(e,r){var o=Math.max(e.length,r.length);if(o<=30)return I(e,r);o=Math.ceil(o/2);var n=e.slice(o),i=e.slice(0,o),u=r.slice(o),p=r.slice(0,o),a=t(i,p),s=t(n,u),l=t(d(i,n),d(p,u)),f=d(d(a,M(w(w(l,a),s),o)),M(s,2*o));return y(f),f}(u,s):I(u,s),l)},a.prototype.times=a.prototype.multiply,s.prototype._multiplyBySmall=function(t){return f(t.value*this.value)?new s(t.value*this.value):N(Math.abs(t.value),v(Math.abs(this.value)),this.sign!==t.sign)},a.prototype._multiplyBySmall=function(t){return 0===t.value?p[0]:1===t.value?this:-1===t.value?this.negate():N(Math.abs(t.value),this.value,this.sign!==t.sign)},s.prototype.multiply=function(t){return K(t)._multiplyBySmall(this)},s.prototype.times=s.prototype.multiply,l.prototype.multiply=function(t){return new l(this.value*K(t).value)},l.prototype.times=l.prototype.multiply,a.prototype.square=function(){return new a(E(this.value),!1)},s.prototype.square=function(){var t=this.value*this.value;return f(t)?new s(t):new a(E(v(Math.abs(this.value))),!1)},l.prototype.square=function(t){return new l(this.value*this.value)},a.prototype.divmod=function(t){var e=B(this,t);return{quotient:e[0],remainder:e[1]}},l.prototype.divmod=s.prototype.divmod=a.prototype.divmod,a.prototype.divide=function(t){return B(this,t)[0]},l.prototype.over=l.prototype.divide=function(t){return new l(this.value/K(t).value)},s.prototype.over=s.prototype.divide=a.prototype.over=a.prototype.divide,a.prototype.mod=function(t){return B(this,t)[1]},l.prototype.mod=l.prototype.remainder=function(t){return new l(this.value%K(t).value)},s.prototype.remainder=s.prototype.mod=a.prototype.remainder=a.prototype.mod,a.prototype.pow=function(t){var e,r,o,n=K(t),i=this.value,u=n.value;if(0===u)return p[1];if(0===i)return p[0];if(1===i)return p[1];if(-1===i)return n.isEven()?p[1]:p[-1];if(n.sign)return p[0];if(!n.isSmall)throw new Error("The exponent "+n.toString()+" is too large.");if(this.isSmall&&f(e=Math.pow(i,u)))return new s(c(e));for(r=this,o=p[1];!0&u&&(o=o.times(r),--u),0!==u;)u/=2,r=r.square();return o},s.prototype.pow=a.prototype.pow,l.prototype.pow=function(t){var e=K(t),r=this.value,o=e.value,n=BigInt(0),i=BigInt(1),u=BigInt(2);if(o===n)return p[1];if(r===n)return p[0];if(r===i)return p[1];if(r===BigInt(-1))return e.isEven()?p[1]:p[-1];if(e.isNegative())return new l(n);for(var a=this,s=p[1];(o&i)===i&&(s=s.times(a),--o),o!==n;)o/=u,a=a.square();return s},a.prototype.modPow=function(t,e){if(t=K(t),(e=K(e)).isZero())throw new Error("Cannot take modPow with modulus 0");var r=p[1],o=this.mod(e);for(t.isNegative()&&(t=t.multiply(p[-1]),o=o.modInv(e));t.isPositive();){if(o.isZero())return p[0];t.isOdd()&&(r=r.multiply(o).mod(e)),t=t.divide(2),o=o.square().mod(e)}return r},l.prototype.modPow=s.prototype.modPow=a.prototype.modPow,a.prototype.compareAbs=function(t){var e=K(t),r=this.value,o=e.value;return e.isSmall?1:A(r,o)},s.prototype.compareAbs=function(t){var e=K(t),r=Math.abs(this.value),o=e.value;return e.isSmall?r===(o=Math.abs(o))?0:r>o?1:-1:-1},l.prototype.compareAbs=function(t){var e=this.value,r=K(t).value;return(e=e>=0?e:-e)===(r=r>=0?r:-r)?0:e>r?1:-1},a.prototype.compare=function(t){if(t===1/0)return-1;if(t===-1/0)return 1;var e=K(t),r=this.value,o=e.value;return this.sign!==e.sign?e.sign?1:-1:e.isSmall?this.sign?-1:1:A(r,o)*(this.sign?-1:1)},a.prototype.compareTo=a.prototype.compare,s.prototype.compare=function(t){if(t===1/0)return-1;if(t===-1/0)return 1;var e=K(t),r=this.value,o=e.value;return e.isSmall?r==o?0:r>o?1:-1:r<0!==e.sign?r<0?-1:1:r<0?1:-1},s.prototype.compareTo=s.prototype.compare,l.prototype.compare=function(t){if(t===1/0)return-1;if(t===-1/0)return 1;var e=this.value,r=K(t).value;return e===r?0:e>r?1:-1},l.prototype.compareTo=l.prototype.compare,a.prototype.equals=function(t){return 0===this.compare(t)},l.prototype.eq=l.prototype.equals=s.prototype.eq=s.prototype.equals=a.prototype.eq=a.prototype.equals,a.prototype.notEquals=function(t){return 0!==this.compare(t)},l.prototype.neq=l.prototype.notEquals=s.prototype.neq=s.prototype.notEquals=a.prototype.neq=a.prototype.notEquals,a.prototype.greater=function(t){return this.compare(t)>0},l.prototype.gt=l.prototype.greater=s.prototype.gt=s.prototype.greater=a.prototype.gt=a.prototype.greater,a.prototype.lesser=function(t){return this.compare(t)<0},l.prototype.lt=l.prototype.lesser=s.prototype.lt=s.prototype.lesser=a.prototype.lt=a.prototype.lesser,a.prototype.greaterOrEquals=function(t){return this.compare(t)>=0},l.prototype.geq=l.prototype.greaterOrEquals=s.prototype.geq=s.prototype.greaterOrEquals=a.prototype.geq=a.prototype.greaterOrEquals,a.prototype.lesserOrEquals=function(t){return this.compare(t)<=0},l.prototype.leq=l.prototype.lesserOrEquals=s.prototype.leq=s.prototype.lesserOrEquals=a.prototype.leq=a.prototype.lesserOrEquals,a.prototype.isEven=function(){return 0==(1&this.value[0])},s.prototype.isEven=function(){return 0==(1&this.value)},l.prototype.isEven=function(){return(this.value&BigInt(1))===BigInt(0)},a.prototype.isOdd=function(){return 1==(1&this.value[0])},s.prototype.isOdd=function(){return 1==(1&this.value)},l.prototype.isOdd=function(){return(this.value&BigInt(1))===BigInt(1)},a.prototype.isPositive=function(){return!this.sign},s.prototype.isPositive=function(){return this.value>0},l.prototype.isPositive=s.prototype.isPositive,a.prototype.isNegative=function(){return this.sign},s.prototype.isNegative=function(){return this.value<0},l.prototype.isNegative=s.prototype.isNegative,a.prototype.isUnit=function(){return!1},s.prototype.isUnit=function(){return 1===Math.abs(this.value)},l.prototype.isUnit=function(){return this.abs().value===BigInt(1)},a.prototype.isZero=function(){return!1},s.prototype.isZero=function(){return 0===this.value},l.prototype.isZero=function(){return this.value===BigInt(0)},a.prototype.isDivisibleBy=function(t){var e=K(t);return!e.isZero()&&(!!e.isUnit()||(0===e.compareAbs(2)?this.isEven():this.mod(e).isZero()))},l.prototype.isDivisibleBy=s.prototype.isDivisibleBy=a.prototype.isDivisibleBy,a.prototype.isPrime=function(t){var e=P(this);if(void 0!==e)return e;var r=this.abs(),o=r.bitLength();if(o<=64)return Z(r,[2,3,5,7,11,13,17,19,23,29,31,37]);for(var n=Math.log(2)*o.toJSNumber(),i=Math.ceil(!0===t?2*Math.pow(n,2):n),u=[],p=0;p-o?new s(t-1):new a(n,!0)},l.prototype.prev=function(){return new l(this.value-BigInt(1))};for(var x=[1];2*x[x.length-1]<=e;)x.push(2*x[x.length-1]);var J=x.length,L=x[J-1];function U(t){return Math.abs(t)<=e}function T(t,e,r){e=K(e);for(var o=t.isNegative(),n=e.isNegative(),i=o?t.not():t,u=n?e.not():e,p=0,a=0,s=null,l=null,f=[];!i.isZero()||!u.isZero();)p=(s=B(i,L))[1].toJSNumber(),o&&(p=L-1-p),a=(l=B(u,L))[1].toJSNumber(),n&&(a=L-1-a),i=s[0],u=l[0],f.push(r(p,a));for(var v=0!==r(o?1:0,n?1:0)?bigInt(-1):bigInt(0),h=f.length-1;h>=0;h-=1)v=v.multiply(L).add(bigInt(f[h]));return v}a.prototype.shiftLeft=function(t){var e=K(t).toJSNumber();if(!U(e))throw new Error(String(e)+" is too large for shifting.");if(e<0)return this.shiftRight(-e);var r=this;if(r.isZero())return r;for(;e>=J;)r=r.multiply(L),e-=J-1;return r.multiply(x[e])},l.prototype.shiftLeft=s.prototype.shiftLeft=a.prototype.shiftLeft,a.prototype.shiftRight=function(t){var e,r=K(t).toJSNumber();if(!U(r))throw new Error(String(r)+" is too large for shifting.");if(r<0)return this.shiftLeft(-r);for(var o=this;r>=J;){if(o.isZero()||o.isNegative()&&o.isUnit())return o;o=(e=B(o,L))[1].isNegative()?e[0].prev():e[0],r-=J-1}return(e=B(o,x[r]))[1].isNegative()?e[0].prev():e[0]},l.prototype.shiftRight=s.prototype.shiftRight=a.prototype.shiftRight,a.prototype.not=function(){return this.negate().prev()},l.prototype.not=s.prototype.not=a.prototype.not,a.prototype.and=function(t){return T(this,t,function(t,e){return t&e})},l.prototype.and=s.prototype.and=a.prototype.and,a.prototype.or=function(t){return T(this,t,function(t,e){return t|e})},l.prototype.or=s.prototype.or=a.prototype.or,a.prototype.xor=function(t){return T(this,t,function(t,e){return t^e})},l.prototype.xor=s.prototype.xor=a.prototype.xor;var j=1<<30,C=(e&-e)*(e&-e)|j;function D(t){var r=t.value,o="number"==typeof r?r|j:"bigint"==typeof r?r|BigInt(j):r[0]+r[1]*e|C;return o&-o}function z(t,e){return t=K(t),e=K(e),t.greater(e)?t:e}function R(t,e){return t=K(t),e=K(e),t.lesser(e)?t:e}function k(t,e){if(t=K(t).abs(),e=K(e).abs(),t.equals(e))return t;if(t.isZero())return e;if(e.isZero())return t;for(var r,o,n=p[1];t.isEven()&&e.isEven();)r=R(D(t),D(e)),t=t.divide(r),e=e.divide(r),n=n.multiply(r);for(;t.isEven();)t=t.divide(D(t));do{for(;e.isEven();)e=e.divide(D(e));t.greater(e)&&(o=e,e=t,t=o),e=e.subtract(t)}while(!e.isZero());return n.isUnit()?t:t.multiply(n)}a.prototype.bitLength=function(){var t=this;return t.compareTo(bigInt(0))<0&&(t=t.negate().subtract(bigInt(1))),0===t.compareTo(bigInt(0))?bigInt(0):bigInt(function t(e,r){if(r.compareTo(e)<=0){var o=t(e,r.square(r)),n=o.p,i=o.e,u=n.multiply(r);return u.compareTo(e)<=0?{p:u,e:2*i+1}:{p:n,e:2*i}}return{p:bigInt(1),e:0}}(t,bigInt(2)).e).add(bigInt(1))},l.prototype.bitLength=s.prototype.bitLength=a.prototype.bitLength;var _=function(t,e,r,o){r=r||i,t=String(t),o||(t=t.toLowerCase(),r=r.toLowerCase());var n,u=t.length,p=Math.abs(e),a={};for(n=0;n=p)){if("1"===f&&1===p)continue;throw new Error(f+" is not a valid digit in base "+e+".")}}e=K(e);var s=[],l="-"===t[0];for(n=l?1:0;n"!==t[n]&&n=0;o--)n=n.add(t[o].times(i)),i=i.times(e);return r?n.negate():n}function F(t,e){if((e=bigInt(e)).isZero()){if(t.isZero())return{value:[0],isNegative:!1};throw new Error("Cannot convert nonzero numbers to base 0.")}if(e.equals(-1)){if(t.isZero())return{value:[0],isNegative:!1};if(t.isNegative())return{value:[].concat.apply([],Array.apply(null,Array(-t.toJSNumber())).map(Array.prototype.valueOf,[1,0])),isNegative:!1};var r=Array.apply(null,Array(t.toJSNumber()-1)).map(Array.prototype.valueOf,[0,1]);return r.unshift([1]),{value:[].concat.apply([],r),isNegative:!1}}var o=!1;if(t.isNegative()&&e.isPositive()&&(o=!0,t=t.abs()),e.isUnit())return t.isZero()?{value:[0],isNegative:!1}:{value:Array.apply(null,Array(t.toJSNumber())).map(Number.prototype.valueOf,1),isNegative:o};for(var n,i=[],u=t;u.isNegative()||u.compareAbs(e)>=0;){n=u.divmod(e),u=n.quotient;var p=n.remainder;p.isNegative()&&(p=e.minus(p).abs(),u=u.next()),i.push(p.toJSNumber())}return i.push(u.toJSNumber()),{value:i.reverse(),isNegative:o}}function G(t,e,r){var o=F(t,e);return(o.isNegative?"-":"")+o.value.map(function(t){return function(t,e){return t<(e=e||i).length?e[t]:"<"+t+">"}(t,r)}).join("")}function H(t){if(f(+t)){var e=+t;if(e===c(e))return u?new l(BigInt(e)):new s(e);throw new Error("Invalid integer: "+t)}var o="-"===t[0];o&&(t=t.slice(1));var n=t.split(/e/i);if(n.length>2)throw new Error("Invalid integer: "+n.join("e"));if(2===n.length){var i=n[1];if("+"===i[0]&&(i=i.slice(1)),(i=+i)!==c(i)||!f(i))throw new Error("Invalid integer: "+i+" is not a valid exponent.");var p=n[0],v=p.indexOf(".");if(v>=0&&(i-=p.length-v-1,p=p.slice(0,v)+p.slice(v+1)),i<0)throw new Error("Cannot include negative exponent part for integers");t=p+=new Array(i+1).join("0")}if(!/^([0-9][0-9]*)$/.test(t))throw new Error("Invalid integer: "+t);if(u)return new l(BigInt(o?"-"+t:t));for(var h=[],g=t.length,m=r,d=g-m;g>0;)h.push(+t.slice(d,g)),(d-=m)<0&&(d=0),g-=m;return y(h),new a(h,o)}function K(t){return"number"==typeof t?function(t){if(u)return new l(BigInt(t));if(f(t)){if(t!==c(t))throw new Error(t+" is not an integer.");return new s(t)}return H(t.toString())}(t):"string"==typeof t?H(t):"bigint"==typeof t?new l(t):t}a.prototype.toArray=function(t){return F(this,t)},s.prototype.toArray=function(t){return F(this,t)},l.prototype.toArray=function(t){return F(this,t)},a.prototype.toString=function(t,e){if(void 0===t&&(t=10),10!==t)return G(this,t,e);for(var r,o=this.value,n=o.length,i=String(o[--n]);--n>=0;)r=String(o[n]),i+="0000000".slice(r.length)+r;return(this.sign?"-":"")+i},s.prototype.toString=function(t,e){return void 0===t&&(t=10),10!=t?G(this,t,e):String(this.value)},l.prototype.toString=s.prototype.toString,l.prototype.toJSON=a.prototype.toJSON=s.prototype.toJSON=function(){return this.toString()},a.prototype.valueOf=function(){return parseInt(this.toString(),10)},a.prototype.toJSNumber=a.prototype.valueOf,s.prototype.valueOf=function(){return this.value},s.prototype.toJSNumber=s.prototype.valueOf,l.prototype.valueOf=l.prototype.toJSNumber=function(){return parseInt(this.toString(),10)};for(var Q=0;Q<1e3;Q++)p[Q]=K(Q),Q>0&&(p[-Q]=K(-Q));return p.one=p[1],p.zero=p[0],p.minusOne=p[-1],p.max=z,p.min=R,p.gcd=k,p.lcm=function(t,e){return t=K(t).abs(),e=K(e).abs(),t.divide(k(t,e)).multiply(e)},p.isInstance=function(t){return t instanceof a||t instanceof s||t instanceof l},p.randBetween=function(t,r,o){t=K(t),r=K(r);var n=o||Math.random,i=R(t,r),u=z(t,r).subtract(i).add(1);if(u.isSmall)return i.add(Math.floor(n()*u));for(var a=F(u,e).value,s=[],l=!0,f=0;f= 0 && o < 256){ return { t: 97, v: o }; }
+ if(isInteger && o >= -2147483648 && o <= 2147483647){ return {t: 98, v: o}; }
+ return {t: 110, v: o};
+}
+
+// BigInt to BERT, with https://github.com/peterolson/BigInteger.js
+function bignum(o){
+ if(bigInt.isInstance(o) === false){ return {t: 999, v: [97, 0]}; } // o is not bigInt
+ if(o.greaterOrEquals(0) && o.lesser(256)){
+ // t: 97
+ return {t: 999, v: [97, o.toJSNumber() ]};
+ }
+ if(o.greaterOrEquals(-2147483648) && o.lesserOrEquals(2147483647)){
+ // t: 98
+ return {t: 999, v: [98, o.shiftRight(24).toJSNumber(), o.shiftRight(16).and(255).toJSNumber(), o.shiftRight(8).and(255).toJSNumber(), o.and(255).toJSNumber() ]};
+ }
+ // t: 110
+ if(o.isNegative()){
+ var sign = 1;
+ var s = bignum_to_bytes(o.abs());
+ }else{
+ var sign = 0;
+ var s = bignum_to_bytes(o);
+ }
+ return {t: 999, v: [110, s.length, sign].concat(s) };
+}
+
+function bin(o){
+ return { t: 109, v: o instanceof ArrayBuffer ? new Uint8Array(o) :
+ o instanceof Uint8Array ? o : utf8_enc(o) };
+}
+
+
+// bert encoder
+function enc(o){ return fl([131, ein(o)]); }
+function ein(o){
+ return Array.isArray(o) ? en_108({ t: 108, v: o }) :
+ (o.t == 999 ? o.v : eval('en_' + o.t)(o) ); // t: 999 = bigInt, already encoded in bignum func
+}
+function en_undefined(o){ return [106]; }
+function unilen(o){
+ return (o.v instanceof ArrayBuffer || o.v instanceof Uint8Array) ? o.v.byteLength :
+ (new TextEncoder().encode(o.v)).byteLength;
+}
+function en_70(o){
+ var x = Array(8).fill(0).flat();
+ write_Float(x, o.v, 0, false, 52, 8);
+ return [70].concat(x);
+}
+function en_97(o){ return [97, o.v]; }
+function en_98(o){ return [98, o.v >>> 24, (o.v >>> 16) & 255, (o.v >>> 8) & 255, o.v & 255]; }
+function en_99(o){
+ var obj = o.v.toExponential(20),
+ match = /([^e]+)(e[+-])(\d+)/.exec(obj),
+ exponentialPart = match[3].length == 1 ? "0" + match[3] : match[3],
+ num = Array.from(bin(match[1] + match[2] + exponentialPart).v);
+ return [o.t].concat(num).concat(Array(31 - num.length).fill(0).flat());
+}
+function en_100(o){ return [100, o.v.length >>> 8, o.v.length & 255, ar(o)]; }
+function en_104(o){
+ var l = o.v.length,
+ r = [];
+ for(var i = 0; i < l; i++) r[i] = ein(o.v[i]);
+ return [104, l, r];
+}
+function en_106(o){ return [106]; }
+function en_107(o){ return [107, o.v.length >>> 8, o.v.length & 255, ar(o)]; }
+function en_108(o){
+ var l = o.v.length,
+ r = [];
+ for(var i = 0; i < l; i++) r.push(ein(o.v[i]));
+ return o.v.length == 0 ? [106] :
+ [108, l >>> 24, (l >>> 16) & 255, (l >>> 8) & 255, l & 255, r, 106];
+}
+function en_109(o){
+ var l = unilen(o);
+ return [109, l >>> 24, (l >>> 16) & 255, (l >>> 8) & 255, l & 255, ar(o)];
+}
+function en_110(o){
+ if(o.v < 0){
+ var sign = 1;
+ var s = int_to_bytes(-o.v);
+ }else{
+ var sign = 0;
+ var s = int_to_bytes(o.v);
+ }
+ return [110, s.length, sign].concat(s);
+}
+function en_115(o){ return [115, o.v.length, ar(o)]; }
+function en_116(o){
+ var l = o.v.length,
+ x = [],
+ r = [];
+ for(var i = 0; i < l; i++) r.push([ein(o.v[i].k), ein(o.v[i].v)]);
+ x = [116, l >>> 24, (l >>> 16) & 255, (l >>> 8) & 255, l & 255];
+ return o.v.length == 0 ? x : [x, r];
+}
+function en_118(o){ return [118, ar(o).length >>> 8, ar(o).length & 255, ar(o)]; }
+function en_119(o){ return [119, ar(o).length, ar(o)]; }
+
+
+// bert decoder
+function nop(b){ return []; }
+function big(b){
+ var sk = b == 1 ? sx.getUint8(ix++) : sx.getInt32((a = ix, ix += 4, a));
+ var ret = 0,
+ sig = sx.getUint8(ix++),
+ count = sk;
+ while(count-- > 0){
+ ret = 256 * ret + sx.getUint8(ix + count);
+ }
+ ix += sk;
+ return ret * (sig == 0 ? 1 : -1);
+}
+function int(b){
+ return b == 1 ? sx.getUint8(ix++) : sx.getInt32((a = ix, ix += 4, a));
+}
+function dec(d){
+ sx = new DataView(d);
+ ix = 0;
+ if(sx.getUint8(ix++) !== 131) throw ("BERT?");
+ return din();
+}
+function str(b){
+ var dv,
+ sz = (b == 2 ? sx.getUint16(ix) : (b == 1 ? sx.getUint8(ix) : sx.getUint32(ix)));
+ ix += b;
+ var r = sx.buffer.slice(ix, ix += sz);
+ return utf8_arr(r);
+}
+function run(b){
+ var sz = (b == 1 ? sx.getUint8(ix) : sx.getUint32(ix)),
+ r = [];
+ ix += b;
+ for(var i = 0; i < sz; i++) r.push(din());
+ if(b == 4) ix++;
+ return r;
+}
+function rut(b){
+ var sz = (b == 1 ? sx.getUint8(ix) : sx.getUint32(ix)),
+ r = [];
+ ix += b;
+ for(var i = 0; i < sz; i++) r.push(din());
+ din();
+ return r;
+}
+function dic(b){
+ var sz = sx.getUint32(ix),
+ r = [];
+ ix += 4;
+ for(var i = 0; i < sz; i++) r.push({k: din(), v: din()});
+ return r;
+}
+function iee(x){
+ return read_Float(new Uint8Array(sx.buffer.slice(ix, ix += 8)), 0, false, 52, 8);
+}
+
+function flo(x){
+ return parseFloat(utf8_arr(sx.buffer.slice(ix, ix += 31)));
+}
+
+function arr(b){
+ var dv,
+ sz = sx.getUint16(ix);
+ ix += b;
+ return new Uint8Array(sx.buffer.slice(ix, ix += sz));
+}
+
+function ref(cr){
+ var d,
+ adj = sx.getUint8(ix++);
+ adj += sx.getUint8(ix++);
+ d = din();
+ ix += cr + adj * 4;
+ return d;
+}
+
+function din(){
+ var x,
+ c = sx.getUint8(ix++);
+ switch(c){
+ case 70: x = [iee, 0]; break;
+ case 90: x = [ref, 4]; break;
+ case 97: x = [int, 1]; break;
+ case 98: x = [int, 4]; break;
+ case 99: x = [flo, 0]; break;
+ case 100: x = [str, 2]; break;
+ case 104: x = [run, 1]; break;
+ case 105: x = [run, 4]; break;
+ case 107: x = [arr, 2]; break;
+ case 108: x = [rut, 4]; break;
+ case 109: x = [str, 4]; break;
+ case 110: x = [big, 1]; break;
+ case 111: x = [big, 4]; break;
+ case 114: x = [ref, 1]; break;
+ case 115: x = [str, 1]; break;
+ case 116: x = [dic, 4]; break;
+ case 118: x = [str, 2]; break;
+ case 119: x = [str, 1]; break;
+ default: x = [nop, 0];
+ } return { t: c, v: x[0](x[1]) };
+}
+
+
+// bert helpers
+function int_to_bytes(Int){
+ if(Int % 1 !== 0) return [0];
+ var OriginalInt,
+ Rem,
+ s = [];
+ OriginalInt = Int;
+ while(Int !== 0){
+ Rem = Int % 256;
+ s.push(Rem);
+ Int = Math.floor(Int / 256);
+ }
+ if(Int > 0){ throw ("Argument out of range: " + OriginalInt); }
+ return s;
+}
+
+function bignum_to_bytes(big_Int){
+ var v,
+ big_Int,
+ s = [];
+ big_Int2 = big_Int;
+ while(big_Int2.isZero() === false){
+ v = big_Int2.divmod(256);
+ s.push(v.remainder.toJSNumber());
+ big_Int2 = v.quotient;
+ }
+ if(big_Int2.greater(0)){ throw ("Argument out of range::: " + big_Int.toString() ); }
+ return s;
+}
+
+function uc(u1, u2){
+ if(u1.byteLength == 0) return u2;
+ if(u2.byteLength == 0) return u1;
+ var a = new Uint8Array(u1.byteLength + u2.byteLength);
+ a.set(u1, 0);
+ a.set(u2, u1.byteLength);
+ return a;
+}
+function ar(o){
+ return o.v instanceof ArrayBuffer ? new Uint8Array(o.v) : o.v instanceof Uint8Array ? o.v :
+ Array.isArray(o.v) ? new Uint8Array(o.v) : new Uint8Array(utf8_enc(o.v));
+}
+function fl(a){
+ return a.reduce(function(f, t){
+ return uc(f, t instanceof Uint8Array ? t :
+ Array.isArray(t) ? fl(t) : new Uint8Array([t]));
+ }, new Uint8Array());
+}
+
+
+// save and restore ftp files queue
+function saveState(){
+ var state = ftp.queue.filter(i => i.status !== 'done').map(i => ({
+ id: i.id,
+ uid: i.uid,
+ name: i.name,
+ total: i.total,
+ offset: i.offset,
+ status: 'paused'
+ }));
+ localStorage.setItem('ftp_queue', JSON.stringify(state));
+}
+
+// todo add cleanState // localStorage.removeItem('ftp_queue');
+function restoreState(){
+ var stored = localStorage.getItem('ftp_queue');
+ if(stored){
+ try{
+ var items = JSON.parse(stored);
+ items.forEach(i => {
+ if(ftp.item(i.id)) return; // no dup when too laggy internet
+
+ var item = {
+ id: i.id,
+ uid: i.uid || i.id,
+ name: i.name,
+ status: 'missing_file', // special
+ status_block_id: ftp.status_block_id || 'ftp-status',
+ ui_update: (typeof ui_update !== 'undefined') ? ui_update : null,
+ autostart: false,
+ offset: i.offset,
+ block: 32 * 1024,
+ total: i.total,
+ file: null // no file -- needs to select
+ };
+
+ ftp.queue.push(item);
+
+ if(ui_add){ ui_add(item); }
+ if(item.ui_update){ item.ui_update(item, "Please re-select file for resume upload"); }
+ });
+
+ }catch(e){ console.error("Restore failed: ", e); }
+ }
+}
+
+// WS FTP Logic
+var ftp = {
+ queue: [],
+ active: false,
+ last_save_time: 0, // for save queue - item offset to localStorage
+ init: function(file, ui_add = false, ui_update = false){
+ var id = Math.floor(Math.random() * 1000000).toString();
+ var item = {
+ id: id,
+ uid: id,
+ status: 'init',
+ status_block_id: ftp.status_block_id || 'ftp-status',
+ ui_update: ui_update,
+ autostart: ftp.autostart || false,
+ name: ftp.filename || file.name,
+ offset: 0,
+ block: 32 * 1024, // chunk size 32KB
+ total: file.size,
+ file: file
+ };
+ ftp.queue.push(item);
+ saveState();
+ if(ui_add){ ui_add(item); }
+ if(!ftp.active) ftp.start();
+ return item.id;
+ },
+
+ start: function(id){
+ if(ftp.active && !id) return;
+ var item = id ? ftp.item(id) : ftp.next();
+ ftp.active = (item) ? true : false;
+ if(item){
+ if(item.status === 'init'){
+ ftp.send(item, new Uint8Array(0));
+ }else{
+ ftp.read_slice(item);
+ }
+ }
+ },
+
+ stop: function(id){ // pause
+ var item = ftp.item(id);
+ if(item){
+ item.autostart = false;
+ if(ftp.active && ftp.current_id === item.id) ftp.active = false;
+ if(item.ui_update){ item.ui_update(item, "Paused"); }
+ saveState();
+ }
+ },
+
+ resume: function(id){
+ var item = ftp.item(id);
+ if(item){
+
+ if(!item.file || item.status === 'missing_file'){ // after page reload
+ console.log("File object lost after reload. Please re-select the file using the input button."); // todo add ui
+ return;
+ } // otherwise we have files selected already
+
+ item.autostart = true;
+ item.status = 'init'; // re-init to check offset on server
+ if(ftp.current_id === item.id) ftp.active = false;
+ if(item.ui_update){ item.ui_update(item, "Resuming..."); }
+ if(!ftp.active) ftp.start();
+ }
+ },
+
+ send: function(item, data){
+ ftp.current_id = item.id;
+ // Tuple: {ftp, Id, Sid, Name, Meta, Other1, Other2, Other3, Total, Offset, Block, Data, Status}
+ var msg = tuple(atom('ftp'), bin(item.id), bin(''), bin(item.name), bin(''), bin(''), bin(''), bin(''),
+ number(item.total), number(item.offset), number(data.byteLength), bin(data), bin(item.status));
+ ws_send(enc(msg)); // ws.send(enc(msg));
+ },
+
+ read_slice: function(item){
+ if(!item.autostart) return;
+ var reader = new FileReader();
+ reader.onloadend = function(e){
+ if(e.target.readyState === FileReader.DONE){
+ ftp.send(item, new Uint8Array(e.target.result));
+ }
+ };
+ reader.onerror = function(e){
+ console.error("FileReader error:", e.target.error);
+ item.autostart = false;
+ if(item.ui_update){ item.ui_update(item, "Read error"); }
+ };
+ var end = item.offset + item.block;
+ if(end > item.total) end = item.total;
+ reader.readAsArrayBuffer( item.file.slice(item.offset, end) );
+ },
+
+ item: function(id){ return ftp.queue.find(i => i.id === id || i.uid === id); },
+
+ next: function(){ return ftp.queue.find(next => next && next.autostart && (next.status === 'init' || next.offset < next.total) ); }
+ //next: function(){ return ftp.queue.find(next => next.offset < next.total); }
+};
+
+
+// main Socket logic
+var ws;
+var reconnectTimer = null;
+
+function connect(){
+ if(ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
+
+ var proto = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
+ ws = new WebSocket(proto + window.location.host + "/ws");
+ ws.binaryType = "arraybuffer";
+
+ ws.onopen = function(){
+ console.log("Connected");
+ if(reconnectTimer) { clearInterval(reconnectTimer); reconnectTimer = null; }
+
+ var activeItem = ftp.queue.find(i => i.autostart && i.file && i.status !== 'done'); // filter for not complete file uploads for resume
+ if(activeItem){
+ console.log("Auto-resuming upload:", activeItem.name);
+ ftp.active = false;
+ activeItem.status = 'init';
+ ftp.start(activeItem.id);
+ }else{
+ if(ftp.active) ftp.start();
+ }
+ };
+
+ ws.onclose = function(){
+ console.log("Disconnected. Reconnecting...");
+ ws = null;
+ if(!reconnectTimer) reconnectTimer = setInterval(connect, 3000);
+ };
+
+ ws.onerror = function(e){
+ console.log("WS Error", e);
+ ws.close(); // trigger onclose logic
+ };
+
+ ws.onmessage = function(evt){
+ if(evt.data instanceof ArrayBuffer){
+ if(evt.data.byteLength === 0) return; // ignore empty
+ try{
+ var msg = dec(evt.data);
+ //console.log("WS received:", evt.data, msg);
+ if(msg.t === 104 && msg.v[0].t === 118 && msg.v[0].v === "ftp"){ // check if tuple {ftp, ...}
+ var v = msg.v; // [atom, id, sid, name, meta, o1, o2, o3, total, offset, block, data, status]
+ var server_id = utf8_arr(v[1].v); // server returns file_id
+ var offset = v[9].v;
+ var status = utf8_arr(v[12].v);
+
+ //console.log("WS FTP Reply -> ID:", server_id, "Status:", status, "Offset:", offset);
+
+ //var item = ftp.item(id);
+ var item = ftp.item(server_id); // but if server just changed id (this is answer for init)
+ if(!item && ftp.active && ftp.current_id){
+ //console.log("Mapping ID, Current: ", ftp.current_id, " Server: ", server_id);
+ item = ftp.item(ftp.current_id);
+ if(item){
+ //console.log("Server assigned ID: ", server_id, " to local: ", item.id);
+ item.id = server_id; // but item.uid not changes :)
+ }
+ }
+
+ if(!item){ console.error("Item not found for file_id: ", server_id); return;}
+ item.offset = offset;
+
+ if(status === "send"){
+ item.status = "send";
+ item.offset = offset; // 0 for 1st
+
+ var now = Date.now();
+ if(!ftp.last_save_time || (now - ftp.last_save_time > 2000)){
+ saveState();
+ ftp.last_save_time = now;
+ }
+
+ if(item.ui_update){ try{ item.ui_update(item); }catch(e){ console.error("UI Error ui_update:", e); } }
+
+ if(item.offset < item.total){
+ try{ ftp.read_slice(item); }catch(e){ console.error("Error in read_slice:", e); }
+ }else{
+ item.status = 'done';
+ saveState();
+
+ if(item.ui_update){ item.ui_update(item, "Done!", true); } // console.log("File complete!");
+ //ftp.queue = ftp.queue.filter(i => i.id !== item.id);
+ item.autostart = true;
+ ftp.active = false;
+ ftp.start(); // next file
+ }
+
+ }else if(status === "error"){
+ console.error("Server returned ERROR status");
+ if(item.ui_update){ item.ui_update(item, "Error: Rejected by server"); }
+ item.autostart = false;
+ }
+ }
+ }catch(e){ console.error(e); }
+ }
+ };
+}
+
+function ws_send(data){ // use ws_send(enc(msg)) instead ws.send(enc(msg)) for got status - was send or not
+ if(ws && ws.readyState === WebSocket.OPEN){
+ ws.send(data);
+ return true;
+ }else{
+ console.warn("WebSocket not open, cannot send"); // todo queue messages or retry later
+ return false;
+ }
+}
+
+
+// UTF-8 Support
+function utf8_dec(ab){ return (new TextDecoder()).decode(ab); }
+function utf8_enc(ab){ return (new TextEncoder("utf-8")).encode(ab); }
+function utf8_arr(ab){
+ if(!(ab instanceof ArrayBuffer)) ab = new Uint8Array(utf8_enc(ab)).buffer;
+ return utf8_dec(ab);
+}
+
+
+// IEEE754 (Floats)
+function read_Float(buffer, offset, isLE, mLen, nBytes) {
+ var e, m
+ var eLen = (nBytes * 8) - mLen - 1
+ var eMax = (1 << eLen) - 1
+ var eBias = eMax >> 1
+ var nBits = -7
+ var i = isLE ? (nBytes - 1) : 0
+ var d = isLE ? -1 : 1
+ var s = buffer[offset + i]
+ i += d
+ e = s & ((1 << (-nBits)) - 1)
+ s >>= (-nBits)
+ nBits += eLen
+ for (; nBits > 0; e = (e * 256) + buffer[offset + i], i += d, nBits -= 8) {}
+ m = e & ((1 << (-nBits)) - 1)
+ e >>= (-nBits)
+ nBits += mLen
+ for (; nBits > 0; m = (m * 256) + buffer[offset + i], i += d, nBits -= 8) {}
+ if (e === 0) {
+ e = 1 - eBias
+ } else if (e === eMax) {
+ return m ? NaN : ((s ? -1 : 1) * Infinity)
+ } else {
+ m = m + Math.pow(2, mLen)
+ e = e - eBias
+ }
+ return (s ? -1 : 1) * m * Math.pow(2, e - mLen)
+}
+
+function write_Float(buffer, value, offset, isLE, mLen, nBytes) {
+ var e, m, c
+ var eLen = (nBytes * 8) - mLen - 1
+ var eMax = (1 << eLen) - 1
+ var eBias = eMax >> 1
+ var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0)
+ var i = isLE ? 0 : (nBytes - 1)
+ var d = isLE ? 1 : -1
+ var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0
+ value = Math.abs(value)
+ if (isNaN(value) || value === Infinity) {
+ m = isNaN(value) ? 1 : 0
+ e = eMax
+ } else {
+ e = Math.floor(Math.log(value) / Math.LN2)
+ if (value * (c = Math.pow(2, -e)) < 1) {
+ e--
+ c *= 2
+ }
+ if (e + eBias >= 1) {
+ value += rt / c
+ } else {
+ value += rt * Math.pow(2, 1 - eBias)
+ }
+ if (value * c >= 2) {
+ e++
+ c /= 2
+ }
+ if (e + eBias >= eMax) {
+ m = 0
+ e = eMax
+ } else if (e + eBias >= 1) {
+ m = ((value * c) - 1) * Math.pow(2, mLen)
+ e = e + eBias
+ } else {
+ m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen)
+ e = 0
+ }
+ }
+ for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {}
+ e = (e << mLen) | m
+ eLen += mLen
+ for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {}
+ buffer[offset + i - d] |= s * 128
+}
+
diff --git a/examples_ws/static/form.js b/examples_ws/static/form.js
new file mode 100644
index 0000000..370995d
--- /dev/null
+++ b/examples_ws/static/form.js
@@ -0,0 +1,68 @@
+
+
+// ui helpers
+function ui_add(item){
+ var el = document.createElement("div");
+ el.id = "ftp-" + item.uid;
+ el.innerHTML = `
+
${item.name}
+
Waiting...
+
+
+
+
`;
+ document.getElementById(item.status_block_id).appendChild(el);
+}
+
+function ui_update(item, txt, is_done){
+ var pg = document.getElementById("ftp-pg-" + item.uid);
+ var st = document.getElementById("ftp-st-" + item.uid);
+ if(pg) pg.value = item.offset;
+ if(st){
+ var pct = Math.round((item.offset / item.total) * 100);
+ st.innerHTML = txt || (pct + "% (" + item.offset + " / " + item.total + " bytes)");
+ }
+ if(is_done){
+ var ps = document.getElementById("ftp-ps-" + item.uid);
+ if(ps) ps.remove();
+ var rs = document.getElementById("ftp-rs-" + item.uid);
+ if(rs) rs.remove();
+ }
+}
+
+
+// work with form files
+function selectFiles(input){
+ ftp.autostart = true;
+ for(var i = 0; i < input.files.length; i++){
+ //ftp.init(input.files[i], ui_add, ui_update);
+ var file = input.files[i];
+
+ var existing = ftp.queue.find(item =>
+ item.status === 'missing_file' &&
+ item.name === file.name &&
+ item.total === file.size
+ );
+
+ if(existing){ // resume
+ console.log("Restoring file: ", file.name);
+ existing.file = file;
+ existing.status = 'init';
+ existing.autostart = true;
+
+ saveState();
+ ftp.start(existing.id);
+ }else{ // new file
+ ftp.init(file, ui_add, ui_update);
+ }
+ }
+ input.value = ''; // clear
+}
+
+
+// lets connect ws
+window.addEventListener("load", function(){
+ connect();
+ restoreState();
+}, false);
+
diff --git a/src/websocket/pubsub.zig b/src/websocket/pubsub.zig
index ad4ca85..a3bf10f 100644
--- a/src/websocket/pubsub.zig
+++ b/src/websocket/pubsub.zig
@@ -1,5 +1,5 @@
-// Pub/Sub for WS
+// Pub/Sub, WsSession for WS
const std = @import("std");
const zzz = @import("../lib.zig"); // "zzz" // for zzz.Context
@@ -10,27 +10,33 @@ const SecureSocket = zzz.secsock.SecureSocket;
pub const UserWsHandler = struct {
on_connect: ?*const fn (session: *WsSession) anyerror!void = null,
on_message: ?*const fn (session: *WsSession, data: []const u8) anyerror!void = null,
+ on_binary: ?*const fn (session: *WsSession, data: []const u8) anyerror!void = null,
on_close: ?*const fn (session: *WsSession) void = null,
- on_disconnect: ?*const fn (session: *WsSession) void = null,
+ on_disconnect: ?*const fn (session: *WsSession) void = null, // optional
};
+const QueuedMessage = struct {
+ data: []const u8,
+ is_binary: bool,
+};
-pub const WsSession = struct { // for safe multithread use Pub/Sub
+pub const WsSession = struct { // for safe multithread use Pub/Sub, for save user's state (context)
conn: Conn,
socket_owned: SecureSocket, // owned copy of socket structure (avoid use-after-free error)
- outbox: std.ArrayList([]const u8), // messages queue that need to send to client with conn
+ outbox: std.ArrayList(QueuedMessage), // messages queue that need to send to client with conn
mutex: std.Thread.Mutex,
writer_task_id: usize = 0, // task id for send in native thread
active: bool = true, // flag for to stop writer_task on disconnect
allocator: std.mem.Allocator,
writer_done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), // for avoid race condition
handler: UserWsHandler, // for access callbacks inside session
+ context: ?*anyopaque = null, // save user's state
pub fn init(allocator: std.mem.Allocator, conn: Conn, handler: UserWsHandler) WsSession {
return .{
.conn = conn,
.socket_owned = conn.socket.*,
- .outbox = std.ArrayList([]const u8).init(allocator),
+ .outbox = std.ArrayList(QueuedMessage).init(allocator),
.mutex = .{},
.allocator = allocator,
.handler = handler,
@@ -40,13 +46,21 @@ pub const WsSession = struct { // for safe multithread use Pub/Sub
pub fn deinit(self: *WsSession) void {
self.mutex.lock();
defer self.mutex.unlock();
- for (self.outbox.items) |msg| {
- self.allocator.free(msg);
+ for (self.outbox.items) |item| {
+ self.allocator.free(item.data);
}
self.outbox.deinit();
}
- pub fn scheduleSend(self: *WsSession, data: []const u8) !void { // function for safe call from any thread
+ pub fn scheduleSend(self: *WsSession, data: []const u8) !void { // function for safe call from any thread // send text
+ return self.scheduleSendInternal(data, false);
+ }
+
+ pub fn scheduleSendBinary(self: *WsSession, data: []const u8) !void { // send binary
+ return self.scheduleSendInternal(data, true);
+ }
+
+ fn scheduleSendInternal(self: *WsSession, data: []const u8, is_binary: bool) !void {
self.mutex.lock();
if (!self.active) {
self.mutex.unlock();
@@ -54,7 +68,7 @@ pub const WsSession = struct { // for safe multithread use Pub/Sub
}
const msg_copy = try self.allocator.dupe(u8, data); // copy msg to heap
- try self.outbox.append(msg_copy);
+ try self.outbox.append(.{ .data = msg_copy, .is_binary = is_binary });
self.mutex.unlock();
try self.conn.runtime.trigger(self.writer_task_id); // conn.runtime.trigger is thread-safe // this is task-writer in target thread
@@ -182,13 +196,23 @@ fn writer_task(session: *WsSession) !void { // send message in same thread with
session.mutex.unlock();
if (batch.len > 0) {
- for (batch) |msg| {
- session.conn.send(msg) catch |e| {
- std.log.debug("WS Writer Error: {s}", .{ @errorName(e) });
- };
- session.allocator.free(msg);
+ for (batch) |item| {
+ if (item.is_binary) {
+ session.conn.sendBinary(item.data) catch |e| {
+ std.log.debug("WS Binary Write Error: {s}", .{ @errorName(e) });
+ };
+
+ }else{
+ session.conn.send(item.data) catch |e| {
+ std.log.debug("WS Text Write Error: {s}", .{ @errorName(e) });
+ };
+ }
+
+ session.allocator.free(item.data);
}
session.allocator.free(batch);
+
+ continue;
}
try session.conn.runtime.scheduler.trigger_await();
}
@@ -230,6 +254,13 @@ pub fn handle_upgrade(ctx: *const zzz.Context, user_handler: UserWsHandler, stac
}
}.wrapper,
+ .on_binary = struct {
+ fn wrapper(c: Conn, d: []const u8) !void {
+ const s: *WsSession = @ptrCast(@alignCast(c.user_data));
+ if (s.handler.on_binary) |f| try f(s, d);
+ }
+ }.wrapper,
+
.on_close = struct {
fn wrapper(c: Conn, code: u16, reason: []const u8) !void {
_ = code;
diff --git a/src/websocket/websocket.zig b/src/websocket/websocket.zig
index 6f926cd..2b559e6 100644
--- a/src/websocket/websocket.zig
+++ b/src/websocket/websocket.zig
@@ -116,181 +116,231 @@ pub fn upgrade(
}
pub fn runLoop(conn: Conn, handler: Handler, allocator: Allocator) !void {
- const buffer = try allocator.alloc(u8, 65536);
- defer allocator.free(buffer);
+ //const buffer = try allocator.alloc(u8, 65536);
+ //defer allocator.free(buffer);
- var fragment = std.ArrayList(u8).init(allocator);
+ const read_buffer_size = 4096; // buffer for read from socket, 4 kb for one read syscall
+ const read_buffer = try allocator.alloc(u8, read_buffer_size);
+ defer allocator.free(read_buffer);
+
+ var stash = std.ArrayList(u8).init(allocator); // buffer for incompleted frames between recv calls
+ defer stash.deinit();
+
+ var fragment = std.ArrayList(u8).init(allocator); // for fragmented ws msg - Opcode 0x0
defer fragment.deinit();
+ var is_text: ?bool = null;
+
while (true) {
- const n = conn.socket.recv(conn.runtime, buffer) catch |err| {
+ const n = conn.socket.recv(conn.runtime, read_buffer) catch |err| {
if (err == error.Closed) {
if (handler.on_disconnect) |on_disc| try on_disc(conn);
return;
}
return err;
};
-
- var i: usize = 0;
- while (i < n) {
- const byte1 = buffer[i];
- i += 1;
- if (i >= n) return error.InvalidFrame;
- const byte2 = buffer[i];
- i += 1;
-
- const fin = (byte1 & 0x80) != 0;
- const rsv1 = (byte1 & 0x40) != 0;
- const opcode = byte1 & 0x0F;
- const has_mask = (byte2 & 0x80) != 0;
- var payload_len = @as(usize, byte2 & 0x7F);
-
- if (payload_len == 126) {
- if (i + 2 > n) return error.InvalidFrame;
- //payload_len = @as(usize, @bitCast(std.mem.readIntBig(u16, buffer[i..])));
- //payload_len = @as(usize, @bitCast(std.mem.readInt(u16, buffer[i..], .big)));
- payload_len = @as(usize, @intCast(buffer[i])) << 8 |
- @as(usize, @intCast(buffer[i + 1]));
- i += 2;
- } else if (payload_len == 127) {
- if (i + 8 > n) return error.InvalidFrame;
- //payload_len = @as(usize, @bitCast(std.mem.readIntBig(u64, buffer[i..])));
- //payload_len = @as(usize, @bitCast(std.mem.readInt(u64, buffer[i..], .big)));
- payload_len =
- @as(usize, @intCast(buffer[i + 0])) << 56 |
- @as(usize, @intCast(buffer[i + 1])) << 48 |
- @as(usize, @intCast(buffer[i + 2])) << 40 |
- @as(usize, @intCast(buffer[i + 3])) << 32 |
- @as(usize, @intCast(buffer[i + 4])) << 24 |
- @as(usize, @intCast(buffer[i + 5])) << 16 |
- @as(usize, @intCast(buffer[i + 6])) << 8 |
- @as(usize, @intCast(buffer[i + 7]));
- i += 8;
- }
-
- var mask: [4]u8 = undefined;
- if (has_mask) {
- if (i + 4 > n) return error.InvalidFrame;
- mask = buffer[i..][0..4].*;
- i += 4;
- }
- if (i + payload_len > n) return error.InvalidFrame;
-
- const payload_start = i;
- const payload_end = i + payload_len;
- i = payload_end;
-
- if (has_mask) {
- for (payload_start..payload_end) |j| {
- buffer[j] ^= mask[(j - payload_start) % 4];
- }
+
+ if (n == 0) { // if read 0 bytes - but no error.Closed- this can be disconnect too
+ if (handler.on_disconnect) |on_disc| try on_disc(conn);
+ return;
+ }
+
+ try stash.appendSlice(read_buffer[0..n]); // save readed
+
+ var process_offset: usize = 0;
+ const data = stash.items;
+
+
+ while (true) { // do process collected
+ if (process_offset + 2 > data.len) break; // must be at least 2 bytes - minimal header
+
+ const start_idx = process_offset;
+ const byte1 = data[start_idx];
+ const byte2 = data[start_idx + 1];
+
+ const fin = (byte1 & 0x80) != 0;
+ const rsv1 = (byte1 & 0x40) != 0;
+ const opcode = byte1 & 0x0F;
+ const has_mask = (byte2 & 0x80) != 0;
+ var payload_len = @as(usize, byte2 & 0x7F);
+
+ var header_len: usize = 2;
+
+ if (payload_len == 126) { // check header length
+ if (start_idx + 4 > data.len) break; // lets wait for more data
+ //payload_len = std.mem.readInt(u16, data[start_idx+2..][0..2], .big);
+ payload_len = @as(usize, data[start_idx+2]) << 8 | @as(usize, data[start_idx+3]);
+ header_len += 2;
+ } else if (payload_len == 127) {
+ if (start_idx + 10 > data.len) break; // lets wait for more data
+ //payload_len = std.mem.readInt(u64, data[start_idx+2..][0..8], .big);
+ payload_len =
+ @as(usize, data[start_idx+2]) << 56 |
+ @as(usize, data[start_idx+3]) << 48 |
+ @as(usize, data[start_idx+4]) << 40 |
+ @as(usize, data[start_idx+5]) << 32 |
+ @as(usize, data[start_idx+6]) << 24 |
+ @as(usize, data[start_idx+7]) << 16 |
+ @as(usize, data[start_idx+8]) << 8 |
+ @as(usize, data[start_idx+9]);
+ header_len += 8;
+ }
+
+ var mask: [4]u8 = undefined;
+ if (has_mask) {
+ if (start_idx + header_len + 4 > data.len) break; // lets wait for mask
+ @memcpy(&mask, data[start_idx + header_len..][0..4]);
+ header_len += 4;
+ }
+
+ const total_frame_len = header_len + payload_len; // check we have complete message' body
+ if (start_idx + total_frame_len > data.len) break; // lets wait for rest message' body
+ // here we got complete frame
+
+ const payload_start = start_idx + header_len;
+ const payload = data[payload_start .. payload_start + payload_len];
+
+ if (has_mask) { // unmask
+ var j: usize = 0;
+ while (j < payload_len) : (j += 1) {
+ payload[j] ^= mask[j % 4];
}
-
- var payload = buffer[payload_start..payload_end];
+ }
+
+ // Compression (permessage-deflate) // todo
+ if (rsv1) return error.CompressionNotNegotiated;
+ //var decompressed: ?[]u8 = null;
+ //if (rsv1) {
+ // if (!conn.compression) return error.CompressionNotNegotiated;
+ // const inflated_len = payload.len + 4;
+ // const inflated = try allocator.alloc(u8, inflated_len);
+ // errdefer allocator.free(inflated);
+ // @memcpy(inflated[0..payload.len], payload);
+ // inflated[payload.len + 0] = 0x00;
+ // inflated[payload.len + 1] = 0x00;
+ // inflated[payload.len + 2] = 0xFF;
+ // inflated[payload.len + 3] = 0xFF;
+ // const stream = std.io.fixedBufferStream(inflated);
+ // var inflater = compress.flate.InflateStream.init(stream.reader(), .{});
+ // defer inflater.deinit();
+ // var out = std.ArrayList(u8).init(allocator);
+ // errdefer out.deinit();
+ // try inflater.reader().readAllArrayList(&out, 1024 * 1024);
+ // allocator.free(inflated);
+ // decompressed = try out.toOwnedSlice();
+ // payload = decompressed.?;
+ //}
+
+ switch (opcode) {
+ 0x1 => { // Text
+ if (is_text != null) return error.InvalidWebSocketFrame;
+ is_text = true;
+
+ if (fin) {
+ if (handler.on_message) |on_msg| try on_msg(conn, payload);
+ is_text = null; // because got complete message
+ //if (decompressed) |d| allocator.free(d);
+ } else {
+ try fragment.appendSlice(payload);
+ //if (decompressed) |d| allocator.free(d);
+ }
+ },
- // Compression (permessage-deflate) // todo fix
- if (rsv1) return error.CompressionNotNegotiated;
- //var decompressed: ?[]u8 = null;
- //if (rsv1) {
- // if (!conn.compression) return error.CompressionNotNegotiated;
- // const inflated_len = payload.len + 4;
- // const inflated = try allocator.alloc(u8, inflated_len);
- // errdefer allocator.free(inflated);
- // @memcpy(inflated[0..payload.len], payload);
- // inflated[payload.len + 0] = 0x00;
- // inflated[payload.len + 1] = 0x00;
- // inflated[payload.len + 2] = 0xFF;
- // inflated[payload.len + 3] = 0xFF;
- // const stream = std.io.fixedBufferStream(inflated);
- // var inflater = compress.flate.InflateStream.init(stream.reader(), .{});
- // defer inflater.deinit();
- // var out = std.ArrayList(u8).init(allocator);
- // errdefer out.deinit();
- // try inflater.reader().readAllArrayList(&out, 1024 * 1024);
- // allocator.free(inflated);
- // decompressed = try out.toOwnedSlice();
- // payload = decompressed.?;
- //}
+ 0x2 => { // Binary
+ if (is_text != null) return error.InvalidWebSocketFrame;
+ is_text = false;
+
+ if (fin) {
+ if (handler.on_binary) |on_bin| try on_bin(conn, payload);
+ is_text = null;
+ //if (decompressed) |d| allocator.free(d);
+ } else {
+ try fragment.appendSlice(payload);
+ //if (decompressed) |d| allocator.free(d);
+ }
+ },
- var is_text: ?bool = null;
- switch (opcode) {
- 0x1 => { // Text
- if (is_text == null) is_text = true;
-
- if (fin) {
- if (handler.on_message) |on_msg| try on_msg(conn, payload);
- //if (decompressed) |d| allocator.free(d);
- } else {
- try fragment.appendSlice(payload);
- //if (decompressed) |d| allocator.free(d);
- }
- },
-
- 0x2 => { // Binary
- if (is_text == null) is_text = false;
-
- if (fin) {
- if (handler.on_binary) |on_bin| try on_bin(conn, payload);
- //if (decompressed) |d| allocator.free(d);
- } else {
- try fragment.appendSlice(payload);
- //if (decompressed) |d| allocator.free(d);
- }
- },
+ 0x0 => { // Continuation
+ if (is_text == null) return error.InvalidWebSocketFrame; // we got message' second part but we do not know first part - was no start frame
+
+ try fragment.appendSlice(payload);
+
+ if (fin) {
+ const full = fragment.items;
+ if (is_text.?) { // not null here
+ if (handler.on_message) |f| try f(conn, full);
+ } else {
+ if (handler.on_binary) |f| try f(conn, full);
+ }
+ is_text = null;
+ fragment.clearRetainingCapacity();
+ }
+ //if (decompressed) |d| allocator.free(d);
+ },
+
+ 0x8 => { // Close
+ var code: u16 = 1000; // Normal closure code by default
+ var reason_slice: []const u8 = "";
+
+ if (payload.len >= 2) {
+ code = std.mem.readInt(u16, payload[0..2], .big);
- 0x0 => { // Continuation
- try fragment.appendSlice(payload);
- if (fin) {
- const full = fragment.items;
- if (is_text orelse true) {
- if (handler.on_message) |f| try f(conn, full);
- } else {
- if (handler.on_binary) |f| try f(conn, full);
- }
- is_text = null;
- fragment.clearRetainingCapacity();
- }
- //if (decompressed) |d| allocator.free(d);
- },
- 0x8 => { // Close
- var code: u16 = 1000; // Normal closure code by default
- var reason_slice: []const u8 = "";
- if (payload.len >= 2) {
- code = std.mem.readInt(u16, payload[0..2], .big);
-
- if (payload.len > 2) {
- reason_slice = payload[2..];
- if (!std.unicode.utf8ValidateSlice(reason_slice)) {
- //conn.socket.socket.close_blocking(); // that makes tardy
- //if (decompressed) |d| allocator.free(d);
- if (handler.on_disconnect) |on_disc| try on_disc(conn);
- return;
- }
- }
- }
- conn.close(code, "") catch {}; // just exit
- if (handler.on_close) |on_close| try on_close(conn, code, reason_slice);
- if (handler.on_disconnect) |on_disc| try on_disc(conn);
- //if (decompressed) |d| allocator.free(d);
- return;
- },
- 0x9 => { // Ping
- var buf = std.ArrayList(u8).init(allocator);
- defer buf.deinit();
- try writeFrameHeader(buf.writer(), .pong, payload.len, false);
- try buf.appendSlice(payload);
- _ = try conn.socket.send_all(conn.runtime, buf.items);
- //if (decompressed) |d| allocator.free(d);
- },
- 0xA => { // Pong - ignore
- //if (decompressed) |d| allocator.free(d);
- },
- else => {
- //if (decompressed) |d| allocator.free(d);
- return error.UnsupportedOpcode;
- },
- }
+ if (payload.len > 2) reason_slice = payload[2..];
+ }
+ // if (payload.len > 2) {
+ //reason_slice = payload[2..];
+ //if (!std.unicode.utf8ValidateSlice(reason_slice)) {
+ ////conn.socket.socket.close_blocking(); // that makes tardy
+ ////if (decompressed) |d| allocator.free(d);
+ //if (handler.on_disconnect) |on_disc| try on_disc(conn);
+ //return;
+ //}
+ //}
+
+ conn.close(code, "") catch {}; // just exit
+
+ if (handler.on_close) |on_close| try on_close(conn, code, reason_slice);
+ if (handler.on_disconnect) |on_disc| try on_disc(conn);
+ //if (decompressed) |d| allocator.free(d);
+ return;
+ },
+
+ 0x9 => { // Ping
+ const pong_payload = payload;
+
+ var pong_buf = std.ArrayList(u8).init(allocator);
+ defer pong_buf.deinit();
+
+ try writeFrameHeader(pong_buf.writer(), .pong, pong_payload.len, false);
+ try pong_buf.appendSlice(pong_payload);
+
+ _ = try conn.socket.send_all(conn.runtime, pong_buf.items);
+ //if (decompressed) |d| allocator.free(d);
+ },
+
+ 0xA => { // Pong - ignore
+ //if (decompressed) |d| allocator.free(d);
+ },
+
+ else => {
+ //if (decompressed) |d| allocator.free(d);
+ return error.UnsupportedOpcode;
+ },
+ }
+
+ process_offset += total_frame_len; // shift offset
+ } // while ends - processed collected
+
+ if (process_offset > 0) { // clean frame buffer // maybe todo RingBuffer
+ const remaining = data.len - process_offset;
+ if (remaining == 0) {
+ stash.clearRetainingCapacity();
+ } else {
+ std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]); // mv tail to begin
+ stash.shrinkRetainingCapacity(remaining);
+ }
}
+
}
}
From 667bc20677c5cf25622da2b25cb07c9533f97beb Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Sun, 28 Dec 2025 06:20:22 +0200
Subject: [PATCH 03/26] use dynamic heap buffer
---
examples_ws/example_ws_1.zig | 4 +-
examples_ws/example_ws_2.zig | 4 +-
examples_ws/example_ws_3.zig | 2 +-
examples_ws/example_ws_4.zig | 4 +-
src/http/websocket.zig | 212 ++++++++++++++++++++---------------
5 files changed, 127 insertions(+), 99 deletions(-)
diff --git a/examples_ws/example_ws_1.zig b/examples_ws/example_ws_1.zig
index d6c46dc..038f781 100644
--- a/examples_ws/example_ws_1.zig
+++ b/examples_ws/example_ws_1.zig
@@ -14,8 +14,8 @@ const Socket = zzz.tardy.Socket;
const PORT = 3010;
const HOST = "0.0.0.0";
-//const STACK_SIZE = 10 * 1024 * 1024; // DEBUG
-const STACK_SIZE = 64 * 1024; // RELEASE
+//const STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1mb
+const STACK_SIZE = 16 * 1024; // RELEASE = 16kb
// WebSocket handlers
diff --git a/examples_ws/example_ws_2.zig b/examples_ws/example_ws_2.zig
index f082b05..a9fe905 100644
--- a/examples_ws/example_ws_2.zig
+++ b/examples_ws/example_ws_2.zig
@@ -15,8 +15,8 @@ const Socket = zzz.tardy.Socket;
const PORT = 443;
const HOST = "0.0.0.0";
-//const STACK_SIZE = 10 * 1024 * 1024; // DEBUG
-const STACK_SIZE = 64 * 1024; // RELEASE
+//const STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1mb
+const STACK_SIZE = 32 * 1024; // RELEASE = 32kb
const FULLCHAIN_CERT = "examples_ws/cert/fullchain.pem";
const PRIVKEY_CERT = "examples_ws/cert/privkey.pem";
diff --git a/examples_ws/example_ws_3.zig b/examples_ws/example_ws_3.zig
index 5437843..c683c9c 100644
--- a/examples_ws/example_ws_3.zig
+++ b/examples_ws/example_ws_3.zig
@@ -19,7 +19,7 @@ const PORT = 3010;
//const PORT = 443;
const HOST = "0.0.0.0";
-//const STACK_SIZE = 10 * 1024 * 1024; // DEBUG
+//const STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1 mb
const STACK_SIZE = 16 * 1024; // RELEASE // reader stack // 8-16 kb without ssl usage, 32 kb when use ssl
const WS_WRITER_STACK = 8 * 1024; // stack size for writer task // 8 kb without ssl usage, 32 kb when use ssl
diff --git a/examples_ws/example_ws_4.zig b/examples_ws/example_ws_4.zig
index 72dc732..d619587 100644
--- a/examples_ws/example_ws_4.zig
+++ b/examples_ws/example_ws_4.zig
@@ -17,8 +17,8 @@ const Bert_Value = @import("bert.zig").Bert_Value;
const PORT = 3010;
const HOST = "0.0.0.0";
-//const WS_STACK_SIZE = 1024 * 1024; // DEBUG
-const WS_STACK_SIZE = 32 * 1024; // RELEASE
+//const WS_STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1mb
+const WS_STACK_SIZE = 16 * 1024; // RELEASE = 16kb
const ExtensionsLimits = struct {
diff --git a/src/http/websocket.zig b/src/http/websocket.zig
index 40210ae..ac04e64 100644
--- a/src/http/websocket.zig
+++ b/src/http/websocket.zig
@@ -81,107 +81,135 @@ pub fn upgrade_to_websocket(
// inner loop
fn message_loop(conn: Conn, handler: WebSocketHandler) !void {
- var buffer: [65536]u8 = undefined;
+ const allocator = conn.runtime.allocator;
+ ////var buffer: [65536]u8 = undefined; //const buffer = try allocator.alloc(u8, 65536); //defer allocator.free(buffer);
+
+ const read_buffer_size = 4096; // 4kb = RAM page, so read by 4kb
+ const read_buffer = try allocator.alloc(u8, read_buffer_size);
+ defer allocator.free(read_buffer);
+
+ var stash = std.ArrayList(u8).init(allocator);
+ defer stash.deinit();
+
+ var fragment = std.ArrayList(u8).init(allocator);
+ defer fragment.deinit();
+
while (true) {
- const n = conn.socket.recv(conn.runtime, &buffer) catch |err| {
+ const n = conn.socket.recv(conn.runtime, read_buffer) catch |err| {
if (err == error.Closed) {
- if (handler.on_disconnect) |on_disconnect| {
- _ = on_disconnect(conn);
- }
+ if (handler.on_disconnect) |on_disconnect| { try on_disconnect(conn); } //_ = on_disconnect(conn);
return;
}
return err;
};
-
+
+ if (n == 0) {
+ if (handler.on_disconnect) |on_disconnect| { try on_disconnect(conn); }
+ return;
+ }
+
+ try stash.appendSlice(read_buffer[0..n]);
+
+ var process_offset: usize = 0;
+ const data = stash.items;
+
// RFC 6455
- var i: usize = 0;
- while (i < n) {
- const op = buffer[i];
- const fin = (op & 0x80) != 0;
- const opcode = op & 0x0F;
- i += 1;
-
- if (i >= n) return error.InvalidWebSocketFrame;
-
- const mask_flag = (buffer[i] & 0x80) != 0;
- const payload_len_raw = buffer[i] & 0x7F;
- i += 1;
-
- if (i >= n) return error.InvalidWebSocketFrame;
-
- var payload_len: usize = payload_len_raw;
- var extra: usize = 0;
- if (payload_len_raw == 126) {
- if (i + 2 > n) return error.InvalidWebSocketFrame;
- payload_len = @as(usize, @bitCast(std.mem.readIntBig(u16, buffer[i..])));
- extra = 2;
- } else if (payload_len_raw == 127) {
- if (i + 8 > n) return error.InvalidWebSocketFrame;
- payload_len = @as(usize, @bitCast(std.mem.readIntBig(u64, buffer[i..])));
- extra = 8;
- }
-
- i += extra;
- if (mask_flag) {
- if (i + 4 > n) return error.InvalidWebSocketFrame;
- const mask = buffer[i..][0..4].*;
- i += 4;
- if (i + payload_len > n) return error.InvalidWebSocketFrame;
-
- //for (0..payload_len) |j| {
- // buffer[i + j] ^= mask[j % 4];
- //} // use next vector optimising instead this
-
- var j: usize = 0;
- const vec_len = std.simd.suggestVectorLength(u8) orelse 16;
- const Vector = @Vector(vec_len, u8);
-
- var mask_arr: [vec_len]u8 = undefined;
- for (0..vec_len) |k| mask_arr[k] = mask[k % 4];
- const mask_vec: Vector = mask_arr;
-
- while (j + vec_len <= payload_len) {
- const chunk: Vector = buffer[i+j..][0..vec_len].*;
- const res = chunk ^ mask_vec;
- buffer[i+j..][0..vec_len].* = res;
- j += vec_len;
- }
-
- while (j < payload_len) : (j += 1) {
- buffer[i + j] ^= mask[j % 4];
- }
-
-
+ while (true) {
+ if (process_offset + 2 > data.len) break; // minimal header = 2 bytes
+
+ const start_idx = process_offset;
+ const byte1 = data[start_idx];
+ const byte2 = data[start_idx + 1];
+
+ const fin = (byte1 & 0x80) != 0;
+ const rsv1 = (byte1 & 0x40) != 0;
+ const opcode = byte1 & 0x0F;
+ const has_mask = (byte2 & 0x80) != 0;
+ var payload_len = @as(usize, byte2 & 0x7F);
+
+ var header_len: usize = 2;
+
+ if (payload_len == 126) {
+ if (start_idx + 4 > data.len) break; // waiting more bytes
+ payload_len = @as(usize, data[start_idx + 2]) << 8 | @as(usize, data[start_idx + 3]);
+ header_len += 2;
+
+ } else if (payload_len == 127) {
+ if (start_idx + 10 > data.len) break; // waiting more bytes
+
+ payload_len =
+ @as(usize, data[start_idx+2]) << 56 |
+ @as(usize, data[start_idx+3]) << 48 |
+ @as(usize, data[start_idx+4]) << 40 |
+ @as(usize, data[start_idx+5]) << 32 |
+ @as(usize, data[start_idx+6]) << 24 |
+ @as(usize, data[start_idx+7]) << 16 |
+ @as(usize, data[start_idx+8]) << 8 |
+ @as(usize, data[start_idx+9]);
+ header_len += 8;
+ }
+
+ var mask: [4]u8 = undefined;
+ if (has_mask) {
+ if (start_idx + header_len + 4 > data.len) break;
+ @memcpy(&mask, data[start_idx + header_len..][0..4]);
+ header_len += 4;
+ }
+
+ const total_frame_len = header_len + payload_len;
+ if (start_idx + total_frame_len > data.len) break; // is complete?
+
+ const payload_start = start_idx + header_len; // must be complete
+ const payload = data[payload_start .. payload_start + payload_len];
+
+ if (has_mask) {
+ var j: usize = 0;
+ while (j < payload_len) : (j += 1) {
+ payload[j] ^= mask[j % 4];
}
-
- if (i + payload_len > n) return error.InvalidWebSocketFrame;
-
- switch (opcode) {
- 0x1 => { // Text
- if (fin and handler.on_message) |on_msg| {
- try on_msg(conn, buffer[i .. i + payload_len]);
- }
- },
- 0x8 => { // Close
- conn.close();
- if (handler.on_disconnect) |on_disconnect| {
- _ = on_disconnect(conn);
- }
- return;
- },
- 0x9 => { // Ping - reply Pong
- const pong = ([2]u8{ 0x8A, @intCast(@min(payload_len, 125)) })[0..];
- _ = try conn.socket.send_all(conn.runtime, pong);
- if (payload_len <= 125) {
- _ = try conn.socket.send_all(conn.runtime, buffer[i .. i + payload_len]);
- }
- },
- else => {
- // ignore other opcode (binary, pong etc)
- },
+ }
+
+ switch (opcode) {
+ 0x1 => { // Text
+ if (fin and handler.on_message) |on_msg| {
+ try on_msg(conn, buffer[i .. i + payload_len]);
+ }
+ },
+ 0x8 => { // Close
+ conn.close();
+ if (handler.on_disconnect) |on_disconnect| {
+ _ = on_disconnect(conn);
+ }
+ return;
+ },
+ 0x9 => { // Ping - reply Pong
+ const pong = ([2]u8{ 0x8A, @intCast(@min(payload_len, 125)) })[0..];
+ _ = try conn.socket.send_all(conn.runtime, pong);
+ if (payload_len <= 125) {
+ _ = try conn.socket.send_all(conn.runtime, buffer[i .. i + payload_len]);
+ }
+ },
+ else => {
+ // ignore other opcode (binary, pong etc)
+ },
+ }
+
+ process_offset += total_frame_len;
+ }
+
+ if (process_offset > 0) { // clean
+ const remaining = data.len - process_offset;
+ if (remaining == 0) { // clean buffer
+ stash.clearRetainingCapacity();
+
+ if (stash.capacity > 1024 * 1024) { // free RAM when buffer size more than 1mb
+ stash.shrinkAndFree(0);
}
-
- i += payload_len;
+
+ } else {
+ std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]);
+ stash.shrinkRetainingCapacity(remaining);
+ }
}
}
}
From a7e29050e6dce696f228e72dfb0425481ce38359 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Sun, 28 Dec 2025 06:46:27 +0200
Subject: [PATCH 04/26] define stack size with builtin mode
---
examples_ws/example_ws_1.zig | 8 ++++++--
examples_ws/example_ws_2.zig | 9 +++++++--
examples_ws/example_ws_3.zig | 9 +++++++--
examples_ws/example_ws_4.zig | 7 +++++--
4 files changed, 25 insertions(+), 8 deletions(-)
diff --git a/examples_ws/example_ws_1.zig b/examples_ws/example_ws_1.zig
index 038f781..e102a8d 100644
--- a/examples_ws/example_ws_1.zig
+++ b/examples_ws/example_ws_1.zig
@@ -14,8 +14,11 @@ const Socket = zzz.tardy.Socket;
const PORT = 3010;
const HOST = "0.0.0.0";
-//const STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1mb
-const STACK_SIZE = 16 * 1024; // RELEASE = 16kb
+
+const STACK_SIZE = if (@import("builtin").mode == .Debug)
+ 1 * 1024 * 1024 // DEBUG = 1mb
+else
+ 16 * 1024; // RELEASE = 16kb
// WebSocket handlers
@@ -152,6 +155,7 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
pub fn main() !void{
+ //@compileLog("STACK_SIZE = ", STACK_SIZE);
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
diff --git a/examples_ws/example_ws_2.zig b/examples_ws/example_ws_2.zig
index a9fe905..f886169 100644
--- a/examples_ws/example_ws_2.zig
+++ b/examples_ws/example_ws_2.zig
@@ -15,8 +15,12 @@ const Socket = zzz.tardy.Socket;
const PORT = 443;
const HOST = "0.0.0.0";
-//const STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1mb
-const STACK_SIZE = 32 * 1024; // RELEASE = 32kb
+
+const STACK_SIZE = if (@import("builtin").mode == .Debug)
+ 1 * 1024 * 1024 // DEBUG = 1mb
+else
+ 32 * 1024; // RELEASE = 32kb
+
const FULLCHAIN_CERT = "examples_ws/cert/fullchain.pem";
const PRIVKEY_CERT = "examples_ws/cert/privkey.pem";
@@ -157,6 +161,7 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
pub fn main() !void{
+ //@compileLog("STACK_SIZE = ", STACK_SIZE);
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
diff --git a/examples_ws/example_ws_3.zig b/examples_ws/example_ws_3.zig
index c683c9c..984589c 100644
--- a/examples_ws/example_ws_3.zig
+++ b/examples_ws/example_ws_3.zig
@@ -19,10 +19,15 @@ const PORT = 3010;
//const PORT = 443;
const HOST = "0.0.0.0";
-//const STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1 mb
-const STACK_SIZE = 16 * 1024; // RELEASE // reader stack // 8-16 kb without ssl usage, 32 kb when use ssl
+
+const STACK_SIZE = if (@import("builtin").mode == .Debug)
+ 1 * 1024 * 1024 // DEBUG = 1mb
+else
+ 16 * 1024; // RELEASE = 16kb // reader stack // 8-16 kb without ssl usage, 32 kb when use ssl
+
const WS_WRITER_STACK = 8 * 1024; // stack size for writer task // 8 kb without ssl usage, 32 kb when use ssl
+
var global_pubsub: PubSub = undefined;
diff --git a/examples_ws/example_ws_4.zig b/examples_ws/example_ws_4.zig
index d619587..10a68d9 100644
--- a/examples_ws/example_ws_4.zig
+++ b/examples_ws/example_ws_4.zig
@@ -17,8 +17,11 @@ const Bert_Value = @import("bert.zig").Bert_Value;
const PORT = 3010;
const HOST = "0.0.0.0";
-//const WS_STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1mb
-const WS_STACK_SIZE = 16 * 1024; // RELEASE = 16kb
+
+const WS_STACK_SIZE = if (@import("builtin").mode == .Debug)
+ 1 * 1024 * 1024 // DEBUG = 1mb
+else
+ 16 * 1024; // RELEASE = 16kb
const ExtensionsLimits = struct {
From bb323c4279886ca65050d454cf7d075a12adc165 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Mon, 1 Jun 2026 19:01:14 +0300
Subject: [PATCH 05/26] optimize ws perf (maybe)
---
src/websocket/websocket.zig | 109 ++++++++++++++++++++++++++++++------
1 file changed, 93 insertions(+), 16 deletions(-)
diff --git a/src/websocket/websocket.zig b/src/websocket/websocket.zig
index 2b559e6..1b7fab7 100644
--- a/src/websocket/websocket.zig
+++ b/src/websocket/websocket.zig
@@ -18,24 +18,53 @@ pub const Conn = struct {
//allocator: Allocator, // maybe todo - and pass with on_upgrade
//compression: bool,
- pub fn send(self: Conn, data: []const u8) !void {
- var buf = std.ArrayList(u8).init(self.runtime.allocator);
- defer buf.deinit();
+ //pub fn send(self: Conn, data: []const u8) !void {
+ // var buf = std.ArrayList(u8).init(self.runtime.allocator);
+ // defer buf.deinit();
//try writeFrameHeader(buf.writer(), .text, data.len, true);
- try writeFrameHeader(buf.writer(), .text, data.len, false);
- try buf.appendSlice(data);
- _ = try self.socket.send_all(self.runtime, buf.items);
+ // try writeFrameHeader(buf.writer(), .text, data.len, false);
+ // try buf.appendSlice(data);
+ // _ = try self.socket.send_all(self.runtime, buf.items);
+ //}
+
+ //pub fn sendBinary(self: Conn, binary_data: []const u8) !void {
+ // var buf = std.ArrayList(u8).init(self.runtime.allocator);
+ // defer buf.deinit();
+
+ // try writeFrameHeader(buf.writer(), .binary, binary_data.len, false);
+ // try buf.appendSlice(binary_data);
+ // _ = try self.socket.send_all(self.runtime, buf.items);
+ //}
+
+ pub fn send(self: Conn, data: []const u8) !void {
+ try self.writeFrame(.text, data);
}
pub fn sendBinary(self: Conn, binary_data: []const u8) !void {
- var buf = std.ArrayList(u8).init(self.runtime.allocator);
- defer buf.deinit();
-
- try writeFrameHeader(buf.writer(), .binary, binary_data.len, false);
- try buf.appendSlice(binary_data);
- _ = try self.socket.send_all(self.runtime, buf.items);
-
+ try self.writeFrame(.binary, binary_data);
+ }
+
+ fn writeFrame(self: Conn, opcode: OpCode, data: []const u8) !void {
+ var header: [10]u8 = undefined;
+ const f_byte: u8 = @intFromEnum(opcode) | 0x80;
+ header[0] = f_byte;
+
+ var h_len: usize = 2;
+ if (data.len < 126) {
+ header[1] = @intCast(data.len);
+ } else if (data.len <= 0xFFFF) {
+ header[1] = 126;
+ std.mem.writeInt(u16, header[2..4], @intCast(data.len), .big);
+ h_len = 4;
+ } else {
+ header[1] = 127;
+ std.mem.writeInt(u64, header[2..10], data.len, .big);
+ h_len = 10;
+ }
+
+ _ = try self.socket.send_all(self.runtime, header[0..h_len]);
+ _ = try self.socket.send_all(self.runtime, data);
}
pub fn close(self: Conn, code: u16, reason: []const u8) !void {
@@ -200,13 +229,45 @@ pub fn runLoop(conn: Conn, handler: Handler, allocator: Allocator) !void {
const payload_start = start_idx + header_len;
const payload = data[payload_start .. payload_start + payload_len];
+ //if (has_mask) { // unmask
+ // var j: usize = 0;
+ // while (j < payload_len) : (j += 1) {
+ // payload[j] ^= mask[j % 4];
+ // }
+ //}
+
if (has_mask) { // unmask
var j: usize = 0;
- while (j < payload_len) : (j += 1) {
+ const is_little = @import("builtin").target.cpu.arch.endian() == .little;
+
+ const m64 = if (is_little) blk: { // 64-bits unmasking for little-or-big-endian - compiler knows in comptime
+ // mask[0] is least-significant byte u32 in little-endian
+ const m32 = @as(u32, mask[0]) |
+ (@as(u32, mask[1]) << 8) |
+ (@as(u32, mask[2]) << 16) |
+ (@as(u32, mask[3]) << 24);
+ break :blk (@as(u64, m32) << 32) | m32;
+ } else blk: {
+ // mask[0] is most-significant byte u32 in big-endian
+ const m32 = (@as(u32, mask[0]) << 24) |
+ (@as(u32, mask[1]) << 16) |
+ (@as(u32, mask[2]) << 8) |
+ @as(u32, mask[3]);
+ break :blk (@as(u64, m32) << 32) | m32;
+ };
+
+ while (j + 8 <= payload_len) : (j += 8) { // process every 8 bytes once
+ const val = std.mem.readInt(u64, payload[j..][0..8], if (is_little) .little else .big);
+ std.mem.writeInt(u64, payload[j..][0..8], val ^ m64, if (is_little) .little else .big);
+ }
+
+ while (j < payload_len) : (j += 1) { // rest (< 8 bytes)
payload[j] ^= mask[j % 4];
}
+
}
+
// Compression (permessage-deflate) // todo
if (rsv1) return error.CompressionNotNegotiated;
//var decompressed: ?[]u8 = null;
@@ -331,15 +392,31 @@ pub fn runLoop(conn: Conn, handler: Handler, allocator: Allocator) !void {
process_offset += total_frame_len; // shift offset
} // while ends - processed collected
+ //if (process_offset > 0) { // clean frame buffer // maybe todo RingBuffer
+ // const remaining = data.len - process_offset;
+ // if (remaining == 0) {
+ // stash.clearRetainingCapacity();
+ // } else {
+ // std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]); // mv tail to begin
+ // stash.shrinkRetainingCapacity(remaining);
+ // }
+ //}
+
+
if (process_offset > 0) { // clean frame buffer // maybe todo RingBuffer
const remaining = data.len - process_offset;
if (remaining == 0) {
stash.clearRetainingCapacity();
- } else {
- std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]); // mv tail to begin
+ //} else if (process_offset > 8192 or remaining < process_offset) { // copy if 8kb(+) or remaing too less than offset
+ // std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]);
+ // stash.shrinkRetainingCapacity(remaining);
+ } else { // maybe todo use more big buffer (at this function beginning) without reallocations
+ //std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]);
+ @memcpy(stash.items[0..remaining], stash.items[process_offset..]);
stash.shrinkRetainingCapacity(remaining);
}
}
+
}
}
From 3dbd7b392907939db1149ed85f4c252003ab229a Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Mon, 1 Jun 2026 20:57:17 +0300
Subject: [PATCH 06/26] upd build.zig -- add target examples_http -- Build all
HTTP examples (same as exists - build WS examples only)
---
build.zig | 22 +++++++++++++---------
1 file changed, 13 insertions(+), 9 deletions(-)
diff --git a/build.zig b/build.zig
index 21b77aa..b193b29 100644
--- a/build.zig
+++ b/build.zig
@@ -25,16 +25,17 @@ pub fn build(b: *std.Build) void {
zzz.addImport("secsock", secsock);
- add_example(b, "basic", false, target, optimize, zzz);
- add_example(b, "cookies", false, target, optimize, zzz);
- add_example(b, "form", false, target, optimize, zzz);
- add_example(b, "fs", false, target, optimize, zzz);
- add_example(b, "middleware", false, target, optimize, zzz);
- add_example(b, "sse", false, target, optimize, zzz);
- add_example(b, "tls", true, target, optimize, zzz);
+ const all_http_examples_step = b.step("examples_http", "Build all HTTP examples");
+ add_http_example(b, all_http_examples_step, "basic", false, target, optimize, zzz);
+ add_http_example(b, all_http_examples_step, "cookies", false, target, optimize, zzz);
+ add_http_example(b, all_http_examples_step, "form", false, target, optimize, zzz);
+ add_http_example(b, all_http_examples_step, "fs", false, target, optimize, zzz);
+ add_http_example(b, all_http_examples_step, "middleware", false, target, optimize, zzz);
+ add_http_example(b, all_http_examples_step, "sse", false, target, optimize, zzz);
+ add_http_example(b, all_http_examples_step, "tls", true, target, optimize, zzz);
if (target.result.os.tag != .windows) {
- add_example(b, "unix", false, target, optimize, zzz);
+ add_http_example(b, all_http_examples_step, "unix", false, target, optimize, zzz);
}
@@ -110,8 +111,9 @@ pub fn build(b: *std.Build) void {
test_step.dependOn(&run_test.step);
}
-fn add_example(
+fn add_http_example(
b: *std.Build,
+ all_http_examples_step: *std.Build.Step,
name: []const u8,
link_libc: bool,
target: std.Build.ResolvedTarget,
@@ -135,6 +137,8 @@ fn add_example(
const install_artifact = b.addInstallArtifact(example, .{});
b.getInstallStep().dependOn(&install_artifact.step);
+ all_http_examples_step.dependOn(&install_artifact.step);
+
const build_step = b.step(b.fmt("{s}", .{name}), b.fmt("Build zzz example ({s})", .{name}));
build_step.dependOn(&install_artifact.step);
From e43666c5d6ffc9de9f3e17652b205727598e5523 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Tue, 2 Jun 2026 14:05:54 +0300
Subject: [PATCH 07/26] clean ws
---
src/http/websocket.zig | 144 ++++-------------------------------------
1 file changed, 13 insertions(+), 131 deletions(-)
diff --git a/src/http/websocket.zig b/src/http/websocket.zig
index ac04e64..4acc49f 100644
--- a/src/http/websocket.zig
+++ b/src/http/websocket.zig
@@ -81,137 +81,19 @@ pub fn upgrade_to_websocket(
// inner loop
fn message_loop(conn: Conn, handler: WebSocketHandler) !void {
- const allocator = conn.runtime.allocator;
- ////var buffer: [65536]u8 = undefined; //const buffer = try allocator.alloc(u8, 65536); //defer allocator.free(buffer);
-
- const read_buffer_size = 4096; // 4kb = RAM page, so read by 4kb
- const read_buffer = try allocator.alloc(u8, read_buffer_size);
- defer allocator.free(read_buffer);
-
- var stash = std.ArrayList(u8).init(allocator);
- defer stash.deinit();
-
- var fragment = std.ArrayList(u8).init(allocator);
- defer fragment.deinit();
-
- while (true) {
- const n = conn.socket.recv(conn.runtime, read_buffer) catch |err| {
- if (err == error.Closed) {
- if (handler.on_disconnect) |on_disconnect| { try on_disconnect(conn); } //_ = on_disconnect(conn);
- return;
- }
- return err;
- };
-
- if (n == 0) {
- if (handler.on_disconnect) |on_disconnect| { try on_disconnect(conn); }
- return;
- }
-
- try stash.appendSlice(read_buffer[0..n]);
-
- var process_offset: usize = 0;
- const data = stash.items;
-
- // RFC 6455
- while (true) {
- if (process_offset + 2 > data.len) break; // minimal header = 2 bytes
-
- const start_idx = process_offset;
- const byte1 = data[start_idx];
- const byte2 = data[start_idx + 1];
-
- const fin = (byte1 & 0x80) != 0;
- const rsv1 = (byte1 & 0x40) != 0;
- const opcode = byte1 & 0x0F;
- const has_mask = (byte2 & 0x80) != 0;
- var payload_len = @as(usize, byte2 & 0x7F);
-
- var header_len: usize = 2;
-
- if (payload_len == 126) {
- if (start_idx + 4 > data.len) break; // waiting more bytes
- payload_len = @as(usize, data[start_idx + 2]) << 8 | @as(usize, data[start_idx + 3]);
- header_len += 2;
-
- } else if (payload_len == 127) {
- if (start_idx + 10 > data.len) break; // waiting more bytes
-
- payload_len =
- @as(usize, data[start_idx+2]) << 56 |
- @as(usize, data[start_idx+3]) << 48 |
- @as(usize, data[start_idx+4]) << 40 |
- @as(usize, data[start_idx+5]) << 32 |
- @as(usize, data[start_idx+6]) << 24 |
- @as(usize, data[start_idx+7]) << 16 |
- @as(usize, data[start_idx+8]) << 8 |
- @as(usize, data[start_idx+9]);
- header_len += 8;
- }
-
- var mask: [4]u8 = undefined;
- if (has_mask) {
- if (start_idx + header_len + 4 > data.len) break;
- @memcpy(&mask, data[start_idx + header_len..][0..4]);
- header_len += 4;
- }
-
- const total_frame_len = header_len + payload_len;
- if (start_idx + total_frame_len > data.len) break; // is complete?
-
- const payload_start = start_idx + header_len; // must be complete
- const payload = data[payload_start .. payload_start + payload_len];
-
- if (has_mask) {
- var j: usize = 0;
- while (j < payload_len) : (j += 1) {
- payload[j] ^= mask[j % 4];
- }
- }
-
- switch (opcode) {
- 0x1 => { // Text
- if (fin and handler.on_message) |on_msg| {
- try on_msg(conn, buffer[i .. i + payload_len]);
- }
- },
- 0x8 => { // Close
- conn.close();
- if (handler.on_disconnect) |on_disconnect| {
- _ = on_disconnect(conn);
- }
- return;
- },
- 0x9 => { // Ping - reply Pong
- const pong = ([2]u8{ 0x8A, @intCast(@min(payload_len, 125)) })[0..];
- _ = try conn.socket.send_all(conn.runtime, pong);
- if (payload_len <= 125) {
- _ = try conn.socket.send_all(conn.runtime, buffer[i .. i + payload_len]);
- }
- },
- else => {
- // ignore other opcode (binary, pong etc)
- },
- }
-
- process_offset += total_frame_len;
- }
-
- if (process_offset > 0) { // clean
- const remaining = data.len - process_offset;
- if (remaining == 0) { // clean buffer
- stash.clearRetainingCapacity();
-
- if (stash.capacity > 1024 * 1024) { // free RAM when buffer size more than 1mb
- stash.shrinkAndFree(0);
- }
-
- } else {
- std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]);
- stash.shrinkRetainingCapacity(remaining);
- }
- }
- }
+ try websocket.runLoop(
+ .{
+ .socket = conn.socket,
+ .runtime = conn.runtime,
+ .user_data = conn.user_data,
+ },
+ .{
+ .on_connect = handler.on_connect,
+ .on_message = handler.on_message,
+ .on_disconnect = handler.on_disconnect,
+ },
+ conn.runtime.allocator,
+ );
}
// Sec-WebSocket-Accept
From 8ae97071a6446dc39158e1b7a9396af051a595a9 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Wed, 3 Jun 2026 00:44:43 +0300
Subject: [PATCH 08/26] do not parse headers again
---
src/http/server.zig | 25 ++++++++++++-------------
1 file changed, 12 insertions(+), 13 deletions(-)
diff --git a/src/http/server.zig b/src/http/server.zig
index 9f56e54..81fceb8 100644
--- a/src/http/server.zig
+++ b/src/http/server.zig
@@ -349,26 +349,25 @@ pub const Server = struct {
.handler => {
- const request_text = provision.zc_recv_buffer.as_slice();
- var request = Request.init(rt.allocator);
- request.parse_headers(request_text, .{
- .request_bytes_max = config.request_bytes_max,
- .request_uri_bytes_max = config.request_uri_bytes_max,
- }) catch |e| {
- log.debug("malformed request| {}", .{e});
- break :http_loop;
- };
+ //const request_text = provision.zc_recv_buffer.as_slice();
+ //var request = Request.init(rt.allocator);
+ //request.parse_headers(request_text, .{
+ // .request_bytes_max = config.request_bytes_max,
+ // .request_uri_bytes_max = config.request_uri_bytes_max,
+ //}) catch |e| {
+ // log.debug("malformed request| {}", .{e});
+ // break :http_loop;
+ //};
+ var request = &provision.request;
if (config.on_upgrade) |on_upgrade| { // check is this WebSocket upgrade
request.socket = &secure;
request.runtime = rt;
- const upgrade_header = request.headers.get("Upgrade");
- if (upgrade_header) |upgrade| {
-
+ if (request.headers.get("Upgrade")) |upgrade| {
if (std.mem.eql(u8, upgrade, "websocket")) {
- if (try on_upgrade(&request, upgrade)) {
+ if (try on_upgrade(request, upgrade)) {
continue :http_loop;
}
}
From 5252265fdd023ca58bdcb16c5b04fc29f5a5b555 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Wed, 3 Jun 2026 00:44:56 +0300
Subject: [PATCH 09/26] clean ws
---
src/http/websocket.zig | 108 -----------------------------------------
1 file changed, 108 deletions(-)
delete mode 100644 src/http/websocket.zig
diff --git a/src/http/websocket.zig b/src/http/websocket.zig
deleted file mode 100644
index 4acc49f..0000000
--- a/src/http/websocket.zig
+++ /dev/null
@@ -1,108 +0,0 @@
-
-const std = @import("std");
-
-const zzz = @import("../lib.zig");
-
-const websocket = zzz.websocket;
-const SecureSocket = zzz.secsock.SecureSocket;
-const Runtime = zzz.tardy.Runtime;
-
-const Context = @import("context.zig").Context;
-
-
-pub const WebSocketHandler = struct {
- on_connect: ?*const fn (Conn) anyerror!void = null,
- on_message: ?*const fn (Conn, []const u8) anyerror!void = null,
- on_disconnect: ?*const fn (Conn) anyerror!void = null,
- user_data: ?*anyopaque = null,
-};
-
-pub const Conn = struct {
- socket: *const SecureSocket,
- runtime: *Runtime,
- user_data: ?*anyopaque,
-
- pub fn send(self: Conn, data: []const u8) !void {
- const frame = websocket.frameText(data);
- _ = try self.socket.send_all(self.runtime, frame);
- }
-
- pub fn close(self: Conn) void {
- self.socket.close_blocking();
- }
-};
-
-pub fn upgrade_to_websocket(
- ctx: *const Context,
- handler: WebSocketHandler,
-) !bool {
- const req = ctx.request;
- const res = ctx.response;
-
- // handshake
- if (!std.mem.eql(u8, req.headers.get("Connection") orelse "", "Upgrade"))
- return false;
- if (!std.mem.eql(u8, req.headers.get("Upgrade") orelse "", "websocket"))
- return false;
- if (!std.mem.eql(u8, req.headers.get("Sec-WebSocket-Version") orelse "", "13"))
- return false;
-
- const key = req.headers.get("Sec-WebSocket-Key") orelse return false;
-
- // Sec-WebSocket-Accept
- const accept = try compute_accept(ctx.allocator, key);
-
- // 101 Switching Protocols
- res.clear();
- try res.headers.put("Upgrade", "websocket");
- try res.headers.put("Connection", "Upgrade");
- try res.headers.put("Sec-WebSocket-Accept", accept);
- res.status = .@"Switching Protocols";
-
- try res.headers_into_writer(ctx.header_buffer.writer(), 0);
- _ = try ctx.socket.send_all(ctx.runtime, ctx.header_buffer.items);
-
- // WebSocket Conn
- const conn = Conn{
- .socket = &ctx.socket,
- .runtime = ctx.runtime,
- .user_data = handler.user_data,
- };
-
- // on_connect
- if (handler.on_connect) |on_connect| {
- try on_connect(conn);
- }
-
- // process
- try ctx.runtime.spawn(.{ conn, handler }, message_loop, 64 * 1024);
- return true;
-}
-
-// inner loop
-fn message_loop(conn: Conn, handler: WebSocketHandler) !void {
- try websocket.runLoop(
- .{
- .socket = conn.socket,
- .runtime = conn.runtime,
- .user_data = conn.user_data,
- },
- .{
- .on_connect = handler.on_connect,
- .on_message = handler.on_message,
- .on_disconnect = handler.on_disconnect,
- },
- conn.runtime.allocator,
- );
-}
-
-// Sec-WebSocket-Accept
-fn compute_accept(allocator: std.mem.Allocator, key: []const u8) ![]const u8 {
- const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
- var hasher = std.crypto.hash.sha1.Sha1.init();
- hasher.update(key);
- hasher.update(magic);
- const hash = hasher.final();
- return try std.base64.encode(allocator, &hash);
-}
-
From 70850958452cefa185c80f4d22815acb16563444 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Wed, 3 Jun 2026 00:52:42 +0300
Subject: [PATCH 10/26] ws - rm temp variable
---
src/websocket/websocket.zig | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/websocket/websocket.zig b/src/websocket/websocket.zig
index 1b7fab7..f64c436 100644
--- a/src/websocket/websocket.zig
+++ b/src/websocket/websocket.zig
@@ -47,8 +47,7 @@ pub const Conn = struct {
fn writeFrame(self: Conn, opcode: OpCode, data: []const u8) !void {
var header: [10]u8 = undefined;
- const f_byte: u8 = @intFromEnum(opcode) | 0x80;
- header[0] = f_byte;
+ header[0] = @intFromEnum(opcode) | 0x80;
var h_len: usize = 2;
if (data.len < 126) {
From 685a82effd9bcfc635b13e36fb7cdb68408e2884 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Wed, 3 Jun 2026 01:15:19 +0300
Subject: [PATCH 11/26] rename dir examples -> examples_http ; edit build.zig
-- executables to repo root
---
build.zig | 5 +++--
{examples => examples_http}/basic/main.zig | 0
{examples => examples_http}/cookies/main.zig | 0
{examples => examples_http}/form/main.zig | 0
{examples => examples_http}/fs/main.zig | 0
{examples => examples_http}/fs/static/index.html | 0
{examples => examples_http}/middleware/main.zig | 0
{examples => examples_http}/sse/index.html | 0
{examples => examples_http}/sse/main.zig | 0
{examples => examples_http}/tls/certs/cert.pem | 0
{examples => examples_http}/tls/certs/key.pem | 0
{examples => examples_http}/tls/embed/pico.min.css | 0
{examples => examples_http}/tls/main.zig | 0
{examples => examples_http}/unix/main.zig | 0
14 files changed, 3 insertions(+), 2 deletions(-)
rename {examples => examples_http}/basic/main.zig (100%)
rename {examples => examples_http}/cookies/main.zig (100%)
rename {examples => examples_http}/form/main.zig (100%)
rename {examples => examples_http}/fs/main.zig (100%)
rename {examples => examples_http}/fs/static/index.html (100%)
rename {examples => examples_http}/middleware/main.zig (100%)
rename {examples => examples_http}/sse/index.html (100%)
rename {examples => examples_http}/sse/main.zig (100%)
rename {examples => examples_http}/tls/certs/cert.pem (100%)
rename {examples => examples_http}/tls/certs/key.pem (100%)
rename {examples => examples_http}/tls/embed/pico.min.css (100%)
rename {examples => examples_http}/tls/main.zig (100%)
rename {examples => examples_http}/unix/main.zig (100%)
diff --git a/build.zig b/build.zig
index b193b29..fa033a5 100644
--- a/build.zig
+++ b/build.zig
@@ -122,7 +122,7 @@ fn add_http_example(
) void {
const example = b.addExecutable(.{
.name = name,
- .root_source_file = b.path(b.fmt("./examples/{s}/main.zig", .{name})),
+ .root_source_file = b.path(b.fmt("./examples_http/{s}/main.zig", .{name})),
.target = target,
.optimize = optimize,
.strip = false,
@@ -134,7 +134,8 @@ fn add_http_example(
example.root_module.addImport("zzz", zzz_module);
- const install_artifact = b.addInstallArtifact(example, .{});
+ //const install_artifact = b.addInstallArtifact(example, .{});
+ const install_artifact = b.addInstallBinFile(example.getEmittedBin(), b.fmt("../../{s}", .{name})); // to project root
b.getInstallStep().dependOn(&install_artifact.step);
all_http_examples_step.dependOn(&install_artifact.step);
diff --git a/examples/basic/main.zig b/examples_http/basic/main.zig
similarity index 100%
rename from examples/basic/main.zig
rename to examples_http/basic/main.zig
diff --git a/examples/cookies/main.zig b/examples_http/cookies/main.zig
similarity index 100%
rename from examples/cookies/main.zig
rename to examples_http/cookies/main.zig
diff --git a/examples/form/main.zig b/examples_http/form/main.zig
similarity index 100%
rename from examples/form/main.zig
rename to examples_http/form/main.zig
diff --git a/examples/fs/main.zig b/examples_http/fs/main.zig
similarity index 100%
rename from examples/fs/main.zig
rename to examples_http/fs/main.zig
diff --git a/examples/fs/static/index.html b/examples_http/fs/static/index.html
similarity index 100%
rename from examples/fs/static/index.html
rename to examples_http/fs/static/index.html
diff --git a/examples/middleware/main.zig b/examples_http/middleware/main.zig
similarity index 100%
rename from examples/middleware/main.zig
rename to examples_http/middleware/main.zig
diff --git a/examples/sse/index.html b/examples_http/sse/index.html
similarity index 100%
rename from examples/sse/index.html
rename to examples_http/sse/index.html
diff --git a/examples/sse/main.zig b/examples_http/sse/main.zig
similarity index 100%
rename from examples/sse/main.zig
rename to examples_http/sse/main.zig
diff --git a/examples/tls/certs/cert.pem b/examples_http/tls/certs/cert.pem
similarity index 100%
rename from examples/tls/certs/cert.pem
rename to examples_http/tls/certs/cert.pem
diff --git a/examples/tls/certs/key.pem b/examples_http/tls/certs/key.pem
similarity index 100%
rename from examples/tls/certs/key.pem
rename to examples_http/tls/certs/key.pem
diff --git a/examples/tls/embed/pico.min.css b/examples_http/tls/embed/pico.min.css
similarity index 100%
rename from examples/tls/embed/pico.min.css
rename to examples_http/tls/embed/pico.min.css
diff --git a/examples/tls/main.zig b/examples_http/tls/main.zig
similarity index 100%
rename from examples/tls/main.zig
rename to examples_http/tls/main.zig
diff --git a/examples/unix/main.zig b/examples_http/unix/main.zig
similarity index 100%
rename from examples/unix/main.zig
rename to examples_http/unix/main.zig
From 73787151e2c1d40dc42787f4c065fbd15616257c Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Fri, 5 Jun 2026 01:52:08 +0300
Subject: [PATCH 12/26] fix path - fs example_http
---
examples_http/fs/main.zig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples_http/fs/main.zig b/examples_http/fs/main.zig
index d566ebb..d423f78 100644
--- a/examples_http/fs/main.zig
+++ b/examples_http/fs/main.zig
@@ -49,7 +49,7 @@ pub fn main() !void {
var t = try Tardy.init(allocator, .{ .threading = .auto });
defer t.deinit();
- const static_dir = Dir.from_std(try std.fs.cwd().openDir("examples/fs/static", .{}));
+ const static_dir = Dir.from_std(try std.fs.cwd().openDir("examples_http/fs/static", .{}));
var router = try Router.init(allocator, &.{
Compression(.{ .gzip = .{} }),
From 33d5fa8c06867773ab607af29e6292ef8e0c9de9 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Fri, 5 Jun 2026 01:56:07 +0300
Subject: [PATCH 13/26] upd log path
---
examples_http/tls/main.zig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples_http/tls/main.zig b/examples_http/tls/main.zig
index d8a07e4..14980d2 100644
--- a/examples_http/tls/main.zig
+++ b/examples_http/tls/main.zig
@@ -1,5 +1,5 @@
const std = @import("std");
-const log = std.log.scoped(.@"examples/tls");
+const log = std.log.scoped(.@"examples_http/tls");
const zzz = @import("zzz");
const http = zzz.HTTP;
From 7d75eca597f0b0006a4fab593122a2810f0f39ac Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Fri, 5 Jun 2026 02:36:39 +0300
Subject: [PATCH 14/26] fix json error -- sse http example -- just return POST
response
---
examples_http/sse/index.html | 5 +++--
examples_http/sse/main.zig | 16 ++++++++++++++++
2 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/examples_http/sse/index.html b/examples_http/sse/index.html
index 218bccc..f5b4500 100644
--- a/examples_http/sse/index.html
+++ b/examples_http/sse/index.html
@@ -52,12 +52,13 @@ Server-Sent Events Example
method: 'POST',
})
.then(response => response.json()) // Adjust based on your server response
+ //.then(response => { console.log(response); return response.json() }) // Adjust based on your server response
.then(data => {
- console.log('Message sent:', data); // Handle successful response
+ console.log('Message sent: ', data); // Handle successful response
this.reset(); // Clear the form
})
.catch(error => {
- console.error('Error sending message:', error); // Handle any errors
+ console.error('Error sending message: ', error); // Handle any errors
});
});
diff --git a/examples_http/sse/main.zig b/examples_http/sse/main.zig
index 715dfab..1d5d5f2 100644
--- a/examples_http/sse/main.zig
+++ b/examples_http/sse/main.zig
@@ -28,6 +28,21 @@ fn sse_handler(ctx: *const Context, _: void) !Respond {
return .responded;
}
+fn post_handler(ctx: *const Context, _: void) !Respond {
+ const body = std.json.stringifyAlloc(ctx.allocator, .{
+ .id = 1,
+ .message = "hello from post handler",
+ }, .{}) catch unreachable;
+ defer ctx.allocator.free(body);
+
+ return ctx.response.apply(.{
+ .status = .OK,
+ //.mime = http.Mime.TEXT,
+ .mime = http.Mime.JSON,
+ .body = body,
+ });
+}
+
pub fn main() !void {
const host: []const u8 = "0.0.0.0";
const port: u16 = 9862;
@@ -42,6 +57,7 @@ pub fn main() !void {
const router = try Router.init(allocator, &.{
Route.init("/").embed_file(.{ .mime = http.Mime.HTML }, @embedFile("./index.html")).layer(),
Route.init("/stream").get({}, sse_handler).layer(),
+ Route.init("/message").post({}, post_handler).layer(),
}, .{});
// create socket for tardy
From f19eefb7f23060f36cf50f9f825768629ff20e40 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Fri, 5 Jun 2026 02:51:01 +0300
Subject: [PATCH 15/26] fix cyrillic correct display form fields -- form
example_http
---
examples_http/form/main.zig | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/examples_http/form/main.zig b/examples_http/form/main.zig
index 2cb122f..df400cd 100644
--- a/examples_http/form/main.zig
+++ b/examples_http/form/main.zig
@@ -19,6 +19,10 @@ const Respond = http.Respond;
fn base_handler(ctx: *const Context, _: void) !Respond {
const body =
+ \\
+ \\
+ \\
+ \\
\\
+ \\
+ \\
+ \\
;
return ctx.response.apply(.{
@@ -72,6 +78,9 @@ fn generate_handler(ctx: *const Context, _: void) !Respond {
return ctx.response.apply(.{
.status = .OK,
.mime = http.Mime.TEXT,
+ .headers = &.{
+ .{ "Content-Type", "text/plain; charset=utf-8" },
+ },
.body = body,
});
}
From 17d35df2faefdd0159e12ac97e554fb2717b3697 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Fri, 5 Jun 2026 04:05:26 +0300
Subject: [PATCH 16/26] make zzz less strict on submit empty form values ; fix
form example_http
---
examples_http/form/main.zig | 52 +++++++++++++++++++++++---------
src/http/form.zig | 2 +-
src/http/router/routing_trie.zig | 2 +-
3 files changed, 40 insertions(+), 16 deletions(-)
diff --git a/examples_http/form/main.zig b/examples_http/form/main.zig
index df400cd..e083bb1 100644
--- a/examples_http/form/main.zig
+++ b/examples_http/form/main.zig
@@ -47,31 +47,55 @@ fn base_handler(ctx: *const Context, _: void) !Respond {
}
const UserInfo = struct {
- fname: []const u8,
- mname: []const u8 = "Middle",
- lname: []const u8,
- age: u8,
- height: f32,
- weight: ?[]const u8,
+ fname: ?[]const u8 = null,
+ mname: ?[]const u8 = "Middle",
+ lname: ?[]const u8 = null,
+ age: ?[]const u8 = null,
+ height: ?[]const u8 = null,
+ weight: ?[]const u8 = null,
};
fn generate_handler(ctx: *const Context, _: void) !Respond {
+ //std.debug.print("Handler entered, method: {any}\n", .{ctx.request.method});
const info = switch (ctx.request.method.?) {
- .GET => try Query(UserInfo).parse(ctx.allocator, ctx),
- .POST => try Form(UserInfo).parse(ctx.allocator, ctx),
+ .GET => Query(UserInfo).parse(ctx.allocator, ctx) catch |err| {
+ std.debug.print("Query parse failed: {}\n", .{err});
+ return ctx.response.apply(.{
+ .status = .@"Bad Request",
+ .mime = http.Mime.TEXT,
+ .headers = &.{ .{ "Content-Type", "text/plain; charset=utf-8" } },
+ .body = "Invalid or empty query parameters",
+ });
+ },
+ .POST => Form(UserInfo).parse(ctx.allocator, ctx) catch |err| {
+ std.debug.print("Form parse failed: {}\n", .{err});
+ return ctx.response.apply(.{
+ .status = .@"Bad Request",
+ .mime = http.Mime.TEXT,
+ .headers = &.{ .{ "Content-Type", "text/plain; charset=utf-8" } },
+ .body = "Invalid or empty form data",
+ });
+ },
else => return error.UnexpectedMethod,
};
+ const fname = info.fname orelse "";
+ const mname = info.mname orelse "Middle";
+ const lname = info.lname orelse "";
+ const age = if (info.age) |v| std.fmt.parseInt(u8, v, 10) catch 0 else 0;
+ const height = if (info.height) |v| std.fmt.parseFloat(f32, v) catch 0.0 else 0.0;
+ const weight = if (info.weight) |w| if (w.len == 0) "none" else w else "none";
+
const body = try std.fmt.allocPrint(
ctx.allocator,
"First: {s} | Middle: {s} | Last: {s} | Age: {d} | Height: {d} | Weight: {s}",
.{
- info.fname,
- info.mname,
- info.lname,
- info.age,
- info.height,
- info.weight orelse "none",
+ if (fname.len == 0) "(empty)" else fname,
+ if (mname.len == 0) "Middle" else mname,
+ if (lname.len == 0) "(empty)" else lname,
+ age,
+ height,
+ weight,
},
);
diff --git a/src/http/form.zig b/src/http/form.zig
index 6cd887e..a762cf5 100644
--- a/src/http/form.zig
+++ b/src/http/form.zig
@@ -70,7 +70,7 @@ fn construct_map_from_body(allocator: std.mem.Allocator, m: *AnyCaseStringMap, b
while (pairs.next()) |pair| {
const field_idx = std.mem.indexOfScalar(u8, pair, '=') orelse return error.MissingSeperator;
- if (pair.len < field_idx + 2) return error.MissingValue;
+ //if (pair.len < field_idx + 2) return error.MissingValue; // this makes problem as cannot get emply values when submit POST request with empty form values
const key = pair[0..field_idx];
const value = pair[(field_idx + 1)..];
diff --git a/src/http/router/routing_trie.zig b/src/http/router/routing_trie.zig
index c59ec33..dac92bc 100644
--- a/src/http/router/routing_trie.zig
+++ b/src/http/router/routing_trie.zig
@@ -274,7 +274,7 @@ pub const RoutingTrie = struct {
while (query_iter.next()) |chunk| {
const field_idx = std.mem.indexOfScalar(u8, chunk, '=') orelse return error.MissingValue;
- if (chunk.len < field_idx + 2) return error.MissingValue;
+ //if (chunk.len < field_idx + 2) return error.MissingValue; // this makes problem as connection_refused when submit GET request with empty form values
const key = chunk[0..field_idx];
const value = chunk[(field_idx + 1)..];
From ab9f3c713eef83ae7902a37723239cbd7b5eea5d Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Mon, 15 Jun 2026 05:46:36 +0300
Subject: [PATCH 17/26] upd gitignore
---
.gitignore | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/.gitignore b/.gitignore
index e304926..2ae1e6e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,15 @@ zig-out/
.zig-cache/
perf*.data*
heaptrack*
+/basic
+/cookies
+/form
+/fs
+/middleware
+/rest
+/sse
+/tls
+/unix
/ex_ws_1
/ex_ws_2
/ex_ws_3
From e16e949c2bf0c1d89b362b8c5879d0387f68350e Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Mon, 15 Jun 2026 05:47:13 +0300
Subject: [PATCH 18/26] add rest http example
---
build.zig | 1 +
examples_http/rest/main.zig | 140 ++++++++++++++++++++++++++++++++++++
2 files changed, 141 insertions(+)
create mode 100644 examples_http/rest/main.zig
diff --git a/build.zig b/build.zig
index fa033a5..5bbec0c 100644
--- a/build.zig
+++ b/build.zig
@@ -33,6 +33,7 @@ pub fn build(b: *std.Build) void {
add_http_example(b, all_http_examples_step, "middleware", false, target, optimize, zzz);
add_http_example(b, all_http_examples_step, "sse", false, target, optimize, zzz);
add_http_example(b, all_http_examples_step, "tls", true, target, optimize, zzz);
+ add_http_example(b, all_http_examples_step, "rest", false, target, optimize, zzz);
if (target.result.os.tag != .windows) {
add_http_example(b, all_http_examples_step, "unix", false, target, optimize, zzz);
diff --git a/examples_http/rest/main.zig b/examples_http/rest/main.zig
new file mode 100644
index 0000000..a45219b
--- /dev/null
+++ b/examples_http/rest/main.zig
@@ -0,0 +1,140 @@
+
+// http rest example - Content Negotiation - return different data types depending on client request
+
+const std = @import("std");
+
+const zzz = @import("zzz");
+
+const http = zzz.HTTP;
+const Socket = zzz.tardy.Socket;
+
+
+const PORT = 9862; // 8080;
+const HOST = "0.0.0.0";
+
+
+const STACK_SIZE = if (@import("builtin").mode == .Debug)
+ 1 * 1024 * 1024 // DEBUG = 1mb
+else
+ 32 * 1024; // RELEASE = 32kb
+
+
+const User = struct {
+ id: u32,
+ name: []const u8,
+ email: []const u8,
+};
+
+
+fn on_request(ctx: *const http.Context, _: void) !http.Respond {
+ if(ctx.request.method.? == .GET){
+ return ctx.response.apply(.{
+ .status = .OK,
+ .mime = http.Mime.HTML,
+ .body =
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\zzz REST Content Negotiation
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ });
+ } // else POST request
+
+ const user = User{ .id = 1, .name = "Alice", .email = "alice@example.com" };
+ const accept = ctx.request.headers.get("accept") orelse "*/*";
+
+
+ if(std.mem.indexOf(u8, accept, "application/json") != null){ // if expect json
+ const body = try std.json.stringifyAlloc(ctx.allocator, user, .{});
+ return ctx.response.apply(.{
+ .status = .OK,
+ .mime = http.Mime.JSON,
+ .body = body,
+ });
+ }
+
+
+ if(std.mem.indexOf(u8, accept, "text/html") != null){ // if expect html
+ const body = try std.fmt.allocPrint(ctx.allocator,
+ "User: {s} has ID: {d}
",
+ .{ user.name, user.id });
+
+ return ctx.response.apply(.{
+ .status = .OK,
+ .mime = http.Mime.HTML,
+ .body = body,
+ });
+ }
+
+
+ return ctx.response.apply(.{ // else plain text
+ .status = .OK,
+ .mime = http.Mime.TEXT,
+ .body = try std.fmt.allocPrint(ctx.allocator, "User: {s} (ID: {d})", .{ user.name, user.id }),
+ });
+}
+
+
+
+pub fn main() !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ const allocator = gpa.allocator();
+ defer _ = gpa.deinit();
+
+ const socket = try Socket.init(.{ .tcp = .{ .host = HOST, .port = PORT } });
+ defer socket.close_blocking();
+ try socket.bind();
+ try socket.listen(1024); // max conn count that are waiting in accept queue
+
+ var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{});
+ defer tardy.deinit();
+
+ try tardy.entry(&socket, struct {
+ fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void {
+ const config = zzz.ServerConfig{ .stack_size = STACK_SIZE };
+ const router = try rt.allocator.create(zzz.Router);
+ router.* = try zzz.Router.init(rt.allocator, &.{
+ http.Route.init("/").get({}, on_request).post({}, on_request).layer(),
+ }, .{ .not_found = on_request });
+
+ const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap instead of stack
+ provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); // 1024 = pool size
+ @memset(std.mem.sliceAsBytes(provisions.items), 0); // set zeros -- initialized = false
+
+ const conn_count = try rt.allocator.create(usize);
+ conn_count.* = 0;
+ const accept_q = try rt.allocator.create(bool);
+ accept_q.* = false;
+
+ try rt.spawn(.{ rt, config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, conn_count, accept_q }, zzz.Server.main_frame, config.stack_size);
+ } // end fn entry
+ }.entry);
+}
+
From f0967a4c5ac2094074d7bc08fbf9ca96da8033dc Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Wed, 17 Jun 2026 00:10:57 +0300
Subject: [PATCH 19/26] refactoring
---
examples_http/rest/main.zig | 40 ++++++++++++++++++-------------------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/examples_http/rest/main.zig b/examples_http/rest/main.zig
index a45219b..73f5e3c 100644
--- a/examples_http/rest/main.zig
+++ b/examples_http/rest/main.zig
@@ -116,25 +116,25 @@ pub fn main() !void {
var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{});
defer tardy.deinit();
- try tardy.entry(&socket, struct {
- fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void {
- const config = zzz.ServerConfig{ .stack_size = STACK_SIZE };
- const router = try rt.allocator.create(zzz.Router);
- router.* = try zzz.Router.init(rt.allocator, &.{
- http.Route.init("/").get({}, on_request).post({}, on_request).layer(),
- }, .{ .not_found = on_request });
-
- const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap instead of stack
- provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); // 1024 = pool size
- @memset(std.mem.sliceAsBytes(provisions.items), 0); // set zeros -- initialized = false
-
- const conn_count = try rt.allocator.create(usize);
- conn_count.* = 0;
- const accept_q = try rt.allocator.create(bool);
- accept_q.* = false;
-
- try rt.spawn(.{ rt, config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, conn_count, accept_q }, zzz.Server.main_frame, config.stack_size);
- } // end fn entry
- }.entry);
+ const config = zzz.ServerConfig{ .stack_size = STACK_SIZE };
+
+ const router = try zzz.Router.init(allocator, &.{
+ http.Route.init("/").get({}, on_request).post({}, on_request).layer(),
+ }, .{ .not_found = on_request });
+
+ const Entry_Params = struct {
+ config: zzz.ServerConfig,
+ router: *const http.Router,
+ socket: Socket,
+ };
+
+ try tardy.entry(
+ Entry_Params{ .config = config, .router = &router, .socket = socket },
+ struct {
+ fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void {
+ var server = zzz.Server.init(p.config);
+ try server.serve(rt, p.router, .{ .normal = p.socket });
+ } // end fn entry
+ }.entry);
}
From 9cc78817fb9c072e807961be53d48dd43e376154 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Wed, 17 Jun 2026 00:41:47 +0300
Subject: [PATCH 20/26] refactoring 2
---
examples_http/rest/main.zig | 4 +--
examples_ws/example_ws_1.zig | 63 +++++++++++++-----------------------
2 files changed, 24 insertions(+), 43 deletions(-)
diff --git a/examples_http/rest/main.zig b/examples_http/rest/main.zig
index 73f5e3c..3d9196f 100644
--- a/examples_http/rest/main.zig
+++ b/examples_http/rest/main.zig
@@ -116,8 +116,6 @@ pub fn main() !void {
var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{});
defer tardy.deinit();
- const config = zzz.ServerConfig{ .stack_size = STACK_SIZE };
-
const router = try zzz.Router.init(allocator, &.{
http.Route.init("/").get({}, on_request).post({}, on_request).layer(),
}, .{ .not_found = on_request });
@@ -129,7 +127,7 @@ pub fn main() !void {
};
try tardy.entry(
- Entry_Params{ .config = config, .router = &router, .socket = socket },
+ Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = socket },
struct {
fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void {
var server = zzz.Server.init(p.config);
diff --git a/examples_ws/example_ws_1.zig b/examples_ws/example_ws_1.zig
index e102a8d..e7e50d6 100644
--- a/examples_ws/example_ws_1.zig
+++ b/examples_ws/example_ws_1.zig
@@ -165,48 +165,31 @@ pub fn main() !void{
try socket.bind();
try socket.listen(1024); // max conn count that are waiting in accept queue
- const TardyType = zzz.tardy.Tardy(.auto);
- var tardy = try TardyType.init(allocator, .{});
- //var tardy = try TardyType.init(allocator, .{ .threading = .single });
+ var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{});
defer tardy.deinit();
- try tardy.entry(&socket, struct {
- fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void {
- const config = zzz.ServerConfig{
- .stack_size = STACK_SIZE,
- };
-
- const home_route = zzz.HTTP.Route.init("/").get({}, on_request);
- const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint);
- const layers = &[_]zzz.HTTP.Layer{
- home_route.layer(),
- ws_route.layer(),
- };
-
- const router = try rt.allocator.create(zzz.Router);
- router.* = try zzz.Router.init(rt.allocator, layers, .{
- .not_found = on_request,
- });
- // no defer router - lifetime per server work
-
- const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap, not stack
- provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); // 1024 = pool size
-
- const byte_count = provisions.items.len * @sizeOf(zzz.Provision); // set zeros - for initialized = false
- @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0);
-
- const connection_count = try rt.allocator.create(usize); // use heap, not stack
- connection_count.* = 0;
-
- const accept_queued = try rt.allocator.create(bool);
- accept_queued.* = false;
-
- try rt.spawn(
- .{ rt, config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, connection_count, accept_queued },
- zzz.Server.main_frame,
- config.stack_size
- );
-
+ const home_route = zzz.HTTP.Route.init("/").get({}, on_request);
+ const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint);
+ const layers = &[_]zzz.HTTP.Layer{
+ home_route.layer(),
+ ws_route.layer(),
+ };
+
+ const router = try zzz.Router.init(allocator, layers, .{ .not_found = on_request });
+ // no defer router - lifetime per server work // defer router.deinit(allocator);
+
+ const Entry_Params = struct {
+ config: zzz.ServerConfig,
+ router: *const zzz.Router,
+ socket: Socket,
+ };
+
+ try tardy.entry(
+ Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = socket },
+ struct {
+ fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void {
+ var server = zzz.Server.init(p.config);
+ try server.serve(rt, p.router, .{ .normal = p.socket });
} // end fn entry
}.entry);
}
From 5f26fb4c2a3b81b846b4b04e9628ab387945ffbe Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Wed, 17 Jun 2026 03:51:19 +0300
Subject: [PATCH 21/26] refactoring, trying to fix memleak
---
examples_ws/example_ws_1.zig | 28 +++++++----
examples_ws/example_ws_2.zig | 83 ++++++++++++--------------------
src/http/router/routing_trie.zig | 3 +-
src/http/server.zig | 23 +++++++--
src/websocket/pubsub.zig | 19 +++++---
src/websocket/websocket.zig | 16 +++---
6 files changed, 91 insertions(+), 81 deletions(-)
diff --git a/examples_ws/example_ws_1.zig b/examples_ws/example_ws_1.zig
index e7e50d6..0aa9c57 100644
--- a/examples_ws/example_ws_1.zig
+++ b/examples_ws/example_ws_1.zig
@@ -82,11 +82,13 @@ fn on_upgrade(req: *const zzz.Request, proto: []const u8) !bool {
const key = req.headers.get("Sec-WebSocket-Key") orelse return false;
const ext = req.headers.get("Sec-WebSocket-Extensions");
- var header_buf = std.ArrayList(u8).init(std.heap.page_allocator);
+ //var header_buf = std.ArrayList(u8).init(std.heap.page_allocator);
+ var header_buf = std.ArrayList(u8).init(req.runtime.allocator);
defer header_buf.deinit();
//const res = try websocket.upgrade(req.socket, req.runtime, std.heap.page_allocator, key, ext, header_buf.writer() );
- const res = try websocket.upgrade(req.socket, req.runtime, req.runtime.allocator, key, ext, header_buf.writer() );
+ //const res = try websocket.upgrade(req.socket, req.runtime, req.runtime.allocator, key, ext, header_buf.writer() );
+ const res = try websocket.upgrade(req.socket, req.runtime, key, ext, header_buf.writer() );
_ = try req.socket.send_all(req.runtime, header_buf.items);
@@ -98,8 +100,8 @@ fn on_upgrade(req: *const zzz.Request, proto: []const u8) !bool {
};
if (ws_handler.on_connect) |f| try f(res.conn);
- try req.runtime.spawn(.{ res.conn, ws_handler, std.heap.page_allocator }, websocket.runLoop, STACK_SIZE);
- //try req.runtime.spawn(.{ res.conn, ws_handler, req.runtime.allocator }, websocket.runLoop, STACK_SIZE);
+ //try req.runtime.spawn(.{ res.conn, ws_handler, std.heap.page_allocator }, websocket.runLoop, STACK_SIZE);
+ try req.runtime.spawn(.{ res.conn, ws_handler, req.runtime.allocator }, websocket.runLoop, STACK_SIZE);
return true;
}
@@ -123,7 +125,8 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
var header_buf = std.ArrayList(u8).init(ctx.allocator);
defer header_buf.deinit();
- const res = try websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer());
+ //const res = try websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer());
+ const res = try websocket.upgrade(&ctx.socket, ctx.runtime, key, ext, header_buf.writer());
_ = try ctx.socket.send_all(ctx.runtime, header_buf.items);
@@ -138,8 +141,8 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
std.log.info("Starting WebSocket Loop...", .{});
- //websocket.runLoop(res.conn, ws_handler, ctx.runtime.allocator) catch |err| { // sync loop
- websocket.runLoop(res.conn, ws_handler, std.heap.page_allocator) catch |err| { // sync loop
+ websocket.runLoop(res.conn, ws_handler, ctx.runtime.allocator) catch |err| { // sync loop
+ //websocket.runLoop(res.conn, ws_handler, std.heap.page_allocator) catch |err| { // sync loop
std.log.err("WebSocket RunLoop Error: {s}", .{@errorName(err)});
if (err == error.Closed) {
@@ -156,9 +159,16 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
pub fn main() !void{
//@compileLog("STACK_SIZE = ", STACK_SIZE);
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ var gpa = std.heap.GeneralPurposeAllocator(.{
+ .thread_safe = true,
+ .stack_trace_frames = 8,
+ }){};
const allocator = gpa.allocator();
- defer _ = gpa.deinit();
+ //defer _ = gpa.deinit();
+ defer {
+ const status = gpa.deinit();
+ if (status == .leak){ std.debug.print("MEMORY LEAK DETECTED!", .{}); }else{ std.debug.print("no mem leak.\n", .{}); }
+ }
const socket = try Socket.init(.{ .tcp = .{ .host = HOST, .port = PORT } });
defer socket.close_blocking();
diff --git a/examples_ws/example_ws_2.zig b/examples_ws/example_ws_2.zig
index f886169..1c45d95 100644
--- a/examples_ws/example_ws_2.zig
+++ b/examples_ws/example_ws_2.zig
@@ -130,7 +130,8 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
var header_buf = std.ArrayList(u8).init(ctx.allocator);
defer header_buf.deinit();
- const res = try websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer());
+ //const res = try websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer());
+ const res = try websocket.upgrade(&ctx.socket, ctx.runtime, key, ext, header_buf.writer());
_ = try ctx.socket.send_all(ctx.runtime, header_buf.items);
@@ -152,7 +153,6 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
if (err == error.Closed) {
std.log.info("Socket closed by browser", .{});
}
-
};
std.log.info("WebSocket Loop finished", .{});
@@ -171,67 +171,44 @@ pub fn main() !void{
try socket.bind();
try socket.listen(1024); // max conn count that are waiting in accept queue
+ const home_route = zzz.HTTP.Route.init("/").get({}, on_request);
+ const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint);
+ const layers = &[_]zzz.HTTP.Layer{
+ home_route.layer(),
+ ws_route.layer(),
+ };
+
+ const router = try zzz.Router.init(allocator, layers, .{ .not_found = on_request });
+ // no defer router - lifetime per server work
+
+ var bearssl = zzz.secsock.BearSSL.init(allocator);
+ defer bearssl.deinit();
+
const cert = try std.fs.cwd().readFileAlloc(allocator, FULLCHAIN_CERT, 1024 * 10);
defer allocator.free(cert);
const key = try std.fs.cwd().readFileAlloc(allocator, PRIVKEY_CERT, 1024 * 10);
defer allocator.free(key);
- const ctx = ServerContext{
- .socket = socket,
- .cert_pem = cert,
- .key_pem = key,
- };
+ try bearssl.add_cert_chain("CERTIFICATE", cert, "PRIVATE KEY", key);
+ const secure_socket = try bearssl.to_secure_socket(socket, .server);
+ defer secure_socket.deinit();
const TardyType = zzz.tardy.Tardy(.auto);
var tardy = try TardyType.init(allocator, .{});
- //var tardy = try TardyType.init(allocator, .{ .threading = .single });
defer tardy.deinit();
- try tardy.entry(&ctx, struct {
- fn entry(rt: *zzz.tardy.Runtime, s_ctx: *const ServerContext) !void {
- const bearssl = try rt.allocator.create(zzz.secsock.BearSSL);
- bearssl.* = zzz.secsock.BearSSL.init(rt.allocator);
- //defer bearssl.deinit();
-
- try bearssl.add_cert_chain("CERTIFICATE", s_ctx.cert_pem, "PRIVATE KEY", s_ctx.key_pem);
- const secure_socket = try bearssl.to_secure_socket(s_ctx.socket, .server);
- //defer secure_socket.deinit();
-
- const config = zzz.ServerConfig{
- .stack_size = STACK_SIZE,
- };
-
- const home_route = zzz.HTTP.Route.init("/").get({}, on_request);
- const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint);
- const layers = &[_]zzz.HTTP.Layer{
- home_route.layer(),
- ws_route.layer(),
- };
-
- const router = try rt.allocator.create(zzz.Router);
- router.* = try zzz.Router.init(rt.allocator, layers, .{
- .not_found = on_request,
- });
- // no defer router - lifetime per server work
-
- const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap instead of stack
- provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); // 1024 = pool size
-
- const byte_count = provisions.items.len * @sizeOf(zzz.Provision); // set zeros -- initialized = false
- @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0);
-
- const connection_count = try rt.allocator.create(usize); // use heap instead stack
- connection_count.* = 0;
-
- const accept_queued = try rt.allocator.create(bool);
- accept_queued.* = false;
-
- try rt.spawn(
- .{ rt, config, router, secure_socket, provisions, connection_count, accept_queued },
- zzz.Server.main_frame,
- config.stack_size
- );
-
+ const Entry_Params = struct {
+ config: zzz.ServerConfig,
+ router: *const zzz.Router,
+ socket: zzz.secsock.SecureSocket,
+ };
+
+ try tardy.entry(
+ Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = secure_socket },
+ struct {
+ fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void {
+ var server = zzz.Server.init(p.config);
+ try server.serve(rt, p.router, .{ .secure = p.socket });
} // end fn entry
}.entry);
}
diff --git a/src/http/router/routing_trie.zig b/src/http/router/routing_trie.zig
index dac92bc..319fbdc 100644
--- a/src/http/router/routing_trie.zig
+++ b/src/http/router/routing_trie.zig
@@ -297,7 +297,8 @@ pub const RoutingTrie = struct {
.route = current.route orelse return null,
.captures = captures[0..capture_idx],
.queries = queries,
- .duped = try duped.toOwnedSlice(allocator),
+ //.duped = try duped.toOwnedSlice(allocator),
+ .duped = if (duped.items.len > 0) try duped.toOwnedSlice(allocator) else &.{},
};
}
};
diff --git a/src/http/server.zig b/src/http/server.zig
index 81fceb8..71c1761 100644
--- a/src/http/server.zig
+++ b/src/http/server.zig
@@ -237,6 +237,14 @@ pub const Server = struct {
const index = try provisions.borrow();
defer provisions.release(index);
const provision = provisions.get_ptr(index);
+
+ defer {
+ log.debug("DEBUG: Releasing slot {d} and resetting arena", .{index});
+ provision.request.clear();
+ provision.response.clear();
+ provision.storage.clear();
+ _ = provision.arena.reset(.retain_capacity);
+ }
// if we are growing, we can handle a newly allocated provision here.
// otherwise, it should be initalized.
@@ -368,7 +376,9 @@ pub const Server = struct {
if (request.headers.get("Upgrade")) |upgrade| {
if (std.mem.eql(u8, upgrade, "websocket")) {
if (try on_upgrade(request, upgrade)) {
- continue :http_loop;
+ log.debug("DEBUG: Upgrade successful for slot {d}, exiting main_frame", .{index});
+ //continue :http_loop;
+ return;
}
}
@@ -376,14 +386,19 @@ pub const Server = struct {
}
const found = try router.get_bundle_from_host( // continue as common HTTP if not WebSocket upgrade
- rt.allocator,
+ //rt.allocator,
+ provision.arena.allocator(),
//provision.request.uri.?,
request.uri.?,
provision.captures,
&provision.queries,
);
- defer rt.allocator.free(found.duped);
- defer for (found.duped) |dupe| rt.allocator.free(dupe);
+ //defer rt.allocator.free(found.duped);
+ //defer for (found.duped) |dupe| rt.allocator.free(dupe);
+ //defer if(found.duped.len > 0){
+ // for (found.duped) |item| rt.allocator.free(item);
+ // rt.allocator.free(found.duped);
+ //};
const h_with_data: HandlerWithData = found.route.get_handler(
diff --git a/src/websocket/pubsub.zig b/src/websocket/pubsub.zig
index a3bf10f..db857aa 100644
--- a/src/websocket/pubsub.zig
+++ b/src/websocket/pubsub.zig
@@ -235,11 +235,14 @@ pub fn handle_upgrade(ctx: *const zzz.Context, user_handler: UserWsHandler, stac
var header_buf = std.ArrayList(u8).init(ctx.allocator);
defer header_buf.deinit();
- const res = try zzz.websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer());
+ //const res = try zzz.websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer());
+ const res = try zzz.websocket.upgrade(&ctx.socket, ctx.runtime, key, ext, header_buf.writer());
_ = try ctx.socket.send_all(ctx.runtime, header_buf.items);
- const session = try std.heap.page_allocator.create(WsSession);
- session.* = WsSession.init(std.heap.page_allocator, res.conn, user_handler);
+ //const session = try std.heap.page_allocator.create(WsSession);
+ //session.* = WsSession.init(std.heap.page_allocator, res.conn, user_handler);
+ const session = try ctx.runtime.allocator.create(WsSession);
+ session.* = WsSession.init(ctx.runtime.allocator, res.conn, user_handler);
session.conn.socket = &session.socket_owned; // use heap instead stack
session.conn.user_data = session; // for not use global maps that locks
@@ -291,15 +294,16 @@ pub fn handle_upgrade(ctx: *const zzz.Context, user_handler: UserWsHandler, stac
}
session.deinit();
- std.heap.page_allocator.destroy(session);
+ //std.heap.page_allocator.destroy(session);
+ ctx.runtime.allocator.destroy(session);
std.log.info("WebSocket Loop finished and memory freed", .{});
return .close;
};
}
- //zzz.websocket.runLoop(session.conn, internal_handler, ctx.runtime.allocator) catch |err| { // sync loop
- zzz.websocket.runLoop(session.conn, internal_handler, std.heap.page_allocator) catch |err| { // sync loop
+ zzz.websocket.runLoop(session.conn, internal_handler, ctx.runtime.allocator) catch |err| { // sync loop
+ //zzz.websocket.runLoop(session.conn, internal_handler, std.heap.page_allocator) catch |err| { // sync loop
if (err != error.Closed){
std.log.err("WebSocket RunLoop Error: {s}", .{ @errorName(err) });
}
@@ -321,7 +325,8 @@ pub fn handle_upgrade(ctx: *const zzz.Context, user_handler: UserWsHandler, stac
}
session.deinit();
- std.heap.page_allocator.destroy(session);
+ //std.heap.page_allocator.destroy(session);
+ ctx.runtime.allocator.destroy(session);
std.log.info("WebSocket Loop finished and memory freed", .{});
return .close;
diff --git a/src/websocket/websocket.zig b/src/websocket/websocket.zig
index f64c436..820b9df 100644
--- a/src/websocket/websocket.zig
+++ b/src/websocket/websocket.zig
@@ -103,7 +103,7 @@ pub const HandshakeResult = struct {
pub fn upgrade(
socket: *const SecureSocket,
runtime: *Runtime,
- allocator: Allocator,
+ //allocator: Allocator,
sec_websocket_key: []const u8,
sec_websocket_extensions: ?[]const u8,
response_writer: anytype,
@@ -120,12 +120,12 @@ pub fn upgrade(
// }
//}
- const accept = try computeAccept(allocator, sec_websocket_key);
+ const accept = computeAccept(sec_websocket_key);
try response_writer.writeAll("HTTP/1.1 101 Switching Protocols\r\n");
try response_writer.writeAll("Upgrade: websocket\r\n");
try response_writer.writeAll("Connection: Upgrade\r\n");
try response_writer.writeAll("Sec-WebSocket-Accept: ");
- try response_writer.writeAll(accept);
+ try response_writer.writeAll(&accept);
try response_writer.writeAll("\r\n");
//if (compression) {
// try response_writer.writeAll("Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n");
@@ -423,16 +423,18 @@ pub fn runLoop(conn: Conn, handler: Handler, allocator: Allocator) !void {
// helpers
-fn computeAccept(allocator: Allocator, key: []const u8) ![]const u8 {
+fn computeAccept(key: []const u8) [28]u8 {
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
var hasher = std.crypto.hash.Sha1.init(.{});
hasher.update(key);
hasher.update(magic);
+
var hash: [20]u8 = undefined;
hasher.final(&hash);
- var buf: [28]u8 = undefined;
- const encoded = std.base64.standard.Encoder.encode(&buf, &hash);
- return try allocator.dupe(u8, encoded);
+
+ var out_buf: [28]u8 = undefined;
+ _ = std.base64.standard.Encoder.encode(&out_buf, &hash);
+ return out_buf;
}
const OpCode = enum(u8) {
From dc8adeb93426ebe7b51e15629fc0804bb3765ba8 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Wed, 17 Jun 2026 14:14:56 +0300
Subject: [PATCH 22/26] cannot fix memleaks ((
---
build.zig | 4 +-
build.zig.zon | 10 +--
examples_ws/example_ws_1.zig | 36 +++++++++--
src/http/server.zig | 117 +++++++++++++++++++++++++----------
4 files changed, 120 insertions(+), 47 deletions(-)
diff --git a/build.zig b/build.zig
index 5bbec0c..eb5560a 100644
--- a/build.zig
+++ b/build.zig
@@ -2,8 +2,8 @@ const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
- //const optimize = b.standardOptimizeOption(.{}); // -O Debug
- const optimize = std.builtin.OptimizeMode.ReleaseFast; // -O ReleaseFast
+ const optimize = b.standardOptimizeOption(.{}); // -O Debug
+ //const optimize = std.builtin.OptimizeMode.ReleaseFast; // -O ReleaseFast
const zzz = b.addModule("zzz", .{
.root_source_file = b.path("src/lib.zig"),
diff --git a/build.zig.zon b/build.zig.zon
index ae04fa1..7e346d2 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -1,20 +1,20 @@
.{
.name = .zzz,
- .fingerprint = 0xc3273dca261a7ae0,
+ .fingerprint = 0xc3273dca46e69cd6,
.version = "0.3.0",
.minimum_zig_version = "0.14.1",
.dependencies = .{
.tardy = .{
//.url = "git+https://github.com/tardy-org/tardy?ref=v0.3.0#cd454060f3b6006368d53c05ab96cd16c73c34de",
//.hash = "tardy-0.3.0-69wrgi7PAwDFhO7m0aXae6N15s2b28VIOrnRrSHHake6",
- .url = "git+https://github.com/221V/tardy?ref=zig-0.14#d0364f3142cb859474cd23ccc9da1c97d9dc1e05",
- .hash = "tardy-0.3.0-zwolxXHNAwA6tH6Xr1qgUIAyHy6o4-hgOSVOlGBt6jjL",
+ .url = "git+https://github.com/221V/tardy?ref=zig-0.14#4e0e33d8c4bc666262ab8b1a11dccb2e6703ff78",
+ .hash = "tardy-0.3.0-zwolxQTOAwDsVB0HQYuUzUDDi8YzZApgKg7VXzQws9zq",
},
.secsock = .{
//.url = "git+https://github.com/tardy-org/secsock?ref=v0.1.0#263dcd630e32c7a5c7a0522a8d1fd04e39b75c24",
//.hash = "secsock-0.0.0-p0qurf09AQD95s1NQF2MGpBqMmFz7cKZWibsgv_SQBAr",
- .url = "git+https://github.com/221V/secsock?ref=zig-0.14#625147512dcacbfe5fa35cab2f94332ba76cd563",
- .hash = "secsock-0.0.0-p0qurcM-AQCPUYli5tuTIPncDh3bKpHE-yBEkY5Zma1E",
+ .url = "git+https://github.com/221V/secsock?ref=zig-0.14#3ee4405b051cbbb458a8f7ffaec305ea355a4ea2",
+ .hash = "secsock-0.0.0-p0qurcM-AQBHDulqDh4EfyXGzGEFmEOu06vI2HiajIQJ",
},
},
diff --git a/examples_ws/example_ws_1.zig b/examples_ws/example_ws_1.zig
index 0aa9c57..5aa6c23 100644
--- a/examples_ws/example_ws_1.zig
+++ b/examples_ws/example_ws_1.zig
@@ -160,7 +160,7 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond {
pub fn main() !void{
//@compileLog("STACK_SIZE = ", STACK_SIZE);
var gpa = std.heap.GeneralPurposeAllocator(.{
- .thread_safe = true,
+ //.thread_safe = true,
.stack_trace_frames = 8,
}){};
const allocator = gpa.allocator();
@@ -175,7 +175,9 @@ pub fn main() !void{
try socket.bind();
try socket.listen(1024); // max conn count that are waiting in accept queue
- var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{});
+ var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{
+ .threading = .single, // for debug
+ });
defer tardy.deinit();
const home_route = zzz.HTTP.Route.init("/").get({}, on_request);
@@ -188,19 +190,41 @@ pub fn main() !void{
const router = try zzz.Router.init(allocator, layers, .{ .not_found = on_request });
// no defer router - lifetime per server work // defer router.deinit(allocator);
+ var server = zzz.Server.init(.{ .stack_size = STACK_SIZE });
+
const Entry_Params = struct {
- config: zzz.ServerConfig,
+ server: *zzz.Server,
+ //config: zzz.ServerConfig,
router: *const zzz.Router,
socket: Socket,
};
try tardy.entry(
- Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = socket },
+ Entry_Params{ .server = &server, .router = &router, .socket = socket },
+ //Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = socket },
struct {
fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void {
- var server = zzz.Server.init(p.config);
- try server.serve(rt, p.router, .{ .normal = p.socket });
+ //var server = zzz.Server.init(p.config);
+ //try server.serve(rt, p.router, .{ .normal = p.socket });
+ try p.server.serve(rt, p.router, .{ .normal = p.socket });
+
+ //try rt.spawn(.{rt, &server}, struct {
+ try rt.spawn(.{rt, p.server}, struct {
+ fn shutdown_timer(runtime: *zzz.tardy.Runtime, server1: *zzz.Server) !void {
+ try zzz.tardy.Timer.delay(runtime, .{ .seconds = 30 });
+ std.log.info("AUTO-SHUTDOWN FOR LEAK CHECK...", .{});
+ runtime.stop();
+
+ _ = server1;
+ //server1.deinit();
+ //std.posix.exit(0);
+ }
+ }.shutdown_timer, 16 * 1024);
+
} // end fn entry
}.entry);
+
+ server.deinit();
+ std.log.info("Done. GPA report now.", .{});
}
diff --git a/src/http/server.zig b/src/http/server.zig
index 71c1761..bbad9c7 100644
--- a/src/http/server.zig
+++ b/src/http/server.zig
@@ -150,14 +150,49 @@ pub const Provision = struct {
pub const Server = struct {
const Self = @This();
config: ServerConfig,
+
+ provision_pool: ?*Pool(Provision) = null,
+ connection_count: ?*usize = null,
+ accept_queued: ?*bool = null,
+ allocator: ?std.mem.Allocator = null,
pub fn init(config: ServerConfig) Self {
return Self{ .config = config };
}
- pub fn deinit(self: *const Self) void {
- if (self.tls_ctx) |tls| {
- tls.deinit();
+ //pub fn deinit(self: *const Self) void {
+ pub fn deinit(self: *Self) void {
+ //if (self.tls_ctx) |tls| {
+ // tls.deinit();
+ //}
+ const alloc = self.allocator orelse return;
+
+ if (self.provision_pool) |pool| {
+ for (pool.items) |*provision| {
+ if (provision.initalized) {
+ provision.zc_recv_buffer.deinit();
+ provision.header_buffer.deinit();
+ provision.request.deinit();
+ provision.response.deinit();
+ provision.queries.deinit();
+ provision.storage.deinit();
+ provision.arena.deinit();
+ alloc.free(provision.captures);
+ provision.initalized = false;
+ }
+ }
+ pool.deinit();
+ alloc.destroy(pool);
+ self.provision_pool = null;
+ }
+
+ if (self.connection_count) |ptr| {
+ alloc.destroy(ptr);
+ self.connection_count = null;
+ }
+ if (self.accept_queued) |ptr| {
+ alloc.destroy(ptr);
+ self.accept_queued = null;
}
}
@@ -237,33 +272,37 @@ pub const Server = struct {
const index = try provisions.borrow();
defer provisions.release(index);
const provision = provisions.get_ptr(index);
+ log.debug("DEBUG CONN START: Slot={d} ActiveTasks={d}", .{index, rt.scheduler.runnable});
defer {
- log.debug("DEBUG: Releasing slot {d} and resetting arena", .{index});
+ log.debug("DEBUG CONN END: Slot={d} Releasing resources", .{index});
+ //log.debug("DEBUG: Releasing slot {d} and resetting arena", .{index});
provision.request.clear();
provision.response.clear();
provision.storage.clear();
- _ = provision.arena.reset(.retain_capacity);
+ //_ = provision.arena.reset(.retain_capacity);
+ _ = provision.arena.reset(.free_all);
}
// if we are growing, we can handle a newly allocated provision here.
// otherwise, it should be initalized.
- if (!provision.initalized) {
- log.debug("initalizing new provision", .{});
- provision.zc_recv_buffer = ZeroCopy(u8).init(rt.allocator, config.socket_buffer_bytes) catch {
- @panic("attempting to allocate more memory than available. (ZeroCopyBuffer)");
- };
- provision.header_buffer = std.ArrayList(u8).init(rt.allocator);
- provision.arena = std.heap.ArenaAllocator.init(rt.allocator);
- provision.captures = rt.allocator.alloc(Capture, config.capture_count_max) catch {
- @panic("attempting to allocate more memory than available. (Captures)");
- };
- provision.queries = AnyCaseStringMap.init(rt.allocator);
- provision.storage = TypedStorage.init(rt.allocator);
- provision.request = Request.init(rt.allocator);
- provision.response = Response.init(rt.allocator);
- provision.initalized = true;
- }
+ //if (!provision.initalized) {
+ //log.debug("initalizing new provision", .{});
+ //provision.zc_recv_buffer = ZeroCopy(u8).init(rt.allocator, config.socket_buffer_bytes) catch {
+ // @panic("attempting to allocate more memory than available. (ZeroCopyBuffer)");
+ //};
+ //provision.header_buffer = std.ArrayList(u8).init(rt.allocator);
+ //provision.arena = std.heap.ArenaAllocator.init(rt.allocator);
+ //provision.captures = rt.allocator.alloc(Capture, config.capture_count_max) catch {
+ // @panic("attempting to allocate more memory than available. (Captures)");
+ //};
+ //provision.queries = AnyCaseStringMap.init(rt.allocator);
+ //provision.storage = TypedStorage.init(rt.allocator);
+ //provision.request = Request.init(rt.allocator);
+ //provision.response = Response.init(rt.allocator);
+ ////provision.initalized = true; // what if here we got double init? first time in serve
+ //}
+ assert(provision.initalized);
defer prepare_new_request(null, provision, config) catch unreachable;
var state: State = .{ .request = .header };
@@ -541,20 +580,27 @@ pub const Server = struct {
const count = self.config.connection_count_max orelse 1024;
const pooling: PoolKind = if (self.config.connection_count_max == null) .grow else .static;
- const provision_pool = try rt.allocator.create(Pool(Provision));
- provision_pool.* = try Pool(Provision).init(rt.allocator, count, pooling);
- errdefer rt.allocator.destroy(provision_pool);
+ //const provision_pool = try rt.allocator.create(Pool(Provision));
+ //provision_pool.* = try Pool(Provision).init(rt.allocator, count, pooling);
+ //errdefer rt.allocator.destroy(provision_pool);
+ self.provision_pool = try rt.allocator.create(Pool(Provision));
+ self.provision_pool.?.* = try Pool(Provision).init(rt.allocator, count, pooling);
- const connection_count = try rt.allocator.create(usize);
- errdefer rt.allocator.destroy(connection_count);
- connection_count.* = 0;
+ //const connection_count = try rt.allocator.create(usize);
+ //errdefer rt.allocator.destroy(connection_count);
+ //connection_count.* = 0;
+ self.connection_count = try rt.allocator.create(usize);
+ self.connection_count.?.* = 0;
- const accept_queued = try rt.allocator.create(bool);
- errdefer rt.allocator.destroy(accept_queued);
- accept_queued.* = true;
+ //const accept_queued = try rt.allocator.create(bool);
+ //errdefer rt.allocator.destroy(accept_queued);
+ //accept_queued.* = true;
+ self.accept_queued = try rt.allocator.create(bool);
+ self.accept_queued.?.* = true;
// initialize first batch of provisions :)
- for (provision_pool.items) |*provision| {
+ //for (provision_pool.items) |*provision| {
+ for (self.provision_pool.?.items) |*provision| {
provision.initalized = true;
provision.zc_recv_buffer = ZeroCopy(u8).init(
rt.allocator,
@@ -579,9 +625,12 @@ pub const Server = struct {
self.config,
router,
secure,
- provision_pool,
- connection_count,
- accept_queued,
+ //provision_pool,
+ self.provision_pool.?,
+ //connection_count,
+ self.connection_count.?,
+ //accept_queued,
+ self.accept_queued.?,
},
main_frame,
self.config.stack_size,
From e0db5242fdf149a650b5b81e434c74aafc36bb12 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Wed, 17 Jun 2026 14:18:33 +0300
Subject: [PATCH 23/26] refactoring example 2
---
examples_ws/example_ws_2.zig | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/examples_ws/example_ws_2.zig b/examples_ws/example_ws_2.zig
index 1c45d95..1c25ff8 100644
--- a/examples_ws/example_ws_2.zig
+++ b/examples_ws/example_ws_2.zig
@@ -197,18 +197,23 @@ pub fn main() !void{
var tardy = try TardyType.init(allocator, .{});
defer tardy.deinit();
+ var server = zzz.Server.init(.{ .stack_size = STACK_SIZE });
+
const Entry_Params = struct {
- config: zzz.ServerConfig,
+ //config: zzz.ServerConfig,
+ server: *zzz.Server,
router: *const zzz.Router,
socket: zzz.secsock.SecureSocket,
};
try tardy.entry(
- Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = secure_socket },
+ //Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = secure_socket },
+ Entry_Params{ .server = &server, .router = &router, .socket = secure_socket },
struct {
fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void {
- var server = zzz.Server.init(p.config);
- try server.serve(rt, p.router, .{ .secure = p.socket });
+ //var server = zzz.Server.init(p.config);
+ //try server.serve(rt, p.router, .{ .secure = p.socket });
+ try p.server.serve(rt, p.router, .{ .secure = p.socket });
} // end fn entry
}.entry);
}
From 9a20b8c09bfa48e0bd2155c8f04f41b3d771c332 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Wed, 17 Jun 2026 14:26:31 +0300
Subject: [PATCH 24/26] refactoring ws example 3
---
examples_ws/example_ws_3.zig | 60 ++++++++++++++----------------------
1 file changed, 23 insertions(+), 37 deletions(-)
diff --git a/examples_ws/example_ws_3.zig b/examples_ws/example_ws_3.zig
index 984589c..baddc0a 100644
--- a/examples_ws/example_ws_3.zig
+++ b/examples_ws/example_ws_3.zig
@@ -141,43 +141,29 @@ pub fn main() !void{
//var tardy = try TardyType.init(allocator, .{ .threading = .single });
defer tardy.deinit();
- try tardy.entry(&socket, struct {
- fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void {
- const config = zzz.ServerConfig{
- .stack_size = STACK_SIZE, // stack for http requests
- };
-
- const home_route = zzz.HTTP.Route.init("/").get({}, on_request);
- const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint);
- const layers = &[_]zzz.HTTP.Layer{
- home_route.layer(),
- ws_route.layer(),
- };
-
- const router = try rt.allocator.create(zzz.Router);
- router.* = try zzz.Router.init(rt.allocator, layers, .{
- .not_found = on_request,
- });
- // no defer router - lifetime per server work
-
- const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap instead of stack
- provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static);
-
- const byte_count = provisions.items.len * @sizeOf(zzz.Provision); // set zeros -- initialized = false
- @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0);
-
- const connection_count = try rt.allocator.create(usize); // use heap instead stack
- connection_count.* = 0;
-
- const accept_queued = try rt.allocator.create(bool);
- accept_queued.* = false;
-
- try rt.spawn(
- .{ rt, config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, connection_count, accept_queued },
- zzz.Server.main_frame,
- config.stack_size
- );
-
+ const home_route = zzz.HTTP.Route.init("/").get({}, on_request);
+ const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint);
+ const layers = &[_]zzz.HTTP.Layer{
+ home_route.layer(),
+ ws_route.layer(),
+ };
+
+ const router = try zzz.Router.init(allocator, layers, .{ .not_found = on_request });
+ // no defer router - lifetime per server work
+
+ var server = zzz.Server.init(.{ .stack_size = STACK_SIZE }); // stack for http requests
+
+ const Entry_Params = struct {
+ server: *zzz.Server,
+ router: *const zzz.Router,
+ socket: Socket,
+ };
+
+ try tardy.entry(
+ Entry_Params{ .server = &server, .router = &router, .socket = socket },
+ struct {
+ fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void {
+ try p.server.serve(rt, p.router, .{ .normal = p.socket });
} // end fn entry
}.entry);
}
From de8be00f8bb0a881425bac8e6db53f4242cfb6a2 Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Wed, 17 Jun 2026 14:34:14 +0300
Subject: [PATCH 25/26] refactoring ws example 4
---
examples_ws/example_ws_2.zig | 3 +-
examples_ws/example_ws_3.zig | 3 +-
examples_ws/example_ws_4.zig | 63 ++++++++++++++++--------------------
3 files changed, 29 insertions(+), 40 deletions(-)
diff --git a/examples_ws/example_ws_2.zig b/examples_ws/example_ws_2.zig
index 1c25ff8..38864e3 100644
--- a/examples_ws/example_ws_2.zig
+++ b/examples_ws/example_ws_2.zig
@@ -193,8 +193,7 @@ pub fn main() !void{
const secure_socket = try bearssl.to_secure_socket(socket, .server);
defer secure_socket.deinit();
- const TardyType = zzz.tardy.Tardy(.auto);
- var tardy = try TardyType.init(allocator, .{});
+ var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{});
defer tardy.deinit();
var server = zzz.Server.init(.{ .stack_size = STACK_SIZE });
diff --git a/examples_ws/example_ws_3.zig b/examples_ws/example_ws_3.zig
index baddc0a..d07f14a 100644
--- a/examples_ws/example_ws_3.zig
+++ b/examples_ws/example_ws_3.zig
@@ -136,8 +136,7 @@ pub fn main() !void{
try socket.bind();
try socket.listen(1024); // max conn count that are waiting in accept queue
- const TardyType = zzz.tardy.Tardy(.auto);
- var tardy = try TardyType.init(allocator, .{});
+ var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{});
//var tardy = try TardyType.init(allocator, .{ .threading = .single });
defer tardy.deinit();
diff --git a/examples_ws/example_ws_4.zig b/examples_ws/example_ws_4.zig
index 10a68d9..6f62d68 100644
--- a/examples_ws/example_ws_4.zig
+++ b/examples_ws/example_ws_4.zig
@@ -505,44 +505,35 @@ pub fn main() !void {
try socket.bind();
try socket.listen(1024);
- const TardyType = zzz.tardy.Tardy(.auto);
- var tardy = try TardyType.init(allocator, .{});
+ const home_route = zzz.HTTP.Route.init("/").get({}, on_request);
+ const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint);
+
+ //const js_route = zzz.HTTP.FsDir.serve("/static", zzz.tardy.Dir.cwd()); // bert_ftp.js
+ const std_static_dir = try std.fs.cwd().openDir("examples_ws/static", .{ .iterate = true });
+ const static_dir = zzz.tardy.Dir.from_std(std_static_dir);
+ const js_route = zzz.HTTP.FsDir.serve("/static", static_dir); // bert_ftp.js
+
+ const layers = &[_]zzz.HTTP.Layer{ home_route.layer(), ws_route.layer(), js_route };
+
+ const router = try zzz.Router.init(allocator, layers, .{ .not_found = on_request });
+
+ var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{});
defer tardy.deinit();
+
+ var server = zzz.Server.init(.{ .stack_size = WS_STACK_SIZE });
+
+ const Entry_Params = struct {
+ server: *zzz.Server,
+ router: *const zzz.Router,
+ socket: Socket,
+ };
+
- try tardy.entry(&socket, struct {
- fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void {
- const server_config = zzz.ServerConfig{ .stack_size = 64 * 1024 };
-
- const home_route = zzz.HTTP.Route.init("/").get({}, on_request);
- const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint);
-
- //const js_route = zzz.HTTP.FsDir.serve("/static", zzz.tardy.Dir.cwd()); // bert_ftp.js
- const std_static_dir = try std.fs.cwd().openDir("examples_ws/static", .{ .iterate = true });
- const static_dir = zzz.tardy.Dir.from_std(std_static_dir);
- const js_route = zzz.HTTP.FsDir.serve("/static", static_dir); // bert_ftp.js
-
- const layers = &[_]zzz.HTTP.Layer{ home_route.layer(), ws_route.layer(), js_route };
-
- const router = try rt.allocator.create(zzz.Router);
- router.* = try zzz.Router.init(rt.allocator, layers, .{ .not_found = on_request });
-
- const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision));
- provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static);
- const byte_count = provisions.items.len * @sizeOf(zzz.Provision);
- @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0);
-
- const connection_count = try rt.allocator.create(usize);
- connection_count.* = 0;
- const accept_queued = try rt.allocator.create(bool);
- accept_queued.* = false;
-
- try rt.spawn(
- .{ rt, server_config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, connection_count, accept_queued },
- zzz.Server.main_frame,
- server_config.stack_size
- );
-
- //try rt.spawn(.{rt}, cleanup_task, 64 * 1024);
+ try tardy.entry(
+ Entry_Params{ .server = &server, .router = &router, .socket = socket },
+ struct {
+ fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void {
+ try p.server.serve(rt, p.router, .{ .normal = p.socket });
}
}.entry);
}
From ec7807647f906c36bad9543fc815e884372668fb Mon Sep 17 00:00:00 2001
From: 221V <221v92@gmail.com>
Date: Wed, 17 Jun 2026 14:37:41 +0300
Subject: [PATCH 26/26] refactoring http example rest
---
examples_http/rest/main.zig | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/examples_http/rest/main.zig b/examples_http/rest/main.zig
index 3d9196f..d4cce54 100644
--- a/examples_http/rest/main.zig
+++ b/examples_http/rest/main.zig
@@ -120,18 +120,23 @@ pub fn main() !void {
http.Route.init("/").get({}, on_request).post({}, on_request).layer(),
}, .{ .not_found = on_request });
+ var server = zzz.Server.init(.{ .stack_size = STACK_SIZE }); // stack for http requests
+
const Entry_Params = struct {
- config: zzz.ServerConfig,
+ //config: zzz.ServerConfig,
+ server: *zzz.Server,
router: *const http.Router,
socket: Socket,
};
try tardy.entry(
- Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = socket },
+ //Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = socket },
+ Entry_Params{ .server = &server, .router = &router, .socket = socket },
struct {
fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void {
- var server = zzz.Server.init(p.config);
- try server.serve(rt, p.router, .{ .normal = p.socket });
+ //var server = zzz.Server.init(p.config);
+ //try server.serve(rt, p.router, .{ .normal = p.socket });
+ try p.server.serve(rt, p.router, .{ .normal = p.socket });
} // end fn entry
}.entry);
}