Posts in Javascript (20 found)
Michael Lynch 5 days ago

Read My Blog With Javascript

You can now read my blog with client-side JavaScript. I’m not sure why you’d want to, but you can. Maybe you want to add a blogroll to your site with a list of recent posts from your favorite blogs, but you don’t want to fetch them server side. If you wanted to use JavaScript to show my five most recent post titles, you’d write some code like this: The above code produces this output: I got the idea from a recent comment Simon Willison made on Lobsters . The only thing I had to change to enable JavaScript access was to set Cross-Origin Resource Sharing (CORS) HTTP headers for my RSS feeds. Typically, the thing that prevents other sites from reading each other’s content with client-side JavaScript is same-origin policy . Same-origin policy says that each site (origin) can only read resources associated with its own domain. So, if you visit my blog at mtlynch.io, and I have JavaScript that tries to read your bank balance from chase.com, same-origin policy forbids JavaScript on my page from accessing that information. To allow other websites to read my blog through JavaScript, I had to set CORS headers for my blog’s RSS feeds. This blog is a static site that I currently host on Netlify, so I had to change my Netlify configuration file to specify CORS headers for my RSS feed URLs : CORS is a security mechanism, and I’m always appropriately hesitant to relax security restrictions on an Internet-facing site. But I spent a while considering the risks, and I don’t see any meaningful risk in my situation. The purpose of same-origin policy is that if a user has sensitive data on site A, then site B can’t access any of it unless site A explicitly gives permission. My blog is a static site with no secret information. When I visit mtlynch.io, I see the exact same thing everyone else does. So, even if an attacker convinces me to run malicious JavaScript on a third-party site, the code can’t abuse my privileges on my blog because I don’t have any special privileges here. I do use TalkYard for embedded commenting, and I have admin privileges there, but CORS settings for my RSS feeds don’t affect TalkYard. At first, I thought an attacker might abuse CORS settings to launch a DDoS attack against my site. What if the attacker gets millions of people to visit a page that has JavaScript in the background to constantly request my RSS feed? Then, I realized such a DDoS attack is already possible. CORS only controls whether a third-party domain can read the results of a request, but it doesn’t block the request in the first place. Any domain can make GET and POST requests to any other domain, regardless of CORS settings. Another attack I considered is impersonation. A visitor can go to evil.example.com, and the server there could send back JavaScript that reconstructs my blog in the visitor’s browser even though the URL bar still says evil.example.com. This attack could work, but there are simpler ways to impersonate my blog, regardless of my CORS settings. An impostor can run an HTTP proxy that forwards requests to my blog, which is a simpler way to impersonate my blog. Or, they could just scrape my site and host it somewhere else. Nobody asked me to enable CORS for my RSS feeds, so I don’t know that it actually benefits anyone. But I enjoy the open web and this is an interesting way to allow other sites to interoperate with mine, so I figured why not? If you end up doing anything interesting with my RSS feeds as a result of this change, let me know .

0 views
マリウス 6 days ago

Alpine Linux on a Bare Metal Server

When I began work on 📨🚕 ( MSG.TAXI ) , I kept things deliberately low-key, since I didn’t want it turning into a playground for architecture astronauts . For the web console’s tech stack, I went with the most boring yet easy-to-master CRUD stack I could find , that doesn’t depend on JavaScript . And while deploying Rails in a sane way (without resorting to cOnTaInErS ) is a massive PITA, thanks to the Rails author’s cargo-cult mentality and his followers latching onto every half-baked wannabe-revolutionary idea, like Kamal and more recently Omarchy , as if it were a philosopher’s stone, from a development perspective it’s still the most effective getting-shit-done framework I’ve used to date. Best of all, it doesn’t rely on JavaScript (aside from actions, which can be avoided with a little extra effort). Similarly, on the infrastructure side, I wanted a foundation that was as lightweight as possible and wouldn’t get in my way. And while I’m absolutely the type of person who would run a Gentoo server, I ultimately went with Alpine Linux due to its easier installation, relatively sane defaults (with a few exceptions, more on that later ), and its preference for straightforward, no-nonsense tooling that doesn’t try to hide magic behind the scenes. “Why not NixOS?” you might ask. Since I’m deploying a lightweight, home-brewed Ruby/Rails setup alongside a few other components, I didn’t see the point of wrapping everything as Nix packages just to gain the theoretical benefits of NixOS. In particular, the CI would have taken significantly longer, while the actual payoff in my case would have been negligible. Since I’m paying for 📨🚕 out of my own pocket, I wanted infrastructure that’s cheap yet reliable. With plenty of people on the internet praising Hetzner , I ended up renting AMD hardware in one of their Finnish datacenters. Hetzner doesn’t offer as many Linux deployment options as cloud providers like Vultr , so I had to set up Alpine myself, which was pretty straightforward. To kickstart an Alpine installation on a Hetzner system, you just need to access the server’s iPXE console, either by renting a Hetzner KVM for an hour or by using their free vKVM feature. From there, you can launch the Alpine Linux by initializing the network interface and chain-loading the file: From that point on setup should be easy thanks to Alpine’s installer routine. If you’re using Hetzner’s vKVM feature to install Alpine, this chapter is for you. Otherwise, feel free to skip ahead. vKVM is a somewhat hacky yet ingenious trick Hetzner came up with, and it deserves a round of applause. If you’re curious about how it works under the hood, rent a real KVM once and reboot your server into vKVM mode. What you’ll see is that after enabling vKVM in Hetzner’s Robot , iPXE loads a network image, which boots a custom Linux OS. Within that OS, Hetzner launches a QEMU VM that uses your server’s drives to boot whatever you have installed. It’s basically Inception at the OS level. As long as vKVM is active (meaning the iPXE image stays loaded), your server is actually running inside this virtualized environment, with display output forwarded to your browser. Run while in vKVM mode and you’ll see, for example, your NIC showing up as a VirtIO device. Here’s the catch: When you install Alpine through this virtualized KVM environment, it won’t generate the your physical server actually needs. For instance, if your server uses an NVMe drive, you may discover that doesn’t include the module, causing the OS to fail on boot. Hetzner’s documentation doesn’t mention this, and it can easily bite you later. Tl;dr: If you installed your system via vKVM , make sure your includes all necessary modules. After updating , regenerate the . There are several ways to do this, but I prefer . Always double-check that the regenerated really contains everything you need. Unfortunately Alpine doesn’t provide tools for this, so here’s a .tar.gz with Debian’s and . Extract it into , and note that you may need to for them to work properly, due to Alpine’s somewhat annoying defaults (more on that later ). Finally, after rebooting, make sure you’ve actually left the vKVM session. You can double check by running . If the session is still active (default: 1h), your system may have just booted back into the VM, which you can identify by its Virt-devices. As soon as your Alpine Linux system is up and running there are a couple of things that I found important to change right off the bat. Alpine’s default boot timeout is just 1 second, set in ( ). If you ever need to debug a boot-related issue over a high-latency KVM connection, you will dread that 1-second window. I recommend increasing it to 5 seconds and running to apply the change. In practice, you hopefully won’t be rebooting the server that often, so the extra four seconds won’t matter day-to-day. Alpine uses the classic to configure network settings. On Hetzner’s dedicated servers, you can either continue using DHCP for IPv4 or set the assigned IP address statically. For IPv6, you’ll be given a subnet from which you can choose your own address. Keep in mind that the first usable IPv6 on Hetzner’s dedicated servers is : Amongst the first things you do should be disabling root login and password authentication via SSH: Apart from that you might want to limit the type of key exchange methods and algorithms that your SSH server allows, depending on the type of keys that you’re using. Security by obscurity: Move your SSH server from its default port (22) to something higher up and more random to make it harder for port-scanners to hit it. Finicky but more secure: Implement port knocking and use a handy client to open the SSH port for you only, for a limited time only. Secure: Set up a small cloud instance to act as Wireguard peer and configure your server’s SSH port to only accept connections from the cloud instance using a firewall rule . Use Tailscale if a dedicated Wireguard instance is beyond your expertise. You will likely want to have proper (GNU) tools around, over the defaults that Alpine comes with ( see below ). Some of the obvious choices include the following: In addition, I also like to keep a handful of convenience tools around: This is a tricky part because everyone’s monitoring setup looks different. However, there are a few things that make sense in general. Regardless what you do with your logs it’s generally a good idea to switch from BusyBox to something that allows for more advanced configurations, like syslog-ng : You probably should have an overview of how your hardware is doing. Depending on what type of hard drives your server has, you might want to install the or packages. UFW is generally considered an uncomplicated way to implement firewalling without having to complete a CCNP Security certification beforehand: Depending on your SSH setup and whether you are running any other services that could benefit from it, installing Fail2Ban might make sense: The configuration files are located at and you should normally only create/edit the files. The easiest way to backup all the changes that you’ve made to the general configuration is by using , the integrated Alpine local backup solution that was originally intended as a tool to manage diskless mode installations. I would, however, recommend to manually back up installed packages ( ) and use Restic for the rest of the system, including configuration files and important data, e.g.: However, backups depend on the data that your system produces and your desired backup target. If you’re looking for an easy to use, hosted but not-too-expensive one-off option, then Tarsnap might be for you. You should as well look into topics like local mail delivery, system integrity checks (e.g. AIDE ) and intrusion detection/prevention (e.g. CrowdSec ). Also, if you would like to get notified for various server events, check 📨🚕 ( MSG.TAXI ) ! :-) One of the biggest annoyances with Alpine is BusyBox : You need SSH? That’s BusyBox. The logs? Yeah, BusyBox. Mail? That’s BusyBox, too. You want to untar an archive? BusyBox. What? It’s gzipped? Guess what, you son of a gun, gzip is also BusyBox. I understand why Alpine chose BusyBox for pretty much everything, given the context that Alpine is most often used in ( cOnTaInErS ). Unfortunately, most BusyBox implementations are incomplete or incompatible with their full GNU counterparts, leaving you wondering why something that worked flawlessly on your desktop Linux fails on the Alpine box. By the time I finished setting up the server, there was barely any BusyBox tooling left. However, I occasionally had to resort to some odd trickery to get things working. You now have a good basis to set up whatever it is that you’re planning to use the machine for. Have fun! Footnote: The artwork was generated using AI and further botched by me using the greatest image manipulation program .

0 views
crtns 1 weeks ago

On Being Blocked From Contributing to lodash

My Github account was blocked from contributing security improvements to the lodash project. This was my first open source work in a while, and unfortunately, it appears it was a waste of time. That said, I did learn a few lessons about contributing to open source projects that others might benefit from. I've been going down a rabbit hole to figure out how to improve supply chain security in the JavaScript ecosystem. A common problem is the inability to trust the true origin of code within a package and how that package was built and published. Phishing attacks on npm registry users exploit this weakness - threat actors with stolen credentials will directly publish malicious package versions to the npm registry, bypassing CI/CD entirely. A consumer of the package will be none the wiser. will happily install the malicious package version if configured to do so. One way to detect this type of attack is through package provenance. Publishing packages with provenance is the process of creating a signed statement of attestation during the building of a package so that the build process can later be verified. The statement of attestation contains metadata about the build, provided in part by a trusted CI/CD platform, like Github Actions runners. While not a security panacea, dependent projects can check the statement of attestation to detect whether a package was published by a trusted CI/CD process or directly uploaded to the artifact registry, bypassing CI/CD. The npm client itself has made the process of generating and publishing a statement of attestation trivial - all you need is a Github Actions workflow that runs the command with these flags. For packages that already use Github as a code forge, adopting a workflow to publish packages with these additional flags is, in most cases, a low-effort task. Despite this, the adoption of package provenance appears abysmally low. Of the ten most popular packages on the npm registry, only two are published with provenance. Even the package is not published with provenance. I've always felt like an outsider in the world of open source - mostly a consumer, not a contributor. It occurred to me that since I value having npm packages published with provenance, I could be the one to push those PRs. This could be my way of giving something back. Among the top ten packages on npmjs.com without provenance, lodash was the one that caught my eye. I have used lodash many times, both professionally and personally. What stood out specifically about it is that there has not been a new release in over 5 years, but it's still being downloaded tens of millions of times per week. When I went to the Github repo, I saw the main branch had not received a new commit in 9 months. When I checked for a workflow to publish the package, I found nothing. I saw what I thought was a good opportunity. I was certain I could figure out the build process, create a new Github Actions workflow to automatically build lodash, and add provenance to boot. I figured if my initial PR was rejected, I could at least start a conversation about supply chain security and reproducible builds for this very popular project. Within a few hours, I had a workflow that was mostly working. I reverse-engineered the packaging workflow by digging through git history, reading docs and the wiki. I even managed to publish a forked version of lodash to npmjs with provenance after some trial and error. Unfortunately for me, that trial and error included opening a PR against the lodash repo before I was ready. I quickly closed the PR because I realized that the build artifacts in my version were not quite a 1-to-1 match with what was in the latest build of lodash. I spent a few more hours in vain trying to figure out how to replicate the build. Eventually, I called it a day and decided to open an issue in the morning to ask the last maintainer who published a build how they managed to generate it. I quickly typed up a Github issue the next morning and pressed "Create". Nothing happened. I looked around and saw an error I had never seen before on Github: "Error - Failed to create the issue." I figured Github was having an outage, so I took a break and did something else. When I tried again though, I saw the same error repeatedly, even after changing the title and content of the issue several times in the off chance I was triggering some kind of blocklist. I decided to try and reproduce the issue in my own repository. No error - I was able to create a Github issue successfully. At this point, I suspected something was up specifically with lodash but I wasn't sure what. I went back to my closed PR on a hunch and noticed some new activity. The PR, despite being already closed, was locked and limited to collaborators. At this point, I was fairly certain I had been blocked. What finally gave it away was when I tried to use the watch action on the lodash Github repo, I got a truly bizarre and nonsensical error - I couldn't watch more than 10,000 repos on Github. I don't even watch 100 repos, and I could watch any other repo I wanted, outside of the lodash organization. In an attempt to reach out to clear up any misunderstandings, I opened a Github issue on my forked repo, tagged the lodash org and primary maintainer, and explained the situation. I gave context as to why I had opened the PR, and what I wanted to achieve. Two weeks later, no response. As a last-ditch effort, I sent an email directly to the same primary maintainer, using an email I found in git commit history, but I have also received no response there either. At this point, I believe I've been permanently blocked. While the effort I ultimately put into this project appears not to have furthered my goals, at least this was a learning experience. My contributions being rejected reinforces a belief I already held before going into this project - open source maintainers don't owe me anything. That said, I'm still confused as to why I was blocked for opening a single PR. I wanted to spark a conversation on how to publish a new version of lodash and ultimately make the build process transparent and trustworthy. I'm genuinely curious if my PR was viewed incorrectly as malicious, or if the maintainer who blocked me simply has no interest in what I was trying to do and is signaling that to the lodash community. When I have time, I will continue to try and add provenance to open source NPM packages where I believe there is value, but I will start slowly and open an issue first to discuss the change. If there's interest, I'll create a pull request. If I'm ignored, I'll move on. My mistake with lodash was jumping in headfirst without gauging the interest of the maintainers or getting a better sense of what was happening in the project. I found out after the fact that the primary maintainer of lodash declared "issue bankruptcy" back in 2023 , closing a slew of open Github issues and starting from scratch, and that a major rewrite of the codebase seems to have stalled out with no progress in 11 months. While the CONTRIBUTING.md in the repo indicates "Contributions are always welcome", I mistakenly believed that demonstrating enthusiasm through a pull request was the best way to contribute to open source. I should have known better. As a professional software engineer, I've learned this lesson before: more effort upfront doesn't guarantee results. A five-minute conversation to gauge interest can save hours of work on an unwanted PR.

0 views
iDiallo 1 weeks ago

How to Get Started Programming: Build a Blog

The moment I learned how to program, I wanted to experiment with my new super powers. Building a BMI calculator in the command line wouldn't cut it. I didn't want to read another book, or follow any other tutorial. What I wanted was to experience chaos. Controlled, beautiful, instructive chaos that comes from building something real and watching it spectacularly fail. That's why whenever someone asks me how they can practice their new found skill, I suggest something that might sound old-fashioned in our framework-obsessed world. Build your own blog from scratch. Not with WordPress. Not with Next.js or Gatsby or whatever the cool kids are using this week. I mean actually build it. Write every messy, imperfect line of code. A blog is deceptively simple. On the surface, it's just text on a page. But underneath? It's a complete web application in miniature. It accepts input (your writing). It stores data (your posts). It processes logic (routing, formatting, displaying). It generates output (the pages people read). When I was in college, I found myself increasingly frustrated with the abstract nature of what we were learning. We'd implement different sorting algorithms, and I'd think: "Okay, but when does this actually matter ?" We'd study data structures in isolation, divorced from any practical purpose. It all felt theoretical, like memorizing chess moves without ever playing a game. Building a blog changed that completely. Suddenly, a data structure wasn't just an abstract concept floating in a textbook. It was the actual list of blog posts I needed to sort by date. A database wasn't a theoretical collection of tables; it was the real place where my article drafts lived, where I could accidentally delete something important at 2 AM and learn about backups the hard way. This is what makes a blog such a powerful learning tool. You can deploy it. Share it. Watch people actually read the words your code is serving up. It's real. That feedback loop, the connection between your code and something tangible in the world, is irreplaceable. So how do you start? I'm not going to give you a step-by-step tutorial. You've probably already done a dozen of those. You follow along, copy the code, everything works perfectly, and then... you close the browser tab and realize you've learned almost nothing. The code evaporates from your memory because you never truly owned it. Instead, I'm giving you permission to experiment. To fumble. To build something weird and uniquely yours. You can start with a single file. Maybe it's an that clumsily echoes "Hello World" onto a blank page. Or perhaps you're feeling adventurous and fire up a Node.js server with an that doesn't use Express to handle a simple GET request. Pick any language you are familiar with and make it respond to a web request. That's your seed. Everything else grows from there. Once you have that first file responding, the questions start arriving. Not abstract homework questions, but real problems that need solving. Where do your blog posts live? Will you store them as simple Markdown or JSON files in a folder? Or will you take the plunge into databases, setting up MySQL or PostgreSQL and learning SQL to and your articles? I started my first blog with flat files. There's something beautiful about the simplicity. Each post is just a text file you can open in any editor. But then I wanted tags, and search, and suddenly I was reinventing databases poorly. That's when I learned why databases exist. Not from a lecture, but from feeling the pain of their absence. You write your first post. Great! You write your second post. Cool! On the third post, you realize you're copying and pasting the same HTML header and footer, and you remember learning something about DRY (don't repeat yourself) in class. This is where you'll inevitably invent your own primitive templating system. Maybe you start with simple includes: at the top of each page in PHP. Maybe you write a JavaScript function that stitches together HTML strings. Maybe you create your own bizarre templating syntax. It will feel like magic when it works. It will feel like a nightmare when you need to change something and it breaks everywhere. And that's the moment you'll understand why templating engines exist. I had a few blog posts written down on my computer when I started thinking about this next problem: How do you write a new post? Do you SSH into your server and directly edit a file with vim? Do you build a crude, password-protected page with a textarea that writes to your flat files? Do you create a whole separate submission form? This is where you'll grapple with forms, authentication (or a hilariously insecure makeshift version of it), file permissions, and the difference between GET and POST requests. You'll probably build something that would make a security professional weep, and that's okay. You'll learn by making it better. It's one thing to write code in a sandbox, but a blog needs to be accessible on the Internet. That means getting a domain name (ten bucks a year). Finding a cheap VPS (five bucks a month). Learning to into that server. Wrestling with Nginx or Apache to actually serve your files. Discovering what "port 80" means, why your site isn't loading, why DNS takes forever to propagate, and why everything works on your laptop but breaks in production. These aren't inconveniences, they're the entire point. This is the knowledge that separates someone who can write code from someone who can ship code. Your blog won't use battle-tested frameworks or well-documented libraries. It will use your solutions. Your weird routing system. Your questionable caching mechanism. Your creative interpretation of MVC architecture. Your homemade caching will fail spectacularly under traffic ( what traffic?! ). Your clever URL routing will throw mysterious 404 errors. You'll accidentally delete a post and discover your backup system doesn't work. You'll misspell a variable name and spend three hours debugging before you spot it. You'll introduce a security vulnerability so obvious that even you'll laugh when you finally notice it. None of this is failure. This is the entire point. When your blog breaks, you'll be forced to understand the why behind everything. Why do frameworks exist? Because you just spent six hours solving a problem that Express handles in three lines. Why do ORMs exist? Because you just wrote 200 lines of SQL validation logic that Sequelize does automatically. Why do people use TypeScript? Because you just had a bug caused by accidentally treating a string like a number. You'll emerge from this experience not just as someone who can use tools, but as someone who understands what problems those tools were built to solve. That understanding is what transforms a code-copier into a developer. Building your own blogging engine used to be a rite of passage. Before Medium and WordPress and Ghost, before React and Vue and Svelte, developers learned by building exactly this. A simple CMS. A place to write. Something that was theirs. We've lost a bit of that spirit. Now everyone's already decided they'll use React on the frontend and Node on the backend before they even know why. The tools have become the default, not the solution. Your blog is your chance to recover that exploratory mindset. It's your sandbox. Nobody's judging. Nobody's watching. You're not optimizing for scale or maintainability or impressing your coworkers. You're learning, deeply and permanently, by building something that matters to you. So here's my challenge: Stop reading. Stop planning. Stop researching the "best" way to do this. Create a folder. Create a file. Pick a language and make it print "Hello World" in a browser. Then ask yourself: "How do I make this show a blog post?" And then: "How do I make it show two blog posts?" And then: "How do I make it show the most recent one first?" Build something uniquely, personally, wonderfully yours. Make it ugly. Make it weird. Make it work, then break it, then fix it again. Embrace the technical chaos. This is how you learn. Not by following instructions, but by discovering problems, attempting solutions, failing, iterating, and eventually (accidentally) building something real. Your blog won't be perfect. It will probably be kind of a mess. But it will be yours, and you will understand every line of code in it, and that understanding is worth more than any tutorial completion certificate. If you don't know what that first blog post will be, I have an idea. Document your process of building your very own blog from scratch. The blog you build to learn programming becomes the perfect place to share what programming taught you. Welcome to development. The real kind, where things break and you figure out why. You're going to love it.

0 views
Grumpy Gamer 1 weeks ago

Git, JSON and Markdown walk into bar

I want to talk about three things that has fundamentally changed my dev-life. There are a lot of things, like ImGUI, that are very amazing and useful but they don’t provide a general solution across many problems. In no particular order… I’ve been using Git since 2010 and it really has changed my dev-life. I’d used version control before that, mainly Perform, SVN and PVSC, but Git felt nice and unobtrusive and I like that everything was local until I pushed to the server. It’s both nice and annoying that you can’t lock files (like art). You can (kind of) with LFS but that feels tacked on and not ready for primetime. Don’t think so? Try explaining installing and using it to a non-technical artist sometime. Git can be frustrating if you’re trying to do anything but the basics. Accidentally check in a secret file months ago and need to scrub it? Good luck with that. There are ways but it requires a lot of Git-Fu. I mainly use a GUI for git ( Fork ) and that takes most of the pain away. I do use the command line, but mostly in automation scripts. Before Markdown became the de-facto standard, I used my own custom format. It worked but wasn’t great and only I understood it. Markdown has it’s issues when you start using the more esoteric features. I’m also annoyed at bold and italics notation. Why is italics and bold is ? Why not and . That would make a lot more sense to me. I also have issue with it’s creator, John Gruber. He is a highly annoying smug Apple Fanboy. His writing was fine in the early days when Apple was #3, but got intolerable as Apple became the 800lb gorilla. It’s changed recently as Apple has snubbed him but I still can’t read anything he writes. But, I like his Markdown. I use JSON for just about every data file format in my games. JSON was created by Douglas Crockford as a notation for Javascript objects. I worked with Doug Crockford at Lucasfilm for several years. I always had a lot of respect for Doug and was somewhat intimidated (in a good way) by him. Doug was also the producer for the Nintendo Maniac Mansion. As much as I love JSON, there are some things about it that annoy me. I dislike that trailing commas are not allowed There is no need for this and it makes writing out valid JSON more complex. I also don’t like you that have to wrap keys names in quotes if they are simple ascii. I wrote a custom JSON parser I use in all my games that relaxes these, but then general JSON readers fail on my data.

0 views
Dayvster 1 weeks ago

Is Odin Just a More Boring C?

## Why I Tried Odin ### Background My recent posts have been diving deep into Zig and C, a shift from my earlier focus on React and JavaScript. This isn’t a pivot but a return to my roots. I started programming at 13 with C and C++, and over the years, I’ve built a wide range of projects in systems programming languages like C, C++, Rust, and now Zig. From hobby experiments and custom Linux utilities to professional embedded systems work think vehicle infotainment, tracking solutions, and low-level components I’ve always been drawn to the power and precision of systems programming. Alongside this, I’ve crafted tools for my own environment and tackled plenty of backend engineering, blending my full-stack expertise with a passion for low-level control. ### Why Odin Caught My Eye I like many others initially dismissed Odin as that language that was primarily intended for game development. It took me a moment or should I say many moments to realize just how stupid that notion was. Because let's analyze what game development actually means, it means building complex systems that need to be efficient, performant and reliable. It means working with graphics, physics, input handling, networking and more. It means dealing with concurrency, memory management and low level optimizations. In other words, game development is a perfect fit for a systems programming language like Odin. So basically if it's intended for game development, then it should be a great fit for general systems programming, desktop applications and since game dev usually means manual memory management without a garbage collector, it should also be possible to some extent to use it for embedded systems. So after I've gave myself a good slap on the forehead for being a bit of an idiot. I decided why not give Odin a fair shot and build something useful with it. ## The Project Now I may have been a bit liberal with the word useful there, what I actually decided to build was something that I usually like to build whenever I wanna try out a new language, namely a tiny key-value store with a pub/sub system. It won't win any awards for originality and I'm pretty sure the folks over at redis aren't exactly shaking in their boots. It is the most basic most barebones implementation of both lacking any real useful features that would make it usable in a production environment. But it is a good exercise in understanding the language and its capabilities. Mainly because it involves a few different aspects of programming that are relevant to systems programming. It involves data structures, memory management, concurrency and networking. And even if you create something as basic and lacking as I have in this example, you still have room for experimentation and exploration to add more features. ### Building a Tiny KV Store With Pub/Sub My initial minimal proof of concept was simple and straightforward. ```odin package main import "core:fmt" import "core:time" KVStore :: struct { store: map[string]string, } kvstore_init :: proc() -> KVStore { return KVStore{store = map[string]string{}} } kv_put :: proc(kv: ^KVStore, key: string, value: string) { kv.store[key] = value } kv_get :: proc(kv: ^KVStore, key: string) -> string { if value, ok := kv.store[key]; ok { return value } return "" } PubSub :: struct { subscribers: map[string][]proc(msg: string), } pubsub_init :: proc() -> PubSub { return PubSub{subscribers = map[string][]proc(msg: string){}} } subscribe :: proc(ps: ^PubSub, topic: string, handler: proc(msg: string)) { if arr, ok := ps.subscribers[topic]; ok { new_arr := make([]proc(msg: string), len(arr)+1); for i in 0..<len(arr) { new_arr[i] = arr[i]; } new_arr[len(arr)] = handler; ps.subscribers[topic] = new_arr; } else { ps.subscribers[topic] = []proc(msg: string){handler}; } } publish :: proc(ps: ^PubSub, topic: string, msg: string) { if handlers, ok := ps.subscribers[topic]; ok { for handler in handlers { handler(msg); } } } kv: KVStore; main :: proc() { kv = kvstore_init(); ps := pubsub_init(); handler1 :: proc(msg: string) { fmt.println("Sub1 got:", msg); kv_put(&kv, "last_msg", msg); } handler2 :: proc(msg: string) { fmt.println("Sub2 got:", msg); } handler3 :: proc(msg: string) { fmt.println("Sub3 got:", msg); } subscribe(&ps, "demo", handler1); subscribe(&ps, "demo", handler2); subscribe(&ps, "demo", handler3); publish(&ps, "demo", "Welcome to dayvster.com"); time.sleep(2 * time.Second); publish(&ps, "demo", "Here's another message after 2 seconds"); last := kv_get(&kv, "last_msg"); fmt.println("Last in kvstore:", last); } ``` As you can see it currently lacks any real error handling, concurrency and persistence. But it does demonstrate the basic functionality of a key-value store with pub/sub capabilities. What I have done is created two main structures, `KVStore` and `PubSub`. The `KVStore` structure contains a map to store key-value pairs and provides functions to put and get values. The `PubSub` structure contains a map of subscribers for different topics and provides functions to subscribe to topics and publish messages. The `main` function initializes the key-value store and pub/sub system, defines a few handlers for incoming messages, subscribes them to a topic, and then publishes some messages to demonstrate the functionality. From this basic example we've explored how to handle memory management in Odin, how to work with data structures like maps and slices, and how to define and use procedures. ### Memory Management Like C and Zig, Odin employs manual memory management, but it offers user-friendly utilities to streamline the process, much like Zig, in contrast to C’s more rudimentary approach. For instance, the `make` function in Odin enables the creation of slices with a defined length and capacity, akin to Zig’s slice allocation. In the code above, `make([]proc(msg: string), len(arr)+1)` generates a slice of procedure pointers with a length of `len(arr)+1`. Essentially, it allocates memory on the heap and returns a slice header, which includes a pointer to the allocated memory, along with the length and capacity of the slice. **but how and when is that memory freed?** In this code, memory allocated by `make` (e.g., for the slice in `subscribe`) and for maps (e.g., `kv.store` and `ps.subscribers`) is not explicitly freed. Since this is a short-lived program, the memory is reclaimed by the operating system when the program exits. However, in a long-running application, you’d need to use Odin’s delete procedure to free slices and maps explicitly. For example: ```odin kvstore_deinit :: proc(kv: ^KVStore) { delete(kv.store); } pubsub_deinit :: proc(ps: ^PubSub) { for topic, handlers in ps.subscribers { delete(handlers); } delete(ps.subscribers); } ``` So let's add that in the `main` function before it exits to ensure we clean up properly: ```odin // ... existing code ... main :: proc() { // ... existing code ... pubsub_deinit(&ps); kvstore_deinit(&kv); } // end of main ``` Well would you look at that, we just added proper memory management to our tiny KV store with pub/sub system and all it took was a few lines of code. I'm still a huge fan of C but this does feel nice and clean, not to mention really readable and easy to understand. Is our code now perfect and fully memory safe? Not quite, it still needs error handling and thread safety(way later) for production use, but it’s a solid step toward responsible memory management. ### Adding concurrency Enhancing Pub/Sub with Concurrency in Odin To make our pub/sub system more realistic, we've introduced concurrency to the publish procedure using Odin's core:thread library. This allows subscribers to process messages simultaneously, mimicking real-world pub/sub behavior. Since handler1 modifies kv.store via kv_put, we've added a mutex to KVStore to ensure thread-safe access to the shared map. Here's how it works: - **Concurrent Execution with Threads**: The publish procedure now runs each handler in a separate thread created with thread.create. Each thread receives the handler and message via t.user_args, and thread.start kicks off execution. Threads are collected in a dynamic array (threads), which is cleaned up using defer delete(threads). The thread.join call ensures the program waits for all threads to finish, and thread.destroy frees thread resources. This setup enables handler1, handler2, and handler3 to process messages concurrently, with output order varying based on thread scheduling. - **Thread Safety with Mutex**: Since handler1 updates kv.store via kv_put, concurrent access could lead to race conditions, as Odin's maps aren't inherently thread-safe. To address this, a sync.Mutex is added to KVStore. The kv_put and kv_get procedures lock the mutex during map access, ensuring only one thread modifies or reads kv.store at a time. The mutex is initialized in kvstore_init and destroyed in kvstore_deinit. ```odin publish :: proc(ps: ^PubSub, topic: string, msg: string) { if handlers, ok := ps.subscribers[topic]; ok { threads := make([dynamic]^thread.Thread, 0, len(handlers)) defer delete(threads) // Allocate ThreadArgs for each handler thread_args := make([dynamic]^ThreadArgs, 0, len(handlers)) defer { for args in thread_args { free(args) } delete(thread_args) } for handler in handlers { msg_ptr := new(string) msg_ptr^ = msg t := thread.create(proc(t: ^thread.Thread) { handler := cast(proc(msg: string)) t.user_args[0] msg_ptr := cast(^string) t.user_args[1] handler(msg_ptr^) free(msg_ptr) }) t.user_args[0] = rawptr(handler) t.user_args[1] = rawptr(msg_ptr) thread.start(t) append(&threads, t) } for t in threads { thread.join(t) thread.destroy(t) } } } ``` This implementation adds concurrency by running each handler in its own thread, allowing parallel message processing. The mutex ensures thread safety for kv.store updates in handler1, preventing race conditions. Odin's core:thread library simplifies thread management, offering a clean, pthread-like experience. Odin’s threading feels like a bit like C’s pthreads but without the usual headache, and it’s honestly a breeze to read and write. For this demo, the mutex version keeps everything nice and tidy, However in a real application, you'd still want to consider more robust error handling and possibly a thread pool for efficiency and also some way to handle thread lifecycle and errors and so on... ## Adding Persistence I haven't added persistence to this code-block personally because I feel that would quickly spiral the demo that I wanted to keep simple and focused into something much more complex. But if you wanted to add persistence, you could use Odin's `core:file` library to read and write the `kv.store` map to a file. You would need to serialize the map to a string format (like `JSON` or `CSV`) when saving and deserialize it when loading. Luckily odin has `core:encoding/json` and `core:encoding/csv` libraries that can help with this. Which should at the very least make that step fairly trivial. So if you feel like it, give it a shot and let me know how it goes. Do note that this step is a lot harder than it may seem especially if you want to do it properly and performantly. ## Now to Compile and Run Now here's the thing the first time I ran `odin build .` I thought I messed up somewhere because, it basically took a split second and produced no output no warnings no nothing. But I did see that a binary was produced named after the folder I was in. So I ran it with ```bash ❯ ./kvpub Sub1 got: Welcome to dayvster.com Sub2 got: Welcome to dayvster.com Sub3 got: Welcome to dayvster.com Sub1 got: Here's another message after 2 seconds Sub2 got: Here's another message after 2 seconds Sub3 got: Here's another message after 2 seconds Last in kvstore: Here's another message after 2 seconds ``` And there you have it, a tiny key-value store with pub/sub capabilities built in Odin. That compiled bizarrely fast, in fact I used a util ([pulse](https://github.com/dayvster/pulse)) I wrote to benchmark processes and their execution time and it clocked in at a blazing 0.4 seconds to compile ```bash ❯ pulse --benchmark --cmd 'odin build .' --runs 3 ┌──────────────┬──────┬─────────┬─────────┬─────────┬───────────┬────────────┐ │ Command ┆ Runs ┆ Avg (s) ┆ Min (s) ┆ Max (s) ┆ Max CPU% ┆ Max RAM MB │ ╞══════════════╪══════╪═════════╪═════════╪═════════╪═══════════╪════════════╡ │ odin build . ┆ 3 ┆ 0.401 ┆ 0.401 ┆ 0.401 ┆ 0.00 ┆ 0.00 │ └──────────────┴──────┴─────────┴─────────┴─────────┴───────────┴────────────┘ ``` Well I couldn't believe that so I ran it again this time with `--runs 16` to get a better average and it still came in at a very respectable `0.45` (MAX) seconds. **OK that is pretty impressive.** but consistent maybe my tool is broken? I'm not infallible after all. So I re-confirmed it why `hyperfine` and it came out at: ```bash ❯ hyperfine "odin build ." Benchmark 1: odin build . Time (mean ± σ): 385.1 ms ± 12.5 ms [User: 847.1 ms, System: 354.6 ms] Range (min … max): 357.3 ms … 400.1 ms 10 runs ``` God damn that is fast, now I know the program is tiny and simple but still that is impressive and makes me wonder how it would handle a larger codebase. Please if you have any feedback or insights on this let me know I am really curious. just for sanitysake I also ran `time odin build .` and it came out at you've guessed it `0.4` seconds. ### Right so it's fast, but how's the experience? Well I have to say it was pretty smooth overall. The compiler is fast and the error messages are generally clear and helpful if not perhaps a bit... verbose for my taste **For example** I've intentionally introduced a simple typo in the `map` keyword and named is `masp` to showcase what I mean: ```bash ❯ odin build . /home/dave/Workspace/TMP/odinest/main.odin(44:31) Error: Expected an operand, got ] subscribers: masp[string][]proc(msg: string), ^ /home/dave/Workspace/TMP/odinest/main.odin(44:32) Syntax Error: Expected '}', got 'proc' subscribers: masp[string][]proc(msg: string), ^ /home/dave/Workspace/TMP/odinest/main.odin(44:40) Syntax Error: Expected ')', got ':' subscribers: masp[string][]proc(msg: string), ^ /home/dave/Workspace/TMP/odinest/main.odin(44:41) Syntax Error: Expected ';', got identifier subscribers: masp[string][]proc(msg: string), ^ ``` I chose specifically this map because I wanted to showcase how Odin handles errors when you try to build, it could simply say `Error: Unknown type 'masp'` but instead it goes on to produce 4 separate errors that all stem from the same root cause. This is obviously because the parser gets confused and can't make sense of the code anymore. So essentially you get every single error that results from the initial mistake even if they are on the same line. Now would I love to see them condensed into a single error message? Because it stems from the same line and the same root cause? Yes I would. But that's just my personal preference. ## Where Odin Shines ### Simplicity and Readability Odin kinda feels like a modernized somehow even more boring C but in the best way possible. It's simple, straightforward and easy to read. It does not try to have some sort of clever syntax or fancy features, it really feels like a no-nonsense no frills language that wants you to start coding and being productive as quickly as possible. In fact this brings me to my next point. ### The Built in Libraries Galore I was frankly blown away with just how much is included in the standard and vendored(more on that later) libraries. I mean it has everything you'd expect from a modern systems programming language but it also comes with a ton of complete data structures, algorithms and utilities that you would usually have to rely on third-party libraries for in C or even Zig. For more info just look at [Odin's Core Library](https://pkg.odin-lang.org/core/) and I mean really look at it and read it do not just skim it. Here's an example [flags](https://pkg.odin-lang.org/core/flags/) which is a complete command line argument parser, or even [rbtree](https://pkg.odin-lang.org/core/container/rbtree/) which is a complete implementation of a red-black tree data structure that you can just import and use right away But what really blew me away was ### The Built in Vendor Libraries / Packages Odin comes with a set of vendor libraries that basically give you useful bindings to stuff like `SDL2/3`, `OpenGL`, `Vulkan`, `Raylib`, `DirectX` and more. This is really impressive because it means you can start building games or graphics applications right away without having to worry about setting up bindings or dealing with C interop. Now I'm not super sure if these vendor bindings are all maintained and created by the Odin team from what I could gather so far, it would certainly seem so but I could be wrong. If you know more about this please let me know. But all that aside these bindings are really well done and easy to use. For example here's how you can create a simple window with SDL2 in Odin: ```odin package main import sdl "vendor:sdl2" main :: proc() { sdl.Init(sdl.INIT_VIDEO) defer sdl.Quit() window := sdl.CreateWindow( "Odin SDL2 Black Window", sdl.WINDOWPOS_CENTERED, sdl.WINDOWPOS_CENTERED, 800, 600, sdl.WINDOW_SHOWN, ) defer sdl.DestroyWindow(window) renderer := sdl.CreateRenderer(window, -1, sdl.RENDERER_ACCELERATED) defer sdl.DestroyRenderer(renderer) event: sdl.Event running := true for running { for sdl.PollEvent(&event) { if event.type == sdl.EventType.QUIT { running = false } } sdl.SetRenderDrawColor(renderer, 0, 0, 0, 255) sdl.RenderClear(renderer) sdl.RenderPresent(renderer) } } ``` This code creates a simple window with a black background using SDL2. It's pretty straightforward and easy to understand, especially if you're already familiar with SDL2 or SDL3. ### C Interop Odin makes it trivially easy to interop with C libraries, as long as that. This is done via their `foreign import` where you'd create an import name and link to the library file and `foreign` blocks to link to declared individual function or types. I could explain it with examples here but Odin's own documentation does a way better job and will keep this post from getting even longer than it already is. So please check out [Odin's C interop](https://odin-lang.org/news/binding-to-c/) documentation for more info. ## Where Odin Feels Awkward ### Standard Library Gaps While Odin's standard library is quite comprehensive, there are still some gaps and missing features that can make certain tasks more cumbersome. For example, while it has basic file I/O capabilities, it lacks more advanced features like file watching or asynchronous I/O. Additionally, while it has a decent set of data structures, it lacks some more specialized ones like tries or bloom filters I'd also love to see a b+ tree implementation in the core library. But those are at most nitpicks and finding third-party libraries or writing your own implementations is usually straightforward. However... ### No Package Manager I really like languages that come with their own package manager, it makes it so much easier to discover, install and manage third-party libraries / dependencies. Odin currently lacks a built-in package manager, which means you have to manually download and include third-party libraries in your projects. This can be a bit of a hassle, especially I'd imagine for larger projects with multiple dependencies. ### Smaller Nitpicks - **dir inconsistencies**: I love how it auto named my binary after the folder I was in but I wish it did the same whenever I ran `odin run` and `odin build` I had to explicitly specify `odin run .` and `odin build .` that felt a bit inconsistent to me because if it knows the folder we are in why not just use that as the default value when we wanna tell it to run or build in the current directory? - **Error messages**: As mentioned earlier, while Odin's error messages are generally clear, they can sometimes be overly verbose, especially when multiple errors stem from a single root cause. It would be nice to see more concise error reporting in such cases. So to fix this I'd love to either see error messages collapsed into a single message with an array of messages from the same line, or somehow grouped together into blocks. ### Pointers are ^ and not * I'm on a German keyboard and the `^` character is a bit of a pain to type, especially when compared to the `*` character which is right next to the `Enter` key on my keyboard. I get that Odin wants to differentiate itself from C and C++ but this small change feels unnecessary and adds a bit of friction to the coding experience. These are as the title says just minor nitpicks and in no way detract from the overall experience of using Odin, just minor annoyances that I personally had while using the language your experience may differ vastly and none of these may even bother you. ## So is Odin just a More Boring C? In a way, yes kind of. I mean it's very similar in approach and philosophy but with more "guard rails" and helpful utilities to make the experience smoother and more enjoyable and the what I so far assume are first party bindings to popular libraries via the vendors package really makes it stand out in a great way, where you get a lot more consistency and predictability than you would if you were to use C with those same libraries. And I guess that's the strength of Odin, it's so boring that it just let's you be a productive programmer without getting in your way or trying to be too clever or fancy. I use boring here in an affectionate way, if you've ever read any of my other posts you'll know that I do not appreciate complexity and unnecessary cleverness in programming which is why I suppose I'm often quite critical of rust even though I do like it for certain use cases. In this case I'd say Odin is very similar to Go both are fantastic boring languages that let you get stuff done without much fuss or hassle. The only difference is that Go decided to ship with a garbage collector and Odin did not, which honestly for me personally makes Odin vastly more appealing. ### Syntax and Ergonomics Odin’s syntax is like C with a modern makeover clean, readable, and less prone to boilerplate. It did take me quite a while to get used to replacing my muscle memory for `*` with `^` for pointers and `func`, `fn`, `fun`, `function` with `proc` for functions. But once I got over that initial hump, it felt pretty natural. Also `::` for type declarations is a bit unusual and took me longer than I care to admit, as I'm fairly used to `::` being used for scope resolution in languages like C++ and Rust. But again, once I got used to it, it felt fine. Everything else about the syntax felt pretty intuitive and straightforward. ## Who Odin Might Be Right For ### Ideal Use Cases - **Game Development**: Honestly I totally see where people are coming from when they say Odin is great for game development. The built-in vendor libraries for SDL2/3, OpenGL, Vulkan, Raylib and more make it super easy to get started with game development. Plus the language's performance and low-level capabilities are a great fit for the demands of game programming. - **Systems Programming**: Odin's manual memory management, low-level access, and performance make it a solid choice for systems programming tasks like writing operating systems, device drivers, or embedded systems. I will absolutely be writing some utilities for my Linux setup in Odin in the near future. - **Desktop Applications**: Again this is where those vendor libraries shine, making it easy to build cross-platform desktop applications with graphical interfaces as long as you're fine with doing some manual drawing of components, I'd love to see a binding for something like `GTK` or `Qt` in the vendor packages in the future. - **General Purpose Programming**: This brings me back to my intro where I said that it took me a while to realize that if Odin is good for game development then realistically by all means it should basically be good for anything and everything you wish to create with it. So yea give it a shot make something cool with it. ### Where It’s Not a Good Fit Yet - **Web Development**: The Net library is pretty darn nice and very extensive, however it does seem like it's maybe a bit more fit for general networking tasks rather than simplifying your life as a web backend developer. I'm sure there's already a bunch of third party libraries for this, but if you're a web dev you are almost spoiled for choice at the moment by languages that support web development out of the box with all the fixings and doodads. ## Final Thoughts ### Would I Use It Again? Absolutely in fact I will, I've already started planning some small utilities for my Linux setup in Odin. I really like the simplicity and readability of the language, as well as the comprehensive standard and vendor libraries. The performance is also impressive, especially the fast compile times. ### Source Code and Feedback You can find the complete source code for the tiny key-value store with pub/sub capabilities on my GitHub: [dayvster/odin-kvpub](https://github.com/dayvster/odin-kvpubsub) If you create anything cool with it I'd love to see it so do hit me up on any of my socials. I'd love to hear your thoughts and experiences with Odin, whether you've used it before or are considering giving it a try. Feel free to leave a comment or reach out to me on Twitter [@dayvster](https://twitter.com/dayvsterdev). Appreciate the time you took to read this post, and happy coding!

1 views
sunshowers 1 weeks ago

Cancelling async Rust

This is an edited, written version of my RustConf 2025 talk about cancellations in async Rust. Like the written version of my RustConf 2023 talk , I’ve tried to retain the feel of a talk while making it readable as a standalone blog entry. Some links: Let’s start with a simple example – you decide to read from a channel in a loop and gather a bunch of messages: All good, nothing wrong with this, but you realize sometimes the channel is empty for long periods of time, so you add a timeout and print a message: There’s nothing wrong with this code—it behaves as expected. Now you realize you need to write a bunch of messages out to a channel in a loop: But sometimes the channel gets too full and blocks, so you add a timeout and print a message: It turns out that this code is often incorrect, because not all messages make their way to the channel. Hi, I’m Rain, and this post is about cancelling async Rust. This post is split into three parts: Before we begin, I want to lay my cards on the table – I really love async Rust! I gave a talk at RustConf a couple years ago talking about how async Rust is a great fit for signal handling in complex applications. I’m also the author of cargo-nextest , a next-generation test runner for Rust, where async Rust is the best way I know of to express some really complex algorithms that I wouldn’t know how to express otherwise. I wrote a blog post about this a few years ago. Now, I work at Oxide Computer Company , where we make cloud-in-a-box computers . We make vertically integrated systems where you provide power and networking on one end, and the software you want to run on the other end, and we take care of everything in between. Of course, we use Rust everywhere, and in particular we use async Rust extensively for our higher-level software, such as storage , networking and the customer-facing management API . But along the way we’ve encountered a number of issues around async cancellation, and a lot of this post is about what we learned along the way. What does cancellation mean? Logically, a cancellation is exactly what it sounds like: you start some work, and then change your mind and decide to stop doing that work. As you might imagine this is a useful thing to do: But then you change your mind: you want to cancel it rather than continue it to completion. Before we talk about async Rust, it’s worth thinking about how you’d do cancellations in synchronous Rust. One option is to have some kind of flag you periodically check, maybe stored in an atomic: This approach is fine for smaller bits of code but doesn’t really scale well to large chunks of code since you’d have to sprinkle these checks everywhere. A related option, if you’re working with a framework as part of your work, is to panic with a special payload of some kind. A third option is to kill the whole process . This is a very heavyweight approach, but an effective one in case you spawn processes to do your work. Rather than kill the whole process, can you kill a single thread? All of these options are suboptimal or of limited use in some way. In general, the way I think about it is that there isn’t a universal protocol for cancellation in synchronous Rust. In contrast, there is such a protocol in async Rust, and in fact cancellations are extraordinarily easy to perform in async Rust. Why is that so? To understand that, let’s look at what a future is. Here’s a simple example of a future: In this future, you first perform a network request which returns some data, and then you process it. The Rust compiler looks at this future and generates a state machine, which is just a struct or enum in memory: If you’ve written async Rust before the and keywords, you’ve probably written code like it by hand. It’s basically just an enum describing all the possible states the future can be in. The compiler also generates an implementation of the trait for this future: and when you call on the future, it gets translated down to this underlying function. It is only when or this function is called that something actually happens. Note that this is diametrically opposed to how async works in other languages like Go, JavaScript, or C#. In those languages, when you create a future to await on, it starts doing its thing, immediately, in the background: That’s regardless of whether you await it or not. In Rust, this call does nothing until you actually call on it: I know I sound a bit like a broken record here, but if you can take away one thing from this post, it would be that futures are passive, and completely inert until awaited or polled. So what does the universal protocol to cancel futures look like? It is simply to drop the future, or to not await it, or poll it any more. Since a future is just a state machine, you can throw it away at any time the poll function isn’t actively being called. The upshot of all this is that any Rust future can be cancelled at any await point. Given how hard cancellation tends to be in synchronous environments, the ability to easily cancel futures in async Rust is extraordinarily powerful—in many ways its greatest strength! But there is a flip side, which is that cancelling futures is far, far too easy. This is for two reasons. First, it’s just way too easy to quietly drop a future. As we’re going to see, there are all kinds of code patterns that lead to silently dropping futures. Now this wouldn’t be so bad, if not for the second reason: that cancellation of parent futures propagates down to child futures. Because of Rust’s single ownership model , child futures are owned by parent ones. If a parent future is dropped or cancelled, the same happens to the child. To figure out whether a child future’s cancellation can cause issues, you have to look at its parent, and grandparent, and so on. Reasoning about cancellation becomes a very complicated non-local operation . I’m going to cover some examples in a bit, but before we do that I want to talk about a couple terms, some of which you might have seen references to already. The first term is cancel safety . You might have seen mentions of this in the Tokio documentation. Cancel safety, as generally defined, means the property of a future that can be cancelled (i.e. dropped) without any side effects. For example, a Tokio sleep future is cancel safe: you can just stop waiting on the sleep and it’s completely fine. An example of a future that is not cancel safe is Tokio’s MPSC send , which sends a message over a channel: If this future is dropped, the message is lost forever. The important thing is that cancel safety is a local property of an individual future . But cancel safety is not all that one needs to care about. What actually matters is the context the cancellation happens in, or in other words whether the cancellation actually causes some kind of larger property in the system to be violated. To capture this I tend to use a different term called cancel correctness , which I define as a global property of system correctness in the face of cancellations. (This isn’t a standard term, but it’s a framing I’ve found really helpful in understanding cancellations.) When is cancel correctness violated? It requires three things: The system has a cancel-unsafe future somewhere within it. As we’ll see, many APIs that are cancel-unsafe can be reworked to be cancel-safe. If there aren’t any cancel-unsafe futures in the system, then the system is cancel correct. A cancel-unsafe future is actually cancelled. This may sound a bit trivial, but if cancel-unsafe futures are always run to completion, then the system can’t have cancel correctness bugs. Cancelling the future violates some property of a system. This could be data loss as with , some kind of invariant violation, or some kind of cleanup that must be performed but isn’t. So a lot of making Rust async robust is about trying to tackle one of these three things. I want to zoom in for a second on invariant violations and talk about an example of a Tokio API that is very prone to cancel correctness issues: Tokio mutexes . The way Tokio mutexes work is: you create a mutex, you lock it which gives you mutable access to the data underneath, and then you unlock it by releasing the mutex. If you look at the function’s documentation , in the “cancel safety” section it says: This method uses a queue to fairly distribute locks in the order they were requested. Cancelling a call to lock makes you lose your place in the queue. Okay, so not totally cancel safe, but the only kind of unsafety is fairness, which doesn’t sound too bad. But the problems lie in what you actually do with the mutex. In practice, most uses of mutexes are in order to temporarily violate invariants that are otherwise upheld when a lock isn’t held. I’ll use a real world example of a cancel correctness bug that we found at my job at Oxide: we had code to manage a bunch of data sent over by our computers, which we call sleds. The shared state was guarded by a mutex, and a typical operation was: Here’s a rough sketch of what that looks like: This is all well and good, but the problem is that the action being performed actually had an await point within it: If the code that operated on the mutex got cancelled at that await point, then the data would be stuck in the invalid state. Not great! And keep in mind the non-local reasoning aspect: when doing this analysis, you need to look at the whole chain of callers. Now that we’ve talked about some of the bad things that can happen during cancellations, it’s worth asking what kinds of code patterns lead to futures being cancelled. The most straightforward example, and maybe a bit of a silly one, is that you create a future but simply forget to call on it. Now Rust actually warns you if you don’t call on the future: But a code pattern I’ve sometimes made mistakes with is that the future returns a , and you want to ignore the result so you assign it to an underscore like so: If I forget to call on the future, Rust doesn’t warn me about it at all, and then I’m left scratching my head about why this code didn’t run. I know this sounds really silly and basic, but I’ve made this mistake a bunch of times. (After my talk, it was pointed out to me that Clippy 1.67 and above have a warn-by-default lint for this. Hooray!) Another example of futures being cancelled is operations, such as Tokio’s macro . For example: If you call with a bunch of futures, and all of them succeed, it’s all good. But if one of them fails, the rest simply get cancelled. In fact, at Oxide we had a pretty bad bug around this: we had code to stop a bunch of services, all expressed as futures. We used : If one of these operations failed for whatever reason, we would stop running the code to wait for the other services to exit. Oops! But perhaps the most well-known source of cancellations is Tokio’s macro . Select is this incredibly beautiful operation. It is called with a set of futures, and it drives all of them forward concurrently: Each future has a code block associated with it (above, and ). If one of the futures completes, the corresponding code block is called. But also, all of the other futures are always cancelled! For a variety of reasons, select statements in general, and select loops in particular, are particularly prone to cancel correctness issues. So a lot of the documentation about cancel safety talks about select loops. But I want to emphasize here that select is not the only source of cancellations, just a particularly notable one. So, now that we’ve looked at all of these issues with cancellations, what can be done about it? First, I want to break the bad news to you – there is no general, fully reliable solution for this in Rust today. But in our experience there are a few patterns that have been successful at reducing the likelihood of cancellation bugs. Going back to our definition of cancel correctness, there are three prongs all of which come together to produce a bug: Most solutions we’ve come up with try and tackle one of these prongs. Let’s look at the first prong: the system has a cancel-unsafe future somewhere in it. Can we use code patterns to make futures be cancel-safe? It turns out we can! I’ll give you two examples here. The first is MPSC sends. Let’s come back to the example from earlier where we would lose messages entirely: Can we find a way to make this cancel safe? In this case, yes, and we do so by breaking up the operation into two parts: (I want to put an asterisk here that reserve is not entirely cancel-safe, since Tokio’s MPSC follows a first-in-first-out pattern and dropping the future means losing your place in line. Keep this in mind for now.) The second is with Tokio’s . If you’ve written synchronous Rust you’re probably familiar with the method , which writes an entire buffer out: In synchronous Rust, this is a great API. But within async Rust, the pattern is absolutely not cancel safe! If the future is dropped before completion, you have no idea how much of this buffer was written out. But there’s an alternative API that is cancel-safe, called . This API is carefully designed to enable the reporting of partial progress, and it doesn’t just accept a buffer , but rather something that looks like a cursor on top of it: When part of the buffer is written out, the cursor is advanced by that number of bytes. So if you call in a loop, you’ll be resuming from this partial progress, which works great. Going back to the three prongs: the second prong is about actually cancelling futures. What code patterns can be used to not cancel futures? Here are a couple of examples. The first one is, in a place like a select loop, resume futures rather than cancelling them each time. You’d typically achieve this by pinning a future , and then polling a mutable reference to that future. For example: Coming back to our example of MPSC sends, the one asterisk with is that cancelling it makes you lose your place in line. Instead, if you pin the future and poll a mutable reference to it, you don’t lose your place in line. (Does the difference here matter? It depends, but you can now have this strategy available to you.) The second example is to use tasks. I mentioned earlier that futures are Rust are diametrically opposed to similar notions in languages like JavaScript. Well, there’s an alternative in async Rust that’s much closer to the JavaScript idea, and that’s tasks . A fun example is that at Oxide, we have an HTTP server called Dropshot . Previously, whenever an HTTP request came in, we’d use a future for it, and drop the future if the TCP connection was closed. This was really bad because future cancellations could happen due to the behavior of not just the parent future, but of a process that was running across a network! This is a rather extreme form of non-local reasoning. We addressed this by spinning up a task for each HTTP request, and by running the code to completion even if the connection is closed: The last thing I want to say is that this sucks ! The promise of Rust is that you don’t need to do this kind of non-local reasoning—that you can analyze small bits of code for local correctness, and scale that up to global correctness. Almost everything in Rust, from and to , is geared towards making that possible. Future cancellations fly directly in the face of that, and I think they’re probably the least Rusty part of Rust . This is all really unfortunate. Can we come up with something more systematic than this kind of ad-hoc reasoning? There doesn’t exist anything in safe Rust today, but there are a few different ideas people have come up with. I wanted to give a nod to those ideas: All of these options have really significant implementation challenges, though. This blog post from boats covers some of these solutions, and the implementation challenges with them. In this post, we: Some of the recommendations are: There’s a very deep well of complexity here, a lot more than I can cover in one blog post: If you’re curious about any of these, check out this link where I’ve put together a collection of documents and blog posts about these concepts. In particular, I’d recommend reading these two Oxide RFDs: Thank you for reading this post to the end! And thanks to many of my coworkers at Oxide for reviewing the talk and the RFDs linked above, and for suggestions and constructive feedback. Video of the talk on YouTube. Slides on Google Slides. Repository with links and notes on GitHub. Coverage on Linux Weekly News . What is cancellation? It’s an extremely powerful part of async Rust but also one that is very hard to reason thoroughly about. Analyzing cancellations: Going deep into their mechanics and providing some helpful ways to think about them. What can be done? Solutions, including practical guidance, and real bugs we’ve found and fixed in production codebases. I gave a talk at RustConf a couple years ago talking about how async Rust is a great fit for signal handling in complex applications. I’m also the author of cargo-nextest , a next-generation test runner for Rust, where async Rust is the best way I know of to express some really complex algorithms that I wouldn’t know how to express otherwise. I wrote a blog post about this a few years ago. You may have started a large download or a long network request Maybe you’ve started reading a file, similar to the command. The code that wishes to perform the cancellation can set that flag. Then, the code which checks that flag can exit early. If that feels strange to you, you’re not alone! But the Salsa framework for incremental computation, used by—among other things— rust-analyzer , uses this approach. Something I learned recently was that this only works on build targets which have a notion of panic unwinding , or being able to bubble up the panic. Not all platforms support this, and in particular, Wasm doesn’t. This means that Salsa cancellations don’t work if you build rust-analyzer for Wasm. While some OSes have APIs to perform this action, they tend to warn very strongly against it. That’s because in general, most code is just not ready for a thread disappearing from underneath. In particular, thread killing is not permitted by safe Rust, since it can cause serious corruption. For example, Rust mutexes would likely stay locked forever. First, it’s just way too easy to quietly drop a future. As we’re going to see, there are all kinds of code patterns that lead to silently dropping futures. Now this wouldn’t be so bad, if not for the second reason: that cancellation of parent futures propagates down to child futures. Because of Rust’s single ownership model , child futures are owned by parent ones. If a parent future is dropped or cancelled, the same happens to the child. To figure out whether a child future’s cancellation can cause issues, you have to look at its parent, and grandparent, and so on. Reasoning about cancellation becomes a very complicated non-local operation . For example, if you drop a future which sends a message, but for whatever reason you don’t care about the message any more, it’s not really a bug! The system has a cancel-unsafe future somewhere within it. As we’ll see, many APIs that are cancel-unsafe can be reworked to be cancel-safe. If there aren’t any cancel-unsafe futures in the system, then the system is cancel correct. A cancel-unsafe future is actually cancelled. This may sound a bit trivial, but if cancel-unsafe futures are always run to completion, then the system can’t have cancel correctness bugs. Cancelling the future violates some property of a system. This could be data loss as with , some kind of invariant violation, or some kind of cleanup that must be performed but isn’t. Obtain a lock on the mutex. Obtain the sled-specific data by value, moving it to an invalid state. Perform an action. Set the sled-specific data back to the next valid state. A cancel-unsafe future exists This cancel-unsafe future is cancelled The cancellation violates a system property The first component is the operation to reserve a permit or slot in the channel. This is an initial async operation that’s cancel-safe. The second is to actually send the message , which is an operation that becomes infallible. Unlike futures which are driven by the caller, tasks are driven by the runtime (such as Tokio). With Tokio, dropping a handle to a task does not cause it to be cancelled, which means they’re a good place to run cancel-unsafe code. Async drop would let you run async code when a future is cancelled. This would handle some, though not all, of the cases we discussed today. There’s also a couple different proposals for what are called linear types , where you could force some code to be run on drop, or mark a particular future as non-cancellable (once it’s been created it must be driven to completion). Saw that futures are passive Introduced cancel safety and cancel correctness as concepts Examined some bugs that can occur with cancellation Looked at some recommendations you can use to mitigate the downsides of cancellation Avoid Tokio mutexes Rewrite APIs to make futures cancel-safe Find ways to ensure that cancel-unsafe futures are driven to completion Why are futures passive, anyway? Cooperative cancellation: cancellation tokens Actor model as an alternative to Tokio mutexes Task aborts Structured concurrency Relationship to panic safety and mutex poisoning RFD 397 Challenges with async/await in the control plane by David Pacheco RFD 400 Dealing with cancel safety in async Rust by myself

0 views
annie's blog 2 weeks ago

Shelter or prison

A mental model or set of values starts as a shelter from the unrelenting chaos of reality. We need these shelters. Living without them isn’t really possible. We can’t take in and process adequate information fast enough to make truly new decisions. We need to categorize things and go with default reactions, otherwise we’ll get stuck, overwhelmed, never able to move from processing and analysis to action. Beliefs, mental models, values: These are shortcuts to decision-making. We adopt the ones we are given, adapt them according to our experiences, and use them as a way to understand the world (at least in some fashion). They tell us what the best thing is when we face a choice. They tell us how to react to other people’s choices. These structures give us shelter from chaos. They give us shortcuts so we can live. We stack a bunch of these structures together and call it something bigger: a religion, a culture, civilization. The interactions between the structures form the system we understand as reality. The problem with every system is how it evolves. It begins as a means of supporting the structures, keeping everything working; it ends up as a self-referential entity with the core goal of sustaining itself. The individuals within a system may change and grow and need the system to change and grow with them. But systems resist change. The individuals in a system are often not served by the system, but they’re serving it. They’re trapped within it. Does it shelter them? Does it provide some resources? Does it, perhaps, even keep them alive? Sure. So does a prison. Scifi tell us to fear AI; at some point, the artificial intelligence will become real , exert will, take over. But we should, instead, look at what we’ve already created that has taken over: our structures, our systems, our organizations, our civilizations. Gaining sentience was not even necessary. We, the inhabitants of the system, provide the necessary sentience to grease the wheels, crank the gears, repair the breaks, patch the holes. How could we refuse? After all, it keeps us alive. This shelter, this system, this prison.

1 views
Den Odell 2 weeks ago

HTML’s Best Kept Secret: The &lt;output&gt; Tag

Every developer knows . It’s the workhorse of the web. But ? Most have never touched it. Some don’t even know it exists. That’s a shame, because it solves something we’ve been cobbling together with s and ARIA for years: dynamic results that are announced to screen readers by default. It’s been in the spec for years. Yet it’s hiding in plain sight . Here’s what the HTML5 spec says: The element represents the result of a calculation performed by the application, or the result of a user action. It’s mapped to in the accessibility tree . In plain terms, it announces its value when it changes, as if it already had . In practice, that means updates do not interrupt the user. They are read shortly after, and the entire content is spoken rather than just the part that changed. You can override this behavior by setting your own ARIA properties if needed. Usage is straightforward: That’s it. Built-in assistive technology support. No attributes to memorize. Just HTML doing what it was always meant to do. I discovered on an accessibility project with a multi-step form. The form updated a risk score as fields changed. It looked perfect in the browser, but screen reader users had no idea the score was updating. Adding an ARIA live region fixed it. But I’ve always believed in reaching for semantic HTML first, and live regions often feel like a patch. That’s when I scoured the spec and jumped out. It understands forms without requiring one, and it announces its changes natively. Turns out the simplest fix had been in the spec all along. Because we forgot. It’s not covered in most tutorials. It doesn’t look flashy. When I searched GitHub public repos, it barely showed up at all. It gets overlooked in patterns and component libraries too. That absence creates a feedback loop: if no one teaches it, no one uses it. Like , has a attribute. Here you list the s of any elements the result depends on, separated by spaces: For most users, nothing changes visually. But in the accessibility tree it creates a semantic link, letting assistive technology users connect the inputs with their calculated result. It doesn’t require a either. You can use it anywhere you are updating dynamic text on the page based on the user’s input. By default is inline, so you’ll usually want to style it for your layout, just as you would a or . And because it has been part of the spec since 2008, support is excellent across browsers and screen readers. It also plays nicely with any JavaScript framework you might be using, like React or Vue. Update 7 Oct 2025 : Some screen readers have been found not to announce updates to the tag, so explicitly emphasising the attribute might be worthwhile for now until support improves: . One thing to note: is for results tied to user inputs and actions, not global notifications like toast messages. Those are better handled with or on a generic element, since they represent system feedback rather than calculated output. So what does this look like in practice? I’ve personally reached for in multiple real-world projects since discovering it: During a recent 20-minute coding challenge, I used to display calculation results. Without adding a single ARIA role, the screen reader announced each result as it updated. No hacks required. At Volvo Cars, we displayed user-friendly versions of slider values. Internally the slider might hold , but the output showed . We wrapped the slider and in a container with and a shared label, creating a cohesive React component: I found that password strength indicators and real-time validation messages work beautifully with . The tag even fits modern patterns where you might fetch prices from APIs, show tax calculations, or display server-generated recommendations. Here, a shipping cost calculator updates an tag, informing users once the cost has been calculated: There’s something satisfying about using a native HTML element for what it was designed for, especially when it makes your UI more accessible with less code. might be HTML’s best kept secret, and discovering gems like this shows how much value is still hiding in the spec. Sometimes the best tool for the job is the one you didn’t even know you had. Update 11 Oct 2025 : The ever-excellent Bob Rudis has produced a working example page to support this post. Find it here: https://rud.is/drop/output.html

0 views
マリウス 2 weeks ago

Updates 2025/Q3

This post includes personal updates and some open source project updates. Q3 has been somewhat turbulent, marked by a few unexpected turns. Chief among them, changes to my home base . In mid-Q3, the owner of the apartment I was renting abruptly decided to sell the property, effectively giving me two weeks to vacate. Thanks to my lightweight lifestyle , moving out wasn’t a major ordeal. However, in a global housing landscape distorted by corporations, wealthy boomers, and trust-fund heirs, securing a new place on such short notice proved nearly impossible. Embracing the fact that I am the master of my own time and destiny, and guided by the Taoist principle of Wu Wei , I chose not to force a solution. Instead, I placed all my belongings (including my beloved desk ) into storage and set off for a well-earned break from both the chaos and the gloom of the wet season. Note: If you ever feel you’re not being treated with the respect you deserve, the wisest response is often to simply walk away. Go where you’re treated best. Give yourself the space to reflect, to regain clarity, and most importantly, to reconnect with your sense of self. Resist the urge to look back or second-guess your instincts. Never make life-altering decisions in haste; Make them on your terms, not someone else’s. And remember, when life gives you onions, make onionade . On a different note, my coffee equipment has been extended by a new addition that is the Bookoo Themis Mini Coffee Scale , a super lightweight (~140g) and portable (8cm x 8cm x 1.5cm) coffee scale that allows me to precisely measure the amount of coffee that I’m grinding and brewing up . So far I’m very happy with the device. I don’t use its Bluetooth features at all, but when I initially tried, out of curiosity, their Android app didn’t really work. Speaking of brewing: Unfortunately at the end of Q3 my 9barista espresso maker seemingly broke down . While there are no electronics or mechanics that can actually break, I suspect that during my last descaling procedure enough limestone was removed for the boiler O-ring to not properly seal the water chamber any longer. I took the 9barista apart and couldn’t visually see anything else that could make it misbehave. I have hence ordered a repair kit from the manufacturer’s online store and am waiting for it to be delivered before I can continue enjoying self-made, awesome cups of coffee. Europe is continuing to build its surveillance machinery under claims of online safety , with the UK enforcing online age verification for major platforms, followed by the EU piloting similar acts in several member states . Even though the changes don’t affect me, I find this trend frightening, especially considering the looming threat to online privacy that is Chat Control . Even presumed safe-havens for censorship and surveillance like Matrix have rolled over and implemented age verification on the Matrix.org homeserver. The CEO of Element ( New Vector Limited ) gave the following explanation for it : Now, the Online Safety Act is a very sore point. The fact is that Matrix.org or Element is based in the UK, but even if we weren’t we would still be required to comply to the Online Safety Act for users who are in the UK, because it is British law. That statement is not quite accurate, however. If the Matrix.org homeserver was run by an entity in a non-cooperative jurisdiction they wouldn’t need to implement any of this. This is important, because people need to understand that despite the all the globalism that is being talked about, not every place on earth part-takes in implementing these mindless laws, even if your government would like you to think that it’s the norm. Obviously it’s not exactly easy to run a platform from within an otherwise (at least partially) sanctioned country, especially when user data is at stake. However with regulations like these becoming more and more widespread my pirate mind imagines a future where such setups are becoming viable options, given that the countries in question are scrambling for income streams and would be more than happy to gain leverage over other countries. We’ve already seen this in several instances (e.g. Yandex, Alibaba, ByteDance ( TikTok ), Telegram, DiDi, Tencent ( WeChat ), …) and given the global political climate I can imagine more services heading towards jurisdictions that allow them to avoid requesting IDs from their users or breaking security measures only so government agencies can siphon out data at will. However, a different future outcome might be an increased focus on decentralization (or at least federation ), which would as well be a welcome change. As Matrix correctly pointed out, individually run homeservers are not affected by any of this. Similarly, I haven’t heard of any instances of XMPP service operators being approached by UK officials. Unlike on centralized platforms like Discord, and wannabe-decentralized platforms like Bluesky, enforcing something like age verification on an actual federated/decentralized network is near impossible, especially with services that are being hosted outside of the jurisdiction’s reach. In the future, federated protocols, as well as peer-to-peer projects are going to become more important than ever to counter the mindless policies enacted by the people in power. Looking at this mess from the bright side, with major big tech platforms requiring users to present IDs we can hope for a growing number of people to cut ties with those platforms, driving them, and their perpetrators , into the ground in the long run. If you are looking for decentralized alternatives to centralized services, here is a non-exhaustive list: Since I began publishing code online, I’ve typically used the GPL or MIT license for my projects. However, given the current global climate and the direction the world seems to be heading, I’ve grown increasingly skeptical of these traditional licenses. While I still want to offer free and open source software to people , I find myself more and more reluctant to grant unrestricted use, particularly to organizations whose values or missions I fundamentally disagree with. Unfortunately, exclusions or prohibitions were never part of the vision behind the GNU or OSI frameworks, making most conventional open source licenses unsuitable for this kind of selective restriction. Recently, however, I came across the Hippocratic License , which is designed to address exactly these concerns. In fact, the HL3 already includes three of the four exclusions I would like to enforce: Mass surveillance, military activities, and law enforcement. The fourth, government revenue services, could likely be added in a similar manner. That said, HL3 does overreach in some areas, extending into domains where I don’t believe a software license should have jurisdiction, such as: 3.2. The Licensee SHALL: 3.2.1. Provide equal pay for equal work where the performance of such work requires equal skill, effort, and responsibility, and which are performed under similar working conditions, except where such payment is made pursuant to: 3.2.1.1. A seniority system; 3.2.1.2. A merit system; 3.2.1.3. A system which measures earnings by quantity or quality of production; or 3.2.1.4. A differential based on any other factor other than sex, gender, sexual orientation, race, ethnicity, nationality, religion, caste, age, medical disability or impairment, and/or any other like circumstances (See 29 U.S.C.A. § 206(d)(1); Article 23, United Nations Universal Declaration of Human Rights; Article 7, International Covenant on Economic, Social and Cultural Rights; Article 26, International Covenant on Civil and Political Rights); and 3.2.2. Allow for reasonable limitation of working hours and periodic holidays with pay (See Article 24, United Nations Universal Declaration of Human Rights; Article 7, International Covenant on Economic, Social and Cultural Rights). These aspects of the Hippocratic License have already drawn significant criticism, and I would personally remove them in any variation I choose to adopt. However, a far greater concern lies with the license’s stewardship, the Organization for Ethical Source ( OES ). While supporting a good cause is typically straightforward, the organization’s founder and current president has unfortunately earned a reputation for unprofessional conduct , particularly in addressing the very issues the organization was created to confront. I’m reluctant to have my projects associated with the kind of “drama” that seems to follow the organization’s leadership. For this reason, I would likely need to distance any variation of the license as far as possible from its heritage, to avoid direct association with the OES and the leadership’s behavior. Hence, I’m still on the lookout for alternative licenses, specifically ones that maintain the permissiveness of something like the GPL, but allow for clearly defined, legally enforceable exceptions. If you have experience in working with such licenses, I would very much appreciate your input. PS: I’m fully aware that adopting such a license would render my software non-free in the eyes of organizations like GNU or the OSI. However, those organizations were founded in a different era and have, in my view, failed to adapt to the realities of today’s world. It’s curious how many advocates of GNU/OSI philosophies call for limitations on freedom of speech, yet insist on software being usable without restriction in order to qualify as free and open source . This site has received what some might consider a useless or even comical update, which, however, is meant to further the goal of raising awareness about the role JavaScript plays in browsers. I got the inspiration for this from this post by sizeof.cat , a site I discovered thanks to the friendly folks in the VT100 community room . While sizeof.cat uses this feature purely for the lulz , I believe it can serve as an effective way to encourage people to disable JavaScript in their browsers by default, and to be very selective about which websites they enable it for. As a result, this website now features a similar (but edgier ) option, which you can test by enabling JavaScript for this domain and then sending this tab to the background. Go ahead, I’ll wait. :-) Like sizeof.cat ’s original implementation, this site will randomly alternate between different services . However, unlike the original, you’ll see an overlay when you return to the site, explicitly encouraging you to disable JavaScript in your browser. After having used neomutt for several years, I grew tired of the many cogs ( notmuch , mbsync , w3m , reader , etc.) I had to maintain for the setup to function the way I expected it to do, especially when my primary requirement is to not leave e-mails on the server for more time than really needed. Eventually I got fed up with my e-mail client breaking whenever I needed it most, and with having to deal with HTML e-mail on the command line, thinking that if I’d use an actual GUI things would be much simpler. Little did I know. I moved to Mozilla Thunderbird as my primary e-mail client a while ago. I set up all my IMAP accounts, and I created a “Local Folder” that Mozilla sold me as maildir : Fast-forward to today and I’m stuck with a setup where I cannot access my “Local Folder” maildir by any other maildir -compliant software besides Thunderbird, because even though Mozilla called it maildir , it is not an actual maildir format : Note this is NOT full maildir in the sense that most people, particularly linux users or mail administrators, know as maildir. On top of that, my OpenSnitch database is overflowing with deny rules for Mozilla’s supposed “privacy respecting” software. At this point I’m not even wondering what the heck is wrong with this company anymore. Mozilla has lost it, with Firefox , and seemingly also with other software they maintain. With my e-mails now locked-in into something that Mozilla titles maildir even though it is not, I am looking forward to go back to where I came from. I might however replace the overly complex neomutt setup with a more modern and hopefully lightweight aerc configuration. Unfortunately, I have used Thunderbird ’s “Local Folder” account for too long and I’ll have to figure out a way to get those e-mails into an actual maildir format before I can leave Mozilla’s ecosystem once and for all. Note on Firefox: I don’t care what your benchmark says, in everyday use Firefox is annoyingly slow despite all its wRiTtEn In RuSt components. For reasons that I didn’t care to investigate, it also seemingly hangs and waits for connections made by its extensions (e.g. password managers) and meanwhile prevents websites from loading. The amount of obscure problems that I’ve encountered with Firefox over the past years is unmatched by any other browser. Not to mention the effort that goes into checking the configuration editor with every new release and disabling all the privacy invasive bs that Mozilla keeps adding. At this point I’m not supporting Firefox any longer, despite the world’s need for a Chromium alternative. Firefox is sucking out the air in the room and with it dead hopefully more effort will be put into alternatives. I had to bring my Anker A1257 power bank to a “recycling” facility, due to it being recalled by Anker : There’s an interesting post by lumafield if you want to know the details. However, what Anker calls a recall is effectively a throw it away and we give you a voucher , because apparently we’re too stupid as societies to demand for manufacturers to take back their broken junk and recycle it properly . I tried to be better by not tossing the device into the trash but bring it to a dedicated “recycling” facility, even when I know for sure that they won’t actually recycle it or even dispose of it in a proper way. But that’s pretty much all I, as a consumer, can do in this case. While I, too, got a lousy voucher from Anker, none of their current options fit the specifications of the A1257. I therefor decided to look for alternatives and found the Sharge Pouch Mini P2. I needed something that is lightweight, has a relatively small form factor and doesn’t rely on a single integrated charging cable which would render the device useless the moment it would break. Given how bad Anker USB-C cables usually are in terms of longevity, I would never buy into a power bank from Anker that comes with an integrated USB cable, especially when it’s the only option to charge the power bank. While the Sharge also has a fixed USB cable, it is nevertheless possible to use the USB-C port for re-charging the device. If the integrated red cable ever breaks, I can still continue using the device. As I have zero experience with this manufacturer it remains to be seen how this 213g- heavy power bank will perform long-term. So far the power bank appears sufficient. While charging it barely gets warm, and even though the device lacks a display for indicating the charge level, the LED ring around the power button is sliced into four segments that make it easy to guesstimate the remaining charge. Charging it full takes around an hour. One thing that is slightly annoying is the USB-C port, which won’t fit significantly thicker cable-heads. The maximum that I could fit were my Cable Matters USB4 cables. The situation with GrapheneOS devices (and Android in general) mentioned in my previous updates has prompted me to revive my dormant Pinephone Pro . Trying to do so, however, I found that the original Pinephone battery was pretty much dead. Hence, I ordered a new battery that is compatible with the Samsung Galaxy J7 (models / ) – primarily because Pine64 doesn’t appear to be selling batteries for the Pinephone Pro anymore; Update: Pine64 officially discontinued the Pinephone Pro – and gave the latest version of postmarketOS (with KDE Plasma Mobile) a go. While Pinephone Pro support has become better over the years, with at least the selfie-camera finally “working” , the Pinephone hardware unfortunately remains a dead-end. Even with a new battery the phone discharges within a few hours (with sleep enabled). In fact, it even discharges over night when turned off completely. I don’t know whether newer versions of the Pine64 hardware have fixed the hardware bugs, but judging by the search results that I’m getting I doubt so. The UI has certainly become more usable with hardware acceleration seemingly working fine now, however the Pinephone is still too weak for most use cases. Retrospectively, the Pinephone Pro was a bad investment, as it’s effectively a wire-bound device with an integrated UPS at most, that I would argue isn’t even suitable as a development platform with all its hardware glitches ( Hello %0 battery boot loop! , just to name one). It is in fact so bad that you cannot even boot the device when there is no battery in it, to use it as a regular SBC with integrated display. This is sad because the Pinephone hardware tarnishes the reputation of Linux on mobile, given that it is one of the most prominent options. If you’re considering to give Linux on mobile a try, I do not recommend the Pinephone, and I am somewhat happy that Pine64 decided to discontinue it. They did not discontinue the original Pinephone, yet, however. Having that said, I have been following the progress that Luca Weiss ( Z3ntu ) made with running pmOS on the Fairphone 6 and I have to admit that I’m intrigued. While it’s still a long way to go , it is nice to see a Fairphone engineer that is actively working on bringing mobile Linux to the device. I don’t know whether his efforts are partially funded by his employer, or whether it’s his personal interest, but I truly hope for the former. The Fairphone is an odd value proposition for the average Android user. The native Fairphone Android experience seems average , and their Murena /e/OS partnership is highly questionable at best and might tarnish their reputation in the long run. However, I feel like they could gain a pretty large nerd-following by officially supporting mobile Linux, and actively pushing for it. At least in my books, having full-fledged postmarketOS support on their phones would be an instant money-magnet from the tech sphere, especially with the current bar being as low as the Pinephone. I will keep an eye on the progress, because I would be more than happy to give it a go once UFS support, 3D acceleration and WiFi connectivity issues are fixed. Alternatively, it appears that the OnePlus 6T is among the best supported postmarketOS devices at this point, and from several videos I came across on YouTube it appears that performance is significantly better than the Pinephone. However, a 7-years-old phone battery is probably cooked, and replacing it requires removal of the glued backcover. At an average price (on eBay) of around $100, plus another $30 for the replacement battery, the phone is not a particularly attractive option from a hardware standpoint. I invested quite some time in pursuing my open source projects in the past quarter, hence there are a few updates to share. With 📨🚕 going live Overpush has received a lot of updates over the past months, most of which are as beneficial for self-hosted versions as they are for the hosted service. You can find an overview of the changes on the releases page. zpoweralertd 0.0.2 was released with compatibility for Zig 0.15.1. Apart from the adjustments to compile with the latest Zig release no new things were introduced. Nearly five years after its initial release, zeit has weathered the test of time ( hah ) relatively well and continues to grow in popularity on GitHub . What started as a minimal command-line time-tracking utility has evolved into a program packed with a wide range of features and options. Depending on your preferences, you might however say that it now has one too many these days. zeit began as a personal pet project, with no clear long-term plan. Whenever users requested a new feature or option, I either implemented it myself or accepted their pull requests without much second thought. My mantra was simple: If a small enhancement made the software more useful to even one other person, I was happy to introduce it. Fast forward to today, and the very first version of zeit (dubbed zeit v0 ) has strayed far from its roots as a minimal and clean command-line tool. Instead, it has grown into a somewhat unwieldy UX experience, cluttered with features that are neither intuitive nor well thought out. From a code perspective, some of the decisions that made sense a few years ago now seem less ideal, particularly as we look ahead. While I could have sifted through the original v0 codebase to clean it up and remove features that were added by contributors who ultimately didn’t maintain them long-term, I chose instead to rewrite zeit from the ground up. This new version will be based on more modern dependencies and, hopefully, will be cleaner, more streamlined, and free of the “one-off” features that were added for single users who eventually stopped using zeit altogether. That said, I’ve learned a lot from the feature requests submitted over the past five years. With this new version, I’m working to implement the most useful and practical requests in a way that feels more cohesive and polished from a UX perspective, and less like an afterthought. I’m nearing the release of the first version of this complete rewrite, which will be called zeit v1 and carry the version number v1.0.0 . This new version will not be compatible with your existing zeit v0 database. However, if you’re currently using zeit v0 , you can export your entries using , and then import them into v1 with the new command. If you’re interested in a command-line utility for time tracking, especially if you’re already using a different tracker, I’d love to hear from you . Let me know your top three feature requests for a tool like zeit and which platform(s) you currently use or would like to switch from. Footnote: The artwork was generated using AI and further botched by me using the greatest image manipulation program . Twitter X: Mastodon Facebook: See Twitter X. Reddit: Lemmy Instagram: Pixelfed YouTube: PeerTube Spotify: Funkwhale , or simply host your own Jellyfin server WhatsApp: plenty to choose from The Fed, ECB, etc.: Monero , Bitcoin , et al.

0 views
iDiallo 2 weeks ago

AI Video Overview

Google is creating a bigger and wider chasm between users and the source of data. Currently, my blog's traffic from Google searches has dropped significantly since AI Overviews launched. Where users once clicked through to read my articles, they now get their answers directly from Google's AI summary and never visit the source. Before, it was straightforward: you searched for something, Google showed the website that had the information, and you clicked on it. Now, when you search for information, you're presented with an AI Overview that tries to answer your search query. This is fine from a user's standpoint. You had a question, now you have an answer. But who answered your question? Google crawls the web, finds websites that have the information you need, then summarizes them neatly for end-users. The problem is, with AI summaries, you never get to see the source information. Sure, there's a small link behind a collapsible menu, but it now means you rarely click on links anymore. Links , the very thing that made the web hyperconnected, take a back seat. Long term, since users aren't clicking on links, there are fewer incentives for anyone to create content. And Google will eventually have to find a way to source content from somewhere. But before we get there, I want to put my cards on the table. The next frontier for Google Search is video. And the technology is already here. For videos, Google often presents a YouTube video in the search results and highlights the part that's relevant to your search query. You still watch the video, and there are still incentives for the person who created the instructional video to continue doing so. The creator gets views, ad revenue, subscribers, etc. The ecosystem still works. When you search for "how to fix a leaky faucet," Google shows you a YouTube video and jumps to the 2:30 mark where the actual fix is demonstrated. You watch that person's content, maybe subscribe, maybe watch their other videos. They directly benefit. But this is just the stepping stone to something much bigger. What happens when Google starts showing AI Video Overviews? A few years back, I wrote about how YouTube uses machine learning to predict the most likely video you will want to watch . Their goal is to keep you on the platform for as long as possible. Based on your history, and that of millions of people sharing the same watch pattern, they keep you watching by recommending the most appealing next videos. Earlier this year, I wrote that Google (through YouTube) has all the ingredients to create the perfect video for you. In my article "The Perfect YouTube Video" , I explored how YouTube tracks every aspect of how you watch their video and react to it. Using the different data points you generate, they could prompt Veo (Google's video generator) to create the perfect video for you. A video so enticing that you'd have a hard time skipping it. This might not have been possible when I wrote that article, but at the rate AI is progressing, I wouldn't be surprised if in a couple of years Veo creates video in real time. Now, Google has Genie 3 , an impressive world-building model that creates a world you can navigate in real time. It operates at 720p resolution and 24 frames per second. Combine this with Veo's video generation capabilities, and you have all the ingredients needed to create real-time AI Overview videos. Here is what Google's AI can extract from videos right now: And then here is what they can generate: Let's walk through a scenario. You have some free time today, and you finally want to try your hand at baking cookies. You search for a recipe online, and Google gives you the ingredients from an Old Family Recipe, the link buried somewhere below. Now, you go to the store, buy the ingredients, and you're in your kitchen wearing your apron and chef's hat, ready to bake some cookies. This time you Google "how to bake cookies." You're presented with a wall of text from the AI Overview listing those same ingredients you bought before. But you're not much of a chef or a reader. Instead, you want to see how the cookies will look because you're a visual learner. What's that in the top right corner? A new Google feature? It says "AI Video Overview." You click the button and a new window appears. It loads for just 15 seconds, and you're presented with a hyper-realistic kitchen, with an AI-generated avatar narrating the steps with perfect lip-sync and text overlays listing ingredients. The video is just 30 seconds, cutting all the fluff usually found on cooking channels. In a 30-second video that you can scrub through, you can see all the steps for baking your cookies. Of course, at the end of the video there's a card that appears where you can click and see the source videos Google used to generate this 30-second clip. But who clicks on that? There is a rise on zero click searches . This will be extremely convenient for users. Why waste time hearing all the fluff and a Nord VPN sponsorship when all you need is the steps to bake? But here is what will remain unseen: This isn't science fiction. Yes, it doesn't exist just yet. But it's the logical next step in Google's evolution from search engine to answer engine. Just as my blog now gets fewer clicks because people read the AI Overview instead of visiting my site, video creators will soon face the same reality. The old value exchange model of the internet is breaking down. We were used to Google sending traffic our way when we created high-quality information that helped users. As a reward, we got views, revenue, and built a following. With the new model: Google uses our content as training data → AI generates competing content → Users get information → We get nothing. Sure, there will be attribution buried in a menu somewhere, just like there is for text overviews now. But when was the last time you clicked on those source links after reading an AI summary? The chasm between users and creators isn't just widening. It's becoming a canyon. And unlike text, where you might still want to read the original article for depth or personality, AI video overviews will be so polished and efficient that there will be even less reason to click through to the source. For video creators, what's your value when an AI can synthesize your expertise, replicate your techniques, and present them more efficiently than you ever could? The future may lie in what AI cannot easily replicate. Like live interaction, community building, unique personality, and the kind of deep, original insight that goes beyond answering simple informational queries. I understand that it might take another leap in efficiency before these videos can be generated in real time, but the work is being done. All the major AI players are heavily investing more data centers and research to improve their product. But first, we need to acknowledge what's happening. Google is building a world where your content fuels their answers, but your audience never finds you. Visual elements : Objects, tools, ingredients, environments, lighting setups Actions and sequences : Step-by-step processes, timing, hand movements Audio content : Narration, background music, sound effects Text overlays : Ingredient lists, measurements, temperature settings Style and presentation : Camera angles, pacing, editing choices Realistic environments : Through Genie 3's world modeling Human avatars : With perfect lip-sync and natural movements Coherent narratives : Combining information from multiple sources Optimal pacing : Based on user engagement data The cooking channels that spent hours creating detailed tutorials become invisible training data . Their personality, expertise, and hard work get synthesized into a "perfect" but soulless AI version . Users get their answer without ever engaging with the original source. No views, no ad revenue, no subscribers for the people who actually created the knowledge.

0 views
Simon Willison 2 weeks ago

Claude Sonnet 4.5 is probably the "best coding model in the world" (at least for now)

Anthropic released Claude Sonnet 4.5 today , with a very bold set of claims: Claude Sonnet 4.5 is the best coding model in the world. It's the strongest model for building complex agents. It’s the best model at using computers. And it shows substantial gains in reasoning and math. Anthropic gave me access to a preview version of a "new model" over the weekend which turned out to be Sonnet 4.5. My initial impressions were that it felt like a better model for code than GPT-5-Codex, which has been my preferred coding model since it launched a few weeks ago . This space moves so fast - Gemini 3 is rumored to land soon so who knows how long Sonnet 4.5 will continue to hold the "best coding model" crown. The pricing is the same as the previous Sonnet: $3/million input tokens and $15/million output tokens. This remains significantly cheaper than Claude Opus - $15/$75 - but still quite a bit more than GPT-5 and GPT-5-Codex, both at $1.25/$10. The claude.ai web interface (not yet the Claude iPhone native app) recently added the ability for Claude to write and then directly execute code in a sandboxed server environment, using Python and Node.js. I wrote about that in detail three weeks ago. Anthropic's implementation of this code interpreter pattern is more powerful than ChatGPT's equivalent because it can directly clone code from GitHub and install software packages from NPM and PyPI. Sonnet 4.5 absolutely shines at using this tool! Here's my most ambitious experiment with it. I started like this: It did exactly as I asked, checking out my LLM repo , installing it along with all of its dependencies, and running the tests. ✅ All 466 tests passed in 167.69 seconds (about 2 minutes 47 seconds) Then I posed it a wildly ambitious challenge: As you can see in the resulting conversation transcript , it did a really impressive job of this! It churned away across dozens of different tool calls, eventually outputting the following summary of its activity: I've successfully experimented with adding tree-structured conversations to the LLM CLI tool. Here's what was accomplished: 1. Database Schema Enhancement 2. Comprehensive Utility Module ( ) 3. Complete Test Suite ( ) 4. Integration Verification The implementation is production-ready with comprehensive testing. Next steps would be: All code is tested, documented, and ready to use! Did I mention I kicked off this experiment by entering prompts on my phone? I told it to - you can explore the contents of the file it made me in this Gist . Update : I shared a longer version of this experiment where I had the model experiment with a complex message hashing mechanism as well. I've pushed the code to a branch , which is best explained by the message_matching_notes.md document written by Claude. I released llm-anthropic 0.19 adding support for the new model. Then I ran my classic pelican benchmark , first with thinking turned on : And then without : These are pretty good - they are recognizably pelicans! - though not quite as good as GPT-5-Codex which is better at drawing bicycles. I also tried having it describe this image: This image shows a large gathering of pelicans crowded together along what appears to be a waterfront or shoreline. The birds are densely packed, creating a chaotic scene with dozens of brown pelicans visible. Their distinctive long bills and pouches are clearly visible among the mass of feathered bodies. In the background, there's calm water and a dark tree line or forested area. The lighting suggests this was taken during early morning or late afternoon, giving the scene a golden, warm tone. The pelicans appear to be resting or congregating together, which is typical behavior for these colonial seabirds. The release of this model has been very well coordinated. My embargo on talking about it was due to lift at 10am Pacific today, and I got an email from them linking to their blog post at 10am on the dot. It's also already live on OpenRouter and in Cursor and GitHub Copilot and no doubt a whole bunch of other places as well. Anthropic also shipped a new Claude Code VS Code extension today, plus a big upgrade to the Claude Code terminal app. Plus they rebranded their confusingly named Claude Code SDK to the Claude Agent SDK instead, emphasizing that it's a tool for building agents beyond just customizing the existing Claude Code product. That's available for both TypeScript and Python . You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . Added column to the table via migration Enables tree-structured (branching) conversations instead of just linear sequences Fully backward compatible - existing data unaffected 12 helper functions for tree operations: Navigation (children, parent, siblings, paths) Analysis (depth, descendants, size) Queries (roots, leaves, statistics) Visualization (ASCII tree display) 16 tests covering all tree operations Tests linear chains, branching, multiple roots, forests Includes realistic debugging scenario All tests pass ✅ Updated existing migration tests to expect new column All 6 migration tests pass Total: 22/22 tests passing ✅ README.md - Quick start guide IMPLEMENTATION_SUMMARY.md - Complete technical overview MIGRATION_CODE.md - Database migration details TEST_RESULTS.md - Full test coverage report tree_notes.md - Design decisions and development notes tree_utils.py - Utility functions module test_tree_conversations.py - Test suite Multiple branches from any conversation point Multiple roots per conversation (forest structure) Rich analytics (depth, branching factor, tree size) ASCII tree visualization Cycle detection for safety Integrate into LLM package Add CLI commands ( , ) Update to accept

0 views
pkh.me 2 weeks ago

Code golfing a tiny demo using maths and a pinch of insanity

A matrix can be seen as a function, so mathematically writing p'=M \cdot p would be equivalent to the code with: Doing p'=M \cdot p is rotating the space p lies into, which means it gives the illusion the object is rotating clockwise . Though, in the expression , I can't help but be bothered by the redundancy of , so I would prefer to write instead. Since matrices are not commutative, this will instead do a counter-clockwise rotation of the object. The inlined rotation ends up being: To make the rotation clockwise, we can of course use , or we can transpose the matrix: . This is problematic though: we need to repeat the angle 4 times, which can be particularly troublesome if we want to create a macro and/or don't want an intermediate variable for the angle. But I got you covered: trigonometry has a shitton of identities, and we can express every according to a (and the other way around). For example, here is another formulation of the same expression: Now the angle appears only once, in a vectorized cosine call. GLSL has and functions, but it doesn't expose anything for \pi nor \tau constants. And of course, it doesn't have and implementation either. So it's obvious they want us to use \arccos(-1) for \pi and \arccos(0) for \pi/2 : To specify as a normalized value, we can use . On his Unofficial Shadertoy blog, Fabrice Neyret goes further and provide us with a very cute approximation , which is the one we will use: I checked for the best numbers in 2 digits , and I can confirm they are indeed the ones providing the best accuracy. On this last figure, the slight red/green on the outline of the circle represents the loss of precision. With 3 digits, and can respectively be used instead of and . This is good when we want a dynamic rotation angle (we will need that for the camera panning typically), but sometimes we just need a hardcoded value: for example in the of our combined noise function. is fine but we can do better. Through Inigo's demos I found the following: . It makes a rotation angle of about 37° (around 0.64 radians) in a very tiny form. Since 0.5 was pretty much arbitrary, we can just use this matrix as well. And we can make it even smaller (thank you jolle ): One last rotation tip from Fabrice's bag of tricks: rotating in 3D around an axis can be done with the help of GLSL swizzling: We will use this too. is the same , if we need to save one character and can't transpose the matrix. One last essential before going creative is the camera setup. We start with the 2D pixel coordinates which we are going to make resolution independent by transforming them into a traditional mathematical coordinates system: Since we know our demo will be rendered in landscape mode, dividing by is enough. We can also save one character using : To enter 3D space, we append a third component, giving us either a right or a left-handed Y-up coordinates system. This choice is not completely random. Indeed, it's easier/shorter to add a 3rd dimension at the end compared to interleaving a middle component. Compare the length of to (Z-up convention). In the former case, picking just a plane remains short and easy thanks to swizzling: instead of . To work in 3D, we need an origin point ( for ray origin) and a looking direction ( for ray direction). is picked arbitrarily for the eye position, while is usually calculated thanks to a helper: Which is then used like that, for example: I made a Shadertoy demo to experiment with different 3D coordinate spaces if you are interested in digging this further. All of this is perfectly fine because it is flexible, but it's also way too much unnecessary code for our needs, so we need to shrink it. One approach is to pick a simple origin and straight target point so that the matrix is as simple as possible. And then later on apply some transformations on the point. If we give and , we end up with an identity matrix, so we can ditch everything and just write: This can be shorten further: since the vector is normalized anyway, we can scale it at will, for example by a factor , saving us precious characters: And just like that, we are located at the origin , looking toward Z+, ready to render our scene. It's finally time to build our scene. We're going to start with our function previously defined, but we're going tweak it in various ways to craft a mountain height map function. Here is our first draft: We're exploiting one important correlation of the noise function: at every octave, the amplitude is halving while the frequency is doubling. So instead of having 2 running variables, we just have an amplitude getting halved every octave, and we divide our position by (which is the same as multiplying by a frequency that doubles itself). I actually like this way of writing the loop because we can stop the loop when the amplitude is meaningless ( acts as a precision stopper). Unfortunately, we'll have to change it to save one character: is too long for the iteration, we're going to double instead by using which saves one character. So instead the loop will be written the other way around: . It's not exactly equivalent, but it's good enough (and we can still tweak the values if necessary). We're going to inline the constants and rotate, and use one more cool trick: can be shortened: we just need another . Luckily we have , so we can simply write . Similarly, if we needed we could have written (it works also like that: to shorten ). We can also get rid of the braces of the loop by using the in its local scope. In the end, this is our function: To render this in 3D, we are going to do some ray-marching. The main technique used in most Shadertoy demos is ray-marching. I will assume familiarity with the technique, but if that's not the case, An introduction to Raymarching (YouTube) by kishimisu and Painting with Math: A Gentle Study of Raymarching by Maxime Heckel were good resources for me. In short: we start from a position in space called the ray origin and we project it toward a ray direction . At every iteration we check the distance to the closest solid in our scene, and step toward that distance, hoping to converge closer and closer to the object boundary. We end up with this main loop template: This works fine for solids expressed with 3D distance fields , that is functions that for a given point give the distance to the object. We will use it for our mountain, with one subtlety: the noise height map of the mountain is not exactly a distance (it is only the distance to what's below our current point ): Because of this, we can't step by the distance directly, or we're likely to go through mountains during the stepping ( ). A common workaround here is to step a certain percentage of that distance to play it safe. Technically we should figure out the theorical proper shrink factor , but we're going to take a shortcut today and just arbitrarily cut. Using trial and error I ended up with 20% of the distance. After a few simplifications, we end up with the following (complete) code: We start at so I dropped the variable entirely. Also, to avoid the division by 0 in in , is moved right at the beginning (we could also initialize to a value slightly different than 0). You may be curious about the power at the end; this is just a combination of luminance perception with gamma 2.2 (sRGB) transfer function. It only works well for grayscale; for more information, see my previous article on blending . Compared to the mountain, the clouds and fog will need a 3 dimensional noise. Well, we don't need to be very original here; we simply extend the 2D noise to 3D: The base frequency is lowered to to make it smoother, and the goes from 2 to 3 dimensions. Notice how the rotation is only done on the y-axis, the one pointing up): don't worry, it's good enough for our purpose. We also add a phase (meaning we are offsetting the sinusoid) of ( is the time in seconds, slowed down by the multiply) to slowly morph it over time. The base frequency and time scale being identical is a happy "coincidence" to be factored out later (I actually forgot about it until jolle reminded me of it). You also most definitely noticed isn't explicitly initialized: while only true WebGL, it guarantees zero initialization so we're saving a few characters here. For volumetric material (clouds and fog), the loop is a bit different: instead of calculating the distance to the solid for our current point , we do compute the density of our target "object". Funny enough, it can be thought as a 3D SDF but with the sign flipped: positive inside (because the density increases as we go deeper) and negative outside (there is no density, we're not in it). For simplicity, we're going to rewrite the function like this: Compared to the solid ray-marching loop, the volumetric one doesn't bail out when it reaches the target. Instead, it slowly steps into it, damping the light as the density increases: The core idea is that the volumetric material emit some radiance but also absorbs the atmospheric light. The deeper we get, the smaller the transmittance gets, til it converges to 0 and stops all light. All the threshold you see are chosen by tweaking them through trial and error, not any particular logic. It is also highly dependent on the total number of iterations. Steps get larger and larger as the distance increases; this is because we don't need as much precision per "slice", but we still want to reach a long distance. We want to be positioned below the clouds, so we're going to need a simple sign flip in the function. The fog will take the place at the bottom, except upside down (the sharpness will give a mountain-hug feeling) and at a different position. becomes: Having a single ray-marching loop combining the two methods (solid and volumetric) can be challenging. In theory, we should stop the marching when we hit a solid, bail out of the loop, do some fancy normal calculations along with light position. We can't afford any of that, so we're going to start doing art from now on. We start from the volumetric ray-marching loop, and add the distance to the mountain: If gets small enough, we can assume we hit a solid: In volumetric, the attenuation is calculated with the Beer-Lambert law. For solid, we're simply going to make it fairly high: This has the effect of making the mountain like a very dense gas. We're also going to disable the light emission from the solid (it will be handled differently down the line): The transmittance is not going to be changed when we hit a solid as we just want to accumulate light onto it: Finally, we have to combine the volumetric stepping ( ) with the solid stepping ( ) by choosing the safest step length, that is the minimum: We end up with the following: We can notice the mountain from negative space and the discrete presence of the fog, but it's definitely way too dark. So the first thing we're going to do is boost the radiance, as well as the absorption for the contrast: This will make the light actually overshoot, so we also have to replace the current gamma 2.2 correction with a cheap and simple tone mapping hack : . Halving the color is yet another tweak that is not obtained on anything but trial and error. There might be clever ways to reach the same result, but I leave that up to the reader: The clouds and fog are much better but the mountain is still trying to act cool. So we're going to tweak it in the loop: This boosts the overall emission. While at it, since the horizon is also sadly dark, we want to blast some light into it: When the density is null (meaning we're outside clouds and fog), an additional light is added, proportional to how far we are from any solid (the sky gets the most boost basically). The mountain looks fine but I wanted a more eerie atmosphere, so I changed the attenuation: Now instead of being a hard value, the attenuation is correlated with the proximity to the solid (when getting close to it). This has nothing to with any physics formula or anything, it's more of an implementation trick which relies on the ray-marching algorithm. The effect it creates is those crack-like polygon edges on the mountain. To add more to the effect, the emission boost is tweaked into: This makes the bottom of the mountain darker quadratically: only the tip of the mountain would have the glowing cracks. We've been working in grayscale so far, which is a usually a sound approach to visual art in general. But we can afford a few more characters to move the scene to a decent piece of art from the 21st century. Adding the color just requires very tiny changes. First, the emission boost is going to target only the red component of the color: And similarly, the overall addition of light into the horizon/atmosphere is going to get a redish/orange tint: We're almost done. For the last tweak, we're going to add a cyclic panning rotation of the camera, and adjust the moving speed: I'm currently satisfied with the "seed" of the scene, but otherwise it would have been possible to nudge the noise in different ways. For example, remember the can be replaced with in either or both volumetric and mountain related noises. Similarly, the offsetting could be changed into for a different morphing effect. And of course the rotations can be swapped (either by changing into or transposing the values). At this point, our code went through early stages of code golfing, but it still needs some work to reach perfection. Stripped out of its comments, it looks like this: The first thing we're going to do is notice that both the mountain, clouds, and fog use the exact same loop. Factoring them out and inlining the whole thing in the main function is the obvious move: Next, we are going to do the following changes: Onto the next pass of tricks: I'm also reordering a bit some instructions for clarity 🙃 The last touch is going to be nasty: we're going to reorder the instructions such that the 2nd loop is located at the very beginning of the 1st one: "Why?!" you may ask. Before answering this question, let's see why it still works: the first iteration ends up being executed with , where most calculations just cancel themselves out, leading to one wasted iteration (out of 100). Visually, it makes zero difference. But thanks to this weird change, we end up with a bunch of instructions that we can pack into the last placeholder of the main loop, comma separated. This notably allows us to drop the of the main loop: And here we are. All we have to do now is remove all unnecessary spaces and line breaks to obtain the final version. I'll leave you here with this readable version. I'm definitely breaking the magic of that artwork by explaining everything in detail here. But it should be replaced with an appreciation for how much concepts, math, and art can be packed in so little space. Maybe this is possible because they fundamentally overlap? Nevertheless, writing such a piece was extremely refreshing and liberating. As a developer, we're so used to navigate through mountains of abstractions, dealing with interoperability issues, and pissing glue code like robots. Here, even though GLSL is a very crude language, I can't stop but being in awe by how much beauty we can produce with a standalone shader. It's just... Pure code and math, and I just love it.

2 views
Anton Zhiyanov 2 weeks ago

Go is #2 among newer languages

I checked out several programming languages rankings. If you only include newer languages (version 1.0 released after 2010), the top 6 are: ➀ TypeScript, ➁ Go, ➂ Rust, ➃ Kotlin, ➄ Dart, and ➅ Swift. Sources: IEEE , Stack Overflow , Languish . I'm not using TIOBE because their method has major flaws. TypeScript's position is very strong, of course (I guess no one likes JavaScript these days). And it's great to see that more and more developers are choosing Go for the backend. Also, Rust scores very close in all rankings except IEEE, so we'll see what happens in the coming years.

0 views
iDiallo 2 weeks ago

How to Lead in a Room Full of Experts

Here is a realization I made recently. I'm sitting in a room full of smart people. On one side are developers who understand the ins and outs of our microservice architecture. On the other are the front-end developers who can debug React in their sleep. In front of me is the product team that has memorized every possible user path that exists on our website. And then, there is me. The lead developer. I don't have the deepest expertise on any single technology. So what exactly is my role when I'm surrounded by experts? Well, that's easy. I have all the answers. OK. Technically, I don't have all the answers. But I know exactly where to find them and connect the pieces together. When the backend team explains why a new authentication service would take three weeks to build, I'm not thinking about the OAuth flows or JWT token validation. Instead, I think about how I can communicate it to the product team who expects it done "sometime this week." When the product team requests a "simple" feature, I'm thinking about the 3 teams that need to be involved to update the necessary microservices. Leadership in technical environments isn't about being the smartest person in the room. It's about being the most effective translator. I often get "eye rolls" when I say this to developers: You are not going to convince anyone with facts. In a room full of experts, your technical credibility gets you a seat at the table, but your social skills determine whether anything productive happens once you're there. Where ideally you will provide documentation that everyone can read and understand, in reality, you need to talk to get people to understand. People can get animated when it comes to the tools they use. When the database team and the API team are talking past each other about response times, your role isn't to lay down the facts. Instead it's to read the room and find a way to address technical constraints and unclear requirements. It means knowing when to let a heated technical debate continue because it's productive, and when to intervene because it's become personal. When you are an expert in your field, you love to dive deep. It's what makes you experts. But someone needs to keep one eye on the forest while everyone else is examining the trees. I've sat through countless meetings where engineers debated the merits of different caching strategies while the real issue was that we hadn't clearly defined what "fast enough" meant for the user experience. The technical discussion was fascinating, but it wasn't moving us toward shipping. As a leader, your job isn't to have sophisticated technical opinions. It's to ask how this "discussion" can move us closer to solving our actual problem. When you understand a problem, and you have a room full of experts, the solution often emerges from the discussion. But someone needs to clearly articulate what problem we're actually trying to solve. When a product team says customers are reporting the app is too slow, that's not a clear problem. It's a symptom. It might be that users are not noticing when the shopping cart is loaded, or that maybe we have an event that is not being triggered at the right time. Or maybe the app feels sluggish during peak hours. Each of those problems has different solutions, different priorities, and different trade-offs. Each expert might be looking at the problem with their own lense, and may miss the real underlying problem. Your role as a leader is to make sure the problem is translated in a way the team can clearly understand the problem. By definition, leading is knowing the way forward. But in reality, in a room full of experts, pretending to know everything makes you look like an idiot. Instead, "I don't know, but let's figure it out" becomes a superpower. It gives your experts permission to share uncertainty. It models intellectual humility. And it keeps the focus on moving forward rather than defending ego. It's also an opportunity to let your experts shine. Nothing is more annoying than a lead who needs to be the smartest person in every conversation . Your database expert spent years learning how to optimize queries - let them be the hero when performance issues arise. Your security specialist knows threat models better than you, give them the floor when discussing architecture decisions. Make room for some productive discussion. When two experts disagree about implementation approaches, your job isn't to pick the "right" answer. It's to help frame the decision in terms of trade-offs, timeline, and user impact. Your value isn't in having all the expertise. It's in recognizing which expertise is needed when, and creating space for the right people to contribute their best work. There was this fun blog post I read recently about how non-developers read tutorials written by developers . What sounds natural to you, can be complete gibberish to someone else. As a lead, you constantly need to think about your audience. You need to learn multiple languages to communicate the same thing: Developer language: "The authentication service has a dependency on the user service, and if we don't implement proper circuit breakers, we'll have cascading failures during high load." Product language: "If our login system goes down, it could take the entire app with it. We need to build in some safeguards, which will add about a week to the timeline but prevent potential outages." Executive language: "We're prioritizing system reliability over feature velocity for this sprint. This reduces risk of user-facing downtime that could impact revenue." All three statements describe the same technical decision, but each is crafted for its audience. Your experts shouldn't have to learn product speak, and your product team shouldn't need to understand circuit breaker patterns. But someone needs to bridge that gap. "I'm the lead, and we are going to do it this way." That's probably the worst way to make a decision. That might work in the short term, but it erodes trust and kills the collaborative culture that makes expert teams thrive. Instead, treat your teams like adults and communicate the reason behind your decision: The more comfortable you become with not being the expert, the more effective you become as a leader. When you stop trying to out-expert the experts, you can focus on what expert teams actually need: Your role isn't to have all the answers. It's to make sure the right questions get asked, the right people get heard, and the right decisions get made for the right reasons. Technical leadership in expert environments is less about command and control, and more about connection and context. You're not the conductor trying to play every instrument. You're the one helping the orchestra understand what song they're playing together. That's a much more interesting challenge than trying to be the smartest person in the room.

0 views
Sean Goedecke 3 weeks ago

What I learned building an AI-driven spaced repetition app

I spent the last couple of weeks building an AI-driven spaced repetition app. You can try it out here . Like many software engineering types who were teenagers in the early 2000s 1 , I’ve been interested in this for a long time. The main reason is that, unlike many other learning approaches, spaced repetition works . If you want to learn something, study it now, then study it an hour later, then a day later, then a week later, and so on. You don’t have to spend much time overall, as long as you’re consistent about coming back to it. Eventually you only need to refresh your memory every few years in order to maintain a solid working knowledge of the topic. Spaced repetition learning happens more or less automatically as part of a software engineering job. Specific engineering skills will come up every so often (for instance, using to inspect open network sockets, or the proper regex syntax for backtracking). If they come up often enough, you’ll internalize them. It’s more difficult to use spaced repetition to deliberately learn new things. Even if you’re using a spaced repetition tool like Anki , you have to either write your own deck of flashcards (which requires precisely the kind of expertise you don’t have yet), or search for an existing one that exactly matches the area you’re trying to learn 2 . One way I learn new things is from LLMs. I wrote about this in How I use LLMs to learn new subjects , but the gist is that I ask a ton of follow-up questions about a question I have. The best part about this approach is that it requires zero setup cost: if at any moment I want to learn more about something, I can type a question out and rapidly dig in to something I didn’t already know. What if you could use LLMs to make spaced repetition easier? Specifically, what if you could ask a LLM to give you an infinite feed of spaced repetition flashcards, adjusting the difficulty based on your responses? That’s the idea behind AutoDeck . You give it a topic and it gives you infinite flashcards about that topic. If it’s pitched too easy (e.g. you keep saying “I know”) or too hard, it’ll automatically change the difficulty. The thing I liked most about building AutoDeck is that it’s an AI-driven app where the interface isn’t chat . I think that’s really cool - almost every kind of killer AI app presents a chat interface. To use Claude Code, you chat with an agent. The various data analysis tools are typically in a “chat with your data” mode. To use ChatGPT, you obviously chat with it. That makes sense, since (a) the most unusual thing about LLMs is that you can talk with them, and (b) most AI apps let the user take a huge variety of possible actions, for which the only possible interface is some kind of chat. The problem with chat is that it demands a lot of the user. That’s why most “normal” apps have the user click buttons instead of type out sentences, and that’s why many engineering and design blogs have been writing about how to build AI apps that aren’t chat-based. Still, it’s easier said than done. I think spaced repetition flashcards are a good use-case for AI. Generating them for any topic is something that would be impossible without LLMs, so it’s a compelling idea. But you don’t have to interact with them via text (beyond typing out what topic you want at the outset). How do you use AI to generate an infinite feed of content? I tried a bunch of different approaches here. The two main problems here are speed and consistency . Speed is difficult because AI generation can be pretty slow: counting the time-to-first-token, it’s a few hundred ms, even for quick models. If you’re generating each flashcard with a single request, a user who’s familiar with the subject matter can click through flashcards faster than the AI can generate them. Batching up flashcard generation is quicker (because you only wait for time-to-first-token once) but it forces the user to wait much longer before they see their first card. What if you generate flashcards in parallel? That has two problems of its own. First, you’re still waiting for the time-to-first-token on every request, so throughput is still much slower than the batched approach. Second, it’s very easy to generate duplicate cards that way. Even with a high temperature, if you ask the same model the same question with the same prompt, you’re likely to get similar answers. The parallel-generation flashcard feed was thus pretty repetitive: if you wanted to learn about French history, you’d get “what year was the Bastille stormed” right next to “in what year was the storming of the Bastille”, and so on. The solution I landed on was batching the generation, but saving each card as it comes in . In other words, I asked the model to generate ten cards, but instead of waiting for the entire response to be over before I saved the data, I made each card available to the client as soon as it was generated. This was trickier than it sounds for a few reasons. First, it means you can’t use JSON structured outputs . Structured outputs are great for ensuring you get a response that your code can parse, but you can’t (easily) parse chunks of JSON mid-stream. You have to wait for the entire output before it’s valid JSON, because you need the closing or characters 3 . Instead, I asked the model to respond in XML chunks, which could be easily parsed as they came in. Second, it meant I couldn’t simply have the client request a card and get a card back. The code that generated cards had to be able to run in the background without blocking the client, which forced the client to periodically check for available cards. I built most of AutoDeck with OpenAI’s Codex. It was pretty good! I had to intervene in maybe one change out of three, and I only had to seriously intervene (i.e. completely change the approach) in one change out of ten. Some examples of where I had to intervene: I tried Claude Code at various parts of the process and honestly found it underwhelming. It took longer to make each change and in general required more intervention, which meant I was less comfortable queueing up changes. This is a pretty big win for OpenAI - until very recently, Claude Code has been much better than Codex in my experience. I cannot imagine trying to build even a relatively simple app like this without being a competent software engineer already. Codex saved me a lot of time and effort, but it made a lot of bad decisions that I had to intervene. It wasn’t able to fix every bug I encountered. At this point, I don’t think we’re in the golden age of vibe coding. You still need to know what you’re doing to actually ship an app with one of these tools. One interesting thing about building AI projects is that it kind of forces you to charge money. I’ve released previous apps I’ve built for free, because I wanted people to use them and I’m not trying to do the software entepreneur thing. But an app that uses AI costs me money for each user - not a ton of money, but enough that I’m strongly incentivized to charge a small amount for users who want to use the app more than just kicking the tires. I think this is probably a good thing. Charging money for software is a forcing function for actually making it work. If AI inference was free, I would probably have shipped AutoDeck in a much more half-assed state. Since I’m obliged to charge money for it, I spent more time making sure it was actually useful than I would normally spend on a side project. I had a lot of fun building AutoDeck! It’s still mainly for me, but if you’ve read this far I hope you try it out and see if you like it as well. I’m still trying to figure out the best model. GPT-5 was actually pretty bad at generating spaced repetition cards: the time-to-first-token was really slow, and the super-concise GPT-5 style made the cards read awkwardly. You don’t need the smartest available model for spaced repetition, just a model with a good grasp of a bunch of textbook and textbook-adjacent facts. The surviving ur-text for this is probably Gwern’s 2009 post Spaced Repetition for Efficient Learning . Most existing decks are tailored towards students doing particular classes (e.g. anatomy flashcards for med school), not people just trying to learn something new, so they often assume more knowledge than you might have. I think this is just a lack of maturity in the ecosystem. I would hope that in a year or two you can generate structured XML, JSONL, or other formats that are more easily parseable in chunks. Those formats are just as easy to express as a grammar that the logit sampler can adhere to.

0 views
iDiallo 4 weeks ago

The Modern Trap

Every problem, every limitation, every frustrating debug session seemed to have the same solution: Use a modern solution. Modern encryption algorithms. Modern deployment pipelines. Modern database solutions. The word modern has become the cure-all solution, promising to solve not just our immediate problems, but somehow prevent future ones entirely. I remember upgrading an app from PHP 5.3 to 7.1. It felt like it was cutting edge. But years later, 7.1 was also outdated. The application had a bug, and the immediate suggestion was to use a modern version of PHP to avoid this non-sense. But being stubborn, I dug deeper and found that the function I was using that was deprecated in newer versions, had an alternative since PHP 5.3. A quick fix prevented months of work rewriting our application. The word "modern" doesn't mean what we think it means. Modern encryption algorithms are secure. Modern banking is safe. Modern frameworks are robust. Modern infrastructure is reliable. We read statements like this every day in tech blogs, marketing copy, and casual Slack conversations. But if we pause for just a second, we realize they are utterly meaningless. The word "modern" is a temporal label, not a quality certificate. It tells us when something was made, not how well it was made. Everything made today is, by definition, modern. But let's remember: MD5 was once the modern cryptographic hash. Adobe Flash was the modern way to deliver rich web content. Internet Explorer 6 was a modern browser. The Ford Pinto was a modern car. "Modern" is a snapshot in time, and time has a cruel way of revealing the flaws that our initial enthusiasm blinded us to. Why do we fall for this? "Modern" is psychologically tied to "progress." We're hardwired to believe the new thing solves the problems of the old thing. And sometimes, it does! But this creates a dangerous illusion: that newness itself is the solution. I've watched teams chase the modern framework because the last one had limitations, not realizing they were trading known bugs for unknown ones. I've seen companies implement modern SaaS platforms to replace "legacy" systems, only to create new single points of failure and fresh sets of subscription fees. We become so busy fleeing the ghosts of past failures that we don't look critically at the path we're actually on. "Modern" is often just "unproven" wearing a better suit. I've embraced modern before, being on the very edge of technology. But that meant I had to keep up to date with the tools I use. Developers spend more time learning new frameworks than mastering existing ones, not because the new tools are objectively better, but because they're newer, and thus perceived as better. We sacrifice stability and deep expertise at the altar of novelty. That modern library you imported last week? It's sleek, it's fast, it has great documentation and a beautiful logo. It also has a critical zero-day vulnerability that won't be discovered until next year, or a breaking API change coming in the next major version. "Legacy" codebases have their problems, but they often have the supreme advantage of having already been battle-tested. Their bugs are known, documented, and patched. In the rush to modernize, we discard systems that are stable, efficient, and perfectly suited to their task. I've seen reliable jQuery implementations replaced by over-engineered React applications that do the same job worse, with more overhead and complexity. The goal becomes "be modern" instead of "be effective." But this illusion of "modern" doesn't just lead us toward bad choices; it can bring progress to a halt entirely. When we sanctify something as "modern," we subtly suggest we've arrived at the final answer. Think about modern medicine. While medical advances are remarkable, embedded in that phrase is a dangerous connotation: that we've reached the complete, final word on human health. This framing can make it difficult to question established practices or explore alternative approaches. Modern medicine didn't think it was important for doctors to wash their hands . The same happens in software development. When we declare a framework or architectural pattern "modern," we leave little room for the "next." We forget that today's groundbreaking solution is merely tomorrow's foundation or tomorrow's technical debt. Instead of modern, I prefer the terms "robust" or "stable". The most modern thing you can do is to look at any solution and ask: "How will this look obsolete in ten years?" Because everything we call "modern" today will eventually be someone else's legacy system. And that's not a bug, it's a feature. It's how progress actually works.

0 views
Loren Stewart 4 weeks ago

React Won by Default – And It's Killing Frontend Innovation

React-by-default has hidden costs. Here's a case for making deliberate choices to select the right framework for the job.

0 views
Phil Eaton 1 months ago

In response to a developer asking about systems

Sometimes I get asked questions that would be more fun to answer in public. All letters are treated as anonymous unless permission is otherwise granted. Hey [Redacted]! It's great to hear from you. I'm very glad you joined the coffee club and met some good folks. :) You asked how to learn about systems. A great question! I think I need to start first with what I mean when I say systems. My definition of systems is all of the underlying software we developers use but are taught not to think about because they are so solid: our compilers and interpreters, our databases, our operating system, our browser, and so on. We think of them as basically not having bugs, we just count on them to be correct and fast enough so we can build the applications that really matter to users. But 1) some developers do actually have to work on these fundamental blocks (compilers, databases, operating systems, browsers, etc.) and 2) it's not thaaaat hard to get into this development professionally and 3) even if you don't get into it professionally, having a better understanding of these fundamental blocks will make you a better application developer. At least I think so. To get into systems I think it starts by you just questioning how each layer you build on works. Try building that layer yourself. For example you've probably used a web framework like Rails or Next.js. But you can just go and write that layer yourself too (for education). And you've probably used Postgres or SQLite or DynamoDB. But you can also just go and write that layer yourself (for education). It's this habit of thinking and digging into the next lower layer that will get you into systems. Basically, not being satisfied with the black box. I do not think there are many good books on programming in general, and very very few must-read ones, but one that I recommend to everybody is Designing Data Intensive Applications. I think it's best if you read it with a group of people. (My book club will read it in December when the 2nd edition comes out, you should join.) But this book is specific to data obviously and not interested in the fundamentals of other systems things like compilers or operating systems or browsers or so on. Also, I see getting into this as a long-term thing. Throughout my whole career (almost 11 years now) I definitely always tried to dig into compilers and interpreters, I wrote and blogged about toy implementations a lot. And then 5 years ago I started digging into databases and saw that there was more career potential there. But it still took 4 years until I got my first job as a developer working on a database (the job I currently have). Things take time to learn and that's ok! You have a long career to look forward to. And if you end up not wanting to dig into this stuff that's totally fine too. I think very few developers actually do. And they still have fine careers. Anyway, I hope this is at least mildly useful. I hope you join the Software Internals Discord and nycsystems.xyz as well and look forward to seeing you at future coffee clubs! Cheers, Phil I wrote a letter in response to a developer asking about how to learn systems. pic.twitter.com/2ILNpzl662

0 views
Cassidy Williams 1 months ago

I made a tree visualizer

I was going through some old folders on my laptop and found some old code from my old React Training teaching days for visualizing how component trees in React work, and turned it into its own standalone web app! Here’s the app, if you’re not in the mood to read more . Back when I was teaching React full time, one of my fellow teachers Brad made a tool that let you show a tree (like a data structure tree), and we would use it in our lectures to explain what prop drilling and context were, and how components could work together in React apps. I loved using the tool, and thought about some use cases for it when I found the old code, so I spruced it up a bit. It has some keyboard commands for usage: Most usefully (to me) is the ability to quickly make images for sharing them! The original code had each node maintain its own internal state and it was kind of recursive in how they rendered ( see exact lines here ). In retrospect, I probably should have changed it to be rendered with a big state tree at the top and serialized in the URL or local storage (kind of like what I did with PocketCal ), but eh, it works! See ya next time!

0 views