`%gall`: Interfacing with a Client
Overview
Teaching: 60 min
Exercises: 30 minQuestions
How can I build a
%gall
app which operates on data?Objectives
Produce an intermediate
%gall
app.Understand how
%gall
interfaces 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
%gall
app can talk to a user interface client.