@@ -0,0 +1,834 @@
2+title = "8. Subscriptions"
7+In this lesson we're going to look at subscriptions. Subscriptions are probably
8+the most complicated part of writing agents, so there's a fair bit to cover.
10+## Incoming subscriptions
12+Subscription requests from other entities arrive in your agent's `on-watch` arm.
13+The `on-watch` arm takes the `path` to which they're subscribing, and produces a
23+Your agent's subscription paths would be defined in this arm, typically in a
24+wutlus (`?+`) expression or similar:
27+?+ path (on-watch:def path)
35+ =/ when=@da (slav %da i.t.path)
39+ ?+ t.t.path (on-watch:def path)
47+Subscription paths can be simple and fixed like the first two examples above:
48+`/updates` and `/blah/blah`. They can also contain "wildcard" elements, with an
49+atom of a particular aura encoded in an element of the `path`, as in the `[%foo @ ~]` example. The type pattern matcher is quite limited, so we just specify
50+such variable elements as `@`, and then decode them with something like `(slav %da i.t.path)` (for a `@da`), as in the example. The incoming `path` in this
51+example would look like `/foo/~2021.11.14..13.30.39..6b17`. For more information
52+on decoding atoms in strings, see the [Strings
53+Guide](/docs/hoon/guides/strings#decoding-from-text).
55+In the last case of `[%bar baz *]`, we're allowing a variable number of elements
56+in the path. First we check it's `/foo/bar/...something...`, and then we check
57+what the "something" is in another wutlus expression and handle it
58+appropriately. In this case, it could be `/foo/bar`, `/foo/bar/abc/def`, or
59+`/foo/bar/blah`. You could of course also have "wildcard" elements here too, so
60+there's not really a limit to the complexity of your subscription paths, or the
61+data that might be encoded therein.
63+Permissions can be checked as described in the previous lesson, comparing the
64+source `@p` of the request in `src.bowl` to `our.bowl` or any other logic you
67+If a permission check fails, the path is not valid, or any other reason you want
68+to reject the subscription request, your agent can simply crash. The behavior
69+here is the same as with `on-poke` - Gall will send a `%watch-ack` card in
70+response, which is either an ack (positive acknowledgement) or a nack (negative
71+acknowledgement). The `(unit tang)` in the `%watch-ack` will be null if
72+processing succeeded, and non-null if it crashed, with a stack trace in the
73+`tang`. Like with `poke-ack`s, you don't need to explicitly send a
74+`%watch-ack` - Gall will do it implicitly.
76+As well as sending a `%watch-ack`, Gall will also record the subscription in the
77+`sup` field of the `bowl`, if it succeeded. Then, when you send updates out to
78+subscribers of the `path` in question, the new subscriber will begin receiving
81+Updates to subscribers would usually be sent from other arms, but there's one
82+special case for `on-watch` which is very useful. Normally updates can only be
83+sent to all subscribers of a particular path - you can't target a specific
84+subscriber. There's one exception to this: In `on-watch`, when there's a new
85+subscription, you can send a `%fact` back with a null `(list path)`, and it'll
86+only go to the new subscriber. This is most useful when you want to give the
87+subscriber some initial state, which you otherwise couldn't do without sending
88+it to everyone. It might look something like this:
92+:~ [%give %fact ~ %todo-update !>(`update:todo`initial+tasks)]
96+## Sending updates to subscribers
98+Once your agent has subscribers, it's easy to send them out updates. All you
99+need to do is produce `card`s with `%fact`s in them:
103+:~ [%give %fact ~[/some/path /another/path] %some-mark !>('some data')]
104+ [%give %fact ~[/some/path] %some-mark !>('more data')]
109+The `(list path)` in the `%fact` specifies which subscription `path`s to which
110+the `%fact` should be sent. All subscribers of all `path`s specified will
111+receive the `%fact`. Any agent arm which produces a `(quip card _this)` can send
112+`%fact`s to subscribers. Most often they will be produced in the `on-poke` arm,
113+since new data will often be added in `poke`s.
115+## Kicking subscribers
117+To kick a subscriber, you just send a `%kick` `card`:
120+[%give %kick ~[/some/path] `~sampel-palnet]
123+The `(list path)` specifies which subscription `path`s the ship should be kicked
124+from, and the `(unit ship)` specifies which ship to kick. The `(unit ship)` can also be null, like so:
127+[%give %kick ~[/some/path] ~]
130+In this case, all subscribers to the specified `path`s will be kicked.
132+Note that `%kick`s are not exclusively sent by the agent itself - Gall can also
133+automatically kick subscribers under certain network conditions. Because of
134+this, `%kick`s are not typically interpreted as intentional, and the usual
135+behavior for a kicked agent is to try and resubscribe. Therefore, if you want to
136+disallow a particular subscriber, your agent's `on-watch` arm should reject
137+further subscription requests from them - your agent should not just `%kick`
138+them and call it a day.
140+## Outgoing subscriptions
142+Now that we've covered incoming subscriptions, we'll look at the other side of
143+it: Subscribing to other agents. This is done by `%pass`ing the target agent a
144+`%watch` task in a `card`:
147+[%pass /some/wire %agent [~some-ship %some-agent] %watch /some/path]
150+If yor agent's subscription request is successful, updates will come in to your
151+agent's `on-agent` arm on the `wire` (`/some/wire` in this example) specified.
152+The `wire` can be anything you like - its purpose is for your agent to figure
153+out which subscription the updates are for. The `[ship term]` pair specifies the
154+ship and agent you're trying to subscribe to, and the final `path` (`/some/path`
155+in this example) is the subscription path to which you want to to subscribe - a
156+`path` the target agent has defined in its `on-watch` arm.
158+Gall will deliver the `card` to the target agent and call that agent's
159+`on-watch` arm, which will process the request [as described
160+above](#incoming-subscription-requests), accept or reject it, and send back
161+either a positive or negative `%watch-ack`. The `%watch-ack` will come back in to your agent's `on-agent` arm in a `sign`, along with the `wire` you specified (`/some/wire` in this example):
165+ |= [=wire =sign:agent:gall]
166+ ^- (quip card _this)
170+The `sign` will be of the following format:
173+[%watch-ack p=(unit tang)]
176+How you want to handle the `%watch-ack` really depends on the particular agent. In the simplest case, you can just pass it to the `on-agent` arm of `%default-agent`:
179+++ on-agent on-agent:def
182+`%default-agent` will just accept it and do nothing apart from printing the
183+error in the `%watch-ack` `tang` if it's a nack. You shouldn't have your agent
184+crash on a `%watch-ack` - even if it's a nack your agent should process it
185+successfully. If you wanted to apply some additional logic on receipt of the
186+`%watch-ack`, you'd typically first test the `wire`, then test whether it's a
187+`%watch-ack`, then test whether it's an ack or a nack and do whatever's
192+ |= [=wire =sign:agent:gall]
193+ ^- (quip card _this)
194+ ?+ wire (on-watch:def wire sign)
196+ ?+ -.sign (on-watch:def wire sign)
199+ ...(do something if ack)...
200+ ...(do something if nack)...
204+The `on-agent` arm produces a `(quip card _this)`, so you can produce new
205+`card`s and update your agent's state, as appropriate.
207+One further thing to note with subscriptions is that you can subscribe multiple
208+times to the same `path` on the same ship and agent, as long as the `wire` is
209+unique. If the ship, agent, `path` and `wire` are all the same as an existing
210+subscription, Gall will not allow the request to be sent, and instead fail with
211+an error message fed into the `on-fail` arm of your agent.
215+Assuming the `%watch` succeeded, your agent will now begin receiving any
216+`%fact`s the agent to which you've subscribed publishes on the path to which
217+you've subscribed. These `%fact`s will also come in to your agent's `on-agent`
218+arm in a `sign`, just like the initial `%watch-ack`. The `%fact` `sign` will
219+have the following format:
225+You would typically handle such `%fact`s in the following manner: Test the
226+`wire`, test whether the `sign` is a `%fact`, test the `mark` in the `cage`,
227+extract the data from the `vase` in the `cage`, and apply your logic. For example:
231+ |= [=wire =sign:agent:gall]
232+ ^- (quip card _this)
233+ ?+ wire (on-watch:def wire sign)
235+ ?+ -.sign (on-watch:def wire sign)
237+ ?+ p.cage.sign (on-watch:def wire sign)
239+ =/ foo !<(expected-type q.cage.sign)
244+Note that Gall will not allow `sign`s to come into `on-agent` unsolicited, so
245+you don't necessarily need to include permission logic in this arm.
247+The `on-agent` arm produces a `(quip card _this)`, so you can produce new
248+`card`s and update your agent's state, as appropriate.
252+For whatever reason, the agent you're `%watch`ing might what to kick your agent
253+from a `path` to which it's suscribed, ending your subscription and ceasing to
254+send your agent `%fact`s. To do this, it will send your agent a `%kick` card [as
255+described above](#kicking-subscribers). The `%kick` will come in to your agent's
256+`on-agent` arm in a `sign`, like `%watch-ack`s and `%fact`s do. The `%kick`
257+`sign` will have the following format:
263+Since the `%kick` itself contains no information, you'll need to consider the
264+`wire` it comes in on to know what it pertains to. As explained previously,
265+`%kick`s aren't always intentional - sometimes Gall will kick subscribers due to
266+network issues. Your `on-agent` arm therefore has no way to know whether the
267+other agent actually intended to kick it. This means _your agent should almost
268+always try to resubscribe if it gets kicked_. Then, if the resubscribe `%watch`
269+request is rejected with a negative `%watch-ack`, you can conclude that it was
270+intentional and give up. The logic would look something like this:
274+ |= [=wire =sign:agent:gall]
275+ ^- (quip card _this)
276+ ?+ wire (on-agent:def wire sign)
278+ ?+ -.sign (on-agent:def wire sign)
281+ :~ [%pass /some/wire %agent [src.bowl dap.bowl] %watch /some/path]
286+## Leaving a subscription
288+Eventually you may wish to unsubscribe from a `path` in another agent and stop
289+receiving updates. This is done by `%pass`ing a `%leave` task to the agent in
293+[%pass /some/wire %agent [~some-ship %some-agent] %leave ~]
296+The subcription to be ended is determined by the combination of the `wire`, ship
297+and agent, so the `%leave` task itself always just has `~` at the end.
301+Here we're going to give a pretty well fleshed out example. It will demonstrate
302+both inbound and outbound subscriptions, most of the concepts we've discussed
303+here, as well as some from the previous lesson - `/sur` files, `mark` files, and
306+In previous lessons we've only dealt with things on a local ship - this example
307+will demonstrate messages being sent over the network.
309+The example will be composed of two separate agents - a publisher called
310+`/app/todo.hoon` and a subscriber called `/app/todo-watcher.hoon`, which will
311+live on separate ships. It will be a very rudimentary To-Do app - to-do tasks
312+will be poked into the publisher and sent out to the subscriber as `%fact`s,
313+which will just print them to the dojo. It will have its types defined in
314+`/sur/todo.hoon`, and it will have a couple of `mark` files for pokes and
315+updates: `/mar/todo/action.hoon` and `/mar/todo/update.hoon`.
317+Before we get into trying it out, we'll first walk through the `/sur` file, mark
318+files, and each agent.
322+#### `/sur/todo.hoon`
328++$ task [=name done=?]
329++$ tasks (map id task)
350+This file defines most of the types for the agents. The list of to-do tasks will
351+be stored in the state of the publisher agent as the `tasks` type, a `(map id task)`, where a `task` is a `[=name done=?]`. The set of ships allowed to
352+subscribe will be stored in `friends`, a `(set @p)`, also in the publisher's
353+state. After that, there are the head-tagged unions of accepted poke `action`s
354+and `update`s for subscribers.
356+#### `/mar/todo/action.hoon`
373+This is a very simple mark file for the `action` type.
375+#### `/mar/todo/update.hoon`
392+This is a very simple mark file for the `update` type.
396+#### `/app/todo.hoon`
400+/+ default-agent, dbug
405++$ state-0 [%0 =friends:todo =tasks:todo]
406++$ card card:agent:gall
414+ def ~(. (default-agent this %.n) bowl)
416+ ^- (quip card _this)
423+ ^- (quip card _this)
424+ =/ old !<(versioned-state old-state)
430+ |^ ^- (quip card _this)
431+ ?> =(src.bowl our.bowl)
432+ ?+ mark (on-poke:def mark vase)
435+ (handle-poke !<(action:todo vase))
440+ ^- (quip card _state)
443+ :_ state(tasks (~(put by tasks) now.bowl [name.action %.n]))
444+ :~ [%give %fact ~[/updates] %todo-update !>(`update:todo`[%add now.bowl name.action])]
448+ :_ state(tasks (~(del by tasks) id.action))
449+ :~ [%give %fact ~[/updates] %todo-update !>(`update:todo`action)]
453+ :_ state(tasks (~(jab by tasks) id.action |=(=task:todo task(done !done.task))))
454+ :~ [%give %fact ~[/updates] %todo-update !>(`update:todo`action)]
458+ :_ state(tasks (~(jab by tasks) id.action |=(=task:todo task(name name.action))))
459+ :~ [%give %fact ~[/updates] %todo-update !>(`update:todo`action)]
463+ `state(friends (~(put in friends) who.action))
466+ :_ state(friends (~(del in friends) who.action))
467+ :~ [%give %kick ~[/updates] `who.action]
473+ ^- (quip card _this)
474+ ?+ path (on-watch:def path)
476+ ?> (~(has in friends) src.bowl)
478+ :~ [%give %fact ~ %todo-update !>(`update:todo`initial+tasks)]
481+++ on-leave on-leave:def
482+++ on-peek on-peek:def
483+++ on-agent on-agent:def
484+++ on-arvo on-arvo:def
485+++ on-fail on-fail:def
489+This is the publisher agent, `todo.hoon`. The bulk of its logic is in its
490+`on-poke` arm, where it handles the various possible actions like `%add`ing a
491+task, `%toggle`ing its "done" state, `%rename`ing a task, and so on. It also has
492+a couple of `action`s for `%allow`ing and `%kick`ing subscribers.
494+Most of these cases both update the state of the agent, as well as producing
495+`%fact` cards to send out to subscribers with the new data.
497+You'll notice it only allows these pokes from the local ship, and enforces this
501+?> =(src.bowl our.bowl)
504+Let's look at `on-watch`:
509+ ^- (quip card _this)
510+ ?+ path (on-watch:def path)
512+ ?> (~(has in friends) src.bowl)
514+ :~ [%give %fact ~ %todo-update !>(`update:todo`initial+tasks)]
519+When `on-watch` gets a subscription request, it checks whether the requesting
520+ship is in the `friends` set, and crashes if it is not. If they're in `friends`,
521+it produces a `%fact` card with a null `(list path)`, which means it goes only
522+to the new subscriber. This `%fact` contains the entire `tasks` map as it
523+currently exists, getting the new subscriber up to date.
529+/+ default-agent, dbug
535++$ card card:agent:gall
543+ def ~(. (default-agent this %.n) bowl)
545+ ^- (quip card _this)
552+ ^- (quip card _this)
553+ =/ old !<(versioned-state old-state)
559+ ^- (quip card _this)
560+ ?> =(src.bowl our.bowl)
561+ ?+ mark (on-poke:def mark vase)
563+ =/ action !<(?([%sub @p] [%unsub @p]) vase)
567+ :~ [%pass /todos %agent [+.action %todo] %watch /updates]
571+ :~ [%pass /todos %agent [+.action %todo] %leave ~]
575+++ on-watch on-watch:def
576+++ on-leave on-leave:def
577+++ on-peek on-peek:def
579+ |= [=wire =sign:agent:gall]
580+ ^- (quip card _this)
581+ ?+ wire (on-agent:def wire sign)
583+ ?+ -.sign (on-agent:def wire sign)
586+ ((slog '%todo-watcher: Subscribe succeeded!' ~) `this)
587+ ((slog '%todo-watcher: Subscribe failed!' ~) `this)
590+ %- (slog '%todo-watcher: Got kick, resubscribing...' ~)
592+ :~ [%pass /todos %agent [src.bowl %todo] %watch /updates]
596+ ?+ p.cage.sign (on-agent:def wire sign)
598+ ~& !<(update:todo q.cage.sign)
603+++ on-arvo on-arvo:def
604+++ on-fail on-fail:def
608+This is the subscriber agent. Since it's just for demonstrative purposes, it has
609+no state and just prints updates it receives. In practice it would probably keep the `tasks` map it receives in its own state, and then update it as it receives new
612+The `on-poke` arm is fairly simple - it accepts two pokes, to either `[%sub ~some-ship]` or `[%unsub ~some-ship]`.
614+The `on-agent` arm will print whether a subscription request succeeded or
615+failed, as well as printing a message when it gets kicked. When it receives a
616+`%fact` from the publisher agent, it will just print it to the terminal with a
621+We're going to try this between two different ships. The first ship will be the usual fakezod. We'll add both `mark` files, the `/sur` file, and the `todo.hoon` agent to the `%base` desk of our fakezod, putting them in the following directories:
635+In `~zod`'s dojo, we can `|commit %base`, and then start the `%todo` agent:
638+|rein %base [& %todo]
641+Now we need to spin up another fake ship. We'll use `~nut` in this example:
647+Once it's booted, we can `|mount %base` and then add just the `update.hoon` mark file, the `/sur` file, and the `todo-watcher.hoon` agent like so:
652+│ └── todo-watcher.hoon
660+On `~nut` we can then `|commit %base`, and start the `%todo-watcher` agent:
663+|rein %base [& %todo-watcher]
666+Now, on `~nut`, let's try subscribing:
669+> :todo-watcher [%sub ~zod]
671+%todo-watcher: Subscribe failed!
674+Our `%todo-watcher` agent tried, but received a negative `%watch-ack` from
675+`%todo`, because we haven't yet added `~nut` to the `friends` set of allowed
676+ships. Let's now remedy that on `~zod`:
679+> :todo &todo-action [%allow ~nut]
683+Let's also add a couple of to-do tasks, on `~zod`:
686+> :todo &todo-action [%add 'foo']
688+> :todo &todo-action [%add 'bar']
692+If we now check its state with `+dbug`, we'll see they're in the `tasks` map,
693+and `~nut` will also now be in the `friends` set:
699+ { [ p=170.141.184.505.349.079.206.522.766.950.035.095.552
700+ q=[name='foo' done=%.n]
702+ [ p=170.141.184.505.349.079.278.538.984.166.386.565.120
703+ q=[name='bar' done=%.n]
711+Let's now try subscribing again on `~nut`:
714+> :todo-watcher [%sub ~zod]
716+%todo-watcher: Subscribe succeeded!
719+ { [ p=170.141.184.505.349.079.206.522.766.950.035.095.552
720+ q=[name='foo' done=%.n]
722+ [ p=170.141.184.505.349.079.278.538.984.166.386.565.120
723+ q=[name='bar' done=%.n]
729+As you can see, this time it's worked, and we've immediately received the
732+Now, let's try adding another task on `~zod`:
735+> :todo &todo-action [%add 'baz']
739+On `~nut`, we'll see it has received the `%fact` with the new task in it:
743+ id=170.141.184.505.349.082.779.030.192.959.445.270.528
748+Let's try toggle its done state on `~zod`:
751+> :todo &todo-action [%toggle 170.141.184.505.349.082.779.030.192.959.445.270.528]
755+`~nut` will again get the `%fact`:
759+ id=170.141.184.505.349.082.779.030.192.959.445.270.528
763+Recall that incoming subscriptions are stored in `sup.bowl`, and outgoing
764+subscriptions are stored in `wex.bowl`. Let's have a look at the incoming
765+subscription on `~zod`:
770+ duct=~[/gall/sys/req/~nut/todo /ames/bone/~nut/1 //ames]
772+> :todo +dbug [%incoming %ship ~nut]
776+On `~nut`, let's look at the outgoing subscription:
779+> [wire=/todos agnt=[~zod %todo] path=/updates ackd=%.y]
780+> :todo-watcher +dbug [%outgoing %ship ~zod]
784+Now on `~zod`, let's try kicking `~nut` and removing it from our `friends` set:
787+> :todo &todo-action [%kick ~nut]
791+On `~nut`, we'll see it got the `%kick`, tried resubscribing automatically, but
792+was rejected because `~nut` is no longer in `friends`:
795+%todo-watcher: Got kick, resubscribing...
796+%todo-watcher: Subscribe failed!
801+- Incoming subscription requests arrive in an agent's `on-watch` arm.
802+- An agent will define various subscription `path`s in its `on-watch` arm, which
803+ others can subscribe to.
804+- Gall will automatically produce a negative `%watch-ack` if `on-watch` crashed,
805+ and a positive one if it was successful.
806+- Incoming subscribers are recorded in the `sup` field of the `bowl`.
807+- `on-watch` can produce a `%fact` with a null `(list path)` which will go only
808+ to the new subscriber.
809+- Updates are sent to subscribers in `%fact` cards, and contain a `cage` with a
810+ `mark` and some data in a `vase`.
811+- `%fact`s are sent to all subscribers of the paths specified in the `(list path)`.
812+- A subscriber can be kicked from subscription paths with a `%kick` card
813+ specifying the ship in the `(unit ship)`. All subscribers of the specified
814+ paths will be kicked if the `(unit ship)` is null.
815+- An outgoing subscription can be initiated with a `%watch` card.
816+- The `%watch-ack` will come back in to the subscriber's `on-agent` arm as a
817+ `sign`, and may be positive or negative, depending on whether the `(unit tang)` is null.
818+- `%kick`s will also arrive in the subscriber's `on-agent` arm as a `sign`.
819+ Since kicks may not be intentional, the subscriber should attempt to
820+ resubscribe and only give up if the subsequent `%watch-ack` is negative.
821+- `%fact`s will also arrive in the subscriber's `on-agent` arm.
822+- All such `sign`s that arrive in `on-agent` will also have a `wire`.
823+- The `wire` for subscription updates to arrive on is specified in the initial
825+- A subscriber can unsubscribe by passing a `%leave` card on the original
830+- Have a look at the [Strings Guide](/docs/hoon/guides/strings) if you're
831+ already familiar with decoding/encoding atoms in strings.
832+- Try running through the [Example](#example) yourself, if you've not done so already.
833+- Try modifying `%todo-watcher` to recording the data it receives in its state,
834+ rather than simply printing it to the terminal.