mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-21 05:41:43 +03:00
285 lines
11 KiB
Markdown
285 lines
11 KiB
Markdown
|
This guide is focussed on storing application state using the `%gall`
|
||
|
vane. To show off how we store and distribute data in Urbit we're going
|
||
|
to examine a simple webapp. Some of the material here expects that you
|
||
|
have looked over the [`%ford` guide](). If you haven't, it's a good idea
|
||
|
to start there. There's also more information in the [`%gall`
|
||
|
overview]() and [`%gall` commentary]() but it's not necesarry that you
|
||
|
read those before going forward.
|
||
|
|
||
|
One important thing to keep in mind is that `%gall` services aren't
|
||
|
'started' or 'stopped' as in a unix system. When your files are copied
|
||
|
in they are compiled and begin running immediately and permanently.
|
||
|
`%gall` services simply wake up when certain events happen.
|
||
|
|
||
|
If you need to make updates to the structure of your stored data, you
|
||
|
write connector functions or (when developing) just throw your existing
|
||
|
data away. We'll see examples of how this works, just keep in mind that
|
||
|
when we talk about a `%gall` 'service' it has no concept of 'running'.
|
||
|
|
||
|
Going forward we'll refer to the directory you cloned the repo in as
|
||
|
`/$URB_DIR` and assume that your pier is listening for HTTP connections
|
||
|
on port `8080`.
|
||
|
|
||
|
1.
|
||
|
|
||
|
Get the code.
|
||
|
|
||
|
Clone the GitHub repository and move the files into your `/main` desk,
|
||
|
under the corresponding paths. You will need four files:
|
||
|
|
||
|
- /main/app/lead/core.hook
|
||
|
- /main/pub/lead/hymn.hook
|
||
|
- /main/pub/lead/src/main.css
|
||
|
- /main/pub/lead/src/main.js
|
||
|
|
||
|
When everything is in place, try it:
|
||
|
|
||
|
http://localhost:8080/gen/main/pub/lead/
|
||
|
|
||
|
That URL should render a page and be self explanatory. Try adding names
|
||
|
to the leaderboard and incrementing their scores. It's also fun to open
|
||
|
a few tabs and watch how the state gets updated simultaneously.
|
||
|
|
||
|
2.
|
||
|
|
||
|
How is the code structured?
|
||
|
|
||
|
In our `%ford` guide we generated pages by defining all of their
|
||
|
possible states, but we didn't exactly store any data. When building
|
||
|
applications on top of Urbit we think of them as existing in two natural
|
||
|
parts: page resources and state services. Effectively, we think of any
|
||
|
Urbit app talking to the web as a single page app whose resources are
|
||
|
generated by `%ford` which talks to a `%gall` service if it needs to
|
||
|
persist any state. Let's look more closely at the specifics in this
|
||
|
simple app.
|
||
|
|
||
|
When we load our page, we render the contents of our
|
||
|
`/main/pub/lead/hymn.hook`. This file should look familiar as
|
||
|
[`++sail`](). Our `hymn.hook` file writes the basic HTML elements to the
|
||
|
page, and pulls in our supporting CSS and JavaScript resources.
|
||
|
|
||
|
Our application-specific resources are stored in `/main/pub/lead/src/`.
|
||
|
`/main/pub/lead/src/main.css` simply produces the page layout, while
|
||
|
`/main/pub/lead/src/main.js` updates the page and sends data.
|
||
|
|
||
|
We also use two utility scripts: `/gop/hart.js` and
|
||
|
`/gen/main/lib/urb.js`. These are the standard libraries for handling
|
||
|
data transfer from a browser to Urbit, and are very frequently used.
|
||
|
`hart.js` handles the page heartbeat, making regular AJAX requests so we
|
||
|
can keep track of subscribers, and `urb.js` offers a more complete set
|
||
|
of helper functions. `urb.js` depends on `hart.js`, so that's why
|
||
|
`hart.js` always goes in the `<head>`. For complete documentation, check
|
||
|
out the [`urb.js` reference]().
|
||
|
|
||
|
Our application state is stored and distributed to connected clients by
|
||
|
`/main/app/lead/core.hook`. Let's take a closer look at how that works.
|
||
|
|
||
|
At the top of our `core.hook` we have:
|
||
|
|
||
|
/? 314 :: need urbit 314
|
||
|
|
||
|
This should be familiar from the `%ford` guide. Here we're requiring
|
||
|
that this code run on an Urbit ship where `(lte zuse 314)` is `yes`. In
|
||
|
this `core.hook` we only use one `%ford` rune, but this is where we
|
||
|
would also pull in any dependencies we might have or use other [`/`
|
||
|
runes]().
|
||
|
|
||
|
Below our `/?` you can see that our code is divided into two sections: a
|
||
|
[`|%`]() where we define our models, and a [`|_`]() where we define the
|
||
|
body of our program. We'll look at these more closely one at a time.
|
||
|
|
||
|
3.
|
||
|
|
||
|
How is our state stored?
|
||
|
|
||
|
In `/main/app/lead/core.hook`:
|
||
|
|
||
|
++ axle
|
||
|
$% [%0 p=(map ,@t ,@ud)]
|
||
|
==
|
||
|
|
||
|
is the first arm inside our leading `|%` that's important to notice.
|
||
|
`++axle` defines the tile for our state. By convention we store our
|
||
|
state as a [`$%`](), or labelled cases. We assume that our state can be
|
||
|
versioned, so we want its model to be one of many tagged cases. This
|
||
|
makes it possible to migrate our state to a new version of the service.
|
||
|
Since this is the first version of our app, we tag our state with `%0`.
|
||
|
|
||
|
In this simple application we're keeping track of pairs of names to
|
||
|
scores, and we define that here as `(map ,@t ,@ud)`. You can think of
|
||
|
this kind of like an associative array of strings to numbers, or an
|
||
|
object with string keys and numeric values.
|
||
|
|
||
|
When we use `++axle` to define the type of our state it's kind of like
|
||
|
declaring a schema definition. There's no secondary data storage layer.
|
||
|
Since `%gall` services run permanently your data persists as normal
|
||
|
application state. We use tiles the way we normally would to declare the
|
||
|
type of data that we're passing around.
|
||
|
|
||
|
Looking ahead, you can see that our main `|_` takes a `++axle` as part
|
||
|
of its sample. Let's look at how that core actually works, to get a
|
||
|
sense of what our application is doing.
|
||
|
|
||
|
4.
|
||
|
|
||
|
Where do requests go?
|
||
|
|
||
|
In `/main/app/lead/core.hook`:
|
||
|
|
||
|
++ peer
|
||
|
|= [ost=bone you=ship pax=path]
|
||
|
^- [(list move) _+>]
|
||
|
?~ pax
|
||
|
[[ost %give %rust %json vat-json]~ +>.$]
|
||
|
:_ +>.$
|
||
|
:_ ~
|
||
|
?+ -.pax
|
||
|
=- [ost %give %mean -]
|
||
|
`[%not-found [%leaf "you need to specify a path"]~]
|
||
|
%data
|
||
|
=- [ost %give %rush %json -]
|
||
|
(joba %conn %b &)
|
||
|
==
|
||
|
|
||
|
is the most important arm to look at first. `++peer` is one of the
|
||
|
predefined arms that `%gall` calls when certain events happen. You can
|
||
|
find them all in the [`%gall` overview]().
|
||
|
|
||
|
We 'get a `++peer`' when we get either a HTTP request, or a subscription
|
||
|
request. Each time this happens our main `|_` is populated with a
|
||
|
`++hide` and our current `++axle` in its sample and `++peer` gets passed
|
||
|
three things: `[ost=bone you=ship pax=path]`. The sample in the `|_`
|
||
|
that contains `++peer` is our application state and all of the contained
|
||
|
arms have access to that sample as part of their context. To change the
|
||
|
state we simply produce a new context with changed values.
|
||
|
|
||
|
Let's look at each of these parts of our context and the sample in
|
||
|
`++peer`.
|
||
|
|
||
|
`++hide`, labelled `hid` in peer's context, gives us some information
|
||
|
about the `++request` being passed in. You can look at the specifics in
|
||
|
the [`%arvo` `++models`](), but for our purposes we can think of it
|
||
|
simply as request metadata.
|
||
|
|
||
|
`++axle`, labelled as `vat` in peer's context, should be familiar from
|
||
|
the discussion in the previous step.
|
||
|
|
||
|
`ost` is a `++bone`, or an identifier for an `%arvo` duct. 'Duct' is
|
||
|
actually a pretty good word for what a ++duct does. Informally, when an
|
||
|
event is processed in `%arvo` we patch together our requisite
|
||
|
computations with `++ducts`. For example, when we get a network packet,
|
||
|
parse it, pass it to the webserver, and then pass it to the application
|
||
|
framework we use a `++duct` to make all those connections. In `++peer`
|
||
|
our ost just identifies the incoming request by number. We don't have
|
||
|
access to the connecting `++duct`, but we use `ost` in the effects we
|
||
|
produce so our responses are correctly coupled to the incoming request.
|
||
|
|
||
|
`you` is a `++ship`, which is just a [`@p`]() or a phonemic string like
|
||
|
`~tasfyn- partyv`. `%eyre` does some work to figure out who this is, or
|
||
|
uses a submarine name if it can't be determined. You can read more about
|
||
|
how we parse identities in `%eyre` in the [`%eyre` reference]().
|
||
|
|
||
|
`pax` is a `++path`, or a list of `@ta`. In Hoon we most often write
|
||
|
paths as you would expect, `/something/like/this`. In `%gall` services
|
||
|
requests come in on a specific path, like `/data` or `/`.
|
||
|
|
||
|
`++peer`, as with any arm that handles events, must produce a pair of a
|
||
|
`(list ++move)` and our context, with any intended changes. In this peer
|
||
|
we handle two cases, when `pax` is empty, or `~`, when our `pax` is
|
||
|
`/data`. We throw an error if `pax` is anything else.
|
||
|
|
||
|
5.
|
||
|
|
||
|
What exactly is a list of moves?
|
||
|
|
||
|
Try pointing your browser at:
|
||
|
|
||
|
http://localhost:8082/lead/
|
||
|
|
||
|
to see our response when `pax` in `++peer` is `~`. In our case we use
|
||
|
this URL to load the initial state of the application as JSON. This is
|
||
|
produced by the line `[[ost %give %rust %json vat-json]~ +>.$]` which
|
||
|
produces a single `++move`, and our local context. Let's look more
|
||
|
closely at our `++move`.
|
||
|
|
||
|
++ move ,[p=bone q=[%give gift]] :: output operation
|
||
|
|
||
|
From our prior discussion we're familiar with a `++bone`, and `++gift`
|
||
|
is defined right above in `core.hook`:
|
||
|
|
||
|
++ gift :: output action
|
||
|
$% [%rust gilt] :: total update
|
||
|
[%rush gilt] :: partial update
|
||
|
[%mean (unit (pair term (list tank)))] :: Error, maybe w/ msg
|
||
|
[%nice ~] :: Response message
|
||
|
==
|
||
|
::
|
||
|
|
||
|
Which clearly depends on `++gilt`:
|
||
|
|
||
|
++ gilt :: subscription frame
|
||
|
$% [%hymn p=manx] :: html tree
|
||
|
[%json p=json] :: json
|
||
|
==
|
||
|
::
|
||
|
|
||
|
`++gift` defines the possible actions we can take in the moves that we
|
||
|
produce. We can send either partial or total updates with `%rush` or
|
||
|
`%rust` respectively. We can also send either an error, `%mean` or
|
||
|
default acknowledgement, `%nice`.
|
||
|
|
||
|
Returning to our original `++move`, `[ost %give %rust %json vat-json]`
|
||
|
we can now read it as 'send a total update with `++vat-json` as
|
||
|
`++json`'. `++vat-json` simply takes our `(map @t @ud)` and turns it in
|
||
|
to JSON.
|
||
|
|
||
|
Looking at the remainer of `++peer` we can see that it is mostly
|
||
|
control-flow that produces a `%mean` if our `pax` is not matched, and a
|
||
|
`%rush` if our `pax` is `%data`. We'll revisit this `%data` path later
|
||
|
on.
|
||
|
|
||
|
5.
|
||
|
|
||
|
How do we change our state?
|
||
|
|
||
|
All of our state changes happen in `++poke-json`. Incoming messages are
|
||
|
handled by `++poke` arms in `%gall` services. If an incoming message has
|
||
|
a `%logo` it is appeneded after a `-`. Messages from the web are often
|
||
|
sent as JSON, so `++poke- json` is common for services that face the
|
||
|
web.
|
||
|
|
||
|
Let's walk through this part:
|
||
|
|
||
|
=. p.vat
|
||
|
(~(put by p.vat) newl)
|
||
|
:_ +>.$
|
||
|
:* [ost %give %nice ~]
|
||
|
(deliver %upd-lead (joba -.newl [%n (scot %ud +.newl)]))
|
||
|
==
|
||
|
|
||
|
Using [`=.`]() we update the value of `p.vat` in our context using
|
||
|
[`put:by`](), one of our map container functions. Then, we produce
|
||
|
`+>.$` as our context. Since we have changed the value of `p.vat` within
|
||
|
our immediate context, `$`, this is equivalient to updating the state of
|
||
|
our service. Changing a value in your context and producing it is all
|
||
|
you need to do to update your permanent state. That's one of the main
|
||
|
goals of `%gall`, to be a single-level store.
|
||
|
|
||
|
So, how did we get to this point in `++poke-json`?
|
||
|
|
||
|
=+ ^= jop
|
||
|
^- kiss
|
||
|
%- need %. jon
|
||
|
=> jo %- of
|
||
|
:~ [%new-lead so]
|
||
|
[%add-lead so]
|
||
|
==
|
||
|
|
||
|
6.
|
||
|
|
||
|
`++deliver`
|
||
|
|
||
|
7.
|
||
|
|
||
|
main.js
|