diff --git a/README.md b/README.md index 4bf6840..992c9b6 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ Here's what works: itself query parameters of all supported types. - **[cookies](examples/cookies/cookies.zig)**: a simple example sending itself a cookie and responding with a session cookie. +- **[websockets](examples/websockets/websockets.zig)**: a simple websockets chat +for the browser. I'll continue wrapping more of facil.io's functionality and adding stuff to zap diff --git a/build.zig b/build.zig index 0c7b4db..7ae9697 100644 --- a/build.zig +++ b/build.zig @@ -52,6 +52,7 @@ pub fn build(b: *std.build.Builder) !void { .{ .name = "endpoint_auth", .src = "examples/endpoint_auth/endpoint_auth.zig" }, .{ .name = "http_params", .src = "examples/http_params/http_params.zig" }, .{ .name = "cookies", .src = "examples/cookies/cookies.zig" }, + .{ .name = "websockets", .src = "examples/websockets/websockets.zig" }, }) |excfg| { const ex_name = excfg.name; const ex_src = excfg.src; diff --git a/build.zig.zon b/build.zig.zon index 841ee35..66b6566 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,12 +1,11 @@ .{ .name = "zap", - .version = "0.0.15", + .version = "0.0.16", .dependencies = .{ .@"facil.io" = .{ - .url = "https://github.com/zigzap/facil.io/archive/refs/tags/zap-0.0.7.tar.gz", - .hash = "1220d03e0579bbb726efb8224ea289b26227bc421158b45c1b16a60b31bfa400ab33", - + .url = "https://github.com/zigzap/facil.io/archive/refs/tags/zap-0.0.8.tar.gz", + .hash = "122071fcc675e114941331726291ca1f0c0c33751d992782c6abf1f0f2ddddc5734d", } } } diff --git a/examples/websockets/frontend/index.html b/examples/websockets/frontend/index.html new file mode 100644 index 0000000..9a15c86 --- /dev/null +++ b/examples/websockets/frontend/index.html @@ -0,0 +1,209 @@ + + +
+ + + + +[^\r]+?<\/pre>)/gm,function(e,r){return r.replace(/^ /gm,"¨0").replace(/¨0/g,"")}),x.subParser("hashBlock")("","gim"),e=s.converter._dispatch("hashPreCodeTags.after",e,n,s)}),x.subParser("headers",function(e,n,s){"use strict";e=s.converter._dispatch("headers.before",e,n,s);var o=isNaN(parseInt(n.headerLevelStart))?1:parseInt(n.headerLevelStart),r=n.smoothLivePreview?/^(.+)[ \t]*\n={2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n=+[ \t]*\n+/gm,t=n.smoothLivePreview?/^(.+)[ \t]*\n-{2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n-+[ \t]*\n+/gm,r=(e=(e=e.replace(r,function(e,r){var t=x.subParser("spanGamut")(r,n,s),r=n.noHeaderId?"":' id="'+i(r)+'"',r="\n"+e+"\n",r,t)}),e=t.converter._dispatch("blockQuotes.after",e,r,t)}),x.subParser("codeBlocks",function(e,n,s){"use strict";e=s.converter._dispatch("codeBlocks.before",e,n,s);return e=(e=(e+="¨0").replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=¨0))/g,function(e,r,t){var a="\n",r=x.subParser("outdent")(r,n,s);return r=x.subParser("encodeCode")(r,n,s),r="",x.subParser("hashBlock")(r,n,s)+t})).replace(/¨0/,""),e=s.converter._dispatch("codeBlocks.after",e,n,s)}),x.subParser("codeSpans",function(e,n,s){"use strict";return e=(e=void 0===(e=s.converter._dispatch("codeSpans.before",e,n,s))?"":e).replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,function(e,r,t,a){return a=(a=a.replace(/^([ \t]*)/g,"")).replace(/[ \t]*$/g,""),a=r+""+(r=(r=(r=x.subParser("detab")(r,n,s)).replace(/^\n+/g,"")).replace(/\n+$/g,""))+(a=n.omitExtraWLInCodeBlocks?"":a)+"
"+(a=x.subParser("encodeCode")(a,n,s))+"
",a=x.subParser("hashHTMLSpans")(a,n,s)}),e=s.converter._dispatch("codeSpans.after",e,n,s)}),x.subParser("completeHTMLDocument",function(e,r,t){"use strict";if(!r.completeHTMLDocument)return e;e=t.converter._dispatch("completeHTMLDocument.before",e,r,t);var a,n="html",s="\n",o="",i='\n',l="",c="";for(a in void 0!==t.metadata.parsed.doctype&&(s="\n","html"!==(n=t.metadata.parsed.doctype.toString().toLowerCase())&&"html5"!==n||(i='')),t.metadata.parsed)if(t.metadata.parsed.hasOwnProperty(a))switch(a.toLowerCase()){case"doctype":break;case"title":o=""+t.metadata.parsed.title+" \n";break;case"charset":i="html"===n||"html5"===n?'\n':'\n';break;case"language":case"lang":l=' lang="'+t.metadata.parsed[a]+'"',c+='\n';break;default:c+='\n'}return e=s+"\n\n"+o+i+c+"\n\n"+e.trim()+"\n\n",e=t.converter._dispatch("completeHTMLDocument.after",e,r,t)}),x.subParser("detab",function(e,r,t){"use strict";return e=(e=(e=(e=(e=(e=t.converter._dispatch("detab.before",e,r,t)).replace(/\t(?=\t)/g," ")).replace(/\t/g,"¨A¨B")).replace(/¨B(.+?)¨A/g,function(e,r){for(var t=r,a=4-t.length%4,n=0;n/g,">"),e=t.converter._dispatch("encodeAmpsAndAngles.after",e,r,t)}),x.subParser("encodeBackslashEscapes",function(e,r,t){"use strict";return e=(e=(e=t.converter._dispatch("encodeBackslashEscapes.before",e,r,t)).replace(/\\(\\)/g,x.helper.escapeCharactersCallback)).replace(/\\([`*_{}\[\]()>#+.!~=|:-])/g,x.helper.escapeCharactersCallback),e=t.converter._dispatch("encodeBackslashEscapes.after",e,r,t)}),x.subParser("encodeCode",function(e,r,t){"use strict";return e=(e=t.converter._dispatch("encodeCode.before",e,r,t)).replace(/&/g,"&").replace(//g,">").replace(/([*_{}\[\]\\=~-])/g,x.helper.escapeCharactersCallback),e=t.converter._dispatch("encodeCode.after",e,r,t)}),x.subParser("escapeSpecialCharsWithinTagAttributes",function(e,r,t){"use strict";return e=(e=(e=t.converter._dispatch("escapeSpecialCharsWithinTagAttributes.before",e,r,t)).replace(/<\/?[a-z\d_:-]+(?:[\s]+[\s\S]+?)?>/gi,function(e){return e.replace(/(.)<\/?code>(?=.)/g,"$1`").replace(/([\\`*_~=|])/g,x.helper.escapeCharactersCallback)})).replace(/-]|-[^>])(?:[^-]|-[^-])*)--)>/gi,function(e){return e.replace(/([\\`*_~=|])/g,x.helper.escapeCharactersCallback)}),e=t.converter._dispatch("escapeSpecialCharsWithinTagAttributes.after",e,r,t)}),x.subParser("githubCodeBlocks",function(e,s,o){"use strict";return s.ghCodeBlocks?(e=o.converter._dispatch("githubCodeBlocks.before",e,s,o),e=(e=(e+="¨0").replace(/(?:^|\n)(?: {0,3})(```+|~~~+)(?: *)([^\s`~]*)\n([\s\S]*?)\n(?: {0,3})\1/g,function(e,r,t,a){var n=s.omitExtraWLInCodeBlocks?"":"\n";return a=x.subParser("encodeCode")(a,s,o),a="",a=x.subParser("hashBlock")(a,s,o),"\n\n¨G"+(o.ghCodeBlocks.push({text:e,codeblock:a})-1)+"G\n\n"})).replace(/¨0/,""),o.converter._dispatch("githubCodeBlocks.after",e,s,o)):e}),x.subParser("hashBlock",function(e,r,t){"use strict";return e=(e=t.converter._dispatch("hashBlock.before",e,r,t)).replace(/(^\n+|\n+$)/g,""),e="\n\n¨K"+(t.gHtmlBlocks.push(e)-1)+"K\n\n",e=t.converter._dispatch("hashBlock.after",e,r,t)}),x.subParser("hashCodeTags",function(e,n,s){"use strict";e=s.converter._dispatch("hashCodeTags.before",e,n,s);return e=x.helper.replaceRecursiveRegExp(e,function(e,r,t,a){t=t+x.subParser("encodeCode")(r,n,s)+a;return"¨C"+(s.gHtmlSpans.push(t)-1)+"C"},""+(a=(a=(a=x.subParser("detab")(a,s,o)).replace(/^\n+/g,"")).replace(/\n+$/g,""))+n+"
]*>","
","gim"),e=s.converter._dispatch("hashCodeTags.after",e,n,s)}),x.subParser("hashElement",function(e,r,t){"use strict";return function(e,r){return r=(r=(r=r.replace(/\n\n/g,"\n")).replace(/^\n/,"")).replace(/\n+$/g,""),r="\n\n¨K"+(t.gHtmlBlocks.push(r)-1)+"K\n\n"}}),x.subParser("hashHTMLBlocks",function(e,r,n){"use strict";e=n.converter._dispatch("hashHTMLBlocks.before",e,r,n);function t(e,r,t,a){return-1!==t.search(/\bmarkdown\b/)&&(e=t+n.converter.makeHtml(r)+a),"\n\n¨K"+(n.gHtmlBlocks.push(e)-1)+"K\n\n"}var a=["pre","div","h1","h2","h3","h4","h5","h6","blockquote","table","dl","ol","ul","script","noscript","form","fieldset","iframe","math","style","section","header","footer","nav","article","aside","address","audio","canvas","figure","hgroup","output","video","p"];r.backslashEscapesHTMLTags&&(e=e.replace(/\\<(\/?[^>]+?)>/g,function(e,r){return"<"+r+">"}));for(var s=0;s]*>)","im"),i="<"+a[s]+"\\b[^>]*>",l=""+a[s]+">";-1!==(c=x.helper.regexIndexOf(e,o));){var c=x.helper.splitAtIndex(e,c),u=x.helper.replaceRecursiveRegExp(c[1],t,i,l,"im");if(u===c[1])break;e=c[0].concat(u)}return e=e.replace(/(\n {0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,x.subParser("hashElement")(e,r,n)),e=(e=x.helper.replaceRecursiveRegExp(e,function(e){return"\n\n¨K"+(n.gHtmlBlocks.push(e)-1)+"K\n\n"},"^ {0,3}\x3c!--","--\x3e","gm")).replace(/(?:\n\n)( {0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,x.subParser("hashElement")(e,r,n)),e=n.converter._dispatch("hashHTMLBlocks.after",e,r,n)}),x.subParser("hashHTMLSpans",function(e,r,t){"use strict";function a(e){return"¨C"+(t.gHtmlSpans.push(e)-1)+"C"}return e=(e=(e=(e=(e=t.converter._dispatch("hashHTMLSpans.before",e,r,t)).replace(/<[^>]+?\/>/gi,a)).replace(/<([^>]+?)>[\s\S]*?<\/\1>/g,a)).replace(/<([^>]+?)\s[^>]+?>[\s\S]*?<\/\1>/g,a)).replace(/<[^>]+?>/gi,a),e=t.converter._dispatch("hashHTMLSpans.after",e,r,t)}),x.subParser("unhashHTMLSpans",function(e,r,t){"use strict";e=t.converter._dispatch("unhashHTMLSpans.before",e,r,t);for(var a=0;a ]*>\\s* ]*>","^ {0,3}
\\s*
]*>/.test(c)&&(u=!0)}n[o]=c}return e=(e=(e=n.join("\n")).replace(/^\n+/g,"")).replace(/\n+$/g,""),t.converter._dispatch("paragraphs.after",e,r,t)}),x.subParser("runExtension",function(e,r,t,a){"use strict";return e.filter?r=e.filter(r,a.converter,t):e.regex&&((a=e.regex)instanceof RegExp||(a=new RegExp(a,"g")),r=r.replace(a,e.replace)),r}),x.subParser("spanGamut",function(e,r,t){"use strict";return e=t.converter._dispatch("spanGamut.before",e,r,t),e=x.subParser("codeSpans")(e,r,t),e=x.subParser("escapeSpecialCharsWithinTagAttributes")(e,r,t),e=x.subParser("encodeBackslashEscapes")(e,r,t),e=x.subParser("images")(e,r,t),e=x.subParser("anchors")(e,r,t),e=x.subParser("autoLinks")(e,r,t),e=x.subParser("simplifiedAutoLinks")(e,r,t),e=x.subParser("emoji")(e,r,t),e=x.subParser("underline")(e,r,t),e=x.subParser("italicsAndBold")(e,r,t),e=x.subParser("strikethrough")(e,r,t),e=x.subParser("ellipsis")(e,r,t),e=x.subParser("hashHTMLSpans")(e,r,t),e=x.subParser("encodeAmpsAndAngles")(e,r,t),r.simpleLineBreaks?/\n\n¨K/.test(e)||(e=e.replace(/\n+/g,"
\n")):e=e.replace(/ +\n/g,"
\n"),e=t.converter._dispatch("spanGamut.after",e,r,t)}),x.subParser("strikethrough",function(e,t,a){"use strict";return t.strikethrough&&(e=(e=a.converter._dispatch("strikethrough.before",e,t,a)).replace(/(?:~){2}([\s\S]+?)(?:~){2}/g,function(e,r){return r=r,""+(r=t.simplifiedAutoLink?x.subParser("simplifiedAutoLinks")(r,t,a):r)+""}),e=a.converter._dispatch("strikethrough.after",e,t,a)),e}),x.subParser("stripLinkDefinitions",function(i,l,c){"use strict";function e(e,r,t,a,n,s,o){return r=r.toLowerCase(),i.toLowerCase().split(r).length-1<2?e:(t.match(/^data:.+?\/.+?;base64,/)?c.gUrls[r]=t.replace(/\s/g,""):c.gUrls[r]=x.subParser("encodeAmpsAndAngles")(t,l,c),s?s+o:(o&&(c.gTitles[r]=o.replace(/"|'/g,""")),l.parseImgDimensions&&a&&n&&(c.gDimensions[r]={width:a,height:n}),""))}return i=(i=(i=(i+="¨0").replace(/^ {0,3}\[([^\]]+)]:[ \t]*\n?[ \t]*(data:.+?\/.+?;base64,[A-Za-z0-9+/=\n]+?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n\n|(?=¨0)|(?=\n\[))/gm,e)).replace(/^ {0,3}\[([^\]]+)]:[ \t]*\n?[ \t]*([^>\s]+)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n+|(?=¨0))/gm,e)).replace(/¨0/,"")}),x.subParser("tables",function(e,y,P){"use strict";if(!y.tables)return e;function r(e){for(var r=e.split("\n"),t=0;t"+(n=x.subParser("spanGamut")(n,y,P))+"\n"));for(t=0;t"+x.subParser("spanGamut")(i,y,P)+"\n"));h.push(_)}for(var m=d,f=h,b="\n\n\n",w=m.length,k=0;k\n \n\n",k=0;k\n";for(var v=0;v\n"}return b+=" \n
\n"}return e=(e=(e=(e=P.converter._dispatch("tables.before",e,y,P)).replace(/\\(\|)/g,x.helper.escapeCharactersCallback)).replace(/^ {0,3}\|?.+\|.+\n {0,3}\|?[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*(?:[-=]){2,}[\s\S]+?(?:\n\n|¨0)/gm,r)).replace(/^ {0,3}\|.+\|[ \t]*\n {0,3}\|[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*\n( {0,3}\|.+\|[ \t]*\n)*(?:\n|¨0)/gm,r),e=P.converter._dispatch("tables.after",e,y,P)}),x.subParser("underline",function(e,r,t){"use strict";return r.underline?(e=t.converter._dispatch("underline.before",e,r,t),e=(e=r.literalMidWordUnderscores?(e=e.replace(/\b___(\S[\s\S]*?)___\b/g,function(e,r){return""+r+""})).replace(/\b__(\S[\s\S]*?)__\b/g,function(e,r){return""+r+""}):(e=e.replace(/___(\S[\s\S]*?)___/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/__(\S[\s\S]*?)__/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/(_)/g,x.helper.escapeCharactersCallback),t.converter._dispatch("underline.after",e,r,t)):e}),x.subParser("unescapeSpecialChars",function(e,r,t){"use strict";return e=(e=t.converter._dispatch("unescapeSpecialChars.before",e,r,t)).replace(/¨E(\d+)E/g,function(e,r){r=parseInt(r);return String.fromCharCode(r)}),e=t.converter._dispatch("unescapeSpecialChars.after",e,r,t)}),x.subParser("makeMarkdown.blockquote",function(e,r){"use strict";var t="";if(e.hasChildNodes())for(var a=e.childNodes,n=a.length,s=0;s ")}),x.subParser("makeMarkdown.codeBlock",function(e,r){"use strict";var t=e.getAttribute("language"),e=e.getAttribute("precodenum");return"```"+t+"\n"+r.preList[e]+"\n```"}),x.subParser("makeMarkdown.codeSpan",function(e){"use strict";return"`"+e.innerHTML+"`"}),x.subParser("makeMarkdown.emphasis",function(e,r){"use strict";var t="";if(e.hasChildNodes()){t+="*";for(var a=e.childNodes,n=a.length,s=0;s",e.hasAttribute("width")&&e.hasAttribute("height")&&(r+=" ="+e.getAttribute("width")+"x"+e.getAttribute("height")),e.hasAttribute("title")&&(r+=' "'+e.getAttribute("title")+'"'),r+=")"),r}),x.subParser("makeMarkdown.links",function(e,r){"use strict";var t="";if(e.hasChildNodes()&&e.hasAttribute("href")){for(var a=e.childNodes,n=a.length,t="[",s=0;s"),e.hasAttribute("title")&&(t+=' "'+e.getAttribute("title")+'"'),t+=")"}return t}),x.subParser("makeMarkdown.list",function(e,r,t){"use strict";var a="";if(!e.hasChildNodes())return"";for(var n=e.childNodes,s=n.length,o=e.getAttribute("start")||1,i=0;i"+r.preList[e]+""}),x.subParser("makeMarkdown.strikethrough",function(e,r){"use strict";var t="";if(e.hasChildNodes()){t+="~~";for(var a=e.childNodes,n=a.length,s=0;str>th"),s=e.querySelectorAll("tbody>tr"),o=0;o/g,"\\$1>")).replace(/^#/gm,"\\#")).replace(/^(\s*)([-=]{3,})(\s*)$/,"$1\\$2$3")).replace(/^( {0,3}\d+)\./gm,"$1\\.")).replace(/^( {0,3})([+-])/gm,"$1\\$2")).replace(/]([\s]*)\(/g,"\\]$1\\(")).replace(/^ {0,3}\[([\S \t]*?)]:/gm,"\\[$1]:")});"function"==typeof define&&define.amd?define(function(){"use strict";return x}):"undefined"!=typeof module&&module.exports?module.exports=x:this.showdown=x}.call(this);
+//# sourceMappingURL=showdown.min.js.map
diff --git a/examples/websockets/websockets.zig b/examples/websockets/websockets.zig
new file mode 100644
index 0000000..0e5f66b
--- /dev/null
+++ b/examples/websockets/websockets.zig
@@ -0,0 +1,180 @@
+const std = @import("std");
+const zap = @import("zap");
+const WebSockets = zap.WebSockets;
+
+const Context = struct {
+ userName: []const u8,
+ channel: []const u8,
+ // we need to hold on to them and just re-use them for every incoming connection
+ subscribeArgs: WebsocketHandler.SubscribeArgs,
+ settings: WebsocketHandler.WebSocketSettings,
+};
+
+const ContextList = std.ArrayList(*Context);
+
+const ContextManager = struct {
+ allocator: std.mem.Allocator,
+ channel: []const u8,
+ usernamePrefix: []const u8,
+ lock: std.Thread.Mutex = .{},
+ contexts: ContextList = undefined,
+
+ const Self = @This();
+
+ pub fn init(allocator: std.mem.Allocator, channelName: []const u8, usernamePrefix: []const u8) Self {
+ return .{
+ .allocator = allocator,
+ .channel = channelName,
+ .usernamePrefix = usernamePrefix,
+ .contexts = ContextList.init(allocator),
+ };
+ }
+
+ pub fn deinit(self: *Self) void {
+ for (self.contexts.items) |ctx| {
+ self.allocator.free(ctx.userName);
+ }
+ self.contexts.deinit();
+ }
+
+ pub fn newContext(self: *Self) !*Context {
+ self.lock.lock();
+ defer self.lock.unlock();
+
+ var ctx = try self.allocator.create(Context);
+ var userName = try std.fmt.allocPrint(self.allocator, "{s}{d}", .{ self.usernamePrefix, self.contexts.items.len });
+ ctx.* = .{
+ .userName = userName,
+ .channel = self.channel,
+ // used in subscribe()
+ .subscribeArgs = .{
+ .channel = self.channel,
+ .force_text = true,
+ .context = ctx,
+ },
+ // used in upgrade()
+ .settings = .{
+ .on_open = on_open_websocket,
+ .on_close = on_close_websocket,
+ .on_message = handle_websocket_message,
+ .context = ctx,
+ },
+ };
+ try self.contexts.append(ctx);
+ return ctx;
+ }
+};
+
+//
+// Websocket Callbacks
+//
+fn on_open_websocket(context: ?*Context, handle: WebSockets.WsHandle) void {
+ if (context) |ctx| {
+ _ = WebsocketHandler.subscribe(handle, &ctx.subscribeArgs) catch |err| {
+ std.log.err("Error opening websocket: {any}", .{err});
+ return;
+ };
+
+ // say hello
+ var buf: [128]u8 = undefined;
+ const message = std.fmt.bufPrint(&buf, "{s} joined the chat.", .{ctx.userName}) catch unreachable;
+
+ // send notification to all others
+ WebsocketHandler.publish(.{ .channel = ctx.channel, .message = message });
+ std.log.info("new websocket opened: {s}", .{message});
+ }
+}
+
+fn on_close_websocket(context: ?*Context, uuid: isize) void {
+ _ = uuid;
+ if (context) |ctx| {
+ // say goodbye
+ var buf: [128]u8 = undefined;
+ const message = std.fmt.bufPrint(&buf, "{s} left the chat.", .{ctx.userName}) catch unreachable;
+
+ // send notification to all others
+ WebsocketHandler.publish(.{ .channel = ctx.channel, .message = message });
+ std.log.info("websocket closed: {s}", .{message});
+ }
+}
+fn handle_websocket_message(context: ?*Context, handle: WebSockets.WsHandle, message: []const u8, is_text: bool) void {
+ _ = is_text;
+ _ = handle;
+ if (context) |ctx| {
+ // say goodbye
+ var buf: [128]u8 = undefined;
+ const chat_message = std.fmt.bufPrint(&buf, "{s}: {s}", .{ ctx.userName, message }) catch unreachable;
+
+ // send notification to all others
+ WebsocketHandler.publish(.{ .channel = ctx.channel, .message = chat_message });
+ std.log.info("{s}", .{chat_message});
+ }
+}
+
+//
+// HTTP stuff
+//
+fn on_request(r: zap.SimpleRequest) void {
+ r.setHeader("Server", "zap.example") catch unreachable;
+ r.sendBody("This is a simple Websocket chatroom example
") catch return;
+}
+
+fn on_upgrade(r: zap.SimpleRequest, target_protocol: []const u8) void {
+ // make sure we're talking the right protocol
+ if (!std.mem.eql(u8, target_protocol, "websocket")) {
+ std.log.warn("received illegal target protocol: {s}", .{target_protocol});
+ r.setStatus(.bad_request);
+ r.sendBody("400 - BAD REQUEST") catch unreachable;
+ return;
+ }
+ var context = GlobalContextManager.newContext() catch |err| {
+ std.log.err("Error creating context: {any}", .{err});
+ return;
+ };
+
+ WebsocketHandler.upgrade(r.h, &context.settings) catch |err| {
+ std.log.err("Error in websocketUpgrade(): {any}", .{err});
+ return;
+ };
+ std.log.info("connection upgrade OK", .{});
+}
+
+// global variables, yeah!
+var GlobalContextManager: ContextManager = undefined;
+
+const WebsocketHandler = WebSockets.Handler(Context);
+var handler_instance: WebsocketHandler = .{};
+
+// here we go
+pub fn main() !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{
+ .thread_safe = true,
+ }){};
+ var allocator = gpa.allocator();
+
+ GlobalContextManager = ContextManager.init(allocator, "chatroom", "user-");
+ defer GlobalContextManager.deinit();
+
+ // setup listener
+ var listener = zap.SimpleHttpListener.init(
+ .{
+ .port = 3010,
+ .on_request = on_request,
+ .on_upgrade = on_upgrade,
+ .max_clients = 1000,
+ .max_body_size = 1 * 1024,
+ .public_folder = "examples/websockets/frontend",
+ .log = true,
+ },
+ );
+ try listener.listen();
+ std.log.info("", .{});
+ std.log.info("Connect with browser to http://localhost:3010.", .{});
+ std.log.info("Connect to websocket on ws://localhost:3010.", .{});
+ std.log.info("Terminate with CTRL+C", .{});
+
+ zap.start(.{
+ .threads = 1,
+ .workers = 1,
+ });
+}
diff --git a/src/fio.zig b/src/fio.zig
index 6346c85..22abfa3 100644
--- a/src/fio.zig
+++ b/src/fio.zig
@@ -362,6 +362,32 @@ pub const websocket_settings_s = extern struct {
on_close: ?*const fn (isize, ?*anyopaque) callconv(.C) void,
udata: ?*anyopaque,
};
+
+// struct websocket_subscribe_s_zigcompat {
+// ws_s *ws;
+// fio_str_info_s channel;
+// void (*on_message)(ws_s *ws, fio_str_info_s channel, fio_str_info_s msg, void *udata);
+// void (*on_unsubscribe)(void *udata);
+// void *udata;
+// fio_match_fn match;
+// unsigned char force_binary;
+// unsigned char force_text;
+// };
+
+pub const websocket_subscribe_s_zigcompat = extern struct {
+ ws: ?*ws_s,
+ channel: fio_str_info_s,
+ on_message: ?*const fn (?*ws_s, fio_str_info_s, fio_str_info_s, ?*anyopaque) callconv(.C) void,
+ on_unsubscribe: ?*const fn (?*anyopaque) callconv(.C) void,
+ udata: ?*anyopaque,
+ match: fio_match_fn,
+ force_binary: u8,
+ force_text: u8,
+};
+
+/// 0 on failure
+pub extern fn websocket_subscribe_zigcompat(websocket_subscribe_s_zigcompat) callconv(.C) usize;
+
pub extern fn http_upgrade2ws(http: [*c]http_s, websocket_settings_s) c_int;
pub extern fn websocket_connect(url: [*c]const u8, settings: websocket_settings_s) c_int;
pub extern fn websocket_attach(uuid: isize, http_settings: [*c]http_settings_s, args: [*c]websocket_settings_s, data: ?*anyopaque, length: usize) void;
@@ -375,6 +401,19 @@ pub const struct_websocket_subscribe_s = opaque {};
pub extern fn websocket_subscribe(args: struct_websocket_subscribe_s) usize;
pub extern fn websocket_unsubscribe(ws: ?*ws_s, subscription_id: usize) void;
pub extern fn websocket_optimize4broadcasts(@"type": isize, enable: c_int) void;
+
+pub extern fn fio_publish(args: fio_publish_args_s) void;
+pub const fio_publish_args_s = struct_fio_publish_args_s;
+pub const struct_fio_publish_args_s = extern struct {
+ engine: ?*anyopaque = null,
+ // we don't support engines other than default
+ // engine: [*c]const fio_pubsub_engine_s,
+ filter: i32 = 0,
+ channel: fio_str_info_s,
+ message: fio_str_info_s,
+ is_json: u8,
+};
+
pub const http_sse_s = struct_http_sse_s;
pub const struct_http_sse_s = extern struct {
on_open: ?*const fn ([*c]http_sse_s) callconv(.C) void,
diff --git a/src/websockets.zig b/src/websockets.zig
new file mode 100644
index 0000000..515132d
--- /dev/null
+++ b/src/websockets.zig
@@ -0,0 +1,207 @@
+const std = @import("std");
+const zap = @import("zap.zig");
+const fio = @import("fio.zig");
+const util = @import("util.zig");
+
+pub const WsHandle = ?*fio.ws_s;
+pub fn Handler(comptime ContextType: type) type {
+ return struct {
+ /// OnMessage Callback on a websocket
+ pub const WsOnMessageFn = *const fn (
+ /// user-provided context, passed in from websocketHttpUpgrade()
+ context: ?*ContextType,
+ /// websocket handle, used to identify the websocket internally
+ handle: WsHandle,
+ /// the received message
+ message: []const u8,
+ /// indicator if message is text or binary
+ is_text: bool,
+ ) void;
+
+ /// Callback when websocket is closed. uuid is a connection identifier,
+ /// it is -1 if a connection could not be established
+ pub const WsOnCloseFn = *const fn (context: ?*ContextType, uuid: isize) void;
+
+ /// A websocket callback function. provides the context passed in at
+ /// websocketHttpUpgrade().
+ pub const WsFn = *const fn (context: ?*ContextType, handle: WsHandle) void;
+
+ pub const WebSocketSettings = struct {
+ /// on_message(context, handle, message, is_text)
+ on_message: ?WsOnMessageFn = null,
+ /// on_open(context)
+ on_open: ?WsFn = null,
+ /// on_ready(context)
+ on_ready: ?WsFn = null,
+ /// on_shutdown(context, uuid)
+ on_shutdown: ?WsFn = null,
+ /// on_close(context)
+ on_close: ?WsOnCloseFn = null,
+ /// passed-in user-defined context
+ context: ?*ContextType = null,
+ };
+
+ /// This function will end the HTTP stage of the connection and attempt to "upgrade" to a WebSockets connection.
+ pub fn upgrade(h: [*c]fio.http_s, settings: *WebSocketSettings) WebSocketError!void {
+ var fio_settings: fio.websocket_settings_s = .{
+ .on_message = internal_on_message,
+ .on_open = internal_on_open,
+ .on_ready = internal_on_ready,
+ .on_shutdown = internal_on_shutdown,
+ .on_close = internal_on_close,
+ .udata = settings,
+ };
+ if (fio.http_upgrade2ws(h, fio_settings) != 0) {
+ return error.UpgradeError;
+ }
+ }
+
+ fn internal_on_message(handle: WsHandle, msg: fio.fio_str_info_s, is_text: u8) callconv(.C) void {
+ var user_provided_settings: ?*WebSocketSettings = @ptrCast(?*WebSocketSettings, @alignCast(@alignOf(?*WebSocketSettings), fio.websocket_udata_get(handle)));
+ var message = msg.data[0..msg.len];
+ if (user_provided_settings) |settings| {
+ if (settings.on_message) |on_message| {
+ on_message(settings.context, handle, message, is_text == 1);
+ }
+ }
+ }
+
+ fn internal_on_open(handle: WsHandle) callconv(.C) void {
+ var user_provided_settings: ?*WebSocketSettings = @ptrCast(?*WebSocketSettings, @alignCast(@alignOf(?*WebSocketSettings), fio.websocket_udata_get(handle)));
+ if (user_provided_settings) |settings| {
+ if (settings.on_open) |on_open| {
+ on_open(settings.context, handle);
+ }
+ }
+ }
+
+ fn internal_on_ready(handle: WsHandle) callconv(.C) void {
+ var user_provided_settings: ?*WebSocketSettings = @ptrCast(?*WebSocketSettings, @alignCast(@alignOf(?*WebSocketSettings), fio.websocket_udata_get(handle)));
+ if (user_provided_settings) |settings| {
+ if (settings.on_ready) |on_ready| {
+ on_ready(settings.context, handle);
+ }
+ }
+ }
+
+ fn internal_on_shutdown(handle: WsHandle) callconv(.C) void {
+ var user_provided_settings: ?*WebSocketSettings = @ptrCast(?*WebSocketSettings, @alignCast(@alignOf(?*WebSocketSettings), fio.websocket_udata_get(handle)));
+ if (user_provided_settings) |settings| {
+ if (settings.on_shutdown) |on_shutdown| {
+ on_shutdown(settings.context, handle);
+ }
+ }
+ }
+
+ fn internal_on_close(uuid: isize, udata: ?*anyopaque) callconv(.C) void {
+ var user_provided_settings: ?*WebSocketSettings = @ptrCast(?*WebSocketSettings, @alignCast(@alignOf(?*WebSocketSettings), udata));
+ if (user_provided_settings) |settings| {
+ if (settings.on_close) |on_close| {
+ on_close(settings.context, uuid);
+ }
+ }
+ }
+
+ const WebSocketError = error{
+ WriteError,
+ UpgradeError,
+ SubscribeError,
+ };
+
+ pub inline fn write(handle: WsHandle, message: []const u8, is_text: bool) WebSocketError!void {
+ if (fio.websocket_write(
+ handle,
+ fio.str2fio(message),
+ if (is_text) 1 else 0,
+ ) != 0) {
+ return error.WriteError;
+ }
+ }
+
+ pub fn udataToContext(udata: *anyopaque) *ContextType {
+ return @ptrCast(*ContextType, @alignCast(@alignOf(*ContextType), udata));
+ }
+
+ pub inline fn close(handle: WsHandle) void {
+ fio.websocket_close(handle);
+ }
+
+ const PublishArgs = struct {
+ channel: []const u8,
+ message: []const u8,
+ is_json: bool = false,
+ };
+
+ /// publish a message in a channel
+ pub inline fn publish(args: PublishArgs) void {
+ fio.fio_publish(.{
+ .channel = util.str2fio(args.channel),
+ .message = util.str2fio(args.message),
+ .is_json = if (args.is_json) 1 else 0,
+ });
+ }
+
+ pub const SubscriptionOnMessageFn = *const fn (context: ?*ContextType, handle: WsHandle, channel: []const u8, message: []const u8) void;
+ pub const SubscriptionOnUnsubscribeFn = *const fn (context: ?*ContextType) void;
+
+ pub const SubscribeArgs = struct {
+ channel: []const u8,
+ on_message: ?SubscriptionOnMessageFn = null,
+ on_unsubscribe: ?SubscriptionOnUnsubscribeFn = null,
+ /// this is not wrapped nicely yet
+ match: fio.fio_match_fn = null,
+ /// When using direct message forwarding (no on_message callback), this indicates if
+ /// messages should be sent to the client as binary blobs, which is the safest approach.
+ /// By default, facil.io will test for UTF-8 data validity and send the data as text if
+ /// it's a valid UTF-8. Messages above ~32Kb might be assumed to be binary rather than
+ /// tested.
+ force_binary: bool = false,
+ /// When using direct message forwarding (no on_message callback), this indicates if
+ /// messages should be sent to the client as UTF-8 text. By default, facil.io will test
+ /// for UTF-8 data validity and send the data as text if it's a valid UTF-8. Messages
+ /// above ~32Kb might be assumed to be binary rather than tested. force_binary has
+ /// precedence over force_text.
+ force_text: bool = false,
+ context: ?*ContextType = null,
+ };
+
+ /// Returns a subscription ID on success and 0 on failure.
+ /// we copy the pointer so make sure the struct stays valid.
+ /// we need it to look up the ziggified callbacks.
+ pub inline fn subscribe(handle: WsHandle, args: *SubscribeArgs) WebSocketError!usize {
+ if (handle == null) return error.SubscribeError;
+ var fio_args: fio.websocket_subscribe_s_zigcompat = .{
+ .ws = handle.?,
+ .channel = util.str2fio(args.channel),
+ .on_message = if (args.on_message) |_| internal_subscription_on_message else null,
+ .on_unsubscribe = if (args.on_unsubscribe) |_| internal_subscription_on_unsubscribe else null,
+ .match = args.match,
+ .force_binary = if (args.force_binary) 1 else 0,
+ .force_text = if (args.force_text) 1 else 0,
+ .udata = args,
+ };
+ const ret = fio.websocket_subscribe_zigcompat(fio_args);
+ if (ret == 0) {
+ return error.SubscribeError;
+ }
+ return ret;
+ }
+
+ pub fn internal_subscription_on_message(handle: WsHandle, channel: fio.fio_str_info_s, message: fio.fio_str_info_s, udata: ?*anyopaque) callconv(.C) void {
+ if (udata) |p| {
+ const args = @ptrCast(*SubscribeArgs, @alignCast(@alignOf(*SubscribeArgs), p));
+ if (args.on_message) |on_message| {
+ on_message(args.context, handle, channel.data[0..channel.len], message.data[0..message.len]);
+ }
+ }
+ }
+ pub fn internal_subscription_on_unsubscribe(udata: ?*anyopaque) callconv(.C) void {
+ if (udata) |p| {
+ const args = @ptrCast(*SubscribeArgs, @alignCast(@alignOf(*SubscribeArgs), p));
+ if (args.on_unsubscribe) |on_unsubscribe| {
+ on_unsubscribe(args.context);
+ }
+ }
+ }
+ };
+}
diff --git a/src/zap.zig b/src/zap.zig
index 5afcff1..06b8ac6 100644
--- a/src/zap.zig
+++ b/src/zap.zig
@@ -10,6 +10,7 @@ pub usingnamespace @import("util.zig");
pub usingnamespace @import("http.zig");
pub usingnamespace @import("mustache.zig");
pub usingnamespace @import("http_auth.zig");
+pub const WebSockets = @import("websockets.zig");
pub const Log = @import("log.zig");
@@ -496,16 +497,32 @@ pub const CookieArgs = struct {
pub const HttpRequestFn = *const fn (r: [*c]fio.http_s) callconv(.C) void;
pub const SimpleHttpRequestFn = *const fn (SimpleRequest) void;
+/// websocket connection upgrade
+/// fn(request, targetstring)
+pub const SimpleHttpUpgradeFn = *const fn (r: SimpleRequest, target_protocol: []const u8) void;
+
+/// http finish, called when zap finishes. You get your udata back in the
+/// struct.
+pub const SimpleHttpFinishSettings = [*c]fio.struct_http_settings_s;
+pub const SimpleHttpFinishFn = *const fn (SimpleHttpFinishSettings) void;
+
pub const SimpleHttpListenerSettings = struct {
port: usize,
interface: [*c]const u8 = null,
on_request: ?SimpleHttpRequestFn,
- on_response: ?*const fn ([*c]fio.http_s) callconv(.C) void = null,
+ on_response: ?SimpleHttpRequestFn = null,
+ on_upgrade: ?SimpleHttpUpgradeFn = null,
+ on_finish: ?SimpleHttpFinishFn = null,
+ // provide any pointer in there for "user data". it will be passed pack in
+ // on_finish()'s copy of the struct_http_settings_s
+ udata: ?*anyopaque = null,
public_folder: ?[]const u8 = null,
max_clients: ?isize = null,
max_body_size: ?usize = null,
timeout: ?u8 = null,
log: bool = false,
+ ws_timeout: u8 = 40,
+ ws_max_msg_size: usize = 262144,
};
pub const SimpleHttpListener = struct {
@@ -520,6 +537,9 @@ pub const SimpleHttpListener = struct {
};
}
+ // on_upgrade: ?*const fn ([*c]fio.http_s, [*c]u8, usize) callconv(.C) void = null,
+ // on_finish: ?*const fn ([*c]fio.struct_http_settings_s) callconv(.C) void = null,
+
// we could make it dynamic by passing a SimpleHttpListener via udata
pub fn theOneAndOnlyRequestCallBack(r: [*c]fio.http_s) callconv(.C) void {
if (the_one_and_only_listener) |l| {
@@ -534,6 +554,39 @@ pub const SimpleHttpListener = struct {
}
}
+ pub fn theOneAndOnlyResponseCallBack(r: [*c]fio.http_s) callconv(.C) void {
+ if (the_one_and_only_listener) |l| {
+ var req: SimpleRequest = .{
+ .path = util.fio2str(r.*.path),
+ .query = util.fio2str(r.*.query),
+ .body = util.fio2str(r.*.body),
+ .method = util.fio2str(r.*.method),
+ .h = r,
+ };
+ l.settings.on_response.?(req);
+ }
+ }
+
+ pub fn theOneAndOnlyUpgradeCallBack(r: [*c]fio.http_s, target: [*c]u8, target_len: usize) callconv(.C) void {
+ if (the_one_and_only_listener) |l| {
+ var req: SimpleRequest = .{
+ .path = util.fio2str(r.*.path),
+ .query = util.fio2str(r.*.query),
+ .body = util.fio2str(r.*.body),
+ .method = util.fio2str(r.*.method),
+ .h = r,
+ };
+ var zigtarget: []u8 = target[0..target_len];
+ l.settings.on_upgrade.?(req, zigtarget);
+ }
+ }
+
+ pub fn theOneAndOnlyFinishCallBack(s: [*c]fio.struct_http_settings_s) callconv(.C) void {
+ if (the_one_and_only_listener) |l| {
+ l.settings.on_finish.?(s);
+ }
+ }
+
pub fn listen(self: *Self) !void {
var pfolder: [*c]const u8 = null;
var pfolder_len: usize = 0;
@@ -546,22 +599,23 @@ pub const SimpleHttpListener = struct {
var x: fio.http_settings_s = .{
.on_request = if (self.settings.on_request) |_| Self.theOneAndOnlyRequestCallBack else null,
- .on_upgrade = null,
- .on_response = self.settings.on_response,
- .on_finish = null,
+ .on_upgrade = if (self.settings.on_upgrade) |_| Self.theOneAndOnlyUpgradeCallBack else null,
+ .on_response = if (self.settings.on_response) |_| Self.theOneAndOnlyResponseCallBack else null,
+ .on_finish = if (self.settings.on_finish) |_| Self.theOneAndOnlyFinishCallBack else null,
.udata = null,
.public_folder = pfolder,
.public_folder_length = pfolder_len,
.max_header_size = 32 * 1024,
.max_body_size = self.settings.max_body_size orelse 50 * 1024 * 1024,
- .max_clients = self.settings.max_clients orelse 100,
+ // fio provides good default:
+ .max_clients = self.settings.max_clients orelse 0,
.tls = null,
.reserved1 = 0,
.reserved2 = 0,
.reserved3 = 0,
.ws_max_msg_size = 0,
.timeout = self.settings.timeout orelse 5,
- .ws_timeout = 0,
+ .ws_timeout = self.settings.ws_timeout,
.log = if (self.settings.log) 1 else 0,
.is_client = 0,
};
@@ -624,7 +678,7 @@ pub fn listen(port: [*c]const u8, interface: [*c]const u8, settings: ListenSetti
var x: fio.http_settings_s = .{
.on_request = settings.on_request,
.on_upgrade = settings.on_upgrade,
- .on_response = settings.on_response orelse null,
+ .on_response = settings.on_response,
.on_finish = settings.on_finish,
.udata = null,
.public_folder = pfolder,
@@ -636,7 +690,7 @@ pub fn listen(port: [*c]const u8, interface: [*c]const u8, settings: ListenSetti
.reserved1 = 0,
.reserved2 = 0,
.reserved3 = 0,
- .ws_max_msg_size = 0,
+ .ws_max_msg_size = settings.ws_max_msg_size,
.timeout = settings.keepalive_timeout_s,
.ws_timeout = 0,
.log = if (settings.log) 1 else 0,