martyw.dev

Notes

Short-form — showerthoughts, links, observations, things that don't need a whole post.

TIL that CREATE TEMPORARY TABLE in Postgres takes an ON COMMIT clause:

CREATE TEMP TABLE staging (id bigint, payload jsonb) ON COMMIT DROP;

Three settings. PRESERVE ROWS is the default and keeps the table around for the whole session. DELETE ROWS truncates it at every commit. DROP bins the table itself at the end of the transaction that created it, which is almost always what I actually wanted — a scratch table for one bulk load or one gnarly multi-step query, gone the moment I’m done with it. No cleanup step, no DROP TABLE IF EXISTS at the top guarding against whatever the last run left lying around.

Postgres deviates from the standard here, incidentally: SQL says the default should be DELETE ROWS, and Postgres went with PRESERVE ROWS.

The thing that made it click was PgBouncer’s feature map, which lists ON COMMIT DROP temp tables as working under transaction pooling and PRESERVE/DELETE ROWS as “Never”. Of course it does — in transaction pooling the server connection goes back in the pool after each commit, so anything session-scoped either leaks into someone else’s transaction or vanishes. Scope the table to the transaction and there’s nothing left to leak.

Threw together Relink Codex — playstyles, combo routes, weapons and sigil loadouts for all 28 characters in Granblue Fantasy: Relink.

Two things collided. The expansion pulled me back into the game, I’m somewhere around Proud and trying to learn characters I’ve never touched, and the usual way to do that is trawling Reddit threads and pausing YouTube videos on the frame where the actual information is. I didn’t fancy it. The other thing is that I pay for Claude Max every month and reliably don’t get near the token allowance, which is its own quiet kind of waste — the plan costs the same whether I spend it or not.

So the site is less a project than the output of a research task I didn’t want to do by hand. Three power brackets rather than one canonical build, because a loadout that’s right at 12,000 PWR is wrong by the time Endless Ragnarok has you at 40,000. Astro, static, no backend.

It’s current as of patch 2.0.2 and will quietly stop being true the moment Cygames ships another one. But re-running the research costs me nothing I wasn’t already paying, which was most of the point.

Summer Game Fest dropped on Friday and somehow the lineup read like it was scraped straight from my wishlist. Three of my favourite franchises, all in one show, all within a couple of hours of each other. I keep waiting for the catch.

Code Veronica is finally getting the remake treatment — retitled just Resident Evil: Veronica, Claire back as the lead, the reveal opening on a rainy Paris apartment block. It’s been the obvious gap in Capcom’s remake run for years and they’ve finally filled it. 2027, and on Switch 2 alongside the usual platforms.

Then FF7 Remake Part 3 turned out to be Final Fantasy 7 Revelation — Hamaguchi on stage, the Highwind in your hands, skydiving into an open world, job classes in combat. Spring 2027. The trilogy actually lands.

And the one I didn’t see coming: Stellar Blade: Blood Rain. New protagonist — Evie, not Eve — swapping the sword for gauntlet brawling, and Shift Up self-publishing this time so it’s not a PlayStation exclusive. Still early, but pointed at 2027 too.

So that’s my three most-anticipated games sharing a release year and a single showcase. Either the universe owes me one or 2027 is going to cost me a fortune.

Spent the week deciding on a physics engine for the game engine and landed on Rapier. It’s written in Rust and ships to the browser as WASM, which sounds like overkill for a 2D hobby engine until you watch the alternatives fall over.

PhysicsJS was the first I crossed off — last meaningful release was years ago, and I’m not building on something nobody’s touched since the previous decade. Matter.js was the real contender: pure JS, easy to drop in, lovely docs, and a big enough community that every problem I’d hit has already been answered. But the maintenance has gone quiet, and once I started pushing body counts the frame budget got tight in a way I couldn’t profile my way out of.

Rapier wins on the two things I actually care about. It’s fast — the WASM core doesn’t blink at the body counts that made Matter.js sweat — and it’s deterministic across platforms, which matters the moment you think about networking or replays. The cost is a heavier, less JS-native API and a WASM blob to load. Worth it. I’d rather pay that upfront than fight the physics layer every time the simulation needs to grow.

The comments on here are down right now. They run on Cusdis — open-source, on their free hosted tier, and the most lightweight option I could find. I’ve used a few commenting systems over the years — FastComments, Disqus, that lot — and Cusdis was the leanest by a distance: no tracker payload, no account wall, just a textbox. The catch with lightweight-and-free is that when the hosted instance falls over, my comments fall over with it, and there’s nothing to do but wait.

Which is fine. Comments were never the point of this place, and only a handful of posts ever get any. But it’s a tidy illustration of what you sign up for when you lean on someone else’s free service: you’ve outsourced a feature and its uptime. I could write my own in an afternoon — it’s a form, a table, and a bit of moderation — but then the uptime would be mine too, and that’s the part nobody misses until it’s theirs.

New site, new shape. The old one was a developer-CV-with-blog. This one is broader — software, hardware, AI, the homestead, gaming, the occasional showerthought. Short stuff lives here in Notes; longer pieces under Posts.

Built with Astro and Tailwind on Cloudflare Workers. Most of the wiring — components, porting old articles, even the voice this note is written in — went through Claude Code. Worth a post of its own eventually.

The instinct when something’s slow is to put Redis in front of it. Sometimes that’s right. Often it’s a way to defer thinking about the actual problem, with the added bonus of now having two: the original slowness plus a cache that drifts out of sync with the truth in ways you can’t predict.

The question that filters most “let’s add a cache” proposals: how does it get invalidated? If the answer is “TTL and we’ll cross our fingers”, you’re not designing a cache, you’re rate-limiting how often your bug appears. A cache without a clear purging story — what triggers it, what gets purged, what happens when purging fails — is a wager that staleness will hurt less than slowness. Sometimes true. Rarely measured.

A common shape: fetch a flat list of rows, then group them in application code into a Map<parentId, Row[]> before returning. It works, it’s familiar, and it’s nearly always slower and more memory-hungry than asking the database to do it.

SELECT
  parent_id,
  ARRAY_AGG(id)::uuid[] AS child_ids
FROM children
WHERE parent_id = ANY($1)
GROUP BY parent_id;

One row per parent, the database handles the grouping, and the type cast keeps the application honest about what it’s getting back. MySQL has GROUP_CONCAT with the same idea. The harder ones to spot are the in-memory groupings already sitting in your codebase, doing it the slow way because that’s how it was written the first time.

The dataloader package ships with caching on. For the GraphQL resolver tree it was designed for that makes sense — stable reads within a single request are what you want, and the loader is garbage-collected with the request. Reuse a loader across requests or hold one for the lifetime of a server though and the cache becomes a Map that grows forever, never evicts, and quietly hands out stale reads.

I default to cache: false everywhere and only turn it on when the underlying data is genuinely small, immutable, and unambiguously worth the trouble — a lookup table of statuses, a tiny set of feature flags. Anything that can change while the process is running, the cache is off.