The making of Nomad Hypertext

The making of Nomad Hypertext

Sat Dec 30 2023 tools-for-thoughtnomad-hypertextcode
To be frank, I made some regrettable engineering decisions while making Nomad Hypertext. I built this during my Recurse Center batch and was focused on getting a working prototype out, so some shortcuts were taken. Nevertheless, reflecting back, I would say these shortcuts were not worth it. The two big shortcuts I took were using Javascript instead of Typescript and using Electron instead of Tauri.


I opted to use Javascript instead of Typescript out of mix of curiosity and laziness. Like most people, I was introduced to Javascript before I was introduced to Typescript. Like most people, I suffered through many null errors and was relieved when I discovered that the Typescript compiler could save me from many of them. Like most people, though I am grateful for Typescript, I find it occasionally cumbersome.

I read Rich Harris' twitter thread on how Javascript with JSDoc is actually pretty good, and decided to give it a shot. To be clear, I'm not blaming Rich Harris for my choice - he qualified his point, saying JS with JSDoc is preferable to Typescript for library development, because not having an intermediate build step makes it easier to reproduce bugs in a REPL. He still recommends using Typescript for projects (I think), so I was acting against his advice here.

In any case I wanted to try something new because that's what Recurse is all about, so I tried making a project with just JS and JSDoc for some semblance of type safety.

The results were... mixed. I did get to move faster at the start, but started running into more and more type-related bugs as development went on. If I were to do this project again, I'd stick to Typescript.


Electron really, really, really sucks. For starters, you have to use CommonJS. That itself would be bad enough, but there are a huge pile of other inconveniences that Electron dumps on you: Using window.prompt() doesn't work (because it blocks the main thread apparently) and it generates HUGE executables without making the slightest effort to tree-shake anything (it was literally bundling in sample text files I was using in the repo). Creating my own window.prompt() alternative was an interesting exercise, but I wish I wasn't forced into doing it.

Why not Tauri? With Tauri you have to write your backend routes in Rust. Surprisingly, I could not find any in-memory vector databases in Rust. I also have never written Rust in my life, though I would have been happy to pick it up to avoid using Electron. More importantly, I don't think there was a library that would have let me run Hugging Face's text embedding model in Rust. For these reasons, I had to use JS for my backend, which meant I had to use Electron.


You might object: Just because the vector DB and text embedding model are tied to JS doesn't mean your whole application does! You'd be right. I could have rolled the vector DB and text embedding model into a separate executable, compiling a node wrapper around those libraries into their own executables, then calling those executables from my Tauri app with IPC.

I avoided this IPC-executable approach because I thought it'd be really complicated. To some extent, this was the correct decision - I'm glad I got a MVP of my project out during my time at Recurse.

However, when building Yurt, the static site builder for nomad hypertext, I found myself repeating a lot of logic. I found myself wanting to add features to the indexing engine, like being able to use cloud providers instead of local models (because doing all this processing locally can take ages!). In the future, I might want to use multimodal models, so I can see similarity between images, text, and audio. Factoring out the semantic search element of my app would have been the correct, unix-ey thing to do, and it would allow me to make these extensions in the future. I plan on doing this soon.