The last few weeks have seen a lot of development activity, first in Holster itself then in applications using it. This has been driven by the Free Association project, which is now capable of using Holster instead of Gun. The start of the migration process required some further updates to Holster, which is now at version 1.0.21 and feeling pretty stable!
The rest of the work was done in switching API's in the application itself. Holster's API is not only slightly different to Gun, but also conceptually different. The biggest difference is that Gun streams data whereas Holster resolves all references before returning. This doesn't mean much from a performance perspective, but it does significantly change your application code. I've done a few of these migrations now so I already had the notes from the first one, which I've used again successfully since. I'm sharing them here as hopefully they're general enough for other migrations to benefit from.
On the server I've been developing with both Node and Bun, the first step in Node is:
import Holster from "@mblaney/holster/src/holster.js"
const holster = Holster()
Holster is a module so the path to the file is required. If you're not using a build tool you can switch src to build in the path above. The only option I left out of both examples above was secure: true, which means only data under user accounts will be saved (no public data).
Here is the browser code I previously used for Gun (also not including secure here):
And here is the browser code for Holster. Note that it doesn't rewrite the scheme for WebSockets like Gun, so you need to specify it or use the default:
import Holster from "@mblaney/holster/src/holster.js"
// If on localhost assume Holster is directly available and use the default
// settings, otherwise assume a secure connection is required.
let peers
if (window.location.hostname !== "localhost") {
peers = ["wss://" + window.location.hostname]
}
const holster = Holster({peers: peers, indexedDB: true})
The rest of these notes are for the API's in general, so apply to both browser and server. First is error handling, where Gun returns an object with an err property Holster returns just an error. This means a lot of code changes from ack.err to just err.
The SEA functions work pretty much the same, except Gun uses encryption keys under user._.sea, Holster simplifies this to rely on the user.is object. They can also be used on their own with import SEA from "@mblaney/holster/src/sea.js"
I used user.get(key).once(callback) a lot when working with Gun. In Holster, that pattern is now just user.get(key, callback). I would also previously use user.get(key), and pass that around as a reference to use later. Holster removes the context that gets created once a callback is provided, so that isn't possible and you instead need to write the full query each time.
As mentioned above, the big difference is between streaming data and resolving nodes. So when using Gun you could do: get("items").map().once((item, key) => callback) to get a stream of updates. In Holster it's just get("items", items => callback). So to get the equivalent of Gun's map() you would do: Object.entries(items).forEach(([key, item]) => {callback(item, key)}) on the returned data if you wanted to use the same callback.
Also get(key).map().once(callback) subscribes to updates in Gun, you would need to use get(key).on(callback) instead in Holster for updates.
Gun has get(key).then() which is nice for awaiting data, but it's just a convenience function which can be done in Holster with: await new Promise(res => {get(key, res)})
The biggest change in terms of lines of code has generally been modifying get().get() chains from Gun, to get().next() in Holster. One more thing to watch out for is calling put() in a loop, it's better to use an actual for loop rather than forEach and awaiting each put() so they don't make updates at the same time, especially to the same node.
Migrating from Gun to Holster
The rest of the work was done in switching API's in the application itself. Holster's API is not only slightly different to Gun, but also conceptually different. The biggest difference is that Gun streams data whereas Holster resolves all references before returning. This doesn't mean much from a performance perspective, but it does significantly change your application code. I've done a few of these migrations now so I already had the notes from the first one, which I've used again successfully since. I'm sharing them here as hopefully they're general enough for other migrations to benefit from.
On the server I've been developing with both Node and Bun, the first step in Node is:
My previous Gun initialization code was:
This has been replaced by:
Holster is a module so the path to the file is required. If you're not using a build tool you can switch
src
tobuild
in the path above. The only option I left out of both examples above wassecure: true
, which means only data under user accounts will be saved (no public data).Here is the browser code I previously used for Gun (also not including secure here):
And here is the browser code for Holster. Note that it doesn't rewrite the scheme for WebSockets like Gun, so you need to specify it or use the default:
The rest of these notes are for the API's in general, so apply to both browser and server. First is error handling, where Gun returns an object with an
err
property Holster returns just an error. This means a lot of code changes fromack.err
to justerr
.The
SEA
functions work pretty much the same, except Gun uses encryption keys underuser._.sea
, Holster simplifies this to rely on theuser.is
object. They can also be used on their own withimport SEA from "@mblaney/holster/src/sea.js"
I used
user.get(key).once(callback)
a lot when working with Gun. In Holster, that pattern is now justuser.get(key, callback)
. I would also previously useuser.get(key)
, and pass that around as a reference to use later. Holster removes the context that gets created once a callback is provided, so that isn't possible and you instead need to write the full query each time.As mentioned above, the big difference is between streaming data and resolving nodes. So when using Gun you could do:
get("items").map().once((item, key) => callback)
to get a stream of updates. In Holster it's justget("items", items => callback)
. So to get the equivalent of Gun'smap()
you would do:Object.entries(items).forEach(([key, item]) => {callback(item, key)})
on the returned data if you wanted to use the same callback.Also
get(key).map().once(callback)
subscribes to updates in Gun, you would need to useget(key).on(callback)
instead in Holster for updates.Gun has
get(key).then()
which is nice forawait
ing data, but it's just a convenience function which can be done in Holster with:await new Promise(res => {get(key, res)})
The biggest change in terms of lines of code has generally been modifying
get().get()
chains from Gun, toget().next()
in Holster. One more thing to watch out for is callingput()
in a loop, it's better to use an actualfor
loop rather thanforEach
andawait
ing eachput()
so they don't make updates at the same time, especially to the same node.