Self Hosting

Hello! This is like a First Post all over again, this time publishing from i.haza.website! 🎉️

I've held off posting a few updates so that I could finish the changes required to bring this blog over, and that day has finally come. Like with the docs and holster sites, there were a lot of incremental changes to make it happen. I will talk more about them in future posts, for now I'm happy to be writing in a nice editor, that auto saves updates, allows me to create drafts, and has some nice templating features built in.

There's quite a few features you need for a static site to publish a blog, one I really wanted from the beginning is multiple posts on the home page. This was added as Front Matter at the start of a template, which makes it capable of publishing pages that don't otherwise exist in your folder structure:

holster.haza.website

The Holster website has been gone since rebuilding i.haza.website, it's now back as a replacement for the github wiki docs with updated information for all API's and better examples 🎉

I Haza Website also got some updates as I was working on this, so it now does nice preview navigation while you're editing pages. You can start from one page in preview mode, navigate to another page you're working on and continue editing from there.

There's also a lot of code examples on the site so I added a Copy Code plugin that allows you to copy code blocks if you want to run them interactively in Node. There's also a theme switcher plugin for light and dark mode:

Holster 2.3.0

A new Holster release is available that provides await support.

Using Holster for building i.haza.website was very different than working on RSStream as it needed to be much more responsive to Holster data changes. Looking back at the code up until this point, it was full of await new Promise(resolve => { }) wrappers so that state could update in the correct order.

The latest Holster release removes all of this from application code by providing a Thenables interface onto terminal API calls. That sounds weird but it does exactly what you need, which is when you would otherwise provide a callback to the Holster API you can just await the call instead. Internally Holster knows when a callback isn't provided and can extend the response to include a then() function, if you were chaining the request then the extra function is ignored, otherwise it provides your data.

This works for get and next in the API, put requests can also be awaited but they now internally return a promise as they only ever returned an error or null anyway. It allows for simplifying get requests like:

// From wrapped calls:
const existing = await new Promise(resolve =>
  user.get("mediaRootId", resolve),
)

// To just:
const existing = await user.get("mediaRootId")

And previously put requests where you waited for the result were:

async function bumpProjectsVersion(user) {
  const now = Date.now()
  return new Promise((resolve, reject) =>
    user
      .get("projectsVersion")
      .put(now, err => (err ? reject(new Error(err)) : resolve(now))),
  )
}

Are now:

async function bumpProjectsVersion(user) {
  const now = Date.now()
  await user.get("projectsVersion").put(now)
  return now
}

It also makes it easier to work with try and catch blocks, but it's also nice that the old callback style isn't effected by these changes when you want to use it.

I Haza Website

Happy to share that i.haza.website is now available again as a new local-first app!

It was previously a PHP app with a similar goal to what the new version is doing, which is to make it easy to publish web pages. The new version is a web app backed by Holster, and besides publishing websites is great for taking notes and transferring media between devices. Holster provides automatic syncing between devices that you're logged in to, as well as encryption and offline support.

This has been another app where the complimentary goal has been Holster improvements alongside having useful tools. This has been true again because I Haza Website is much more interactive with the Holster API then other apps, so it's caught a few things that have needed fixing.

This blog hasn't been replaced yet because I Haza Website doesn't have some of the blogging features of the old app. Expect that to change soon though, I'm looking forward to moving to the new interface it's much nicer to write in. 😊

Nested listeners

Another Holster release to add a new nested listener feature that I've needed for multiple projects now. In RSStream I added it outside of Holster itself, since I wasn't sure how often I would need it.

The idea is that you add a listener to a query in Holster, but that query always refers to a particular node. That node can reference other nodes, but those references don't change. So even though the API chains nodes together you need to explicitly list which ones you want to listen to changes on. The external way to do this is to create on() callbacks for each of the child nodes in the parent's on() callback. This works fine, but the code is really the same no matter what the project. So it's now included with Holster and just needs the extra parameter to the parent query to set up all listeners below that point. They're also automatically removed when the parent off() is called.

I asked Claude for a simple example and it of course gave a stand alone example that works in Node 🤖:

import Holster from "@mblaney/holster/src/holster.js"

const holster = Holster()
const user = holster.user()
  
user.create("alice", "password", () => {
    user.auth("alice", "password", () => {
      // Without {nested: true}, this listener only fires when a NEW key is
      // added to "team" - it won't fire again for a later, value-only
      // change to an EXISTING member, because that's a write to the
      // member's own soul (every object-valued child gets its own), not to
      // "team" itself.
      user.get("team").on(members => {
        console.log("team changed:", members)
      }, true, {nested: true})

      // A brand new key - the listener above always sees this.
      user.get("team").next("alice").put({role: "engineer"})

      // A value-only change to an ALREADY-KNOWN key - only reaches the
      // listener because of {nested: true}. Without it, this write would
      // be silently missed.
      setTimeout(() => {
        user.get("team").next("alice").put({role: "lead engineer"})
      }, 100)
    })
})