PageSourceSearch

https://leafo.net/posts/itchio-and-coroutines.html

html leafo.net collected 2026-10-02 02:38:55 UTC 26,412 bytes, 291 lines download raw bytes

1<!DOCTYPE HTML>
2<html lang="en">
3<head>
4  <meta charset="UTF-8">
5  <title>How itch.io uses Coroutines for non-blocking IO</title>
6  <link rel="stylesheet" href="../style/style.css?Fri Jan 16 20:28:13 2026" />
7  <link rel="alternate" type="application/rss+xml" title="leafo.net posts" href="../feed.xml" />
8  <meta name="viewport" content="width=device-width, initial-scale=1" />
9
10  <meta property="og:site_name" content="leafo.net" />
11  <meta property="og:title" content="How itch.io uses Coroutines for non-blocking IO" />
12  <meta property="og:image" content="http://leafo.net/img/leafo.png" />
13  
14  <meta property="og:description" content="itch.io is a website for hosting indie games, its implementation is unique because it&rsquo;s written entirely in Lua ( MoonScript ). It runs inside of an nginx distribution called OpenResty . It uses coroutines for all asynchronous operations like d..." />
15  <meta name="description" content="itch.io is a website for hosting indie games, its implementation is unique because it&rsquo;s written entirely in Lua ( MoonScript ). It runs inside of an nginx distribution called OpenResty . It uses coroutines for all asynchronous operations like d..." />
16  
17  
17<script type="text/javascript">
18    window.site_root = "..";
19  </script>
19
20  
20<script type="text/javascript">
21  if (window.location.hostname != "localhost") {
22    var _gaq = _gaq || [];
23    _gaq.push(['_setAccount', 'UA-136625-1']);
24    _gaq.push(['_trackPageview']);
25
26    (function() {
27      var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
28      ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
29      var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
30    })();
31  }
32</script>
32
33  <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,700" rel="stylesheet">
34</head>
35<body class="nice_fonts">
36  <div class="post"><div class="sidebar"><div class="logo_wrapper"><a title="leafo.net" href=".."><img alt="leafo.net" class="logo" src="../img/leafo.svg"/></a></div><div class="sidebar_content"><div class="sidebar_minor"><h3>My projects</h3><ul><li><a href="https://sightreading.training">sightreading.training</a></li><li><a href="https://streak.club">streak.club</a></li><li><a href="https://leafo.net/lapis/">Lapis</a></li><li><a href="https://itch.io">itch.io</a></li><li><a href="https://moonscript.org">MoonScript</a></li><li><a href="https://ludumdare.itch.io">Ludum Dare game browser</a></li></ul><p><a href="..#projects">View more →</a></p><h3>Recent guides</h3><ul><li><a href="../guides/testing-lua-modules-with-github-actions.html">Testing Lua projects with GitHub Actions</a></li><li><a href="../guides/postgresql-calculating-percentile.html">Calculating Percentile (and Median) in PostgreSQL</a></li><li><a href="../guides/nginx-reverse-proxy-to-self.html">NGINX reverse proxy to self</a></li><li><a href="../guides/postgresql-preloading.html">PostgreSQL Preloading</a></li><li><a href="../guides/building-openresty-with-openssl-1-0.html">Building OpenResty with OpenSSL 1.0</a></li><li><a href="../guides/customizing-the-luarocks-tree.html">Using LuaRocks to install packages in the current directory</a></li><li><a href="../guides/dynamic-scoping-in-lua.html">Dynamic scoping in Lua</a></li><li><a href="../guides/dsl-in-lua.html">Writing a DSL in Lua</a></li><li><a href="../guides/function-cloning-in-lua.html">Cloning a function in Lua</a></li><li><a href="../guides/setfenv-in-lua52-and-above.html">Implementing setfenv in Lua 5.2, 5.3, and above</a></li></ul><p><a href="../guides.html">View all →</a></p><h3>Recent posts</h3><ul><li><a href="../posts/a-new-leafonet.html">A new leafo.net</a></li><li><a href="../posts/introducing_streakclub.html">Introducing Streak Club</a></li><li><a href="../posts/creating_an_image_server.html">Nginx image processing server</a></li><li><a href="../posts/moonscript_v024.html">MoonScript v0.2.4</a></li><li><a href="../posts/introducing_itchio.html">Introducing itch.io</a></li><li><a href="../posts/moonscript_v023.html">MoonScript v0.2.3</a></li><li><a href="../posts/moonscript_v022.html">MoonScript v0.2.2</a></li><li><a href="../posts/getting_started_with_moonscript.html">Getting started with MoonScript</a></li><li><a href="../posts/lua_on_heroku.html">Running Lua on Heroku</a></li><li><a href="../posts/moonscript_v020.html">MoonScript v0.2.0</a></li></ul></div></div></div><div class="main_column"><div class="post_header"><h1>How itch.io uses Coroutines for non-blocking IO</h1><div class="post_meta">Posted <span class="post_date" title="Thu Jun 09 2016 09:24:37">June 09, 2016</span> by leafo (<a href="https://twitter.com/moonscript">@moonscript</a>) &middot; Tags: lua, moonscript</div><div class="discussions">
36<script type="text/javascript">reddit_url = "http:\/\/leafo.net\/posts\/itchio-and-coroutines.html"</script>
36<script type="text/javascript" src="//www.redditstatic.com/button/button1.js"></script>
36<a class="twitter-share-button" href="https://twitter.com/share" data-url="http://leafo.net/posts/itchio-and-coroutines.html" data-via="moonscript" data-hashtags="lualang">Tweet</a>            
36<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script>
36
37          </div></div><div class="inner post_body"><p><a href="https://itch.io">itch.io</a> is a website for hosting indie games, its implementation is
38unique because it&rsquo;s written entirely in Lua (<a href="http://moonscript.org">MoonScript</a>). It runs inside
39of an nginx distribution called <a href="https://openresty.org/en/">OpenResty</a>. It uses coroutines for all
40asynchronous operations like database queries and HTTP requests.</p>
41
42<div class="index"><ul><li><a href="#overview-of-coroutines">Overview of coroutines</a></li>
43<li><a href="#coroutines-in-practice">Coroutines in practice</a></li>
44<ul><li><a href="#coroutines-in-practice/whats-wrong-with-async-callbacks">What&rsquo;s wrong with async callbacks?</a></li>
45<li><a href="#coroutines-in-practice/the-coroutine-approach">The coroutine approach </a></li>
46<li><a href="#coroutines-in-practice/how-openresty-works">How OpenResty works</a></li></ul>
47<li><a href="#conclusion">Conclusion</a></li></ul></div>
48
49
50<p>I like Lua for many reasons<sup>1</sup>, it gets the design of many core
51components correct:</p>
52
53<ul>
54<li>Errors are handled locally for functional calls &mdash; we use multiple return values to signal status without additional allocations. (no temporary arrays, or exceptions)</li>
55<li>Prototypical inheritance is implemented in a smart way &mdash; there are no <em>reserved</em> hash table keys to change functionality. (looking at you <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol">JavaScript Symbol</a>)</li>
56<li><strong>Coroutines are implemented as first-class functionality</strong>, and not tacked on as additional syntax like many other languages.</li>
57</ul>
58
59
60<h2 id="overview-of-coroutines">Overview of coroutines</h2>
61
62<p>Coroutines are a tool, but they probably aren&rsquo;t a tool you're familiar with.
63When you first started programming you may have viewed regular expressions as
64some mystical thing that you didn&rsquo;t really get, but let you do some powerful
65things. As you matured as a programmer you learned the syntax, and learned when
66and how to use them effectively. Coroutines are in the same category. The only
67problem is that most languages don&rsquo;t expose coroutines like they do regular
68expressions, so you haven&rsquo;t had the opportunity to use them effectively.</p>
69
70<p>This post is about coroutines. <strong>When coroutines are done right, they can be
71used as an implementation detail and not a syntax change.</strong> Examples of
72languages that do it right include Lisp and Lua. (I'm sure there are more, but
73I haven&rsquo;t used them.)</p>
74
75<p>The best way to think of coroutines is to understand where they sit in the
76taxonomy of <em>control transfer</em>. Chances are you're already familiar with
77exceptions. I like to sort things like this:</p>
78
79<pre><code>Exceptions &lt; Coroutines &lt; Continuations
80</code></pre>
81
82<p><strong>Exceptions</strong> can be thought of as a <em>subclass</em> of coroutines. You can
83implement an exception mechanism with coroutines. They are an abstract
84construct that let you control where your code is executing (see
85<a href="http://man7.org/linux/man-pages/man3/longjmp.3.html"><code>longjmp</code></a>). When there&rsquo;s an error your <a href="https://en.wikipedia.org/wiki/Program_counter">program counter</a> jumps to the
86<code>try</code> <code>catch</code> block you've written further up in the execution stack. Jumps
87like this are called control transfer.</p>
88
89<p>When an exception happens you lose access to where the error happened.
90(Although exception implementations will capture information about where the
91error happened, like a backtrace, and return it in an exception object.) You
92can&rsquo;t say <em>try running that line again</em>, which makes sense, because that line
93resulted in an error.</p>
94
95<p>With coroutines you can tell the code to jump back and start running exactly
96where it left off. We don&rsquo;t use coroutines for unrecoverable errors, there are
97many scenarios where it makes sense to go back to where we were and keep on
98executing. This is called <a href="https://en.wikipedia.org/wiki/Thread_%28computing%29#Scheduling">cooperative multithreading</a>.</p>
99
100<p>Coroutines do not use <code>try</code> <code>catch</code> or <code>throw</code>. In Lua, after creating a
101coroutine, you run it, just like a function, and it returns values, just like a
102function. Instead of using <code>return</code>, a coroutine can <em>yield</em> to signal that it
103might want to return back to that spot. When a yield happens, control is
104transferred back to where the coroutine was initially executed. The original
105caller can decide to <em>resume</em> the coroutine if it makes sense.</p>
106
107<p><strong>Continuations</strong> are like coroutines but with <em>even more control</em>. You can
108return to the same location multiple times. Additionally, when continuations
109are implemented as first-class objects, you can pass around a control transfer
110object. You might be starting to see the parallels to how first-class functions
111work.</p>
112
113<h2 id="coroutines-in-practice">Coroutines in practice</h2>
114
115<h3 id="coroutines-in-practice/whats-wrong-with-async-callbacks">What&rsquo;s wrong with async callbacks?</h3>
116
117<p>I mentioned above that coroutines work best when they're <strong>an implementation
118detail and not a syntax change</strong>. As the consumer of some
119interface/library/framework, you should be able to take advantage of
120coroutines, but it shouldn&rsquo;t require you to change how you write code.</p>
121
122<p>JavaScript definitely popularized asynchronous code via callbacks. It also
123completely changed how many of us write code. For complicated applications, it
124changed it for the worse.</p>
125
126<p>Now that Node.js has brought server side JavaScript to the masses, a common
127task for a web developer is to run a few queries that are dependent on one
128another, then return the result. Queries should be executed asynchronous since
129they preform networks operations that should not block the main thread.</p>
130
131<p>The naïve approach looks something like this:</p>
132
133<pre class="highlight lang_javascript"><code><span></span><span class="kd">function</span><span class="w"> </span><span class="nx">getUserProfileLinks</span><span class="p">(</span><span class="nx">userId</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
134<span class="w">  </span><span class="nx">query</span><span class="p">(</span><span class="s2">&quot;select * from users where id = ? limit 1&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">userId</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">users</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
135<span class="w">    </span><span class="nx">query</span><span class="p">(</span><span class="s2">&quot;select * from profiles where user_id = ? limit 1&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">users</span><span class="p">[</span><span class="mf">0</span><span class="p">].</span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">profiles</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
136<span class="w">      </span><span class="nx">query</span><span class="p">(</span><span class="s2">&quot;select * from profile_links where profile_id = ?&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">profile</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">links</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
137<span class="w">        </span><span class="c1">// return links??</span>
138<span class="w">      </span><span class="p">})</span>
139<span class="w">    </span><span class="p">})</span>
140<span class="w">  </span><span class="p">})</span>
141
142<span class="w">  </span><span class="c1">// ?? can&#39;t return here since everything is async, see above</span>
143<span class="p">}</span></code>
144</pre>
145
146
147<p>As more queries are added the nesting becomes extreme. Additionally, the real
148disadvantage, it changes the interface to our function. It&rsquo;s impossible to
149return the result because we don&rsquo;t have the final query&rsquo;s response. You must
150restructure your functions to no longer use return values, but instead take a
151callback.</p>
152
153<p>The code above doesn&rsquo;t even handle errors. Do we add a second callback for
154errors? Do we pass a special argument that represents an error to the one
155callback? Assuming we decide on how we want to handle errors, how do
156we get everyone else to agree with it so we can have uniform syntax?</p>
157
158<p>This is an actual problem in the JavaScript community: There are still people
159building new libraries trying to figure out the best way to manage asynchronous
160control. A language that creates this kind of schism in the community is not a
161well designed language.</p>
162
163<p>It gets worse&hellip;</p>
164
165<p>This asynchronous interface contaminates your code. If you have any other
166function in your application that needs to work with this function, it then
167needs to be rewritten asynchronous-callback style to match the control
168transfer, <em>even if this hypothetical function does nothing asynchronous</em>. This
169is a massive overhead for developers.</p>
170
171<p>If someone writes a library that abstracts all this database logic away, you,
172as the consumer, must use their asynchronous interface. The asynchronous
173interface will permeate throughout your code base, even if you never write
174anything asynchronous. You can not hide asynchronous control transfer via
175callbacks within a procedural interface.</p>
176
177<p>Luckily the JavaScript community is coming to agreements on how to handle
178asynchronous interfaces. But the core problems still stands: <strong>We have two
179competing ways to do flow control in our code base.</strong></p>
180
181<h3 id="coroutines-in-practice/the-coroutine-approach">The coroutine approach </h3>
182
183<p>Here&rsquo;s an how I would write the snippet above for itch.io with Lua running
184within OpenResty:</p>
185
186<pre class="highlight lang_lua"><code><span></span><span class="kr">function</span><span class="w"> </span><span class="nf">get_links</span><span class="p">(</span><span class="nv">user_id</span><span class="p">)</span>
187<span class="w">  </span><span class="kd">local</span><span class="w"> </span><span class="nv">users</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">query</span><span class="p">(</span><span class="s2">&quot;select * from users where id = ? limit 1&quot;</span><span class="p">,</span><span class="w"> </span><span class="nv">user_id</span><span class="p">)</span>
188<span class="w">  </span><span class="kd">local</span><span class="w"> </span><span class="nv">profiles</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">query</span><span class="p">(</span><span class="s2">&quot;select * from profiles where user_id = ? limit 1&quot;</span><span class="p">,</span><span class="w"> </span><span class="nv">users</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
189<span class="w">  </span><span class="kr">return</span><span class="w"> </span><span class="nf">query</span><span class="p">(</span><span class="s2">&quot;select * from profile_links where profile_id = ?&quot;</span><span class="p">,</span><span class="w"> </span><span class="nv">profiles</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
190<span class="kr">end</span></code>
191</pre>
192
193
194<p><strong>Whoa</strong>, wait a second: that&rsquo;s just procedural code. That&rsquo;s exactly why
195coroutines are important. Coroutines do not require us to rewrite our code.
196OpenResty gives an environment to execute in where the implementation of my
197non-blocking functions, like <code>query</code>, can yield when it&rsquo;s time to give control
198back to the scheduler.</p>
199
200<p><strong>As an application developer I change <em>nothing</em> about how I write code.</strong>
201Coroutines are an implementation detail of the <code>query</code> function, not a
202requirement on how I must structure my business logic.</p>
203
204<p>What about errors? Easy: I use the same interface as any other function. In Lua
205errors are handled by returning a second value. And since I'm just writing
206procedural code, I can <code>return</code> on error:</p>
207
208<pre class="highlight lang_lua"><code><span></span><span class="kr">function</span><span class="w"> </span><span class="nf">get_links</span><span class="p">(</span><span class="nv">user_id</span><span class="p">)</span>
209<span class="w">  </span><span class="kd">local</span><span class="w"> </span><span class="nv">users</span><span class="p">,</span><span class="w"> </span><span class="nv">err</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">query</span><span class="p">(</span><span class="s2">&quot;select * from users where id = ? limit 1&quot;</span><span class="p">,</span><span class="w"> </span><span class="nv">user_id</span><span class="p">)</span>
210<span class="w">  </span><span class="kr">if</span><span class="w"> </span><span class="ow">not</span><span class="w"> </span><span class="nv">users</span><span class="w"> </span><span class="kr">then</span>
211<span class="w">    </span><span class="kr">return</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="nv">err</span>
212<span class="w">  </span><span class="kr">end</span>
213<span class="w">  </span><span class="c1">-- etc.</span>
214<span class="kr">end</span></code>
215</pre>
216
217
218<p>The framework developers determine how the code executes, not me. This is
219perfect, since writing the asynchronous callback code in JavaScript is just
220busy work.</p>
221
222<p>It gets even better: In my test environment I don&rsquo;t need non-blocking code, I
223don&rsquo;t have a scheduler, I don&rsquo;t even run my code inside of OpenResty.  <strong>It
224doesn&rsquo;t matter.</strong> I can provide my own implementation of <code>query</code> which operates
225synchronously. My business logic runs exactly the same. If I'm upgrading from
226an old synchronous project to non-blocking IO, I don&rsquo;t have to rewrite
227everything.</p>
228
229<p><strong>Runtime errors are what you expect:</strong> Since each coroutine has its own stack,
230when a runtime exception happens (eg. I try to dereference <code>nil</code>), my backtrace
231only contains the stack that my coroutine is running in. Error backtraces are
232short and easy to parse.</p>
233
234<p>Since there is only one function interface, non-blocking code already works
235fine with all existing code. There are no new interfaces to learn, and there is
236no confusion about the best way to call a function. The language gives us an
237interface we can all agree on.</p>
238
239<h3 id="coroutines-in-practice/how-openresty-works">How OpenResty works</h3>
240
241<p>OpenResty is clever about their implementation for non-blocking IO. OpenResty
242re-implements the <code>socket</code> interface provided by <code>LuaSocket</code>. It directs all
243network activity to non-blocking IO managed by Nginx&rsquo;s event loop. Every time
244Lua code triggers a network operation, control is yielded back to Nginx where
245it can resume any coroutines that have IO ready for reading. <strong>Although I'm
246using cooperative multithreading, I don&rsquo;t have to think about yielding</strong>.</p>
247
248<p>Why is this important? <strong>I can use the same exact library</strong>, and have it run
249either synchronous on the command line or non-blocking inside of OpenResty. Not
250only have coroutines saved my sanity as an application developer, but also as a
251library developer. I've done exactly this with my Lua PostgreSQL driver:
252<a href="https://github.com/leafo/pgmoon">pgmoon</a>.</p>
253
254<h2 id="conclusion">Conclusion</h2>
255
256<p>I hope this post encourages you to check out <a href="https://openresty.org/en/">OpenResty</a>. If you're looking
257for a full featured web framework to use with it then I suggest <a href="http://leafo.net/lapis/">Lapis</a>.
258itch.io has been running Lapis in production for over 3 years. I've written
259over 100 thousand lines of MoonScript for itch.io without a single asynchronous
260callback.</p>
261
262<p>The slowest asynchronous APIs that itch.io has to communicate with are payment
263gateways. I remember speaking to another marketplace about how they needed to
264build new queueing infrastructure to handle all the asynchronous communication
265with their payment gateways. Since they were using a blocking web framework,
266Rails, their throughput would be destroyed had they tried wait for PayPal to
267respond in their web app.</p>
268
269<p>In itch.io I just write <code>http.request()</code> and read the return value. Throughput
270is unaffected. Imagine saving hundreds of thousands of dollars and months of
271headaches because you decided to learn about coroutines. 😉</p>
272
273<hr />
274
275<p><sup>1</sup> These are just <em>some</em> of my favorites, check out the <a href="https://www.lua.org/manual/5.3/">Lua reference manual for more</a>.</p>
276<div class="tagged_pages"><div class="more_posts_header">Here are some more guides tagged &#039;lua&#039;</div><div class="related_page_box"><div class="page_title"><a href="../guides/testing-lua-modules-with-github-actions.html">Testing Lua projects with GitHub Actions</a></div><div class="sub">Posted <span title="Sun 26 Apr 2020 11:06:30 AM PDT">April 26, 2020</span></div></div><div class="related_page_box"><div class="page_title"><a href="../guides/customizing-the-luarocks-tree.html">Using LuaRocks to install packages in the current directory</a></div><div class="sub">Posted <span title="Thu Jan 28 01:45:11 PST 2016">January 28, 2016</span></div></div><div class="related_page_box"><div class="page_title"><a href="../guides/dynamic-scoping-in-lua.html">Dynamic scoping in Lua</a></div><div class="sub">Posted <span title="Sun Jan 24 00:53:45 PST 2016">January 24, 2016</span></div></div><div class="related_page_box"><div class="page_title"><a href="../guides/dsl-in-lua.html">Writing a DSL in Lua</a></div><div class="sub">Posted <span title="Sat Aug  8 12:20:12 PDT 2015">August 08, 2015</span></div></div><div class="related_page_box"><div class="page_title"><a href="../guides/function-cloning-in-lua.html">Cloning a function in Lua</a></div><div class="sub">Posted <span title="Wed Jul  8 13:04:58 PDT 2015">July 08, 2015</span></div></div><div class="related_page_box"><div class="page_title"><a href="../guides/setfenv-in-lua52-and-above.html">Implementing setfenv in Lua 5.2, 5.3, and above</a></div><div class="sub">Posted <span title="Tue Jul 7 19:46:23 2015 -0700">July 08, 2015</span></div></div><div class="related_page_box"><div class="page_title"><a href="../guides/moonscript-classes.html">An in-depth look into the MoonScript class implementation</a></div><div class="sub">Posted <span title="Sun Jul 5 01:19:19 2015 -0700">July 05, 2015</span></div></div><div class="related_page_box"><div class="page_title"><a href="../guides/parsing-expression-grammars.html">An introduction to Parsing Expression Grammars with LPeg</a></div><div class="sub">Posted <span title="Sat Jul  4 00:35:09 PDT 2015">July 04, 2015</span></div></div><div class="related_page_box"><div class="page_title"><a href="../guides/using-postgres-with-openresty.html">Using PostgreSQL with OpenResty</a></div><div class="sub">Posted <span title="Fri Jul  3 21:28:18 PDT 2015">July 04, 2015</span></div></div></div></div></div><div class="misc_column"><h3>Related projects</h3><div class="project_box"><div class="project_name"><a href="https://moonscript.org">MoonScript</a></div><p class="project_blurb">A programming language that compiles to Lua, inspired by CoffeeScript and written in MoonScript.</p><div class="project_buttons"><a href="https://github.com/leafo/moonscript">GitHub Repo</a></div></div><div class="project_box"><div class="project_name"><a href="https://github.com/leafo/pgmoon">pgmoon</a></div><p class="project_blurb">A PostgreSQL client library written in pure Lua, designed for standalone use and use within OpenResty</p><div class="project_buttons"><a href="https://github.com/leafo/pgmoon">GitHub Repo</a></div></div></div></div>
277
278  <div class="footer">
279    <p>
280      leafo.net &middot; Generated Fri Jan 16 20:28:13 2026 by <a href="../sitegen">Sitegen</a>
281      <a rel="me" href="https://mastodon.social/@leafo">mastodon.social/@leafo</a>
282    </p>
283  </div>
284
285  
285<script type="text/javascript">
286    if (navigator.appVersion.indexOf("Win") >= 0) {
287      document.body.className = "";
288    }
289  </script>
289
290</body>
291</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.