Your First Stream
Let’s create your first event stream.
Serve
Unlike sqlite, which operates directly on the file system, xs requires a
running process to manage access to the local store. This enables features like
subscribing to real-time updates from the event stream.
Start an xs store in a dedicated window:
xs serve ./store13:35:16.868 TRACE event src/main.rs:185 Starting server with path: "./store" xs:18513:35:16.957 INFO read options=ReadOptions { follow: On, tail: false, last_id: None, limit: None } xs::store:17413:35:16.957 INFO read options=ReadOptions { follow: On, tail: true, last_id: None, limit: None } xs::store:17413:35:16.963 INFO 5ms insert_frame frame=Frame { id: "03d4por2p16i05i81fjy0fx8u", topic: "xs.start", hash: None, meta: None, ttl: None } xs::store:41013:35:16.963 0fx8u xs.start13:35:16.968 INFO read options=ReadOptions { follow: On, tail: false, last_id: None, limit: None } xs::store:174
For a long-running setup you might run xs serve ~/.local/share/cross.stream/store
under a process supervisor. This is also the fallback location used by
xs.nu when $env.XS_ADDR isn’t set. Here we keep the demo scoped to ./store.
To point tools at another store, set XS_ADDR. This can be done temporarily with with-env:
with-env {XS_ADDR: "./store"} { .cat }Client
append command
OK! Let’s append our first event:
"a quick note" | .append notes───────┬─────────────────────────────────────────────────────topic │ notesid │ 03d4q1qhbiv09ovtuhokw5yxvhash │ sha256-wIcRiyKpOjA1Z8O+wZvoiMXYgGEzPQOhlA8AOptOhBY=meta │ttl │ forever───────┴─────────────────────────────────────────────────────
echo "a quick note" | xs append ./store notes{"topic":"notes","id":"03d4qic9vqkve1krajjtlbavd","hash":"sha256-24yYvzQ4Zd3Go/WevV9ol+KzkdTgQvlyNN2NVSGMjFE=","meta":null,"ttl":"forever"}
cat and cas commands
and then cat the stream:
.cat─#─┬──topic───┬────────────id─────────────┬────────────────────────hash─────────────────────────┬─meta─┬───ttl───0 │ xs.start │ 03d4q1o70y6ek0ig8hwy9q00n │ │ │1 │ notes │ 03d4q1qhbiv09ovtuhokw5yxv │ sha256-wIcRiyKpOjA1Z8O+wZvoiMXYgGEzPQOhlA8AOptOhBY= │ │ forever───┴──────────┴───────────────────────────┴─────────────────────────────────────────────────────┴──────┴─────────
These are the raw frames on the stream. The actually content is stored
separately in the Content-Addressable Storage (CAS). You can read more about
that here.
We have the full expressiveness of Nushell available to us—for example, we can get the content hash of the last frame on the stream using:
.cat | last | $in.hashsha256-wIcRiyKpOjA1Z8O+wZvoiMXYgGEzPQOhlA8AOptOhBY=
and then use the .cas command to retrieve the content:
.cat | last | .cas $in.hasha quick note
We can also retrieve the content for a frame by piping it in its entirety
directly to .cas:
.cat | last | .casa quick note
xs cat ./store{"topic":"xs.start","id":"03d4qiab9g5vagrlrvxa2vjw0","hash":null,"meta":null,"ttl":null}{"topic":"notes","id":"03d4qic9vqkve1krajjtlbavd","hash":"sha256-24yYvzQ4Zd3Go/WevV9ol+KzkdTgQvlyNN2NVSGMjFE=","meta":null,"ttl":"forever"}
These are the raw frames on the stream. The actually content is stored
separately in the Content-Addressable Storage (CAS). You can read more about
that here.
We can retrieve the content for a frame using its content hash:
xs cat ./store | tail -n1 | jq -r .hashsha256-24yYvzQ4Zd3Go/WevV9ol+KzkdTgQvlyNN2NVSGMjFE=
xs cas ./store sha256-24yYvzQ4Zd3Go/WevV9ol+KzkdTgQvlyNN2NVSGMjFE=a quick note
head command
Let’s submit another note:
"submit TPS report" | .append notes.cat─#─┬──topic───┬────────────id─────────────┬────────────────────────hash─────────────────────────┬─meta─┬───ttl───0 │ xs.start │ 03d4q1o70y6ek0ig8hwy9q00n │ │ │1 │ notes │ 03d4q1qhbiv09ovtuhokw5yxv │ sha256-wIcRiyKpOjA1Z8O+wZvoiMXYgGEzPQOhlA8AOptOhBY= │ │ forever2 │ notes │ 03d4qbrxizqgav09m7hicksb0 │ sha256-KDyb7pypM+8aLiq5obfpCqbMmb6LvvPnCu2+y9eWd0c= │ │ forever───┴──────────┴───────────────────────────┴─────────────────────────────────────────────────────┴──────┴─────────
We can get the most recent note on the stream using the .head command:
.head notes───────┬─────────────────────────────────────────────────────topic │ notesid │ 03d4qbrxizqgav09m7hicksb0hash │ sha256-KDyb7pypM+8aLiq5obfpCqbMmb6LvvPnCu2+y9eWd0c=meta │ttl │ forever───────┴─────────────────────────────────────────────────────
.head notes | .cassubmit TPS report
echo "submit TPS report" | xs append ./store notesxs cat ./store{"topic":"xs.start","id":"03d4qiab9g5vagrlrvxa2vjw0","hash":null,"meta":null,"ttl":null}{"topic":"notes","id":"03d4qic9vqkve1krajjtlbavd","hash":"sha256-24yYvzQ4Zd3Go/WevV9ol+KzkdTgQvlyNN2NVSGMjFE=","meta":null,"ttl":"forever"}{"topic":"notes","id":"03d4qjwnhwudlfyg1ygemmt7b","hash":"sha256-pwB4w9N1v99Uu/96KsKKaIgDMm18QDMOZJsEwCxBtsA=","meta":null,"ttl":"forever"}
We can get the most recent note on the stream using the head command:
xs head ./store notes{"topic":"notes","id":"03d4qjwnhwudlfyg1ygemmt7b","hash":"sha256-pwB4w9N1v99Uu/96KsKKaIgDMm18QDMOZJsEwCxBtsA=","meta":null,"ttl":"forever"}
xs head ./store notes | jq -r .hash | xargs xs cas ./storesubmit TPS report
riffing
Finally, let’s pull a list of all our notes.
We can filter directly by topic using --topic and then use the each command to pull out the content of each note:
.cat | where topic == "notes" | each { .cas $in.hash }───┬───────────────────0 │ a quick note1 │ submit TPS report───┴───────────────────
Fun! 🎉
xs cat ./store --topic notes | jq -r .hash | xargs -I{} xs cas ./store {}a quick notesubmit TPS report
Fun! 🎉