Once you register an app with Gall using |start, Gall calls specific arms of the app when the app changes.
In this lesson, you'll see everything about how Gall handles compilation, state, resource initialization, and app upgrades. The main action here will take place in the on-init, on-save, and on-load arms.
We'll also get our sneak peak at calling out to Arvo for networking services.
We will modify the below code throughout the lesson. Paste it into a file in app/ called lifecycle.hoon.
/+ default-agen, dbug
|%
+$ versioned-state
$% state-0
==
::
+$ state-0 [%0 val=@]
::
+$ card card:agent:gall
::
--
%- agent:dbug
=| state-0
=* state -
^- agent:gall
|_ =bowl:gall
+* this .
default ~(. (default-agent this %|) bowl)
::
++ on-init
^- (quip card _this)
~& > 'on-init'
~& >>> '%connect Eyre to ~lifecycle'
:_ this(state [%0 99])
:~
[%pass /bind %arvo %e %connect [~ /'~lifecycle'] %lifecycle]
==
++ on-save
~& > 'on-save v0'
!>(state)
++ on-load
|= old-state=vase
^- (quip card _this)
~& > 'on-load v0'
=/ prev !<(versioned-state old-state)
?- -.prev
%0
~& >>> '%0'
`this(state prev)
::
==
++ on-poke on-poke:default
++ on-watch on-watch:default
++ on-leave on-leave:default
++ on-peek on-peek:default
++ on-agent on-agent:default
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?+ wire (on-arvo:default wire sign-arvo)
[%bind ~]
~& >> 'Eyre confirmed the %connect'
`this
==
++ on-fail on-fail:default
--
Near the top of the file, you'll see the line %- agent:dbug. This wraps our program in the dbug Gall agent, which lets us inspect the state of the program as it's running. Once our agent is started, we can type at the Dojo:
> :lifecycle +dbug
and we'll see the current state object printed out. We can also do:
> :lifecycle +dbug %bowl
and we'll see the bowl (Gall metastate for the app) printed. We'll use this later in the lesson.
Before we start, just note that we are using ~& to print messages in on-init, on-save, and on-load. (You can use up to 3 >s with ~& to print the debug messages in different colors).
We have some bookkeeping code in on-poke and on-arvo that you can ignore for now.
Let's register the above application with Gall:
:: commit the new code
> |commit %home
+ /~zod/home/2/app/lifecycle/hoon
:: register the app with Gall
> |start %lifecycle
>=
activated app home/lifecycle
%path: no matches for /~zod/home/~2020.6.11..15.15.55..b397/lib/default-agen/hoon
%plan failed:
ford: %core on /~zod/home/0/app/lifecycle/hoon failed:
What happened? This message is telling us that it can't find something in lib: default-agen/hoon. This is because we spelled the name wrong. We'll learn about libraries in the next lesson. For now, check the very first line of our program: we need to change it to:
/+ default-agent, dbug
Now commit, and you'll see the following:
> |commit %home
>=
: /~zod/home/3/app/lifecycle/hoon
> 'on-init'
>>> '%connect Eyre to ~lifecycle'
>> 'Eyre confirmed the %connect'
[unlinked from [p=~zod q=%lifecycle]]
Notice that on-init runs on the first successful compilation.
When Gall registers an app with |start, it tries to compile it. If the compilation succeeds, it runs on-init. If it fails, it keeps recompiling each time the code is updated with |commit. Once compilation succeeds, it runs on-init.
After that first time, it never runs on-init ever again for that app; it only uses on-save and on-load after app recompilations, as we will soon see.
Let's look at the code of our on-init arm:
++ on-init
^- (quip card _this)
~& > 'on-init'
~& >>> '%connect Eyre to ~lifecycle'
:_ this(state [%0 99])
:~
[%pass /bind %arvo %e %connect [~ /'~lifecycle'] %lifecycle]
==
We see that it returns a (quip card _this). As mentioned in the last lesson, this type means a list of card, along with _this_ at the end (short for $_(this): the type of this, our core).
Gall arms that return this structure are passing 0 or more actions (cards) back to Gall to perform, and also return a new state of our app to Gall. You can see more detail on the structure of cards in the types appendix--this one is an arvo-note. It starts with %pass, which means it's like a function call. The %e is for "Eyre", the Arvo networking vane. This is a command to listen for incoming HTTP requests on address /~lifecycle of our ship. We'll explore this more in the HTTP lesson.
Take it as a given for now that this HTTP card works. What about our new state?
We use :_ to put the end of our return tuple first and put the heaviest code at the bottom (the card). So this(state [%0 99]) is the value of our app that we return. Let's use +dbug to print the state
> :lifecycle +dbug
You should see [%0 val=99] as the result. (our on-save debug print also prints, since dbug calls it).
If we wanted to initialize with more than one card, we would put more elements in the list created with :~. If we wanted 0 elements, we could do either of the following, which are equivalent and both put ~ at the front of our tuple:
:: Syntax 1
[~ this(state [%0 99])]
:: Syntax 2
`this(state [%0 99])
OK, so we know how to initialize our app with 0 or more actions and return an initial state for it. Let's see what happens when we make changes to the state.
Make the following changes to your code:
| :: old | :: new |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Now commit, and you will see something like this:
> |commit %home
>=
: /~zod/home/7/app/lifecycle/hoon
> 'on-save v0'
> 'on-load v1'
>>> '%0'
And let's also check the current state:
> :lifecycle +dbug
[%1 val=100 msg='my message']
Notice that while our state updated as expected, we can see from the debug prints that Gall called the "v0" version of on-save, and the "v1" version of on-load. Why?
Because after each successful compilation, Gall calls:
on-saveof the prior version of the appon-loadof the new version of the app
This allows Gall to move the data from a working prior version to our new version.
We also change our on-init, so that users installing the app from scratch end up in a consistent state with those who upgrade via on-load.
Let's say we want to undo the Eyre binding that we did in on-init. How is that possible, given that on-init only runs once?
The answer is that on-load also returns (quip card this), so we can return cards instead of altering state as part of the transition to a new state.
To illustrate this, make the following modifications to the code:
| :: old | :: new |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Our new state-2 is the same as state-1--we're just going to add an action in the transition from %1 to %2.
The %1 case now returns a card that disconnects our app from Eyre (no more listening for incoming traffic at a URL), and updates its head to %2. We also alter the %0 case to do both the state update and the card update, and bump our version all the way to %2, in case a user of our app missed the %1 update.
We update on-init to remove our Eyre call, since new installs shouldn't connect at all now.
Let's commit and see the result of recompiling:
> |commit %home
>=
: /~zod/home/20/app/lifecycle/hoon
> 'on-save v1'
> 'on-load'
>>> '%1'
> :lifecycle +dbug
[%2 val=100 msg='my message']
You can play around and change some of the print messages to force recompilation. However, the state will remain the same, because when we're in state %2, it sismply sets the current state to the previous (i.e no change).
In addition to any state that we put inside our agent, we also have access to some state that Gall keeps track of and injects whenever it calls an arm in our agent. That state is of type bowl:gall.
Near the top of our lifecycle.hoon file, we have:
^- agent:gall
|_ =bowl:gall
+* this .
default ~(. (default-agent this %|) bowl)
The key here is |_ =bowl:gall, which says that our agent is a door that takes a parameter of type bowl:gall called bowl. We also pass that to default-agent when we initialize that core, and if you use a helper core, you'd pass it also (we'll see this is in later lessons).
You can find the type definition of bowl in /sys/zuze.hoon if you search for ++ bowl. For now, just know that it contains our ship name, the name of the ship that made the current call to us, the name of our agent, the current time, entropy, and data about any subscriptions our agent has made from or to it.
We can print bowl for this app by entering at the Dojo:
> :lifecycle +dbug %bowl
:: you should see something like:
>> [ [our=~zod src=~zod dap=%lifecycle]
[wex={} sup={}]
act=4
eny
\/0v3mh.pdbr4.15e5c.qlakl.ob41u.2f64d.3gauk.r3pm0.q6pv1.h76h0.98kpl.5godq.tvu1u.tdfs7.rjju9.j6vij.m3vmo.78pmb.8dfbd.\/
07nlt.58v8l
\/ \/
now=~2020.6.18..14.50.18..d5f3
byk=[p=~zod q=%home r=[%da p=~2020.6.18..14.50.06..5bec]]
]
on-save is mostly used by Gall itself to transition between state versions. However, it's also used by dbug to get the current state of the app wrapped by dbug. This is possible because dbug takes an agent:gall itself as an argument, so it has access to calling all of that agent's arms.
We're now ready to explicitly state Gall's compiliation lifecycle.
- Register an app.
- Compile it every time its code changes.
- Run
on-initthe first time it compiles successfully, and never again after that. - For all other successful compilations, call the previous version of
on-saveand pass its output to the current version ofon-load.
Remember, the point of on-init is to get fresh installs of your app into the correct state with the correct cards passed out.
State transitions can be either actions passed to the system or updates to app state.
Because on-load is executed after every successful recompilation, it's useful to put a debug print in on-load like 'app recompiled successfully', so that you can quickly see that things have worked. You can just add a space in that text and re-commit if you want to force re-compilation.
If you want to iterate until you get your on-init right, I recommend using the "Faster Fakeship Startup" method from the workflow lesson.
- Find 2 Gall agents in
appthat return cards as part of theiron-load.
- Explain what you think the cards are doing
- Is the action the cards are doing repeated in
on-init? Why or why not? (The answer could vary from program to program).
- Refer to this version of file-server.hoon
- Where are its
versioned-stateandstate-0defined? - What is the advantage of this approach?
- What does the new version of the state (
state-1) enable thatstate-0did not? (The transition inon-loadgives some hints here)
- Modify
app/chat-store.hoonin a fake ship to have a new state,state-4. This state should include theinboxfrom the prior state, but add a(set ship)with facebad-people. - Get it to work in
on-load, and initializebad-peopleto contain the ship~timluc-miptev. - Update
on-initto have~timluc-miptevin its initialbad-peopleif the app is installed from scratch.
List the types of cards that you think your chat admin app will need to pass in its on-init, either to Arvo or other agents. You should refer to your feature list from the prior lesson's exercises.
Prev: The 10 Arms of Gaal: App Structure | Home | Next: Importing Code and Static Resources