`%gall`: Interfacing with a Client
Overview
Teaching: 60 min
Exercises: 30 minQuestions
How can I build a
%gallapp which operates on data?Objectives
Produce an intermediate
%gallapp.Understand how
%gallinterfaces with an external client.
Communications

https://urbit.org/docs/userspace/graph-store/sample-application-overview
We need to examine all of the ways a %gall app can communicate with the outside world. Recall that an agent has ten arms:
|_ =bowl:gall
++ on-init
++ on-save
++ on-load
++ on-arvo
++ on-peek
++ on-poke
++ on-watch
++ on-leave
++ on-agent
++ on-fail
--
Arvo alone interacts with several of these:
++ on-init
++ on-save
++ on-load
++ on-arvo
The ++on-agent and ++on-fail arms are called in certain circumstances (i.e. as an update to a subscription to another agent or cleanup after a %poke crash). We can leave them as boilerplate for now.
If you are exposing information, you can do so via a peek (++on-peek), a response to a poke (++on-poke), or a subscription (++on-watch). (++on-leave handles cleanup after a terminated subscription.) These are the main ways that the Urbit API protocol (formerly Airlock) interacts with an agent on a ship. We’ll focus on these.
++on-peek Scry
A scry represents a direct look into the agent state using the Nock .^ dotket operator.
Only local scries are permitted.
++on-poke Request
A poke initiates some kind of well-defined action by an agent. Typically this either triggers an event (such as charlie and bravo’s modification of hexes) or requests a data return of some kind.
Remote pokes are allowed (and common for single-instance requests).
++on-watch Subscription
A subscription is a data-reactive standing request for changes. For instance, one can watch a database agent for any changes to the database. Whenever a change occurs, the agent notifies all subscribers, who then act as they should in the event of a message being received (e.g. from a particular ship).
Remote subscriptions are in common use.
The agent
%charlie is yet another upgrade of %bravo which allows remote ships to poke each other peer-to-peer and push hex values to or pop hex values from each others’ hexes:
/app/charlie.hoon:
/- charlie
/+ default-agent, dbug
|%
+$ versioned-state
$% state-0
==
::
+$ state-0
$: [%0 hexes=(list @ux)]
==
::
+$ card card:agent:gall
::
--
%- agent:dbug
=| state-0
=* state -
^- agent:gall
=<
|_ =bowl:gall
+* this .
default ~(. (default-agent this %|) bowl)
main ~(. +> bowl)
::
++ on-init
^- (quip card _this)
~& > '%charlie initialized successfully'
=. state [%0 *(list @ux)]
`this
++ on-save on-save:default
++ on-load on-load:default
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?+ mark (on-poke:default mark vase)
%noun
?+ q.vase (on-poke:default mark vase)
%print-state
~& >> state
~& >>> bowl
`this
::
[%push-local @ux]
~& > "got poked from {<src.bowl>} with val: {<+.q.vase>}"
=^ cards state
(handle-action:main ;;(action:charlie q.vase))
[cards this]
::
[%pop-local ~]
~& > "got poked from {<src.bowl>} with val: {<+.q.vase>}"
=^ cards state
(handle-action:main ;;(action:charlie q.vase))
[cards this]
==
::
%charlie-action
~& > %charlie-action
=^ cards state
(handle-action:main !<(action:charlie vase))
[cards this]
==
++ on-arvo on-arvo:default
++ on-watch on-watch:default
++ on-leave on-leave:default
++ on-peek
|= =path
^- (unit (unit cage))
?+ path (on-peek:default path)
[%x %hexes ~]
``noun+!>(hexes)
==
++ on-agent on-agent:default
++ on-fail on-fail:default
--
|_ =bowl:gall
++ handle-action
|= =action:charlie
^- (quip card _state)
?- -.action
::
%push-remote
:_ state
~[[%pass /poke-wire %agent [target.action %charlie] %poke %noun !>([%push-local value.action])]]
::
%push-local
=. hexes.state (weld hexes.state ~[value.action])
~& >> hexes.state
:_ state
~[[%give %fact ~[/hexes] [%atom !>(hexes.state)]]]
::
%pop-remote
:_ state
~[[%pass /poke-wire %agent [target.action %charlie] %poke %noun !>(~[%pop-local])]]
::
%pop-local
=. hexes.state (snip hexes.state)
~& >> hexes.state
:_ state
~[[%give %fact ~[/hexes] [%atom !>(hexes.state)]]]
==
--
At this point, if you are running a fakezod then the fakezods must be able to see each other over the local network. Typically this means running two different fakezods on the same host machine. Alternatively, you can spin up a comet or moon and do this with your teammates. We have no filtering for agent permissions here. This will be critical for real-world deployments.
A %charlie agent needs to know how to do two things: receive a push (with data) and receive a pop (here, functionally, a delete rather than a return).
/sur/charlie.hoon
|%
+$ action
$% [%push-remote target=@p value=@ux]
[%push-local value=@ux]
[%pop-remote target=@p]
[%pop-local ~]
==
--
/mar/charlie/action.hoon
/- charlie
|_ =action:charlie
++ grab
|%
++ noun action:charlie
--
++ grow
|%
++ noun action
--
++ grad %noun
--
The actions:
:charlie &charlie-action [%push-remote ~sampel-palnet 0xbeef]
:charlie &charlie-action [%push-local 0xbeef]
:charlie &charlie-action [%pop-remote ~sampel-palnet]
:charlie &charlie-action pop-local+~
Graph Store and Permissions on Mars (Optional)
Many Gall apps use Graph Store, a backend data storage format and database that both provides internally consistent data and external API communications endpoints.
To understand Graph Store, think in terms of the Urbit data permissions model:
- A store is a local database.
- A hook is a permissions broker for the database. They request and return information after negotiating access with remote agents.
- A view is a data aggregator which parses JSON objects for external clients such as Landscape.
Graph Store handles data access perms at the hook level, not at the store level.
References
Key Points
A
%gallapp can talk to a user interface client.