on c8ea9f2 help sign in
338 lines 12 kB history raw
1+++
2title = "3. Agent"
3weight = 3
4template = "doc.html"
5+++
7Now that we have our agent's types defined and have thought through its
8behavior, we can write the `%journal` agent itself.
10## Imports
12```hoon
13/- *journal
14/+ default-agent, dbug, agentio
15```
17We first import the `/sur/journal.hoon` file we previously created and expose
18its structures. We import the standard `default-agent` and `dbug`, and also an additional library called `agentio`.
20Agentio contains a number of convenience functions to make common agent tasks
21simpler. For example, rather than writing out the full `$card`s when sending
22`%fact`s to subscribers, we can call `++fact` in `agentio` with the `cage` and
23`path`s and it will compose them for us. There are many more functions in
24`agentio` than we'll use here - you can have a look through the library in
25[`/base/lib/agentio.hoon`](https://github.com/urbit/urbit/blob/master/pkg/base-dev/lib/agentio.hoon)
26to see what else it can do.
28## State and type core
30```hoon
31|%
32+$ versioned-state
33 $% state-0
34 ==
35+$ state-0 [%0 =journal =log]
36+$ card card:agent:gall
37++ j-orm ((on id txt) gth)
38++ log-orm ((on @ action) lth)
39++ unique-time
40 |= [=time =log]
41 ^- @
42 =/ unix-ms=@
43 (unm:chrono:userlib time)
44 |-
45 ?. (has:log-orm log unix-ms)
46 unix-ms
47 $(time (add unix-ms 1))
48--
49```
51As we discussed in the previous section, our state will contain a `$journal`
52structure containing all our journal entries, and a `$log` structure containing
53the update log. These are both _ordered maps_, defined as `((mop id txt) gth)`
54and `((mop @ action) lth)` respectively. We can therefore define our _versioned
55state_ as `[%0 =journal =log]`, in the usual manner.
57We've define `$card` for convenience as usual, and we've also added three more
58arms. The first two relate to our two ordered maps. If you'll recall, an
59ordinary `map` is called with the `++by` door in the standard library, like so:
61```hoon
62(~(get by foo) %bar)
63```
65An ordered map uses the `++on` gate in `zuse.hoon` rather than `++by`, and its
66invocation is slightly different. It must first be setup in a similar manner to
67the `mop` type, by providing it the key/value molds and comparator gates. Once
68that's done, its individual functions can be called with the `mop` and
69arguments, like:
71```hoon
72(get:((on @ud @ud) gth) foo %bar)
73```
75This is quite a cumbersome expression to use every time we want to interact with
76our `mop`. To make it easier, we can store the `((on @ud @ud) gth)` part in an
77arm, and then when we need to use it we can just do `(get:arm-name foo %bar)`.
78In this case, we've done one each of our ordered maps like so:
80```hoon
81++ j-orm ((on id txt) gth)
82++ log-orm ((on @ action) lth)
83```
85The last arm in our state definition core is `++unique-time`. Since we'll use
86`now.bowl` to derive the timestamp for updates, we run into an issue if multiple
87pokes arrive in a single Arvo event. In that case, `now.bowl` would be the same
88for each poke, so they'd be given the same key and override each other in the
89`mop`. To avoid this, `++unique-time` is just a simple recursive function that
90will increment the timestamp by one millisecond if the key already exists in the
91`$log` `mop`, ensuring all updates get unique timestamps and there are no
92collisions.
94## Agent core setup
96```hoon
97%- agent:dbug
98=| state-0
99=* state -
100^- agent:gall
101|_ =bowl:gall
102+* this .
103 def ~(. (default-agent this %|) bowl)
104 io ~(. agentio bowl)
105++ on-init on-init:def
106++ on-save
107 ^- vase
108 !>(state)
109::
110++ on-load
111 |= old-vase=vase
112 ^- (quip card _this)
113 `this(state !<(versioned-state old-vase))
114::
115```
117Here we setup our agent core and define the three lifecycle arms. Since we only
118have a single state version at present, these are very simple functions. You'll
119notice in our `+*` arm, along with the usual `this` and `def`, we've also setup
120the `agentio` library we imported, giving it the bowl and an alias of `io`.
122## Pokes
124```hoon
125++ on-poke
126 |= [=mark =vase]
127 ^- (quip card _this)
128 |^
129 ?> (team:title our.bowl src.bowl)
130 ?. ?=(%journal-action mark) (on-poke:def mark vase)
131 =/ now=@ (unique-time now.bowl log)
132 =/ act !<(action vase)
133 =. state (poke-action act)
134 :_ this(log (put:log-orm log now act))
135 ~[(fact:io journal-update+!>(`update`[now act]) ~[/updates])]
136 ::
137 ++ poke-action
138 |= act=action
139 ^- _state
140 ?- -.act
141 %add
142 ?< (has:j-orm journal id.act)
143 state(journal (put:j-orm journal id.act txt.act))
144 ::
145 %edit
146 ?> (has:j-orm journal id.act)
147 state(journal (put:j-orm journal id.act txt.act))
148 ::
149 %del
150 ?> (has:j-orm journal id.act)
151 state(journal +:(del:j-orm journal id.act))
152 ==
153 --
154::
155```
157Here we have our `++on-poke` arm, where we handle `$action`s. Since our
158`%journal` agent is intended for local use only, we make sure only our ship or
159our moons may perform actions with:
161```hoon
162?> (team:title our.bowl src.bowl)
163```
165We haven't yet written our mark files, but our mark for `$action`s will be
166`%journal-action`, so we make sure that's what we've received and if not, call
167`++on-poke:def` to crash with an error message. We make sure the the timestamps
168are unique with our `++unique-time` function described earlier, and then we
169extract the poke's vase to an `$action` structure and call `++poke-action` to
170handle it. We've made `++on-poke` a door with a separate `++poke-action` arm to
171make the logic a little simpler, but in principle we could have had it all
172directly inside the main `++poke-action` gate, or even separated it out into a
173helper core below.
175The logic in `++poke-action` is very simple, with three cases for each of the possible `$action`s:
177- `%add` - Add a new journal entry. We check it doesn't already exist with
178 `++has:j-orm`, and then add it to our `$journal` with `++put:j-orm`.
180- `%edit` - Edit an existing journal entry. We make sure it _does_ exist with
181 `++has:j-orm`, and then override the old entry with the new one using
182 `++put:j-orm` again.
184- `%del` - Delete an existing journal entry. We make sure it exists again with
185 `++has:j-orm`, and then use `++del:j-orm` to delete it from our `$journal`
186 `mop`.
188Back in the main part of `++on-poke`, `++poke-action` updates the state with the
189new `$journal`, then we proceed to:
191```hoon
192:_ this(log (put:log-orm log now act))
193~[(fact:io journal-update+!>(`update`[now act]) ~[/updates])]
194```
196We add the timestamp to the action, converting it to a logged update. We add it
197to the `$log` update log using `++put:log-orm`, and also send the logged update
198out to subscribers on the `/updates` subscription path. We haven't written our
199mark files yet, but `%journal-update` is the mark we'll use for `$update`s, so
200we pack the `$update` in a vase and add the mark to make it a `$cage`. Notice
201we're using the `++fact` function in `agentio` (which we aliased as `io`) rather
202than manually composing the `%fact`.
204## Subscriptions
206```hoon
207++ on-watch
208 |= =path
209 ^- (quip card _this)
210 ?> (team:title our.bowl src.bowl)
211 ?+ path (on-watch:def path)
212 [%updates ~] `this
213 ==
214::
215```
217Our subscription logic is extremely simple - we just have a single `/updates`
218path, which the front-end or other local agents may subscribe to. All updates
219get sent out on this path. We enforce local-only with the `team:title` check.
221We could have had our `++on-watch` arm send out some initial state to new
222subscribers, but for our front-end we'll instead fetch the initial state
223separately with a scry. This just makes it slightly easier if our front-end
224needs to resubscribe at some point - it'll already have some state in that case
225so we don't want it to get sent again.
227## Scry Endpoints
229```hoon
230++ on-peek
231 |= =path
232 ^- (unit (unit cage))
233 ?> (team:title our.bowl src.bowl)
234 =/ now=@ (unm:chrono:userlib now.bowl)
235 ?+ path (on-peek:def path)
236 [%x %entries *]
237 ?+ t.t.path (on-peek:def path)
238 [%all ~]
239 :^ ~ ~ %journal-update
240 !> ^- update
241 [now %jrnl (tap:j-orm journal)]
242 ::
243 [%before @ @ ~]
244 =/ before=@ (rash i.t.t.t.path dem)
245 =/ max=@ (rash i.t.t.t.t.path dem)
246 :^ ~ ~ %journal-update
247 !> ^- update
248 [now %jrnl (tab:j-orm journal `before max)]
249 ::
250 [%between @ @ ~]
251 =/ start=@
252 =+ (rash i.t.t.t.path dem)
253 ?:(=(0 -) - (sub - 1))
254 =/ end=@ (add 1 (rash i.t.t.t.t.path dem))
255 :^ ~ ~ %journal-update
256 !> ^- update
257 [now %jrnl (tap:j-orm (lot:j-orm journal `end `start))]
258 ==
259 ::
260 [%x %updates *]
261 ?+ t.t.path (on-peek:def path)
262 [%all ~]
263 :^ ~ ~ %journal-update
264 !> ^- update
265 [now %logs (tap:log-orm log)]
266 ::
267 [%since @ ~]
268 =/ since=@ (rash i.t.t.t.path dem)
269 :^ ~ ~ %journal-update
270 !> ^- update
271 [now %logs (tap:log-orm (lot:log-orm log `since ~))]
272 ==
273 ==
274::
275```
277Here we have our `++on-peek` arm. The scry endpoints we've defined are divided
278into two parts: querying the update `$log` and retrieving entries from the
279`$journal`. Each end-point is as follows:
281- `/x/entries/all` - Retrieve all entries in the `$journal`. Our front-end will
282 use lazy-loading and only get a few at a time, so it won't use this. It's nice
283 to have it though, in case other agents want to get that data.
285- `/x/entries/before/[before]/[max]` - Retrieve at most `[max]` entries older
286 than the entry on `[before]` date. This is so our lazy-loading front-end can
287 progressively load more as the user scrolls down the page. The Javascript
288 front-end will format numbers without dot separators, so the path will look
289 like `/x/entries/before/1648051573109/10`. We therefore have to use the
290 [`++dem`](docs/hoon/reference/stdlib/4i#dem) parsing `rule` in a
291 [`++rash`](/docs/hoon/reference/stdlib/4g#rash) parser to convert it to an
292 ordinary atom. We then use the `++tap:log-orm` `mop` function to retrieve the
293 requested range as a list and return it as an `$update` with a
294 `%journal-update` mark.
296- `/x/entries/between/[start]/[end]` - Retrieve all journal entries between two
297 dates. This is so our front-end can have a search function, where the user can
298 enter a start and end date and get all the entries in between. The
299 `++lot:j-orm` `mop` function returns the subset of a `mop` between the two
300 given keys as a `mop`, and then we call `++tap:j-orm` to convert it to a list.
301 The `++lot:j-orm function` excludes the start and end values, so we subtract 1
302 from the start and add 1 to the end to make sure it includes the full range.
304- `/x/updates/all` - Retrieve the entire update `$log`. Our front-end won't use
305 this but it might be useful for other agents, so we've included it here.
307- `/x/updates/since/[since]` - Retrieve all `$update`s that have happened since
308 the specified timestamp, if any. This is so our front-end (or another agent)
309 can resynchronize its state in the event its subscription is interrupted,
310 without having to fetch everything from scratch again.
312We don't use any of the other agent arms, so the remainder have all been passed
313to `default-agent` for handling:
315```hoon
316++ on-leave on-leave:def
317++ on-agent on-agent:def
318++ on-arvo on-arvo:def
319++ on-fail on-fail:def
320--
321```
323The full agent source can be viewed
324[here](https://github.com/urbit/docs-examples/blob/main/journal-app/desk/app/journal.hoon).
326## Resources
328- [The Gall Guide](/docs/userspace/gall-guide/intro) - The Gall Guide covers all
329 aspects of writing Gall agents in detail.
331- [Ordered map functions in
332 `zuse.hoon`](https://github.com/urbit/urbit/blob/master/pkg/arvo/sys/zuse.hoon#L5284-L5688) -
333 This section of `zuse.hoon` contains all the functions for working with
334 `mop`s, and is well commented.
336- [`/lib/agentio.hoon`](https://github.com/urbit/urbit/blob/master/pkg/base-dev/lib/agentio.hoon) -
337 The `agentio` library in the `%base` desk contains a large number of useful
338 functions which making writing Gall agents easier.