lmjtfy.git / apps / lmjtfy / README.md
README.mdpreviewREADME.mdsource395 lines · 18.7 KB · raw
1# Chapter 2: the Worker, where everything actually happens
2
3Everything you see at <https://lmjtfy.fun> comes out of this
4folder. It is one Cloudflare Worker: a small program Cloudflare runs at its
5edge, started fresh for each request in milliseconds, written here in Rust
6and compiled to WebAssembly. There is no server to keep running. A request
7arrives, the Worker answers it, and that is that.
8
9Two things have to outlive a single request: what was already asked (so it
10is never asked again) and how much of today's budget is left. For those the
11Worker has two Durable Objects, which are the closest Cloudflare comes to a
12tiny server with a memory: one object per name, anywhere in the world, with
13its own SQLite. The Worker talks to them; they remember.
14
15```mermaid
16flowchart LR
17  V["Visitor's browser"] -->|"/, /ask, /gate"| W["Worker (src/lib.rs)"]
18  V -->|"/live (socket)"| A
19  G["git clone"] -->|"/lmjtfy.git"| W
20  W --> A["Archive object: every call, the feed, the sockets"]
21  W --> B["Budget object: today's spend, per-visitor limits"]
22  A --> J["Jev (TypeSafe)"]
23  A --> L["LLM (Workers AI)"]
24  W --> GH["GitHub (read-only token)"]
25```
26
27## What it serves
28
29| Address | What happens |
30| --- | --- |
31| `/` and `/?q=...` | The page. With `?q=`, it types the question in for you and asks. |
32| `POST /ask` | Answers a question as a stream: the whole transcript, re-sent as each call finishes. |
33| `POST /rate` | A browser's 👍 or 👎 on Jev's answer, and the votes after it. |
34| `POST /seen` | A page's report of itself as it is left: how long it was in view, how far down, the screen; or a link followed off the site. Kept, and answered with nothing. |
35| `POST /gate` | The facts request, run as you type, 300 ms after each pause, so the answer starts from what Jev already said. |
36| `/feed` | The next page of "asked lately", which the feed asks for when it is scrolled to its end. It reads the archive and asks nobody anything. |
37| `/live` | A WebSocket each open page holds: how many are online, toasts, live feeds, and "a new version is live". |
38| `/rules` | The rules engine with every fact clickable, asking nobody anything. |
39| `/rules.svg` | The rules drawn as a standalone picture, for the docs (chapter 6). |
40| `/card.png` | The picture a link unfurls with (chapter 11). |
41| `/icons/<name>.png` | The explorer's file and folder icons (chapter 15⅝). |
42| `/lmjtfy.git` | `git clone` it, or open it in a browser and read the code (you are probably here). |
43| `/jevcrates.git`, `/postjevsql.git`, `/jevsnes.git`, `/jevhooks.git` | The client lmjtfy shares with the owner's three other Jev projects, and those projects, served the same way. |
44
45> **Try it.** Open <https://lmjtfy.fun/rules> and click
46> `answerable` until it says no. Watch every rule but one go grey. That is
47> the engine from chapter 6 running in your browser's address bar.
48
49## Live: who is here, and what just happened
50
51Every browser with the site open keeps one socket to `/live`, held by the
52archive object. Its sockets are "hibernatable": while nothing happens, the
53object can sleep and nothing is billed, and the sockets stay open.
54
55One socket per browser, not per tab: the socket belongs to a SharedWorker
56(`src/live.js`), a script the browser runs once for all of a site's tabs
57and keeps while any of them is open. Each tab talks to it, and it passes on
58everything the archive sends; a tab that opens later is handed the current
59count and places at once. A tab the browser freezes in the background
60cannot take the socket down with it, which is what goes wrong when one tab
61holds the socket for the rest. A browser without SharedWorker opens a socket
62per tab, as every page used to.
63
64> **Aside.** Pages of different builds use different workers (the build is
65> in the worker's address), so after a deploy a reloaded tab gets a fresh
66> one while a tab from before keeps its own until it reloads too.
67
68It pushes four things:
69
70- **How many browsers are open, and where**, shown as "N online" in the top
71  bar; hover it for a flag and a place for each, most first. The place is
72  Cloudflare's: every request arrives with a rough country and city, and the
73  Worker passes them to the archive when a page connects (in headers it sets
74  over any the page sent, so a page cannot name its own). The archive keeps
75  them on that page's socket while it is open, for the list. (What is kept
76  for good is below, in [What is kept about visitors](#what-is-kept-about-visitors).) Pages
77  are told at most once a second, so a deploy, which reconnects every page
78  at once, is one update rather than one per page. A page that reconnects
79  while the deploy is still reaching Cloudflare's edge can come through the
80  Worker from before, which passes no place; ten seconds on, when the deploy
81  has settled, the archive asks it to reconnect (close code 1012), and it
82  comes back placed.
83- **A toast when someone asks a question or clones the code.** A question
84  the feed may show is shown with Jev's answer; any other is "someone asked
85  Jev something". The page that asked does not get its own toast. The site
86  sends pages nothing else about who is connected: no address, no browser,
87  no history.
88- **The feeds themselves.** After each toast the archive sends the home
89  page's feeds as HTML elements with ids, and a page replaces the ones it
90  has: most asked and so far whole. A question just asked goes to the top
91  of asked lately, taking its line from wherever it was, so the older lines
92  a page has scrolled in stay put. No refresh.
93- **The build.** Every page carries the commit its Worker was built from
94  (`build.rs`). A deploy restarts the archive object, every socket closes,
95  every page reconnects, and the archive tells each one the build that is
96  live now. A page from an older build shows a toast that stays, with a
97  Reload button. If the deployed commit has a `Release-Note:` trailer, an
98  older page also gets that line as a toast, so the people on the site hear
99  what changed.
100
101## The code pages
102
103    git clone --recurse-submodules https://lmjtfy.fun/lmjtfy.git
104
105The repository is private on GitHub, and is shared from here instead, so
106nobody needs a GitHub account and nothing is announced. The Worker speaks
107git's smart HTTP (`src/clone.rs`): the two requests a clone or fetch makes,
108`info/refs?service=git-upload-pack` and `git-upload-pack`, are forwarded to
109GitHub with a fine-grained token that can read `deizel/lmjtfy` and
110`deizel/jevcrates` and nothing else. A push is refused here, and GitHub
111would refuse it anyway. jevcrates is served beside it at `/jevcrates.git`,
112because `.gitmodules` names it by the relative URL `../jevcrates.git`, which
113git resolves against wherever lmjtfy was cloned from. The owner's three other
114Jev projects (postjevsql, jevsnes, jevhooks) are served too, and name
115jevcrates the same way, so each clones with its submodule from here.
116
117Clones and pulls are counted, anonymously, per repository; "So far" shows
118lmjtfy's, and a clone of any project but jevcrates is toasted. jevcrates is
119fetched along with every project cloned with its submodules, so its toasts
120would double up. A pull names the commits it
121already has (`have` lines) and a clone does not; a fetch counts on the round
122GitHub answers with the pack, so a long negotiation counts once and a pull
123with nothing new is not counted.
124
125Git only ever asks for `lmjtfy.git/info/refs` and `lmjtfy.git/git-upload-pack`,
126so every other path under `/lmjtfy.git` is free for people. The code pages
127(`src/browse.rs`) are laid out like an editor, full width. On the left, a
128sidebar of panels: the explorer (the whole tree, jevcrates included,
129opened on the way to the page you are on), the outline of the page's
130headings, the clone command, the latest commit, and every repository served
131here; jevcrates' pages add the `Cargo.toml` lines for depending on it, pinned
132to its latest commit. On the right, the page:
133a folder's README and CLAUDE.md as two tabs, "for people" and "for agents",
134between a ◀ tab for the chapter before and a ▶ tab for the chapter after
135(from the chapter's own last line, or, in a repository with no guide, the
136next folder with a README in a depth-first walk),
137with their diagrams drawn (click one to enlarge it); a source file
138with its comments rendered on the left and the code they are about on the
139right, coloured (chapter 12½), or rendered if it is markdown, each with a tab
140for its source top to bottom; `?raw`
141gives a file as it is. `/lmjtfy.git` itself is the root folder, whose
142README is the prologue. On a phone the sidebar moves below the page.
143
144Everything is read from GitHub's API with the same token, kept a minute per
145isolate: folders and files from the contents API, the explorer's tree in one
146request from the git trees API. jevcrates is browsed under
147`third-party/jevcrates/` at the commit lmjtfy pins.
148
149## Storage
150
151Two Durable Objects keep everything that outlasts a request. Each is one
152object for the whole site (`id_from_name`), so every visitor reads the same
153rows.
154
155### The archive (`src/archive.rs`)
156
157A SQLite database, changed only by adding a step to `MIGRATIONS`. The tables
158share no keys, `events.browser` aside: an `asked` row's answers are what the `versions` behind it said,
159copied, and `counts` is a tally.
160
161`askers` is how a question counts each browser once. A browser's first page
162gives it a random id in a cookie (`lmjtfy_browser`, a year); when it asks, the
163Worker hashes the id with the question and the archive keeps only that. A
164browser asking again, or opening its own question from the feed, finds its
165row and is not counted, toasted or moved up the feed again. The rows of two
166questions from one browser have nothing in common, and the id cannot be had
167back from one. So the key says nothing about who asked; the `browser` column
168beside it, which the owner's backend reads, is the id itself. An ask with no cookie (a
169script) counts every time, as before. Counts from before 2026-10-02 stay as
170they were.
171
172`ratings` holds the 👍 and 👎 under each answer ("Was Jev right?"): one vote
173per browser, keyed the same way, and pressed again to take it back. A vote is
174on the answer as it was kept when it was cast, by the hash of its answers, so
175if the question is answered differently later, that answer starts with no
176votes and the old one keeps its own.
177
178```mermaid
179erDiagram
180  versions {
181    text sent_to PK "Jev's endpoint, or a Workers AI model id"
182    text request PK "the exact body sent"
183    integer version PK "1, 2, ...: each time it was sent, newest last"
184    text response "the exact body that came back"
185    text request_id "Jev's id for the call, if it gave one"
186    integer attempts "tries the client made"
187    real took_ms
188    real answered_ms "when it was answered"
189  }
190  asked {
191    text input PK "the question, cleaned"
192    text answers "JSON: one Answer per question Jev answered"
193    real asked_ms "last asked"
194    integer times "how often it was asked"
195    integer listed "1 if the feed may show it (Jev's fit fact)"
196    integer llm "1 if an LLM had to be asked"
197    integer moderated "the owner's say: 1 show, 0 hide, NULL leave it to listed"
198  }
199  events {
200    integer id PK
201    real at_ms "when"
202    text what "view, answer, gate, vote, more, card, fetch, moved, live, left, read, out"
203    text method
204    text host
205    text path
206    text query "as it came"
207    text input "the question typed, asked or voted on"
208    text detail "how an ask ended, which way a vote went, clone or pull"
209    real status
210    real sent "calls sent for it"
211    real kept "calls answered from versions"
212    real llm
213    real took_ms "an answer's time, or how long a page stayed"
214    real first "1 on a browser's first page ever"
215    real daily "1 on its first page of the UTC day"
216    real session "1 on its first page in half an hour"
217    text referrer "the page that linked here, whole"
218    text source "utm_source or ref"
219    text client "browser, git, bot, other"
220    text family "Chrome, Firefox, ..."
221    text os
222    text device "mobile or desktop"
223    text language "Accept-Language"
224    text agent "User-Agent"
225    text browser "the lmjtfy_browser cookie"
226    text ip
227    text country
228    text region
229    text city
230    text postcode
231    text timezone
232    real latitude
233    real longitude
234    real asn "the network's number"
235    text network "whose network: the ISP"
236    text colo "Cloudflare's data centre"
237    text protocol "HTTP and TLS versions"
238    text medium "utm_medium"
239    text campaign "utm_campaign"
240    text screen "1920x1080, as the page says"
241    text viewport "the window"
242    real scroll "how far down the page was seen, 0 to 1"
243  }
244  counts {
245    text name PK "sent, kept, clone:lmjtfy.git, pull:lmjtfy.git, ..."
246    integer n
247  }
248  askers {
249    text input PK "the question"
250    text who PK "SHA-256 of a browser's id and the question"
251    text browser "the browser's id itself, for the owner's reading"
252  }
253  ratings {
254    text input PK "the question"
255    text answer PK "SHA-256 of the answers as kept"
256    text who PK "as in askers"
257    integer vote "1 right, -1 wrong"
258    text browser "as in askers"
259  }
260  migrated {
261    integer version PK "MIGRATIONS steps run"
262  }
263```
264
265It also holds, in memory only, the calls on the wire (so identical asks wait
266on one) and the `/live` sockets of every open page.
267
268### The budgets (`src/meter.rs`)
269
270Key-value storage, a JSON value per key.
271
272```mermaid
273erDiagram
274  neurons {
275    integer day "days since 1970, UTC"
276    real used "Workers AI neurons spent that day"
277  }
278  jev_dollars {
279    integer day
280    real used "dollars of Jev spent that day"
281  }
282  account {
283    integer day
284    real at_ms "when Cloudflare's figure was read"
285    real neurons "the whole account's neurons that day"
286  }
287```
288
289Each visitor's count for the minute (`budget::Visits`) is in memory and
290written nowhere by the site.
291
292## What is kept about visitors
293
294Everything a request says, for the owner alone. Until 2026-10-03 the site
295kept nothing about who asked; the owner then ruled the other way ("anywhere
296in the app where we are dropping data we should plug"), so this is the true
297account now.
298
299Every request worth keeping is a row of `events` in the archive
300(`packages/archive/src/event.rs`): a page viewed, an ask and how it ended,
301each as-you-type request, a vote, a clone, a redirect from an old address, a
302page connecting and leaving, and what each page reports of itself as it is
303left (how long it was in view, how far down it was read, the screen) or when
304a link off the site is followed. A row has the question, the visitor's address,
305their browser's id (the `lmjtfy_browser` cookie, which ties one browser's
306rows together), the user agent, the referring page, and what Cloudflare says
307of where the request came from: the network (the ISP), country, region,
308city, postcode, timezone and coordinates. The Worker writes the row after
309the response has gone (`wait_until`), so keeping it delays nobody.
310
311None of it is shown on the site. A question Jev judged unfit for the feed
312was always kept (`asked.listed`); now so is every question that ended any
313other way, in `events`.
314
315Visits are counted from a second cookie, `lmjtfy_visit`, which holds the
316time the browser was last counted: a page view compares it with now and
317marks itself the browser's first ever, first today, or first in half an
318hour.
319
320### The admin door
321
322The owner reads all of this from a separate, private Worker, which binds
323this Worker's archive object (`script_name` in its `wrangler.toml`) and
324posts `archive::Admin` messages to the object's `/admin` path:
325
326- `Select`: one SQL statement that only reads (`archive::reads_only`), and
327  its rows back.
328- `Moderate`: the owner's say on whether a question is on the feed, kept in
329  `asked.moderated` beside Jev's own verdict in `listed`. The feed shows
330  `COALESCE(moderated, listed)`.
331
332This Worker has no route that reaches that path. A visitor's request is
333passed to the object only as a socket upgrade on `/live`.
334
335### What Cloudflare keeps
336
337Cloudflare, which runs the site, keeps its own record, in the owner's
338account and nowhere public:
339
340- **Workers Logs**, for 3 days: one entry per request the Worker or its
341  objects handle, with the request as it arrived, address included.
342- **Traces**, for 7 days: each request's timing, through the fetches and
343  the Durable Object calls it made.
344- **Metrics**, the request counts and errors every Worker has.
345- **Web Analytics**: on `lmjtfy.fun`, Cloudflare adds its beacon script
346  (`static.cloudflareinsights.com/beacon.min.js`) to each page it serves to
347  a browser, and the browser reports page views and load times to
348  `lmjtfy.fun/cdn-cgi/rum`. It sets no cookie. The Worker's own HTML does
349  not contain the script: Cloudflare adds it on the way out, and only for
350  browsers, so `curl` does not see it.
351
352The owner turned on the logs and traces (2026-10-02) and Web Analytics with
353the domain (2026-10-03). They are configured in
354[wrangler.toml](wrangler.toml) under `[observability]`.
355
356## Credentials
357
358All from 1Password, none ever written to a file here. TypeSafe's API refuses
359browser origins anyway, so every Jev call goes through the Worker and the
360key never reaches a page.
361
362- **`LMJTFY_TYPESAFE_API_KEY`**, lmjtfy's own Jev key, is a Worker secret.
363  Under `wrangler dev` it comes from the process environment, which
364  `op-env-run` fills from the host's 1Password Environment. Deployed, it is
365  set with `lmjtfy-secret`. Without it the Worker still runs, and the page
366  says Jev is offline.
367- **The Cloudflare API token.** Workers AI has no local emulation, so even
368  `wrangler dev` calls the real models on the real account. `lmjtfy-wrangler`
369  (from the owner's devshell) reads the token from 1Password per run and
370  execs wrangler with it.
371- **`LMJTFY_GITHUB_TOKEN`**, a Worker secret: the clone proxy's GitHub
372  fine-grained token, Contents: read on `deizel/lmjtfy` and `deizel/jevcrates`
373  and no other permission. GitHub's API cannot mint a fine-grained token, so
374  it is made on GitHub and kept in the host's 1Password Environment, which
375  `op-env-run` hands to `wrangler dev`; deployed, it is set with
376  `op-env-run -- lmjtfy-secret github`. Without it a clone is answered with 503.
377- **`CLOUDFLARE_ANALYTICS_TOKEN`** and **`CLOUDFLARE_ACCOUNT_ID`**, Worker
378  secrets, are how the budget object reads the account's usage. The token can
379  read analytics and nothing else; nixos-config's infra declares it
380  (`cloudflare_account_token.lmjtfy-analytics`). The dev server runs without
381  them and counts only itself.
382
383## In this folder
384
385| Path | What |
386| --- | --- |
387| [src/](src/) | The Worker's code. Chapter 3 walks through it. |
388| [wrangler.toml](wrangler.toml) | The Worker's name, the pinned Jev model, the chosen LLM, the Jev budget, the bindings and the declared secrets. |
389| [build.rs](build.rs) | Stamps the build with the commit it came from (`LMJTFY_BUILD`). |
390| [Cargo.toml](Cargo.toml) | The crate: a `cdylib` for the Worker, and an `rlib` so its tests run natively. |
391
392`build/` and `.wrangler/` are the build's and the dev server's and are not
393committed.
394
395← Previous: [Chapter 1, apps/](../) · Up: [apps](../) · Next: [Chapter 3, the source](src/) →