Reading List

The most recent articles from a list of feeds I subscribe to.

Xeact 0.70.0: now with the useState hook

Xeact continues to dominate the front-end ecosystem. Every facet of the industry has been forever changed by Xeact, and we are all better for it. Today I have the momentous pleasure of introducing the newest version of Xeact: version 0.70.0. This allows you to track stateful values using the new useState hook.

hero image solo-journey
Image generated by Anything v3 -- 1girl, green hair, hoodie, outdoors, breath of the wild, space needle, walking, long hair, highly detailed, futuristic

To help illustrate the point, I have copied the entire source code of the useState function below:

/**
 * Allows a stateful value to be tracked by consumers.
 *
 * This is the Xeact version of the React useState hook.
 *
 * @type{function(any): [function(): any, function(any): void]}
 */
const useState = (value = undefined) => {
  return [() => value, (x) => {
    value = x;
  }];
};

Mimi, would you care to explain this?

Mimi is coffee
<Mimi> This code defines a function called useState that returns an array with two elements. The first element of the array is a function that takes no arguments and returns the initial value passed in as a parameter to the useState function or undefined if no value is given. This function acts as a getter for the current state value. The second element of the array is a function that takes a single argument and sets the value of value. This function acts as a setter for the state value. This code is implementing a simplified version of the useState hook in React, a commonly used library in JavaScript for building user interfaces.
Aoi is wut
<Aoi> Why would I want to use this instead of normal variables?

The main reason you want to use a stateful hook like this is when you write more complicated components, like the Mastodon share button at the bottom of every post. At a high level, pulling values from HTML elements requires you to either declare each element in variables and then assemble the component like this waifud admin panel component:

// SomeForm.jsx

export default function SomeForm() {
  const input = <input type="text" placeholder="words" />;
  
  return (
    <div>
      {input}
      <button onClick={(e) => alert(input.value)}>Alert</button>
    </div>
  );
}

This creates an input box and a button. When you click on the button, it turns whatever you put into the box into an alert.

However, we can do better.

The useState hook in React allows you to associate components with stateful values, so you can write out all the code like this:

// SomeForm.jsx

import { useState } from "react";

export default function SomeForm() {
  const [msg, setMsg] = useState("");
  
  return (
    <div>
      <input
        type="text"
        placeholder="words"
        onInput={(e) => setMsg(e.target.value)}
      />
      <button onClick={(e) => alert(msg)}>Alert</button>
    </div>
  );
}

As you can see, this lets you have the state be updated in the oninput handler of the <input> box and then used in the onclick handler of the button.

The useState Xeact hook also lets you do this, but with one significant difference: updating the value in the state container does not trigger a redraw of the relevant components that use those values. This does limit the usefulness of the Xeact useState container, but I bet that I'll figure out a way around this should this ever become relevant.

Aoi is coffee
<Aoi> Or you could just use preact like a normal person...
Mara is happy
<Mara> Doing that would not only make sense, that would mean we need to be more inventive for the blog!
Aoi is facepalm
<Aoi> You people astound me.

Oh, and the state reader is a function instead of a value:

// SomeForm.jsx

import { useState } from "xeact";

export default function SomeForm() {
  const [getMsg, setMsg] = useState("");
  
  return (
    <div>
      <input
        type="text"
        placeholder="words"
        onInput={(e) => setMsg(e.target.value)}
      />
      <button onClick={(e) => alert(getMsg())}>Alert</button>
    </div>
  );
}

Either way, I suspect that this will propel Xeact towards its goal of getting many GitHub stars. I've incorporated this state hook into my blog and will write more about it when I've gotten more practical experience with it.

Thank you for following the development of Xeact! There's sure to be more in the future as I figure out what the hell I am supposed to be doing. I'm also starting to realize that I don't suck at frontend development, what I really suck at is design, which is what manifests as feeling like you suck at frontend development.

I'm sure I'll figure it out. Gotta burn sticks to make fire.

Using Tailscale without using Tailscale

I’m always amazed by the power of technology and the creativity of people who use it. Today I want to share with you a project that I did for fun and learning. It’s about how to use Tailscale Funnel to host a Headscale server from behind NAT. Sounds crazy, right? Let me explain.

Tailscale Funnel is a tool that lets you share a web service on your private network with the public internet. It’s like having your own personal cloud without the hassle of setting up servers and domains. Headscale is a tool that lets you create your own Tailscale control plane. It’s like having your own private VPN without relying on a third-party service.

Now, what if you could combine these two tools and create a network that uses Tailscale without using Tailscale? That’s exactly what I did. I used waifud, NixOS, and Funnel to create a virtual machine that runs Headscale and exposes it to the internet. Then I used Funnel to connect other devices to this network and enjoy the benefits of Tailscale.

Why did I do this? Because I love learning new things and challenging myself. Because I believe in the power of open source and decentralized solutions. Because I wanted to have some fun and make some jokes along the way.

This project taught me a lot about networking, security, and automation. It also showed me how much potential there is in tools like Tailscale and Headscale. And it made me laugh at the absurdity of using Tailscale without using Tailscale.

If you’re curious about how I did this, you can check out my blog post where I explain everything in detail. It’s not a serious tutorial, but rather a playful experiment.

I hope this post inspires you to try new things and have fun with technology. Remember, nothing is impossible if you put your mind to it. And don’t forget to laugh at yourself sometimes.

Funnel 101: sharing your local developer preview with the world

🚀 Do you want to share your web server with the world without exposing your computer to the world? 🚀

If you’re like me, you love using Tailscale to create a secure and private network for your devices. But sometimes, you need to let the outside world access your web server, whether it’s for testing, hosting, or collaborating.

That’s why I’m super excited about Tailscale Funnel, a new feature that lets you route traffic from the internet to your Tailscale node. You can think of it as publicly sharing a node for anyone to access, even if they don’t have Tailscale themselves.

Tailscale Funnel is easy to set up and use. You just need to enable it in the admin console and on your node, and you’ll get a public DNS name for your node that points to Tailscale’s Funnel servers. These servers will proxy the incoming requests over Tailscale to your node, where you can terminate the TLS and serve your content.

The best part is that Tailscale Funnel is secure and private. The Funnel servers don’t see any information about your traffic or what you’re serving. They only see the source IP and port, the SNI name, and the number of bytes passing through. And they can’t connect to your nodes directly. They only offer a TCP connection, which your nodes can accept or reject.

Tailscale Funnel is currently in beta and available for all users. I’ve been using it for a while now and I’m blown away by how simple and powerful it is. It’s like having your own personal cloud service without any hassle or cost.

Protos

Cadey is coffee
<Cadey> On July 13, 2020, I was inspired to write out the outline for a short science fiction / horror story about a generative AI being able to write entire features in code and how the market reacted to that. I recently rediscovered it and I feel that now is the time to write it for real.

This is a work of fiction. Names, characters, business, events and incidents are the products of the author’s imagination. Any resemblance to actual persons, living or dead, or actual events is purely coincidental.

One day, Jeff stretched at his desk while he was puzzling out the problem his product manager had thrust upon him. It was an emergency, as usual. The login form had the wrong color at the wrong place, and it was causing people to look at the login form then run away in terror.

hero image jeff-protos
Image generated by Anything V3 -- 1guy, laptop, open office

Or something like that, they just wanted the position of the login button changed so it was under the password box instead of next to it. That should be easy, right?

No. That login form was created by Palima, the person that Jeff had signed off on hiring in the last episode. Ae was absolute force of nature that had single-handedly written half of the missing code in the monolith, and wrote code that was an absolute work of art, but was absolutely impenetrable to anyone trying to modify it. As always, Palima was busy doing god-knows-what and couldn't help with this task that ae felt was beneath aer.

Hiring more people to help with this? Impossible. Headcount was hard to come by due to the recent fad of pointless layoffs. Even E100, the former bastion of refusing to lay anyone off finally succumbed to the investor class pressure to "cut costs". Techaro management had followed suit. So he was left with this problem.

While Jeff was puzzling through the dense block of tokens, he took a look at his favorite news aggregator: Hacker Moose. While scrolling through the links, he saw something called "Protos". It claimed to be a tool that he could install in BS Code and then it could rewrite code to his needs.

Jeff was skeptical. This looks too easy, he thought to himself. But, it had a free trial. He hit "install" and then the commands were available. He pointed it at a personal file he used to learn Palima's HypeScript style, then asked it to refactor a function to take an attribute set instead of normal arguments. Kinda like this:

const fooBar = (bar: number, baz: number) => {
  return bar + baz;
};

To something like this:

interface fooBarArgs {
  bar: string;
  baz: string;
}

const fooBar = ({bar, baz}) => {
  return bar + baz;
};

And then it automatically fixed the rest of the code to match that. Protos was the real deal. Jeff stopped in his tracks and really looked at what was going on. He just did something that he'd spent hours doing manually in seconds.

Jeff immediately pointed Protos at the login form issue, described the change to make, and it started auto-completing the solution. All of the things that Jeff had struggled on for months started to fade away and the solution basically wrote itself.

Jeff was flabbergasted. Just in time for his calendar to fire a reminder that his standup meeting was about to start. He walked over to the lunch area and asked the barista to make him his usual: a double shot latte a-le sirop d'érable. With his cup in hand, he walked over to where his team was standing and started small talk.

Palima was present in the office today, she had her keyboard mounted to her hips and was obviously writing into some smart glasses of some kind. Jeff waved to aer and ae looked up and yawned. "'morning"

"Good afternoon Palima, what're you working on today?"

"Fixing the database. There were problems. It's all better now."

Jeff shuddered at the idea of what the "fix" entailed, but time hit and the manager Ariel spoke up: "Good afternoon everyone! What are you working on, and what did you get done? I've got a lot of 1:1 meetings with many wonderful people today, but I'm happy with our progress in the sprint. Palima, you go next."

"There was an issue with corrupt data being written to the database due to an off-by-one error in encoding JSON. I fixed it, and all the data. We don't have to worry, and this fixes the whyOS app without having to wait for an update to be rejected. Jeff, how're you doing?"

Jeff took a moment to process that and cleared his throat. "I figured out what was wrong with the login form, and I have a PR open for review. Today I'm gonna refactor that code so it's less of a nightmare to deal with in the future."

The standup meeting continued, and nothing of note was really brought up. Jeff walked back to his desk and his manager stopped him on the way back.

"Hey, you really got it done? I thought you estimated a whole week for that."

"I figured it out, estimates are just estimates. This code is really complicated."

Ariel seemed to accept that and started to walk back to his desk. "Congrats though, I've got some more things on the backlog if you want to pick up a few more tickets."

Jeff nodded and walked to his desk. The OurWork that Techaro rented was bubbling with activity like it usually did around lunchtime, but Jeff wasn't hungry today. He was curious.

He made it back to his laptop and opened up BS Code again. The Protos extension had installed a button in the lower right hand of the screen. It was pulsing slightly, beckoning his attention.

He opened up one of the tickets Ariel had talked about and found the bit of code. He described the problem and the changes that needed to be made to Protos, and the logo spun around a bit, then the changes wrote themselves. This was the real deal.

Jeff suddenly became terrified when he realized the power of this technology. He had to be careful with this. He couldn't tell anyone about this and went over to flag the story on Hacker Moose as spam.

This could put him out of a job. He was shaking at his desk when Palima walked over and clicked happily. Jeff looked over at aer and thought he saw something funny but stopped thinking about it. "What's up?"

"Your code change was perfect. It's approved. Feel free to deploy it when you're ready."

Jeff nodded and thanked Palima, then put on his noise-cancelling headphones and hit the merge button. The login form was deployed, peace was brought to the land and product was finally happy for about 20 minutes.

Protos had claimed its first victim. Jeff was supercharged by Protos. It was almost so easy that it wasn't fun. Jeff worked on a few tickets and decided to keep the branches locally so he could release one or two changes per day. Just enough to look like he was working, not enough that it would look suspicious.

Ariel was suspicious though. He also read Hacker Moose and was skeptical that Jeff could have figured out Palima's code so quickly. He was a bit of a developer himself, so he took a look at one of the backlog tickets and fired up Protos to implement a fix.

It took seconds.

Ariel put it up for code review and Jeff was on alert instantly. He didn't know what to do.

Ariel shrugged and continued over to his meeting with the product team. He wanted to show them this neat tool he had found.

The product team was shocked by this discovery. If the product team could just implement things themselves, they wouldn't need any developers at all! Product started using Protos and was able to submit PRs for code review. Jeff was mortified when he saw this get brought up in a meeting.

Eventually, the product team managed to replace everyone but Palima and Jeff on the developer team with Protos. Features kept coming faster and faster, and they were left to pilot a ship that was growing more and more complicated without any way to stop it.

Then Techaro acquired Protos and made it a proprietary internal tool.

Techaro was unstoppable, sending people to Mars, finally solving the secret to self-driving cars, and eventually curing cancer. All without paying more than 150 developers world-wide to review the mad hallucinations of a machine. They were taking over the world, disrupting the government industry, and then


Jeff woke up at his desk. He must have dozed off. The calendar reminder popped up on his screen, reminding him of his standup. The login form wasn't fixed yet. Hacker Moose didn't have a product named Protos on the frontpage. The domain he remembered from his dream didn't resolve.

Jeff sleepily walked over to his standup and grabbed a coffee. The standup was uneventful but at the end Palima spoke up. Ae said "By the way, has anyone tried using ChatGPT yet? It's pretty cool, and it can write code for you. You just have to describe what you want."

Jeff screamed.

How to use a fork of the Go compiler with Nix

hero image coffee-gopher
Image generated by Fluff Proto-r10 -- rodent, gopher, blue fur, blue hair, blue skin, calarts, solo, male, laptop, coffee shop, detailed background, anthro, coffee mug, happy, black nose, best quality, highly detailed, eyes closed

Sometimes God is dead and you need to build something with a different version of Go than upstream released. Juggling multiple Go toolchains is possible, but it's not very elegant.

However, we're in Nix land. We can do anything*.

Aoi is coffee
<Aoi> *with sufficent hackery.

I got accepted to Gophercon EU and a lot of it involves doing weird things with WebAssembly and messing with assumptions people make about how filesystems work. Given that most of my audience is going to be Go programmers and that I'm already going to be cognitively complicating how core assumptions about filesystems work, I want to show my code examples in Go when at all possible.

Go doesn't currently support WASI, but there is a CL in progress that adds the port under the name GOARCH=wasm GOOS=wasip1. I wanted to pull this into my monorepo's Nix flake so that I can run gowasi build foo.go and get foo.wasm in the same folder to experiment with.

Mara is hacker
<Mara> A CL in the Go ecosystem is a change list or change log. You can think about it as analogous to a pull request in GitHub.

Turns out this is really easy. In order to do this, you need to do three things:

  • Add a flake input for the fork of Go in question
  • Create a build of Go with that fork and a fabricated VERSION file
  • Create the wrapper script and populate it in your devShell

Add a flake input

Nix flake inputs don't let you just import other Nix flakes; they also can be used for any git repository, such as the Go source tree. To create an input with an arbitrary fork of Go (such as Xe/go), do this:

# go + wasip1
wasigo = {
  url = "github:Xe/go/wasip1-wasm";
  flake = false;
};

The important part is flake = false, that tells Nix to treat it as a raw repository and not assume that it's a Nix flake. The wasigo variable can be used as the path to the extracted tarball and will contain the following attributes:

  • lastModified - The last modified time for the git repo in unix time
  • lastModifiedDate - The last modified time in datetime format
  • narHash - The Nix ARchive hash in base64-sha256 form
  • outPath - The Nix store path associated with this flake input
  • rev - The full git hash for this flake input
  • shortRev - The short form of the git hash

Add it as an argument to your outputs function.

Build a custom toolchain

It's common to declare a bunch of variables that your flake uses immediately inside your outputs function like this, so I'm going to assume that you are doing this. Add a variable for your Go fork (eg: wasigo'):

wasigo' = pkgs.go_1_20.overrideAttrs (old: {
  src = pkgs.runCommand "gowasi-version-hack" { } ''
    mkdir -p $out
    echo "go-wasip1-dev-${wasigo.shortRev}" > $out/VERSION
    cp -vrf ${wasigo}/* $out
  '';
});

Aoi is wut
<Aoi> Why are you using a ' and calling it wasigo-prime?
Cadey is enby
<Cadey> If I don't name it something else, I will create an infinitely recursive definition. Nix is lazy and only evaluates things when it needs to. Making a binding called wasigo and using the name wasigo inside that will create infinite recursion when it is evaluated. I don't know of a better name for this, but a common pattern in Nix land is to use primes (') for distinct values with the same name. Just like in Haskell.
Aoi is wut
<Aoi> What about that VERSION file, what's that there for?
Cadey is enby
<Cadey> That is there to tell the Go compiler toolchain what version it is. When you clone a git repository into the Nix store, all of the git metadata is purged from the checkout (because it's not byte-for-byte reproducible and random changes there could cause unwanted rebuilds of a lot of packages). If the VERSION file doesn't exist, the Go toolchain will try to discover what version it is from the git metadata, which doesn't exist. This file lies to the toolchain so that builds work.
Aoi is cheer
<Aoi> I see, thanks!

Make a wrapper script

In many cases, you can just add wasigo' to your devShell buildInputs and you'll be fine. In this case, we want to have a separate command that pre-configures the GOOS and GOARCH environment variables to target WASI. The pkgs.writeScriptBin trivial builder lets you write an arbitrary string to the Nix store as a binary. You can use this to create a wrapper script:

gowasi = pkgs.writeShellScriptBin "gowasi" ''
  export GOOS=wasip1
  export GOARCH=wasm
  exec ${wasigo'}/bin/go $*
'';

This will create a file named bin/gowasi in a Nix package that will set the correct environment variables and then execute the version of Go that was just compiled. It will look something like this:

#!/nix/store/0hx32wk55ml88jrb1qxwg5c5yazfm6gf-bash-5.2-p15/bin/bash
export GOOS=wasip1
export GOARCH=wasm
exec /nix/store/px67cnp39lzynhknqqjjn9c3b838qnw9-go-1.20.2/bin/go $*

Mimi is happy
<Mimi> The exec builtin command in Bash is used to execute a command that completely replaces the current shell process. The original shell process is destroyed and overwritten by the new command. Any commands after the exec command in the script do not get executed.

And then you can go off to the races and compile things to your heart's content!

Overriding buildGoModule for that version of Go

If you want to build go modules using this version of Go, you need to make your own buildGoModule analog:

buildGoWasiModule = pkgs.callPackage "${nixpkgs}/pkgs/build-support/go/module.nix" {
  go = wasigo';
};

Then use buildGoWasiModule like you would buildGoModule.

To force it to build webassembly modules, you will need to override the GOOS and GOARCH attributes in wasigo':

wasigo' = {
  # ...
} // {
  GOOS = "wasip1";
  GOARCH = "wasm";
};

This will force the Go compiler to output WebAssembly binaries, but they will be put in $out/bin/wasmp1_wasm/name without the .wasm suffix. This may not be ideal in some cases, but this is a limitation in how GOEXE is not correctly threaded through the buildGoModule stack when it is hacked like this.