Listening to updates

I finally finished adding event listeners to the Holster API, and decided to stick with GunDB's idea of on and off functions.

It took me longer than I expected because on callbacks are long-lived, so the context chain I was using needed to stick around somehow. I also realised that I couldn't do much else once there was an event listener because there was only one context. Or if there was another request the on callback would lose what it was referring to when called.

This meant that the API need to store each context while it was being used, and only remove them when a callback had finished. This took a few attempts, as holster.get().get() previously made sense, but having another request start a new holster.get() meant the two requests couldn't be distinguished from each other and would overwrite the single context available. The solution ended up being that each holster.get() starts a new context, and to differentiate new requests I've added then() to provide the chaining API.

Now Holster can do queries such as:

holster.get("hello").then("world!").on(console.log)

// Which will get called by this update.
holster.get("hello").then("world!").put("update", console.log)

I figured that was enough API changes to add a new page to the Github Wiki, so I've also added a new API page with more details.

Returning resolved data

Another step on the roadmap has been completed and removed! Holster will now resolve all data in and out. It previously just returned an object containing rels, now it resolves the ids and replaces the rel in the returned data with the full object. Luckily the code to do this was quite similar to the way ids are resolved when putting nodes, so both holster.get() and holster.put() should work with any arbitrarily nested data.

This change makes the underlying data completely opaque when using the Holster API, so data out is the same as data in:

holster.get("hello", console.log)

{
  "world!": {
    "things": "I want to add",
    "which": {
      "can": "also have nested objects"
    }
  }
}

Another interesting thing you can now do with Holster is see all your data. This is possible because of the above change, but you also need to know the top level keys to get. You can do that using the wire API to get the root node:

holster.wire.get({"#": "root"}, msg => console.log(Object.keys(msg.put.root)))

// Or use the above to display everything on disk
holster.wire.get({"#": "root"}, async msg => {
  // wire API returns meta data.
  delete msg.put.root._
  for (const key of Object.keys(msg.put.root)) {
    const value = await new Promise(resolve => holster.get(key, resolve))
    console.log(key, value)
  }
})

A few caveats, the wire API can be used to create top level nodes too, so this method will only show everything created with the Holster API. The Promise is required above because holster.get() controls the context until the callback is called, so this forces the loop to wait until the previous get has finished.

Roadmap and null nodes

I've added a roadmap to the Github Wiki for Holster, which includes a list of TODO's, one of which I was already able to remove. 🎉

Holster is able to set keys to null, but this previously just set rels to null and left nodes untouched. Removing nodes is quite difficult as the wire spec just wants to update existing values. Holster will now fetch the node and also set all keys to null, so it will at least scrub existing data when required.

Removing the nodes themselves would be great, but needs some more thought for how to add that to the API.

Chained gets

Yesterday's example was meant to be:

holster.get("hello").get("world!").put({
  things: "I want to add",
  which: {
    can: "also have nested objects",
  },
}, console.log)

Which I decided to test before adding to the post, and of course it didn't work! I then realised I hadn't tested chained gets at all, so I modified the example to something that worked and then looked at the chained get problem today.

I've just pushed a fix for it, which thankfully also cleared up some edge cases in the code around resolving keys, so it now works and the code is nicer too.

Holster API

I've just pushed a pretty big update to Holster, it now has an API that sits on top of the wire spec. This allows you to run nice queries similar to the original Gun API, such as:

holster.get("hello").put({
  things: "I want to add",
  which: {
    can: "also have nested objects",
  },
}, console.log)

It then resolves the query and puts objects on the graph, resolving nested objects as it goes, making extra calls to the wire API as it needs to. The webpack build exposes Holster as a single library, so the previous wire API is now available via holster.wire. I've also added a root node to the graph, so it's also possible to put non-object properties at the root level:

holster.get("key").put("value", console.log)

Check out the tests for more examples! There's also a lot more tests including using mock-socket to add more test coverage to what is now wire.js.