Merge pull request #18 from urbit/duo-launch

DUO launch PR
This commit is contained in:
matildepark 2022-06-29 16:09:00 -07:00 committed by GitHub
commit b80d5ab9b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
438 changed files with 120859 additions and 20259 deletions

View File

@ -1,17 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: false,
node: true,
},
extends: ["plugin:react/recommended", "standard", "prettier"],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 8,
sourceType: "module",
},
plugins: ["react"],
rules: { "react/prop-types": 0 },
};

13
.gitignore vendored
View File

@ -1,19 +1,14 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
.next
# production
/build
/cache
# misc
.DS_Store
@ -25,6 +20,7 @@ yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local
@ -32,3 +28,6 @@ yarn-error.log*
# vercel
.vercel
# editors
/.nova

View File

@ -1,10 +1,34 @@
# Urbit Developers Portal
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
Uses [Next.js](https://nextjs.org/docs) with [remark](https://unifiedjs.com/explore/package/remark/) and [Tailwind CSS](https://tailwindcss.com/docs)
## Getting Started
To develop locally:
First, run the development server:
```
yarn install
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

50
components/BasicPage.js Normal file
View File

@ -0,0 +1,50 @@
import { useRouter } from "next/router";
import Head from "next/head";
import Meta from "./Meta";
import ErrorPage from "../pages/404";
import {
Container,
SingleColumn,
Section,
Markdown,
TableOfContents,
} from "foundation-design-system";
import Header from "./Header";
import Footer from "./Footer";
import classNames from "classnames";
export default function BasicPage({
post,
markdown,
wide = false,
search,
index = false,
}) {
const router = useRouter();
if (!router.isFallback && !post?.slug) {
return <ErrorPage />;
}
return (
<Container>
<Head>
<title>{post.title} developers.urbit.org</title>
{Meta(post)}
</Head>
<Header search={search} />
<SingleColumn>
<Section narrow={!wide}>
<h1>{post.title}</h1>
</Section>
<Section narrow={!wide}>
<div className={classNames("flex", { sidebar: index })}>
<div className={classNames("markdown", { "max-w-prose": index })}>
<Markdown.render content={JSON.parse(markdown)} />
</div>
{index && <TableOfContents />}
</div>
</Section>
</SingleColumn>
<Footer />
</Container>
);
}

44
components/BlogPreview.js Normal file
View File

@ -0,0 +1,44 @@
import { BackgroundImage } from "foundation-design-system";
import Link from "next/link";
import { generateDisplayDate, formatDate } from "../lib/lib";
export default function BlogPreview({ post }) {
const date = generateDisplayDate(post.date);
return (
<div key={post.slug} className="mb-20 cursor-pointer">
<Link href={`/blog/${post.slug}`}>
<div>
{
// Not all blog posts have images
post.extra.image ? (
<BackgroundImage
src={post.extra.image}
className="rounded-lg aspect-w-5 aspect-h-4"
/>
) : null
}
<h3 className="mt-10">{post.title}</h3>
{post?.description && <p className="mt-3">{post.description}</p>}
<div className="flex items-center justify-between mt-4">
<div className="flex items-baseline">
{post.extra.author ? (
<div className="type-sub-bold mr-2">{post.extra.author}</div>
) : null}
{post.extra.ship ? (
<Link
href={`https://urbit.org/ids/${post.extra.ship}`}
passHref
>
<a className="type-sub-bold text-wall-500 font-mono">
{post.extra.ship}
</a>
</Link>
) : null}
</div>
<div className="text-wall-500 type-sub">{formatDate(date)}</div>
</div>
</div>
</Link>
</div>
);
}

48
components/Card.js Normal file
View File

@ -0,0 +1,48 @@
import Link from "next/link";
import classNames from "classnames";
export default function Card({
href = "/",
icon = null,
title,
text,
callout = "",
className = "",
disableArrow = false,
}) {
return callout ? (
<div
id={callout === "" && !disableArrow ? "card" : ""}
className={classNames(
"bg-wall-100 rounded-xl p-7 items-stretch relative flex",
{ "space-x-4": icon },
className
)}
>
{icon}
<div className="flex flex-col space-y-4 justify-between pr-4">
<h3 className="font-bold">{title}</h3>
<p>{text}</p>
<Link href={href} passHref>
<a className="button-sm bg-green-400 text-white w-fit">{callout}</a>
</Link>
</div>
</div>
) : (
<Link href={href}>
<div
id={callout === "" && !disableArrow ? "card" : ""}
className={
"bg-wall-100 rounded-xl flex space-x-4 p-7 items-center relative cursor-pointer " +
className
}
>
{icon}
<div className="flex flex-col pr-4 basis-2/3">
<p className="font-bold">{title}</p>
<p className="text-sm">{text}</p>
</div>
</div>
</Link>
);
}

View File

@ -1,9 +0,0 @@
import React from "react";
export default function Container({ children }) {
return (
<div className="flex flex-col min-h-screen w-full items-center">
{children}
</div>
);
}

86
components/ContentArea.js Normal file
View File

@ -0,0 +1,86 @@
import { useState, useEffect, useRef } from "react";
import { useRouter } from "next/router";
import { TableOfContents } from "foundation-design-system";
export default function ContentArea(props) {
const [shortcut, setShortcut] = useState("");
const detectOS = () => {
const agent = window.navigator.appVersion;
if (agent.includes("Win")) {
return "Ctrl+K";
} else if (agent.includes("Mac")) {
return "⌘K";
} else if (agent.includes("Linux")) {
return "Ctrl+K";
}
};
useEffect(() => {
setShortcut(detectOS());
}, []);
const scrollBox = useRef();
const router = useRouter();
useEffect(() => {
const handleRouteChange = () => {
if (scrollBox.current === null) return;
scrollBox.current.scrollTop = 0;
};
router.events.on("routeChangeComplete", handleRouteChange);
// If the component is unmounted, unsubscribe
// from the event with the `off` method:
return () => {
router.events.off("routeChangeStart", handleRouteChange);
};
}, []);
return (
<div className="w-full min-w-0 flex flex-col items-center">
<div
ref={scrollBox}
className="px-4 md:px-12 lg:px-24 pt-8 md:pt-10 lg:pt-16 flex flex-col w-full max-w-screen-xl max-h-screen h-screen overflow-y-scroll"
>
<div className="flex justify-between w-full items-center shrink-0">
<div className="font-semibold antialiased text-lg text-black">
{props.breadcrumbs}
</div>
<div className="hidden md:block">
<button
onClick={(e) => {
e.stopPropagation();
props.search.toggleSearch();
}}
className="bg-wall-100 text-wall-500 flex px-3 py-2 rounded-lg font-semibold text-lg"
>
Search<div className="ml-4 text-wall-400">{shortcut}</div>
</button>
</div>
</div>
<div className="flex justify-between">
<div className="min-w-0 max-w-screen-md">
<div className="mb-16">
<h2 className="mt-24">{props.title}</h2>
{props?.description && (
<p className="text-wall-400 font-bold text-xl">
{props.description}
</p>
)}
</div>
{props.children}
<div className="pb-24" />
</div>
{!props?.disableToC && (
<TableOfContents
key={props.params.slug?.join("/") || Math.random()}
/>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,88 @@
import Link from "next/link";
import { DateTime } from "luxon";
import { DateRange } from "../components/Snippets";
import { generateDisplayDate, generateRealtimeDate } from "../lib/lib";
import classNames from "classnames";
export default function EventPreview({ event, className, big }) {
// Event tiles have a 'dark mode' used when their background images are dark and white text is needed for legibility.
const grayText = event?.dark ? "text-washedWhite" : "text-wall-400";
const blackText = event?.dark ? "text-white" : "text-wall-600";
const starts = generateDisplayDate(event.starts, event.timezone);
const ends = generateDisplayDate(event.ends, event.timezone);
const inFuture = generateRealtimeDate(starts) > DateTime.now();
const happeningNow =
generateRealtimeDate(event.starts) > DateTime.now() && !inFuture;
return (
<div
className={`cursor-pointer aspect-w-4 aspect-h-5 md:aspect-w-5 md:aspect-h-4 ${className}`}
>
<div
key={event.slug}
className={`bg-wall-100 rounded-xl bg-cover bg-center bg-no-repeat `}
style={{ backgroundImage: `url(${event.image})` || "" }}
>
<Link href={`/community/events/${event.slug}`}>
<div
className={classNames(
"flex flex-col p-6 justify-between items-between h-full relative",
{
"backdrop-brightness-50 rounded-xl": Boolean(
event?.darken_image
),
}
)}
>
<div
className={`grow-1 flex ${
big ? "justify-center" : ""
} flex-col h-full`}
>
<h3 className={`${blackText} mb-2`}>{event.title}</h3>
<p className={blackText + " truncate text-sm"}>
{event.description}
</p>
</div>
<div className="absolute p-6 left-0 bottom-0 w-full pr-32">
<p className={`${blackText} type-sub mb-1`}>{event.location}</p>
<DateRange
starts={starts}
ends={ends}
className={`${grayText} type-sub`}
/>
</div>
{inFuture && event.registration_url ? (
<div className="absolute right-0 bottom-0 p-6">
<a
className="button-sm bg-green-400 text-white"
href={event.registration_url}
onClick={(e) => e.stopPropagation()}
target="_blank"
>
RSVP
</a>
</div>
) : event.youtube ? (
<div className="absolute right-0 bottom-0 p-6">
<a
className="button-sm bg-wall-600 text-white"
href={`https://www.youtube.com/watch?v=${event.youtube}`}
onClick={(e) => e.stopPropagation()}
target="_blank"
>
Watch
</a>
</div>
) : null}
</div>
</Link>
</div>
</div>
);
}

View File

@ -1,163 +1,6 @@
import React from "react";
import Link from "next/link";
import SingleColumn from "./SingleColumn";
import Section from "./Section";
import { Footer as FooterComponent } from "foundation-design-system";
import { footerData } from "../lib/constants";
export default function Footer() {
return (
<footer className="bg-wall-100 mt-20 w-full flex justify-center">
<SingleColumn>
<Section short className="flex flex-row flex-wrap">
<div className="w-1/2 md:w-1/3 flex flex-col flex-shrink">
<h4 className="mt-16 mb-8">Use Urbit</h4>
<Link href="https://urbit.org/getting-started">
<div>
<a className="type-bold text-wall-500">Getting Started</a>
</div>
</Link>
<Link href="https://urbit.org/using">
<div>
<a className="type-bold text-wall-500">User&apos;s Manual</a>
</div>
</Link>
<Link href="https://github.com/urbit/port/releases">
<div>
<a className="type-bold text-wall-500">Urbit Client</a>
</div>
</Link>
<Link href="https://github.com/urbit/urbit/releases">
<div>
<a className="mt-2 type-bold text-wall-500">Urbit Binaries</a>
</div>
</Link>
<Link href="https://urbit.org/getting-started/planet/#hosting-providers">
<div>
<a className="mt-2 type-bold text-wall-500">
Hosting Providers
</a>
</div>
</Link>
</div>
<div className="w-1/2 md:w-1/3 flex flex-col flex-shrink">
<h4 className="mt-16 mb-8">About</h4>
<Link href="https://urbit.org/understanding-urbit">
<div>
<a className="type-bold text-wall-500">What&apos;s Urbit?</a>
</div>
</Link>
<Link href="https://urbit.org/understanding-urbit/urbit-id">
<div>
<a className="mt-2 type-bold text-wall-500">Urbit ID</a>
</div>
</Link>
<Link href="https://urbit.org/understanding-urbit/urbit-os">
<div>
<a className="mt-2 type-bold text-wall-500">Urbit OS</a>
</div>
</Link>
<Link href="https://urbit.org/faq">
<div>
<a className="mt-2 type-bold text-wall-500">FAQ</a>
</div>
</Link>
</div>
<div className="w-1/2 md:w-1/3 flex flex-col flex-shrink">
<h4 className="mt-16 mb-8">News</h4>
<Link href="https://urbit.org/blog">
<div>
<a className="type-bold text-wall-500">Blog</a>
</div>
</Link>
<Link href="https://urbit.org/events">
<div>
<a className="mt-2 type-bold text-wall-500">Events</a>
</div>
</Link>
<Link href="https://urbit.org/updates">
<div>
<a className="mt-2 type-bold text-wall-500">Updates</a>
</div>
</Link>
</div>
<div className="w-1/2 md:w-1/3 flex flex-col">
<h4 className="mt-16 mb-8">Develop</h4>
<Link href="https://urbit.org/docs">
<div>
<a className="type-bold text-wall-500">Documentation</a>
</div>
</Link>
<Link href="https://github.com/urbit">
<div>
<a className="mt-2 type-bold text-wall-500">Github</a>
</div>
</Link>
<Link href="https://github.com/urbit/awesome-urbit#http-apis-airlock">
<div>
<a className="mt-2 type-bold text-wall-500">Airlock APIs</a>
</div>
</Link>
</div>
<div className="w-1/2 md:w-1/3 flex flex-col">
<h4 className="mt-16 mb-8">Contribute</h4>
<Link href="https://github.com/urbit/urbit/issues">
<div>
<a className="type-bold text-wall-500">Issue Tracker</a>
</div>
</Link>
<Link href="https://urbit.org/grants">
<div>
<a className="mt-2 type-bold text-wall-500">Urbit Grants</a>
</div>
</Link>
</div>
<div className="w-1/2 md:w-1/3 flex flex-col">
<h4 className="mt-16 mb-8">Community</h4>
<Link href="https://groups.google.com/a/urbit.org/g/dev?pli=1">
<div>
<a className="type-bold text-wall-500">Dev Mailing List</a>
</div>
</Link>
<Link href="https://github.com/urbit/azimuth">
<div>
<a className="mt-2 type-bold text-wall-500">Governance</a>
</div>
</Link>
<Link href="https://twitter.com/urbit">
<div>
<a className="mt-2 type-bold text-wall-500">Twitter</a>
</div>
</Link>
</div>
</Section>
<Section className="flex flex-col md:flex-row">
<div className="md:w-1/3">
<Link href="https://urbit.org/privacy">
<div>
<a className="type-bold text-wall-500">Privacy Policy</a>
</div>
</Link>
</div>
<div className="md:w-1/3">
<Link href="https://urbit.org/terms-of-service">
<div>
<a className="type-bold text-wall-500">Terms of Service</a>
</div>
</Link>
</div>
<div className="md:w-1/3">
<div>
<a
href={"mailto:support@urbit.org"}
className="type-bold text-wall-500"
>
support@urbit.org
</a>
</div>
</div>
</Section>
</SingleColumn>
</footer>
);
return <FooterComponent data={footerData} />;
}

View File

@ -1,8 +1,10 @@
import React from "react";
import Link from "next/link";
import SingleColumn from "./SingleColumn";
import { useRouter } from "next/router";
import { useState } from "react";
import classnames from "classnames";
import MenuTray from "../components/MenuTray";
import { capitalize } from "../lib/lib";
import { IntraNav } from "foundation-design-system";
function ActiveLink({ children, href, className, currentPath }) {
const firstCrumb = currentPath.split("/")[1];
@ -13,54 +15,149 @@ function ActiveLink({ children, href, className, currentPath }) {
});
return (
<Link href={href}>
<Link href={href} passHref>
<a className={`${className} ${activeClassName}`}>{children}</a>
</Link>
);
}
export default function Header(props) {
const currentPath = useRouter().asPath;
return (
<div className="flex flex-col w-full items-center">
<SingleColumn>
<header className=" layout px-4 md:px-8 flex justify-between items-center pt-8 md:pt-10 lg:pt-12 pb-10 md:pb-12 lg:pb-24">
<Link href="/">
<a className="type-ui">Urbit Developers</a>
</Link>
export default function Header({ search }) {
const [isOpen, setTray] = useState(false);
<nav className="items-center hidden md:flex">
<ActiveLink
currentPath={currentPath}
className="mr-5 type-ui"
href="/learn"
>
Learn
</ActiveLink>
<ActiveLink
currentPath={currentPath}
className="mr-5 type-ui"
href="/community"
>
Community
</ActiveLink>
<ActiveLink
currentPath={currentPath}
className="mr-5 type-ui"
href="/opportunities"
>
Opportunities
</ActiveLink>
<ActiveLink
currentPath={currentPath}
className="text-green-400 type-ui button-text"
href="/why"
>
Why Urbit?
</ActiveLink>
</nav>
</header>
</SingleColumn>
</div>
const currentPath = useRouter().asPath;
const routeDepth = currentPath.split("/").length;
const firstCrumb = currentPath.split("/")[1];
return (
<>
{" "}
<IntraNav ourSite="https://developers.urbit.org" search={search} />
<header className="layout max-w-screen-lg px-4 md:px-8 flex justify-between items-end pt-8 md:pt-10 lg:pt-12 pb-10 md:pb-12 lg:pb-24">
<div>
<Link href="/" passHref>
<a className="text-lg font-semibold leading-3 mr-5">
<span className="">Urbit </span>Developers
</a>
</Link>
{routeDepth > 2 ? (
<Link href={`/${firstCrumb}`} passHref>
<a className="inline md:hidden type-ui text-wall-500 ml-2">
{capitalize(firstCrumb)}
</a>
</Link>
) : null}
</div>
{
// Large screen header
}
<nav className="items-center hidden md:flex">
<ActiveLink
currentPath={currentPath}
className="mr-5 type-ui"
href="/overview"
>
Overview
</ActiveLink>
<ActiveLink
currentPath={currentPath}
className="mr-5 type-ui"
href="/guides"
>
Guides
</ActiveLink>
<ActiveLink
currentPath={currentPath}
className="mr-5 type-ui"
href="/reference"
>
Reference
</ActiveLink>
<ActiveLink
currentPath={currentPath}
className="mr-5 type-ui button-text"
href="/courses"
>
Courses
</ActiveLink>
<ActiveLink
currentPath={currentPath}
className="mr-5 type-ui"
href="/community"
>
Community
</ActiveLink>
<ActiveLink
currentPath={currentPath}
className="type-ui"
href="/blog"
>
Blog
</ActiveLink>
</nav>
{
// Small screen header
}
<MenuTray isOpen={isOpen} setTray={setTray} search={search}>
<Link href="/" passHref>
<a className="font-semibold mb-4">Urbit Developers</a>
</Link>
<Link href="https://urbit.org" passHref>
<a className="mt-2">Urbit.org</a>
</Link>
<Link href="https://operators.urbit.org" passHref>
<a className="mt-2">Operators</a>
</Link>
<Link href="/" passHref>
<a className="font-semibold mt-2 mb-4">Developers</a>
</Link>
<hr className="border-wall-200" />
<ActiveLink
currentPath={currentPath}
className="mt-4 mr-5 mb-3 type-ui"
href="/overview"
>
Overview
</ActiveLink>
<ActiveLink
currentPath={currentPath}
className="mr-5 mb-3 type-ui"
href="/guides"
>
Guides
</ActiveLink>
<ActiveLink
currentPath={currentPath}
className="mr-5 mb-3 type-ui"
href="/reference"
>
Reference
</ActiveLink>
<ActiveLink
currentPath={currentPath}
className="mr-5 mb-3 type-ui button-text"
href="/courses"
>
Courses
</ActiveLink>
<ActiveLink
currentPath={currentPath}
className="mr-5 mb-3 type-ui"
href="/community"
>
Community
</ActiveLink>
<ActiveLink
currentPath={currentPath}
className="mr-5 type-ui"
href="/blog"
>
Blog
</ActiveLink>
</MenuTray>
</header>
</>
);
}

View File

@ -1,15 +0,0 @@
import React from "react";
import Meta from "./Meta";
import Header from "./Header";
import Footer from "./Footer";
export default function Layout({ children }) {
return (
<>
<Meta />
<Header />
<div className="">{children}</div>
<Footer />
</>
);
}

View File

@ -1,24 +0,0 @@
import remark from "remark";
import gfm from "remark-gfm";
import slug from "remark-slug";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeRaw from "rehype-raw";
const options = {
handlers: {},
};
export default async function Markdown(content) {
const result = await remark()
.use(remarkParse, options)
.use(gfm)
.use(slug)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeStringify)
.process(content);
return result.toString();
}

104
components/MenuTray.js Normal file
View File

@ -0,0 +1,104 @@
import classnames from "classnames";
import { useSwipeable } from "react-swipeable";
export default function MenuTray({ isOpen, setTray, search, children }) {
// Locks document scrolling when menu is open
if (typeof document !== "undefined") {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "visible";
}
}
const handlers = useSwipeable({
onSwipedLeft: (e) => setTray(false),
});
// Slides the tray in or out from the left
const trayClasses = classnames({
"tray-menu-open": isOpen,
"tray-menu-closed": !isOpen,
});
// Fades the background overlay in or out
const overlayClasses = classnames({
"tray-overlay-open": isOpen,
"tray-overlay-closed": !isOpen,
});
// Hides or shows the menu
const menuClasses = classnames({
"menu-open": isOpen,
"menu-closed": !isOpen,
});
return (
<>
<nav
{...handlers}
className={`z-10 w-screen h-screen top-0 left-0 fixed block md:hidden ${menuClasses}`}
>
<div
onClick={() => setTray(!isOpen)}
className={`bg-washedWhite w-screen h-screen ${overlayClasses}`}
/>
<div
className={`absolute bg-wall-100 h-screen top-0 left-0 tray-menu-width overflow-y-scroll mb-24 ${trayClasses}`}
>
<div
{...handlers}
className="flex flex-col px-4 md:px-8 pt-8 md:pt-10 lg:pt-12"
>
{children}
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
search.toggleSearch(e);
}}
className={`z-10 fixed px-4 items-center justify-center type-ui rounded-xl h-16 bg-white text-wall-500 left-4 right-4 bottom-4 mobile-search-button-width ${
isOpen ? "flex" : "hidden"
}`}
>
Search
</button>
</nav>
<button
onClick={() => setTray(!isOpen)}
className="z-10 fixed bottom-4 right-4 w-16 h-16 bg-wall-600 flex items-center justify-center rounded-full md:hidden"
>
{isOpen ? (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.39382 13.7045C-0.131273 14.2296 -0.131274 15.081 0.39382 15.6061C0.918913 16.1312 1.77026 16.1312 2.29535 15.6061L7.99999 9.90142L13.7047 15.6061C14.2297 16.1312 15.0811 16.1312 15.6062 15.6061C16.1313 15.081 16.1313 14.2296 15.6062 13.7046L9.90152 7.99989L15.6061 2.29535C16.1312 1.77026 16.1312 0.918913 15.6061 0.39382C15.081 -0.131273 14.2296 -0.131273 13.7045 0.39382L7.99999 6.09836L2.29548 0.393844C1.77038 -0.131249 0.919038 -0.13125 0.393945 0.393844C-0.131148 0.918937 -0.131148 1.77028 0.393945 2.29537L6.09846 7.99989L0.39382 13.7045Z"
fill="white"
/>
</svg>
) : (
<svg
width="16"
height="16"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="16" height="3" rx="1.5" fill="white" />
<rect y="7" width="16" height="3" rx="1.5" fill="white" />
<rect y="14" width="16" height="3" rx="1.5" fill="white" />
</svg>
)}
</button>
</>
);
}

View File

@ -1,13 +1,12 @@
import React from "react";
import Head from "next/head";
export default function Meta() {
const author = "Urbit";
export default function Meta(post, disableImage) {
const author = post?.extra?.author || "Urbit";
const title = post?.title ? `${post.title} - ` : "";
const description =
"Urbit is a general-purpose platform for building decentralized, peer-to-peer applications.";
const image = "https://media.urbit.org/logo/urbit-logo-card.png";
post?.description || "Urbit is a personal server built from scratch.";
const image =
post?.extra?.image || "https://media.urbit.org/logo/urbit-logo-card.png";
return (
<Head>
<>
<link rel="icon" type="image/png" href="/images/favicon.ico" />
<meta
name="twitter:card"
@ -16,11 +15,17 @@ export default function Meta() {
/>
<meta name="twitter:site" content="@urbit" key="twitter-site" />
<meta name="twitter:creator" content="@urbit" key="twitter-creator" />
<meta name="og:title" content="Urbit Developers" key="title" />
<meta
name="og:title"
content={`${title}developers.urbit.org`}
key="title"
/>
<meta name="og:description" content={description} key="description" />
<meta name="description" content={description} />
<meta name="author" content={author} key="author" />
<meta name="twitter:image" content={image} key="image" />
</Head>
{!disableImage && (
<meta name="twitter:image" content={image} key="image" />
)}
</>
);
}

24
components/Pagination.js Normal file
View File

@ -0,0 +1,24 @@
import Link from "next/link";
import { formatDate } from "../lib/lib";
export default function Pagination(props) {
return (
<Link
href={`/${props.section}/${props.post.slug}`}
key={`post-${props.post.slug}`}
>
<div
className="bg-wall-100 cursor-pointer font-semibold p-2 px-4 rounded-xl flex"
style={{ maxWidth: "45%" }}
>
<p className="shrink-0 pr-1">{props.previous ? "<- " : ""}</p>
<p className="text-wall-600 truncate">{props.post.title}</p>
<p className="shrink-0 pl-1">{props.next ? " ->" : ""}</p>
</div>
</Link>
);
}
Pagination.defaultProps = {
className: "",
};

50
components/PostPreview.js Normal file
View File

@ -0,0 +1,50 @@
import Link from "next/link";
import { BackgroundImage } from "foundation-design-system";
import { formatDate, generateDisplayDate } from "../lib/lib";
export default function PostPreview(props) {
const section = props?.section ? props.section : "blog";
const date = generateDisplayDate(props.post.date);
return (
<div className={`cursor-pointer ${props.className}`}>
{props.title ? <h3 className="mb-2">{props.title}</h3> : null}
<Link
href={`/${section}/${props.post.slug}`}
key={`post-${props.post.slug}`}
>
<div>
<BackgroundImage
className="aspect-w-5 aspect-h-4 rounded-lg"
src={props.post.extra.image || ""}
/>
<h4 className="mt-2">{props.post.title}</h4>
<div className="flex items-baseline mt-1">
{props.post.extra.author ? (
<div className="type-sub-bold mr-2">
{props.post.extra.author}
</div>
) : null}
{props.post.extra.ship ? (
<Link
href={`https://urbit.org/ids/${props.post.extra.ship}`}
passHref
>
<a className="type-sub-bold text-wall-500 font-mono">
{props.post.extra.ship}
</a>
</Link>
) : null}
</div>
<div className="text-wall-500 type-sub mt-1">{formatDate(date)}</div>
</div>
</Link>
</div>
);
}
PostPreview.defaultProps = {
className: "",
};

389
components/Search.js Normal file
View File

@ -0,0 +1,389 @@
import { Component, createRef } from "react";
// import { glossary } from "../lib/glossary";
import { withRouter } from "next/router";
import debounce from "lodash.debounce";
import Downshift from "downshift";
import ob from "urbit-ob";
import Sigil from "./Sigil";
// import levenSort from "leven-sort";
class Search extends Component {
constructor(props) {
super(props);
this.state = {
results: [],
};
this.searchEndpoint = this.searchEndpoint.bind(this);
this.onInputValueChange = this.onInputValueChange.bind(this);
this.onSelect = this.onSelect.bind(this);
this.glossarySearch = this.glossarySearch.bind(this);
this.patpSearch = this.patpSearch.bind(this);
this.urbitOrgSearch = this.urbitOrgSearch.bind(this);
this.opsSearch = this.opsSearch.bind(this);
}
searchEndpoint(query) {
return `/api/search?q=${query}`;
}
glossarySearch(query) {
return `/api/glossary?q=${encodeURIComponent(query)}`;
}
opsSearch(query) {
return `/api/ops-search?q=${query}`;
}
patpSearch(query) {
return (
(ob.isValidPatp(`~${deSig(query.toLowerCase())}`) &&
`~${deSig(query.toLowerCase())}`.length < 15) ||
(!isNaN(query) && query <= 4294967295)
);
}
urbitOrgSearch(query) {
return `/api/urbit-org-search?q=${query}`;
}
onSelect(item) {
if (item.slug) {
this.props.router.push(item.slug);
}
this.setState({
query: "",
results: [],
});
this.props.closeSearch();
}
onInputValueChange = debounce(async (query) => {
if (query.length) {
fetch(this.searchEndpoint(query))
.then((res) => res.json())
.then(async (res) => {
// Wrap results in an object which will tell React what component to use to render results.
const results = res.results.map((item) => ({
type: "RESULT",
content: item,
}));
const patp = this.patpSearch(query)
? !isNaN(query)
? ob.patp(query)
: ob.patp(ob.patp2dec(`~${deSig(query)}`))
: null;
const patpResult = this.patpSearch(query)
? [
{
type: "PATP",
content: {
patp: patp,
slug: `https://urbit.org/ids/${patp}`,
},
},
]
: [];
const urbitOrgResults = await fetch(this.urbitOrgSearch(query))
.then((res) => res.json())
.then((res) => {
return res.results.map((item) => ({
type: "URBIT_ORG_RESULT",
content: item,
}));
});
const glossaryResults = await fetch(this.glossarySearch(query))
.then((res) => res.json())
.then((res) => {
return res.results.map((item) => ({
type: "GLOSSARY_RESULT",
content: item,
}));
});
const opsResults = await fetch(this.opsSearch(query))
.then((res) => res.json())
.then((res) => {
return res.results.map((item) => ({
type: "OPS_RESULT",
content: item,
}));
});
const list = [
...glossaryResults,
...patpResult,
...results,
...urbitOrgResults,
...opsResults,
];
this.setState({ results: list });
});
} else {
this.setState({ results: [] });
}
}, 250);
render() {
const { state, props } = this;
if (props.showSearch) {
return (
<Downshift
onSelect={(selection) => this.onSelect(selection)}
onInputValueChange={(event) => this.onInputValueChange(event)}
itemToString={(item) => (item ? item.slug : "")}
defaultHighlightedIndex={0}
>
{({
getInputProps,
getItemProps,
getLabelProps,
getMenuProps,
isOpen,
inputValue,
highlightedIndex,
selectedItem,
getRootProps,
}) => (
<div className="fixed w-screen h-screen z-50 flex flex-col items-center p-4">
<div
onClick={(event) => props.closeSearch(event)}
className="top-0 left-0 fixed w-screen h-screen bg-washedWall"
/>
<div className="relative flex flex-col max-w-screen-lg md:my-32 w-full md:w-10/12 lg:w-8/12 xl:w-6/12 rounded-xl bg-white min-h-0 overflow-hidden">
<div
style={{ display: "inline-block" }}
{...getRootProps({}, { suppressRefError: true })}
>
<input
autoFocus
className="text-lg md:text-xl lg:text-2xl font-medium text-green-400 bg-transparent py-2 px-4 outline-none relative w-full"
placeholder="Search..."
type="text"
onClick={(e) => e.stopPropagation()}
{...getInputProps({
onKeyDown: (event) => {
if (event.key === "Escape") {
// Prevent Downshift's default 'Escape' behavior.
event.nativeEvent.preventDownshiftDefault = true;
this.props.closeSearch(event);
}
},
})}
/>
</div>
<ul {...getMenuProps()} className="overflow-y-scroll">
{isOpen
? state.results.map((item, index) => {
const selected = highlightedIndex === index;
if (item.type === "PATP") {
return (
<li
className={`cursor-pointer p-2 flex space-x-2 items-center text-left w-full ${
selected ? "bg-green-400" : ""
}`}
{...getItemProps({
key: item.content.slug + "-" + index,
index,
item: item.content,
selected: highlightedIndex === index,
})}
>
<div className="rounded-md overflow-hidden">
<Sigil
patp={item.content.patp}
size={25}
icon
/>
</div>
<p className="font-mono">{item.content.patp}</p>
</li>
);
}
if (item.type === "GLOSSARY_RESULT") {
return (
<li
className={`cursor-pointer flex text-left w-full ${
selected ? "bg-green-400" : ""
}`}
{...getItemProps({
key: item.content.slug + "-" + index,
index,
item: item.content,
selected: highlightedIndex === index,
})}
>
<div className="font-semibold p-3">
<p
className={`text-base ${
selected ? "text-white" : "text-wall-600"
}`}
>
{item.content.symbol.length > 0 && (
<code
className={`mr-1 rounded px-1 py-0.5 ${
selected
? "bg-washedWhite"
: "bg-wall-100"
}`}
>
{item.content.symbol}
</code>
)}
{item.content.name}
</p>
<p
className={`font-normal text-base mt-1 ${
selected ? "text-white" : "text-wall-600"
}`}
dangerouslySetInnerHTML={{
__html: item.content.desc,
}}
></p>
</div>
</li>
);
}
if (item.type === "RESULT") {
return (
<li
className={`cursor-pointer flex text-left w-full ${
selected ? "bg-green-400" : ""
}`}
{...getItemProps({
key: item.content.link + "-" + index,
index,
item: item.content,
selected,
})}
>
<div className="p-3">
<p
className={`font-medium text-base ${
selected ? "text-white" : "text-wall-600"
}`}
>
{item.content.parent !== "Content"
? `${item.content.parent} /`
: ""}{" "}
{item.content.title}
</p>
<p
className={`text-base font-regular text-small ${
selected ? "text-midWhite" : "text-wall-500"
}`}
>
{item.content.content}
</p>
</div>
</li>
);
}
if (item.type === "URBIT_ORG_RESULT") {
const urbOrgItem = Object.assign({}, item.content);
urbOrgItem[
"slug"
] = `https://urbit.org${item.content.slug}`;
return (
<li
className={`cursor-pointer flex text-left w-full ${
selected ? "bg-green-400" : ""
}`}
{...getItemProps({
key: item.content.link + "-" + index,
index,
item: urbOrgItem,
selected,
})}
>
<div className="p-3">
<p
className={`font-medium text-base ${
selected ? "text-white" : "text-wall-600"
}`}
>
<span className="text-wall-400">
{item.content.parent !== "Content"
? `urbit.org / ${item.content.parent} /`
: "urbit.org /"}{" "}
</span>
{item.content.title}
</p>
<p
className={`text-base font-regular text-small ${
selected ? "text-midWhite" : "text-wall-500"
}`}
>
{item.content.content}
</p>
</div>
</li>
);
}
if (item.type === "OPS_RESULT") {
const opsItem = Object.assign({}, item.content);
opsItem[
"slug"
] = `https://operators.urbit.org${item.content.slug}`;
return (
<li
className={`cursor-pointer flex text-left w-full ${
selected ? "bg-green-400" : ""
}`}
{...getItemProps({
key: item.content.link + "-" + index,
index,
item: opsItem,
selected,
})}
>
<div className="p-3">
<p
className={`font-medium text-base ${
selected ? "text-white" : "text-wall-600"
}`}
>
<span className="text-wall-400">
{item.content.parent !== "Content"
? `operators.urbit.org / ${item.content.parent} /`
: "operators.urbit.org /"}{" "}
</span>
{item.content.title}
</p>
<p
className={`text-base font-regular text-small ${
selected ? "text-midWhite" : "text-wall-500"
}`}
>
{item.content.content}
</p>
</div>
</li>
);
}
return null;
})
: null}
</ul>
</div>
</div>
)}
</Downshift>
);
} else {
return null;
}
}
}
function deSig(string) {
return string.startsWith("~") ? string.substring(1) : string;
}
export default withRouter(Search);

View File

@ -1,22 +0,0 @@
import React from "react";
import classnames from "classnames";
// Provides a flexible layout building block
export default function Section({
children,
className = "",
short = false,
narrow = false,
}) {
const spacing = classnames({
"py-8": short,
"py-20": !short,
"layout-narrow": narrow,
layout: !narrow,
});
return (
<section className={`w-full px-4 md:px-8 ${spacing} ${className}`}>
{children}
</section>
);
}

34
components/Sidebar.js Normal file
View File

@ -0,0 +1,34 @@
import Link from "next/link";
import { useState } from "react";
import MenuTray from "./MenuTray";
export default function Sidebar(props) {
const [isOpen, setTray] = useState(false);
return (
<>
<div className="hidden md:flex flex-col w-96 bg-wall-100 max-h-screen h-screen">
<header className="flex shrink-0 justify-between items-center pl-6 pt-12 mt-5 pb-8">
<Link href="/" passHref>
<a className="text-lg font-semibold text-wall-500">
Urbit Developers
</a>
</Link>
</header>
<div className="overflow-y-auto p-6 pt-16">
{props.children}
<div className="pb-32" />
</div>
</div>
<MenuTray isOpen={isOpen} setTray={setTray} search={props.search}>
<header className="flex shrink-0 justify-between items-center pb-8">
<Link href="/" passHref>
<a className="type-ui text-wall-500">Urbit</a>
</Link>
</header>
{props.children}
<div className="pt-64" />
</MenuTray>
</>
);
}

View File

@ -1,16 +1,42 @@
import React from "react";
import { sigil, reactRenderer } from "@tlon/sigil-js";
export default function Sigil(props) {
export const foregroundFromBackground = (background) => {
const rgb = {
r: parseInt(background.slice(1, 3), 16),
g: parseInt(background.slice(3, 5), 16),
b: parseInt(background.slice(5, 7), 16),
};
const brightness = (299 * rgb.r + 587 * rgb.g + 114 * rgb.b) / 1000;
const whiteBrightness = 255;
return whiteBrightness - brightness < 50 ? "black" : "white";
};
export const Sigil = ({
patp,
size,
color = "#24201E",
icon,
className = "",
}) => {
if (patp.length > 14) {
return <div />;
}
const foreground = foregroundFromBackground(color);
return (
<div className="p-4 bg-black inline-block rounded-lg">
<div
className={(icon ? "p-1 " : "") + className}
style={{ backgroundColor: icon ? color || "black" : "transparent" }}
>
{sigil({
patp: props.patp,
patp: patp,
renderer: reactRenderer,
size: 32,
margin: false,
colors: ["black", "white"],
size: icon ? size / 2 : size,
colors: [color, foreground],
icon: icon || false,
})}
</div>
);
}
};
export default Sigil;

View File

@ -1,9 +0,0 @@
import React from "react";
export default function SingleColumn({ children }) {
return (
<div className="flex flex-col w-full items-center max-w-screen-2xl">
{children}
</div>
);
}

107
components/Snippets.js Normal file
View File

@ -0,0 +1,107 @@
import Link from "next/link";
import { formatDate, formatTime, formatTimeZone } from "../lib/lib";
export function Name({ children, className }) {
return <b className={`font-normal ${className || ""}`}>{children}</b>;
}
export function Patp({ children, className }) {
return (
<Link href={`https://urbit.org/ids/${children}`} passHref>
<a>
<code className={`font-mono ${className || ""}`}>{children}</code>
</a>
</Link>
);
}
// Used to render a human name alongside their @p
export function Person({
name,
patp,
nameClassNames,
patpClassNames,
className,
}) {
return (
<>
{patp && name ? (
<>
<Name className={nameClassNames}>{name}</Name>{" "}
<Patp className={patpClassNames + " opacity-60"}>{patp}</Patp>
</>
) : patp ? (
<Patp className={patpClassNames}>{patp}</Patp>
) : name ? (
<Name className={nameClassNames}>{name}</Name>
) : null}
</>
);
}
// This goes inside a <p/> tag
export function ReadableList({ children, serial = ",", conjunction = "and" }) {
return (
<>
{children.map((Child, index) => {
if (index < children.length - 2) {
return (
<>
{Child}
{serial}
</>
);
} else if (index < children.length - 1) {
return (
<>
{Child} {conjunction}{" "}
</>
);
}
return <>{Child}</>;
})}
</>
);
}
export function ShowOrHide({ children, condition }) {
if (condition) {
return children;
}
return null;
}
export function DateRange({ starts, ends, className, short }) {
// For events which have no end datetime
if (!ends.isValid) {
return (
<div>
<p className={className}>
{`${formatDate(starts)}${formatTime(starts)} ${formatTimeZone(
starts
)}`}
</p>
</div>
);
}
// For events which start and end on the same day
if (starts.hasSame(ends, "day")) {
return (
<div>
<p className={className}>
{`${formatDate(starts)}${formatTime(starts)} to ${formatTime(
ends
)} ${formatTimeZone(starts)}`}
</p>
</div>
);
}
// For multi-day events
return (
<div>
<p className={className}>{`${starts.toFormat(
"cccc, LLLL d"
)} to ${formatDate(ends)}`}</p>
</div>
);
}

41
components/TallCard.js Normal file
View File

@ -0,0 +1,41 @@
import Link from "next/link";
export default function TallCard({
image,
title,
description,
callout,
href,
className = "",
}) {
return (
<div
className={`cursor-pointer lg:aspect-w-8 lg:aspect-h-10 xl:aspect-w-8 xl:aspect-h-8 ${className}`}
>
<div key={title} className={`bg-wall-100 rounded-xl min-h-0 `}>
<Link href={href}>
<div className="flex flex-col space-y-4 p-6 justify-center items-center h-full relative">
<img
className="rounded-lg self-center overflow-hidden border-transparent border w-full shrink-0"
src={image}
/>
<div className={`grow-1 flex flex-col h-full w-full`}>
<h3 className="mb-2">{title}</h3>
<p>{description}</p>
</div>
<div className="self-start">
<Link href={href} passHref>
<a
className="button-sm bg-green-400 text-white w-fit"
onClick={(e) => e.stopPropagation()}
>
{callout}
</a>
</Link>
</div>
</div>
</Link>
</div>
</div>
);
}

40
components/icons/arvo.js Normal file
View File

@ -0,0 +1,40 @@
export default function Arvo() {
return (
<svg
width="82"
height="80"
viewBox="0 0 82 80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M32.7954 58.5399V43.6799H47.6554V58.5399H32.7954ZM34.8253 51.1056C34.8253 54.0578 37.2487 56.4444 40.2254 56.4444C43.2015 56.4444 45.6255 54.0669 45.6255 51.1056C45.6255 48.1443 43.2015 45.7667 40.2254 45.7667C37.2488 45.7667 34.8253 48.1533 34.8253 51.1056Z"
stroke="black"
/>
<mask id="path-2-inside-1_643_827" fill="white">
<path d="M26.1954 51.1099C26.1954 61.6833 10.3354 61.6833 10.3354 51.1099C10.3354 40.5366 26.1954 40.5366 26.1954 51.1099Z" />
</mask>
<path
d="M25.1954 51.1099C25.1954 53.4819 24.3159 55.1874 23.053 56.31C21.7716 57.449 20.0349 58.0399 18.2654 58.0399C16.496 58.0399 14.7593 57.449 13.4779 56.31C12.215 55.1874 11.3354 53.4819 11.3354 51.1099H9.33545C9.33545 54.0246 10.4384 56.2841 12.1492 57.8048C13.8416 59.3092 16.0699 60.0399 18.2654 60.0399C20.461 60.0399 22.6893 59.3092 24.3817 57.8048C26.0925 56.2841 27.1954 54.0246 27.1954 51.1099H25.1954ZM11.3354 51.1099C11.3354 48.738 12.215 47.0325 13.4779 45.9098C14.7593 44.7709 16.496 44.1799 18.2654 44.1799C20.0349 44.1799 21.7716 44.7709 23.053 45.9098C24.3159 47.0325 25.1954 48.738 25.1954 51.1099H27.1954C27.1954 48.1952 26.0925 45.9357 24.3817 44.415C22.6893 42.9107 20.461 42.1799 18.2654 42.1799C16.0699 42.1799 13.8416 42.9107 12.1492 44.415C10.4384 45.9357 9.33545 48.1952 9.33545 51.1099H11.3354Z"
fill="black"
mask="url(#path-2-inside-1_643_827)"
/>
<path
d="M62.6904 21.7977L69.43 25.6944L69.5432 33.6731L62.9008 37.7222L56.1612 33.8256L56.048 25.8468L62.6904 21.7977Z"
stroke="black"
/>
<path
d="M36.6493 31.5629L36.7029 31.1047L36.2505 31.0144L32.4002 30.2464L35.7481 28.1318L36.1336 27.8883L35.9265 27.482L34.1199 23.9376L37.8107 25.2688L38.249 25.4268L38.4376 25.0008L40.0414 21.3773L41.3099 25.1355L41.4591 25.5778L41.9106 25.4593L45.706 24.4627L43.5927 27.8267L43.3502 28.2128L43.7123 28.4898L46.859 30.8968L42.9445 31.3223L42.4857 31.3722L42.4987 31.8335L42.6108 35.8147L39.8594 32.9942L39.5322 32.6588L39.1762 32.9636L36.1854 35.525L36.6493 31.5629Z"
stroke="black"
/>
<path
d="M63.4056 43.9013L65.139 45.7038L65.309 45.8805L65.5527 45.8543L68.0392 45.5878L68.2085 48.0828L68.2251 48.3274L68.4286 48.464L70.5047 49.8582L69.0306 51.8783L68.8861 52.0763L68.9542 52.3118L69.6485 54.7143L67.2208 55.3142L66.9828 55.373L66.8835 55.5972L65.8711 57.8839L63.6257 56.7829L63.4056 56.675L63.1855 56.7829L60.9401 57.8839L59.9277 55.5972L59.8284 55.373L59.5904 55.3142L57.1627 54.7143L57.857 52.3118L57.925 52.0763L57.7805 51.8783L56.3064 49.8582L58.3825 48.464L58.5861 48.3274L58.6027 48.0828L58.7719 45.5878L61.2585 45.8543L61.5022 45.8805L61.6721 45.7038L63.4056 43.9013Z"
stroke="black"
/>
<path
d="M26.3072 31.5902L18.3354 37.382L10.3637 31.5902L13.4087 22.2188L23.2622 22.2188L26.3072 31.5902Z"
stroke="black"
/>
</svg>
);
}

File diff suppressed because one or more lines are too long

21
components/icons/comms.js Normal file
View File

@ -0,0 +1,21 @@
export default function Comms({ className }) {
return (
<svg
width="81"
height="80"
viewBox="0 0 81 80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="0.490967" width="80" height="80" rx="10" fill="#208DF2" />
<path
d="M57.0526 32.9366H38.6793C36.4472 32.9366 34.7418 34.6421 34.7418 36.8741V47.375C34.7418 49.4741 36.4472 51.3125 38.6793 51.3125H51.8035L57.0526 56.5616V51.3125C59.2847 51.3125 60.9901 49.4741 60.9901 47.375V36.8741C60.9901 34.6445 59.2847 32.9366 57.0526 32.9366Z"
fill="white"
/>
<path
d="M41.3026 23.75H22.9293C20.6972 23.75 18.9918 25.4554 18.9918 27.6875V38.1883C18.9918 40.2875 20.6972 42.1258 22.9293 42.1258V47.375L28.1785 42.1258H32.116V36.8767C32.116 33.2025 35.0026 30.3133 38.6793 30.3133H45.2427V27.6875C45.2402 25.4554 43.5348 23.75 41.3027 23.75H41.3026Z"
fill="white"
/>
</svg>
);
}

View File

@ -0,0 +1,72 @@
export default function Cryptography() {
return (
<svg
width="80"
height="81"
viewBox="0 0 80 81"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="15.5"
y="23.4717"
width="51"
height="34"
rx="3.5"
stroke="black"
/>
<path d="M21.2188 30.3154H23.25V32.3467H21.2188V30.3154Z" fill="black" />
<path d="M31.375 30.3154H33.4062V32.3467H31.375V30.3154Z" fill="black" />
<path
d="M35.4375 30.3154H37.4688V32.3467H35.4375V30.3154Z"
fill="black"
/>
<path d="M47.625 30.3154H49.6562V32.3467H47.625V30.3154Z" fill="black" />
<path
d="M51.6875 30.3154H53.7188V32.3467H51.6875V30.3154Z"
fill="black"
/>
<path d="M21.2188 36.4092H23.25V38.4404H21.2188V36.4092Z" fill="black" />
<path
d="M25.2812 36.4092H27.3125V38.4404H25.2812V36.4092Z"
fill="black"
/>
<path d="M29.3438 36.4092H31.375V38.4404H29.3438V36.4092Z" fill="black" />
<path
d="M33.4062 36.4092H35.4375V38.4404H33.4062V36.4092Z"
fill="black"
/>
<path d="M39.5 36.4092H41.5312V38.4404H39.5V36.4092Z" fill="black" />
<path
d="M43.5625 36.4092H45.5938V38.4404H43.5625V36.4092Z"
fill="black"
/>
<path
d="M57.7812 36.4092H59.8125V38.4404H57.7812V36.4092Z"
fill="black"
/>
<path d="M53.7188 42.5029H55.75V44.5342H53.7188V42.5029Z" fill="black" />
<path
d="M43.5625 42.5029H45.5938V44.5342H43.5625V42.5029Z"
fill="black"
/>
<path d="M39.5 42.5029H41.5312V44.5342H39.5V42.5029Z" fill="black" />
<path
d="M27.3125 42.5029H29.3438V44.5342H27.3125V42.5029Z"
fill="black"
/>
<path d="M23.25 42.5029H25.2812V44.5342H23.25V42.5029Z" fill="black" />
<path d="M21.2188 48.5967H23.25V50.6279H21.2188V48.5967Z" fill="black" />
<path
d="M27.3125 48.5967H29.3438V50.6279H27.3125V48.5967Z"
fill="black"
/>
<path d="M31.375 48.5967H33.4062V50.6279H31.375V48.5967Z" fill="black" />
<path d="M47.625 48.5967H49.6562V50.6279H47.625V48.5967Z" fill="black" />
<path
d="M51.6875 48.5967H53.7188V50.6279H51.6875V48.5967Z"
fill="black"
/>
</svg>
);
}

View File

@ -0,0 +1,31 @@
export default function Database({ className }) {
return (
<svg
width="64"
height="65"
viewBox="0 0 64 65"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<line x1="3" y1="36.4545" x2="61" y2="36.4545" stroke="#BCBCBC" />
<line x1="3" y1="16.6816" x2="61" y2="16.6816" stroke="#BCBCBC" />
<line x1="3" y1="43.0454" x2="61" y2="43.0454" stroke="#BCBCBC" />
<line x1="3" y1="29.8634" x2="61" y2="29.8634" stroke="#BCBCBC" />
<line
x1="3.8573"
y1="56.2788"
x2="59.5778"
y2="56.2788"
stroke="#BCBCBC"
/>
<line x1="3" y1="23.2727" x2="61" y2="23.2727" stroke="#BCBCBC" />
<line x1="3" y1="49.6366" x2="61" y2="49.6366" stroke="#BCBCBC" />
<path d="M11.0184 4.45313L11.0184 62" stroke="#2A2A2A" />
<line x1="24.178" y1="4" x2="24.178" y2="62" stroke="#BCBCBC" />
<line x1="37.291" y1="4" x2="37.291" y2="62" stroke="#BCBCBC" />
<path d="M3.8573 10.2487L60.0938 10.2487" stroke="#2A2A2A" />
<rect x="3.5" y="4.5" width="57" height="57" rx="9.5" stroke="#2B2B2B" />
</svg>
);
}

View File

@ -0,0 +1,49 @@
export default function Distribution({ className }) {
return (
<svg
width="64"
height="65"
viewBox="0 0 64 65"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g clipPath="url(#clip0_663_420)">
<rect
x="23.7222"
y="24.3007"
width="16.3986"
height="16.3986"
rx="4.5"
stroke="#24221E"
/>
<path
d="M31.9215 3.13989L32.2818 2.7932C32.1875 2.69525 32.0575 2.63989 31.9215 2.63989C31.7856 2.63989 31.6555 2.69525 31.5612 2.7932L31.9215 3.13989ZM38.9845 10.4799L38.9845 10.9799C39.1849 10.9799 39.366 10.8602 39.4445 10.6757C39.523 10.4912 39.4838 10.2777 39.3447 10.1332L38.9845 10.4799ZM24.8586 10.4799L24.4983 10.1332C24.3593 10.2777 24.32 10.4912 24.3985 10.6757C24.477 10.8602 24.6581 10.9799 24.8586 10.9799L24.8586 10.4799ZM29.203 10.4799H29.703C29.703 10.3473 29.6503 10.2201 29.5565 10.1264C29.4628 10.0326 29.3356 9.97992 29.203 9.97992V10.4799ZM29.203 21.6259H28.703C28.703 21.902 28.9268 22.1259 29.203 22.1259V21.6259ZM34.64 21.6259V22.1259C34.7726 22.1259 34.8998 22.0732 34.9936 21.9794C35.0874 21.8857 35.14 21.7585 35.14 21.6259H34.64ZM34.64 10.4799V9.97992C34.3639 9.97992 34.14 10.2038 34.14 10.4799H34.64ZM31.5612 3.48658L38.6242 10.8266L39.3447 10.1332L32.2818 2.7932L31.5612 3.48658ZM25.2188 10.8266L32.2818 3.48658L31.5612 2.7932L24.4983 10.1332L25.2188 10.8266ZM29.203 9.97992L24.8586 9.97992L24.8586 10.9799L29.203 10.9799V9.97992ZM29.703 21.6259L29.703 10.4799H28.703L28.703 21.6259H29.703ZM34.64 21.1259L29.203 21.1259V22.1259L34.64 22.1259V21.1259ZM34.14 10.4799L34.14 21.6259H35.14L35.14 10.4799H34.14ZM38.9845 9.97992L34.64 9.97992V10.9799L38.9845 10.9799L38.9845 9.97992Z"
fill="#24221E"
/>
<path
d="M31.9215 61.8601L31.5612 62.2068C31.6555 62.3048 31.7856 62.3601 31.9215 62.3601C32.0575 62.3601 32.1875 62.3048 32.2818 62.2068L31.9215 61.8601ZM24.8586 54.5201L24.8586 54.0201C24.6581 54.0201 24.477 54.1398 24.3985 54.3243C24.32 54.5088 24.3593 54.7223 24.4983 54.8668L24.8586 54.5201ZM38.9845 54.5201L39.3447 54.8668C39.4838 54.7223 39.523 54.5088 39.4445 54.3243C39.366 54.1398 39.1849 54.0201 38.9845 54.0201L38.9845 54.5201ZM34.64 54.5201L34.14 54.5201C34.14 54.7962 34.3639 55.0201 34.64 55.0201L34.64 54.5201ZM34.64 43.3741L35.14 43.3741C35.14 43.098 34.9162 42.8741 34.64 42.8741L34.64 43.3741ZM29.203 43.3741L29.203 42.8741C29.0704 42.8741 28.9432 42.9268 28.8494 43.0206C28.7557 43.1143 28.703 43.2415 28.703 43.3741L29.203 43.3741ZM29.203 54.5201L29.203 55.0201C29.3356 55.0201 29.4628 54.9674 29.5565 54.8736C29.6503 54.7799 29.703 54.6527 29.703 54.5201L29.203 54.5201ZM32.2818 61.5134L25.2188 54.1734L24.4983 54.8668L31.5612 62.2068L32.2818 61.5134ZM38.6242 54.1734L31.5612 61.5134L32.2818 62.2068L39.3447 54.8668L38.6242 54.1734ZM34.64 55.0201L38.9845 55.0201L38.9845 54.0201L34.64 54.0201L34.64 55.0201ZM34.14 43.3741L34.14 54.5201L35.14 54.5201L35.14 43.3741L34.14 43.3741ZM29.203 43.8741L34.64 43.8741L34.64 42.8741L29.203 42.8741L29.203 43.8741ZM29.703 54.5201L29.703 43.3741L28.703 43.3741L28.703 54.5201L29.703 54.5201ZM24.8586 55.0201L29.203 55.0201L29.203 54.0201L24.8586 54.0201L24.8586 55.0201Z"
fill="#24221E"
/>
<path
d="M61.2816 32.7719L61.6283 33.1321C61.7263 33.0379 61.7816 32.9078 61.7816 32.7719C61.7816 32.6359 61.7263 32.5058 61.6283 32.4116L61.2816 32.7719ZM53.9416 39.8348L53.4416 39.8348C53.4416 40.0353 53.5613 40.2164 53.7458 40.2949C53.9303 40.3734 54.1438 40.3341 54.2883 40.1951L53.9416 39.8348ZM53.9416 25.7089L54.2883 25.3486C54.1438 25.2096 53.9303 25.1703 53.7458 25.2488C53.5613 25.3273 53.4416 25.5084 53.4416 25.7089L53.9416 25.7089ZM53.9416 30.0533V30.5533C54.0742 30.5533 54.2014 30.5006 54.2951 30.4069C54.3889 30.3131 54.4416 30.1859 54.4416 30.0533H53.9416ZM42.7956 30.0533V29.5533C42.5195 29.5533 42.2956 29.7772 42.2956 30.0533H42.7956ZM42.7956 35.4904H42.2956C42.2956 35.623 42.3483 35.7502 42.4421 35.8439C42.5358 35.9377 42.663 35.9904 42.7956 35.9904V35.4904ZM53.9416 35.4904H54.4416C54.4416 35.3578 54.3889 35.2306 54.2951 35.1368C54.2014 35.0431 54.0742 34.9904 53.9416 34.9904V35.4904ZM60.9349 32.4116L53.5949 39.4745L54.2883 40.1951L61.6283 33.1321L60.9349 32.4116ZM53.5949 26.0692L60.9349 33.1321L61.6283 32.4116L54.2883 25.3486L53.5949 26.0692ZM54.4416 30.0533L54.4416 25.7089L53.4416 25.7089L53.4416 30.0533H54.4416ZM42.7956 30.5533L53.9416 30.5533V29.5533L42.7956 29.5533V30.5533ZM43.2956 35.4904V30.0533H42.2956V35.4904H43.2956ZM53.9416 34.9904H42.7956V35.9904L53.9416 35.9904V34.9904ZM54.4416 39.8348L54.4416 35.4904H53.4416L53.4416 39.8348L54.4416 39.8348Z"
fill="#24221E"
/>
<path
d="M2.5614 32.7719L2.21471 32.4116C2.11676 32.5058 2.0614 32.6359 2.0614 32.7719C2.0614 32.9078 2.11676 33.0379 2.21471 33.1321L2.5614 32.7719ZM9.90143 25.7089L10.4014 25.7089C10.4014 25.5084 10.2817 25.3273 10.0972 25.2488C9.91273 25.1703 9.6992 25.2096 9.55474 25.3486L9.90143 25.7089ZM9.90143 39.8348L9.55474 40.1951C9.69921 40.3341 9.91273 40.3734 10.0972 40.2949C10.2817 40.2164 10.4014 40.0353 10.4014 39.8348L9.90143 39.8348ZM9.90143 35.4904L9.90143 34.9904C9.62529 34.9904 9.40143 35.2142 9.40143 35.4904L9.90143 35.4904ZM21.0474 35.4904L21.0474 35.9904C21.3235 35.9904 21.5474 35.7665 21.5474 35.4904L21.0474 35.4904ZM21.0474 30.0533L21.5474 30.0533C21.5474 29.9207 21.4947 29.7935 21.401 29.6998C21.3072 29.606 21.18 29.5533 21.0474 29.5533L21.0474 30.0533ZM9.90143 30.0533L9.40143 30.0533C9.40143 30.1859 9.45411 30.3131 9.54788 30.4069C9.64165 30.5006 9.76882 30.5533 9.90143 30.5533L9.90143 30.0533ZM2.90809 33.1321L10.2481 26.0692L9.55474 25.3486L2.21471 32.4116L2.90809 33.1321ZM10.2481 39.4745L2.90809 32.4116L2.21471 33.1321L9.55474 40.1951L10.2481 39.4745ZM9.40143 35.4904L9.40143 39.8348L10.4014 39.8348L10.4014 35.4904L9.40143 35.4904ZM21.0474 34.9904L9.90143 34.9904L9.90143 35.9904L21.0474 35.9904L21.0474 34.9904ZM20.5474 30.0533L20.5474 35.4904L21.5474 35.4904L21.5474 30.0533L20.5474 30.0533ZM9.90143 30.5533L21.0474 30.5533L21.0474 29.5533L9.90143 29.5533L9.90143 30.5533ZM9.40143 25.7089L9.40143 30.0533L10.4014 30.0533L10.4014 25.7089L9.40143 25.7089Z"
fill="#24221E"
/>
</g>
<defs>
<clipPath id="clip0_663_420">
<rect
width="45.2548"
height="45.2548"
fill="white"
transform="translate(32.0001 0.5) rotate(45)"
/>
</clipPath>
</defs>
</svg>
);
}

View File

@ -0,0 +1,82 @@
export default function Functional({ className = "" }) {
return (
<svg
width="64"
height="65"
viewBox="0 0 64 65"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<line
x1="32.2175"
y1="58.3634"
x2="55.8175"
y2="33.6907"
stroke="#24221E"
stroke-dasharray="2 2"
/>
<line
x1="7.71708"
y1="33.9482"
x2="31.8594"
y2="58.0905"
stroke="#24221E"
stroke-dasharray="2 2"
/>
<line
x1="31.3249"
y1="9.70914"
x2="7.72494"
y2="34.3819"
stroke="#24221E"
stroke-dasharray="2 2"
/>
<line
x1="55.8252"
y1="34.1245"
x2="31.6828"
y2="9.98223"
stroke="#24221E"
stroke-dasharray="2 2"
/>
<line
x1="31.5"
y1="41"
x2="31.5"
y2="51"
stroke="#24221E"
stroke-dasharray="2 2"
/>
<line
x1="31.5"
y1="16"
x2="31.5"
y2="25"
stroke="#24221E"
stroke-dasharray="2 2"
/>
<line
x1="24"
y1="33.5"
x2="14"
y2="33.5"
stroke="#24221E"
stroke-dasharray="2 2"
/>
<line
x1="49"
y1="33.5"
x2="40"
y2="33.5"
stroke="#24221E"
stroke-dasharray="2 2"
/>
<circle cx="7.5" cy="33.1001" r="5" fill="white" stroke="black" />
<circle cx="31.1001" cy="9.5" r="5" fill="white" stroke="black" />
<circle cx="54.7" cy="33.1001" r="5" fill="white" stroke="black" />
<circle cx="31.1001" cy="56.7" r="5" fill="white" stroke="black" />
<circle cx="31.5" cy="33.5" r="8" stroke="black" />
</svg>
);
}

28
components/icons/hoon.js Normal file
View File

@ -0,0 +1,28 @@
export default function Hoon() {
return (
<svg
width="82"
height="80"
viewBox="0 0 82 80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.5045 24.5455C15.4931 23.9167 15.5954 23.411 15.8113 23.0284C16.0272 22.6458 16.3075 22.3674 16.6522 22.1932C17.0007 22.0189 17.3681 21.9318 17.7545 21.9318C18.1408 21.9318 18.4931 22.017 18.8113 22.1875C19.1332 22.3542 19.5007 22.6174 19.9136 22.9773C20.1939 23.2197 20.4173 23.3902 20.584 23.4886C20.7545 23.5871 20.9401 23.6364 21.1408 23.6364C21.4666 23.6364 21.7242 23.5189 21.9136 23.2841C22.1029 23.0455 22.1939 22.7008 22.1863 22.25H23.4136C23.4249 22.8788 23.3226 23.3845 23.1067 23.767C22.8946 24.1496 22.6143 24.428 22.2658 24.6023C21.9173 24.7765 21.5499 24.8636 21.1636 24.8636C20.7772 24.8636 20.4249 24.7803 20.1067 24.6136C19.7886 24.4432 19.4211 24.178 19.0045 23.8182C18.7279 23.5758 18.5045 23.4053 18.334 23.3068C18.1636 23.2083 17.9779 23.1591 17.7772 23.1591C17.4704 23.1591 17.2166 23.267 17.0158 23.483C16.8189 23.6951 16.7242 24.0492 16.7317 24.5455H15.5045Z"
fill="black"
/>
<line x1="31.1863" y1="24.5" x2="68.1863" y2="24.5" stroke="black" />
<line x1="15.1863" y1="57.5" x2="68.1863" y2="57.5" stroke="black" />
<line x1="31.1863" y1="44.5" x2="68.1863" y2="44.5" stroke="black" />
<line x1="15.1863" y1="31.5" x2="53.1863" y2="31.5" stroke="black" />
<line x1="55.1863" y1="31.5" x2="68.1863" y2="31.5" stroke="black" />
<line x1="15.1863" y1="51.5" x2="53.1863" y2="51.5" stroke="black" />
<line x1="15.1863" y1="37.5" x2="39.1863" y2="37.5" stroke="black" />
<line x1="15.1863" y1="64.5" x2="39.1863" y2="64.5" stroke="black" />
<line x1="15.1863" y1="44.5" x2="29.1863" y2="44.5" stroke="black" />
<line x1="55.1863" y1="51.5" x2="68.1863" y2="51.5" stroke="black" />
<line x1="42.1863" y1="37.5" x2="68.1863" y2="37.5" stroke="black" />
<line x1="42.1863" y1="64.5" x2="68.1863" y2="64.5" stroke="black" />
</svg>
);
}

View File

@ -0,0 +1,94 @@
export default function Identity({ className }) {
return (
<svg
width="64"
height="65"
viewBox="0 0 64 65"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<rect
x="7"
y="8.5"
width="49.2268"
height="49.2268"
rx="5.5"
fill="white"
stroke="#363636"
/>
<g clipPath="url(#clip0_662_1567)">
<path
d="M48.5432 33.8682C48.2453 33.8682 48.0041 33.6269 48.0041 33.329C48.0162 29.7314 46.8179 26.2338 44.6019 23.4005C44.4186 23.1646 44.4604 22.8263 44.6963 22.643C44.9322 22.4596 45.2705 22.5014 45.4538 22.7373C47.8181 25.7607 49.096 29.4916 49.0825 33.329C49.0825 33.4719 49.0259 33.6094 48.9248 33.7105C48.8237 33.8116 48.6862 33.8682 48.5433 33.8682L48.5432 33.8682Z"
fill="#24221E"
/>
<path
d="M41.0972 19.9578C40.9907 19.9578 40.8869 19.9255 40.7979 19.8662C38.1493 18.1018 35.0395 17.1569 31.8573 17.1515C28.6736 17.1448 25.5613 18.0789 22.906 19.8338C22.6594 19.9834 22.3372 19.912 22.1768 19.6707C22.0178 19.4294 22.0757 19.1059 22.3103 18.9361C25.1422 17.0639 28.4634 16.0678 31.8589 16.0745C35.2542 16.0799 38.5714 17.0881 41.3967 18.9711C41.5935 19.1032 41.6812 19.3486 41.6124 19.575C41.5437 19.8014 41.3347 19.9578 41.0975 19.9578L41.0972 19.9578Z"
fill="#24221E"
/>
<path
d="M15.1151 33.8681C14.8172 33.8681 14.5759 33.6268 14.5759 33.3289C14.5611 28.6613 16.4522 24.1903 19.8112 20.9498C20.0282 20.7422 20.3733 20.7503 20.5809 20.9673C20.7884 21.1843 20.7803 21.5294 20.5633 21.737C17.4173 24.7711 15.6448 28.9578 15.6541 33.329C15.6541 33.4719 15.5975 33.6094 15.4964 33.7105C15.3953 33.8116 15.2578 33.8682 15.1149 33.8682L15.1151 33.8681Z"
fill="#24221E"
/>
<path
d="M41.5043 47.5037C41.2953 47.5037 41.1039 47.3837 41.015 47.1937C40.926 47.0036 40.9557 46.7799 41.0891 46.6194C42.3858 45.064 43.332 42.7941 43.9008 39.88C43.9588 39.5875 44.2418 39.3961 44.5343 39.4541C44.8268 39.512 45.0182 39.7951 44.9603 40.0876C44.3564 43.1905 43.3428 45.622 41.9194 47.3177C41.8156 47.4377 41.6633 47.5064 41.5042 47.5037L41.5043 47.5037Z"
fill="#24221E"
/>
<path
d="M44.9711 35.9954H44.9415C44.6449 35.9792 44.4171 35.7258 44.4319 35.4293C44.4832 34.4534 44.4832 33.7147 44.4832 33.3589C44.4832 30.934 43.7876 28.559 42.4829 26.5156C41.1767 24.4721 39.3139 22.8451 37.113 21.8262C36.9768 21.7709 36.869 21.6617 36.8137 21.5256C36.7598 21.3881 36.7639 21.2344 36.8259 21.101C36.8865 20.9675 37.0011 20.8651 37.1399 20.8166C37.2801 20.7681 37.4324 20.7802 37.5632 20.8476C39.9476 21.9502 41.9669 23.7105 43.3848 25.921C44.8014 28.133 45.5576 30.7019 45.5643 33.3292C45.5643 33.7147 45.5643 34.4749 45.5104 35.4859C45.4942 35.7716 45.2583 35.9954 44.9712 35.9954L44.9711 35.9954Z"
fill="#24221E"
/>
<path
d="M16.7002 39.5889C16.4616 39.5889 16.2527 39.4326 16.184 39.2048C16.1152 38.977 16.2055 38.7303 16.4037 38.5996C17.9052 37.6102 18.0966 35.2648 18.0966 33.3318C18.1007 29.691 19.5483 26.2 22.1228 23.6241C24.6973 21.0497 28.1883 19.6007 31.8291 19.5966C32.127 19.5966 32.3683 19.8378 32.3683 20.1357C32.3683 20.4336 32.127 20.6749 31.8291 20.6749C28.4742 20.6789 25.2567 22.0133 22.8844 24.3843C20.5121 26.7566 19.1763 29.974 19.1722 33.3291C19.1722 35.5315 18.9242 38.2248 16.994 39.4971C16.9064 39.555 16.8053 39.5874 16.7001 39.5887L16.7002 39.5889Z"
fill="#24221E"
/>
<path
d="M36.2852 49.7952C36.0696 49.7938 35.8741 49.6644 35.7906 49.4649C35.707 49.2668 35.7501 49.0363 35.8997 48.8813C40.3072 44.3468 40.9653 37.195 40.9653 33.3293C40.9639 31.1753 40.2037 29.0915 38.8154 27.4442C37.4284 25.7971 35.5035 24.6931 33.382 24.3265C31.2591 23.9599 29.0766 24.3548 27.2167 25.4412C26.9633 25.5625 26.6587 25.4695 26.5172 25.2256C26.3757 24.9816 26.4458 24.6716 26.6776 24.5112C28.756 23.2981 31.1959 22.8586 33.5682 23.2684C35.9391 23.6782 38.0902 24.9128 39.6406 26.7528C41.1907 28.5941 42.0425 30.9233 42.0439 33.3294C42.0439 37.3515 41.3457 44.8217 36.6713 49.6281C36.5702 49.7333 36.4313 49.7939 36.2857 49.7953L36.2852 49.7952Z"
fill="#24221E"
/>
<path
d="M22.1538 33.8681C21.8559 33.8681 21.6146 33.6268 21.6146 33.3289C21.6173 30.958 22.4422 28.6626 23.9492 26.8321C24.1419 26.6124 24.4735 26.5854 24.6986 26.7714C24.9237 26.9574 24.9601 27.289 24.7795 27.5195C23.433 29.1559 22.6956 31.21 22.6929 33.329C22.6929 33.4719 22.6363 33.6094 22.5352 33.7105C22.4341 33.8116 22.2967 33.8682 22.1538 33.8682L22.1538 33.8681Z"
fill="#24221E"
/>
<path
d="M17.9591 43.2039C17.6882 43.2079 17.4564 43.0084 17.4186 42.7388C17.3809 42.4706 17.5493 42.2158 17.8108 42.1444C20.0942 41.5163 20.9973 38.2517 21.3315 36.3618C21.3571 36.2216 21.4367 36.0963 21.5539 36.014C21.6712 35.9318 21.8168 35.8995 21.957 35.9251C22.2508 35.9776 22.4463 36.2567 22.3937 36.5505C22.0135 38.691 20.9514 42.3976 18.0966 43.185C18.0521 43.1972 18.0049 43.2039 17.9591 43.2039L17.9591 43.2039Z"
fill="#24221E"
/>
<path
d="M31.2605 50.5822C31.0273 50.5822 30.8211 50.4326 30.7469 50.2116C30.6741 49.9905 30.7523 49.7479 30.9397 49.6091C35.6035 46.1529 36.9997 40.4055 37.3447 36.1947C37.3569 36.0518 37.4243 35.9197 37.5334 35.8267C37.644 35.7337 37.7855 35.6879 37.9284 35.7C38.2262 35.7243 38.4473 35.9858 38.423 36.2837C38.0564 40.7157 36.5764 46.7812 31.5811 50.4772C31.4881 50.5459 31.3762 50.5823 31.2603 50.5823L31.2605 50.5822Z"
fill="#24221E"
/>
<path
d="M20.1887 45.8728C19.8908 45.8998 19.6279 45.6801 19.601 45.3822C19.574 45.0843 19.7937 44.8214 20.0916 44.7945C24.8873 43.9129 25.1303 35.0896 25.1303 33.3266C25.1289 31.2819 26.0603 29.3491 27.6616 28.0779C29.2629 26.8067 31.3562 26.335 33.3472 26.8001C35.3381 27.2638 37.008 28.613 37.8817 30.4609C38.0017 30.7277 37.8857 31.0418 37.6216 31.1672C37.3574 31.2939 37.0406 31.186 36.9085 30.9245C36.1753 29.3745 34.7748 28.2436 33.1049 27.854C31.4348 27.4658 29.6785 27.8608 28.3361 28.927C26.9936 29.9931 26.2104 31.6147 26.2118 33.3293C26.2118 36.0251 25.7859 44.8485 20.2809 45.8568C20.2512 45.8649 20.2202 45.8703 20.1892 45.873L20.1887 45.8728Z"
fill="#24221E"
/>
<path
d="M26.5075 49.7252C26.2514 49.7265 26.029 49.5459 25.9778 49.2952C25.9265 49.0432 26.0586 48.7911 26.2945 48.69C28.2274 47.8786 29.8166 46.4188 30.7883 44.5627C30.9312 44.3012 31.2587 44.2055 31.5202 44.3484C31.7817 44.4913 31.8774 44.8188 31.7345 45.0803C30.6454 47.1534 28.8675 48.7804 26.7069 49.6821C26.6436 49.709 26.5762 49.7239 26.5074 49.7252L26.5075 49.7252Z"
fill="#24221E"
/>
<path
d="M32.592 42.4519C32.4073 42.4533 32.2348 42.3603 32.1351 42.2053C32.0353 42.0489 32.0205 41.8535 32.0987 41.6863C33.2673 39.0525 33.89 36.2098 33.9264 33.3292C33.9264 32.17 32.9856 31.2292 31.8264 31.2292C30.6672 31.2292 29.7264 32.1701 29.7264 33.3292C29.7264 33.6271 29.4851 33.8684 29.1872 33.8684C28.8893 33.8684 28.6481 33.6271 28.6481 33.3292C28.6481 31.5743 30.0715 30.1509 31.8264 30.1509C33.5814 30.1509 35.0048 31.5743 35.0048 33.3292C34.9657 36.3634 34.3106 39.3571 33.08 42.1311C32.9937 42.3239 32.8036 42.4506 32.592 42.4519L32.592 42.4519Z"
fill="#24221E"
/>
<path
d="M22.6094 47.819C22.3115 47.8271 22.0648 47.5912 22.0567 47.2934C22.0486 46.9955 22.2845 46.7488 22.5824 46.7407C26.1893 46.5655 27.6614 41.9557 28.2597 38.0979C28.3055 37.8 28.5846 37.5965 28.8825 37.6423C29.1803 37.6881 29.3839 37.9672 29.338 38.265C28.3918 44.3495 26.0788 47.6546 22.6497 47.8216L22.6094 47.819Z"
fill="#24221E"
/>
<path
d="M47.4108 39.9312C47.2329 39.9312 47.0658 39.8423 46.966 39.6953C46.8649 39.5484 46.8447 39.3611 46.9094 39.1953C47.6359 37.3257 48.0079 35.3361 48.0039 33.3292C48.0039 33.0313 48.2452 32.79 48.5431 32.79C48.8409 32.79 49.0822 33.0313 49.0822 33.3292C49.0849 35.467 48.69 37.5873 47.9149 39.5807C47.8368 39.791 47.6359 39.9312 47.4108 39.9312L47.4108 39.9312Z"
fill="#24221E"
/>
</g>
<defs>
<clipPath id="clip0_662_1567">
<rect
width="34.5064"
height="34.5064"
fill="white"
transform="translate(14.5759 16.0759)"
/>
</clipPath>
</defs>
</svg>
);
}

14
components/icons/index.js Normal file
View File

@ -0,0 +1,14 @@
export { default as Arvo } from "./arvo";
export { default as Azimuth } from "./azimuth";
export { default as Comms } from "./comms";
export { default as Cryptography } from "./cryptography";
export { default as Database } from "./database";
export { default as Distribution } from "./distribution";
export { default as Functional } from "./functional";
export { default as Hoon } from "./hoon";
export { default as Identity } from "./identity";
export { default as Interface } from "./interface";
export { default as MintFiller } from "./mintfiller";
export { default as Nock } from "./nock";
export { default as Peer } from "./peer";
export { default as Vere } from "./vere";

View File

@ -0,0 +1,91 @@
export default function Interface({ className }) {
return (
<svg
width="64"
height="65"
viewBox="0 0 64 65"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<rect x="0.5" y="1" width="63" height="63" rx="9.5" stroke="#24221E" />
<rect x="14.5" y="17" width="35" height="7" rx="3.5" stroke="#24221E" />
<line
x1="14.5"
y1="31"
x2="46.5"
y2="31"
stroke="black"
stroke-linecap="round"
/>
<line
x1="14.5"
y1="41"
x2="46.5"
y2="41"
stroke="black"
stroke-linecap="round"
/>
<line
x1="14.5"
y1="51"
x2="46.5"
y2="51"
stroke="black"
stroke-linecap="round"
/>
<line
x1="14.5"
y1="46"
x2="41.5"
y2="46"
stroke="black"
stroke-linecap="round"
/>
<line
x1="14.5"
y1="36"
x2="33.5"
y2="36"
stroke="black"
stroke-linecap="round"
/>
<line
x1="14.5"
y1="56"
x2="33.5"
y2="56"
stroke="black"
stroke-linecap="round"
/>
<line
x1="0.666504"
y1="9.12134"
x2="63.3332"
y2="9.12134"
stroke="#24221E"
/>
<ellipse
cx="7.99984"
cy="5.0606"
rx="1.33333"
ry="1.30303"
fill="#24221E"
/>
<ellipse
cx="12.6666"
cy="5.0606"
rx="1.33333"
ry="1.30303"
fill="#24221E"
/>
<ellipse
cx="17.3332"
cy="5.0606"
rx="1.33333"
ry="1.30303"
fill="#24221E"
/>
</svg>
);
}

View File

@ -0,0 +1,13 @@
export default function MintFiller({ className }) {
return (
<svg
width="83"
height="80"
viewBox="0 0 83 80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="0.314575" width="81.7853" height="80" rx="10" fill="#A4D9BF" />
</svg>
);
}

54
components/icons/nock.js Normal file
View File

@ -0,0 +1,54 @@
export default function Nock() {
return (
<svg
width="80"
height="80"
viewBox="0 0 80 80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="15.5" cy="27.725" r="2" stroke="black" />
<circle cx="15.5" cy="35.95" r="1.5" fill="black" />
<circle cx="15.5" cy="44.175" r="2" stroke="black" />
<circle cx="15.5" cy="52.4" r="1.5" fill="black" />
<circle cx="15.5" cy="60.625" r="1.5" fill="black" />
<circle cx="23.7251" cy="19.5" r="1.5" fill="black" />
<circle cx="23.7251" cy="27.725" r="2" stroke="black" />
<circle cx="23.7251" cy="35.95" r="1.5" fill="black" />
<circle cx="23.7251" cy="44.175" r="1.5" fill="black" />
<circle cx="23.7251" cy="52.4" r="1.5" fill="black" />
<circle cx="23.7251" cy="60.625" r="2" stroke="black" />
<circle cx="31.95" cy="19.5" r="1.5" fill="black" />
<circle cx="31.95" cy="27.725" r="1.5" fill="black" />
<circle cx="31.95" cy="35.95" r="1.5" fill="black" />
<circle cx="31.95" cy="44.175" r="2" stroke="black" />
<circle cx="31.95" cy="52.4" r="1.5" fill="black" />
<circle cx="31.95" cy="60.625" r="1.5" fill="black" />
<circle cx="40.175" cy="19.5" r="2" stroke="black" />
<circle cx="40.175" cy="27.725" r="1.5" fill="black" />
<circle cx="40.175" cy="35.95" r="1.5" fill="black" />
<circle cx="40.175" cy="44.175" r="1.5" fill="black" />
<circle cx="40.175" cy="52.4" r="2" stroke="black" />
<circle cx="40.175" cy="60.625" r="1.5" fill="black" />
<circle cx="48.3999" cy="19.5" r="1.5" fill="black" />
<circle cx="48.3999" cy="27.725" r="2" stroke="black" />
<circle cx="48.3999" cy="35.95" r="1.5" fill="black" />
<circle cx="48.3999" cy="44.175" r="1.5" fill="black" />
<circle cx="48.3999" cy="52.4" r="1.5" fill="black" />
<circle cx="48.3999" cy="60.625" r="1.5" fill="black" />
<circle cx="56.625" cy="19.5" r="1.5" fill="black" />
<circle cx="56.625" cy="27.725" r="2" stroke="black" />
<circle cx="56.625" cy="35.95" r="1.5" fill="black" />
<circle cx="56.625" cy="44.175" r="1.5" fill="black" />
<circle cx="56.625" cy="52.4" r="1.5" fill="black" />
<circle cx="56.625" cy="60.625" r="2" stroke="black" />
<circle cx="64.8501" cy="19.5" r="2" stroke="black" />
<circle cx="64.8501" cy="27.725" r="1.5" fill="black" />
<circle cx="64.8501" cy="35.95" r="2" stroke="black" />
<circle cx="64.8501" cy="44.175" r="2" stroke="black" />
<circle cx="64.8501" cy="52.4" r="1.5" fill="black" />
<circle cx="64.8501" cy="60.625" r="1.5" fill="black" />
<circle cx="15.5" cy="19.5" r="1.5" fill="black" />
</svg>
);
}

77
components/icons/peer.js Normal file
View File

@ -0,0 +1,77 @@
export default function Peer({ className }) {
return (
<svg
width="64"
height="65"
viewBox="0 0 64 65"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<rect
x="6"
y="6.5"
width="15.6154"
height="15.6154"
rx="3.5"
stroke="#24221E"
/>
<rect
x="6"
y="25.1923"
width="15.6154"
height="15.6154"
rx="3.5"
stroke="#24221E"
/>
<rect
x="6"
y="43.8846"
width="15.6154"
height="15.6154"
rx="3.5"
stroke="#24221E"
/>
<rect
x="24.6923"
y="6.5"
width="15.6154"
height="15.6154"
rx="3.5"
stroke="#24221E"
/>
<rect
x="24.6923"
y="25.1923"
width="15.6154"
height="15.6154"
rx="3.5"
stroke="#24221E"
/>
<rect
x="24.6923"
y="43.8846"
width="15.6154"
height="15.6154"
rx="3.5"
stroke="#24221E"
/>
<rect
x="43.3846"
y="6.5"
width="15.6154"
height="15.6154"
rx="3.5"
stroke="#24221E"
/>
<rect
x="43.3846"
y="25.1923"
width="15.6154"
height="15.6154"
rx="3.5"
stroke="#24221E"
/>
</svg>
);
}

104
components/icons/vere.js Normal file
View File

@ -0,0 +1,104 @@
export default function Vere() {
return (
<svg
width="80"
height="80"
viewBox="0 0 80 80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="22.7571"
y="22.7571"
width="34.4857"
height="34.4857"
rx="0.5"
stroke="black"
/>
<circle cx="28.4286" cy="28.4286" r="1.54286" fill="black" />
<circle cx="51.5714" cy="28.4286" r="1.54286" fill="black" />
<circle cx="51.5714" cy="51.5714" r="1.54286" fill="black" />
<circle cx="28.4286" cy="51.5714" r="1.54286" fill="black" />
<path
d="M24.9569 20.7143C25.5602 20.7143 26.1141 20.2418 26.1141 19.6252V17.1166C26.1141 16.7861 25.9633 16.5023 25.7111 16.3115V13.7098C25.7111 13.3305 25.3599 13 24.9569 13C24.5539 13 24.2028 13.3305 24.2028 13.7098V16.3625C23.9506 16.551 23.7998 16.835 23.7998 17.1677V19.674C23.8493 20.2419 24.3536 20.7143 24.957 20.7143H24.9569Z"
fill="black"
/>
<path
d="M32.6713 20.7143C33.2746 20.7143 33.8284 20.2418 33.8284 19.6252V17.1166C33.8284 16.7861 33.6776 16.5023 33.4254 16.3115V13.7098C33.4254 13.3305 33.0743 13 32.6713 13C32.2683 13 31.9172 13.3305 31.9172 13.7098V16.3625C31.665 16.551 31.5142 16.835 31.5142 17.1677V19.674C31.5637 20.2419 32.068 20.7143 32.6713 20.7143H32.6713Z"
fill="black"
/>
<path
d="M40.3856 20.7143C40.989 20.7143 41.5428 20.2418 41.5428 19.6252V17.1166C41.5428 16.7861 41.392 16.5023 41.1398 16.3115V13.7098C41.1398 13.3305 40.7887 13 40.3857 13C39.9827 13 39.6315 13.3305 39.6315 13.7098V16.3625C39.3793 16.551 39.2285 16.835 39.2285 17.1677V19.674C39.278 20.2419 39.7823 20.7143 40.3857 20.7143H40.3856Z"
fill="black"
/>
<path
d="M48.0998 20.7143C48.7031 20.7143 49.2569 20.2418 49.2569 19.6252V17.1166C49.2569 16.7861 49.1061 16.5023 48.8539 16.3115V13.7098C48.8539 13.3305 48.5028 13 48.0998 13C47.6968 13 47.3456 13.3305 47.3456 13.7098V16.3625C47.0935 16.551 46.9426 16.835 46.9426 17.1677V19.674C46.9921 20.2419 47.4965 20.7143 48.0998 20.7143H48.0998Z"
fill="black"
/>
<path
d="M55.8141 20.7143C56.4174 20.7143 56.9713 20.2418 56.9713 19.6252V17.1166C56.9713 16.7861 56.8204 16.5023 56.5683 16.3115V13.7098C56.5683 13.3305 56.2171 13 55.8141 13C55.4111 13 55.06 13.3305 55.06 13.7098V16.3625C54.8078 16.551 54.657 16.835 54.657 17.1677V19.674C54.7065 20.2419 55.2108 20.7143 55.8141 20.7143H55.8141Z"
fill="black"
/>
<path
d="M55.8143 59.2856C55.211 59.2856 54.6572 59.758 54.6572 60.3747L54.6572 62.8832C54.6572 63.2137 54.808 63.4976 55.0601 63.6884L55.0601 66.2901C55.0601 66.6694 55.4113 66.9999 55.8143 66.9999C56.2173 66.9999 56.5684 66.6694 56.5684 66.2901L56.5684 63.6374C56.8206 63.4488 56.9714 63.1649 56.9714 62.8322L56.9714 60.3258C56.9219 59.758 56.4176 59.2856 55.8143 59.2856L55.8143 59.2856Z"
fill="black"
/>
<path
d="M48.1 59.2856C47.4966 59.2856 46.9428 59.758 46.9428 60.3747L46.9428 62.8832C46.9428 63.2137 47.0936 63.4976 47.3458 63.6884L47.3458 66.2901C47.3458 66.6694 47.6969 66.9999 48.0999 66.9999C48.5029 66.9999 48.8541 66.6694 48.8541 66.2901L48.8541 63.6374C49.1063 63.4488 49.2571 63.1649 49.2571 62.8322L49.2571 60.3258C49.2076 59.758 48.7033 59.2856 48.0999 59.2856L48.1 59.2856Z"
fill="black"
/>
<path
d="M40.3856 59.2856C39.7823 59.2856 39.2284 59.758 39.2284 60.3747L39.2284 62.8832C39.2284 63.2137 39.3793 63.4976 39.6314 63.6884L39.6314 66.2901C39.6314 66.6694 39.9826 66.9999 40.3856 66.9999C40.7886 66.9999 41.1397 66.6694 41.1397 66.2901L41.1397 63.6374C41.3919 63.4488 41.5427 63.1649 41.5427 62.8322L41.5427 60.3258C41.4932 59.758 40.9889 59.2856 40.3856 59.2856L40.3856 59.2856Z"
fill="black"
/>
<path
d="M32.6715 59.2856C32.0682 59.2856 31.5143 59.758 31.5143 60.3747L31.5143 62.8832C31.5143 63.2137 31.6652 63.4976 31.9173 63.6884L31.9173 66.2901C31.9173 66.6694 32.2685 66.9999 32.6715 66.9999C33.0745 66.9999 33.4256 66.6694 33.4256 66.2901L33.4256 63.6374C33.6778 63.4488 33.8286 63.1649 33.8286 62.8322L33.8286 60.3258C33.7791 59.758 33.2748 59.2856 32.6715 59.2856L32.6715 59.2856Z"
fill="black"
/>
<path
d="M24.9571 59.2856C24.3538 59.2856 23.8 59.758 23.8 60.3747L23.8 62.8832C23.8 63.2137 23.9508 63.4976 24.203 63.6884L24.203 66.2901C24.203 66.6694 24.5541 66.9999 24.9571 66.9999C25.3601 66.9999 25.7113 66.6694 25.7113 66.2901L25.7113 63.6374C25.9634 63.4488 26.1143 63.1649 26.1143 62.8322L26.1143 60.3258C26.0648 59.758 25.5604 59.2856 24.9571 59.2856L24.9571 59.2856Z"
fill="black"
/>
<path
d="M59.2857 24.957C59.2857 25.5604 59.7582 26.1142 60.3748 26.1142L62.8834 26.1142C63.2139 26.1142 63.4977 25.9634 63.6885 25.7112L66.2902 25.7112C66.6695 25.7112 67 25.3601 67 24.9571C67 24.5541 66.6695 24.2029 66.2902 24.2029L63.6375 24.2029C63.449 23.9508 63.165 23.7999 62.8323 23.7999L60.326 23.7999C59.7581 23.8494 59.2857 24.3538 59.2857 24.9571L59.2857 24.957Z"
fill="black"
/>
<path
d="M59.2857 32.6713C59.2857 33.2746 59.7582 33.8284 60.3748 33.8284L62.8834 33.8284C63.2139 33.8284 63.4977 33.6776 63.6885 33.4254L66.2902 33.4254C66.6695 33.4254 67 33.0743 67 32.6713C67 32.2683 66.6695 31.9172 66.2902 31.9172L63.6375 31.9172C63.449 31.665 63.165 31.5142 62.8323 31.5142L60.326 31.5142C59.7581 31.5637 59.2857 32.068 59.2857 32.6713L59.2857 32.6713Z"
fill="black"
/>
<path
d="M59.2857 40.3856C59.2857 40.989 59.7582 41.5428 60.3748 41.5428L62.8834 41.5428C63.2139 41.5428 63.4977 41.392 63.6885 41.1398L66.2902 41.1398C66.6695 41.1398 67 40.7887 67 40.3857C67 39.9827 66.6695 39.6315 66.2902 39.6315L63.6375 39.6315C63.449 39.3793 63.165 39.2285 62.8323 39.2285L60.326 39.2285C59.7581 39.278 59.2857 39.7823 59.2857 40.3857L59.2857 40.3856Z"
fill="black"
/>
<path
d="M59.2857 48.0999C59.2857 48.7032 59.7582 49.257 60.3748 49.257L62.8834 49.257C63.2139 49.257 63.4977 49.1062 63.6885 48.854L66.2902 48.854C66.6695 48.854 67 48.5029 67 48.0999C67 47.6969 66.6695 47.3457 66.2902 47.3457L63.6375 47.3457C63.449 47.0936 63.165 46.9427 62.8323 46.9427L60.326 46.9427C59.7581 46.9922 59.2857 47.4966 59.2857 48.0999L59.2857 48.0999Z"
fill="black"
/>
<path
d="M59.2857 55.8141C59.2857 56.4174 59.7582 56.9713 60.3748 56.9713L62.8834 56.9713C63.2139 56.9713 63.4977 56.8204 63.6885 56.5683L66.2902 56.5683C66.6695 56.5683 67 56.2171 67 55.8141C67 55.4111 66.6695 55.06 66.2902 55.06L63.6375 55.06C63.449 54.8078 63.165 54.657 62.8323 54.657L60.326 54.657C59.7581 54.7065 59.2857 55.2108 59.2857 55.8141L59.2857 55.8141Z"
fill="black"
/>
<path
d="M20.7143 55.8142C20.7143 55.2109 20.2418 54.657 19.6252 54.657L17.1166 54.657C16.7861 54.657 16.5023 54.8079 16.3115 55.06L13.7098 55.06C13.3305 55.06 13 55.4112 13 55.8142C13 56.2172 13.3305 56.5683 13.7098 56.5683L16.3625 56.5683C16.551 56.8205 16.835 56.9713 17.1677 56.9713L19.674 56.9713C20.2419 56.9218 20.7143 56.4175 20.7143 55.8142L20.7143 55.8142Z"
fill="black"
/>
<path
d="M20.7143 48.1C20.7143 47.4966 20.2418 46.9428 19.6252 46.9428L17.1166 46.9428C16.7861 46.9428 16.5023 47.0936 16.3115 47.3458L13.7098 47.3458C13.3305 47.3458 13 47.6969 13 48.0999C13 48.5029 13.3305 48.8541 13.7098 48.8541L16.3625 48.8541C16.551 49.1063 16.835 49.2571 17.1677 49.2571L19.674 49.2571C20.2419 49.2076 20.7143 48.7033 20.7143 48.0999L20.7143 48.1Z"
fill="black"
/>
<path
d="M20.7143 40.3856C20.7143 39.7823 20.2418 39.2284 19.6252 39.2284L17.1166 39.2284C16.7861 39.2284 16.5023 39.3793 16.3115 39.6314L13.7098 39.6314C13.3305 39.6314 13 39.9826 13 40.3856C13 40.7886 13.3305 41.1397 13.7098 41.1397L16.3625 41.1397C16.551 41.3919 16.835 41.5427 17.1677 41.5427L19.674 41.5427C20.2419 41.4932 20.7143 40.9889 20.7143 40.3856L20.7143 40.3856Z"
fill="black"
/>
<path
d="M20.7143 32.6714C20.7143 32.0681 20.2418 31.5142 19.6252 31.5142L17.1166 31.5142C16.7861 31.5142 16.5023 31.665 16.3115 31.9172L13.7098 31.9172C13.3305 31.9172 13 32.2684 13 32.6713C13 33.0743 13.3305 33.4255 13.7098 33.4255L16.3625 33.4255C16.551 33.6777 16.835 33.8285 17.1677 33.8285L19.674 33.8285C20.2419 33.779 20.7143 33.2747 20.7143 32.6713L20.7143 32.6714Z"
fill="black"
/>
<path
d="M20.7143 24.957C20.7143 24.3537 20.2418 23.7999 19.6252 23.7999L17.1166 23.7999C16.7861 23.7999 16.5023 23.9507 16.3115 24.2028L13.7098 24.2028C13.3305 24.2028 13 24.554 13 24.957C13 25.36 13.3305 25.7111 13.7098 25.7111L16.3625 25.7111C16.551 25.9633 16.835 26.1141 17.1677 26.1141L19.674 26.1141C20.2419 26.0646 20.7143 25.5603 20.7143 24.957L20.7143 24.957Z"
fill="black"
/>
</svg>
);
}

View File

@ -0,0 +1,50 @@
+++
title = "App School Live"
date = "2022-06-29"
description = "Were running a cohort class of App School to teach you how to terraform Mars."
[extra]
author = "N E Davis"
ship = "~lagrev-nocfep"
image = "https://media.urbit.org/site/posts/essays/blog-asl-header-iron-hot.png"
+++
![](https://media.urbit.org/site/posts/essays/blog-asl-header-iron-hot.png)
What should you do after Hoon School? Well, presumably you've become interested
in building apps on Urbit, and certainly knowing the language is a prerequisite.
But apps require knowing the platform as a platform—what system services and
affordances exist, how to hook up a front-end user interface, and how to build
apps that interoperate with each other.
The Urbit Foundation offered Hoon School Live as a reboot of previous live Hoon
educational efforts, including Hooniversity and Hoon 101. HSL consisted of
synchronous livestream sessions, homework, and office hours, and concluded with
a competition (on the results of which more soon). 61 participants completed
HSL and received finisher `%gorae`.
In July, the Urbit Foundation will follow up HSL with App School Live, another
synchronous cohort class based on the App School guides (formerly known as the
Gall Guides). We will cover the structure of an Urbit backend app, Urbit OS
system calls, app coordination across multiple ships, how to build a front-end,
and more.
Most—but not all—Urbit app development has focused thus far on social media and
cryptocurrency applications. This makes sense, and it's a strength of the
current Urbit platform primitives. But so much more can be done: games, of
course, but also life management, instrumentation, CLIs, data visualization, and
so much more. With Urbit's peer-to-peer distribution system, there are no
gatekeepers, and anyone can start their own app store.
At the same time as ASL, the Urbit Foundation is holding a hackathon with prizes
to be unveiled at [Assembly Miami](https://assembly.urbit.org). We're excited
to see what you're going to build. If you're not sure how to produce your
vision yet, start with App School Live and then peruse the submissions for
current [grant projects](https://urbit.org/grants) to see how others are
successfully building apps.
If you are interested in participating in App School Live, drop us a line at
[this registration
form](https://docs.google.com/forms/d/e/1FAIpQLSfxAEdcdaLG_yK3RCOCLLScslcyjhBmAN2qUqHLajwSMgT-lw/viewform)
or join us on Mars at ~hiddev-dannut/new-hooniverse. We'll start on Tuesday,
July 12 and run through August 23.

View File

@ -0,0 +1,28 @@
+++
title = "(Re)Launching developers.urbit.org"
date = "2022-06-29"
description = "Weve revamped the documentation and guides."
[extra]
author = "N E Davis"
ship = "~lagrev-nocfep"
image = "https://media.urbit.org/site/posts/essays/blog-relaunching-header-margaret-bourke-white.png"
+++
![](https://media.urbit.org/site/posts/essays/blog-relaunching-header-margaret-bourke-white.png)
The Urbit docs have evolved a lot over the years. Some of this has been due to changes in the audience—for instance, early docs were voiced towards core kernel developers. Other changes have been made as we've rolled out new features, such as the 2020 Ford Fusion project or the more recent Azimuth L2 implementation. As the docs have continued to evolve, we've worked hard on putting out quality tutorials and curriculum that make Urbit easier for new developers and old alike to figure out what they need to take their projects forward. We've also seen a growing fractionation of the Urbit userbase into regular users, power users, and developers, all of whom have slightly different requirements.
We have migrated and reorganized our coursework, our reference documentation, and our developer site to better reflect the needs of these communities. The upgraded `developers.urbit.org` now houses the one-stop-shop for programmers, allowing `urbit.org` and `operators.urbit.org` to better speak to particular user groups besides Hoon programmers.
This site now contains:
1. [Overview](/overview) outlines the Urbit project from a technical standpoint.
2. [Guides](/guides) span the written Hoon School and App School (formerly Gall Guides), as well as a new feature we're introducing called Lightning Tutorials, which will discuss how to quickly implement basic app functionality.
3. [Reference](/reference) hosts the language and standard library documentation: Nock, Hoon runes and standard library, Arvo, Azimuth, and the runtime.
4. [Courses](/courses) are landing pages for Hoon School Live and App School Live, the cohort-based versions of Hoon School and App School.
5. [Community](/community) includes developer events and support.
There are often growing pains with new websites, so if you find a dead link or are certain there was a reference for something and you just can't put your finger on it anymore, drop us a line on Github or Urbit.
_Header image by [Margaret Bourke-White](https://www.moma.org/collection/works/46907)._

View File

@ -0,0 +1,52 @@
+++
title = "Layer 2 Guides"
date = "2022-02-14"
description = "Urbits Layer 2 system is live and operational."
[extra]
author = "Reid Scoggin"
ship = "~sitful-hatred"
image = "https://media.urbit.org/site/posts/essays/l2-blogpost.png"
+++
Urbits [Layer 2](/reference/azimuth/l2/layer2) system, naive rollups, allows planets to be spawned at substantially lower cost. This system operates in parallel to Layer 1 Azimuth, but introduces some new concepts and differences that are important to understand. Read on for a high-level survey, and check the star and planet guides linked at the bottom for details and instructions.
After a year of development and testing, **Urbits Layer 2 system is live and operational**. Star and planet operators can now take advantage of subsidized Azimuth operations. If you operate a star, you can distribute planets cheaply or for free; if youve been waiting to buy an ID due to transaction fees, you will find planets are available much more cheaply.
Layer 2 (“L2”) introduces some changes that are important to understand, whether youre new to the network or not. Stars and planet operators that are considering migrating to L2 have different **trade-offs to weigh** before they make a decision.
### Background
Urbit can be broadly divided into two parts: an operating system, Arvo, and the identity layer, Azimuth. These two systems are interlocked. Arvo uses Azimuth to verify that you own an address, which maps to your name on the network. [Azimuth](/reference/glossary/azimuth) is a public key infrastructure, implemented as contracts on the Ethereum blockchain. Modifications to the ownership or properties of an identitys keys are recorded on Ethereum. Azimuth faithfully serves its purpose as an authoritative, trustless registry of ownership, but it inherits both the strengths and disadvantages of the ETH ecosystem.
Due to ETHs value and popularity, performing transactions directly on the Ethereum blockchain (Layer 1) has become prohibitively expensive for many simple operations. The Ethereum smart contracts that control the logic of Azimuth were developed in a time when ETH was not worth as much. The soaring value of ETH means that the gas fee to spawn or modify a planet routinely costs more than the planet itself. Azimuth is not unique in this regard. All projects built on Ethereum have had to deal with this issue in one form or another. Fortunately, Urbit engineers have come up with a solution for those seeking to get onto the network, albeit with several important trade-offs.
### Naive rollups
Layer 2 refers to technologies built on top of blockchains to enable scaling. [Rollups](https://vitalik.ca/general/2021/01/05/rollup.html) are an Ethereum ecosystem Layer 2 innovation that reduces costs by moving computation off-chain. _Naive rollups_ are a bespoke technology developed for Urbit that augment the original Azimuth contracts. For technical details, you can review the [excellent summary](https://urbit.org/blog/rollups) by `~datnut-pollen`, or the [original proposal](https://groups.google.com/a/urbit.org/g/dev/c/p6rP_WsxLS0) by `~wicdev-wisryt`. In brief: rather than using the Ethereum network to perform the computation associated with PKI modifications (Layer 1), the computation is performed on the Urbit network itself, with the results published to the blockchain by nodes called rollers. Signed data resulting from the combined transactions is posted on a regular basis to the main blockchain by the rollers. Due to this batched, off-chain computation, fees are roughly **65-100x cheaper** than Layer 1 operations.
### What everyone should know
Whether youre new to the network or a longtime participant, you should gain familiarity with the new changes. There are a few things that everyone should know:
- All ships spawned before now have been on Layer 1 those ships have the option of migrating to Layer 2, or remaining on Layer 1.
- **You dont have to do anything**. Migrating is opt-in, and a Layer 1 ship will continue to have full functionality on the network.
- Layer 2 lets you perform **Azimuth transactions cheaply or for free**. If you operate a star, you can spawn planets for free with Tlons roller. Planets can make use of Tlons roller for actions like factory resets and transferring ownership and proxy addresses for free.
- **Migrating is one-way**. If you migrate to Layer 2, there is not currently an option to reverse your decision.
- **Migrating does not change which address owns a point**. After migrating, you will still log into Bridge with the same keys.
### Layer 2 guides
Theres lots more to learn about the new solution. These updates apply to the software on your ship, the Azimuth PKI, and new features and major updates to Bridge. You can learn more in the following guides, with in-depth background and illustrated walkthroughs for common tasks:
[Layer 2 for stars](https://operators.urbit.org/guides/layer-2-for-stars) Its particularly important for star operators to understand the pros and cons of migrating. This guide will explain the technical background, trade-offs, and how to use Layer 2 on Bridge.
- Stars can migrate their spawn proxy to Layer 2 to spawn up to six planets per week for free using Tlons roller.
- Stars can migrate their ownership key to Layer 2 to perform all Azimuth operations on Layer 2, including planet spawning, factory resets, and point adoption.
- Migrating is currently a one-way process and cannot be reversed.
- Layer 2 stars cannot be wrapped as $WSTR tokens, or interact with any other Layer 1 tools or contracts (e.g. MetaMask, OpenSea).
[Layer 2 for planets](https://urbit.org/getting-started/layer-2-for-planets) Just bought a planet and want to know what all of this means for you? Wondering whether you should migrate your Layer 1 planet? Look here for guidance.
- Planets spawned by a Layer 2 star will spawn on Layer 2.
- Migration is one-way; if your planet is on Layer 2, there is no way to migrate it to Layer 1.
- Planets on Layer 2 can take advantage of subsidized, free Azimuth transactions using Tlons roller.
- Planets on Layer 2 cannot currently interact with Layer 1 tools or contracts like MetaMask or OpenSea.

View File

@ -1,49 +0,0 @@
---
title: Community
directory:
- patp: ~wolref-podlex
job: Executive Director, Urbit Foundation
roles: Partnerships, grants, employment opportunities, technical projects, Urbit Foundation
- patp: ~ravmel-ropdyl
job: CEO, Tlon
roles: Partnerships, employment opportunities, geodesic domes, future of Urbit
- patp: ~timluc-miptev
job: Technical Director, Urbit Foundation
roles: Userspace development, grants
- patp: ~lagrev-nocfep
job: Technical Director, Urbit Foundation
roles: Urbit education
- patp: ~poldec-tonteg
job: Program Manager, The Combine
roles: Urbit entrepreneurship, organizational grants
- patp: ~tirwyd-sarmes
job: Content Manager, Urbit Foundation
roles: Content grants, newsletter, blog
- patp: ~taller-ravnut
job: Developer Relations, Urbit Foundation
roles: Grants, meetups, gifts, Developer Calls
- patp: ~tinnus-napbus
job: Technical writer and mentor, Urbit Foundation
roles: Developer documentation, developer support
- patp: ~wicdev-wisryt
job: CTO, Tlon
roles: Infrastructure development, Arvo, apprenticeships
- patp: ~master-morzod
job: Senior infrastructure engineer, Tlon
roles: Infrastructure development, Vere, Arvo
- patp: ~rovnys-ricfer
job: Senior infrastructure engineer, Tlon
roles: Infrastructure development, Arvo, software distribution
---
The developer community is a combination of top-down stewardship from the Urbit Foundation and Tlon, as well as organic, bottom-up coordination from unaffiliated enthusiasts.
The Forge is a community-run Landscape group (`~middev/the-forge`) focused on all things related to Urbit development. Its members consist of enthusiasts, Tlon and Foundation developers, and grant workers. Its also a great repository of community-compiled knowledge of common issues and development techniques.
The Urbit Foundation makes direct investments of address space into the community as a means of improving the network. The Foundation also maintains developer documentation, runs Developer Calls, and acts as the hub of the Urbit ecosystem. You can find us hanging around in the Foundation group in Landscape (`~wolref-podlex/foundation`).
Tlon is the primary developer of Urbit itself and the foremost product developer within the Urbit ecosystem. Theyve been developing Urbit since 2013 and are naturally a great source of information. They maintain the most popular Landscape groups, Urbit Community (`~bitbet-bolbel/urbit-community`) and Urbit Index (`~bollug-worlus/urbit-index`).
## Directory
Our community is comprised of individuals, all of whom happen to be pretty friendly. Heres a list of prominent figures youll likely encounter that are open to being contacted:

View File

@ -0,0 +1,6 @@
+++
name = "N.E. Davis"
patp = "~lagrev-nocfep"
org = "Urbit Foundation"
title = "Technical Director, Urbit Foundation"
+++

View File

@ -0,0 +1,6 @@
+++
name = "Joe Bryan"
patp = "~master-morzod"
org = "Tlon"
title = "Senior infrastructure engineer, Tlon"
+++

View File

@ -0,0 +1,6 @@
+++
name = "~palfun-foslup"
patp = "~palfun-foslup"
org = "Tlon"
title = "Software engineer, Tlon"
+++

View File

@ -0,0 +1,6 @@
+++
name = "Anthony Arroyo"
patp = "~poldec-tonteg"
org = "Urbit Foundation"
title = "Director of the Combine, Urbit Foundation"
+++

View File

@ -0,0 +1,6 @@
+++
name = "Galen Wolfe-Pauly"
patp = "~ravmel-ropdyl"
org = "Tlon"
title = "CEO, Tlon Corporation"
+++

View File

@ -0,0 +1,6 @@
+++
name = "Ted Blackman"
patp = "~rovnys-ricfer"
org = "Tlon"
title = "Senior infrastructure engineer, Tlon"
+++

View File

@ -0,0 +1,6 @@
+++
name = "Kenny Rowe"
patp = "~sicdev-pilnup"
org = "Additional Community Members"
title = "Representative, Dalten Collective"
+++

View File

@ -0,0 +1,6 @@
+++
name = "Mike Osbourne"
patp = "~taller-ravnut"
org = "Urbit Foundation"
title = "Developer Relations, Urbit Foundation"
+++

View File

@ -0,0 +1,6 @@
+++
name = "~timluc-miptev"
patp = "~timluc-miptev"
org = "Additional Community Members"
title = "CEO of Uqbar"
+++

View File

@ -0,0 +1,6 @@
+++
name = "Philip Monk"
patp = "~wicdev-wisryt"
org = "Tlon"
title = "CTO, Urbit Foundation"
+++

View File

@ -0,0 +1,6 @@
+++
name = "Josh Lehman"
patp = "~wolref-podlex"
org = "Urbit Foundation"
title = "Executive Director, Urbit Foundation"
+++

View File

@ -0,0 +1,23 @@
+++
title = "Developer Call: Urbits improved Terminal Stack"
starts = "2022-06-16T09:00:00"
ends = "2022-06-16T10:30:00"
timezone = "America/Los_Angeles"
location = "Online Event"
image = ""
registration_url = "https://www.meetup.com/urbit-sf/events/286426447/"
description = ""
dark = false
youtube = "E-6E-l1SxFw"
[[hosts]]
name = "Neal Davis"
patp = "~lagrev-nocfep"
[[guests]]
patp = "~palfun-foslup"
+++
This time Tlons ~palfun-foslup will join Neal Davis to discuss Urbits current terminal stack and the improvements he is building for it. Multiple sessions and apparently a mouse has been spotted in the upcoming release. All in all some very exciting terminal improvements are coming up shortly. For the long-term ~palfun-foslup is aiming towards native UI which he will describe as well. Be sure to join the hangout after the Developer Call to ask ~palfun-foslup everything you want to know.
~palfun-foslup has been part of Tlon for over 4 years and works in the Infrastructure team.

View File

@ -1,23 +1,26 @@
---
title: Opportunities
---
+++
title = "Opportunities"
+++
## Grants
### Grants
Urbit is a community project. While anyone can contribute, we help focus development and reward exceptional contribution through our grants program.
Contributors of all types have access to a wide variety of resources while working on projects, including a supportive team at urbit.org, Tlon developers, and community mentors.
<a href="https://urbit.org/grants#find-a-grant" className="button-lg bg-green-400 text-white text-ui inline-flex">See Current Grants</a>
{% button label="See Current Grants" link="https://urbit.org/grants" color="bg-green-400 text-white" %}
{% /button %}
## Jobs
### Jobs
Many companies in the Urbit ecosystem hire from the community. Core contributors are often considered first when making a decision to hire. Here are a few companies that may be hiring:
- [dcSpark](https://careers.dcspark.io/)
- [Tlon Corporation](https://tlon.io/careers/)
- [Tirrel Corporation](https://tirrel.io/)
- [Tlon Corporation](https://tlon.io)
- [Tirrel Corporation](https://tirrel.io)
- [Holium](https://holium.com)
## The Combine
Check out jobs channel in the [Foundation](https://urbit.org/groups/~wolref-podlex/foundation) group for recent posts from the community.
The Combine invests in teams and individuals who are working on Urbit projects and are in the fundraising stage. Learn more at [The Combine](https://www.the-combine.org/)
### The Combine
The Combine invests in teams and individuals who are working on Urbit projects and are in the fundraising stage. Learn more at [The Combine](https://the-combine.org).

View File

@ -0,0 +1,18 @@
+++
title = "Support"
+++
### Email
Questions? Email [support@urbit.org](mailto:support@urbit.org) and well get back as soon as possible.
### GitHub
Have a particular technical situation or noticed a bug? Open an issue on [GitHub](https://github.com/urbit/urbit).
### Urbit Groups
There are a number of Urbit groups where friendly people can answer questions.
- [Urbit Community](https://urbit.org/groups/~bitbet-bolbel/urbit-community) - Help and Development channels
- [New Hooniverse](https://urbit.org/groups/~hiddev-dannut/new-hooniverse) - For Hoon and Gall questions

36
content/courses/asl.md Normal file
View File

@ -0,0 +1,36 @@
+++
title = "App School Live"
weight = 6
+++
Periodically, the Urbit Foundation will offer synchronous cohort classes which
cover the [App School](/guides/core/hoon-school) curriculum.
App School focuses on how to build a backend Gall agent, then on connecting it
to a React-based front-end. When you're done, you'll be able to produce and
distribute your own Urbit apps.
If you prefer to learn as part of a group with a hands-on instructor, regular
exercises and discussions, and a completion certification, then App School Live
will be a good fit for you.
The next cohort for App School Live will run from Tuesday July 12 until Tuesday
August 23. The live session will meet at 8:30 p.m. Central Daylight Time, but
recordings will be posted.
- [Complete this form](https://forms.gle/3c8xBubvSiQfj7Tr6) to get on our
mailing list about that opportunity.
Until then, feel free to work through the [App School
docs](/guides/core/app-school).
## What will you learn?
- `lesson0`. A Simple Agent
- `lesson1`. Arvo Services
- `lesson2`. Agents
- `lesson3`. Passing Data
- `lesson4`. React Front-End
- `lesson5`. Threads
- `lesson6`. Production Apps

34
content/courses/hsl.md Normal file
View File

@ -0,0 +1,34 @@
+++
title = "Hoon School Live"
weight = 3
+++
Periodically, the Urbit Foundation has offered synchronous cohort classes which
cover the [Hoon School](/guides/core/hoon-school) curriculum.
If you prefer to learn as part of a group with a hands-on instructor, regular
exercises and discussions, and a completion certification, then Hoon School Live
will be a good fit for you. We seek to offer it a few times a year; at the
current time, we anticipate that we will next offer it in the fall after
[Assembly 2022](https://assembly.urbit.org).
- [Complete this form](https://forms.gle/V1jKKEqcsayDx6Hi6) to get on our
mailing list about that opportunity.
Until then, feel free to work through the [Hoon School
docs](/guides/core/hoon-school).
## What will you learn?
- `basics-1`. Introduction to Computing (optional)
- `basics0`. Setting Up Urbit (optional)
- `lesson1`. Hoon Syntax
- `lesson2`. Hoon Conventions
- `lesson3`. Making Decisions
- `lesson4`. Repeating Yourself
- `lesson5`. Subject-Oriented Programming
- `lesson6`. Molding Values
- `lesson7`. The Standard Library
- `lesson8`. Managing State
- `lesson9`. Production Code

3
content/guides/_index.md Normal file
View File

@ -0,0 +1,3 @@
+++
title = "Guides"
+++

View File

@ -0,0 +1,5 @@
+++
title = "Additional Guides"
weight = 30
type = "tab"
+++

View File

@ -0,0 +1,79 @@
+++
title = "Aqua Tests"
weight = 5
+++
# Concepts
Aqua (short for "aquarium", alluding to the idea that you're running
multiple ships in a safe, artificial environment and watching them
carefully) is an app that lets you run one or more virtual ships from
within a single host.
pH is a library of functions designed to make it easy to write
integration tests using Aqua.
# First test
To run your first pH test, run the following commands:
```
|start %aqua
:aqua +solid
-ph-add
```
This will start Aqua, compile a new kernel for it, and then compile and
run /ted/ph/add.hoon. Here are the contents of that file:
```
/- spider
/+ *ph-io
=, strand=strand:spider
^- thread:spider
|= args=vase
=/ m (strand ,vase)
;< ~ bind:m start-simple
;< ~ bind:m (raw-ship ~bud ~)
;< ~ bind:m (dojo ~bud "[%test-result (add 2 3)]")
;< ~ bind:m (wait-for-output ~bud "[%test-result 5]")
;< ~ bind:m end-simple
(pure:m *vase)
```
There's a few lines of boilerplate, with three important lines defining
the test.
```
;< ~ bind:m (raw-ship ~bud ~)
;< ~ bind:m (dojo ~bud "[%test-result (add 2 3)]")
;< ~ bind:m (wait-for-output ~bud "[%test-result 5]")
```
We boot a ship with `+raw-ship`. In this case the ship we are booting will be `~bud`. These ships exist in a virtual environment so you could use any valid `@p`.
Next we enter some commands with `+dojo`, and then we wait until we get a line that includes some expected output. Each of these commands we need to specify the ship we want to run on.
Many tests can be created with nothing more than these simple tools.
Try starting two ships and having one send a `|hi` to the other, and
check that it arrives.
Many more complex tests can be created, including file changes, personal
breaches, mock http clients or servers, or anything you can imagine.
Check out `/lib/ph/io.hoon` for other available functions, and look at
other tests in `/ted/ph/` for inspiration.
# Reference
Aqua has the following commands:
`:aqua +solid` Compiles a "pill" (kernel) for the guest ships and loads it into Aqua.
`:aqua [%swap-files ~]` modifies the pill to use the files you have in
your filesystem without rebuilding the whole pill. For example, if you
change an app and you want to test the new version, you must install it
in the pill. This command will do that.
`:aqua [%swap-vanes ~[%a]]` Modifies the pill to load a new version of a
vane (`%a` == Ames in this example, but it can be any list of vanes).
This is faster than running `:aqua +solid`.

View File

@ -0,0 +1,439 @@
+++
title = "CLI apps"
weight = 10
+++
## Introduction
In this walkthrough we will go in-depth on how to build command line interface (CLI)
applications in Urbit using the `shoe` library.
There are three CLI apps that currently ship with urbit - `%dojo`, `%chat-cli`,
and `%shoe`. You should be familiar with the former two, the latter is an
example app that shows off how the `shoe` library works that we will be looking
at closely. These are all Gall apps, and their source can be found in the `app/`
folder of your `%base` desk.
In [the `shoe` library](#the-shoe-library) we take a closer look at the `shoe` library and its
cores and how they are utilized in CLI apps. Then in [the `sole`
library](#the-sole-library) we look at what `shoe` effects ultimately break down
into. Finally in [`%shoe` app walkthrough](#shoe-app-walkthrough) we explore
the functionality of the `%shoe` app and then go through the code line-by-line.
This tutorial can be
considered to be an application equivalent of the [Hoon school
lesson](/guides/core/hoon-school/P-stdlib#ask-generators) on `sole` and `%ask`
generators, which only covers the bare minimum necessary to write generators
that take user input.
## The `shoe` library {% #the-shoe-library %}
Here we describe how sessions are identified, the specialized `card`s that Gall agents
with the `shoe` library are able to utilize, and the different cores of `/lib/shoe.hoon` and their purpose.
### Session identifiers
An app using the `shoe` library will automatically track sessions by their `sole-id=@ta`.
These are opaque identifiers generated by the connecting client. An app using the `shoe`
library may be connected to by a local or remote ship in order to send commands,
and each of these connections is assigned a unique `@ta` that identifies the
ship and which session on that ship if there are multiple.
### `%shoe` `card`s
Gall agents with the `shoe` library are able to utilize `%shoe` `card`s. These
additions to the standard set of `cards` have the following shape:
```hoon
[%shoe sole-ids=(list @ta) effect=shoe-effect]`
```
`sole-ids` is the `list` of session ids that the following `effect` is
emitted to. An empty `sole-ids` sends the effect to all connected sessions.
`shoe-effect`s, for now, are always of the shape `[%sole effect=sole-effect]`, where
`sole-effect`s are basic console events such as displaying text, changing the
prompt, beeping, etc. These are described in the section on the [`sole` library](#the-sole-library).
For example, a `%shoe` `card` that causes all connected sessions to beep would
be `[%shoe ~ %sole %bel ~]`.
### `shoe` core
An iron (contravariant) door that defines an interface for Gall agents utilizing
the `shoe` library. Use this core whenever you want to receive input from the user and run a command. The input will get
put through the parser (`+command-parser`) and results in a noun of
`command-type` that the underlying application specifies, which shoe then feeds back into the underlying app as an `+on-command` callback.
In addition to the ten arms that all Gall core apps possess, `+shoe` defines and expects a few
more, tailored to common CLI logic. Thus you will need to wrap the `shoe:shoe` core using the `agent:shoe` function to obtain a standard
10-arm Gall agent core. See the [shoe example app
walkthrough](#shoe-example-app-walkthrough) for how to do this.
The additional arms are described below. The Hoon code shows their expected type signature. As we'll see [later](#shoe-app-walkthrough), the `command-type` can differ per application. Note also that most of these take a session identifier as an argument. This lets applications provide different users (at potentially different "places" within the application) with different affordances.
#### `+command-parser`
```hoon
++ command-parser
|~ sole-id=@ta
|~(nail *(like [? command-type]))
```
Input parser for a specific command-line session. Will be run on whatever the user tries to input into the command prompt, and won't let them type anything that doesn't parse. If the head of the result is true,
instantly run the command. If it's false, require the user to press return.
#### `+tab-list`
```hoon
++ tab-list
|~ sole-id=@ta
*(list (option:auto tank))
```
Autocomplete options for the command-line session (to match `+command-parser`).
#### `+on-command`
```hoon
++ on-command
|~ [sole-id=@ta command=command-type]
*(quip card _this)
```
Called when a valid command is run.
#### `+can-connect`
```hoon
++ can-connect
|~ sole-id=@ta
*?
```
Called to determine whether a session may be opened or connected to. For example, you
may only want the local ship to be able to connect.
#### `+on-connect`
```hoon
++ on-connect
|~ sole-id=@ta
*(quip card _^|(..on-init))
```
Called when a session is opened or connected to.
#### `+on-disconnect`
```hoon
++ on-disconnect
|~ sole-id=@ta
*(quip card _^|(..on-init))
```
Called when a previously made session gets disconnected from.
### `default` core
This core contains the bare minimum implementation of the additional `shoe` arms
beyond the 10 standard Gall app ams. It is used
analogously to how the `default-agent` core is used for regular Gall apps.
### `agent` core
This is a function for wrapping a `shoe` core, which has too many
arms to be a valid Gall agent core. This turns it into a standard Gall agent core by
integrating the additional arms into the standard ones.
## The `sole` library {% #the-sole-library %}
`shoe` apps may create specialized `card`s of the `[%shoe (list @ta) shoe-effect]` shape, where `shoe-effect` currently just wrap `sole-effect`s, i.e. instructions for displaying text and producing other effects in the console.
The list of possible `sole-effects` can be found in `/sur/sole.hoon`. A few
commonly used ones are as follows.
- `[%txt tape]` is used to display a line of text.
- `[%bel ~]` is used to emit a beep.
- `[%pro sole-prompt]` is used to set the prompt.
- `[%mor (list sole-effect)]` is used to emit multiple effects.
For example, a `sole-effect` that beeps and displays `This is some text.` would
be structured as
```hoon
[%mor [%txt "This is some text."] [%bel ~] ~]
```
## `%shoe` app walkthrough {% #shoe-app-walkthrough %}
Here we explore the capabilities of the `%shoe` example app and then go through
the code, explaining what each line does.
### Playing with `%shoe`
First let's test the functionality of `%shoe` so we know what we're getting
into.
Start two fake ships, one named `~zod` and the other can have any name - we will
go with `~nus`. Fake ships run locally are able to see each other, and our
intention is to connect their `%shoe` apps.
On each fake ship start `%shoe` by entering `|start %shoe` into dojo. This will
automatically
change the prompt to `~zod:shoe>` and `~nus:shoe>`. Type `demo` and watch the following appear:
```
~zod ran the command
~zod:shoe>
```
`~zod ran the command` should be displayed in bold green text, signifying that
the command originated locally.
Now we will connect the sessions. Switch `~zod` back to dojo with `Ctrl-X` and enter `|link ~nus %shoe`. If this succeeds you will see the following.
```
>=
; ~nus is your neighbor
[linked to [p=~nus q=%shoe]]
```
Now `~zod` will have two `%shoe` sessions running - one local one on `~zod` and
one remote one on `~nus`, which you can access by pressing `Ctrl-X` until you see
`~nus:shoe>` from `~zod`'s console. On the other hand, you should not see
`~zod:shoe>` on `~nus`'s side, since you have not connected `~nus` to `~zod`'s
`%shoe` app. When you enter `demo` from `~nus:shoe>` on
`~zod`'s console you will again see `~zod ran the command`, but this time it
should be in the ordinary font used by the console, signifying that the command
is originating from a remote session. Contrast this with entering `demo` from
`~nus:shoe>` in `~nus`'s console, which will display `~nus ran the command` in
bold green text.
Now try to link to `~zod`'s `%shoe` session from `~nus` by switching to the dojo
on `~nus` and entering `|link ~zod %shoe`. You should see
```
>=
[unlinked from [p=~zod q=%shoe]]
```
and if you press `Ctrl-X` you will not get a `~zod:shoe>` prompt. This is
because the example app is set up to always allow `~zod` to connect (as well as
subject moons if the ship happens to be a planet) but not `~nus`, so this
message means that `~nus` failed to connect to `~zod`'s `%shoe` session.
### `%shoe`'s code
```hoon
:: shoe: example usage of /lib/shoe
::
:: the app supports one command: "demo".
:: running this command renders some text on all sole clients.
::
/+ shoe, verb, dbug, default-agent
```
`/+` is the Ford rune which imports libraries from the `/lib` directory into
the subject.
- `shoe` is the `shoe` library.
- `verb` is a library used to print what a Gall agent is doing.
- `dbug` is a library of debugging tools.
- `default-agent` contains a Gall agent core with minimal implementations of
required Gall arms.
```hoon
|%
+$ state-0 [%0 ~]
+$ command ~
::
+$ card card:shoe
--
```
The types used by the app.
`state-0` stores the state of the app, which is null as there is no state to
keep track of. It is good practice to include a version number anyways,
in case the app is made stateful at a later time.
`command` is typically a set of tagged union types that represent the possible
commands that can be entered by the user. Since this app only supports one
command, it is unnecessary for it to have any associated data, thus the command
is represented by `~`.
In a non-trivial context, a `command` is commonly given by `[%name data]`, where `%name` is the identifier for the type of command and `data` is
a type or list of types that contain data needed to execute the command. See
`app/chat-cli.hoon` for examples of commands, such as `[%say letter:store]` and
`[%delete path]`. This is not required though, and you could use something like
`[chat-room=@t =action]`.
`card` is either an ordinary Gall agent `card` or a `%shoe` `card`, which takes
the shape `[%shoe sole-ids=(list @ta) effect=shoe-effect]`. A `%shoe` `card` is
sent to all `sole`s listed in `sole-ids`, imaking them run the `sole-effect`
specified by `effect` (i.e. printing some text). Here we can
reference `card:shoe` because of `/+ shoe` at the beginning of the app.
```hoon
=| state-0
=* state -
::
```
Add the bunt value of `state-0` to the head of the subject, then give it the
macro `state`. The `-` here is a lark expression referring to the head of the
subject. This allows us to use `state` to refer to the state elsewhere in the
code no matter what version we're using, while also getting direct access to the
contents of `state` (if it had any).
```hoon
%+ verb |
%- agent:dbug
^- agent:gall
%- (agent:shoe command)
^- (shoe:shoe command)
```
The casts here are just reminders of what is being produced. So let's focus on
what the `%` runes are doing, from bottom to top. We call `(agent:shoe command)`
on what follows (i.e. the rest of the app),
producing a standard Gall agent core. Then we call wrap the Gall agent core with
`agent:dbug`, endowing it with additional arms useful for debugging, and then
wrap again with `verb`.
```hoon
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
des ~(. (default:shoe this command) bowl)
```
This is boilerplate Gall agent core code. We set `this` to be a macro for the
subject, which is the Gall agent core itself. We set `def` and `des` to be
macros for initialized `default-agent` and `default:shoe` doors respectively.
Next we implement all of the arms required for a `shoe` agent. Starting with
the standard Gall arms:
```hoon
++ on-init on-init:def
++ on-save !>(state)
++ on-load
|= old=vase
^- (quip card _this)
[~ this]
::
++ on-poke on-poke:def
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
```
These are minimalist Gall app arm implementations using the default behavior found in `def`.
Here begins the implementation of the additional arms required by the
`(shoe:shoe command)` interface.
```hoon
++ command-parser
|= sole-id=@ta
^+ |~(nail *(like [? command]))
(cold [& ~] (jest 'demo'))
```
`+command-parser` is of central importance - it is what is used to parse user
input and transform it into `command`s for the app to execute. Writing a proper
command parser requires understanding of the Hoon parsing functions found in the
standard library. How to do so may be found in the [parsing tutorial](/guides/additional/hoon/parsing). For now, it is sufficient to know that this arm matches the text "demo" and
produces a `[? command]`-shaped noun in response. Note how the `&` signifies that the command will be run as soon as it has been entered, without waiting for the user to press return.
```hoon
++ tab-list
|= sole-id=@ta
^- (list [@t tank])
:~ ['demo' leaf+"run example command"]
==
```
`+tab-list` is pretty much plug-n-play. For each command you want to be tab
completed, add an entry to the `list` begun by `:~` of the form `[%command leaf+"description"]`. Now whenever the user types a partial command and presses
tab, the console will display the list of commmands that match the partial
command as well as the descriptions given here.
Thus here we have that starting to type `demo` and pressing tab will result in
the following output in the console:
```
demo run example command
~zod:shoe> demo
```
with the remainder of `demo` now added to the input line.
Next we have `+on-command`, which is called whenever `+command-parser`
recognizes that `demo` has been entered by a user.
```hoon
++ on-command
|= [sole-id=@ta =command]
^- (quip card _this)
```
This is a gate that takes in the `sole-id` corresponding to the session and the
`command` noun parsed by `+command-parser` and returns a `list` of `card`s and
`_this`, which is our shoe agent core including its state.
```hoon
=- [[%shoe ~ %sole -]~ this]
```
This creates a cell of a `%shoe` card that triggers a `sole-effect` given by the head of
the subject `-`, then the Gall agent core `this` - i.e. the return result of
this gate. The use of the `=-` rune means that what follows this
expression is actually run first, which puts the desired `sole-effect` into the
head of the subject.
```hoon
=/ =tape "{(scow %p src.bowl)} ran the command"
```
We define the `tape` that we want to be printed.
```hoon
?. =(src our):bowl
[%txt tape]
[%klr [[`%br ~ `%g] [(crip tape)]~]~]
```
We cannot just produce the `tape` we want printed, - it needs to fit the
`sole-effect` type. This tells us that if the
origin of the command is not our ship to just print it normally with the `%txt`
`sole-effect`. Otherwise we use `%klr`, which prints it stylistically (here it
makes the text green and bold).
The following allows either `~zod`, or the host ship and its moons, to connect to
this app's command line interface using `|link`.
```hoon
++ can-connect
|= sole-id=@ta
^- ?
?| =(~zod src.bowl)
(team:title [our src]:bowl)
==
```
We use the minimal implementations for the final two `shoe` arms, since we don't
want to do anything special when users connect or disconnect.
```hoon
++ on-connect on-connect:des
++ on-disconnect on-disconnect:des
--
```
This concludes our review of the code of the `%shoe` app. To continue learning
how to build your own CLI app, we recommend checking out `/app/chat-cli.hoon`.

View File

@ -0,0 +1,697 @@
+++
title = "HTTP API"
weight = 15
+++
[Eyre] is the web-server [vane] (kernel module) of [Arvo], an Urbit ship's
kernel and operating system. In this guide, we'll look at interacting with a
ship through Eyre's web API using the [@urbit/http-api] Javascript module.
## Background
Before we dig into the details of Eyre's API, we'll first give a quick run-down
of some concepts in Arvo. Eyre's API is a fairly thin overlay on some of Arvo's
internal systems, so there's some basic things to understand.
### Clay
[Clay] is the filesystem vane. It's typed, and it's revision-controlled in a
similar way to git. Clay is comprised of a number of [desk]s, which are a bit
like git repositories. Each app on your ship's home screen corresponds to a desk
in Clay. That desk contains the source code and resources for that app.
#### Marks
Most of Clay's workings aren't relevant to front-end development, but there's
one important concept to understand: [mark]s. Clay is a typed filesystem, and
marks are the filetypes. There's a mark for `.hoon` files, a mark for `.txt`
files, and so on. The mark specifies the hoon datatype for those files, and it
also specifies conversion methods between different marks. Marks aren't just
used for files saved in Clay, but also for data that goes to and from the web
through Eyre.
When you send a [poke](#pokes) or run a [thread](#threads) through Eyre, it will
look at the mark specified (for example `graph-action-3`, `settings-event`, etc)
and use the corresponding mark file in the desk to convert the given JSON to
that datatype before passing it to the target [agent](#gall-agents) or thread.
The same conversion will happen in reverse for responses.
Note that Eyre makes a best effort attempt to convert data to and from JSON. If
the marks in question do not contain appropriate JSON conversion functions, it
will fail. Not all [scry endpoints](#scry-endpoints), [subscription
paths](#subscriptions), pokes, etc, are intended to be used from a front-end, so
not all use marks which can be converted to and from JSON (the `noun` mark for
example). If documentation is available for the agent or thread in question, it
should note in some fashion whether it can take or produce JSON. The majority of
things you'll want to interact with through Eyre will work with JSON.
### Gall agents
An _agent_ is a userspace application managed by the [Gall] vane. A desk may
contain multiple agents that do different things. The Groups app, for example,
has `graph-store`, `group-store`, `contact-store`, and others in its desk.
Agents are the main thing you'll interact with through Eyre. They have a simple
interface with three main parts:
| Interface | Description |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pokes | One-off message to an agent. Pokes often represent actions, commands, requests, etc. |
| Subscriptions | An agent may have a number of different paths to which you may subscribe. If a subscription request is accepted, you'll continue to receive update/events the agent sends out on that path until you either cancel the subscription or are kicked by the agent. |
| Scries | Scries are one-off requests for data. Like subscriptions, they are organized by path. Scry requests will be fulfilled immediately. Scries are "read-only", they cannot alter the state of an agent. |
#### Pokes
Pokes are single, stand-alone messages to agents. Pokes are how you send data
and requests to agents. Agents will send back either a positive acknowledgement
(ack) or a negative acknowledgement (nack). The agent can't send actual data
back in the acks. If they have any response to give back, it will be sent out to
subscribers on a subscription path instead.
The pokes an agent accepts will be defined in the `on-poke` section of its
source in the desk's `/app` directory, and typically in its type definition file
in the `/sur` directory too. If the agent has documentation, the data structures
and marks of its pokes should be listed there.
`http-api` includes a `poke` function which allows you to perform pokes through
Eyre, and is [detailed below](#poke).
#### Subscriptions
Agents define subscription paths which you can subscribe to through Eyre. A path
might be simple and fixed like `/foo/bar`, or it might have variable elements
where you can specify a date, ship, or what have you. Each agent will define its
subscription paths in the `on-watch` section of its source, and if it has
documentation the paths should also be listed there.
You can subscribe by sending a watch request to the agent with the desired path
specifed. The agent will apply some logic (such as checking permissions) to the
request and then ack or nack it. If acked, you'll be subscribed and begin
receiving any updates the agent sends out on that path. What you'll receive on a
given path depends entirely on the agent.
Agents can kick subscribers, and you can also unsubscribe at any time.
`http-api` includes a `subscribe` function which allows you to perform pokes through
Eyre, and is [detailed below](#subscribe).
#### Scry Endpoints
Pokes and subscriptions can modify the state of the agent. A third kind of
interaction called a _scry_ does not - it simply retrieves data from the agent
without any side-effects. Agents can define _scry endpoints_ which (like
subscriptions) are paths. A scry to one of these endpoints will retrieve some
data (as determined by the agent's design) from the agent's state. Like
subscription paths, scry paths can be simple like `/foo/bar` or contain variable
elements. Unlike subscriptions, a scry is a one-off request and the data will
come back immediately.
Scry endpoints are defined in the `on-peek` section of an agent's source, and
will likely be listed in the agent's documentation if it has any. Scry endpoints
will often be written with a leading letter like `/x/foo/bar`. All scries
through Eyre are `x` scries, so that letter (called a `care`) needn't be
specified.
`http-api` includes a `scry` function which allows you to perform scries through
Eyre, and is [detailed below](#scry).
### Threads
A thread is a monadic function in Arvo that takes arguments and produces a
result. Threads are conceptually similar to Javascript promises - they can
perform a series of asynchronous I/O operations and may succeed or fail. Threads
are often used to handle complex I/O operations for agents. Threads live in the
`/ted` directory of a desk.
`http-api` includes a `thread` function which allows you to run threads through
Eyre, and is [detailed below](#thread).
## `http-api` basics
The `http-api` module provides convenient functions to interact with a ship
through Eyre. In this section we'll discuss the basics of how it works.
### Importing the module
The `http-api` module is available in npm as `@urbit/http-api`, and can be
installed with:
```
npm i @urbit/http-api
```
Once installed, you can import it into your app with:
```
import Urbit from '@urbit/http-api';
```
Note that the examples in this guide are simple HTML documents with vanilla
Javascript in `<script>` tags, so they use [unpkg.com](https://unpkg.com) to
import `@urbit/http-api`. This is not typical, and is just done here for
purposes of simplicity.
### `Urbit`
All functionality is contained within the `Urbit` class. There are two ways to
instantiate it, depending on whether your web app is served directly from the
ship or whether it's served externally. The reason for the difference is that
you require a session cookie to talk to the ship. If your app is served from the
ship, the user will already have a cookie. If it's not, they'll need to
authenticate, which is [detailed separately below](#authentication).
In the case of a front-end served from the ship, the `Urbit` class contains a `constructor` which takes three arguments:
| Argument | Type | Description | Example |
| -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| `url` | `string` | The host of the ship without the protocol. This is mandatory but would typically be left blank as requests will still work if they're root-relative paths. | `"example.com"`, `"localhost:8080"`, `""` |
| `code` | `string` | The web login code of the ship. You'll typically use this constructor when it's served directly from the ship and therefore you'll already have a session cookie, so this field is optional and is typically omitted or left empty. | `""`, `"lidlut-tabwed-pillex-ridrup"` |
| `desk` | `string` | The desk on which you want to run threads. This field is optional and is only used if you want to run threads, no other function uses it. If you're not running threads, you can omit it or leave it empty. | `"landscape"`, `""` |
To create an `Urbit` instance, you can simply do:
```javascript
const api = new Urbit("");
```
If you want to specify a desk, you can do:
```javascript
const api = new Urbit("", "", "landscape");
```
### `/session.js`
Most functions of the `Urbit` class need to know the ship name or they will
fail. This is given explicitly with the external authentication method [detailed
below](#authentication), but not when using the `Urbit` constructor in a web app
served directly from the ship. Rather than having the user manually enter it,
Urbit ships serve a JS library at `/session.js` that contains the following:
```javascript
window.ship = "zod";
```
`"zod"` will be replaced with the actual name of the ship in question. You can
include this file like:
```
<script src="/session.js"></script>
```
Then you need to set the `ship` field in the `Urbit` object. You would typically
do it immediately after instantiating it:
```javascript
const api = new Urbit("");
api.ship = window.ship;
```
### Channels
With the exception of scries and threads, all communication with Eyre happens
through its channel system.
The `Urbit` object will generate a random channel ID that looks like
`1646295453-e1bdfd`, and use a path of `/~/channel/1646295453-e1bdfd` for
channel communications. Pokes will and subscription requests will be sent to
that channel. Responses and subscription updates will be sent out to you on that
channel too.
Eyre sends out updates and responses on an [SSE] (Server Sent Event) stream for
the channel. The `Urbit` object handles this internally with an `eventSource`
object, but you won't deal with it directly. Eyre requires all events it sends
out be acknowledged by the client, and will eventually close the channel if
enough unacknowledged events accumulate. The `Urbit` object handles event
acknowledgement automatically, so you don't need to worry about this.
Eyre automatically creates a channel when a poke or subscription request is
first sent to `/~/channel/[a new channel ID]`. If your web app is served
external to the ship and you use the `authenticate` function [described
below](#authentication), the function will automatically send a poke and open
the channel. If your web app is served directly from the ship and you use
`Urbit` class constructor, it won't open the channel right away. Instead, the
channel will be opened whenever you first send a poke or subscription request.
### Connection state
`Urbit` class includes three optional callbacks that fire when the SSE
connection state changes:
| Callback | Description |
| --------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `Urbit.onOpen` | Called when an SSE channel connection is successfully established. |
| `Urbit.onRetry` | This is called whenever a reconnection attempt is made due to an interruption, for example if there are network problems. |
| `Urbit.onError` | This is called when there is an unrecoverable error, for example after enough reconnection attemps have failed. |
As mentioned in the previous section, typically a channel will be opened and an
SSE connection established after you first poke the ship or make a subscription
request. If successful, whatever function you provided to `onOpen` will be
called. If at some point the connection is interrupted, a reconnection attempt will be made three times:
1. Instantly.
2. 750ms after the first.
3. 3000ms after the second.
Each attempt will call the function you provided to `onRetry`, if any. If all
three reconnection attempts failed, or if a fatal error occurred, the function
you provided `onError` will be called with an `Error` object containing an error
message as its argument.
How you use these (if you do at all) is up to you and depends on the nature of
your app. If you want to try reconnecting when `onError` fires, note that Eyre
will delete a channel if it's had no messages from the client in the last 12
hours. The timeout is reset whenever it receives a message, including the acks
that are automatically sent by the `Urbit` object in response to subscription
updates.
If you don't want to account for the possibility of the channel having been
deleted in your app's logic, you can also just call the [`reset()`](#reset)
function before you try reconnecting and consequently open a brand new channel.
### Authentication
**If your front-end is served directly from the Urbit ship, this step can be
skipped.** If your web app is served externally to the ship, you must
authenticate and obtain a session cookie before commencing communications with
the ship.
The `Urbit` class in `http-api` includes an `authenticate` function which does
the following:
1. Login to the user's ship with their `code` and obtain a session cookie.
2. Generate a random channel ID for the connection.
3. Poke the user's ship and print "opening airlock" in the dojo to initialize
the channel.
The `authenticate` function takes four arguments in an object: `ship`, `url`,
`code` and `verbose`:
| Argument | Type | Description | Example |
| --------- | --------- | -------------------------------------------------------------------------------------- | --------------------------------- |
| `ship` | `string` | The ship ID (`@p`) without the leading `~`. | `"sampel-palnet"` or `"zod"` |
| `url` | `string` | The base URL for the ship without the protocol. | `localhost:8080` or `example.com` |
| `code` | `string` | The user's web login code. | `"lidlut-tabwed-pillex-ridrup"` |
| `verbose` | `boolean` | Whether to log details to the console. This field is optional and defaults to `false`. | `true` |
This function returns a promise that if successful, produces an `Urbit` object
which can then be used for communications with the ship.
**Note:** An Urbit ship will deny CORS requests from external URLs by default.
In order to allow CORS for an external web app, the user will need to run the
following in the dojo (replacing `https://example.com` with the address in
question):
```
|cors-approve 'https://example.com'
```
#### Example
Here's a simple example that will run `authenticate` for a fakezod when the
_Connect_ button is pressed:
```
<html>
<head>
<script src="https://unpkg.com/@urbit/http-api"></script>
</head>
<body>
<button id="start" type="button" onClick="connect()" >Connect</button>
</body>
<script>
async function connect() {
window.api = await UrbitHttpApi.Urbit.authenticate({
ship: "zod",
url: "localhost:8080",
code: "lidlut-tabwed-pillex-ridrup",
verbose: true
});
document.body.innerHTML = "Connected!";
};
</script>
</html>
```
`window.api` can then be used for further communications.
## Functions
The various functions you can use in the `Urbit` object are described below.
### Poke
For poking a ship, the `Urbit` class in `http-api` includes a `poke` function.
The `poke` function takes six arguments in a object:
| Argument | Type | Description | Example |
| ----------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
| `app` | `string` | The Gall agent to poke. | `"graph-store"` |
| `mark` | `string` | The mark of the data to poke the agent with. | `"graph-update-3"` |
| `json` | any JSON | The data to poke the agent with. | `{foo: "bar", baz: 42}` |
| `ship` | `string` | The Urbit ID (`@p`) of the ship without the `~`. This may be ommitted if it's already been set for the whole `Urbit` object. | `"sampel-palnet"` |
| `onSuccess` | A function. | This is called if the poke succeeded (the ship ack'd the poke). | `someFunction` |
| `onError` | A function. | This is called if the poke failed (the ship nack'd the poke). | `anotherFunction` |
#### Example
This example lets you "hi" your ship with a given message, which will be printed
in the terminal. It pokes the `hood` agent (the system agent) with a `helm-hi`
mark. The `json` argument just contains the message in a string. If for some
reason the `hood` agent rejects the poke, "Poke failed!" will be displayed on
the page.
```
<html>
<head>
<script src="https://unpkg.com/@urbit/http-api"></script>
<script src="/session.js"></script>
</head>
<body>
<input id="msg" type="text" placeholder="Message for ship" />
<button id="submit" type="button" onClick="doPoke()" >Submit</button>
<p id="err"></p>
</body>
<script>
document.getElementById("msg")
.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
document.getElementById("submit").click();
}
})
const api = new UrbitHttpApi.Urbit("");
api.ship = window.ship;
function doPoke() {
const msg = document.getElementById("msg").value;
api.poke({
app: "hood",
mark: "helm-hi",
json: msg,
onSuccess: success,
onError: error
});
}
function success() {
document.getElementById("msg").value = "";
document.getElementById("err").innerHTML = "";
}
function error() {
document.getElementById("msg").value = "";
document.getElementById("err").innerHTML = "Poke failed!";
}
</script>
</html>
```
### Subscribe and Unsubscribe {% #subscribe %}
For subscribing to a particular path in an agent, the `Urbit` class in `http-api` includes a `subscribe` function.
The `subscribe` function takes six arguments in a object:
| Argument | Type | Description | Example |
| -------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| `app` | `string` | The Gall agent to which you'll subscribe. | `"graph-store"` |
| `path` | `string` | The subscription path. | `"/updates"` |
| `ship` | `string` | The Urbit ID (`@p`) of the ship without the `~`. This may be ommitted if it's already been set for the whole `Urbit` object. | `"sampel-palnet"` |
| `err` | A function. | This is called if the subscription request fails. | `someFunction` |
| `event` | A function. | This is the function to handle each update you receive for this subscription. The function's argument is the update's JSON data. | `anotherFunction` |
| `quit` | A function. | This is called if you are kicked from the subscription. | `yetAnotherFunction` |
The `subscribe` function returns a subscription ID, which is just a number. This
ID can be used to unsubscribe down the line.
If the subscription request is successful, you'll continue to receive updates
until you either unsubscribe or are kicked by the agent. You may subscribe to
multiple different agents and subscription paths by calling the `subscribe`
function for each one.
If you wish to unsubscribe from a particular subscription, the `Urbit` class in
`http-api` includes an `unsubscribe` function. This function just takes a single
argument: the subscription ID number of an existing subscription. Once
unsubscribed, you'll stop receiving updates for the specified subscription.
#### Example
This example will, upon clicking the "Subscribe" button, subscribe to the
`/updates` path of the `graph-store` agent, and print the raw JSON of each
update it receives. You can test it by doing something like posting a message in
a chat. You can also unsubscribe by clicking the "Unsubscribe" button.
````
<html>
<head>
<script src="https://unpkg.com/@urbit/http-api"></script>
<script src="/session.js"></script>
</head>
<body>
<button id="toggle" type="button" onClick="doSub()" >Subscribe</button>
<pre id="event">```
</body>
<script>
const api = new UrbitHttpApi.Urbit("");
api.ship = window.ship;
const toggle = document.getElementById("toggle");
const event = document.getElementById("event");
function doSub() {
window.id = api.subscribe({
app: "graph-store",
path: "/updates",
event: printEvent,
quit: doSub,
err: subFail
});
toggle.innerHTML = "Unsubscribe";
toggle.onclick = doUnsub;
event.innerHTML = "Awaiting event";
};
function doUnsub() {
api.unsubscribe(window.id);
toggle.innerHTML = "Subscribe";
toggle.onclick = doSub;
event.innerHTML = "Subscription closed";
};
function printEvent(update) {
event.innerHTML = JSON.stringify(update, null, 2);
};
function subFail() {
event.innerHTML = "Subscription failed!";
};
</script>
</html>
````
### Subscribe Once
The `subscribeOnce` function in the `Urbit` class of `http-api` is a variation
on the ordinary [`subscribe`](#subscribe) function. Rather than keeping the
subscription going and receiving an arbitrary number of updates, instead it
waits to receive a single update and then closes the subscription. This is
useful if, for example, you send a poke and just want a response to that one
poke.
The `subscribeOnce` function also takes an optional `timeout` argument, which
specifies the number of milliseconds to wait for an update before closing the
subscription. If omitted, `subscribeOnce` will wait indefinitely.
`subscribeOnce` takes three arguments (these can't be in an object like most
other `Urbit` functions):
| Argument | Type | Description | Example |
| --------- | -------- | ------------------------------------------------------------------------------------------------------------------------ | --------------- |
| `app` | `string` | The Gall agent to which you'll subscribe. | `"graph-store"` |
| `path` | `string` | The subscription path. | `"/updates"` |
| `timeout` | `number` | The number of milliseconds to wait for an update before closing the subscription. If omitted, it will wait indefinitely. | `5000` |
`subscribeOnce` returns a Promise. If successful, the Promise produces the JSON
data of the update it received. If it failed due to either timing out or getting
kicked from the subscription, it will return an error message of either
`"timeout"` or `"quit"`.
#### Example
Here's a variation on the ordinary [subscribe](#subscribe) function. Rather than
printing every `graph-store` update it receives, it will instead just print the
first one it receives and close the subscription. Additionally, it sets a five
second timeout, and prints an error message if it times out.
````
<html>
<head>
<script src="https://unpkg.com/@urbit/http-api"></script>
<script src="/session.js"></script>
</head>
<body>
<button type="button" onClick="doSub()" >Subscribe Once</button>
<pre id="event">```
</body>
<script>
const api = new UrbitHttpApi.Urbit("");
api.ship = window.ship;
const event = document.getElementById("event");
function doSub() {
event.innerHTML = "";
api.subscribeOnce("graph-store","/updates",5000)
.then(printEvent, noEvent);
};
function printEvent(update) {
event.innerHTML = JSON.stringify(update, null, 2);
};
function noEvent(error) {
event.innerHTML = error;
};
</script>
</html>
````
### Scries
To scry agents on the ship, the `Urbit` class in `http-api` includes a `scry` function.
The `scry` function takes two arguments in a object:
| Argument | Type | Description | Example |
| -------- | -------- | ---------------------------------- | --------------- |
| `app` | `string` | The agent to scry. | `"graph-store"` |
| `path` | `string` | The path to scry, sans the `care`. | `"/keys"` |
The `scry` function returns a promise that, if successful, contains the
requested data as JSON. If the scry failed, for example due to a non-existent
scry endpoint, connection problem, or mark conversion failure, the promise will
fail.
#### Example
Upon pressing the "Scry Graphs" button, this example will scry the `graph-store`
agent's `/keys` endpoint for the list of graphs, and print the resulting JSON
data.
````
<html>
<head>
<script src="https://unpkg.com/@urbit/http-api"></script>
<script src="/session.js"></script>
</head>
<body>
<button id="scry" type="button" onClick="doScry()" >Scry Graphs</button>
<pre id="result">```
</body>
<script>
const api = new UrbitHttpApi.Urbit("");
api.ship = window.ship;
async function doScry() {
var groups = await api.scry({app: "graph-store", path: "/keys"});
document.getElementById("result")
.innerHTML = JSON.stringify(groups, null, 2);
}
</script>
</html>
````
### Thread
To run a thread, the `Urbit` class in `http-api` includes a `thread` function.
The `thread` function takes five arguments in an object:
| Argument | Type | Description | Example |
| ------------ | -------- | ------------------------------------------------------------------------------------------------------------- | ----------------------- |
| `inputMark` | `string` | The mark to convert your JSON data to before giving it to the thread as its argument. | `"graph-view-action"` |
| `outputMark` | `string` | The result of the thread should be converted to this mark before being converted to JSON and returned to you. | `"tang"` |
| `threadName` | `string` | The name of the thread to run. | `"graph-eval"` |
| `body` | any JSON | The data to give to the thread as its argument. | `{foo: "bar", baz: 42}` |
| `desk` | `string` | The desk in which the thread resides. This may be ommitted if previously set for the whole `Urbit` object. | `"landscape"` |
The `thread` function will produce a promise that, if successful, contains the
JSON result of the thread. If the thread failed, a connection error occurred, or
mark conversion failed, the promise will fail.
#### Example
This example takes a hoon expression (such as `(add 1 1)`), evalutes it with the
`graph-eval` thread in the `landscape` desk, and prints the result.
````
<html>
<head>
<script src="https://unpkg.com/@urbit/http-api"></script>
<script src="/session.js"></script>
</head>
<body>
<input id="hoon" type="text" placeholder="Hoon to evaluate" />
<button id="submit" type="button" onClick="runThread()" >Submit</button>
<pre id="expr">```
<pre id="result">```
</body>
<script>
document.getElementById("hoon")
.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
document.getElementById("submit").click();
}
});
const hoon = document.getElementById("hoon");
const expr = document.getElementById("expr");
const result = document.getElementById("result");
const desk = "landscape";
const api = new UrbitHttpApi.Urbit("", "", desk);
api.ship = window.ship;
async function runThread() {
const threadResult = await api.thread({
inputMark: "graph-view-action",
outputMark: "tang",
threadName: "graph-eval",
body: {eval: hoon.value}
});
result.innerHTML = threadResult[0].join("\n");
expr.innerHTML = hoon.value;
hoon.value = "";
};
</script>
</html>
````
### Delete Channel
Rather than just closing individual subscriptions, the entire channel can be
closed with the `delete` function in the `Urbit` class of `http-api`. The
function takes no arguments, and can be called like `api.cancel()` (replacing
`api` with whatever you've named the `Urbit` object you previously instantiated).
When a channel is closed, all subscriptions will be cancelled and all pending
updates will be discarded.
### Reset
An existing instance of `http-api`'s `Urbit` class can be reset with its
`reset` function. This function takes no arguments, and can be called like
`api.reset()` (replacing `api` with whatever you've named the `Urbit` object you
previously instantiated). When called, the channel will be closed and all
subscriptions cancelled. Additionally, all outstanding outbound pokes will be
discarded and a fresh channel ID will be generated.
## Further reading
- [Eyre External API Reference][eyre-ext-ref] - Lower-level documentation of
Eyre's external API.
- [Eyre Guide][eyre-guide] - Lower-level examples of using Eyre's external API
with `curl`.
- [`http-api` on Github][http-api-src] - The source code for `@urbit/http-api`.
## Examples
Here are some examples which make use of `@urbit/http-api` that might be useful
as a reference:
- [Bitcoin Wallet](https://github.com/urbit/urbit/tree/master/pkg/btc-wallet)
- [Web Terminal](https://github.com/urbit/urbit/tree/master/pkg/interface/webterm)
- [UrChatFM](https://github.com/urbit/urbit-webrtc/tree/master/urchatfm)
[eyre-ext-ref]: reference/arvo/eyre/external-api-ref
[eyre-guide]: reference/arvo/eyre/guide
[http-api-src]: https://github.com/urbit/urbit/tree/master/pkg/npm/http-api
[eyre]: /reference/glossary/eyre
[vane]: /reference/glossary/vane
[arvo]: /reference/glossary/arvo
[hoon]: /reference/glossary/hoon
[gall]: /reference/glossary/gall
[clay]: /reference/glossary/clay
[desk]: /reference/glossary/desk
[mark]: /reference/glossary/mark
[sse]: https://html.spec.whatwg.org/#server-sent-events
[@urbit/http-api]: https://github.com/urbit/urbit/tree/master/pkg/npm/http-api

View File

@ -0,0 +1,512 @@
+++
title = "JSON"
weight = 20
+++
If you are working on a Gall agent with any kind of web interface, it's likely you will encounter the problem of converting Hoon data structures to JSON and vice versa. This is what we'll look at in this document.
Urbit represents JSON data with the `$json` structure (defined in `lull.hoon`). You can refer to the [json type](#the-json-type) section below for details.
JSON data on the web is encoded in text, so Urbit has two functions in `zuse.hoon` for dealing with this:
- [`+en-json:html`](/reference/hoon/zuse/2e_2-3#en-jsonhtml) - For printing `$json` to a text-encoded form.
- [`+de-json:html`](/reference/hoon/zuse/2e_2-3#de-jsonhtml) - For parsing text-encoded JSON to a `$json` structure.
You typically want `$json` data converted to some other `noun` structure or vice versa, so Urbit has three collections of functions for this purpose, also in `zuse.hoon`:
- [`+enjs:format`](/reference/hoon/zuse/2d_1-5#enjsformat) - Functions for converting various atoms and structures to `$json`.
- [`+dejs:format`](/reference/hoon/zuse/2d_6#dejsformat) - Many "reparsers" for converting `$json` data to atoms and other structures.
- [`+dejs-soft:format`](/reference/hoon/zuse/2d_7#dejs-softformat) - Largely the same as `+dejs:format` except its reparsers produce `unit`s which are null upon failure rather than simply crashing.
The relationship between these types and functions look like this:
![json diagram](https://media.urbit.org/docs/json-diagram.svg)
Note this diagram is a simplification - the `+dejs:format` and `+enjs:format` collections in particular are tools to be used in writing conversion functions rather than simply being used by themselves, but it demonstrates the basic relationships. Additionally, it is less common to do printing/parsing manually - this would typically be handled automatically by Eyre, though it may be necessary if you're retrieving JSON data via the web client vane Iris.
### In practice
A typical Gall agent will have a number of structures defined in a file in the `/sur` directory. These will define the type of data it expects to be `%poke`ed with, the type of data it will `%give` to subscribers, and the type of data its scry endpoints produce.
If the agent only interacts with other agents inside Urbit, it may just use a `%noun` `mark`. If, however, it needs to talk to a web interface of some kind, it usually must handle `$json` data with a `%json` mark.
Sometimes an agent's interactions with a web interface are totally distinct from its interactions with other agents. If so, the agent could just have separate scry endpoints, poke handlers, etc, that directly deal with `$json` data with a `%json` mark. In such a case, you can include `$json` encoding/decoding functions directly in the agent or associated libraries, using the general techniques demonstrated in the [$json encoding and decoding example](#json-encoding-and-decoding-example) section below.
If, on the other hand, you want a unified interface (whether interacting with a web client or within Urbit), a different approach is necessary. Rather than taking or producing either `%noun` or `%json` marked data, custom `mark` files can be created which specify conversion methods for both `%noun` and `%json` marked data.
With this approach, an agent would take and/or produce data with some `mark` like `%my-custom-mark`. Then, when the agent must interact with a web client, the webserver vane Eyre can automatically convert `%my-custom-mark` to `%json` or vice versa. This way the agent only ever has to handle the `%my-custom-mark` data. This approach is used by `%graph-store` with its `%graph-update-2` mark, for example, and a number of other agents.
For details of creating a `mark` file for this purpose, the [mark file example](#mark-file-example) section below walks through a practical example.
## The `$json` type
Urbit represents JSON data with the `$json` structure (defined in `/sys/lull.hoon`):
```hoon
+$ json :: normal json value
$@ ~ :: null
$% [%a p=(list json)] :: array
[%b p=?] :: boolean
[%o p=(map @t json)] :: object
[%n p=@ta] :: number
[%s p=@t] :: string
== ::
```
The correspondence of `$json` to JSON types is fairly self-evident, but here's a table comparing the two for additional clarity:
| JSON Type | `$json` Type | JSON Example | `$json` Example |
| :-------- | :--------------------- | ------------------------- | :----------------------------------------------------------- |
| Null | `~` | `null` | `~` |
| Boolean | `[%b p=?]` | `true` | `[%b p=%.y]` |
| Number | `[%n p=@ta]` | `123` | `[%n p=~.123]` |
| String | `[%s p=@t]` | `"foo"` | `[%s p='foo']` |
| Array | `[%a p=(list json)]` | `["foo",123]` | `[%a p=~[[%s p='foo'] [%n p=~.123]]]` |
| Object | `[%o p=(map @t json)]` | `{"foo":"xyz","bar":123}` | `[%o p={[p='bar' q=[%n p=~.123]] [p='foo' q=[%s p='xyz']]}]` |
Since the `$json` `%o` object and `%a` array types may themselves contain any `$json`, you can see how JSON structures of arbitrary complexity can be represented. Note the `%n` number type is a `@ta` rather than something like a `@ud` that you might expect. This is because JSON's number type may be either an integer or floating point, so it's left as a `knot` which can then be parsed to a `@ud` or `@rd` with the appropriate [`+dejs:format`](/reference/hoon/zuse/2d_6) function.
## `$json` encoding and decoding example
Let's have a look at a practical example. Here's a core with three arms. It has the structure arm `$user`, and then two more: `+to-js` converts a `$user` structure to `$json`, and `+from-js` does the opposite. Usually we'd define the structure in a separate `/sur` file, but for simplicity it's all in the one core.
#### `json-test.hoon`
```hoon
|%
+$ user
$: username=@t
name=[first=@t mid=@t last=@t]
joined=@da
email=@t
==
++ to-js
|= usr=user
|^ ^- json
%- pairs:enjs:format
:~
['username' s+username.usr]
['name' name]
['joined' (sect:enjs:format joined.usr)]
['email' s+email.usr]
==
++ name
:- %a
:~
[%s first.name.usr]
[%s mid.name.usr]
[%s last.name.usr]
==
--
++ from-js
=, dejs:format
^- $-(json user)
%- ot
:~
[%username so]
[%name (at ~[so so so])]
[%joined du]
[%email so]
==
--
```
**Note**: This example (and a couple of others in this guide) sometimes use a syntax of `foo+bar`. This is just syntactic sugar to tag the head of `bar` with the `term` constant `%foo`, and is equivalent to `[%foo bar]`. Since `json` data is a union with head tags of `%b`, `%n`, `%s`, `%a`, or `%o`, it's sometimes convenient to do `s+'some string'`, `b+&`, etc.
### Try it out
First we'll try using our `$json` encoding/decoding library, and afterwards we'll take a closer look at its construction. To begin, save the code above in `/lib/json-test.hoon` of the `%base` desk on a fake ship and `|commit` it:
```
> |commit %base
>=
+ /~zod/base/5/lib/json-test/hoon
```
Then we need to build it so we can use it. We'll give it a face of `user-lib`:
```
> =user-lib -build-file %/lib/json-test/hoon
```
Let's now create an example of a `$user` structure:
```
> =usr `user:user-lib`['john456' ['John' 'William' 'Smith'] now 'john.smith@example.com']
> usr
[ username='john456'
name=[first='John' mid='William' last='Smith']
joined=~2021.9.12..09.47.58..1b65
email='john.smith@example.com'
]
```
Now we can try calling the `+to-js` function with our data to convert it to `$json`:
```
> =usr-json (to-js:user-lib usr)
> usr-json
[ %o
p
{ [p='email' q=[%s p='john.smith@example.com']]
[p='name' q=[%a p=~[[%s p='John'] [%s p='William'] [%s p='Smith']]]]
[p='username' q=[%s p='john456']]
[p='joined' q=[%n p=~.1631440078]]
}
]
```
Let's also see how that `$json` would look as real JSON encoded in text. We can do that with `+en-json:html`:
```
> (crip (en-json:html (to-js:user-lib usr)))
'{"joined":1631440078,"username":"john456","name":["John","William","Smith"],"email":"john.smith@example.com"}'
```
Finally, let's try converting the `$json` back to a `$user` again with our `+from-js` arm:
```
> (from-js:user-lib usr-json)
[ username='john456'
name=[first='John' mid='William' last='Smith']
joined=~2021.9.12..09.47.58
email='john.smith@example.com'
]
```
### Analysis
#### Converting to `$json`
Here's our arm that converts a `$user` structure to `$json`:
```hoon
++ to-js
|= usr=user
|^ ^- json
%- pairs:enjs:format
:~
['username' s+username.usr]
['name' name]
['joined' (sect:enjs:format joined.usr)]
['email' s+email.usr]
==
++ name
:- %a
:~
[%s first.name.usr]
[%s mid.name.usr]
[%s last.name.usr]
==
--
```
There are different ways we could represent our `$user` structure as JSON, but in this case we've opted to encapsulate it in an object and have the `name` as an array (since JSON arrays preserve order).
[`+enjs:format`](/reference/hoon/zuse/2d_1-5#enjsformat)includes the convenient [`+pairs`](/reference/hoon/zuse/2d_1-5#pairsenjsformat) function, which converts a list of `[@t json]` to an object containing those key-value pairs. We've used this to assemble the final object. Note that if you happen to have only a single key-value pair rather than a list, you can use [`+frond`](/reference/hoon/zuse/2d_1-5#frondenjsformat) instead of `+pairs`.
For the `joined` field, we've used the [`+sect`](/reference/hoon/zuse/2d_1-5#sectenjsformat) function from `+enjs` to convert the `@da` to a Unix seconds timestamp in a `$json` number. The `+sect` function, like others in `+enjs`, takes in a noun (in this case a `@da`) and produces `$json` (in this case a `[%n @ta]` number). `+enjs` contains a handful of useful functions like this, but for the rest we've just hand-made the `$json` structure. This is fairly typical when encoding `$json`, it's usually [decoding](#converting-from-json) that makes more extensive use of the `$json` utility functions in `+format`.
For the `name` field we've just formed a cell of `%a` and a list of `$json` strings, since a `$json` array is `[%a p=(list json)]`. Note we've separated this part into its own arm and wrapped the whole thing in a `|^` - a core with a `$` arm that's computed immediately. This is simply for readability - our structure here is quite simple but when dealing with deeply-nested `$json` structures or complex logic, having a single giant function can quickly become unwieldy.
#### Converting from `$json`
Here's our arm that converts `$json` to our `$user` structure:
```hoon
++ from-js
=, dejs:format
^- $-(json user)
%- ot
:~
[%username so]
[%name (at ~[so so so])]
[%joined du]
[%email so]
==
```
This is the inverse of the [encoding](#converting-to-json) function described in the previous section.
We make extensive use of [`+dejs:format`](/reference/hoon/zuse/2d_6) functions here, so we've used `=,` to expose the namespace and allow succinct `+dejs` function calls.
We use the [`+ot`](/reference/hoon/zuse/2d_6#otdejsformat) function from `+dejs:format` to decode the `$json` object to a n-tuple. It's a wet gate that takes a list of pairs of keys and other `+dejs` functions and produces a new gate that takes the `$json` to be decoded (which we've given it in `jon`).
The [`+so`](/reference/hoon/zuse/2d_6#sodejsformat) functions just decode `$json` strings to `cord`s. The [`+at`](/reference/hoon/zuse/2d_6#atdejsformat) function converts a `$json` array to a tuple, decoding each element with the respective function given in its argument list. Like `+ot`, `+at` is also a wet gate that produces a gate that takes `$json`. In our case we've used `+so` for each element, since they're all strings.
For `joined`, we've used the [`+du`](/reference/hoon/zuse/2d_6#dudejsformat) function, which converts a Unix seconds timestamp in a `$json` number to a `@da` (it's basically the inverse of the `+sect:enjs:format` we used earlier).
Notice how `+ot` takes in other `+dejs` functions in its argument. One of its arguments includes the `+at` function which itself takes in other `+dejs` functions. There are several `+dejs` functions like this that allow complex nested JSON structures to be decoded. For other examples of common `+dejs` functions like this, see the [More `+dejs`](#more-dejs) section below.
There are dozens of different functions in [`+dejs:format`](/reference/hoon/zuse/2d_6) that will cover a great many use cases. If there isn't a `+dejs` function for a particular case, you can also just write a custom function - it just has to take `$json`. Note there's also the [`+dejs-soft:format`](/reference/hoon/zuse/2d_7) functions - these are similar to `+dejs` functions except they produce `unit`s rather than simply crashing if decoding fails.
## More `+dejs`
We looked at the commonly used `+ot` function in the [first example](#converting-from-json), now let's look at a couple more common `+dejs` functions.
### `+of`
The [`+of`](/reference/hoon/zuse/2d_6#ofdejsformat) function takes an object containing a single key-value pair, decodes the value with the corresponding `+dejs` function in a key-function list, and produces a key-value tuple. This is useful when there are multiple possible objects you might receive, and tagged unions are a common data structure in hoon.
Let's look at an example. Here's a gate that takes in some `$json`, decodes it with an `+of` function that can handle three possible objects, casts the result to a tagged union, switches against its head with `?-`, performs some transformation and finally returns the result. You can save it as `gen/of-test.hoon` in the `%base` desk of a fake ship and `|commit %base`.
#### `of-test.hoon`
```hoon
|= jon=json
|^ ^- @t
=/ =fbb
(to-fbb jon)
?- -.fbb
%foo (cat 3 +.fbb '!!!')
%bar ?:(+.fbb 'Yes' 'No')
%baz :((cury cat 3) p.fbb q.fbb r.fbb)
==
+$ fbb
$% [%foo @t]
[%bar ?]
[%baz p=@t q=@t r=@t]
==
++ to-fbb
=, dejs:format
%- of
:~ foo+so
bar+bo
baz+(at ~[so so so])
==
--
```
Let's try it:
```
> +of-test (need (de-json:html '{"foo":"Hello"}'))
'Hello!!!'
> +of-test (need (de-json:html '{"bar":true}'))
'Yes'
> +of-test (need (de-json:html '{"baz":["a","b","c"]}'))
'abc'
```
### `+ou`
The [`+ou`](/reference/hoon/zuse/2d_6#oudejsformat) function decodes a `$json` object to an n-tuple using the matching functions in a key-function list. Additionally, it lets you set some key-value pairs in an object as optional and others as mandatory. The mandatory ones crash if they're missing and the optional ones are replaced with a given noun.
`+ou` is different to other `+dejs` functions - the functions it takes are `$-((unit json) grub)` rather than the usual `$-(json grub)` of most `+dejs` functions. There are only two `+dejs` functions that fit this - [`+un`](/reference/hoon/zuse/2d_6#undejsformat) and [`+uf`](/reference/hoon/zuse/2d_6#ufdejsformat). These are intended to be used with `+ou` - you would wrap each function in the key-function list of `+ou` with either `+un` or `+uf`.
`+un` crashes if its argument is `~`. `+ou` gives functions a `~` if the matching key-value pair is missing in the `$json` object, so `+un` crashes if the key-value pair is missing. Therefore, `+un` lets you set key-value pairs as mandatory.
`+uf` takes two arguments - a noun and a `+dejs` function. If the `(unit json)` it's given by `+ou` is `~`, it produces the noun it was given rather than the product of the `+dejs` function. This lets you specify key-value pairs as optional, replacing missing ones with whatever you want.
Let's look at a practical example. Here's a generator you can save in the `%base` desk of a fake ship in `gen/ou-test.hoon` and `|commit %base`. It takes in a `$json` object and produces a triple. The `+ou` in `+decode` has three key-function pairs - the first two are mandatory and the last is optional, producing the bunt of a set if the `%baz` key is missing.
#### `ou-test.hoon`
```hoon
|= jon=json
|^ ^- [@t ? (set @ud)]
(decode jon)
++ decode
=, dejs:format
%- ou
:~ foo+(un so)
bar+(un bo)
baz+(uf *(set @ud) (as ni))
==
--
```
Let's try it:
```
> +ou-test (need (de-json:html '{"foo":"hello","bar":true,"baz":[1,2,3,4]}'))
['hello' %.y {1 2 3 4}]
> +ou-test (need (de-json:html '{"foo":"hello","bar":true}'))
['hello' %.y {}]
> +ou-test (need (de-json:html '{"foo":"hello"}'))
[%key 'bar']
dojo: hoon expression failed
```
### `+su`
The [`+su`](/reference/hoon/zuse/2d_6#sudejsformat) function parses a string with the given parsing `rule`. Hoon's functional parsing library is very powerful and lets you create arbitrarily complex parsers. JSON will often have data types encoded in strings, so this function can be very useful. The writing of parsers is outside the scope of this guide, but you can see the [Parsing Guide](/guides/additional/hoon/parsing) and sections 4e to 4j of the standard library documentation for details.
Here are some simple examples of using `+su` to parse strings:
```
> `@ux`((su:dejs:format hex) s+'deadbeef1337f00D')
0xdead.beef.1337.f00d
> `(list @)`((su:dejs:format (most lus dem)) s+'1+2+3+4')
~[1 2 3 4]
> `@ub`((su:dejs:format ven) s+'+>-<->+<+')
0b11.1000.1101
```
Here's a more complex parser that will parse a GUID like `824e7749-4eac-9c00-db16-4cb816cd6f19` to a `@ux`:
#### `su-test.hoon`
```hoon
|= jon=json
^- @ux
%. jon
%- su:dejs:format
%+ cook
|= parts=(list [step @])
^- @ux
(can 3 (flop parts))
;~ plug
(stag 4 ;~(sfix (bass 16 (stun 8^8 six:ab)) hep))
(stag 2 ;~(sfix qix:ab hep))
(stag 2 ;~(sfix qix:ab hep))
(stag 2 ;~(sfix qix:ab hep))
(stag 6 (bass 16 (stun 12^12 six:ab)))
(easy ~)
==
```
Save it in the `/gen` directory of the `%base` desk and `|commit` it. We can then try it with:
```
> +su-test s+'5323a61d-0c26-d8fa-2b73-18cdca805fd8'
0x5323.a61d.0c26.d8fa.2b73.18cd.ca80.5fd8
```
If we delete the last character it'll no longer be a valid GUID and the parsing will fail:
```
> +su-test s+'5323a61d-0c26-d8fa-2b73-18cdca805fd'
/gen/su-test/hoon:<[2 1].[16 3]>
/gen/su-test/hoon:<[3 1].[16 3]>
{1 36}
syntax error
dojo: naked generator failure
```
## `mark` file example
Here's a simple `mark` file for the `$user` structure we created in the [first example](#json-encoding-and-decoding-example). It imports the [json-test.hoon](#json-testhoon) library we created and saved in our `%base` desk's `/lib` directory.
#### `user.hoon`
```hoon
/+ *json-test
|_ usr=user
++ grab
|%
++ noun user
++ json from-js
--
++ grow
|%
++ noun usr
++ json (to-js usr)
--
++ grad %noun
--
```
The [Marks section](/reference/arvo/clay/marks/marks) of the Clay documentation covers `mark` files comprehensively and is worth reading through if you want to write a mark file.
In brief, a mark file contains a `door` with three arms. The door's sample type is the type of the data in question - in our case the `$user` structure. The `+grab` arm contains methods for converting _to_ our mark, and the `+grow` arm contains methods for converting _from_ our mark. The `+noun` arms are mandatory, and then we've added `+json` arms which respectively call the `+from-js` and `+to-js` functions from our `json-test.hoon` library. The final `+grad` arm defines various revision control functions, in our case we've delegated these to the `%noun` mark.
From this mark file, Clay can build mark conversion gates between the `%json` mark and our `%user` mark, allowing the conversion of `$json` data to a `$user` structure and vice versa.
### Try it out
First, we'll save the code above as `user.hoon` in the `/mar` directory our of `%base` desk:
```
> |commit %base
>=
+ /~zod/base/9/mar/user/hoon
```
Let's quickly create a `$json` object to work with:
```
> =jon (need (de-json:html '{"joined":1631440078,"username":"john456","name":["John","William","Smith"],"email":"john.smith@example.com"}'))
> jon
[ %o
p
{ [p='email' q=[%s p='john.smith@example.com']]
[p='name' q=[%a p=~[[%s p='John'] [%s p='William'] [%s p='Smith']]]]
[p='username' q=[%s p='john456']]
[p='joined' q=[%n p=~.1631440078]]
}
]
```
We'll also build our library so we can use its types from the dojo:
```
> =user-lib -build-file %/lib/json-test/hoon
```
Now we can ask Clay to build a mark conversion gate from a `%json` mark to our `%user` mark. We'll use a scry with a `%f` `care` which produces a static mark conversion gate:
```
> =json-to-user .^($-(json user:user-lib) %cf /===/json/user)
```
Let's try converting our `$json` to a `$user` structure with our new mark conversion gate:
```
> =usr (json-to-user jon)
> usr
[ username='john456'
name=[first='John' mid='William' last='Smith']
joined=~2021.9.12..09.47.58
email='john.smith@example.com'
]
```
Now let's try the other direction. We'll again scry Clay to build a static mark conversion gate, this time _from_ `%user` _to_ `%json` rather than the reverse:
```
> =user-to-json .^($-(user:user-lib json) %cf /===/user/json)
```
Let's test it out by giving it our `$user` data:
```
> (user-to-json usr)
[ %o
p
{ [p='email' q=[%s p='john.smith@example.com']]
[p='name' q=[%a p=~[[%s p='John'] [%s p='William'] [%s p='Smith']]]]
[p='username' q=[%s p='john456']]
[p='joined' q=[%n p=~.1631440078]]
}
]
```
Finally, let's see how that looks as JSON encoded in text:
```
> (crip (en-json:html (user-to-json usr)))
'{"joined":1631440078,"username":"john456","name":["John","William","Smith"],"email":"john.smith@example.com"}'
```
Usually (though not in all cases) these mark conversions will be performed implicitly by Gall or Eyre and you'd not deal with the mark conversion gates directly, but it's still informative to see them work explicitly.
## Further reading
[The Zuse library reference](/reference/hoon/zuse/table-of-contents) - This includes documentation of the JSON parsing, printing, encoding and decoding functions.
[The Marks section of the Clay documentation](/reference/arvo/clay/marks/marks) - Comprehensive documentation of `mark`s.
[The External API Reference section of the Eyre documentation](/reference/arvo/eyre/external-api-ref) - Details of the webserver vane Eyre's external API.
[The Iris documentation](/reference/arvo/iris/iris) - Details of the web client vane Iris, which may be used to fetch external JSON data among other things.
[Strings Guide](/guides/additional/hoon/strings) - Atom printing functions like `+scot` will often be useful for JSON encoding - see the [Encoding in Text](/guides/additional/hoon/strings#encoding-in-text) section for usage.
[Parsing Guide](/guides/additional/hoon/parsing) - Learn how to write functional parsers in hoon which can be used with `+su`.

View File

@ -0,0 +1,708 @@
+++
title = "Parsing"
weight = 25
+++
This document serves as an introduction to parsing text with Hoon. No prior
knowledge of parsing is required, and we will explain the basic structure of how
parsing works in a purely functional language such as Hoon before moving on to
how it is implemented in Hoon.
**Note:** For JSON printing/parsing and encoding/decoding, see the [JSON Guide](/guides/additional/hoon/json-guide).
## What is parsing? {% #what-is-parsing %}
A program which takes a raw sequence of characters as an input and produces a data
structure as an output is known as a _parser_. The data structure produced
depends on the use case, but often it may be represented as a tree and the
output is thought of as a structural representation of the input. Parsers are ubiquitous in
computing, commonly used for to perform tasks such as reading files, compiling
source code, or understanding commands input in a command line interface.
Parsing a string is rarely done all at once. Instead, it is usually done
character-by-character, and the return contains the data structure representing
what has been parsed thus far as well as the remainder of the string to be
parsed. They also need to be able to fail in case the input is improperly
formed. We will see each of these standard practices implemented in [Hoon below](#parsing-in-hoon).
## Functional parsers
How parsers are built varies substantially depending on what sort of programming
language it is written in. As Hoon is a functional programming language, we will
be focused on understanding _functional parsers_, also known as _combinator
parsers_. In this section we will make light use of pseudocode, as introducing
the Hoon to describe functional parsers here creates a chicken-and-egg problem.
Complex functional parsers are built piece by piece from simpler parsers
that are plugged into one another in various ways to perform the desired task.
The basic building blocks, or primitives, are parsers that read only a
single character. There are frequently a few types of possible input characters,
such as letters, numbers, and symbols. For example, `(parse "1" integer)` calls
the parsing routine on the string `"1"` and looks for an integer, and so it
returns the integer `1`. However, taking into account what was said above about
parsers returning the unparsed portion of the string as well, we should
represent this return as a tuple. So we should expect something like this:
```
> (parse "1" integer)
[1 ""]
> (parse "123" integer)
[1 "23"]
```
What if we wish to parse the rest of the string? We would need to apply the
`parse` function again:
```
> (parse (parse "123" integer) integer)
[12 "3"]
> (parse (parse (parse "123" integer) integer) integer)
[123 ""]
```
So we see that we can parse strings larger than one character by stringing
together parsing functions for single characters. Thus in addition to parsing
functions for single input characters, we want _parser combinators_ that
allow you to combine two or more parsers to form a more complex one.
Combinators come in a few shapes and sizes, and typical operations they may
perform would be to repeat the same parsing operation until the string is
consumed, try a few different parsing operations until one of them works,
or perform a sequence of parsing operations. We will see how all of this is done
with Hoon in the next section.
# Parsing in Hoon
In this section we will cover the basic types, parser functions, and parser
combinators that are utilized for parsing in Hoon. This is not a complete guide
to every parsing-related functionality in the standard library (of which there
are quite a few), but ought to be
sufficient to get started with parsing in Hoon and be equipped to discover the
remainder yourself.
## Basic types
In this section we discuss the types most commonly used for Hoon parsers. In short:
- A `hair` is the position in the text the parser is at,
- A `nail` is parser input,
- An `edge` is parser output,
- A `rule` is a parser.
### `hair`
```hoon
++ hair [p=@ud q=@ud]
```
A `hair` is a pair of `@ud` used to keep track of what has already been parsed
for stack tracing purposes. This allows the parser to reveal where the problem
is in case it hits something unexpected during parsing.
`p` represents the column and `q` represents the line.
### `nail`
```hoon
++ nail [p=hair q=tape]
```
We recall from our [discussion above](#what-is-parsing) that parsing functions must keep
track of both what has been parsed and what has yet to be parsed. Thus a `nail`
consists of both a `hair`, giving the line and column up to which the input
sequence has already been parsed, and a `tape`, consisting of what remains of
the original input string (i.e. everything after the location indicated by the
`nail`, including the character at that `nail`).
For example, if you wish to feed the entire `tape` `"abc"` into a parser, you
would pass it as the `nail` `[[1 1] "abc"]`. If the parser successfully parses the first
character, the `nail` it returns will be `[[1 2] "bc"]` (though we note that
parser outputs are actually `edge`s which contain a `nail`, see the following).
The `nail` only matters for book-keeping reasons - it could be any value here
since it doesn't refer to a specific portion of the string being input, but only
what has theoretically already been parsed up to that point.
### `edge`
```hoon
++ edge [p=hair q=(unit [p=* q=nail])]
```
An `edge` is the output of a parser. If parsing succeeded, `p` is the location
of the original input `tape `up to which the text has been parsed. If parsing
failed, `p` will be the first `hair` at which parsing failed.
`q` may be `~`, indicating that parsing has failed .
If parsing did not fail, `p.q` is the data structure that is the result of the
parse up to this point, while `q.q` is the `nail` which contains the remainder
of what is to be parsed. If `q` is not null, `p` and `p.q.q` are identical.
### `rule`
```hoon
++ rule _|:($:nail $:edge)
```
A `rule` is a gate which takes in a `nail` and returns an `edge` - in other
words, a parser.
## Parser builders
These functions are used to build `rule`s (i.e. parsers), and thus are often
called rule-builders. For a complete list of parser builders, see [4f: Parsing
(Rule-Builders)](/reference/hoon/stdlib/4f), but also the more specific
functions in [4h: Parsing (ASCII
Glyphs)](/reference/hoon/stdlib/4h), [4i: Parsing (Useful
Idioms)](/reference/hoon/stdlib/4i), [4j: Parsing (Bases and Base
Digits)](/reference/hoon/stdlib/4j), [4l: Atom Parsing](/reference/hoon/stdlib/4l).
### [`+just`](/reference/hoon/stdlib/4f/#just)
The most basic rule builder, `+just` takes in a single `char` and produces a
`rule` that attempts to match that `char` to the first character in the `tape`
of the input `nail`.
```
> =edg ((just 'a') [[1 1] "abc"])
> edg
[p=[p=1 q=2] q=[~ [p='a' q=[p=[p=1 q=2] q="bc"]]]]
```
We note that `p.edg` is `[p=1 q=2]`, indicating that the next character to be
parsed is in line 1, column 2. `q.edg` is not null, indicating that parsing
succeeded. `p.q.edg` is `'a'`, which is the result of the parse. `p.q.q.edg` is the same as `p.edg`, which is always the case for
`rule`s built using standard library functions when parsing succeeds. Lastly,
`q.q.edg` is `"bc"`, which is the part of the input `tape` that has yet to be parsed.
Now let's see what happens when parsing fails.
```
> =edg ((just 'b') [[1 1] "abc"])
> edg
[p=[p=1 q=1] q=~]
```
Now we have that `p.edg` is the same as the input `hair`, `[1 1]`, meaning the
parser has not advanced since parsing failed. `q.edg` is null, indicating that
parsing has failed.
Later we will use [+star](#star) to string together a sequence of `+just`s in
order to parse multiple characters at once.
### [`+jest`](/reference/hoon/stdlib/4f/#jest)
`+jest` is a `rule` builder used to match a `cord`. It takes an input `cord` and
produces a `rule` that attempts to match that `cord` against the beginning of
the input.
Let's see what happens when we successfully parse the entire input `tape`.
```
> =edg ((jest 'abc') [[1 1] "abc"])
> edg
[p=[p=1 q=4] q=[~ [p='abc' q=[p=[p=1 q=4] q=""]]]]
```
`p.edg` is `[p=1 q=4]`, indicating that the next character to be parsed is at
line 1, column 4. Of course, this does not exist since the input `tape` was only
3 characters long, so this actually indicates that the entire `tape` has been
successfully parsed (since the `hair` does not advance in the case of failure).
`p.q.edg` is `'abc'`, as expected. `q.q.edg` is `""`, indicating that nothing
remains to be parsed.
What happens if we only match some of the input `tape`?
```
> =edg ((jest 'ab') [[1 1] "abc"])
> edg
[p=[p=1 q=3] q=[~ [p='ab' q=[p=[p=1 q=3] q="c"]]]]
```
Now we have that the result, `p.q.edg`, is `'ab'`, while the remainder `q.q.q.edg`
is `"c"`. So `+jest` has successfully parsed the first two characters, while the
last character remains. Furthermore, we still have the information that the
remaining character was in line 1 column 3 from `p.edg` and `p.q.q.edg`.
What happens when `+jest` fails?
```
> ((jest 'bc') [[1 1] "abc"])
[p=[p=1 q=1] q=~]
```
Despite the fact that `'bc'` appears in `"abc"`, because it was not at the
beginning the parse failed. We will see in [parser
combinators](#parser-combinators) how to modify this `rule` so that it
finds `bc` successfully.
### [`+shim`](/reference/hoon/stdlib/4f/#shim)
`+shim` is used to parse characters within a given range. It takes in two atoms
and returns a `rule`.
```
> ((shim 'a' 'z') [[1 1] "abc"])
[p=[p=1 q=2] q=[~ [p='a' q=[p=[p=1 q=2] q="bc"]]]]
```
### [`+next`](/reference/hoon/stdlib/4f/#next)
`+next` is a simple `rule` that takes in the next character and returns it as the
parsing result.
```
> (next [[1 1] "abc"])
[p=[p=1 q=2] q=[~ [p='a' q=[p=[p=1 q=2] q="bc"]]]]
```
### [`+cold`](/reference/hoon/stdlib/4f/#cold)
`+cold` is a `rule` builder that takes in a constant noun we'll call `cus` and a
`rule` we'll call `sef`. It returns a `rule` identical to the `sef` except it
replaces the parsing result with `cus`.
Here we see that `p.q` of the `edge` returned by the `rule` created with `+cold`
is `%foo`.
```
> ((cold %foo (just 'a')) [[1 1] "abc"])
[p=[p=1 q=2] q=[~ u=[p=%foo q=[p=[p=1 q=2] q="bc"]]]]
```
One common scenario where `+cold` sees play is when writing [command line
interface (CLI) apps](/guides/additional/hoon/cli-tutorial). We usher the
reader there to find an example where `+cold` is used.
### [`+knee`](/reference/hoon/stdlib/4f/#knee)
Another important function in the parser builder library is `+knee`, used for building
recursive parsers. We delay discussion of `+knee` to the
[section below](#recursive-parsers) as more context is needed to explain it
properly.
## Outside callers
Since `hair`s, `nail`s, etc. are only utilized within the context of writing
parsers, we'd like to hide them from the rest of the code of a program that
utilizes parsers. That is to say, you'd like the programmer to only worry about
passing `tape`s to the parser, and not have to dress up the `tape` as a `nail`
themselves. Thus we have several functions for exactly this purpose.
These functions take in either a `tape` or a `cord`,
alongside a `rule`, and attempt to parse the input with the `rule`. If the
parse succeeds, it returns the result. There are crashing and unitized versions
of each caller, corresponding to what happens when a parse fails.
For additional information including examples see [4g: Parsing (Outside Caller)](/reference/hoon/stdlib/4g).
### Parsing `tape`s
[`+scan`](/reference/hoon/stdlib/4g/#scan) takes in a `tape` and a `rule` and attempts to parse the `tape` with the
`rule`.
```
> (scan "hello" (jest 'hello'))
'hello'
> (scan "hello zod" (jest 'hello'))
{1 6}
'syntax-error'
```
[`+rust`](/reference/hoon/stdlib/4g/#rust) is the unitized version of `+scan`.
```
> (rust "a" (just 'a'))
[~ 'a']
> (rust "a" (just 'b'))
~
```
For the remainder of this tutorial we will make use of `+scan` so that we do not
need to deal directly with `nail`s except where it is illustrative to do so.
### Parsing atoms
[Recall from Hoon School](/guides/core/hoon-school/E-types) that `cord`s are atoms with the aura
`@t` and are typically used to represent strings internally as data, as atoms
are faster for the computer to work with than `tape`s, which are `list`s of
`@tD` atoms. [`+rash`](/reference/hoon/stdlib/4g/#rash) and [`+rush`](/reference/hoon/stdlib/4g/#rush) are for parsing atoms, with `+rash` being
analogous to `+scan` and `+rush` being analogous to `+rust`. Under the hood, `+rash`
calls `+scan` after converting the input atom to a `tape`, and `+rush` does
similary for `+rust`.
## Parser modifiers
The standard library provides a number of gates that take a `rule` and produce a
new modified `rule` according to some process. We call these _parser modifiers_.
These are documented among the [parser
builders](/reference/hoon/stdlib/4f).
### [`+ifix`](/reference/hoon/stdlib/4f/#ifix)
`+ifix` modifies a `rule` so that it matches that `rule` only when it is
surrounded on both sides by text that matches a pair of `rule`s, which is discarded.
```
> (scan "(42)" (ifix [pal par] (jest '42')))
'42'
```
`+pal` and `+par` are shorthand for `(just '(')` and `(just ')')`, respectively. All
ASCII glyphs have counterparts of this sort, documented
[here](/reference/hoon/stdlib/4h).
### [`+star`](/reference/hoon/stdlib/4f/#star) {% #star %}
`+star` is used to apply a `rule` repeatedly. Recall that `+just` only parses
the first character in the input `tape.`
```
> ((just 'a') [[1 1] "aaaa"])
[p=[p=1 q=2] q=[~ [p='a' q=[p=[p=1 q=2] q="aaa"]]]]
```
We can use `+star` to get the rest of the `tape`:
```
> ((star (just 'a')) [[1 1] "aaa"])
[p=[p=1 q=4] q=[~ [p=[i='a' t=<|a a|>] q=[p=[p=1 q=4] q=""]]]]
```
and we note that the parsing ceases when it fails.
```
> ((star (just 'a')) [[1 1] "aaab"])
[p=[p=1 q=4] q=[~ [p=[i='a' t=<|a a|>] q=[p=[p=1 q=4] q="b"]]]]
```
We can combine `+star` with `+next` to just return the whole input:
```
> ((star next) [[1 1] "aaabc"])
[p=[p=1 q=6] q=[~ [p=[i='a' t=<|a a b c|>] q=[p=[p=1 q=6] q=""]]]]
```
### [`+cook`](/reference/hoon/stdlib/4f/#cook)
`+cook` takes a `rule` and a gate and produces a modified version of the `rule`
that passes the result of a successful parse through the given gate.
Let's modify the rule `(just 'a')` so that it when it successfully parses `a`,
it returns the following letter `b` as the result.
```
((cook |=(a=@ `@t`+(a)) (just 'a')) [[1 1] "abc"])
[p=[p=1 q=2] q=[~ u=[p='b' q=[p=[p=1 q=2] q="bc"]]]]
```
## Parser combinators
Building complex parsers from simpler parsers is accomplished in Hoon with the
use of two tools: the monadic applicator rune
[`;~`](/reference/hoon/rune/mic/#-micsig) and [parsing
combinators](/reference/hoon/stdlib/4e). First we introduce a few
combinators, then we examine more closely how `;~` is used to chain them together.
The syntax to combine `rule`s is
```hoon
;~(combinator rule1 rule2 ... ruleN)
```
The `rule`s are composed together using the combinator as an
intermediate function, which takes the product of a `rule` (an `edge`) and a `rule` and turns
it into a sample (a `nail`) for the next `rule` to handle. We elaborate on this
behavior [below](#-micsig).
### [`+plug`](/reference/hoon/stdlib/4e/#plug)
`+plug` simply takes the `nail` in the `edge` produced by one rule and passes it
to the next `rule`, forming a cell of the results as it proceeds.
```
> (scan "starship" ;~(plug (jest 'star') (jest 'ship')))
['star' 'ship']
```
### [`+pose`](/reference/hoon/stdlib/4e/#pose)
`+pose` tries each `rule` you hand it successively until it finds one that
works.
```
> (scan "a" ;~(pose (just 'a') (just 'b')))
'a'
> (scan "b" ;~(pose (just 'a') (just 'b')))
'b'
```
### [`+glue`](/reference/hoon/stdlib/4e/#glue)
`+glue` parses a delimiter in between each `rule` and forms a cell of the
results of each `rule`.
```
> (scan "a,b" ;~((glue com) (just 'a') (just 'b')))
['a' 'b']
> (scan "a,b,a" ;~((glue com) (just 'a') (just 'b')))
{1 4}
syntax error
> (scan "a,b,a" ;~((glue com) (just 'a') (just 'b') (just 'a')))
['a' 'b' 'a']
```
### [`;~`](/reference/hoon/rune/mic/#-micsig) {% #-micsig %}
Understanding the rune `;~` is essential to building parsers with Hoon. Let's
take this opportunity to think about it carefully.
The `rule` created by `;~(combinator (list rule))` may be understood
inductively. To do this, let's consider the base case where our `(list rule)` has only a
single entry.
```
> (scan "star" ;~(plug (jest 'star')))
'star'
```
Our output is identical to that given by `(scan "star" (jest 'star'))`. This is to
be expected. The combinator `+plug` is specifically used for chaining together
`rule`s in the `(list rule)`, but if there is only one `rule`, there is nothing
to chain. Thus, swapping out `+plug` for another combinator makes no difference here:
```
> (scan "star" ;~(pose (jest 'star')))
'star'
> (scan "star" ;~((glue com) (jest 'star')))
'star'
```
`;~` and the combinator only begin to play a role once the `(list rule)` has at
least two elements. So let's look at an example done with `+plug`, the simplest
combinator.
```
> (scan "star" ;~(plug (jest 'st') (jest 'ar')))
['st' 'ar']
```
Our return suggests that we first parsed `"star"` with the `rule` `(jest 'st')` and passed
the resulting `edge` to `(jest 'ar')` - in other words, we called `+plug` on `(jest 'st')` and the `edge` returned once it had been used to parse `"star"`. Thus
`+plug` was the glue that allowed us to join the two `rule`s, and `;~` performed
the gluing operation. And so, swapping `+plug` for `+pose` results in a crash,
which clues us into the fact that the combinator now has an effect since there
is more than one `rule`.
```
> (scan "star" ;~(pose (jest 'st') (jest 'ar')))
{1 3}
syntax error
```
## Parsing numbers
Functions for parsing numbers are documented in [4j: Parsing (Bases and Base
Digits)](/reference/hoon/stdlib/4j). In particular,
[`dem`](/reference/hoon/stdlib/4i/#dem) is a `rule` for parsing decimal
numbers.
```
> (scan "42" dem)
42
> (add 1 (scan "42" dem))
43
```
## Recursive parsers
Naively attempting to write a recursive `rule`, i.e. like
```
> |-(;~(plug prn ;~(pose $ (easy ~))))
```
results in an error:
```
-find.,.+6
-find.,.+6
rest-loop
```
Here, [`+prn`](/reference/hoon/stdlib/4i/#prn) is a `rule` used
to parse any printable character, and
[`+easy`](/reference/hoon/stdlib/4f/#easy) is a `rule` that always returns a
constant (`~` in this case) regardless of the input.
Thus some special sauce is required, the
[`+knee`](/reference/hoon/stdlib/4f/#knee) function.
`+knee` takes in a noun that is the default value of the parser, typically given
as the bunt value of the type that the `rule` produces, as well as a gate that
accepts a `rule`. `+knee` produces a `rule` that implements any recursive calls
in the `rule` in a manner acceptable to the compiler. Thus the preferred manner
to write the above `rule` is as follows:
```hoon
|-(;~(plug prn ;~(pose (knee *tape |.(^$)) (easy ~))))
```
You may want to utilize the `~+` rune when writing recursive parsers to cache
the parser to improve performance. In the following section, we will be writing a recursive
parser making use of `+knee` and `~+` throughout.
# Parsing arithmetic expressions
In this section we will be applying what we have learned to write a parser for
arithmetic expressions in Hoon. That is, we will make a `rule` that takes in
`tape`s of the form `"(2+3)*4"` and returns `20` as a `@ud`.
We call a `tape` consisting of some consistent arithmetic string of numbers,
`+`, `*`, `(`, and `)` an _expression_. We wish to build a `rule` that takes in an
expression and returns the result of the arithmetic computation described by the
expression as a `@ud`.
To build a parser it is a helpful exercise to first describe its
[grammar](https://en.wikipedia.org/wiki/Parsing_expression_grammar). This has a
formal mathematical definition, but we will manage to get by here describing the grammar
for arithmetic expressions informally.
First let's look at the code we're going to use, and then dive into explaining
it. If you'd like to follow along, save the following as `expr-parse.hoon` in
your `gen/` folder.
```hoon
:: expr-parse: parse arithmetic expressions
::
|= math=tape
|^ (scan math expr)
++ factor
%+ knee *@ud
|. ~+
;~ pose
dem
(ifix [pal par] expr)
==
++ term
%+ knee *@ud
|. ~+
;~ pose
((slug mul) tar ;~(pose factor term))
factor
==
++ expr
%+ knee *@ud
|. ~+
;~ pose
((slug add) lus ;~(pose term expr))
term
==
--
```
Informally, the grammar here is:
- A factor is either an integer or an expression surrounded by parentheses.
- A term is either a factor or a factor times a term.
- An expression is either a term or a term plus an expression.
### Factors, terms, and expressions
Our grammar consists of three `rule`s: one for factors, one for terms, and one
for expressions.
#### Factors
```hoon
++ factor
%+ knee *@ud
|. ~+
;~ pose
dem
(ifix [pal par] expr)
==
```
A _factor_ is either a decimal number or an expression surrounded by parentheses. Put
into Hoon terms, a decimal number is parsed by the `rule` `+dem` and an
expression is parsed by removing the surrounding parentheses and then passing
the result to the expression parser arm `+expr`, given by the `rule` `(ifix [pal par] expr)`. Since we want to parse our expression with one or the other, we
chain these two `rule`s together using the monadic applicator rune `;~` along
with `+pose`, which says to try each rule in succession until one of them works.
Since expressions ultimately reduce to factors, we are actually building a
recursive rule. Thus we need to make use of `+knee`. The first argument for
`+knee` is `*@ud`, since our final answer should be a `@ud`.
Then follows the definition of the gate utilized by `+knee`:
```hoon
|. ~+
;~ pose
dem
(ifix [pal par] expr)
==
```
`~+` is used to cache the parser, so that it does not need to be computed over
and over again. Then it follows the `rule` we described above.
#### Parsing expressions
An _expression_ is either a term plus an expression or a term.
In the case of a term plus an expression, we actually must compute what that equals. Thus we will
make use of [`+slug`](/reference/hoon/stdlib/4f#slug), which parses a
delimited list into `tape`s separated by a given delimiter and then composes
them by folding with a binary gate. In this case, our delimiter is `+` and our
binary gate is `+add`. That is to say, we will split the input string into terms
and expressions separated by luses, parse each term and expression until they
reduce to a `@ud`, and then add them together. This is accomplished with the
`rule` `((slug add) lus ;~(pose term expr))`.
If the above `rule` does not parse the expression, it must be a `term`, so the
`tape` is automatically passed to `+term` to be evaluated. Again we use `;~` and `pose` to
accomplish this:
```hoon
;~ pose
((slug add) lus ;~(pose term expr))
term
==
```
The rest of the `+expr` arm is structured just like how `+factor` is, and for
the same reasons.
#### Parsing terms
A _term_ is either a factor times a term or a factor. This is handled similarly
for expressions, we just need to swap `lus` for `tar`, `add` for `mul`, and
`;~(pose factor term)` instead of `;~(pose term expr)`.
```hoon
++ expr
%+ knee *@ud
|. ~+
;~ pose
((slug add) lus ;~(pose term expr))
term
==
```
### Try it out
Let's feed some expressions to `+expr-parse` and see how it does.
```
> +expr-parse "3"
3
> +expr-parse "3+3"
6
> +expr-parse "3+3+(2*3)+(4+2)*(4+1)"
42
> +expr-parse "3+3+2*3"
12
```
As an exercise, add exponentiation (e.g. `2^3 = 8`) to `+expr-parse`.

View File

@ -0,0 +1,565 @@
+++
title = "Sail (HTML)"
weight = 30
+++
Sail is a domain-specific language for composing HTML (and XML) structures in
Hoon. Like everything else in Hoon, a Sail document is a noun, just one produced
by a specialized markup language within Hoon.
Front-ends for Urbit apps are often created and uploaded separately to the
rest of the code in the desk. Sail provides an alternative approach, where
front-ends can be composed directly inside agents.
This document will walk through the basics of Sail and its syntax.
## Basic example
Its easy to see how Sail can directly translate to HTML:
{% table %}
- Sail
- HTML
---
- ```
;html
;head
;title = My page
;meta(charset "utf-8");
==
;body
;h1: Welcome!
;p
; Hello, world!
; Welcome to my page.
; Here is an image:
;br;
;img@"/foo.png";
==
==
==
```
- ```
<html>
<head>
<title>My page</title>
<meta charset="utf-8" />
</head>
<body>
<h1>Welcome!</h1>
<p>Hello, world! Welcome to my
page. Here is an image:
<br />
<img src="/foo.png" />
</p>
</body>
</html>
```
{% /table %}
## Tags and Closing
In Sail, tag heads are written with the tag name prepended by `;`. Unlike in
HTML, there are different ways of closing tags, depending on the needs of the
tag. One of the nice things about Hoon is that you dont have to constantly
close expressions; Sail inherits this convenience.
### Empty
Empty tags are closed with a `;` following the tag. For example, `;div;` will be
rendered as `<div></div>`. Non-container tags `;br;` and `;img@"some-url";` in
particular will be rendered as a single tag like `<br />` and `<img src="some-url" />`.
### Filled
Filled tags are closed via line-break. To fill text inside, add `:` after the
tag name, then insert your plain text following a space. Example:
| Sail | HTML |
| ---------------- | -------------------- |
| `;h1: The title` | `<h1>The title</h1>` |
### Nested
To nest tags, simply create a new line. Nested tags need to be closed with `==`,
because they expect a list of sub-tags.
If we nest lines of plain text with no tag, the text will be wrapped in a
`<p>` tag. Additionally, any text with atom auras or `++arm:syntax` in such
plain text lines will be wrapped in `<code>` tags.
Example:
{% table %}
- Sail
- HTML
---
- ```
;body
;h1: Blog title
This is some good content.
==
```
- ```
<body>
<h1>Blog title</h1>
<p>This is some good content.</p>
</body>
```
{% /table %}
If we want to write a string with no tag at all, then we can prepend
those untagged lines with `;` and then a space:
{% table %}
- Sail
- HTML
---
- ```
;body
;h1: Welcome!
; Hello, world!
; Were on the web.
==
```
- ```
<body>
<h1>Welcome!</h1>
Hello, world!
Were on the web.
</body>
```
{% /table %}
## Attributes
Adding attributes is simple: just add the desired attribute between parentheses,
right after the tag name without a space. We separate different attributes of
the same node by using `,`.
Attributes can also be specified in tall form, with each key prefixed by `=`,
followed by two spaces, and then a tape with its value. These two styles are
shown below.
### Generic
{% table %}
- Form
- Example
---
- Wide
- ```
;div(title "a tooltip", style "color:red")
;h1: Foo
foo bar baz
==
```
---
- Tall
- ```
;div
=title "a tooltip"
=style "color:red"
;h1: Foo
foo bar baz
==
```
---
- HTML
- ```
<div title="a tooltip" style="color:red">
<h1>Foo</h1>
<p>foo bar baz </p>
</div>
```
{% /table %}
### IDs
Add `#` after tag name to add an ID:
| Sail | HTML |
| ------------------- | ----------------------------- |
| `;nav#header: Menu` | `<nav id="header">Menu</nav>` |
### Classes
Add `.` after tag name to add a class:
| Sail | HTML |
| ---------------------- | ---------------------------------- |
| `;h1.text-blue: Title` | `<h1 class="text-blue">Title</h1>` |
For class values containing spaces, you can add additional `.`s like so:
| Sail | HTML |
| ------------------- | --------------------------------- |
| `;div.foo.bar.baz;` | `<div class="foo bar baz"></div>` |
Otherwise, if your class value does not conform to the allowed `@tas`
characters, you must use the generic attribute syntax:
| Sail | HTML |
| ------------------------ | ----------------------------- |
| `;div(class "!!! !!!");` | `<div class="!!! !!!"></div>` |
### Images
Add `@` after the tag name to link your source:
| Sail | HTML |
| --------------------- | -------------------------- |
| `;img@"example.png";` | `<img src="example.png"/>` |
To add attributes to the image, like size specifications, add the desired
attribute after the `"` of the image name and before the final `;` of the `img`
tag like `;img@"example.png"(width "100%");`.
### Links
Add `/` after tag name to start an `href`.
{% table %}
- Sail
- HTML
---
- ```
;a/"urbit.org": A link to Urbit.org
```
- ```
<a href="urbit.org">A link to Urbit.org</a>
```
{% /table %}
## Interpolation
The textual content of tags, despite not being enclosed in double-quotes, are
actually tapes. This means they support interpolated Hoon expressions in the
usual manner. For example:
{% table %}
- Sail
- HTML
---
- ```
=| =time
;p: foo {<time>} bar
```
- ```
<p>foo ~2000.1.1 baz</p>
```
{% /table %}
Likewise:
{% table .w-full %}
- Sail
---
- ```
=/ txt=tape " bananas"
;article
;b: {(a-co:co (mul 42 789))}
; {txt}
{<our>} {<now>} {<`@ux`(end 6 eny)>}
==
```
{% /table %}
{% table .w-full %}
- HTML
---
- ```
<article>
<b>33138</b> bananas
<p>~zod ~2022.2.21..09.54.21..5b63 0x9827.99c7.06f4.8ef9</p>
</article>
```
{% /table %}
## A note on CSS
The CSS for a page is usually quite large. The typical approach is to include a
separate arm in your agent (`++style` or the like) and write out the CSS in a
fenced cord block. You can then call `++trip` on it and include it in a style
tag. For example:
```hoon
++ style
^~
%- trip
'''
main {
width: 100%;
color: red;
}
header {
color: blue;
font-family: monospace;
}
'''
```
And then your style tag might look like:
```hoon
;style: {style}
```
A cord is used rather than a tape so you don't need to escape braces. The
[ketsig](/reference/hoon/rune/ket#-ketsig) (`^~`) rune means `++trip` will
be run at compile time rather than call time.
## Types and marks
So far we've shown rendered HTML for demonstrative purposes, but Sail syntax
doesn't directly produce HTML text. Instead, it produces a
[$manx](/reference/hoon/stdlib/5e#manx). This is a Hoon type used to
represent an XML hierarchical structure with a single root node. There are six
XML-related types defined in the standard library:
```hoon
+$ mane $@(@tas [@tas @tas]) :: XML name+space
+$ manx $~([[%$ ~] ~] [g=marx c=marl]) :: dynamic XML node
+$ marl (list manx) :: XML node list
+$ mars [t=[n=%$ a=[i=[n=%$ v=tape] t=~]] c=~] :: XML cdata
+$ mart (list [n=mane v=tape]) :: XML attributes
+$ marx $~([%$ ~] [n=mane a=mart]) :: dynamic XML tag
```
More information about these can be found in [section 5e of the standard library
reference](/reference/hoon/stdlib/5e).
You don't need to understand these types in order to write Sail. The main thing
to note is that a `$manx` is a node (a single tag) and its contents is a
[$marl](/reference/hoon/stdlib/5e#marl), which is just a `(list manx)`.
### Rendering
A `$manx` can be rendered as HTML in a tape with the `++en-xml:html` function in
`zuse.hoon`. For example:
```
> ;p: foobar
[[%p ~] [[%$ [%$ "foobar"] ~] ~] ~]
> =x ;p: foobar
> (en-xml:html x)
"<p>foobar</p>"
> (crip (en-xml:html x))
'<p>foobar</p>'
```
### Sanitization
The `++en-xml:html` function will sanitize the contents of both attributes and
elements, converting characters such as `>` to HTML entities. For example:
```
> =z ;p(class "\"><script src=\"example.com/xxx.js"): <h1>FOO</h1>
> (crip (en-xml:html z))
'<p class="&quot;&gt;<script src=&quot;example.com/xxx.js"><h1&gt;FOO</h1&gt;</p>'
```
### Marks
There are a few different HTML and XML related marks, so it can be a bit
confusing. We'll look at the ones you're most likely to use.
#### `%html`
- Type: `@t`
This mark is used for HTML that has been printed as text in a cord. You may wish
to return this mark when serving pages to the web. To do so, you must run the
`$manx` produced by your Sail expressions through `++en-xml:html`, and then run
the resulting `tape` through `++crip`.
#### `%hymn`
- Type: `$manx`
The `%hymn` mark is intended to be used for complete HTML documents - having an
`<html>` root element, `<head>`, `<body>`, etc. This isn't enforced on
the type level but it is assumed in certain mark conversion pathways. Eyre can
automatically convert a `%hymn` to printed `%html` if it was requested through
Eyre's scry interface.
#### `%elem`
- Type: `$manx`
The type of the `%elem` mark is a `$manx`, just like a `%hymn`. While `%hymn`s
are intended for complete HTML documents, `%elem`s are intended for more general
XML structures. You may wish to use an `%elem` mark if you're producing smaller
fragments of XML or HTML rather than whole documents. Like a `%hymn`, Eyre can
automatically convert it to `%html` if requested through its scry interface.
#### Summary
In general, if you're going to be composing web pages and serving them to web
clients, running the result of your Sail through `++en-xml:html`, `++crip`ping
it and producing `%html` is the most straight-forward approach. If you might
want to pass around a `$manx` to other agents or ships which may wish to
manipulate it futher, a `%hymn` or `%elem` is better.
## Sail Runes
In addition to the syntax so far described, there are also a few Sail-specific
runes:
### `;+` Miclus
The [miclus rune](/reference/hoon/rune/mic#-miclus) makes a `$marl` from a
complex hoon expression that produces a single `$manx`. Its main use is nesting
tall-form hoon logic in another Sail element. For example:
```hoon
;p
;b: {(a-co:co number)}
; is an
;+ ?: =(0 (mod number 2))
;b: even
;b: odd
; number.
==
```
Produces one of these depending on the value of `number`:
```
<p><b>2 </b>is an <b>even </b>number.</p>
```
```
<p><b>12345 </b>is an <b>odd </b>number.</p>
```
### `;*` Mictar
The [mictar rune](/reference/hoon/rune/mic#-mictar) makes a `$marl` (a list
of XML nodes) from a complex hoon expression. This rune lets you add many
elements inside another Sail element. For example:
{% table %}
- Sail
- HTML
---
- ```
=/ nums=(list @ud) (gulf 1 9)
;p
;* %+ turn nums
|= n=@ud
?: =(0 (mod n 2))
;sup: {(a-co:co n)}
;sub: {(a-co:co n)}
==
```
- ```
<p>
<sub>1</sub><sup>2</sup>
<sub>3</sub><sup>4</sup>
<sub>5</sub><sup>6</sup>
<sub>7</sub><sup>8</sup>
<sub>9</sub>
</p>
```
{% /table %}
### `;=` Mictis
The [mictis rune](/reference/hoon/rune/mic#-mictis) makes a `$marl` (a list
of XML nodes) from a series of `$manx`es. This is mostly useful if you want to
make the list outside of an element and then be able to insert it afterwards.
For example:
{% table %}
- Sail
- HTML
---
- ```
=/ paras=marl
;= ;p: First node.
;p: Second node.
;p: Third node.
==
;main
;* paras
==
```
- ```
<main>
<p>First node.</p>
<p>Second node.</p>
<p>Third node.</p>
</main>
```
{% /table %}
### `;/` Micfas
The [micfas rune](/reference/hoon/rune/mic#-micfas) turns an ordinary tape
into a `$manx`. For example:
```
> %- en-xml:html ;/ "foobar"
"foobar"
```
In order to nest it inside another Sail element, it must be preceeded with a
`;+` rune or similar, it cannot be used directly. For example:
```hoon
;p
;+ ;/ ?: =(0 (mod eny 2))
"even"
"odd"
==
```
## Good examples
Here's a couple of agents that make use of Sail, which you can use as a
reference:
- [Pals by ~palfun-foslup][pals]
- [Gora by ~rabsef-bicrym][gora]
[pals]: https://github.com/Fang-/suite/blob/master/app/pals/webui/index.hoon
[gora]: https://github.com/dalten-collective/gora/blob/master/sail/app/gora/goraui/index.hoon

View File

@ -0,0 +1,281 @@
+++
title = "Software Distribution"
weight = 35
+++
In this document we'll walk through an example of creating and publishing a desk that others can install. We'll create a simple "Hello World!" front-end with a "Hello" tile to launch it. For simplicity, the desk won't include an actual Gall agent, but we'll note everything necessary if there were one.
## Create desk
To begin, we'll need to clone the [Urbit Git repo](https://github.com/urbit/urbit) from the Unix terminal:
```sh
[user@host ~]$ git clone https://github.com/urbit/urbit urbit-git
```
Once that's done, we can navigate to the `pkg` directory in our cloned repo:
```sh
[user@host ~]$ cd urbit-git/pkg
[user@host pkg]$ ls .
arvo btc-wallet garden grid interface npm webterm
base-dev docker-image garden-dev herb landscape symbolic-merge.sh
bitcoin ent ge-additions hs libaes_siv urbit
```
Each desk defines its own `mark`s, in its `/mar` folder. There are no longer shared system marks that all userspace code knows, nor common libraries in `/lib` or `/sur`. Each desk is completely self-contained. This means any new desk will need a number of base files.
To make the creation of a new desk easier, `base-dev` and `garden-dev` contain symlinks to all `/sur`, `/lib` and `/mar` files necessary for interacting with the `%base` and `%garden` desks respectively. These dev desks can be copied and merged with the `symbolic-merge.sh` included.
Let's create a new `hello` desk:
```sh
[user@host pkg]$ mkdir hello
[user@host pkg]$ ./symbolic-merge.sh base-dev hello
[user@host pkg]$ ./symbolic-merge.sh garden-dev hello
[user@host pkg]$ cd hello
[user@host hello]$ ls
lib mar sur
```
### `sys.kelvin`
Our desk must include a `sys.kelvin` file which specifies the kernel version it's compatible with. Let's create that:
```sh
[user@host hello]$ echo "[%zuse 418]" > sys.kelvin
[user@host hello]$ cat sys.kelvin
[%zuse 418]
```
### `desk.ship`
We can also add a `desk.ship` file to specify the original publisher of this desk. We'll try this on a fakezod so let's just add `~zod` as the publisher:
```sh
[user@host hello]$ echo "~zod" > desk.ship
[user@host hello]$ cat desk.ship
~zod
```
### `desk.bill`
If we had Gall agents in this desk which should be automatically started when the desk is installed, we'd add them to a `hoon` list in the `desk.bill` file. It would look something like this:
```hoon
:~ %some-app
%another
==
```
In this example we're not adding any agents, so we'll simply omit the `desk.bill` file.
### `desk.docket-0`
The final file we need is `desk.docket-0`. This one's more complicated, so we'll open it in our preferred text editor:
```
[user@host hello]$ nano desk.docket-0
```
In the text editor, we'll add the following:
```hoon
:~ title+'Hello'
info+'A simple hello world app.'
color+0x81.88c9
image+'https://media.urbit.org/guides/additional/dist/wut.svg'
base+'hello'
glob-ames+[~zod 0v0]
version+[0 0 1]
website+'https://developers.urbit.org/guides/additional/dist/guide'
license+'MIT'
==
```
You can refer to the [Docket File](/guides/additional/dist/docket) documentation for more details of what is required. In brief, the `desk.docket-0` file contains a `hoon` list of [clauses](/guides/additional/dist/docket) which configure the appearance of the app tile, the source of the [glob](/guides/additional/dist/glob), and some other metadata.
We've given the app a [`%title`](/guides/additional/dist/docket#title) of "Hello", which will be displayed on the app tile and will be the name of the app when others browse to install it. We've given the app tile a [`%color`](/guides/additional/dist/docket#color) of `#8188C9`, and also specified the URL of an [`%image`](/guides/additional/dist/docket#image) to display on the tile.
The [`%base`](/guides/additional/dist/docket#base) clause specifies the base URL path for the app. We've specified "hello" so it'll be `http://localhost:8080/apps/hello/...` in the browser. For the [glob](/guides/additional/dist/glob), we've used a clause of [`%glob-ames`](/guides/additional/dist/docket#glob-ames), which means the glob will be served from a ship over Ames, as opposed to being served over HTTP with a [`%glob-http`](/guides/additional/dist/docket#glob-http) clause or having an Eyre binding with a [`%site`](/guides/additional/dist/docket#site) clause. You can refer to the [glob](/guides/additional/dist/glob) documentation for more details of the glob options. In our case we've specified `[~zod 0v0]`. Since `~zod` is the fakeship we'll install it on, the `%docket` agent will await a separate upload of the `glob`, so we can just specify `0v0` here as it'll get overwritten later.
The [`%version`](/guides/additional/dist/docket#version) clause specifies the version as a triple of major version, minor version and patch version. The rest is just some additional informative metadata which will be displayed in _App Info_.
So let's save that to the `desk.docket-0` file and have a look at our desk:
```
[user@host hello]$ ls
desk.docket-0 desk.ship lib mar sur sys.kelvin
```
That's everything we need for now.
## Install
Let's spin up a fakezod in which we can install our desk. By default a fakezod will be out of date, so we need to bootstrap with a pill from our urbit-git repo. The pills are stored in git lfs and need to be pulled into our repo first:
```
[user@host hello]$ cd ~/urbit-git
[user@host urbit-git]$ git lfs install
[user@host urbit-git]$ git lfs pull
[user@host urbit-git]$ cd ~/piers/fake
[user@host fake]$ urbit -F zod -B ~/urbit-git/bin/multi-brass.pill
```
Once our fakezod is booted, we'll need to create a new `%hello` desk for our app and mount it. We can do this in the dojo like so:
```
> |merge %hello our %base
>=
> |mount %hello
>=
```
Now, back in the Unix terminal, we should see the new desk mounted:
```
[user@host fake]$ cd zod
[user@host zod]$ ls
hello
```
Currently it's just a clone of the `%base` desk, so let's delete its contents:
```
[user@host zod]$ rm -r hello/*
```
Next, we'll copy in the contents of the `hello` desk we created earlier. We must use `cp -LR` to resolve all the symlinks:
```
[user@host zod]$ cp -LR ~/urbit-git/pkg/hello/* hello/
```
Back in the dojo we can commit the changes and install the desk:
```
> |commit %hello
> |install our %hello
kiln: installing %hello locally
docket: awaiting manual glob for %hello desk
```
The `docket: awaiting manual glob for %hello desk` message is because our `desk.docket-0` file includes a [`%glob-ames`](/guides/additional/dist/docket#glob-ames) clause which specifies our ship as the source, so it's waiting for us to upload the glob.
If we open a browser now, navigate to `http://localhost:8080` and login with the default fakezod code `lidlut-tabwed-pillex-ridrup`, we'll see our tile's appeared but it says "installing" with a spinner due to the missing glob:
![Installing Tile](https://media.urbit.org/guides/additional/dist/local-install-1.png)
## Create files for glob
We'll now create the files for the glob. We'll use a very simple static HTML page that just displayes "Hello World!" and an image. Typically we'd have a more complex JS web app that talked to apps on our ship through Eyre's channel system, but for the sake of simplicity we'll forgo that. Let's hop back in the Unix terminal:
```
[user@host zod]$ cd ~
[user@host ~]$ mkdir hello-glob
[user@host ~]$ cd hello-glob
[user@host hello-glob]$ mkdir img
[user@host hello-glob]$ wget -P img https://media.urbit.org/guides/additional/dist/pot.svg
[user@host hello-glob]$ tree
.
└── img
└── pot.svg
1 directory, 1 file
```
We've grabbed an image to use in our "Hello world!" page. The next thing we need to add is an `index.html` file in the root of the folder. The `index.html` file is mandatory; it's what will be loaded when the app's tile is clicked. Let's open our preferred editor and create it:
```
[user@host hello-glob]$ nano index.html
```
In the editor, paste in the following HTML and save it:
```html
<!DOCTYPE html>
<html>
<head>
<style>
div {
text-align: center;
}
</style>
</head>
<title>Hello World</title>
<body>
<div>
<h1>Hello World!</h1>
<img src="img/pot.svg" alt="pot" width="219" height="196" />
</div>
</body>
</html>
```
Our `hello-glob` folder should now look like this:
```
[user@host hello-glob]$ tree
.
├── img
│ └── pot.svg
└── index.html
1 directory, 2 files
```
## Upload to glob
We can now create a glob from the directory. To do so, navigate to `http://localhost:8080/docket/upload` in the browser. This will bring up the `%docket` app's [Globulator](/guides/additional/dist/glob#globulator) tool:
![Globulator](https://media.urbit.org/guides/additional/dist/globulator.png)
Simply select the `hello` desk from the drop-down, click `Choose file` and select the `hello-glob` folder in the the file browser, then hit `glob!`.
Now if we return to our ship's homescreen, we should see the tile looks as we specified in the docket file:
![Installed Tile](https://media.urbit.org/guides/additional/dist/local-install-2.png)
And if we click on the tile, it'll load the `index.html` in our glob:
![Hello World!](https://media.urbit.org/guides/additional/dist/local-install-3.png)
Our app is working!
## Publish
The final step is publishing our desk with the `%treaty` agent so others can install it. To do this, there's a simple command in the dojo:
```
> :treaty|publish %hello
>=
```
Note: For desks without a docket file (and therefore without a tile and glob), treaty can't be used. Instead you can make the desk public with `|public %desk-name`.
## Remote install
Let's spin up another fake ship so we can try install it:
```
[user@host hello-glob]$ cd ~/piers/fake
[user@host fake]$ urbit -F bus
```
Note: For desks without a docket file (and therefore without a tile and glob), users cannot install them through the web interface. Instead remote users can install it from the dojo with `|install ~our-ship %desk-name`.
In the browser, navigate to `http://localhost:8081` and login with `~bus`'s code `riddec-bicrym-ridlev-pocsef`. Next, type `~zod/` in the search bar, and it should pop up a list of `~zod`'s published apps, which in this case is our `Hello` app:
![Remote install search](https://media.urbit.org/guides/additional/dist/remote-install-1.png)
When we click on the app, it'll show some of the information from the clauses in the docket file:
![Remote app info](https://media.urbit.org/guides/additional/dist/remote-install-2.png)
Click `Get App` and it'll ask as if we want to install it:
![Remote app install](https://media.urbit.org/guides/additional/dist/remote-install-3.png)
Finally, click `Get "Hello"` and it'll be installed as a tile on `~bus` which can then be opened:
![Remote app finished](https://media.urbit.org/guides/additional/dist/remote-install-4.png)

View File

@ -0,0 +1,530 @@
+++
title = "Strings"
weight = 40
+++
This document discusses hoon's two main string types: `cord`s (as well as its
subsets `knot` and `term`) and `tape`s. The focus of this
document is on their basic properties, syntax and the most common text-related
functions you'll regularly encounter. In particular, it discusses conversions
and the encoding/decoding of atom auras in strings.
Hoon has a system for writing more elaborate functional parsers, but that is not
touched on here. Instead, see the [Parsing](/guides/additional/hoon/parsing) guide.
Hoon also has a type for UTF-32 strings, but those are rarely used and not
discussed in this document.
There are a good deal more text manipulation functions than are discussed here.
See the [Further Reading](#further-reading) section for details.
## `tape`s vs. text atoms
As mentioned, urbit mainly deals with two kinds of strings: `tape`s and
`cord`/`knot`/`term`s. The former is a list of individual UTF-8 characters.
The latter three encode UTF-8 strings in a single atom.
Cords may contain any UTF-8 characters, while `knot`s and `term`s only allow a
smaller subset. Each of these are discussed below in the [Text
atoms](#text-atoms) section.
Text atoms like `cord`s are more efficient to store and move around. They are
also more efficient to manipulate with simple bitwise operations. Their downside
is that UTF-8 characters vary in their byte-length. ASCII characters are all
8-bit, but others can occupy up to four bytes. Accounting for this variation in
character size can complicate otherwise simple functions. Tapes, on the other
hand, don't have this problem because each character is a separate item in the
list, regardless of it byte-length. This fact makes it much easier to process
tapes in non-trivial ways with simple list functions.
In light of this, a general rule of thumb is to use cords for simple things like
storing chat messages or exchanging them over the network. If text requires
complex processing on the other hand, it is generally easier with tapes. Note
there _are_ cord manipulation functions in the standard library, so you needn't
always convert cords to tapes for processing, it just depends on the case.
Next we'll look at these different types of strings in more detail.
## Text atoms
### `cord`
A [`cord`](/reference/hoon/stdlib/2q#cord) has an aura of `@t`. It denotes
UTF-8 text encoded in an atom, little-endian. That is, the first character in
the text is the least-significant byte. A cord may contain any UTF-8 characters,
there are no restrictions.
The `hoon` syntax for a cord is some text wrapped in single-quotes like:
```hoon
'This is a cord!'
```
single-quotes and backslashes must be escaped with a backslash like:
```hoon
'\'quotes\' \\backslashes\\'
```
Characters can also be entered as hex, they just have to be escaped by a
backslash. For example, `'\21\21\21'` will render as `'!!!'`. This is useful for
entering special characters such as line breaks like `'foo\0abar'`.
Cords divided over multiple lines are allowed. There are two ways to do this.
The first is to start and end with three single-quotes like:
```hoon
'''
foo
bar
baz
'''
```
The line endings will be encoded Unix-style as line feed characters like:
```hoon
'foo\0abar\0abaz'
```
The second is to begin with a single-quote like usual, then break the line by
ending it with a backslash and start the next line with a forward-slash like:
```hoon
'foo\
/bar\
/baz'
```
This will be parsed to:
```hoon
'foobarbaz'
```
### `knot`
A [`knot`](/reference/hoon/stdlib/2q#knot) has an aura of `@ta`, and is a
subset of a [`cord`](#cord). It allows lower-case letters, numbers, and four
special characters: Hyphen, tilde, underscore and period. Its restricted set of
characters is intended to be URL-safe.
The `hoon` syntax for a knot is a string containing any of the aforementioned
characters prepended with `~.` like:
```hoon
~.abc-123.def_456~ghi
```
### `term`
A [`term`](/reference/hoon/stdlib/2q#term) has an aura of `@tas`, and is a
subset of a [`knot`](#knot). It only allows lower-case letters, numbers, and
hyphens. Additionally, the first character cannot be a hyphen or number. This is
a very restricted text atom, and is intended for naming data structures and the
like.
The `hoon` syntax for a term is a string conforming to the prior description,
prepended with a `%` like:
```hoon
%foo-123
```
#### A note about `term` type inference
There is actually an even more restricted text atom form with the same `%foo`
syntax as a term, where the type of the text is the text itself. For example, in
the dojo:
```
> `%foo`%foo
%foo
```
The hoon parser will, by default, infer the type of `%foo`-style syntax this
way. If we try with the dojo type printer:
```
> ? %foo
%foo
%foo
```
This type-as-itself is used for many things, such as unions like:
```hoon
?(%foo %bar %bas)
```
In order to give `%foo` the more generic `@tas` aura, it must be explicitly
upcast like:
```
> ? `@tas`%foo
@tas
%foo
```
This is something to be wary of. For example, if you wanted to form a `(set @tas)` you might think to do:
```hoon
(silt (limo ~[%foo %bar %baz]))
```
However, this will actually form a set of the union `?(%foo %bar %baz)` due to
the specificity of type inference:
{% customFence %}
\> ? (silt (limo ~[%foo %bar %baz]))
?(%~ [?(n=%bar n=%baz n=%foo) l=nlr(?(%bar %baz %foo)) r=nlr(?(%bar %baz %foo))])
[n=%baz l&#x7B;&#x25;bar} r=&#x7B;&#x25;foo}]
{% /customFence %}
One further note about the type-as-itself form: Ocassionally you may wish to
form a union of strings which contain characters disallowed in `term`s. To get
around this, you can enclose the text after the `%` with single-quotes like
`%'HELLO!'`.
### Aura type validity
The hoon parser will balk at `cord`s, `knot`s and `term`s containing invalid
characters. However, because they're merely auras, any atom can be cast to them.
When cast (or clammed), they will **not** be validated in terms of whether the
characters are allowed in the specified aura.
For example, you can do this:
```
> `@tas`'!%* $@&'
%!%* $@&
```
This means you cannot rely on mere aura-casting if you need the text to conform
to the specified aura's restrictions. Instead, there are a couple of function in
the standard library to check text aura validity:
[`+sane`](/reference/hoon/stdlib/4b#sane) and
[`+sand`](/reference/hoon/stdlib/4b#sane).
The `+sane` function takes an argument of either `%ta` or `%tas` to validate
`@ta` and `@tas` respectively (you can technically give it `%t` for `@t` too but
there's no real point). It will return `%.y` if the given atom is valid for the
given aura, and `%.n` if it isn't. For example:
```
> ((sane %tas) 'foo')
%.y
> ((sane %tas) 'foo!')
%.n
```
The `+sand` function does the same thing, but rather than returning a `?` it
returns a `unit` of the given atom, or `~` if validation failed. For example:
```
> `(unit @tas)`((sand %tas) 'foo')
[~ %foo]
> `(unit @tas)`((sand %tas) 'foo!')
~
```
## `tape`
A [`tape`](/reference/hoon/stdlib/2q#tape) is the other
main string type in hoon. Rather than a single atom, it's instead a list of
individual `@tD` characters (the `D` specifies a bit-length of 8, see the
[Auras](/reference/hoon/auras#bitwidth) documentation for
details). The head of the list is the first character in the string.
The `hoon` syntax for a tape is some text wrapped in double-quotes like:
```hoon
"This is a tape!"
```
Double-quotes, backslashes and left-braces must be escaped by a backslash
character:
```hoon
"\"double-quotes\" \\backslash\\ left-brace:\{"
```
Like with `cord`s, characters can also be entered as hex escaped by a backslash
so `"\21\21\21"` renders as `"!!!"`.
Tapes divided over multiple lines are allowed. Unlike [`cord`](#cord)s, there is
only one way to do this, which is by starting and ending with three
double-quotes like:
```hoon
"""
foo
bar
baz
"""
```
The line endings will be encoded Unix-style as line feed characters like:
```hoon
"foo\0abar\0abaz"
```
As mentioned earlier, tapes are lists of single characters:
```
> `tape`~['f' 'o' 'o']
"foo"
```
This means they can be manipulated with ordinary list functions:
```
> `tape`(turn "foobar" succ)
"gppcbs"
```
### Interpolation
Tapes, unlike cords, allow string interpolation. Arbitrary `hoon` may be
embedded in the tape syntax and its product will be included in the resulting
tape. There are two ways to do it:
#### Manual
In the first case, the code to be evaluated is enclosed in braces. The type of
the product of the code must itself be a tape. For example, if the `@p` of our
ship is stored in `our`, simply doing `"{our}"` will fail because its type will
be `@p` rather than `tape`. Instead, we must explicitly use the
[`+scow`](/reference/hoon/stdlib/4m#scow) function to
render `our` as a tape:
```
> "{(scow %p our)}"
"~zod"
```
Another example:
```
> "[{(scow %p our)} {(scow %da now)}]"
"[~zod ~2021.10.3..08.59.10..2335]"
```
#### Automatic
Rather than having to manually render data as a `tape`, angle brackets _inside_
the braces tell the interpreter to automatically pretty-print the product of the
expression as a tape. This way we needn't use functions like `+scow` and can
just reference things like `our` directly:
```
> "{<our>}"
~zod
```
Another example:
```
> "{<(add 1 2)>}"
"3"
```
And another:
```
> "{<our now>}"
"[~zod ~2021.10.3..09.01.14..1654]"
```
## Conversions
Tapes can easily be converted to cords and vice versa. There are two stdlib
functions for this purpose: [`+crip`](/reference/hoon/stdlib/4b#crip) and
[`+trip`](/reference/hoon/stdlib/4b#trip). The former converts a `tape` to
a `cord` and the latter does the opposite. For example:
```
> (crip "foobar")
'foobar'
> (trip 'foobar')
"foobar"
```
Knots and terms can also be converted to tapes with `+trip`:
```
> (trip %foobar)
"foobar"
> (trip ~.foobar)
"foobar"
```
Likewise, the output of `+crip` can be cast to a knot or term:
```
> `@tas`(crip "foobar")
%foobar
> `@ta`(crip "foobar")
~.foobar
> `@tas`(need ((sand %tas) (crip "foobar")))
%foobar
```
## Encoding in text
It's common to encode atoms in cords or knots, particularly when constructing a
[scry](/reference/arvo/concepts/scry) [`path`](/reference/hoon/stdlib/2q#path)
or just a `path` in general. There are two main functions for this purpose:
[`+scot`](/reference/hoon/stdlib/4m#scot) and
[`+scow`](/reference/hoon/stdlib/4m#scow). The former produces a `knot`,
and the latter produces a `tape`. Additionally, there are two more functions for
encoding `path`s in cords and tapes respectively:
[`+spat`](/reference/hoon/stdlib/4m#spat) and
[`+spud`](/reference/hoon/stdlib/4m#spud).
### `+scot` and `+spat`
`+scot` encodes atoms of various auras in a `knot` (or `cord`/`term` with
casting). It takes two arguments: the aura in a `@tas` and the atom to be
encoded. For example:
```
> (scot %p ~zod)
~.~zod
> (scot %da now)
~.~2021.10.4..07.35.54..6d41
> (scot %ux 0xaa.bbbb)
~.0xaa.bbbb
```
Note the aura of the atom needn't actually match the specified aura:
```
> (scot %ud ~zod)
~.0
```
Hoon can of course be evaluated in its arguments as well:
```
> (scot %ud (add 1 1))
~.2
```
You'll most commonly see this used in constructing a `path` like:
```
> /(scot %p our)/garden/(scot %da now)/foo/(scot %ud 123.456)
[~.~zod %garden ~.~2021.10.4..07.43.14..a556 %foo ~.123.456 ~]
> `path`/(scot %p our)/garden/(scot %da now)/foo/(scot %ud 123.456)
/~zod/garden/~2021.10.4..07.43.23..9a0f/foo/123.456
```
`+spat` simply encodes a `path` in a cord like:
```
> (spat /foo/bar/baz)
'/foo/bar/baz'
```
### `+scow` and `+spud`
`+scow` is the same as [`+scot`](#scot-and-spat) except it produces a tape
rather than a knot. For example:
```
> (scow %p ~zod)
"~zod"
> (scow %da now)
"~2021.10.4..07.45.25..b720"
> (scow %ux 0xaa.bbbb)
"0xaa.bbbb"
```
`+spud` simply encodes a `path` in a tape:
```
> (spud /foo/bar/baz)
"/foo/bar/baz"
```
## Decoding from text
For decoding atoms of particular auras encoded in cords, there are three
functions: [`+slat`](/reference/hoon/stdlib/4m#slat),
[`+slav`](/reference/hoon/stdlib/4m#slav), and
[`+slaw`](/reference/hoon/stdlib/4m#slaw). Additionally, there is
[`+stab`](/reference/hoon/stdlib/4m#stab) for decoding a cord to a path.
`+slav` parses the given cord with the aura specified as a `@tas`, crashing if
the parsing failed. For example:
```
> `@da`(slav %da '~2021.10.4..11.26.54')
~2021.10.4..11.26.54
> `@p`(slav %p '~zod')
~zod
> (slav %p 'foo')
dojo: hoon expression failed
```
`+slaw` is like `+slav` except it produces a `unit` which is null if parsing
failed, rather than crashing. For example:
```
> `(unit @da)`(slaw %da '~2021.10.4..11.26.54')
[~ ~2021.10.4..11.26.54]
> `(unit @p)`(slaw %p '~zod')
[~ ~zod]
> (slaw %p 'foo')
~
```
`+slat` is a curried version of `+slaw`, meaning it's given the aura and
produces a new gate which takes the actual cord. For example:
```
> `(unit @da)`((slat %da) '~2021.10.4..11.26.54')
[~ ~2021.10.4..11.26.54]
> `(unit @p)`((slat %p) '~zod')
[~ ~zod]
> ((slat %p) 'foo')
~
```
Finally, `+stab` parses a cord containing a path to a `path`. For example:
```
> (stab '/foo/bar/baz')
/foo/bar/baz
```
## Futher reading
- [Parsing](/guides/additional/hoon/parsing) - A guide to writing fully-fledged
functional parsers in hoon.
- [Auras](/reference/hoon/auras) - Details of auras in hoon.
- [stdlib 2b: List logic](/reference/hoon/stdlib/2b) - Standard library
functions for manipulating lists, which are useful for dealing with tapes.
- [stdlib 2q: Molds and Mold-builders](/reference/hoon/stdlib/2q) - Several
text types are defined in this section of the standard library.
- [stdlib 4b: Text processing](/reference/hoon/stdlib/4b) - Standard
library functions for manipulating and converting tapes and strings.
- [stdlib 4m: Formatting functions](/reference/hoon/stdlib/4m) - Standard
library functions for encoding and decoding atom auras in strings.

View File

@ -0,0 +1,114 @@
+++
title = "Unit tests"
weight = 45
+++
## Structure
The `%base` desk includes a `-test` thread which can run unit tests you've
written. A test is a Hoon file which produces a `core`. The `-test` thread will
look for any arms in the core whose name begin with `test-`, e.g:
```hoon
|%
++ test-foo
...
++ test-bar
...
++ test-foo-bar
...
--
```
Any arms that don't begin with `test-` will be ignored. Each `test-` arm must
produce a `tang` (a `(list tank)`). If the `tang` is empty (`~`), it indicates
success. If the `tang` is non-empty, it indicates failure, and the contents of
the `tang` is the error message.
To make test-writing easier, the `%base` desk includes the `/lib/test.hoon`
library which you can import into your test file. The library contains four
functions which all produce `tang`s:
- `expect-eq` - test whether an expression produces the expected value. This
function takes `[expected=vase actual=vase]`, comparing `.expected` to
`.actual`.
- `expect` - test whether an expression produces `%.y`. This function takes a
`vase` containing the result to check.
- `expect-fail` - tests whether the given `trap` crashes, failing if it succeeds.
- `category` - this is a utility that prepends an error message to a failed test
(non-null `tang`), passing through an empty `tang` (successful test)
unchanged.
The most commonly used function is `expect-eq`, which is used like:
```hoon
++ test-foo
%+ expect-eq
!> 'the result I expect'
!> (function-i-want-to-test 'some argument')
```
Of course, you'll want to test something else you've written rather than just
expressions in the test file itself. To do that, you'd just import the file with
`/=` or a similar Ford rune, and then call its functions in the test arms.
You're free to do any compositions, import types, etc, as long as the file
ultimately produces a `core` with `test-*` arms.
## Running
The `-test` thread takes a `(list path)` in the Dojo, where each path is a path
to a test file. The `path` _must_ include the full path prefix
(`/[ship]/[desk]/[case]`). The `path` _may_ omit the mark, since a `.hoon` file
is assumed. The `path` _may_ include the name of a test arm after the filename.
In that case, only the specified test arm will be run.
The conventional location for tests is a `/tests`
directory in the root of a desk.
The output of the `-test` thread will note which arms were tested and whether
they succeeded. It will also include:
- The number of micro-seconds it took to execute each test arm.
- A `?` specifying whether all tests succeeded.
- A message confirming the file was built successfully.
Here's an example of running the tests for the `naive.hoon` library:
```
> -test %/tests/lib/naive ~
built /tests/lib/naive/hoon
> test-zod-spawn-to-zero: took 81359µs
OK /lib/naive/test-zod-spawn-to-zero
> test-zod-spawn-proxy: took 128125µs
OK /lib/naive/test-zod-spawn-proxy
.............................
....truncated for brevity....
.............................
> test-approval-for-all: took 647403µs
OK /lib/naive/test-approval-for-all
> test-address-padding: took 75104µs
OK /lib/naive/test-address-padding
ok=%.y
```
Here's an example of running just a single test for `naive.hoon`, the
`++test-deposit` arm:
```
> -test %/tests/lib/naive/test-deposit ~
built /tests/lib/naive/hoon
> test-deposit: took ms/45.542
OK /lib/naive/test-deposit
ok=%.y
```
## More info
A good reference example is the test file for the `/lib/number-to-words.hoon`
library, located in `/tests/lib/number-to-words.hoon`. Note that the `/tests`
directory is not typically included in standard pills. If you want to have a
look at existing tests as a reference, you may need to clone the `urbit/urbit`
repo on [Github](https://github.com/urbit/urbit).
If you write tests for some of your code, you may wish to exclude the `/tests`
directory from the production version of your desk.

View File

@ -0,0 +1,5 @@
+++
title = "Core Curriculum"
weight = 2
type = "tab"
+++

View File

@ -0,0 +1,87 @@
+++
title = "1. Introduction"
weight = 1
+++
This series walks through the writing of a full Gall agent, and then the process
of integrating it with a React front-end. This series follows on from [App School I](/guides/core/app-school/intro). If you haven't
completed that, or otherwise aren't familiar with the basics of writing Gall
agents, it's strongly recommended to work through that guide first.
The app we'll be looking at is a simple journal with an agent called `%journal`.
In the browser, users will be able to add plain text journal entries organized
by date. Entries may be scrolled through in ascending date order, with more
entries loaded each time the bottom of the list is reached. Old entries will be
able to be edited and deleted, and users will be able to search through entries
by specifying a date range.
The `Journal` app we'll be looking at can be installed on a live ship from
`~pocwet/journal`, and its source code is available [here](https://github.com/urbit/docs-examples/tree/main/journal-app).
![journal ui screenshot](https://media.urbit.org/guides/core/app-school-full-stack-guide/entries.png)
This walkthrough does not contain exercises, nor does it completely cover every
aspect of building the app in full depth. Rather, its purpose is to demonstrate
the process of creating a full-stack Urbit app, showing how everything fits
together, and how concepts you've previously learned are applied in practice.
The primary focus of the walkthrough is to show how a Javascript front-end is
integrated with a Gall agent and distributed as a complete app. Consequently,
the example app is fairly simple and runs on a local ship only, rather than one
with more complex inter-ship networking.
Each section of this walkthrough will list additional resources and learning
material at the bottom of the page, which will cover the concepts discussed in a
more comprehensive manner.
Here is the basic structure of the app we'll be building:
![journal app
diagram](https://media.urbit.org/guides/core/app-school-full-stack-guide/journal-app-diagram.svg)
## Sections
#### [1. Introduction](/guides/core/app-school-full-stack/1-intro)
An overview of the guide and table of contents.
#### [2. Types](/guides/core/app-school-full-stack/2-types)
Creating the `/sur` structure file for our `%journal` agent.
#### [3. Agent](/guides/core/app-school-full-stack/3-agent)
Creating the `%journal` agent itself.
#### [4. JSON](/guides/core/app-school-full-stack/5-json)
Writing a library to convert between our agent's marks and JSON. This lets our
React front-end poke our agent, and our agent send updates back to it.
#### [5. Marks](/guides/core/app-school-full-stack/4-marks)
Creating the mark files for the pokes our agent takes and updates it sends out.
#### [6. Eyre](/guides/core/app-school-full-stack/6-eyre)
A brief overview of how the webserver vane Eyre works.
#### [7. React App Setup](/guides/core/app-school-full-stack/7-react-setup)
Creating a new React app, installing the required packages, and setting up some
basic things for our front-end.
#### [8. React App Logic](/guides/core/app-school-full-stack/8-http-api)
Analyzing the core logic of our React app, with particular focus on using
methods of the `Urbit` class from `@urbit/http-api` to communicate with our
agent.
#### [9. Desk and Glob](/guides/core/app-school-full-stack/9-web-scries)
Building and "globbing" our front-end, and putting together a desk for
distribution.
#### [10. Summary](/guides/core/app-school-full-stack/10-final)
Some final comments and additional resources.

View File

@ -0,0 +1,117 @@
+++
title = "10. Summary"
weight = 10
+++
That's it! We've built our agent and React front-end, put together a desk and
published it. We hope this walkthrough has helped you see how all the pieces
for together for building and distributing an app in Urbit.
The reference material for each section of this walkthrough is listed
[below](#reference-material), the source code for our app is available
[here](https://github.com/urbit/docs-examples/tree/main/journal-app), and it can
be installed from `~pocwet/journal`.
In this guide we've built a separate React app for the front-end, but Hoon also
has a native domain-specific language for composing HTML structures called Sail.
Sail allows you to compose a front-end inside a Gall agent and serve it
directly. See the [Sail guide](/guides/additional/hoon/sail) for details.
Along with `@urbit/http-api`, there's also the `@urbit/api` NPM package, which
contains a large number of helpful functions for dealing with Hoon data types
and interacting with a number of agents - particularly those used by the Groups
app. Its source code is [available
here](https://github.com/urbit/urbit/tree/master/pkg/npm/api).
## Reference material
Here is the reference material for each section of this walkthrough.
#### Types
- [App School /sur section](/guides/core/app-school/7-sur-and-marks#sur) -
This section of App School covers writing a `/sur` structure library for
an agent.
- [Ordered map functions in
`zuse.hoon`](https://github.com/urbit/urbit/blob/master/pkg/arvo/sys/zuse.hoon#L5284-L5688) -
This section of `zuse.hoon` contains all the functions for working with
`mop`s, and is well commented.
#### Agent
- [App School I](/guides/core/app-school/intro) - App School I covers all
aspects of writing Gall agents in detail.
- [Ordered map functions in
`zuse.hoon`](https://github.com/urbit/urbit/blob/master/pkg/arvo/sys/zuse.hoon#L5284-L5688) -
This section of `zuse.hoon` contains all the functions for working with
`mop`s, and is well commented.
- [`/lib/agentio.hoon`](https://github.com/urbit/urbit/blob/master/pkg/base-dev/lib/agentio.hoon) -
The `agentio` library in the `%base` desk contains a large number of useful
functions which making writing Gall agents easier.
#### JSON
- [The JSON Guide](/guides/additional/hoon/json-guide) - The stand-alone JSON guide
covers JSON encoding/decoding in great detail.
- [The Zuse Reference](/reference/hoon/zuse/table-of-contents) - The
`zuse.hoon` reference documents all JSON-related functions in detail.
- [`++enjs:format` reference](/reference/hoon/zuse/2d_1-5#enjsformat) -
This section of the `zuse.hoon` documentation covers all JSON encoding
functions.
- [`++dejs:format` reference](/reference/hoon/zuse/2d_6) - This section of
the `zuse.hoon` documentation covers all JSON _decoding_ functions.
- [Eyre Overview](/reference/arvo/eyre/eyre) - This section of the Eyre vane
documentation goes over the basic features of the Eyre vane.
#### Marks
- [The Marks section of the Clay documentation](/reference/arvo/clay/marks/marks) -
This section of the Clay vane documentation covers mark files comprehensively.
- [The mark file section of the Gall
Guide](/guides/core/app-school/7-sur-and-marks#mark-files) - This part of
App School goes through the basics of mark files.
- [The JSON Guide](/guides/additional/hoon/json-guide) - This also covers writing mark
files to convert to/from JSON.
#### Eyre
- [The Eyre vane documentation](/reference/arvo/eyre/eyre) - This section of the vane
docs covers all aspects of Eyre.
- [Eyre External API Reference](/reference/arvo/eyre/external-api-ref) - This section
of the Eyre documentation contains reference material for Eyre's external API.
- [The Eyre Guide](/reference/arvo/eyre/guide) - This section of the Eyre
documentation walks through using Eyre's external API at a low level (using
`curl`).
#### React App Setup and Logic
- [HTTP API Guide](/guides/additional/http-api-guide) - Reference documentation for
`@urbit/http-api`.
- [React app source
code](https://github.com/urbit/docs-examples/tree/main/journal-app/ui) - The
source code for the Journal app UI.
- [`@urbit/http-api` source
code](https://github.com/urbit/urbit/tree/master/pkg/npm/http-api) - The
source code for the `@urbit/http-api` NPM package.
#### Desk and Glob
- [App publishing/distribution docs](/guides/additional/dist/dist) -
Documentation covering third party desk composition, publishing and
distribution.
- [Glob documentation](/guides/additional/dist/glob) - Comprehensive documentation
of handling front-end files.
- [Desk publishing guide](/guides/additional/dist/guide) - A step-by-step guide to
creating and publishing a desk.

View File

@ -0,0 +1,214 @@
+++
title = "2. Types"
weight = 2
+++
The best place to start when building a new agent is its type definitions in its
`/sur` structure file. The main things to think through are:
1. What basic types of data does my agent deal with?
2. What actions/commands does my agent need to handle?
3. What updates/events will my agent need to send out to subscribers?
4. What does my agent need to store in its state?
Let's look at each of these questions in turn, and put together our agent's
`/sur` file, which we'll call `/sur/journal.hoon`.
### 1. Basic types
Our journal entries will just be plain text, so a simple `@t` will work fine to
store their contents. Entries will be organized by date, so we'll also need to
decide a format for that.
One option would be to use an `@da`, and then use the date functions included
in the `@urbit/api` NPM package on the front-end to convert them to ordinary
Javascript `Date` objects. In this case, to keep it simple, we'll just use the
number of milliseconds since the Unix Epoch as an `atom`, since it's natively
supported by the Javascript `Date` object.
The structure for a journal entry can therefore be:
```hoon
+$ id @
+$ txt @t
+$ entry [=id =txt]
```
### 2. Actions
Now that we know what a journal entry looks like, we can think about what kind
of actions/commands our agent will handle in its `++on-poke` arm. For our
journal app, there are three basic things we might do:
1. Add a new journal entry.
2. Edit an existing journal entry.
3. Delete an existing journal entry.
We can create a tagged union structure for these actions, like so:
```hoon
+$ action
$% [%add =id =txt]
[%edit =id =txt]
[%del =id]
==
```
### 3. Updates
Updates are a little more complicated than our actions. Firstly, our front-end
needs to be able to retrieve an initial list of journal entries to display. Once
it has that, it also needs to be notified of any changes. For example, if a new
entry is added, it needs to know so it can add it to the list it's displaying.
If an entry gets deleted, it needs to remove it from the list. Etc.
The simplest approach to the initial entries is just a `(list entry)`. Then, for
the subsequent updates, we could send out the `$action`. Since an `$action` is a
tagged union, it's simpler to have all updates be a tagged union, so when we get
to doing mark conversions we can just switch on the head tag. Therefore, we can
define an `$update` structure like so:
```hoon
+$ update
$% action
[%jrnl list=(list entry)]
==
```
There's one drawback to this structure. Suppose either an agent on a remote ship
or an instance of the front-end client is subscribed for updates, and the
network connection is disrupted. In the remote ship case, Gall will only allow
so many undelivered messages to accumulate in Ames before it automatically kicks
the unresponsive subscriber. In the front-end case, the subscription will also
be ended if enough unacknowledged messages accumulate, and additionally the
client may sometimes need to establish an entirely new connection with the ship,
discarding existing subscriptions. When this happens, the remote ship or web
client has no way to know how many (if any) updates they've missed.
The only way to resynchronize their state with ours is to discard their existing
state, refetch the entire initial state once again, and then resubscribe for
updates. This might be fine if the state of our agent is small, but it becomes a
problem if it's very large. For example, if our agent holds tens of thousands of
chat messages, having to resend them all every time anyone has connectivity
issues is quite inefficient.
One solution to this is to keep an _update log_. Each update can be tagged with
the time it occurred, and stored in our agent's state, separately to the
entries. If an agent or web client needs to resynchronize with our agent, it can
just request all updates since the last one it received. This approach is used
by the `%graph-store` agent, for example. Our agent is local-only and doesn't
have a huge state so it might not be strictly necessary, but we'll use it to
demonstrate the approach.
We can define a logged update like so, where the `@` is the update timestamp in
milliseconds since the Unix Epoch:
```hoon
+$ logged (pair @ action)
+$ update
%+ pair @
$% action
[%jrnl list=(list entry)]
[%logs list=(list logged)]
==
```
### 4. State
We need to store two things in our state: the journal entries and the update
log. We could just use a couple of `map`s like so:
```hoon
+$ journal (map id txt)
+$ log (map @ action)
```
Ordinary `map`s are fine if we just want to access one value at a time, but we
want to be able to:
1. Retrieve only some of the journal entries at a time, so we can have "lazy
loading" in the front-end, loading more entries each time the user scrolls to
the bottom of the list.
2. Retrieve only logged updates newer than a certain time, in the case where the
subscription is interrupted due to connectivity issues.
3. Retrieve journal entries between two dates.
Maps are ordered by the hash of their key, so if we convert them to a list
they'll come out in seemingly random order. That means we'd have to convert the
map to a list, sort the list, and then iterate over it again to pull out the
items we want. We could alternatively store things in a list directly, but
retrieving or modifying arbitrary items would be less efficient.
To solve this, rather than using a `map` or a `list`, we can use an _ordered
map_. The mold builder for an ordered map is a `mop`, and it's included in the
[`zuse.hoon`](https://github.com/urbit/urbit/blob/master/pkg/arvo/sys/zuse.hoon#L5284)
utility library rather than the standard library.
A `mop` is defined similarly to a `map`, but it takes an extra argument in the
following manner:
```hoon
((mop key-mold val-mold) comparator-gate)
```
The gate is a binary gate which takes two keys and produces a `?`. The
comparator is used to decide how to order the items in the mop. In our case,
we'll create a `$journal` and `$log` `mop` like so:
```hoon
+$ journal ((mop id txt) gth)
+$ log ((mop @ action) lth)
```
The entries in `$journal` are arranged in ascending time order using `++gth`, so
the right-most item is the newest. The `$log` `mop` contains the update log, and
is arranged in descending time order, so the right-most item is the oldest.
We'll look at how to use ordered maps later when we get to writing the agent
itself.
## Conclusion
When we put each of these parts together, we have our complete
`/sur/journal.hoon` file:
```hoon
|%
:: Basic types of the data we're dealing with
::
+$ id @
+$ txt @t
+$ entry [=id =txt]
:: Poke actions
::
+$ action
$% [%add =id =txt]
[%edit =id =txt]
[%del =id]
==
:: Types for updates to subscribers or returned via scries
::
+$ logged (pair @ action)
+$ update
%+ pair @
$% action
[%jrnl list=(list entry)]
[%logs list=(list logged)]
==
:: Types for our agent's state
::
+$ journal ((mop id txt) gth)
+$ log ((mop @ action) lth)
--
```
## Resources
- [App School I /sur section](/guides/core/app-school/7-sur-and-marks#sur) -
This section of App School covers writing a `/sur` structure library for
an agent.
- [Ordered map functions in
`zuse.hoon`](https://github.com/urbit/urbit/blob/master/pkg/arvo/sys/zuse.hoon#L5284-L5688) -
This section of `zuse.hoon` contains all the functions for working with
`mop`s, and is well commented.

View File

@ -0,0 +1,337 @@
+++
title = "3. Agent"
weight = 3
+++
Now that we have our agent's types defined and have thought through its
behavior, we can write the `%journal` agent itself.
## Imports
```hoon
/- *journal
/+ default-agent, dbug, agentio
```
We first import the `/sur/journal.hoon` file we previously created and expose
its structures. We import the standard `default-agent` and `dbug`, and also an additional library called `agentio`.
Agentio contains a number of convenience functions to make common agent tasks
simpler. For example, rather than writing out the full `$card`s when sending
`%fact`s to subscribers, we can call `++fact` in `agentio` with the `cage` and
`path`s and it will compose them for us. There are many more functions in
`agentio` than we'll use here - you can have a look through the library in
[`/base/lib/agentio.hoon`](https://github.com/urbit/urbit/blob/master/pkg/base-dev/lib/agentio.hoon)
to see what else it can do.
## State and type core
```hoon
|%
+$ versioned-state
$% state-0
==
+$ state-0 [%0 =journal =log]
+$ card card:agent:gall
++ j-orm ((on id txt) gth)
++ log-orm ((on @ action) lth)
++ unique-time
|= [=time =log]
^- @
=/ unix-ms=@
(unm:chrono:userlib time)
|-
?. (has:log-orm log unix-ms)
unix-ms
$(time (add unix-ms 1))
--
```
As we discussed in the previous section, our state will contain a `$journal`
structure containing all our journal entries, and a `$log` structure containing
the update log. These are both _ordered maps_, defined as `((mop id txt) gth)`
and `((mop @ action) lth)` respectively. We can therefore define our _versioned
state_ as `[%0 =journal =log]`, in the usual manner.
We've define `$card` for convenience as usual, and we've also added three more
arms. The first two relate to our two ordered maps. If you'll recall, an
ordinary `map` is called with the `++by` door in the standard library, like so:
```hoon
(~(get by foo) %bar)
```
An ordered map uses the `++on` gate in `zuse.hoon` rather than `++by`, and its
invocation is slightly different. It must first be setup in a similar manner to
the `mop` type, by providing it the key/value molds and comparator gates. Once
that's done, its individual functions can be called with the `mop` and
arguments, like:
```hoon
(get:((on @ud @ud) gth) foo %bar)
```
This is quite a cumbersome expression to use every time we want to interact with
our `mop`. To make it easier, we can store the `((on @ud @ud) gth)` part in an
arm, and then when we need to use it we can just do `(get:arm-name foo %bar)`.
In this case, we've done one each of our ordered maps like so:
```hoon
++ j-orm ((on id txt) gth)
++ log-orm ((on @ action) lth)
```
The last arm in our state definition core is `++unique-time`. Since we'll use
`now.bowl` to derive the timestamp for updates, we run into an issue if multiple
pokes arrive in a single Arvo event. In that case, `now.bowl` would be the same
for each poke, so they'd be given the same key and override each other in the
`mop`. To avoid this, `++unique-time` is just a simple recursive function that
will increment the timestamp by one millisecond if the key already exists in the
`$log` `mop`, ensuring all updates get unique timestamps and there are no
collisions.
## Agent core setup
```hoon
%- agent:dbug
=| state-0
=* state -
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
io ~(. agentio bowl)
++ on-init on-init:def
++ on-save
^- vase
!>(state)
::
++ on-load
|= old-vase=vase
^- (quip card _this)
`this(state !<(versioned-state old-vase))
::
```
Here we setup our agent core and define the three lifecycle arms. Since we only
have a single state version at present, these are very simple functions. You'll
notice in our `+*` arm, along with the usual `this` and `def`, we've also setup
the `agentio` library we imported, giving it the bowl and an alias of `io`.
## Pokes
```hoon
++ on-poke
|= [=mark =vase]
^- (quip card _this)
|^
?> (team:title our.bowl src.bowl)
?. ?=(%journal-action mark) (on-poke:def mark vase)
=/ now=@ (unique-time now.bowl log)
=/ act !<(action vase)
=. state (poke-action act)
:_ this(log (put:log-orm log now act))
~[(fact:io journal-update+!>(`update`[now act]) ~[/updates])]
::
++ poke-action
|= act=action
^- _state
?- -.act
%add
?< (has:j-orm journal id.act)
state(journal (put:j-orm journal id.act txt.act))
::
%edit
?> (has:j-orm journal id.act)
state(journal (put:j-orm journal id.act txt.act))
::
%del
?> (has:j-orm journal id.act)
state(journal +:(del:j-orm journal id.act))
==
--
::
```
Here we have our `++on-poke` arm, where we handle `$action`s. Since our
`%journal` agent is intended for local use only, we make sure only our ship or
our moons may perform actions with:
```hoon
?> (team:title our.bowl src.bowl)
```
We haven't yet written our mark files, but our mark for `$action`s will be
`%journal-action`, so we make sure that's what we've received and if not, call
`++on-poke:def` to crash with an error message. We make sure the the timestamps
are unique with our `++unique-time` function described earlier, and then we
extract the poke's vase to an `$action` structure and call `++poke-action` to
handle it. We've made `++on-poke` a door with a separate `++poke-action` arm to
make the logic a little simpler, but in principle we could have had it all
directly inside the main `++poke-action` gate, or even separated it out into a
helper core below.
The logic in `++poke-action` is very simple, with three cases for each of the possible `$action`s:
- `%add` - Add a new journal entry. We check it doesn't already exist with
`++has:j-orm`, and then add it to our `$journal` with `++put:j-orm`.
- `%edit` - Edit an existing journal entry. We make sure it _does_ exist with
`++has:j-orm`, and then override the old entry with the new one using
`++put:j-orm` again.
- `%del` - Delete an existing journal entry. We make sure it exists again with
`++has:j-orm`, and then use `++del:j-orm` to delete it from our `$journal`
`mop`.
Back in the main part of `++on-poke`, `++poke-action` updates the state with the
new `$journal`, then we proceed to:
```hoon
:_ this(log (put:log-orm log now act))
~[(fact:io journal-update+!>(`update`[now act]) ~[/updates])]
```
We add the timestamp to the action, converting it to a logged update. We add it
to the `$log` update log using `++put:log-orm`, and also send the logged update
out to subscribers on the `/updates` subscription path. We haven't written our
mark files yet, but `%journal-update` is the mark we'll use for `$update`s, so
we pack the `$update` in a vase and add the mark to make it a `$cage`. Notice
we're using the `++fact` function in `agentio` (which we aliased as `io`) rather
than manually composing the `%fact`.
## Subscriptions
```hoon
++ on-watch
|= =path
^- (quip card _this)
?> (team:title our.bowl src.bowl)
?+ path (on-watch:def path)
[%updates ~] `this
==
::
```
Our subscription logic is extremely simple - we just have a single `/updates`
path, which the front-end or other local agents may subscribe to. All updates
get sent out on this path. We enforce local-only with the `team:title` check.
We could have had our `++on-watch` arm send out some initial state to new
subscribers, but for our front-end we'll instead fetch the initial state
separately with a scry. This just makes it slightly easier if our front-end
needs to resubscribe at some point - it'll already have some state in that case
so we don't want it to get sent again.
## Scry Endpoints
```hoon
++ on-peek
|= =path
^- (unit (unit cage))
?> (team:title our.bowl src.bowl)
=/ now=@ (unm:chrono:userlib now.bowl)
?+ path (on-peek:def path)
[%x %entries *]
?+ t.t.path (on-peek:def path)
[%all ~]
:^ ~ ~ %journal-update
!> ^- update
[now %jrnl (tap:j-orm journal)]
::
[%before @ @ ~]
=/ before=@ (rash i.t.t.t.path dem)
=/ max=@ (rash i.t.t.t.t.path dem)
:^ ~ ~ %journal-update
!> ^- update
[now %jrnl (tab:j-orm journal `before max)]
::
[%between @ @ ~]
=/ start=@
=+ (rash i.t.t.t.path dem)
?:(=(0 -) - (sub - 1))
=/ end=@ (add 1 (rash i.t.t.t.t.path dem))
:^ ~ ~ %journal-update
!> ^- update
[now %jrnl (tap:j-orm (lot:j-orm journal `end `start))]
==
::
[%x %updates *]
?+ t.t.path (on-peek:def path)
[%all ~]
:^ ~ ~ %journal-update
!> ^- update
[now %logs (tap:log-orm log)]
::
[%since @ ~]
=/ since=@ (rash i.t.t.t.path dem)
:^ ~ ~ %journal-update
!> ^- update
[now %logs (tap:log-orm (lot:log-orm log `since ~))]
==
==
::
```
Here we have our `++on-peek` arm. The scry endpoints we've defined are divided
into two parts: querying the update `$log` and retrieving entries from the
`$journal`. Each end-point is as follows:
- `/x/entries/all` - Retrieve all entries in the `$journal`. Our front-end will
use lazy-loading and only get a few at a time, so it won't use this. It's nice
to have it though, in case other agents want to get that data.
- `/x/entries/before/[before]/[max]` - Retrieve at most `[max]` entries older
than the entry on `[before]` date. This is so our lazy-loading front-end can
progressively load more as the user scrolls down the page. The Javascript
front-end will format numbers without dot separators, so the path will look
like `/x/entries/before/1648051573109/10`. We therefore have to use the
[`++dem`](docs/hoon/reference/stdlib/4i#dem) parsing `rule` in a
[`++rash`](/reference/hoon/stdlib/4g#rash) parser to convert it to an
ordinary atom. We then use the `++tap:log-orm` `mop` function to retrieve the
requested range as a list and return it as an `$update` with a
`%journal-update` mark.
- `/x/entries/between/[start]/[end]` - Retrieve all journal entries between two
dates. This is so our front-end can have a search function, where the user can
enter a start and end date and get all the entries in between. The
`++lot:j-orm` `mop` function returns the subset of a `mop` between the two
given keys as a `mop`, and then we call `++tap:j-orm` to convert it to a list.
The `++lot:j-orm function` excludes the start and end values, so we subtract 1
from the start and add 1 to the end to make sure it includes the full range.
- `/x/updates/all` - Retrieve the entire update `$log`. Our front-end won't use
this but it might be useful for other agents, so we've included it here.
- `/x/updates/since/[since]` - Retrieve all `$update`s that have happened since
the specified timestamp, if any. This is so our front-end (or another agent)
can resynchronize its state in the event its subscription is interrupted,
without having to fetch everything from scratch again.
We don't use any of the other agent arms, so the remainder have all been passed
to `default-agent` for handling:
```hoon
++ on-leave on-leave:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--
```
The full agent source can be viewed
[here](https://github.com/urbit/docs-examples/blob/main/journal-app/desk/app/journal.hoon).
## Resources
- [App School I](/guides/core/app-school/intro) - App School I covers all
aspects of writing Gall agents in detail.
- [Ordered map functions in
`zuse.hoon`](https://github.com/urbit/urbit/blob/master/pkg/arvo/sys/zuse.hoon#L5284-L5688) -
This section of `zuse.hoon` contains all the functions for working with
`mop`s, and is well commented.
- [`/lib/agentio.hoon`](https://github.com/urbit/urbit/blob/master/pkg/base-dev/lib/agentio.hoon) -
The `agentio` library in the `%base` desk contains a large number of useful
functions which making writing Gall agents easier.

View File

@ -0,0 +1,356 @@
+++
title = "4. JSON"
weight = 4
+++
Data sent between our agent and our front-end will all be encoded as JSON. In
this section, we'll briefly look at how JSON works in Urbit, and write a library
to convert our agent's structures to and from JSON for our front-end.
JSON data comes into Eyre as a string, and Eyre parses it with the
[`++de-json:html`](/reference/hoon/zuse/2e_2-3#de-jsonhtml) function in
[`zuse.hoon`](/reference/hoon/zuse/table-of-contents). The
hoon type it's parsed to is `$json`, which is defined as:
```hoon
+$ json :: normal json value
$@ ~ :: null
$% [%a p=(list json)] :: array
[%b p=?] :: boolean
[%o p=(map @t json)] :: object
[%n p=@ta] :: number
[%s p=@t] :: string
== ::
```
Once Eyre has converted the raw JSON string to a `$json` structure, it will be
converted to the mark the web client specified and then delivered to the target
agent (unless the mark specified is already `%json`, in which case it will be
delivered directly). Outbound facts will go through the same process in
reverse - converted from the agent's native mark to `$json`, then encoded in a
string by Eyre using
[`++en-json:html`](/reference/hoon/zuse/2e_2-3#en-jsonhtml) and delivered
to the web client. The basic flow for both inbound messages (pokes) and outbound
messages (facts and scry results) looks like this:
![eyre mark flow diagram](https://media.urbit.org/guides/core/app-school-full-stack-guide/eyre-mark-flow-diagram.svg)
The mark conversion will be done by the corresponding mark file in `/mar` on the
agent's desk. In our case it would be `/mar/journal/action.hoon` and
`/mar/journal/update.hoon` in the `%journal` desk for our `%journal-action` and
`%journal-update` marks, which are for the `$action` and `$update` structures we
defined previously.
Mark conversion functions can be included directly in the mark file, or they can
be written in a separate library, then imported and called by the mark file. We
will do the latter in this case, so before we create the mark files themselves,
we'll write a library called `/lib/journal.hoon` with the conversion functions.
## `$json` utilities
[`zuse.hoon`](/reference/hoon/zuse/table-of-contents) contains three main
cores for converting to and from `$json`:
- [`++enjs:format`](/reference/hoon/zuse/2d_1-5#enjsformat) - Functions to
help encode data structures as `$json`.
- [`++dejs:format`](/reference/hoon/zuse/2d_6#dejsformat) - Functions to
decode `$json` to other data structures.
- [`++dejs-soft:format`](/reference/hoon/zuse/2d_7#dejs-softformat) -
Mostly the same as `++dejs:format` except the functions produce units which
are null if decoding fails, rather than just crashing.
### `++enjs:format`
This contains ten functions for encoding `$json`. Most of them are for specific
hoon data types, such as `++tape:enjs:format`, `++ship:enjs:format`,
`++path:enjs:format`, etc. We'll just have a look at the two most general and
useful ones: `++frond:enjs:format` and `++pairs:enjs:format`.
#### `++frond`
This function is for forming a JSON object from a single key-value pair. For
example:
```
> (frond:enjs:format 'foo' s+'bar')
[%o p={[p='foo' q=[%s p='bar']]}]
```
When stringified by Eyre, this will look like:
```json
{ "foo": "bar" }
```
#### `++pairs`
This is similar to `++frond` and also forms a JSON object, but it takes multiple
key-value pairs rather than just one:
```
> (pairs:enjs:format ~[['foo' n+~.123] ['bar' s+'abc'] ['baz' b+&]])
[%o p={[p='bar' q=[%s p='abc']] [p='baz' q=[%b p=%.y]] [p='foo' q=[%n p=~.123]]}]
```
When stringified by Eyre, this will look like:
```json
{
"foo": 123,
"baz": true,
"bar": "abc"
}
```
Notice that we used a knot for the value of `foo` (`n+~.123`). Numbers in JSON
can be signed or unsigned and integers or floating point values. The `$json`
structure uses a knot so that you can decide whether a particular number should
be treated as `@ud`, `@sd`, `@rs`, etc.
### `++dejs:format`
This core contains many functions for decoding `$json`. We'll touch on some
useful families of `++dejs` functions in brief, but because there's so many, in
practice you'll need to look through the [`++dejs`
reference](/reference/hoon/zuse/2d_6) to find the correct functions for
your use case.
#### Number functions
- `++ne` - decode a number to a `@rd`.
- `++ni` - decode a number to a `@ud`.
- `++no` - decode a number to a `@ta`.
- `++nu` - decode a hexadecimal string to a `@ux`.
For example:
```
> (ni:dejs:format n+'123')
123
```
#### String functions
- `++sa` - decode a string to a `tape`.
- `++sd` - decode a string containing a `@da` aura date value to a `@da`.
- `++se` - decode a string containing the specified aura to that aura.
- `++so` - decode a string to a `@t`.
- `++su` - decode a string by parsing it with the given [parsing
rule](/reference/hoon/stdlib/4f).
#### Array functions
`++ar`, `++as`, and `++at` decode a `$json` array to a `list`, `set`, and
n-tuple respectively. These gates take other `++dejs` functions as an argument,
producing a new gate that will then take the `$json` array. For example:
```
> ((ar so):dejs:format a+[s+'foo' s+'bar' s+'baz' ~])
<|foo bar baz|>
```
Notice that `++so` is given as the argument to `++ar`. `++so` is a `++dejs`
function that decodes a `$json` string to a `cord`. The gate resulting from `(ar so)` is then called with a `$json` array as its argument, and its product is a
`(list @t)` of the elements of the array.
Many `++dejs` functions take other `++dejs` functions as their arguments. A
complex nested `$json` decoding function can be built up in this manner.
#### Object functions
- `++of` - decode an object containing a single key-value pair to a head-tagged
cell.
- `++ot` - decode an object to a n-tuple.
- `++ou` - decode an object to an n-tuple, replacing optional missing values
with a given value.
- `++oj` - decode an object of arrays to a `jug`.
- `++om` - decode an object to a `map`.
- `++op` - decode an object to a `map`, and also parse the object keys with a
[parsing rule](/reference/hoon/stdlib/4f).
For example:
```
> =js %- need %- de-json:html
'''
{
"foo": "hello",
"baz": true,
"bar": 123
}
'''
> %- (ot ~[foo+so bar+ni]):dejs:format js
['hello' 123]
```
## Our types as JSON
We need to decide how our `$action` and `$update` types will be represented as
JSON in order to write our conversion functions. There are many ways to do this,
but in this case we'll do it as follows:
### Actions
| JSON | Noun |
| ------------------------------------------------- | ---------------------------------------------- |
| `{"add":{"id":1648366311070,"txt":"some text"}}` | `[%add id=1.648.366.034.844 txt='some text']` |
| `{"edit":{"id":1648366311070,"txt":"some text"}}` | `[%edit id=1.648.366.034.844 txt='some text']` |
| `{"del":{"id":1648366311070}}` | `[%del id=1.648.366.034.844]` |
### Updates
| Noun | JSON |
| ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `[1.648.366.492.459 %add id=1.648.366.034.844 txt='some text']` | `{time:1648366481425,"add":{"id":1648366311070,"txt":"some text"}}` |
| `[1.648.366.492.459 %edit id=1.648.366.034.844 txt='some text']` | `{time:1648366481425,"edit":{"id":1648366311070,"txt":"some text"}}` |
| `[1.648.366.492.459 %del id=1.648.366.034.844]` | `{time:1648366481425,"del":{"id":1648366311070}}` |
| `[1.648.366.492.459 %jrnl ~[[id=1.648.366.034.844 txt='some text'] ...]` | `{time:1648366481425,"entries":[{"id":1648366311070,"txt":"some text"},...]}` |
| `[1.648.366.492.459 %logs ~[[1.648.366.492.459 %add id=1.648.366.034.844 txt='some text'] ...]` | `{time:1648366481425,"logs":[{time:1648366481425,"add":{id":1648366311070,"txt":"some text"}},...]}` |
Now let's write our library of encoding/decoding functions.
## `/lib/journal.hoon`
```hoon
/- *journal
|%
```
First, we'll import the `/sur/journal.hoon` structures we previously created.
Next, we'll create two arms in our core, `++dejs-action` and `++enjs-update`, to
handle incoming poke `$action`s and outgoing facts or scry result `$update`s.
### `$json` to `$action`
```hoon
++ dejs-action
=, dejs:format
|= jon=json
^- action
%. jon
%- of
:~ [%add (ot ~[id+ni txt+so])]
[%edit (ot ~[id+ni txt+so])]
[%del (ot ~[id+ni])]
==
```
The first thing we do is use the [`=,`
rune](/reference/hoon/rune/tis#-tiscom) to expose the `++dejs:format`
namespace. This allows us to reference `ot`, `ni`, etc rather than having to
write `ot:dejs:format` every time. Note that you should be careful using `=,`
generally as the exposed wings can shadow previous wings if they have the same
name.
We then create a gate that takes `$json` and returns a `$action` structure.
Since we'll only take one action at a time, we can use the `++of` function,
which takes a single key-value pair. `++of` takes a list of all possible `$json`
objects it will receive, tagged by key.
For each key, we specify a function to handle its value. Ours will be objects,
so we use `++ot` and specify the pairs of the key and `+dejs` function to decode
it. We then cast the output to our `$action` structure.
You'll notice the nesting of these `++dejs` functions approximately reflects the
nested structure of the `$json` it's decoding.
### `$update` to `$json`
```hoon
++ enjs-update
=, enjs:format
|= upd=update
^- json
|^
?+ -.q.upd (logged upd)
%jrnl
%- pairs
:~ ['time' (numb p.upd)]
['entries' a+(turn list.q.upd entry)]
==
::
%logs
%- pairs
:~ ['time' (numb p.upd)]
['logs' a+(turn list.q.upd logged)]
==
==
++ entry
|= ent=^entry
^- json
%- pairs
:~ ['id' (numb id.ent)]
['txt' s+txt.ent]
==
++ logged
|= lgd=^logged
^- json
?- -.q.lgd
%add
%- pairs
:~ ['time' (numb p.lgd)]
:- 'add'
%- pairs
:~ ['id' (numb id.q.lgd)]
['txt' s+txt.q.lgd]
== ==
%edit
%- pairs
:~ ['time' (numb p.lgd)]
:- 'edit'
%- pairs
:~ ['id' (numb id.q.lgd)]
['txt' s+txt.q.lgd]
== ==
%del
%- pairs
:~ ['time' (numb p.lgd)]
:- 'del'
(frond 'id' (numb id.q.lgd))
==
==
--
--
```
Our `$update` encoding function's a little more complex than our `$action`
decoding function, since our `$update` structure is more complex.
Like the previous one, we use `=,` to expose the namespace of `++enjs:format`.
Our gate takes an `$update` and returns a `$json` structure. We use `|^` so we
can separate out the encoding functions for individual entries (`++entry`) and
individual logged actions (`++logged`).
We first test the head of the `$update`, and if it's `%jrnl` (a list of
entries), we `turn` over the entries and call `++entry` to encode each one. If
it's `%logs`, we do the same, but call `++logged` for each item in the list.
Otherwise, if it's just a single update, we encode it with `++logged`.
We primarily use `++pairs` to form the object, though sometimes `++frond` if it
only contains a single key-value pair. We also use `++numb` to encode numerical
values.
You'll notice more of our encoding function is done manually than our previous
decoding function. For example, we form arrays by tagging an ordinary `list`
with `%a`, and strings by tagging an ordinary `cord` with `%s`. This is typical
when you write `$json` encoding functions, and is the reason there are far fewer
`+enjs` functions than `+dejs` functions.
## Resources
- [The JSON Guide](/guides/additional/hoon/json-guide) - The stand-alone JSON guide
covers JSON encoding/decoding in great detail.
- [The Zuse reference](/reference/hoon/zuse/table-of-contents) - The
`zuse.hoon` reference documents all JSON-related functions in detail.
- [`++enjs:format` reference](/reference/hoon/zuse/2d_1-5#enjsformat) -
This section of the `zuse.hoon` documentation covers all JSON encoding
functions.
- [`++dejs:format` reference](/reference/hoon/zuse/2d_6) - This section of
the `zuse.hoon` documentation covers all JSON _decoding_ functions.
- [Eyre overview](/reference/arvo/eyre/eyre) - This section of the Eyre vane
documentation goes over the basic features of the Eyre vane.

View File

@ -0,0 +1,96 @@
+++
title = "5. Marks"
weight = 5
+++
In this section we'll write the mark files for our agent. We'll need two marks,
one for poke `$action`s and one for subscription updates and scry results, both
of which are `$update`s. Our `$action` mark will be called `%journal-action` and
our `$update` mark will be called `%journal-update`. These will be located at
`/mar/journal/action.hoon` and `/mar/journal/update.hoon`.
Note that a mark called `%foo-bar` will first be looked for in
`/mar/foo-bar.hoon`, and if it's not there it will be looked for in
`/mar/foo/bar.hoon`. That's why we can have a single name like `%journal-action`
but have it in `/mar/journal/action.hoon`
## `%journal-action`
```hoon
/- *journal
/+ *journal
|_ act=action
++ grow
|%
++ noun act
--
++ grab
|%
++ noun action
++ json dejs-action
--
++ grad %noun
--
```
First we import our `/sur/journal.hoon` structure file and also our
`/lib/journal.hoon` library (containing our `$json` conversion functions). The
sample of our mark door is just our `$action` structure. The `++grow` arm of a
mark core, if you recall, contains methods for converting _from_ our mark _to_
another mark. Actions only ever come inwards in pokes, so we don't need to worry
about converting an `$action` _to_ `$json`. The `++grow` arm can therefore just
handle the generic `%noun` case, simply returning our mark door's sample without
doing anything.
`++grab`, conversely, defines methods for converting _to_ our mark _from_
another mark. Since `$action`s will come in from the front-end as `$json`, we
need to be able to convert `$json` data to our `$action` structure. Our
`/lib/journal.hoon` library contains the `++dejs-action` function for performing
this conversion, so we can just specify that function for the `%json` case.
We'll also define a standard `%noun` method, which will just "clam" (or "mold")
the incoming noun with the `$action` mold. Clamming/molding coerces a noun to a
type and is done by calling a mold as a function.
Lastly, `++grad` defines revision control methods, but can be delegated to
another mark. Since this mark will never be used for actually storing files in
Clay, we can just delegate it to the generic `%noun` mark rather than writing a
proper set of `++grad` methods.
## `%journal-update`
```hoon
/- *journal
/+ *journal
|_ upd=update
++ grow
|%
++ noun upd
++ json (enjs-update upd)
--
++ grab
|%
++ noun update
--
++ grad %noun
--
```
Next we have our `%journal-update` mark file. The sample of our mark door is our
`$update` structure. Our `$update`s are always outbound, never inbound, so we
only need to define a method for converting our `$update` structure to `$json`
in the `++grow` arm, and not the opposite direction in `++grad`. Our
`/lib/journal.hoon` library contains the `++enjs-update` function for performing
this conversion, so we can call it with the sample `$update` as its argument. We
can add `%noun` conversion methods and delegate revision control to the `%noun`
mark in the same manner as our `%journal-action` mark above.
## Resources
- [The Marks section of the Clay documentation](/reference/arvo/clay/marks/marks) -
This section of the Clay vane documentation covers mark files comprehensively.
- [The mark file section of the Gall
Guide](/guides/core/app-school/7-sur-and-marks#mark-files) - This part of
App School goes through the basics of mark files.
- [The JSON Guide](/guides/additional/hoon/json-guide) - This also covers writing mark
files to convert to/from JSON.

View File

@ -0,0 +1,78 @@
+++
title = "6. Eyre"
weight = 6
+++
Now that we have our structure file, agent, `$json` conversion library and mark
file, our back-end is complete. Before we start writing our front-end, though,
we should give a brief overview of how Eyre works.
[Eyre](/reference/arvo/eyre/eyre) is the HTTP server [vane](/reference/glossary/vane) of
Arvo. Eyre has a handful of different subsystems, but the main two are the
channel system and the scry interface. These two are what we'll focus on here.
In order to use the channel system or perform scries, a web client must have
authenticated with the ship's web login code (e.g.
`lidlut-tabwed-pillex-ridrup`) and obtained a session cookie. Our front-end will
be served directly from the ship by the `%docket` agent, so we can assume a
session cookie was already obtained when the user logged into landscape, and
skip over authentication.
## Channels
Eyre's channel system is the main way to interact with agents from a web client.
It provides a JSON interface to the ordinary poke and subscription system for
Gall agents.
First, a unique channel ID is generated by the web client (`@urbit/http-api`
uses the current Unix time concatenated with a random hex string). The client
then sends a poke or subscription request for the channel, and Eyre
automatically opens a new channel with that ID. Once open, the client can then
connect to the channel and receive any events such as poke acks, watch acks,
facts from subscriptions, etc.
The new channel is an SSE ([Server Sent
Event](https://html.spec.whatwg.org/#server-sent-events)) stream, and can be
handled by an `EventSource` object in Javascript. The `@urbit/http-api` library
we'll use abstracts this for us, so we won't need to deal with an `EventSource`
object directly. The channel can handle multiple concurrent subscriptions to
different agent subscription paths, and different agents can be poked through
the one channel. This means a client only needs to open a single channel for all
of its interactions with the ship. Each subscription is given a different ID, so
they can be individually unsubscribed later.
A channel will timeout after 12 hours of inactivity, and the timeout is reset
every time Eyre receives a message of any kind from the client. Additionally,
each subscription on the channel may only accumulate 50 unacknowledged facts
before it's considered "clogged", in which case the individual clogged
subscription will be closed by Eyre after a short delay. All events of any kind
which Eyre sends out on the channel must be ack'd by the client. Ack'ing one
event will also ack all previous events too. The `@urbit/http-api` library we'll
use automatically acks events for us, so we don't need to worry about clogged
subscriptions or manually ack'ing events.
Eyre expects a particular JSON object structure for each of these different
requests, but the `@urbit/http-api` library we'll use includes functions to send
pokes, subscription requests, etc, so we won't need to manually construct the
JSON objects in our front-end.
## Scries
Eyre's scry interface is separate to the channel system. Scries are performed by
a simple GET request to a path with a format of `/~/scry/{agent}{path}.{mark}`.
If successful, the HTTP response will contain the result with the mark
specified. If unsuccessful, an HTTP error will be thrown in response.
The `@urbit/http-api` library we'll use includes a function for performing
scries, so we'll not need to manually send GET requests to the ship.
## Resources
- [The Eyre vane documentation](/reference/arvo/eyre/eyre) - This section of the vane
docs covers all aspects of Eyre.
- [Eyre External API Reference](/reference/arvo/eyre/external-api-ref) - This section
of the Eyre documentation contains reference material for Eyre's external API.
- [The Eyre Guide](/reference/arvo/eyre/guide) - This section of the Eyre
documentation walks through using Eyre's external API at a low level (using
`curl`).

View File

@ -0,0 +1,213 @@
+++
title = "7. React app setup"
weight = 7
+++
Now that we have a basic idea of how Eyre works, we can begin working on our
React app front-end.
## Create React app
Node.js must be installed, and can be downloaded from their
[website](https://nodejs.org/en/download). With that installed, we'll have the
`npm` package manager available. The first thing we'll do is globally install
the `create-react-app` package with the following command:
```sh
npm install -g create-react-app
```
Once installed, we can use it to create a new `journal-ui` directory and setup a
new React app in it with the following command:
```sh
create-react-app journal-ui
```
We can then open our new directory:
```sh
cd journal-ui
```
Its contents should look something like this:
```
journal-ui
├── node_modules
├── package.json
├── package-lock.json
├── public
├── README.md
└── src
```
## Install `http-api`
Inside our React app directory, let's install the `@urbit/http-api` NPM package:
```sh
npm i @urbit/http-api
```
We also install a handful of other packages for the UI components (`bootstrap react-bootstrap react-textarea-autosize date-fns react-bottom-scroll-listener react-day-picker`), but that's not important to our purposes here.
## Additional tweaks
Our front-end will be served directly from the ship by the `%docket` app, where
a user will open it by clicking on its homescreen tile. Docket serves such
front-ends with a base URL path of `/apps/[desk]/`, so in our case it will be
`/apps/journal`. In order for our app to be built with correct resource paths,
we must add the following line to `package.json`:
```json
"homepage": "/apps/journal/",
```
Our app also needs to know the name of the ship it's being served from in order
to talk with it. The `%docket` agent serves a small file for this purpose at
`[host]/session.js`. This file is very simple and just contains:
```js
window.ship = "sampel-palnet";
```
`sampel-palnet` will of course be replaced by the actual name of the ship. We
include this script by adding the following line to the `<head>` section of
`public/index.html`:
```
<script src="/session.js"></script>
```
## Basic API setup
With everything now setup, we can begin work on the app itself. In this case
we'll just edit the existing `App.js` file in the `/src` directory. The first thing is to import the `Urbit` class from `@urbit/http-api`:
```js
import Urbit from "@urbit/http-api";
```
We also need to import a few other things, mostly relating to UI components (but
these aren't important for our purposes here):
```js
import React, { Component } from "react";
import "bootstrap/dist/css/bootstrap.min.css";
import "react-day-picker/lib/style.css";
import TextareaAutosize from "react-textarea-autosize";
import Button from "react-bootstrap/Button";
import Card from "react-bootstrap/Card";
import Stack from "react-bootstrap/Stack";
import Tab from "react-bootstrap/Tab";
import Tabs from "react-bootstrap/Tabs";
import ToastContainer from "react-bootstrap/ToastContainer";
import Toast from "react-bootstrap/Toast";
import Spinner from "react-bootstrap/Spinner";
import CloseButton from "react-bootstrap/CloseButton";
import Modal from "react-bootstrap/Modal";
import DayPickerInput from "react-day-picker/DayPickerInput";
import endOfDay from "date-fns/endOfDay";
import startOfDay from "date-fns/startOfDay";
import { BottomScrollListener } from "react-bottom-scroll-listener";
```
Inside the existing `App` class:
```js
class App extends Component {
```
...we'll clear out the existing demo code and start adding ours. The first thing
is to define our app's state. We'll look at most of the state entries in the
next section. For now, we'll just consider `status`.
```js
state = {
// .....
status: null,
// .....
};
```
Next, we'll setup the `Urbit` API object in `componentDidMount`. We could do
this outside the `App` class since we're adding it to `window`, but we'll do it
this way so it's all in one place:
```js
componentDidMount() {
window.urbit = new Urbit("");
window.urbit.ship = window.ship;
window.urbit.onOpen = () => this.setState({status: "con"});
window.urbit.onRetry = () => this.setState({status: "try"});
window.urbit.onError = (err) => this.setState({status: "err"});
this.init();
};
```
The first thing we do is create a new instance of the `Urbit` class we imported
from `@urbit/http-api`, and save it to `window.urbit`. The `Urbit` class
constructor takes three arguments: `url`, `desk` and `code`, of which only `url`
is mandatory.
- `url` is the URL of the ship we want to talk to. Since our React app will be
served by the ship, we can just leave it as an empty `""` string and let
`Urbit` use root-relative paths.
- `desk` is only necessary if we want to run threads through Eyre, and since
we're not going to do that, we can exclude it.
- `code` is the web login code for authentication, but since the user will
already have logged in, we can also exclude that.
Therefore, we call the class contructor with just the empty `url` string:
```js
window.urbit = new Urbit("");
```
Next, we need to set the ship name in our `Urbit` instance. Eyre requires the
ship name be specified in all requests, so if we don't set it, Eyre will reject
all the messages we send. We previously included `session.js` which sets
`window.ship` to the ship name, so we just set `window.urbit.ship` as that:
```js
window.urbit.ship = window.ship;
```
Next, we set three callbacks: `onOpen`, `onRetry`, and `onError`. These
callbacks are fired when the state of our channel connection changes:
- `onOpen` is called when a connection is established.
- `onRetry` is called when a channel connection has been interrupted (such as by
network issues) and the `Urbit` object is trying to reconnect. Reconnection
will be attempted up to three times: immediately, after 750ms, and after
3000ms.
- `onError` is called with an `Error` message once all retries have failed, or
otherwise when a fatal error occurs.
We'll look at how we handle these cases in the next section. For now, we'll just
set the `status` entry in the state to either `"con"`, `"try"`, or `"err"` as
the case may be. Note that it's not mandatory to set these callbacks, but
leaving connection problems unhandled is usually a bad idea.
The last thing we do is call:
```js
this.init();
```
This function will fetch initial entries and subscribe for updates. We'll look
at it in the next section.
## Resources
- [HTTP API Guide](/guides/additional/http-api-guide) - Reference documentation for
`@urbit/http-api`.
- [React app source
code](https://github.com/urbit/docs-examples/tree/main/journal-app/ui) - The
source code for the Journal app UI.
- [`@urbit/http-api` source
code](https://github.com/urbit/urbit/tree/master/pkg/npm/http-api) - The
source code for the `@urbit/http-api` NPM package.

View File

@ -0,0 +1,405 @@
+++
title = "8. React app logic"
weight = 8
+++
With the basic things setup, we can now go over the logic of our app. We'll just
focus on functions that are related to ship communications using the `Urbit`
object we previously setup, and ignore UI components and other helper functions.
## State
In the previous section we just mentioned the connection `status` field of our
state. Here's the full state of our App:
```js
state = {
entries: [], // list of journal entries for display
drafts: {}, // edits which haven't been submitted yet
newDraft: {}, // new entry which hasn't been submitted yet
results: [], // search results
searchStart: null, // search query start date
searchEnd: null, // search query end date
resultStart: null, // search results start date
resultEnd: null, // search results end date
searchTime: null, // time of last search
latestUpdate: null, // most recent update we've received
entryToDelete: null, // deletion target for confirmation modal
status: null, // connection status (con, try, err)
errorCount: 0, // number of errors so far
errors: new Map(), // list of error messages for display
};
```
We'll see how these are used subsequently.
## Initialize
The first thing our app does is call `init()`:
```js
init = () => {
this.getEntries().then(
(result) => {
this.handleUpdate(result);
this.setState({ latestUpdate: result.time });
this.subscribe();
},
(err) => {
this.setErrorMsg("Connection failed");
this.setState({ status: "err" });
}
);
};
```
This function just calls `getEntries()` to retrieve the initial list of journal
entries then, if that succeeded, it calls `subscribe()` to subscribe for new
updates. If the initial entry retrieval failed, we set the connection `status`
and save an error message in the `errors` map. We'll look at what we do with
errors later.
## Getting entries
![entries screenshot](https://media.urbit.org/guides/core/app-school-full-stack-guide/entries.png)
The `getEntries` function scries our `%journal` agent for up to 10 entries
before the oldest we currently have. We call this initially, and then each time
the user scrolls to the bottom of the list.
```js
getEntries = async () => {
const { entries: e } = this.state;
const before = e.length === 0 ? Date.now() : e[e.length - 1].id;
const max = 10;
const path = `/entries/before/${before}/${max}`;
return window.urbit.scry({
app: "journal",
path: path,
});
};
```
The scry is done with the `Urbit.scry` method. This function takes two arguments
in an object:
- `app` - the agent to scry.
- `path` - the scry path. Note the `care` is not included - all scries through
Eyre are `%x` scries.
The `Urbit.scry` method only allows JSON results, but note that scries done via
direct GET requests allow other marks too.
The `Urbit.scry` method returns a Promise which will contain an HTTP error
message if the scry failed. We handle it with a `.then` expression back in the
function that called it, either [`init()`](#initialize) or `moreEntries()`. If
the Promise is successfuly, the results are passed to the
[`handleUpdate`](#updates) function which appends the new entries to the
existing ones in state.
## Subscription
A subscription to the `/updates` path of our `%journal` agent is opened with our
`subscribe()` function:
```js
subscribe = () => {
try {
window.urbit.subscribe({
app: "journal",
path: "/updates",
event: this.handleUpdate,
err: () => this.setErrorMsg("Subscription rejected"),
quit: () => this.setErrorMsg("Kicked from subscription"),
});
} catch {
this.setErrorMsg("Subscription failed");
}
};
```
We use the `Urbit.subscribe` method for this, which takes five arguments in an
object:
- `app` - the target agent.
- `path` - the `%watch` path we're subscribing to.
- `event` - a function to handle each fact the agent sends out. We call our
`handleUpdate` function, which we'll describe below.
- `err` - a function to call if the subscription request is rejected (nacked).
We just display an error in this case.
- `quit` - a function to call if we get kicked from the subscription. We also
just display an error in this case.
Note that the `Urbit.subscribe` method returns a subscription ID number. Since
we only have one subscription in our app which we never close, we don't bother
to record it. If your app has multiple subscriptions to manage, you may wish to
keep track of these IDs in your app's state.
## Updates
This `handleUpdate` function handles all updates we receive. It's called
whenever an event comes in for our subscription, and it's also called with the
results of [`getEntries`](#getting-entries) and [`getUpdates`](#error-handling)
(described later).
It's a bit complex, but basically it just checks whether the JSON object is
`add`, `edit`, `delete`, or `entries`, and then updates the state appropriately.
The object it's receiving is just the `$update` structure converted to JSON by
the mark conversion functions we wrote previously.
```js
handleUpdate = (upd) => {
const { entries, drafts, results, latestUpdate } = this.state;
if (upd.time !== latestUpdate) {
if ("entries" in upd) {
this.setState({ entries: entries.concat(upd.entries) });
} else if ("add" in upd) {
const { time, add } = upd;
const eInd = this.spot(add.id, entries);
const rInd = this.spot(add.id, results);
const toE =
entries.length === 0 || add.id > entries[entries.length - 1].id;
const toR = this.inSearch(add.id, time);
toE && entries.splice(eInd, 0, add);
toR && results.splice(rInd, 0, add);
this.setState({
...(toE && { entries: entries }),
...(toR && { results: results }),
latestUpdate: time,
});
} else if ("edit" in upd) {
const { time, edit } = upd;
const eInd = entries.findIndex((e) => e.id === edit.id);
const rInd = results.findIndex((e) => e.id === edit.id);
const toE = eInd !== -1;
const toR = rInd !== -1 && this.inSearch(edit.id, time);
if (toE) entries[eInd] = edit;
if (toR) results[rInd] = edit;
(toE || toR) && delete drafts[edit.id];
this.setState({
...(toE && { entries: entries }),
...(toR && { results: results }),
...((toE || toR) && { drafts: drafts }),
latestUpdate: time,
});
} else if ("del" in upd) {
const { time, del } = upd;
const eInd = entries.findIndex((e) => e.id === del.id);
const rInd = results.findIndex((e) => e.id === del.id);
const toE = eInd !== -1;
const toR = this.inSearch(del.id, time) && rInd !== -1;
toE && entries.splice(eInd, 1);
toR && results.splice(rInd, 1);
(toE || toR) && delete drafts[del.id];
this.setState({
...(toE && { entries: entries }),
...(toR && { results: results }),
...((toE || toR) && { drafts: drafts }),
latestUpdate: time,
});
}
}
};
```
## Add, edit, delete
![add screenshot](https://media.urbit.org/guides/core/app-school-full-stack-guide/add.png)
When a user writes a new journal entry and hits submit, the `submitNew` function
is called. It uses the `Urbit.poke` method to poke our `%journal` agent.
```js
submitNew = (id, txt) => {
window.urbit.poke({
app: "journal",
mark: "journal-action",
json: { add: { id: id, txt: txt } },
onSuccess: () => this.setState({ newDraft: {} }),
onError: () => this.setErrorMsg("New entry rejected"),
});
};
```
The `Urbit.poke` method takes five arguments:
- `app` is the agent to poke.
- `mark` is the mark of the data we're sending. We specify `"journal-action"`,
so Eyre will use the `/mar/journal/action.hoon` mark we created to convert it
to a `$action` structure with a `%journal-action` mark before it's delivered
to our agent.
- `json` is the actual data we're poking our agent with. In this case it's the
JSON form of the `%add` `$action`.
- `onSuccess` is a callback that fires if we get a positive ack in response. In
this case we just clear the draft.
- `onError` is a callback that fires if we get a negative ack (nack) in
response, meaning the poke failed. In this case we just set an error message
to be displayed.
`onSuccess` and `onError` are optional, but it's usually desirable to handle
these cases.
The `delete` and `submitEdit` functions are similar to `submitNew`, but for the
`%del` and `%edit` actions rather than `%add`:
![edit screenshot](https://media.urbit.org/guides/core/app-school-full-stack-guide/edit.png)
```js
submitEdit = (id, txt) => {
if (txt !== null) {
window.urbit.poke({
app: "journal",
mark: "journal-action",
json: { edit: { id: id, txt: txt } },
onError: () => this.setErrorMsg("Edit rejected"),
});
} else this.cancelEdit(id);
};
```
![delete screenshot](https://media.urbit.org/guides/core/app-school-full-stack-guide/delete.png)
```js
delete = (id) => {
window.urbit.poke({
app: "journal",
mark: "journal-action",
json: {"del": {"id": id}},
onError: ()=>this.setErrorMsg("Deletion rejected")
})
this.setState({rmModalShow: false, entryToDelete: null})
};
```
Note that whether we're adding, editing or deleting entries, we update our state
when we receive the update back on the `/updates` subscription, not when we poke
our agent.
## Search
![search screenshot](https://media.urbit.org/guides/core/app-school-full-stack-guide/search.png)
When searching for entries between two dates, the `getSearch` function is
called, which uses the `Urbit.scry` method to scry for the results in a similar
fashion to [`getEntries`](#getting-entries), but using the
`/x/entries/between/[start]/[end]` endpoint.
```js
getSearch = async () => {
const { searchStart: ss, searchEnd: se, latestUpdate: lu } = this.state;
if (lu !== null && ss !== null && se !== null) {
let start = ss.getTime();
let end = se.getTime();
if (start < 0) start = 0;
if (end < 0) end = 0;
const path = `/entries/between/${start}/${end}`;
window.urbit
.scry({
app: "journal",
path: path,
})
.then(
(result) => {
this.setState({
searchTime: result.time,
searchStart: null,
searchEnd: null,
resultStart: ss,
resultEnd: se,
results: result.entries,
});
},
(err) => {
this.setErrorMsg("Search failed");
}
);
} else {
lu !== null && this.setErrorMsg("Searh failed");
}
};
```
## Error handling
When the channel connection is interrupted, the `Urbit` object will begin trying to reconnect. On each attempt, it sets the connection `status` to `"try"`, as we specified for the `onRetry` callback. When this is set, a "reconnecting" message is displayed at the bottom of the screen:
![reconnecting screenshot](https://media.urbit.org/guides/core/app-school-full-stack-guide/reconnecting.png)
If all three reconnection attempts fail, the `onError` callback is fired and we replace the "reconnecting" message with a "reconnect" button:
![reconnect screenshot](https://media.urbit.org/guides/core/app-school-full-stack-guide/reconnect.png)
When clicked, the following function is called:
```js
reconnect = () => {
window.urbit.reset();
const latest = this.state.latestUpdate;
if (latest === null) {
this.init();
} else {
this.getUpdates().then(
(result) => {
result.logs.map((e) => this.handleUpdate(e));
this.subscribe();
},
(err) => {
this.setErrorMsg("Connection failed");
this.setState({ status: "err" });
}
);
}
};
```
Our `reconnect()` function first calls the `Urbit.reset` method. This closes the
channel connection, wipes event counts and subscriptions, and generates a new
channel ID. We could have tried reconnecting without resetting the connection,
but we don't know whether the channel still exists. We could time how long the
connection has been down and estimate whether it still exists, but it's easier
to just start fresh in this case.
Since we've reset the channel, we don't know if we've missed any updates. Rather
than having to refresh our whole state, we can use the `getUpdates()` function
to get any missing update:
```js
getUpdates = async () => {
const { latestUpdate: latest } = this.state;
const since = latest === null ? Date.now() : latest;
const path = `/updates/since/${since}`;
return window.urbit.scry({
app: "journal",
path: path,
});
};
```
This function uses the `Urbit.scry` method to scry the
`/x/updates/since/[since]` path, querying the update `$log` for entries more
recent than `latestUpdate`, which is always set to the last logged action we
received. The `getUpdates` function returns a Promise to the `reconnect`
function above which called it. The `reconnect` function handles it in a `.then`
expression, where the success case passes each update retrieved to the
[`handleUpdate`](#updates) function, updating our state.
Lastly, as well as handling channel connection errors, we also handle errors
such as poke nacks or failed scries by printing error messages added to the
`error` map by the `setErrorMsg` function. You could of course handle nacks,
kicks, scry failures, etc differently than just printing an error, it depends on
the needs of your app.
![search failed screenshot](https://media.urbit.org/guides/core/app-school-full-stack-guide/search-failed.png)
## Resources
- [HTTP API Guide](/guides/additional/http-api-guide) - Reference documentation for
`@urbit/http-api`.
- [React app source
code](https://github.com/urbit/docs-examples/tree/main/journal-app/ui) - The
source code for the Journal app UI.
- [`@urbit/http-api` source
code](https://github.com/urbit/urbit/tree/master/pkg/npm/http-api) - The
source code for the `@urbit/http-api` NPM package.

View File

@ -0,0 +1,225 @@
+++
title = "9. Desk and glob"
weight = 9
+++
With our React app now complete, we can put together the final desk and publish
it.
## Config files
So far we've written the following files for the back-end:
```
ourfiles
├── app
│ └── journal.hoon
├── lib
│ └── journal.hoon
├── mar
│ └── journal
│ ├── action.hoon
│ └── update.hoon
└── sur
└── journal.hoon
```
There's a handful of extra files we need in the root of our desk:
- `desk.bill` - the list of agents that should be started when our app is
installed.
- `sys.kelvin` - the kernel version our app is compatible with.
- `desk.docket-0` - configuration of our app tile, front-end glob and other
metadata.
We only have one agent to start, so `desk.bill` is very simple:
```
:~ %journal
==
```
Likewise, `sys.kelvin` just contains:
```
[%zuse 418]
```
The `desk.docket-0` file is slightly more complicated:
```
:~
title+'Journal'
info+'Dear diary...'
color+0xd9.b06d
version+[0 1 0]
website+'https://urbit.org'
license+'MIT'
base+'journal'
glob-ames+[~zod 0v0]
==
```
The fields are as follows:
- `title` is the name of the app - this will be displayed on the tile and when
people search for the app to install it.
- `info` is a brief description of the app.
- `color` - the RGB hex color of the tile.
- `version` - the version number of the app. The fields represent major, minor
and patch version.
- `website` - a link to a website for the app. This would often be its Github repo.
- `license` - the license of for the app.
- `base` - the desk name of the app.
- `glob-ames` - the ship to retrieve the front-end files from, and the hash of
those files. We've put `~zod` here but this would be the actual ship
distributing the app when it's live on the network. The hash is `0v0`
initially, but once we upload the front-end files it will be updated to the
hash of those files automatically. Note that it's also possible to distribute
front-end files from a separate web server. In that case, you'd use
`glob-http` rather than `glob-ames`. The [Glob section of the distribution
guide](/guides/additional/dist/glob) covers this alternative approach in more
detail.
Our files should now look like this:
```
ourfiles
├── app
│ └── journal.hoon
├── desk.bill
├── desk.docket-0
├── lib
│ └── journal.hoon
├── mar
│ └── journal
│ ├── action.hoon
│ └── update.hoon
├── sur
│ └── journal.hoon
└── sys.kelvin
```
## New desk
Next, we'll create a new `%journal` desk on our ship by forking an existing one.
Once created, we can mount it to the unix filesystem.
In the dojo of a fake ship:
```
> |merge %journal our %webterm
>=
> |mount %journal
>=
```
Now we can browse to it in the unix terminal:
```sh
cd ~/zod/journal
```
Currently it has the same files as the `%webterm` desk, so we need to delete
those:
```sh
rm -r .
```
Apart from the kernel and standard library, desks need to be totally
self-contained, including all mark files and libraries necessary to build them.
For example, since our app contains a number of `.hoon` files, we need the
`hoon.hoon` mark, and its dependencies. The easiest way to ensure our desk has
everything it needs is to copy in the "dev" versions of the `%base` and
`%garden` desks. To do this, we first clone the Urbit git repository:
```sh
git clone https://github.com/urbit/urbit.git urbit-git
```
If we navigate to the `pkg` directory in the cloned repo:
```sh
cd ~/urbit-git/pkg
```
...we can combine the `base-dev` and `garden-dev` desks with the included
`symbolic-merge.sh` script:
```sh
./symbolic-merge.sh base-dev journal
./symbolic-merge.sh garden-dev journal
```
Now, we copy the contents of the new `journal` folder into our empty desk:
```sh
cp -rL journal/* ~/zod/journal/
```
Note we've used the `L` flag to resolve symbolic links, because the dev-desks
contain symlinks to files in the actual `arvo` and `garden` folders.
We can copy across all of our own files too:
```sh
cp -r ~/ourfiles/* ~/zod/journal/
```
Finally, in the dojo, we can commit the whole lot:
```
|commit %journal
```
## Glob
The next step is to build our front-end and upload the files to our ship. In the
`journal-ui` folder containing our React app, we can run:
```sh
npm run build
```
This will create a `build` directory containing the compiled front-end files. To
upload it to our ship, we need to first install the `%journal` desk. In the
dojo:
```
|install our %journal
```
Next, in the browser, we navigate to the `%docket` globulator at
`http://localhost:8080/docket/upload` (replacing localhost with the actual host):
![globulator screenshot](https://m.tinnus-napbus.xyz/pub/globulator.png)
We select our `journal` desk, then we hit `Choose file`, and select the whole
`build` directory which was created when we build our React app. Finally, we hit
`glob!` to upload it.
If we now return to the homescreen of our ship, we'll see our tile displayed, and we can open our app by clicking on it:
![tiles screenshot](https://m.tinnus-napbus.xyz/pub/tiles.png)
## Publishing
The last thing we need to do is publish our app, so other users can install it
from our ship. To do that, we just run the following command in the dojo:
```
:treaty|publish %journal
```
## Resources
- [App publishing/distribution documentation](/guides/additional/dist/dist) -
Documentation covering third party desk composition, publishing and
distribution.
- [Glob documentation](/guides/additional/dist/glob) - Comprehensive documentation
of handling front-end files.
- [Desk publishing guide](/guides/additional/dist/guide) - A step-by-step guide to
creating and publishing a desk.

View File

@ -0,0 +1,51 @@
+++
title = "App School II (Full-Stack)"
weight = 7
sort_by = "weight"
insert_anchor_links = "right"
+++
#### [1. Introduction](/guides/core/app-school-full-stack/1-intro)
An overview of the guide and table of contents.
#### [2. Types](/guides/core/app-school-full-stack/2-types)
Creating the `/sur` structure file for our `%journal` agent.
#### [3. Agent](/guides/core/app-school-full-stack/3-agent)
Creating the `%journal` agent itself.
#### [4. JSON](/guides/core/app-school-full-stack/5-json)
Writing a library to convert between our agent's marks and JSON. This lets our
React front-end poke our agent, and our agent send updates back to it.
#### [5. Marks](/guides/core/app-school-full-stack/4-marks)
Creating the mark files for the pokes our agent takes and updates it sends out.
#### [6. Eyre](/guides/core/app-school-full-stack/6-eyre)
A brief overview of how the webserver vane Eyre works.
#### [7. React App Setup](/guides/core/app-school-full-stack/7-react-setup)
Creating a new React app, installing the required packages, and setting up some
basic things for our front-end.
#### [8. React App Logic](/guides/core/app-school-full-stack/8-http-api)
Analysing the core logic of our React app, with particular focus on using
methods of the `Urbit` class from `@urbit/http-api` to communicate with our
agent.
#### [9. Desk and Glob](/guides/core/app-school-full-stack/9-web-scries)
Building and "globbing" our front-end, and putting together a desk for
distribution.
#### [10. Summary](/guides/core/app-school-full-stack/10-final)
Some final comments and additional resources.

View File

@ -0,0 +1,304 @@
+++
title = "1. Arvo"
weight = 5
+++
This document is a prologue to App School I. If you've worked though [Hoon
School](/guides/core/hoon-school/A-intro) (or have otherwise learned the basics of
Hoon), you'll likely be familiar with generators, but not with all the other
parts of the Arvo operating system or the way it fits together. We'll go over
the basic details here so you're better oriented to learn Gall agent
development. We'll not go into the internal workings of the kernel much, but
just what is necessary to understand it from the perspective of userspace.
## Arvo and its Vanes
[Arvo](/reference/arvo/overview) is the Urbit OS and kernel which is written in
[Hoon](/reference/glossary/hoon), compiled to [Nock](/reference/glossary/nock), and
executed by the runtime environment and virtual machine
[Vere](/reference/glossary/vere). Arvo has eight kernel modules called vanes:
[Ames](/reference/arvo/ames/ames), [Behn](/reference/arvo/behn/behn),
[Clay](/reference/arvo/clay/clay), [Dill](/reference/arvo/dill/dill),
[Eyre](/reference/arvo/eyre/eyre), [Gall](/reference/arvo/gall/gall),
[Iris](/reference/arvo/iris/iris), and [Jael](/reference/arvo/jael/jael).
Arvo itself has its own small codebase in `/sys/arvo.hoon` which primarily
implements the [transition function](/reference/arvo/overview#operating-function) `(State, Event) -> (State, Effects)` for
events injected by the runtime. It also handles inter-vane messaging, the
[scry](/reference/arvo/concepts/scry) system, and a couple of other things. Most of
the heavy lifting is done by the vanes themselves - Arvo itself typically just
routes events to the relevant vanes.
Each vane has its own state. Gall's state contains the agents it's managing,
Clay's state contains all the desks and their files, Jael's state contains all
its PKI data, etc. All the vanes and their states live in Arvo's state, so
Arvo's state ultimately contains the entire OS and its data.
Here's a brief summary of each of the vanes:
- **Ames**: This is both the name of Urbit's networking protocol, as well as the
vane that handles communications over it. All inter-ship communications are
done with Ames, but you'd not typically deal with it directly in a Gall agent
because Gall itself handles it for you.
- **Behn**: A simple timer vane. Behn lets your Gall agent set timers which go off
at the time specified and notify your agent.
- **Clay**: Filesystem vane. Clay is a revision-controlled, typed filesystem with a
built-in build system. Your agent's source code lives in Clay. Your agent's
source code and relevant files are automatically built and loaded upon
installation, so your Gall agent itself would not need to interact with Clay
unless you specifically wanted to read and write files.
- **Dill**: Terminal driver vane. You would not typically interact with Dill
directly; printing debug messages to the terminal is usually done with hinting runes
and functions rather than tasks to Dill, and CLI apps are mediated by a
sub-module of the `%hood` system agent called `%drum`. CLI apps will not be touched
on in this guide, but there's a separate [CLI
Apps](/guides/additional/hoon/cli-tutorial) guide which covers them if you're
interested.
- **Eyre**: Webserver vane. App web front-ends are served via Eyre. It's possible to
handle HTTP requests directly in a Gall agent (see the [Eyre
Guide](/reference/arvo/eyre/guide) for details), but usually you'd just serve a
front-end [glob](/guides/additional/dist/glob) via the `%docket` agent, so you'd
not typically have your agent deal with Eyre directly.
- **Gall**: App management vane; this is where your agent lives.
- **Iris**: Web client vane. If you want your agent to query external web APIs and
the like, it's done via Iris. Oftentimes web API interactions are
spun out into [threads](reference/arvo/threads/overview) to avoid
complicating the Gall agent itself, so a Gall agent would not necessary deal
with Iris directly, even if it made use of external APIs.
- **Jael**: Key infrastructure vane. Jael keeps track of PKI data for your ship and
other ships on the network. Jael's data is most heavily used by Ames, and
since Gall handles Ames communications for you, you'd not typically deal with
Jael directly unless your were specifically writing something that made use of
its data.
## Userspace
Gall agents live in "userspace" as opposed to "kernelspace". Kernelspace is Arvo
and its eight vanes. Userspace is primarily Gall agents, generators, threads,
front-ends, and all of their related files in Clay. The distinction looks
something like this:
[![kernelspace/userspace diagram](https://media.urbit.org/guides/core/app-school/kernelspace-userspace-diagram.svg)](https://media.urbit.org/guides/core/app-school/kernelspace-userspace-diagram.svg)
By and large, Gall _is_ the userspace vane - the majority of userspace is either
Gall agents, or things used by Gall agents. Apart from the agents themselves,
there's also:
- **Generators**: These are basically scripts. You'll likely already be familiar
with these from Hoon School. Aside from learning exercises, their main use is
to make interacting with Gall agents easier from the dojo. Rather than having
to manually craft `%poke`s to agents, generators can take a simpler input,
reformat it into what the agent actually expects, and poke the agent with it.
When you do something like `:dojo|wipe` in the dojo, you're actually running
the `/gen/dojo/wipe.hoon` generator and poking the `%dojo` agent with its
output.
- **Threads**: While generators are for strictly synchronous operations, threads
make it easy to implement sequences of asynchronous operations. Threads are
managed by the `%spider` agent. They can be used as mere scripts like
generators, but their main purpose is for performing complex IO. For example,
suppose you need to query some external web API, then with the data in its
response you make another API call, and then another, before finally having
the data you need. If one of the API calls fails, your Gall agent is
potentially left in a strange intermediary state. Instead, you can put all the
IO logic in a separate thread which is completely atomic. That way the Gall
agent only has to deal with the two conditions of success or failure. Writing
threads is covered in a [separate
guide](reference/arvo/threads/basics/fundamentals), which you might like to
work through after completing App School I.
- **Front-end**: Web UIs. It's possible for Gall agents to handle HTTP requests
directly and dynamically produce responses, but it's also possible to have a
static [glob](/guides/additional/dist/glob) of HTML, CSS, Javascript, images,
etc, which are served to the client like an ordinary web app. Such front-end
files are typically managed by the `%docket` agent which serves them via Eyre.
The [software distribution guide](/guides/additional/dist/guide) covers this in
detail, and you might like to work through it after completing App School I.
## The Filesystem
On an ordinary OS, you have persistent disk storage and volatile memory. An
application is launched by reading an executable file on disk, loading it into
memory and running it. The application will maybe read some more files from
disk, deserialize them into data structures in memory, perform some computations
and manipulate the data, then serialize the new data and write it back to disk.
This process is necessary because persistent storage is too slow to operate on
directly and the fast memory is wiped when it loses power. The result is that
all non-emphemeral data is ultimately stored as files in the filesystem on disk.
Arvo on the other hand is completely different.
Arvo has no concept of volatile memory - its whole state is assumed to be
persistent. This means it's unnecessary for a Gall agent to write its data to
the filesystem or read it in from the filesystem - an agent can just modify its
state in situ and leave it there. The urbit runtime writes events to disk and
backs up Arvo's state on the host OS to ensure data integrity but Arvo itself
isn't concerned with such details.
The result of this total persistence is that the filesystem—Clay—does not have
the same fundamental role as on an ordinary OS. In Arvo, very little of its data
is actually stored in Clay. The vast majority is just in the state of Gall
agents and vanes. For example, none of the chat messages, notebooks, etc, in the
Groups app exist in Clay - they're all in the state of the `%graph-store` agent.
For the most part, Clay just stores source code.
Clay has a few unique features—it's a typed filesystem, with all file types
defined in `mark` files. It's revision controlled, in a similar way to Git. It
also has a built-in build system (formerly a separate vane called Ford, but was
merged with Clay in 2020 to make atomicity of upgrades easier). We'll look at
some of these features in more detail later in the guide.
## Desk Anatomy
The fundamental unit in Clay is a desk. Desks are kind of like git repositories.
By default, new urbits come with the following desks included: `%base`, `%garden`, `%landscape`, `%webterm`
and `%bitcoin`.
- `%base` - This desk contains the kernel as well as some core agents and utilities.
- `%garden` - This desk contains agents and utilities for managing apps, and the
home screen that displays other app tiles.
- `%landscape` - This desk contains everything for the Groups app.
- `%webterm` - This desk is for the web dojo app.
- `%bitcoin` - This desk is for the bitcoin wallet app and bitcoin provider.
You'll typically also have a `%kids` desk, which is just a copy of `%base` from
upstream that sponsored ships (moons in the case of a planet, planets in the
case of a star) sync their `%base` desk from. Any third-party apps you've
installed will also have their own desks.
Desks are typically assumed to store their files according to the following directory structure:
```
desk
├── app
├── gen
├── lib
├── mar
├── sur
├── sys
├── ted
└── tests
```
- `app`: Gall agents.
- `gen`: Generators.
- `lib`: Libraries - these are imported with the `/+` Ford rune.
- `mar`: `mark` files, which are filetype definitions.
- `sur`: Structures - these typically contain type definitions and structures,
and would be imported with the `/-` Ford rune.
- `sys`: Kernel files and standard library. Only the `%base` desk has this
directory, it's omitted entirely in all other desks.
- `ted`: Threads.
- `tests`: Unit tests, to be run by the `%test` thread. This is often omitted in distributed desks.
This directory hierarchy is not strictly enforced, but most tools expect things
to be in their right place. Any of these folders can be omitted if they'd
otherwise be empty.
As mentioned, the `%base` desk alone includes a `/sys` directory containing the
kernel and standard libraries. It looks like this:
```
sys
├── arvo.hoon
├── hoon.hoon
├── lull.hoon
├── vane
│ ├── ames.hoon
│ ├── behn.hoon
│ ├── clay.hoon
│ ├── dill.hoon
│ ├── eyre.hoon
│ ├── gall.hoon
│ ├── iris.hoon
│ └── jael.hoon
└── zuse.hoon
```
- `arvo.hoon`: Source code for Arvo itself.
- `hoon.hoon`: Hoon standard library and compiler.
- `lull.hoon`: Mostly structures and type definitions for interacting with
vanes.
- `vane`: This directory contains the source code for each of the vanes.
- `zuse.hoon`: This is an extra utility library. It mostly contains
cryptographic functions and functions for dealing with web data like JSON.
The chain of dependency for the core kernel files is `hoon.hoon` -> `arvo.hoon`
-> `lull.hoon` -> `zuse.hoon`. For more information, see the [Filesystem
Hierarchy](/reference/arvo/reference/filesystem) documentation.
In addition to the directories discussed, there's a handful of special files a
desk might contain. All of them live in the root of the desk, and all are
optional in the general case, except for `sys.kelvin`, which is mandatory.
- `sys.kelvin`: Specifies the kernel version with which the desk is compatible.
- `desk.bill`: Specifies Gall agents to be auto-started upon desk installation.
- `desk.ship`: If the desk is being republished, the original publisher can be
specified here.
- `desk.docket-0`: Configures the front-end, tile, and other metadata for desks
which include a home screen app.
Each desk must be self-contained; it must include all the `mark`s, libraries,
threads, etc, that it needs. The one exception is the kernel and standard
libraries from the `%base` desk. Agents, threads and generators in other desks
all have these libraries available to them in their subject.
## APIs
You should now have a general idea of the different parts of Arvo, but how does
a Gall agent interact with these things?
There are two basic ways of interacting with other parts of the system: by
scrying into them, and by passing them messages and receiving messages in response.
There are also two basic things to interact with: vanes, and other agents.
- Scries: The scry system allows you to access the state of other agents and
vanes in a read-only fashion. Scries can be performed from any context with
the dotket (`.^`) rune. Each vane has "scry endpoints" which define what you
can read, and these are comprehensively documented in the Scry Reference of
each vane's section of the [Arvo documentation](/reference/arvo/overview). Agents
define scry endpoints in the `+on-peek` arm of their agent core.
Scries can only be done on the local ship; it is not yet possible to perform
scries over the network (but this functionality is planned for the future). There is a separate [guide to
scries](/reference/arvo/concepts/scry) which you might like to read through for
more details.
- Messages:
- Vanes: Each vane has a number of `task`s it can be passed and `gift`s it can
respond with in its respective section of `lull.hoon`. These might do all
manner of things, depending on the vane. For example, Iris might fetch an
external HTTP resource for you, Clay might read or build a specified file,
etc. The `task`s and `gift`s of each vane are comprehensively documented in the
API Reference of each vane's section of the [Arvo
documentation](/reference/arvo/overview).
- Agents: These can be `%poke`d with some data, which is a request to perform a single action. They can also be `%watch`ed,
which means to subscribe for updates. We'll discuss these in detail later in
the guide.
Here's a simplified diagram of the ways an agent can interact with other parts
of the system:
![api diagram](https://media.urbit.org/guides/core/app-school/api-diagram.svg)
Things like `on-poke` are arms of the agent core. Don't worry about their
meaning for now, we'll discuss them in detail later in the guide.
Inter-agent messaging can occur over the network, so you can interact with
agents on other ships as well as local ones. You can only talk to local vanes,
but some vanes like Clay are able to make requests to other ships on your
behalf. Note this summary is simplified - vanes don't just talk in `task`s and
`gift`s in all cases. For example, requests from HTTP clients through Eyre (the
webserver vane) behave more like those from agents than vanes, and a couple of
other vanes also have some different behaviours. Agent interactions are also a
little more complicated, and we'll discuss that later, but the basic patterns
described here cover the majority of cases.
## Environment Setup
Before proceeding with App School, you'll need to have an appropriate text
editor installed and configured, and know how to work with a fake ship for
development. Best practices are described in the [environment setup
guide](/guides/additional/development/environment). Example agents and other code throughout
this guide will just be committed to the `%base` desk of a fake ship, but it's a
good idea to have a read through that guide for when you begin work on your own
apps.

View File

@ -0,0 +1,362 @@
+++
title = "10. Scries"
weight = 50
+++
In this lesson we'll look at scrying agents, as well as how agents handle such
scries. If you're not at all familiar with performing scries in general, have a
read through the [Scry Guide](/reference/arvo/concepts/scry), as well as the [dotket
rune documentation](/reference/hoon/rune/dot#-dotket).
## Scrying
A scry is a read-only request to Arvo's global namespace. Vanes and agents
define _scry endpoints_ which allow data to be requested from their states. The
endpoints can process the data in any way before returning it, but they cannot
alter the actual state - scries can only read, not modify.
Most of the time, scry requests are handled by Arvo, which routes the request to
the appropriate vane. When you scry a Gall agent you actually scry Gall itself.
Gall interprets the request, runs it on the specified agent, and then returns
the result. Scries are performed with the
[dotket](/reference/hoon/rune/dot#-dotket) (`.^`) rune. Here's a summary of
their format:
![scry summary diagram](https://media.urbit.orgreference/arvo/scry-diagram-v2.svg)
A note on `care`s: Cares are most carefully implemented by Clay, where they specify
submodules and have tightly defined behaviors. For Gall agents, most of these
don't have any special behavior, and are just used to indicate the general kind
of data produced by the endpoint. There are a handful of exceptions to this:
`%d`, `%e`, `%u` and `%x`.
#### `%d`
A scry to Gall with a `%d` `care` and no `path` will produce the `desk` in which
the specified agent resides. For example:
```
> .^(desk %gd /=hark-store=)
%garden
> .^(desk %gd /=hood=)
%base
```
#### `%e`
A scry to Gall with a `%e` `care`, a `desk` rather than agent in the `desk`
field of the above diagram, and no path, will produce a set of all installed
agents on that desk and their status. For example:
```
> .^((set [=dude:gall live=?]) %ge /=garden=)
{ [dude=%hark-system-hook live=%.y]
[dude=%treaty live=%.y]
[dude=%docket live=%.y]
[dude=%settings-store live=%.y]
[dude=%hark-store live=%.y]
}
```
#### `%u`
A scry to Gall with a `%u` `care` and no `path` will check whether or not the
specified agent is installed and running:
```
> .^(? %gu /=btc-wallet=)
%.y
> .^(? %gu /=btc-provider=)
%.n
> .^(? %gu /=foobar=)
%.n
```
#### `%x`
A scry to Gall with a `%x` `care` will be passed to the agent for handling. Gall
handles `%x` specially, and expects an extra field at the end of the `path` that
specifies the `mark` to return. Gall will take the data produced by the
specified endpoint and try to convert it to the given mark, crashing if the mark
conversion fails. The extra field specifying the mark is not passed through to
the agent itself. Here's a couple of examples:
```
> =store -build-file /=landscape=/sur/graph-store/hoon
> .^(update:store %gx /=graph-store=/keys/noun)
[p=~2021.11.18..10.50.41..c914 q=[%keys resources={[entity=~zod name=%dm-inbox]}]]
> (crip (en-json:html .^(json %gx /=graph-store=/keys/json)))
'{"graph-update":{"keys":[{"name":"dm-inbox","ship":"zod"}]}}'
```
The majority of Gall agents simply take `%x` `care`s in their scry endpoints,
but in principle it's possible for a Gall agent to define a scry endpoint that
takes any one of the `care`s listed in the diagram above. An agent's scry
endpoints are defined in its `on-peek` arm, which we'll look at next.
## Handling scries
When a scry is performed on a Gall agent, Gall will strip out some extraneous
parts, and deliver it to the agent's `on-peek` arm as a `path`. The `path` will
only have two components from the diagram above: The _care_ and the _path_. For
example, a scry of `.^(update:store %gx /=graph-store=/keys/noun)` will come
into the `on-peek` arm of `%graph-store` as `/x/keys`.
The `on-peek` arm produces a `(unit (unit cage))`. The reason for the double
`unit` is that Arvo interprets `~` to mean the scry path couldn't be resolved,
and interprets `[~ ~]` to means it resolved to nothing. In either case the
dotket expression which initiated the scry will crash. The `cage` will contain
the actual data to return.
An ordinary `on-peek` arm, therefore, begins like so:
```hoon
++ on-peek
|= =path
^- (unit (unit cage))
....
```
Typically, you'd handle the `path` similarly to `on-watch`, as we discussed in
the lesson on subscriptions. You'd use something like a wutlus expression to
test the value of the `path`, defining your scry endpoints like so:
```hoon
?+ path (on-peek:def path)
[%x %some %path ~] ....
[%x %foo ~] ....
[%x %blah @ ~]
=/ =ship (slav %p i.t.t.path)
.....
....
```
Each endpoint would then compose the `(unit (unit cage))`. The simplest way to
format it is like:
```hoon
``noun+!>('some data')
```
If it requires a more complex expression to retrieve or compose the data, you
can do something like:
```hoon
:^ ~ ~ %some-mark
!> ^- some-type
:+ 'foo'
'bar'
'baz'
```
Previously we discussed custom `mark` files. Such mark files are most commonly
used when the data might be accessed through Eyre's HTTP API, and therefore
required JSON conversion methods. We cover such things separately in the
[Full-Stack Walkthrough](/guides/core/app-school-full-stack/1-intro), but note that if
that's the case for your agent, you may wish to also have your scry endpoints
return data with your custom `mark` so it can easily be converted to JSON when
accessed from the web.
In some cases, typically with scry `path`s that contain wildcards like the `[%x %blah @ ~]` example above, your agent may not always be able to find the
requested data. In such cases, you can just produce a cell of `[~ ~]` for the
`(unit (unit cage))`. Keep in mind, however, that this will result in a crash
for the dotket expression which initiated the scry. In some cases you may want
that, but in other cases you may not, so instead you could wrap the data inside
the `vase` in a `unit` and have _that_ be null instead. It all depends on the
needs of your particular application and its clients.
## Example
Here's a simple example agent with three scry endpoints:
#### `peeker.hoon`
```hoon
/+ default-agent, dbug
|%
+$ versioned-state
$% state-0
==
+$ state-0 [%0 data=(map @p @t)]
+$ card card:agent:gall
--
%- agent:dbug
=| state-0
=* state -
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
::
++ on-init
^- (quip card _this)
`this
::
++ on-save
^- vase
!>(state)
::
++ on-load
|= old-state=vase
^- (quip card _this)
=/ old !<(versioned-state old-state)
?- -.old
%0 `this(state old)
==
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?> =(src.bowl our.bowl)
?+ mark (on-poke:def mark vase)
%noun
`this(data (~(put by data) !<([@p @t] vase)))
==
::
++ on-watch on-watch:def
++ on-leave on-leave:def
::
++ on-peek
|= =path
^- (unit (unit cage))
?+ path (on-peek:def path)
[%x %all ~] ``noun+!>(data)
::
[%x %has @ ~]
=/ who=@p (slav %p i.t.t.path)
``noun+!>(`?`(~(has by data) who))
::
[%x %get @ ~]
=/ who=@p (slav %p i.t.t.path)
=/ maybe-res (~(get by data) who)
?~ maybe-res
[~ ~]
``noun+!>(`@t`u.maybe-res)
==
::
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--
```
The agent's `on-poke` arm takes a cell of `[@p @t]` and saves it in the agent's
state, which contains a `(map @p @t)` called `data`. The `on-peek` arm is:
```hoon
++ on-peek
|= =path
^- (unit (unit cage))
?+ path (on-peek:def path)
[%x %all ~] ``noun+!>(data)
::
[%x %has @ ~]
=/ who=@p (slav %p i.t.t.path)
``noun+!>(`?`(~(has by data) who))
::
[%x %get @ ~]
=/ who=@p (slav %p i.t.t.path)
=/ maybe-res (~(get by data) who)
?~ maybe-res
[~ ~]
``noun+!>(`@t`u.maybe-res)
==
```
It defines three scry endpoints, all using a `%x` `care`: `/x/all`,
`/x/has/[ship]`, and `/x/get/[ship]`. The first will simply return the entire
`(map @p @t)` in the agent's state. The second will check whether the given ship
is in the map and produce a `?`. The third will produce the `@t` for the given
`@p` if it exists in the map, or else return `[~ ~]` to indicate the data
doesn't exist, producing a crash in the dotket expression.
Let's try it out. Save the agent above as `/app/peeker.hoon` in the `%base`
desk, `|commit %base` and start the agent with `|rein %base [& %peeker]`.
First, let's add some data to the map:
```
> :peeker [~zod 'foo']
>=
> :peeker [~nut 'bar']
>=
> :peeker [~wet 'baz']
>=
```
Now if we use `dbug` to inspect the state, we'll see the data has been added:
```
> {[p=~wet q='baz'] [p=~nut q='bar'] [p=~zod q='foo']}
> :peeker +dbug [%state %data]
>=
```
Next, let's try the `/x/all` scry endpoint:
```
> .^((map @p @t) %gx /=peeker=/all/noun)
{[p=~wet q='baz'] [p=~nut q='bar'] [p=~zod q='foo']}
```
The `/x/has/[ship]` endpoint:
```
> .^(? %gx /=peeker=/has/~zod/noun)
%.y
> .^(? %gx /=peeker=/has/~wet/noun)
%.y
> .^(? %gx /=peeker=/has/~nes/noun)
%.n
```
And finally, the `/x/get/[ship]` endpoint:
```
> .^(@t %gx /=peeker=/get/~zod/noun)
'foo'
> .^(@t %gx /=peeker=/get/~wet/noun)
'baz'
```
We'll now try scrying for a ship that doesn't exist in the map. Note that due to
a bug at the time of writing, the resulting crash will wipe the dojo's subject,
so don't try this if you've got anything pinned there that you want to keep.
```
~zod:dojo> .^(@t %gx /=peeker=/get/~nes/noun)
crash!
```
## Summary
- Scries are read-only requests to vanes or agents which can be done inside any
code, during its evaluation.
- Scries are performed with the dotket (`.^`) rune.
- Scries will fail if the scry endpoint does not exist, the requested data does
not exist, or the data does not nest in the return type specified.
- Scries can only be performed on the local ship, not on remote ships.
- Gall scries with an agent name in the `desk` field will be passed to that
agent's `on-peek` arm for handling.
- Gall scries with a `%x` `care` take a `mark` at the end of the scry `path`,
telling Gall to convert the data returned by the scry endpoint to the mark
specified.
- The `on-peek` arm takes a `path` with the `care` in the head and the `path` part
of the scry in the tail, like `/x/some/path`.
- The `on-peek` arm produces a `(unit (unit cage))`. The outer `unit` is null if
the scry endpoint does not exist, and the inner `unit` is null if the data
does not exist.
## Exercises
- Have a read through the [Scry Guide](/reference/arvo/concepts/scry).
- Have a read through the [dotket rune
documentation](/reference/hoon/rune/dot#-dotket).
- Run through the [Example](#example) yourself if you've not done so already.
- Try adding another scry endpoint to the `peeker.hoon` agent, which uses a
[`wyt:by`](/reference/hoon/stdlib/2i#wytby) map function to produce the
number of items in the `data` map.
- Have a look through the `on-peek` arms of some other agents on your ship, and
try performing some scries to some of the endpoints.

View File

@ -0,0 +1,145 @@
+++
title = "11. Failure"
weight = 55
+++
In this lesson we'll cover the last agent arm we haven't touched on yet:
`on-fail`. We'll also touch on one last concept, which is the _helper core_.
# Failures
When crashes or errors occur in certain cases, Gall passes them to an agent's
`on-fail` arm for handling. This arm is very seldom used, almost all agents
leave it for `default-agent` to handle, which just prints the error message to
the terminal. While you're unlikely to use this arm, we'll briefly go over its
behavior for completeness.
`on-fail` takes a `term` error message and a `tang`, typically containing a
stack trace, and often with additional messages about the error. If it weren't
delegated to `on-fail:def`, it would begin with:
```hoon
++ on-fail
|= [=term =tang]
^- (quip card _this)
....
```
Gall calls `on-fail` in four cases:
- When there's a crash in the `on-arvo` arm.
- When there's a crash in the `on-agent` arm.
- When there's a crash in the `on-leave` arm.
- When an agent produces a `%watch` card but the `wire`, ship, agent and `path`
specified are the same as an existing subscription.
For an `on-arvo` failure, the `term` will always be `%arvo-response`, and the
`tang` will contain a stack trace.
For `on-agent`, the `term` will be the head of the `sign` (`%poke-ack`, `%fact`,
etc). The `tang` will contain a stack trace and a message of "closing
subscription".
For an `on-leave` failure, the `term` will always be `%leave`, and the `tang`
will contain a stack trace.
For a `%watch` failure, the `term` will be `%watch-not-unique`. The `tang` will
include a message of "subscribe wire not unique", as well as the agent name, the
`wire`, the target ship and the target agent.
How you might handle these cases (if you wanted to manually handle them) depends
on the purpose of your particular agent.
## Helper core
Back in the lesson on lustar virtual arms, we briefly mentioned a common pattern
is to define a deferred expression for a helper core named `hc` like:
```hoon
+* this .
def ~(. (default-agent this %.n) bowl)
hc ~(. +> bowl)
```
The name `do` is also used frequently besides `hc`.
A helper core is a separate core composed into the subject of the agent core,
containing useful functions for use by the agent arms. Such a helper core would
typically contain functions that would only ever be used internally by the
agent - more general functions would usually be included in a separate `/lib`
library and imported with a [faslus](/reference/arvo/ford/ford#ford-runes) (`/+`)
rune. Additionally, you might recall that the example agent of the
[subscriptions lesson](/guides/core/app-school/8-subscriptions#example) used
a barket (`|^`) rune to create a door in the `on-poke` arm with a separate
`handle-poke` arm. That approach is typically used when functions will only be
used in that one arm. The helper core, on the other hand, is useful when
functions will be used by multiple agent arms.
The conventional pattern is to have the helper core _below_ the agent core, so
the structure of the agent file is like:
```
[imports]
[state types core]
[agent core]
[helper core]
```
Recall that the build system will implicitly compose any discrete expressions.
If we simply added the helper core below the agent core, the agent core would be
composed into the subject of the helper core, which is the opposite of what we
want. Instead, we must inversely compose the two cores with a
[tisgal](/reference/hoon/rune/tis#-tisgal) (`=<`) rune. We add the tisgal
rune directly above the agent core like:
```hoon
.....
=<
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
hc ~(. +> bowl)
++ on-init
.....
```
We can then add the helper core below the agent core. The helper core is most
typically a door like the agent core, also with the `bowl` as its sample. This
is just so any functions you define in it have ready access to the `bowl`. It
would look like:
```hoon
|_ =bowl:gall
++ some-function ...
++ another ....
++ etc ...
--
```
Back in the lustar virtual arm of the agent core, we give it a deferred expression name of `hc`
and call it like so:
```hoon
hc ~(. +> bowl)
```
To get to the helper core we composed from within the door, we use a
[censig](/reference/hoon/rune/cen#-censig) expression to call `+>` of the
subject (`.`) with the `bowl` as its sample. After that, any agent arms can make
use of helper core functions by calling them like `(some-function:hc ....)`.
## Summary
- `on-fail` is called in certain cases of crashes or failures.
- Crashes in the `on-agent`, `on-arvo`, or `on-watch` arms will trigger a call
to `on-fail`.
- A non-unique `%watch` `card` will also trigger a call to `on-fail`.
- `on-fail` is seldom used - most agents just leave it to `%default-agent` to
handle, which just prints the error to the terminal.
- A helper core is an extra core of useful functions, composed into the subject
of the agent core.
- Helper cores are typically placed below the agent core, and composed with a
tisgal (`=<`) rune.
- The helper core is typically a door with the `bowl` as a sample.
- The helper core is typically given a name of `hc` or `do` in the lustar virtual arm
of the agent core.

View File

@ -0,0 +1,49 @@
+++
title = "12. Next Steps"
weight = 60
+++
We've now covered all the arms of a Gall agent, and everything you need to know
to start writing your own agent.
The things haven't touched on yet are front-end development and integration,
Eyre's HTTP API for communicating with agents from the web, and dealing with
JSON data. The [Full-stack Walkthrough](/guides/core/app-school-full-stack/1-intro)
covers these aspects of Urbit app development, and it also puts into practice
many of the concepts we've discussed in this guide, so you might like to work
through that next. In addition to that walkthrough, you can refer to the
following documents for help writing a web front-end for your app:
- [Eyre's external API reference](/reference/arvo/eyre/external-api-ref) - This
explains Eyre's HTTP API, through which a browser or other HTTP client can
interact with a Gall agent.
- [Eyre's usage guide](/reference/arvo/eyre/guide) - This walks through examples of
using Eyre's HTTP API.
- [JSON guide](/guides/additional/hoon/json-guide) - This walks through the basics of
converting Hoon data structures to JSON, for use with a web client. It also
covers JSON conversion methods in `mark` files.
- [Zuse reference](/reference/hoon/zuse/table-of-contents) - This contains
documentation of all JSON encoding and decoding functions included in the
`zuse.hoon` utility library.
- [The software distribution guide](/guides/additional/dist/dist) - This covers
everything you need to know to distribute apps to other ships. It includes
details of bundling a web front-end and serving it to the user in the browser.
- [The HTTP API guide](/guides/additional/http-api-guide) - This is a reference
and guide to using the `@urbit/http-api` NPM module.
- [The Sail guide](/guides/additional/hoon/sail) - Sail is a domain-specific language
for composing XML structure in Hoon. It can be used to compose front-ends for
Urbit apps directly in agents, as an alternative approach to having a
separate Javascript app.
In addition to these documents about creating a web-based user interface for
your app, there are some other guides you might like to have a look at:
- [Threads guide](reference/arvo/threads/overview) - Threads are like transient
agents, typically used for handling complex I/O functionality for Gall
agents - like interacting with an external HTTP API.
- [The software distribution guide](/guides/additional/dist/dist) - This explains
how to set up a desk for distribution, so other people can install your app.
For more development resources, and for ways to get involved with the Urbit
development community, see the [Urbit Developers
site](https://developers.urbit.org/).

View File

@ -0,0 +1,304 @@
+++
title = "2. The Agent Core"
weight = 10
+++
In this lesson we'll look at the basic type and structure of a Gall agent.
A Gall agent is a [door](/reference/glossary/door) with exactly ten [arms](/reference/glossary/arm). Each arm is responsible for
handling certain kinds of events that Gall feeds in to the agent. A door is
just a [core](/reference/glossary/core) with a sample - it's made with the
[barcab](/reference/hoon/rune/bar#_-barcab) rune (`|_`) instead of the
usual [barcen](/reference/hoon/rune/bar#-barcen) rune (`|%`).
## The ten arms
We'll discuss each of the arms in detail later. For now, here's a quick summary.
The arms of an agent can be be roughly grouped by purpose:
### State management
These arms are primarily for initializing and upgrading an agent.
- `on-init`: Handles starting an agent for the first time.
- `on-save`: Handles exporting an agent's state - typically as part of the
upgrade process but also when suspending, uninstalling and debugging.
- `on-load`: Handles loading a previously exported agent state - typically as
part of the upgrade process but also when resuming or reinstalling an agent.
### Request handlers
These arms handle requests initiated by outside entities, e.g. other agents,
HTTP requests from the front-end, etc.
- `on-poke`: Handles one-off requests, actions, etc.
- `on-watch`: Handles subscription requests from other entities.
- `on-leave`: Handles unsubscribe notifications from other, previously subscribed
entities.
### Response handlers
These two arms handle responses to requests our agent previously initiated.
- `on-agent`: Handles request acknowledgements and subscription updates from
other agents.
- `on-arvo`: Handles responses from vanes.
### Scry handler
- `on-peek`: Handles local read-only requests.
### Failure handler
- `on-fail`: Handles certain kinds of crash reports from Gall.
## Bowl
The sample of a Gall agent door is always a `bowl:gall`. Every time an event triggers the
agent, Gall populates the bowl with things like the current date-time, fresh entropy,
subscription information, which ship the request came from, etc, so that all the
arms of the agent have access to that data. For the exact structure and contents
of the bowl, have a read through [its entry in the Gall vane types
documentation](/reference/arvo/gall/data-types#bowl).
One important thing to note is that the bowl is only repopulated when there's a
new Arvo event. If a local agent or web client were to send multiple
messages to your agent at the same time, these would all arrive in the same
event. This means if your agent depended on a unique date-time or entropy to
process each message, you could run into problems if your agent doesn't account
for this possibility.
## State
If you've worked through [Hoon School](/guides/core/hoon-school/), you may
recall that a core is a cell of `[battery payload]`. The battery is the core
itself compiled to Nock, and the payload is the subject which it operates on.
For an agent, the payload will at least contain the bowl, the usual Hoon and `zuse` standard
library functions, and the **state** of the agent. For example, if your agent
were for an address book app, it might keep a `map` of ships to address book
entries. It might add entries, delete entries, and modify entries. This address
book `map` would be part of the state stored in the payload.
## Transition function
If you recall from the prologue, the whole Arvo operating system works on the
basis of a simple transition function `(event, oldState) -> (effects, newState)`. Gall agents also function the same way. Eight of an agent's ten arms
produce the same thing, a cell of:
- **Head**: A list of effects called `card`s (which we'll discuss later).
- **Tail**: A new agent core, possibly with a modified payload.
It goes something like this:
1. An event is routed to Gall.
2. Gall calls the appropriate arm of the agent, depending on the kind of event.
3. That arm processes the event, returning a list `card`s to be sent off, and
the agent core itself with a modified state in the payload.
4. Gall sends the `card`s off and saves the modified agent core.
5. Rinse and repeat.
## Virtualization
When a crash occurs in the kernel, the system usually aborts the computation and
discards the event as though it never happened. Gall on the other hand
virtualizes all its agents, so this doesn't happen. Instead, when a crash occurs
in an agent, Gall intercepts the crash and takes appropriate action depending on
the kind of event that caused it. For example, if a poke from another ship
caused a crash in the `on-poke` arm, Gall will respond to the poke with a
"nack", a negative acknowledgement, telling the original ship the poke was
rejected.
What this means is that you can intentionally design your agent to crash in
cases it can't handle. For example, if a poke comes in with an unexpected
`mark`, it crashes. If a permission check fails, it crashes. This is quite
different to most programs written in procedural languages, which must handle
all exceptions to avoid crashing.
## Example
Here's about the simplest valid Gall agent:
```hoon
|_ =bowl:gall
++ on-init `..on-init
++ on-save !>(~)
++ on-load |=(vase `..on-init)
++ on-poke |=(cage !!)
++ on-watch |=(path !!)
++ on-leave |=(path `..on-init)
++ on-peek |=(path ~)
++ on-agent |=([wire sign:agent:gall] !!)
++ on-arvo |=([wire sign-arvo] !!)
++ on-fail |=([term tang] `..on-init)
--
```
This is just a dummy agent that does absolutely nothing - it has no state and
rejects all messages by crashing. Typically we'd cast this to an `agent:gall`,
but in this instance we won't so it's easier to examine its structure in the
dojo. We'll get to what each of the arms do later. For now, we'll just consider
a few particular points.
Firstly, note its structure - it's a door (created with `|_`) with a sample of
`bowl:gall` and the ten arms described earlier.
Secondly, you'll notice some of the arms return:
```hoon
`..on-init
```
A backtick at the beginning is an irregular syntax meaning "prepend with null",
so for example, in the dojo:
```
> `50
[~ 50]
```
The next part has `..on-init`, which means "the subject of the `on-init` arm".
The subject of the `on-init` arm is our whole agent. In the [transition
function](#transition-function) section we mentioned that most arms return a
list of effects called `card`s and a new agent core. Since an empty list is `~`,
we've created a cell that fits that description.
Let's examine our agent. In the dojo of a fake ship, mount the `%base` desk with
`|mount %base`. On the Unix side, navigate to `/path/to/fake/ship/base`, and save
the above agent in the `/app` directory as `skeleton.hoon`. Back in the dojo,
commit the file to the desk with `|commit %base`.
For the moment we won't install our `%skeleton` agent. Instead, we'll use the
`%build-file` thread to build it and save it in the dojo's subject so we can
have a look. Run the following in the dojo:
```
> =skeleton -build-file %/app/skeleton/hoon
```
Now, let's have a look:
```
> skeleton
< 10.fxw
[ bowl
[ [our=@p src=@p dap=@tas]
[ wex=nlr([p=[wire=/ ship=@p term=@tas] q=[acked=?(%.y %.n) path=/]])
sup=nlr([p=it(/) q=[p=@p q=/]])
]
act=@ud
eny=@uvJ
now=@da
byk=[p=@p q=@tas r=?([%da p=@da] [%tas p=@tas] [%ud p=@ud])]
]
<17.zbp 33.wxp 14.dyd 53.vlb 77.wir 232.wfe 51.qbt 123.zao 46.hgz 1.pnw %140>
]
>
```
The dojo pretty-prints cores with a format of `number-of-arms.hash`. You can see
the head of `skeleton` is `10.fxw` - that's the battery of the core, our 10-arm
agent. If we try printing the head of `skeleton` we'll see it's a whole lot of
compiled Nock:
```
> -.skeleton
[ [ 11
[ 1.953.460.339
1
[ 7.368.801
7.957.707.045.546.060.659
1.852.796.776
0
]
[7 15]
7
34
]
...(truncated for brevity)...
```
The battery's not too important, it's not something we'd ever touch in practice.
Instead, let's have a look at the core's payload by printing the tail of
`skeleton`. We'll see its head is the `bowl:gall` sample we specified, and then
the tail is just all the usual standard library functions:
```
> +.skeleton
[ bowl
[ [our=~zod src=~zod dap=%$]
[wex={} sup={}]
act=0
eny=0v0
now=~2000.1.1
byk=[p=~zod q=%$ r=[%ud p=0]]
]
<17.zbp 33.wxp 14.dyd 53.vlb 77.wir 232.wfe 51.qbt 123.zao 46.hgz 1.pnw %140>
]
```
Currently `skeleton` has no state, but of course in practice you'd want to store
some actual data. We'll add `foo=42` as our state with the `=+` rune at the
beginning of our agent:
```hoon
=+ foo=42
|_ =bowl:gall
++ on-init `..on-init
++ on-save !>(~)
++ on-load |=(vase `..on-init)
++ on-poke |=(cage !!)
++ on-watch |=(path !!)
++ on-leave |=(path `..on-init)
++ on-peek |=(path ~)
++ on-agent |=([wire sign:agent:gall] !!)
++ on-arvo |=([wire sign-arvo] !!)
++ on-fail |=([term tang] `..on-init)
--
```
Save the modified `skeleton.hoon` in `/app` on the `%base` desk like before, and run `|commit %base` again in the dojo. Then, rebuild it with the same `%build-file` command as before:
```
> =skeleton -build-file %/app/skeleton/hoon
```
If we again examine our agent core's payload by looking at the tail of
`skeleton`, we'll see `foo=42` is now included:
```
> +.skeleton
[ bowl
[ [our=~zod src=~zod dap=%$]
[wex={} sup={}]
act=0
eny=0v0
now=~2000.1.1
byk=[p=~zod q=%$ r=[%ud p=0]]
]
foo=42
<17.zbp 33.wxp 14.dyd 53.vlb 77.wir 232.wfe 51.qbt 123.zao 46.hgz 1.pnw %140>
]
```
## Summary
- A Gall agent is a door with exactly ten specific arms and a sample of `bowl:gall`.
- Each of the ten arms handle different kinds of events - Gall calls the
appropriate arm for the kind event it receives.
- The ten arms fit roughly into five categories:
- State management.
- Request handlers.
- Response handlers.
- Scry handler.
- Failure handler.
- The state of an agent—the data it's storing—lives in the core's payload.
- Most arms produce a list of effects called `card`s, and a new agent core with
a modified state in its payload.
## Exercises
- Run through the [Example](#example) yourself on a fake ship if you've not done
so already.
- Have a look at the [`bowl` entry in the Gall data types
documentation](/reference/arvo/gall/data-types#bowl) if you've not done so already.

View File

@ -0,0 +1,283 @@
+++
title = "3. Imports and Aliases"
weight = 15
+++
In the last lesson we looked at the most basic aspects of a Gall agent's
structure. Before we get into the different agent arms in detail, there's some
boilerplate to cover that makes life easier when writing Gall agents.
## Useful libraries
There are a couple of libraries that you'll very likely use in every agent you
write. These are [`default-agent`](#default-agent) and [`dbug`](#dbug). In
brief, `default-agent` provides simple default behaviours for each agent arm,
and `dbug` lets you inspect the state and bowl of an agent from the dojo, for
debugging purposes. Every example agent we look at from here on out will make
use of both libraries.
Let's look at each in more detail:
### `default-agent`
The `default-agent` library contains a basic agent with sane default behaviours
for each arm. In some cases it just crashes and prints an error message to the
terminal, and in others it succeeds but does nothing. It has two primary uses:
- For any agent arms you don't need, you can just have them call the matching
function in `default-agent`, rather than having to manually handle events on
those arms.
- A common pattern in an agent is to switch on the input of an arm with
[wutlus](/reference/hoon/rune/wut#-wutlus) (`?+`) runes or maybe
[wutcol](/reference/hoon/rune/wut#-wutcol) (`?:`) runes. For any
unexpected input, you can just pass it to the relevant arm of `default-agent`
rather than handling it manually.
The `default-agent` library lives in `/lib/default-agent/hoon` of the `%base`
desk, and you would typically include a copy in any new desk you created. It's
imported at the beginning of an agent with the
[faslus](/reference/arvo/ford/ford#ford-runes) (`/+`) rune.
The library is a wet gate which takes two arguments: `agent` and `help`. The
first is your agent core itself, and the second is a `?`. If `help` is `%.y` (equivalently, `%&`), it
will crash in all cases. If `help` is `%.n` (equivalently, `%|`), it will use its defaults. You would
almost always have `help` as `%.n`.
The wet gate returns an `agent:gall` door with a sample of `bowl:gall` - a
typical agent core. Usually you would define an alias for it in a virtual arm
([explained below](#virtual-arms)) so it's simple to call.
### `dbug`
The `dbug` library lets you inspect the state and `bowl` of your agent from the
dojo. It includes an `agent:dbug` function which wraps your whole `agent:gall`
door, adding its extra debugging functionality while transparently passing
events to your agent for handling like usual.
To use it, you just import `dbug` with a
[faslus](/reference/arvo/ford/ford#ford-runes) (`/+`) rune at the beginning, then add
the following line directly before the door of your agent:
```hoon
%- agent:dbug
```
With that done, you can poke your agent with the `+dbug` generator from the dojo
and it will pretty-print its state, like:
```
> :your-agent +dbug
```
The generator also has a few useful optional arguments:
- `%bowl`: Print the agent's bowl.
- `[%state 'some code']`: Evaluate some code with the agent's state as its
subject and print the result. The most common case is `[%state %some-face]`,
which will print the contents of the wing with the given face.
- `[%incoming ...]`: Print details of the matching incoming subscription, one
of:
- `[%incoming %ship ~some-ship]`
- `[%incoming %path /part/of/path]`
- `[%incoming %wire /part/of/wire]`
- `[%outgoing ...]`: Print details of the matching outgoing subscription, one
of:
- `[%outgoing %ship ~some-ship]`
- `[%outgoing %path /part/of/path]`
- `[%outgoing %wire /part/of/wire]`
- `[outgoing %term %agent-name]`
By default it will retrieve your agent's state by using its `on-save` arm, but
if your app implements a scry endpoint with a path of `/x/dbug/state`, it will
use that instead.
We haven't yet covered some of the concepts described here, so don't worry if
you don't fully understand `dbug`'s functionality - you can refer back here
later.
## Virtual arms
An agent core must have exactly ten arms. However, there's a special kind of
"virtual arm" that can be added without actually increasing the core's arm
count, since it really just adds code to the other arms in the core. A virtual arm is created with the
[lustar](/reference/hoon/rune/lus#-lustar) (`+*`) rune, and its purpose is
to define _deferred expressions_. It takes a list of pairs of names and Hoon
expressions. When compiled, the deferred expressions defined in the virtual arm are
implicitly inserted at the beginning of every other arm of the core, so they all
have access to them. Each time a name in a `+*` is called, the associated Hoon is evaluated in its place, similar to lazy evaluation except it is re-evaluated whenever needed. See the [tistar](/reference/hoon/rune/tis#-tistar) reference for more information on deferred expressions.
A virtual arm in an agent often looks something like this:
```hoon
+* this .
def ~(. (default-agent this %.n) bowl)
```
`this` and `def` are the deferred expressions, and next to each one is the Hoon
expression it evaluates whenever called. Notice that unlike most things that
take _n_ arguments, a virtual arm is not terminated with a `==`. You can define
as many aliases as you like. The two in this example are conventional ones you'd
use in most agents you write. Their purposes are:
```hoon
this .
```
Rather than having to return `..on-init` like we did in the last lesson,
instead our arms can just refer to `this` whenever modifying or returning the
agent core.
```hoon
def ~(. (default-agent this %.n) bowl)
```
This sets up the `default-agent` library we [described above](#default-agent),
so you can easily call its arms like `on-poke:def`, `on-agent:def`, etc.
## Additional cores
While Gall expects a single 10-arm agent core, it's possible to include
additional cores by composing them into the subject of the agent core itself.
The contents of these cores will then be available to arms of the agent core.
Usually to compose cores in this way, you'd have to do something like insert
[tisgar](/reference/hoon/rune/tis#-tisgar) (`=>`) runes in between them.
However, Clay's build system implicitly composes everything in a file by
wrapping it in a [tissig](/reference/hoon/rune/tis#-tissig) (`=~`)
expression, which means you can just butt separate cores up against one another
and they'll all still get composed.
You can add as many extra cores as you'd like before the agent core, but
typically you'd just add one containing type definitions for the agent's state,
as well as any other useful structures. We'll look at the state in more detail
in the next lesson.
## Example
Here's the `skeleton.hoon` dummy agent from the previous lesson, modified with
the concepts discussed here:
```hoon
/+ default-agent, dbug
|%
+$ card card:agent:gall
--
%- agent:dbug
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
++ on-init
^- (quip card _this)
`this
++ on-save on-save:def
++ on-load on-load:def
++ on-poke on-poke:def
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--
```
The first line uses the faslus (`/+`) Ford rune to import
`/lib/default-agent.hoon` and `/lib/dbug.hoon`, building them and loading them
into the subject of our agent so they're available for use. You can read more
about Ford runes in the [Ford section of the vane
documenation](/reference/arvo/ford/ford#ford-runes).
Next, we've added an extra core. Notice how it's not explicitly composed, since
the build system will do that for us. In this case we've just added a single
`card` arm, which makes it simpler to reference the `card:agent:gall` type.
After that core, we call `agent:dbug` with our whole agent core as its argument.
This allows us to use the `dbug` features described earlier.
Inside our agent door, we've added an extra virtual arm and defined a couple
deferred expressions:
```hoon
+* this .
def ~(. (default-agent this %.n) bowl)
```
In most of the arms, you see we've been able to replace the dummy code with
simple calls to the corresponding arms of `default-agent`, which we set up as a deferred
expression named `def` in the virtual arm. We've also replaced the old `..on-init`
with our deferred expression named `this` in the `on-init` arm as an example - it makes things a bit
simpler.
You can save the code above in `/app/skeleton.hoon` of your `%base` desk like
before and `|commit %base` in the dojo. Additionally, you can start the agent so
we can try out `dbug`. To start it, run the following in the dojo:
```
> |rein %base [& %skeleton]
```
For details of using the `|rein` generator, see the [Dojo
Tools](/using/os/dojo-tools#rein) documentation.
Now our agent should be running, so let's try out `dbug`. In the dojo, let's try
poking our agent with the `+dbug` generator:
```
> ~
> :skeleton +dbug
>=
```
It just printed out `~`. Our dummy `skeleton` agent doesn't have any state
defined, so it's printing out null as a result. Let's try printing the `bowl`
instead:
```
> [ [our=~zod src=~zod dap=%skeleton]
[wex={} sup={}]
act=5
eny
0v209.tg795.bc2e8.uja0d.11eq9.qp3b3.mlttd.gmf09.q7ro3.6unfh.16jiu.m9lh9.6jlt8.4f847.f0qfh.up08t.3h4l2.qm39h.r3qdd.k1r11.bja8l
now=~2021.11.5..13.28.24..e20e
byk=[p=~zod q=%base r=[%da p=~2021.11.5..12.02.22..f99b]]
]
> :skeleton +dbug %bowl
>=
```
We'll use `dbug` more throughout the guide, but hopefully you should now have an
idea of its basic usage.
## Summary
The key takeaways are:
- Libraries are imported with `/+`.
- `default-agent` is a library that provides default behaviors for Gall agent
arms.
- `dbug` is a library that lets you inspect the state and `bowl` of an agent
from the dojo, with the `+dbug` generator.
- Convenient deferred expressions for Hoon expressions can be defined in a virtual arm with
the [lustar](/reference/hoon/rune/lus#-lustar) (`+*`) rune.
- `this` is a conventional deferred expression name for the agent core itself.
- `def` is a conventional deferred expression name for accessing arms in the `default-agent`
library.
- Extra cores can be composed into the subject of the agent core. The
composition is done implicitly by the build system. Typically we'd include one
extra core that defines types for our agent's state and maybe other useful
types as well.
## Exercises
- Run through the [example](#example) yourself on a fake ship if you've not done
so already.
- Have a read through the [Ford rune
documentation](/reference/arvo/ford/ford#ford-runes) for details about importing
libraries, structures and other things.
- Try the `+dbug` generator out on some other agents, like `:settings-store +dbug`, `:btc-wallet +dbug`, etc, and try some of its options [described
above](#dbug).
- Have a quick look over the source of the `default-agent` library, located at
`/lib/default-agent.hoon` in the `%base` desk. We've not yet covered what the
different arms do but it's still useful to get a general idea, and you'll
likely want to refer back to it later.

View File

@ -0,0 +1,498 @@
+++
title = "4. Lifecycle"
weight = 20
+++
In the last lesson we looked at a couple of useful things used as boilerplate in
most agents. Now we're going to get into the guts of how agents work, and start
looking at what the agent arms do. The first thing we'll look at is the agent's
state, and the three arms for managing it: `on-init`, `on-save`, and `on-load`.
These arms handle what we call an agent's "lifecycle".
## Lifecycle
An agent's lifecycle starts when it's first installed. At this point, the
agent's `on-init` arm is called. This is the _only_ time `on-init` is ever
called - its purpose is just to initialize the agent. The `on-init` arm might be
very simple and just set an initial value for the state, or even do nothing at
all and return the agent core exactly as-is. It may also be more complicated,
and perform some [scries](/reference/arvo/concepts/scry) to obtain extra data or
check that another agent is also installed. It might send off some `card`s to
other agents or vanes to do things like load data in to the `%settings-store`
agent, bind an Eyre endpoint, or anything else. It all depends on the needs of
your particular application. If `on-init` fails for whatever reason, the agent
installation will fail and be aborted.
Once initialized, an agent will just go on doing its thing - processing events,
updating its state, producing effects, etc. At some point, you'll likely want to
push an update for your agent. Maybe it's a bug fix, maybe you want to add extra
features. Whatever the reason, you need to change the source code of your agent,
so you commit a modified version of the file to Clay. When the commit completes, Gall updates the app as follows:
- The agent's `on-save` arm is called, which packs the agent's state in a `vase`
and exports it.
- The new version of the agent is built and loaded into Gall.
- The previously exported `vase` is passed to the `on-load` arm of the newly
built agent. The `on-load` arm will process it, convert it to the new version
of the state if necessary, and load it back into the state of the agent.
A `vase` is just a cell of `[type-of-the-noun the-noun]`. Most data an agent
sends or receives will be encapsulated in a vase. A vase is made with the
[zapgar](/reference/hoon/rune/zap#-zapgar) (`!>`) rune like
`!>(some-data)`, and unpacked with the
[zapgal](/reference/hoon/rune/zap#-zapgal) (`!<`) rune like
`!<(type-to-extract vase)`. Have a read through the [`vase` section of the type
reference for details](/guides/core/app-school/types#vase).
We'll look at the three arms described here in a little more detail, but first
we need to touch on the state itself.
## Versioned state type
In the previous lesson we introduced the idea of composing additional cores into
the subject of the agent core. Here we'll look at using such a core to define
the type of the agent's state. In principle, we could make it as simple as this:
```hoon
|%
+$ my-state-type @ud
--
```
However, when you update your agent as described in the [Lifecycle](#lifecycle)
section, you may want to change the type of the state itself. This means
`on-load` might find different versions of the state in the `vase` it receives,
and it might not be able to distinguish between them.
For example, if you were creating an agent for a To-Do task management app, your
tasks might initially have a `?(%todo %done)` union to specify whether they're
complete or not. Something like:
```hoon
(map task=@t status=?(%todo %done))
```
At some point, you might want to add a third status to represent "in progress",
which might involve changing `status` like:
```hoon
(map title=@t status=?(%todo %done %work))
```
The conventional way to keep this managable and reliably differentiate possible
state types is to have _versioned states_. The first version of the state would
typically be called `state-0`, and its head would be tagged with `%0`. Then,
when you change the state's type in an update, you'd add a new structure called
`state-1` and tag its head with `%1`. The next would then be `state-2`, and so
on.
In addition to each of those individual state versions, you'd also define a
structure called `versioned-state`, which just contains a union of all the
possible states. This way, the vase `on-load` receives can be unpacked to a
`versioned-state` type, and then a
[wuthep](/reference/hoon/rune/wut#-wuthep) (`?-`) expression can switch on
the head (`%0`, `%1`, `%2`, etc) and process each one appropriately.
For example, your state definition core might initially look like:
```hoon
|%
+$ versioned-state
$% state-0
==
+$ state-0 [%0 tasks=(map title=@t status=?(%todo %done))]
--
```
When you later update your agent with a new state version, you'd change it to:
```hoon
|%
+$ versioned-state
$% state-0
state-1
==
+$ state-0 [%0 tasks=(map title=@t status=?(%todo %done))]
+$ state-1 [%1 tasks=(map title=@t status=?(%todo %done %work))]
--
```
Another reason for versioning the state type is that there may be cases where
the state type doesn't change, but you still want to apply special transition
logic for an old state during upgrade. For example, you may need to reprocess
the data for a new feature or to fix a bug.
## Adding the state
Along with a core defining the type of the state, we also need to actually add
it to the subject of the core. The conventional way to do this is by adding the following immediately before the agent core itself:
```hoon
=| state-0
=* state -
```
The first line bunts (produces the default value) of the state type we defined
in the previous core, and adds it to the head of the subject _without a face_.
The next line uses [tistar](/reference/hoon/rune/tis#-tistar) to give it
the name of `state`. You might wonder why we don't just give it a face when we
bunt it and skip the tistar part. If we did that, we'd have to refer to `tasks`
as `tasks.state`. With tistar, we can just reference `tasks` while also being
able to reference the whole `state` when necessary.
Note that adding the state like this only happens when the agent is built - from
then on the arms of our agent will just modify it.
## State management arms
We've described the basic lifecycle process and the purpose of each state
management arm. Now let's look at each arm in detail:
### `on-init`
This arm takes no argument, and produces a `(quip card _this)`. It's called
exactly once, when the agent is first installed. Its purpose is to initialize
the agent.
`(quip a b)` is equivalent to `[(list a) b]`, see the [types
reference](/guides/core/app-school/types#quip) for details.
A `card` is a message to another agent or vane. We'll discuss `card`s in detail
later.
`this` is our agent core, which we give the `this` alias in the virtual arm
described in the previous lesson. The underscore at the beginning is the
irregular syntax for the [buccab](/reference/hoon/rune/buc#_-buccab) (`$_`)
rune. Buccab is like an inverted bunt - instead of producing the default value
of a type, instead it produces the type of some value. So `_this` means "the
type of `this`" - the type of our agent core.
Recall that in the last lesson, we said that most arms return a cell of
`[effects new-agent-core]`. That's exactly what `(quip card _this)` is.
### `on-save`
This arm takes no argument, and produces a `vase`. Its purpose is to export the
state of an agent - the state is packed into the vase it produces. The main time
it's called is when an agent is upgraded. When that happens, the agent's state
is exported with `on-save`, the new version of the agent is compiled and loaded,
and then the state is imported back into the new version of the agent via the
[`on-load`](#on-load) arm.
As well as the agent upgrade process, `on-save` is also used when an agent is
suspended or an app is uninstalled, so that the state can be restored when it's
resumed or reinstalled.
The state is packed in a vase with the
[zapgar](/reference/hoon/rune/zap#-zapgar) (`!>`) rune, like `!>(state)`.
### `on-load`
This arm takes a `vase` and produces a `(quip card _this)`. Its purpose is to
import a state previously exported with [`on-save`](#on-save). Typically
you'd have used a [versioned state](#versioned-state-type) as described above,
so this arm would test which state version the imported data has, convert data
from an old version to the new version if necessary, and load it into the
`state` wing of the subject.
The vase would be unpacked with a
[zapgal](/reference/hoon/rune/zap#-zapgal) (`!<`) rune, and then typically
you'd test its version with a [wuthep](/reference/hoon/rune/wut#-wuthep)
(`?-`) expression.
## Example
Here's a new agent to demonstrate the concepts we've discussed here:
```hoon
/+ default-agent, dbug
|%
+$ versioned-state
$% state-0
==
+$ state-0 [%0 val=@ud]
+$ card card:agent:gall
--
%- agent:dbug
=| state-0
=* state -
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
::
++ on-init
^- (quip card _this)
`this(val 42)
::
++ on-save
^- vase
!>(state)
::
++ on-load
|= old-state=vase
^- (quip card _this)
=/ old !<(versioned-state old-state)
?- -.old
%0 `this(state old)
==
::
++ on-poke on-poke:def
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--
```
Let's break it down and have a look at the new parts we've added. First, the
state core:
```hoon
|%
+$ versioned-state
$% state-0
==
+$ state-0 [%0 val=@ud]
+$ card card:agent:gall
--
```
In `state-0` we've defined the structure of our state, which is just a `@ud`.
We've tagged the head with a `%0` constant representing the version number, so
`on-load` can easily test the state version. In `versioned-state` we've created
a union and just added our `state-0` type. We've added an extra `card` arm as
well, just so we can use `card` as a type, rather than the unweildy
`card:agent:gall`.
After that core, we have the usual `agent:dbug` call, and then we have this:
```hoon
=| state-0
=* state -
```
We've just bunted the `state-0` type, which will produce `[%0 val=0]`, pinning
it to the head of the subject. Then, we've use
[tistar](/reference/hoon/rune/tis#-tistar) (`=*`) to give it a name of
`state`.
Inside our agent core, we have `on-init`:
```hoon
++ on-init
^- (quip card _this)
`this(val 42)
```
The `a(b c)` syntax is the irregular form of the
[centis](/reference/hoon/rune/cen#-centis) (`%=`) rune. You'll likely be
familiar with this from recursive functions, where you'll typically call the buc
arm of a trap like `$(a b, c d, ...)`. It's the same concept here - we're saying
`this` (our agent core) with `val` replaced by `42`. Since `on-init` is only
called when the agent is first installed, we're just initializing the state.
Next we have `on-save`:
```hoon
++ on-save
^- vase
!>(state)
```
This exports our agent's state, and is called during upgrades, suspensions, etc.
We're having it pack the `state` value in a `vase`.
Finally, we have `on-load`:
```hoon
++ on-load
|= old-state=vase
^- (quip card _this)
=/ old !<(versioned-state old-state)
?- -.old
%0 `this(state old)
==
```
It takes in the old state in a `vase`, then unpacks it to the `versioned-state`
type we defined earlier. We test its head for the version, and load it back into
the state of our agent if it matches. This test is a bit redundant at this stage
since we only have one state version, but you'll soon see the purpose of it.
You can save it as `/app/lifecycle.hoon` in the `%base` desk and `|commit %base`. Then, run `|rein %base [& %lifecycle]` to start it.
Let's try inspecting our state with `dbug`:
```
> [%0 val=42]
> :lifecycle +dbug
>=
```
`dbug` can also dig into the state with the `%state` argument, printing the value of the specified face:
```
> 42
> :lifecycle +dbug [%state %val]
>=
```
Next, we're going to modify our agent and change the structure of the state so
we can test out the upgrade process. Here's a modified version, which you can
again save in `/app/lifecycle.hoon` and `|commit %base`:
```hoon
/+ default-agent, dbug
|%
+$ versioned-state
$% state-0
state-1
==
+$ state-0 [%0 val=@ud]
+$ state-1 [%1 val=[@ud @ud]]
+$ card card:agent:gall
--
%- agent:dbug
=| state-1
=* state -
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
::
++ on-init
^- (quip card _this)
`this(val [27 32])
::
++ on-save
^- vase
!>(state)
::
++ on-load
|= old-state=vase
^- (quip card _this)
=/ old !<(versioned-state old-state)
?- -.old
%1 `this(state old)
%0 `this(state 1+[val.old val.old])
==
::
++ on-poke on-poke:def
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--
```
As soon as you `|commit` it, Gall will immediately export the existing state
with `on-save`, build the new version of the agent, then import the state back
in with `on-load`.
In the state definition core, you'll see we've added a new state version with a different structure:
```hoon
+$ versioned-state
$% state-0
state-1
==
+$ state-0 [%0 val=@ud]
+$ state-1 [%1 val=[@ud @ud]]
+$ card card:agent:gall
--
```
We've also changed the part that adds the state, so it uses the new version instead:
```hoon
=| state-1
=* state -
```
In `on-init`, we've updated it to initialize the state with a value that fits the new type we've defined:
```hoon
++ on-init
^- (quip card _this)
`this(val [27 32])
```
`on-init` won't be called in this case, but if someone were to directly install
this new version of the agent, it would be, so we still need to update it.
`on-save` has been left unchanged, but `on-load` has been updated like so:
```hoon
++ on-load
|= old-state=vase
^- (quip card _this)
=/ old !<(versioned-state old-state)
?- -.old
%1 `this(state old)
%0 `this(state 1+[val.old val.old])
==
```
We've updated the `?-` expression with a new case that handles our new state
type, and for the old state type we've added a function that converts it to the
new type - in this case by duplicating `val` and changing the head-tag from `%0`
to `%1`. This is an extremely simple state type transition function - it would
likely be more complicated for an agent with real functionality.
Note: the `a+b` syntax (as in `1+[val.old val.old]`) forms a cell of the
constant `%a` and the noun `b`. The constant may either be an integer or a `@tas`.
For example:
```
> foo+'bar'
[%foo 'bar']
> 42+'bar'
[%42 'bar']
```
Let's now use `dbug` to confirm our state has successfully been updated to the new
type:
```
> [%1 val=[42 42]]
> :lifecycle +dbug
>=
```
## Summary
- The app lifecycle rougly consists of initialization, state export, upgrade,
state import and state version transition.
- This is managed by three arms: `on-init`, `on-save` and `on-load`.
- `on-init` initializes the agent and is called when it's first installed.
- `on-save` exports the agent's state and is called during upgrade or
when an app is suspended.
- `on-load` imports an agent's state and is called during upgrade or
when an app is unsuspended. It also handles converting data from old
state versions to new state versions.
- The type of an agent's state is typically defined in a separate core.
- The state type is typically versioned, with a new type definition for each
version of the state.
- The state is initially added by bunting the state type and then naming it
`state` with the tistar (`=*`) rune, so its contents can be referenced
directly.
- A `vase` is a cell of `[type-of-the-noun the-noun]`.
- `(quip a b)` is the same as `[(list a) b]`, and is the `[effects new-agent-core]` pair returned by many arms of an agent core.
## Exercises
- Run through the [example](#example) yourself on a fake ship if you've not done
so already.
- Have a look at the [`vase` entry in the type
reference](/guides/core/app-school/types#vase).
- Have a look at the [`quip` entry in the type
reference](/guides/core/app-school/types#quip).
- Try modifying the second version of the agent in the [example](#example)
section, adding a third state version. Include functions in the wuthep
expression in `on-load` to convert old versions to your new state type.

View File

@ -0,0 +1,243 @@
+++
title = "5. Cards"
weight = 25
+++
As we previously discussed, most arms of an agent core produce a cell of
`[effects new-agent-core]`, and the type we use for this is typically `(quip card _this)`. We've covered `_this`, but we haven't yet looked at `card` effects
in detail. That's what we'll do here. In explaining `card`s we'll touch on some
concepts relating to the mechanics of pokes, subscriptions and other things
we've not yet covered. Don't worry if you don't understand how it all fits
together yet, we just want to give you a basic idea of `card`s so we can then
dig into how they work in practice.
## `card` type
The `card:agent:gall` type (henceforth just `card`) has a slightly complex
structure, so we'll walk through it step-by-step.
`lull.hoon` defines a `card` like so:
```hoon
+$ card (wind note gift)
```
A `wind` is defined in `arvo.hoon` as:
```hoon
++ wind
|$ [a b]
$% [%pass p=wire q=a]
[%slip p=a]
[%give p=b]
==
```
Gall will not accept a `%slip`, so we can ignore that. A `card`, then, is one
of:
```hoon
[%pass wire note]
[%give gift]
```
We'll consider each separately.
## `%pass`
```hoon
[%pass wire note]
```
The purpose of a `%pass` card is to send some kind of one-off request, action,
task, or what have you, to another agent or vane. A `%pass` card is a request
your agent _initiates_. This is in contrast to a [`%give`](#give) card, which is
sent in _response_ to another agent or vane.
The type of the first field in a `%pass` card is a `wire`. A `wire` is just a
list of `@ta`, with a syntax of `/foo/bar/baz`. When you `%pass` something to an
agent or vane, the response will come back on the `wire` you specify here. Your
agent can then check the `wire` and maybe do different things depending on its
content. The [`wire`](/guides/core/app-school/types#wire) type is covered in
the [types reference](/guides/core/app-school/types). We'll show how `wire`s
are practically used later on.
The type of the next field is a `note:agent:gall` (henceforth just `note`), which
`lull.hoon` defines as:
```hoon
+$ note
$% [%agent [=ship name=term] =task]
[%arvo note-arvo]
[%pyre =tang]
==
```
- An `%agent` `note` is a request to another Gall agent, either local or on a
remote ship. The `ship` and `name` fields are just the target ship and agent
name. The `task` is the request itself, we'll discuss it separately
[below](#task).
- An `%arvo` `note` is a request to a vane. We'll discuss such requests
[below](#note-arvo).
- A `%pyre` `note` is used to abort an event. It's mostly used internally by
`kiln` (a submodule of `%hood`), it's unlikely you'd use it in your own agent. The `tang` contains an
error message.
### `task`
A `task:agent:gall` (henceforth just `task`) is defined in `lull.hoon` as:
```hoon
+$ task
$% [%watch =path]
[%watch-as =mark =path]
[%leave ~]
[%poke =cage]
[%poke-as =mark =cage]
==
```
Note a few of these include a `path` field. The `path` type is exactly the same
as a `wire` - a list of `@ta` with a syntax of `/foo/bar/baz`. The reason for
the `wire`/`path` distinction is just to indicate their separate purposes. While
a `wire` is for _responses_, a `path` is for _requests_. The
[`path`](/guides/core/app-school/types#path) type is also covered in the
[types reference](/guides/core/app-school/types).
The kinds of `task`s can be divided into two categories:
#### Subscriptions
`%watch`, `%watch-as` and `%leave` all pertain to subscriptions.
- `%watch`: A request to subscribe to the specified `path`. Once subscribed,
your agent will receive any updates the other agent sends out on that `path`.
You can subscribe more than once to the same `path`, but each subscription
must have a separate `wire` specified at the beginning of the [`%pass`
card](#pass).
- `%watch-as`: This is the same as `%watch`, except Gall will convert updates to
the given `mark` before delivering them to your agent.
- `%leave`: Unsubscribe. The subscription to cancel is determined by the `wire`
at the beginning of the [`pass` card](#pass) rather than the subscription
`path`, so its argument is just `~`.
**Examples**
![subscription card examples](https://media.urbit.org/guides/core/app-school/sub-cards.svg)
#### Pokes
Pokes are requests, actions, or just some data which you send to another agent.
Unlike subscriptions, these are just one-off messages.
A `%poke` contains a `cage` of some data. A `cage` is a cell of `[mark vase]`.
The `mark` is just a `@tas` like `%foo`, and corresponds to a mark file in the
`/mar` directory. We'll cover `mark`s in greater detail later. The `vase` contains
the actual data you're sending.
The `%poke-as` task is the same as `%poke` except Gall will convert the `mark`
in the `cage` to the `mark` you specify before sending it off.
**Examples**
![poke card examples](https://media.urbit.org/guides/core/app-school/poke-cards.svg)
### `note-arvo`
A `note-arvo` is defined in `lull.hoon` like so:
```hoon
+$ note-arvo
$~ [%b %wake ~]
$% [%a task:ames]
[%b task:behn]
[%c task:clay]
[%d task:dill]
[%e task:eyre]
[%g task:gall]
[%i task:iris]
[%j task:jael]
[%$ %whiz ~]
[@tas %meta vase]
==
```
The letter at the beginning corresponds to the vane - `%b` for Behn, `%c` for
Clay, etc. After then vane letter comes the task. Each vane has an API with a
set of tasks that it will accept, and are defined in each vane's section of
`lull.hoon`. Each vane's tasks are documented on the API Reference page of its
section in the [Arvo documentation](/reference/arvo/arvo).
#### Examples
![arvo card examples](https://media.urbit.org/guides/core/app-school/arvo-cards.svg)
## `%give`
```hoon
[%give gift]
```
The purpose of a `%give` card is to respond to a request made by another agent
or vane. More specifically, it's either for acknowledging a request, or for
sending out updates to subscribers. This is in contrast to a [`%pass`](#give)
card, which is essentially unsolicited.
A `%give` card contains a `gift:agent:gall` (henceforth just `gift`), which is
defined in `lull.hoon` as:
```hoon
+$ gift
$% [%fact paths=(list path) =cage]
[%kick paths=(list path) ship=(unit ship)]
[%watch-ack p=(unit tang)]
[%poke-ack p=(unit tang)]
==
```
These can be divided into two categories:
### Acknowledgements
`%watch-ack` is sent in response to a `%watch` or `%watch-as` request, and
`%poke-ack` is sent in response to a `%poke` or `%poke-as` request. If the
`(unit tang)` is null, it's an ack - a positive acknowledgement. If the `(unit tang)` is non-null, it's a nack - a negative acknowledgement, and the `tang`
contains an error message. Gall automatically sends a nack with a stack trace if
your agent crashes while processing the request, and automatically sends an ack
if it does not. Therefore, you would not explicitly produce a `%watch-ack` or
`%poke-ack` gift.
#### Examples
![ack card examples](https://media.urbit.org/guides/core/app-school/ack-cards.svg)
### Subscriptions
`%fact` and `%kick` are both sent out to existing subscribers - entities that
have previously `%watch`ed a path on your ship.
A `%kick` gift takes a list of subscription `path`s. and a `(unit ship)`, which
is the ship to kick from those paths. If the `unit` is null, all subscribers are
kicked from the specified paths. Note that sometimes Gall can produce `%kick`
gifts without your agent explicitly sending a card, due to networking
conditions.
`%fact`s are how updates are sent out to subscribers. The `paths` field is a
list of subscription paths - all subscribers of the specified `path`s will
receive the `%fact`. The `cage` is the data itself - a cell of a `mark` and a
`vase`.
#### Examples
![gift card examples](https://media.urbit.org/guides/core/app-school/gift-cards.svg)
## Summary
Here's a diagram that summarizes the different kinds of `card`s:
[![card diagram](https://media.urbit.org/guides/core/app-school/card-diagram.svg)](https://media.urbit.org/guides/core/app-school/card-diagram.svg)
## Exercises
- Have a read of the [`wire`](/guides/core/app-school/types#wire) and
[`path`](/guides/core/app-school/types#path) entries in the type reference.

View File

@ -0,0 +1,537 @@
+++
title = "6. Pokes"
weight = 30
+++
In this lesson we'll look at sending and receiving one-off messages called
`%poke`s. We'll look at the `on-poke` agent arm which handles incoming pokes.
We'll also introduce the `on-agent` arm, and look at the one kind of response it can
take - a `%poke-ack`.
## Receiving a poke
Whenever something tries to poke your agent, Gall calls your agent's `on-poke`
arm and give it the `cage` from the poke as its sample. The `on-poke` arm will
produce a `(quip card _this)`. Here's how it would typically begin:
```hoon
++ on-poke
|= [=mark =vase]
^- (quip card _this)
...
```
The sample of the gate is usually specified as a cell of `mark` and `vase`
rather than just `cage`, simply because it's easier to work with.
Typically, you'd first test the `mark` with something like a
[wutlus](/reference/hoon/rune/wut#-wutlus) `?+` expression, passing
unexpected `mark`s to `default-agent`, which just crashes. We'll look at custom
`mark`s in a subsequent lesson, but the basic patten looks like:
```hoon
?+ mark (on-poke:def mark vase)
%noun ...
%something-else ...
...
==
```
After testing the `mark`, you'd usually extract the `vase` to the expected type,
and then apply whatever logic you need. For example:
```hoon
=/ action !<(some-type vase)
?- -.action
%foo ...
%bar ...
...
==
```
Your agent will then produce a list of `card`s to be sent off and a new,
modified state, as appropriate. We'll go into subscriptions in the next lesson,
but just to give you an idea of a typical pattern: An agent for a chat app might
take new messages as pokes, add them to the list of messages in its state, and
send out the new messages to subscribed chat participants as `gift`s.
As discussed in the previous lesson, Gall will automatically send a `%poke-ack`
`gift` back to wherever the poke came from. The `%poke-ack` will be a nack if
your agent crashed while processing the poke, and an ack otherwise. If it's a
nack, the `tang` in the `%poke-ack` will contain a stack trace of the crash.
As a result, you do not need to explicitly send a `%poke-ack`. Instead, you
would design your agent to handle only what you expect and crash in all other
cases. You can crash by passing the `cage` to `default-agent`, or just with a
`!!`. In the latter case, if you want to add an error message to the stack
trace, you can do so like:
```hoon
~| "some error message"
!!
```
This will produce a trace that looks something like:
```
/sys/vane/gall/hoon:<[1.372 9].[1.372 37]>
/app/pokeme/hoon:<[31 3].[43 5]>
/app/pokeme/hoon:<[32 3].[43 5]>
/app/pokeme/hoon:<[34 5].[42 7]>
/app/pokeme/hoon:<[35 5].[42 7]>
/app/pokeme/hoon:<[38 7].[41 27]>
/app/pokeme/hoon:<[39 9].[40 11]>
"some error message"
/app/pokeme/hoon:<[40 9].[40 11]>
```
Note that the `tang` in the nack is just for debugging purposes, you should not
try to pass actual data by encoding it in the nack `tang`.
## Sending a poke
An agent can send pokes to other agents by producing [`%poke`
`card`s](/guides/core/app-school/5-cards#pokes). Any agent arm apart from
`on-peek` and `on-save` can produce such `card`s. The arms would typically
produce the `(quip card _this)` like so:
```hoon
:_ this
:~ [%pass /some/wire %agent [~target-ship %target-agent] %poke %some-mark !>('some data')]
==
```
The [colcab](/reference/hoon/rune/col#_-colcab) (`:_`) rune makes an
inverted cell, it's just `:-` but with the head and tail swapped. We use colcab
to produce the `(quip card _this)` because the list of cards is "heavier"
here than the new agent core expression (`this`), so it makes it more
readable.
### Receiving the `%poke-ack`
The pokes will be processed by their targets [as described in the previous
section](#receiving-a-poke), and they'll `%give` back a `%poke-ack` on the
`wire` you specified (`/some/wire` in the previous example). When Gall gets the
`%poke-ack` back, it will call the `on-agent` arm of your agent, with the `wire`
it came in on and the `%poke-ack` itself in a `sign:agent:gall`. Your `on-agent`
arm would therefore begin like so:
```hoon
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
...
```
A `sign:agent:gall` (henceforth just `sign`) is defined in `lull.hoon` as:
```hoon
+$ sign
$% [%poke-ack p=(unit tang)]
[%watch-ack p=(unit tang)]
[%fact =cage]
[%kick ~]
==
```
It's basically the same as a [`gift`](/guides/core/app-school/5-cards#give),
but incoming instead of outgoing.
The simplest way to handle a `%poke-ack` by passing it to `default-agent`'s
`on-agent` arm, which will just print an error message to the terminal if it's a
nack, and otherwise do nothing. Sometimes you'll want your agent to do something
different depending on whether the poke failed or succeeded (and therefore
whether it's a nack or an ack).
As stated in the [Precepts](/guides/additional/development/precepts#specifics): "Route on wire before sign, never sign before wire.". Thus we first test the
`wire` so you can tell what the `%poke-ack` was for. You might do something
like:
```hoon
?+ wire (on-agent:def wire sign)
[%some %wire ~] ...
...
==
```
After that, you'll need to see what kind of `sign` it is:
```hoon
?+ -.sign (on-agent:def wire sign)
%poke-ack ...
...
```
Then, you can tell whether it's an ack or a nack by testing whether the `(unit tang)` in the `%poke-ack` is null:
```hoon
?~ p.sign
...(what to do if the poke succeeded)...
...(what to do if the poke failed)...
```
Finally, you can produce the `(quip card _this)`.
## Example
We're going to look at a couple of agents to demonstrate both sending and
receiving pokes. Here's the first, an agent that receives pokes:
### `pokeme.hoon`
```hoon
/+ default-agent, dbug
|%
+$ versioned-state
$% state-0
==
+$ state-0 [%0 val=@ud]
+$ card card:agent:gall
--
%- agent:dbug
=| state-0
=* state -
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
::
++ on-init
^- (quip card _this)
`this
::
++ on-save
^- vase
!>(state)
::
++ on-load
|= old-state=vase
^- (quip card _this)
=/ old !<(versioned-state old-state)
?- -.old
%0 `this(state old)
==
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?+ mark (on-poke:def mark vase)
%noun
=/ action !<(?(%inc %dec) vase)
?- action
%inc `this(val +(val))
::
%dec
?: =(0 val)
~| "Can't decrement - already zero!"
!!
`this(val (dec val))
==
==
::
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--
```
This is a very simple agent that just has `val`, a number, in its state. It will
take pokes that either increment or decrement `val`. Here's its `on-poke` arm:
```hoon
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?+ mark (on-poke:def mark vase)
%noun
=/ action !<(?(%inc %dec) vase)
?- action
%inc `this(val +(val))
%dec
?: =(0 val)
~| "Can't decrement - already zero!"
!!
`this(val (dec val))
==
==
```
It only expects pokes with a `%noun` mark, and passes all others to
`on-poke:def`, which just crashes. For `%noun` pokes, it expects to receive
either `%inc` or `%dec` in the `vase`. If it's `%inc`, it produces a new `this`
with `val` incremented. If it's `%dec`, it produces `this` with `val`
decremented, or crashes if `val` is already zero.
Let's try it out. Save the agent above as `/app/pokeme.hoon` in the `%base` desk
and `|commit %base`. Then, start it up with `|rein %base [& %pokeme]`. We can
check its initial state with `dbug`:
```
> 0
> :pokeme +dbug [%state %val]
>=
```
Next, we'll try poking it. The dojo lets you poke agents with the following syntax:
```
:agent-name &some-mark ['some' 'noun']
```
If the `mark` part is omitted, it'll just default to `%noun`. Since our agent
only takes a `%noun` mark, we can skip that. The rest will be packed in a vase
by the dojo and delivered as a poke, so we can do:
```
> :pokeme %inc
>=
```
If we now look at the state with `dbug`, we'll see the poke was successful and
it's been incremented:
```
> 1
> :pokeme +dbug [%state %val]
>=
```
Let's try decrement:
```
> :pokeme %dec
>=
> 0
> :pokeme +dbug [%state %val]
>=
```
As you can see, it's back at zero. If we try again, we'll see it fails, and the
dojo will print the `tang` in the `%poke-ack` nack:
```
> :pokeme %dec
/sys/vane/gall/hoon:<[1.372 9].[1.372 37]>
/app/pokeme/hoon:<[31 3].[43 5]>
/app/pokeme/hoon:<[32 3].[43 5]>
/app/pokeme/hoon:<[34 5].[42 7]>
/app/pokeme/hoon:<[35 5].[42 7]>
/app/pokeme/hoon:<[38 7].[41 27]>
/app/pokeme/hoon:<[39 9].[40 11]>
"Can't decrement - already zero!"
/app/pokeme/hoon:<[40 9].[40 11]>
dojo: app poke failed
```
### `pokeit.hoon`
Here's a second agent. It takes a poke of `%inc` or `%dec` like before, but
rather than updating its own state, it sends two pokes to `%pokeme`, so
`%pokeme`'s state will be incremented or decremented by two.
```hoon
/+ default-agent, dbug
|%
+$ versioned-state
$% state-0
==
+$ state-0 [%0 ~]
+$ card card:agent:gall
--
%- agent:dbug
=| state-0
=* state -
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
::
++ on-init
^- (quip card _this)
`this
::
++ on-save
^- vase
!>(state)
::
++ on-load
|= old-state=vase
^- (quip card _this)
=/ old !<(versioned-state old-state)
?- -.old
%0 `this(state old)
==
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?+ mark (on-poke:def mark vase)
%noun
=/ action !<(?(%inc %dec) vase)
?- action
%inc
:_ this
:~ [%pass /inc %agent [our.bowl %pokeme] %poke %noun !>(%inc)]
[%pass /inc %agent [our.bowl %pokeme] %poke %noun !>(%inc)]
==
::
%dec
:_ this
:~ [%pass /dec %agent [our.bowl %pokeme] %poke %noun !>(%dec)]
[%pass /dec %agent [our.bowl %pokeme] %poke %noun !>(%dec)]
==
==
==
::
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
::
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
?+ wire (on-agent:def wire sign)
[%inc ~]
?. ?=(%poke-ack -.sign)
(on-agent:def wire sign)
?~ p.sign
%- (slog '%pokeit: Increment poke succeeded!' ~)
`this
%- (slog '%pokeit: Increment poke failed!' ~)
`this
::
[%dec ~]
?. ?=(%poke-ack -.sign)
(on-agent:def wire sign)
?~ p.sign
%- (slog '%pokeit: Decrement poke succeeded!' ~)
`this
%- (slog '%pokeit: Decrement poke failed!' ~)
`this
==
::
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--
```
Here's the `on-poke` arm:
```hoon
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?+ mark (on-poke:def mark vase)
%noun
=/ action !<(?(%inc %dec) vase)
?- action
%inc
:_ this
:~ [%pass /inc %agent [our.bowl %pokeme] %poke %noun !>(%inc)]
[%pass /inc %agent [our.bowl %pokeme] %poke %noun !>(%inc)]
==
%dec
:_ this
:~ [%pass /dec %agent [our.bowl %pokeme] %poke %noun !>(%dec)]
[%pass /dec %agent [our.bowl %pokeme] %poke %noun !>(%dec)]
==
==
==
```
It's similar to `%pokeme`, except it sends two `%poke` `card`s to `%pokeme` for
each case, rather than modifying its own state. The `%inc` pokes specify a
`wire` of `/inc`, and the `%dec` pokes specify a `wire` of `/dec`, so we can
differentiate the responses. It also has the following `on-agent`:
```hoon
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
?+ wire (on-agent wire sign)
[%inc ~]
?. ?=(%poke-ack -.sign)
(on-agent wire sign)
?~ p.sign
%- (slog '%pokeit: Increment poke succeeded!' ~)
`this
%- (slog '%pokeit: Increment poke failed!' ~)
`this
::
[%dec ~]
?. ?=(%poke-ack -.sign)
(on-agent wire sign)
?~ p.sign
%- (slog '%pokeit: Decrement poke succeeded!' ~)
`this
%- (slog '%pokeit: Decrement poke failed!' ~)
`this
==
```
`on-agent` tests the `wire`, checks if it's a `%poke-ack`, and then prints to
the terminal whether it succeeded or failed.
Save this agent to `/app/pokeit.hoon` on the `%base` desk, `|commit %base`, and
start it with `|rein %base [& %pokeme] [& %pokeit]`.
Let's try it out:
```
%pokeit: Increment poke succeeded!
%pokeit: Increment poke succeeded!
> :pokeit %inc
>=
```
`%pokeit` has received positive `%poke-ack`s, which means both pokes succeeded.
It could tell they were increments because the `%poke-ack`s came back on the
`/inc` wire we specified. We can check the state of `%pokeme` to confirm:
```
> 2
> :pokeme +dbug [%state %val]
>=
```
Let's try decrementing `%pokeme` so it's an odd number, and then try a `%dec`
via `%pokeit`:
```
> :pokeme %dec
>=
%pokeit: Decrement poke succeeded!
%pokeit: Decrement poke failed!
> :pokeit %dec
>=
```
The `on-agent` arm of `%pokeit` has received one ack and one nack. The first
took `val` to zero, and the second crashed trying to decrement below zero.
## Summary
- Incoming pokes go to the `on-poke` arm of an agent.
- The `on-poke` arm takes a `cage` and produces an `(quip card _this)`.
- Gall will automatically return a `%poke-ack` to the poke's source, with a
stack trace in the `(unit tang)` if your agent crashed while processing the
poke.
- Outgoing pokes can be sent by including `%poke` `%pass` `card`s in the `quip`
produced by most agent arms.
- `%poke-ack`s in response to pokes you've sent will come in to the `on-agent`
arm in a `sign`, on the `wire` you specified in the original `%poke` `card`.
- You can poke agents from the dojo with a syntax of `:agent &mark ['some' 'noun']`.
## Exercises
- Run through the [example](#example) yourself on a fake ship if you've not done
so already.
- Have a look at the `on-agent` arm of `/lib/default-agent.hoon` to see how
`default-agent` handles incoming `sign`s.
- Try modifying the `%pokeme` agent with another action of your choice (in
addition to `%inc` and `%dec`).
- Try modifying the `%pokeit` agent to send your new type of poke to `%pokeme`,
and handle the `%poke-ack` it gets back.

View File

@ -0,0 +1,407 @@
+++
title = "7. Structures and Marks"
weight = 35
+++
Before we get into subscription mechanics, there's three things we need to touch
on that are very commonly used in Gall agents. The first is defining an agent's
types in a `/sur` structure file, the second is `mark` files, and the third is
permissions. Note the example code presented in this lesson will not yet build a
fully functioning Gall agent, we'll get to that in the next lesson.
## `/sur`
In the [previous lesson on pokes](/guides/core/app-school/6-pokes), we used a
very simple union in the `vase` for incoming pokes:
```hoon
=/ action !<(?(%inc %dec) vase)
```
A real Gall agent is likely to have a more complicated API. The most common
approach is to define a head-tagged union of all possible poke types the agent
will accept, and another for all possible updates it might send out to
subscribers. Rather than defining these types in the agent itself, you would
typically define them in a separate core saved in the `/sur` directory of the
desk. The `/sur` directory is the canonical location for userspace type
definitions.
With this approach, your agent can simply import the structures file and make use
of its types. Additionally, if someone else wants to write an agent that
interfaces with yours, they can include your structure file in their own desk
to interact with your agent's API in a type-safe way.
#### Example
Let's look at a practical example. If we were creating a simple To-Do app, our
agent might accept a few possible `action`s as pokes: Adding a new task,
deleting a task, toggling a task's "done" status, and renaming an existing task.
It might also be able to send `update`s out to subscribers when these events
occur. If our agent were named `%todo`, it might have the following structure in
`/sur/todo.hoon`:
```hoon
|%
+$ id @
+$ name @t
+$ task [=name done=?]
+$ tasks (map id task)
+$ action
$% [%add =name]
[%del =id]
[%toggle =id]
[%rename =id =name]
==
+$ update
$% [%add =id =name]
[%del =id]
[%toggle =id]
[%rename =id =name]
[%initial =tasks]
==
--
```
Our `%todo` agent could then import this structure file with a [fashep ford
rune](/reference/arvo/ford/ford#ford-runes) (`/-`) at the beginning of the agent like
so:
```hoon
/- todo
```
The agent's state could be defined like:
```hoon
|%
+$ versioned-state
$% state-0
==
+$ state-0 [%0 =tasks:todo]
+$ card card:agent:gall
--
```
Then, in its `on-poke` arm, it could handle these actions in the following
manner:
```hoon
++ on-poke
|= [=mark =vase]
^- (quip card _this)
|^
?> =(src.bowl our.bowl)
?+ mark (on-poke:def mark vase)
%todo-action
=^ cards state
(handle-poke !<(action:todo vase))
[cards this]
==
::
++ handle-poke
|= =action:todo
^- (quip card _state)
?- -.action
%add
:_ state(tasks (~(put by tasks) now.bowl [name.action %.n]))
:~ :* %give %fact ~[/updates] %todo-update
!>(`update:todo`[%add now.bowl name.action])
==
==
::
%del
:_ state(tasks (~(del by tasks) id.action))
:~ :* %give %fact ~[/updates] %todo-update
!>(`update:todo`action)
==
==
::
%toggle
:_ %= state
tasks %+ ~(jab by tasks)
id.action
|=(=task:todo task(done !done.task))
==
:~ :* %give %fact ~[/updates] %todo-update
!>(`update:todo`action)
==
==
::
%rename
:_ %= state
tasks %+ ~(jab by tasks)
id.action
|=(=task:todo task(name name.action))
==
:~ :* %give %fact ~[/updates] %todo-update
!>(`update:todo`action)
==
==
::
%allow
`state(friends (~(put in friends) who.action))
::
%kick
:_ state(friends (~(del in friends) who.action))
:~ [%give %kick ~[/updates] `who.action]
==
==
--
```
Let's break this down a bit. Firstly, our `on-poke` arm includes a
[barket](/reference/hoon/rune/bar#-barket) (`|^`) rune. Barket creates a
core with a `$` arm that's computed immediately. We extract the `vase` to the
`action:todo` type and immediately pass it to the `handle-poke` arm of the core
created with the barket. This `handle-poke` arm tests what kind of `action` it's
received by checking its head. It then updates the state, and also sends an
update to subscribers, as appropriate. Don't worry too much about the `%give`
`card` for now - we'll cover subscriptions in the next lesson.
Notice that the `handle-poke` arm produces a `(quip card _state)` rather than
`(quip card _this)`. The call to `handle-poke` is also part of the following
expression:
```hoon
=^ cards state
(handle-poke !<(action:todo vase))
[cards this]
```
The [tisket](/reference/hoon/rune/tis#-tisket) (`=^`) expression takes two
arguments: A new named noun to pin to the subject (`cards` in this case), and an
existing wing of the subject to modify (`state` in this case). Since
`handle-poke` produces `(quip card _state)`, we're saving the `card`s it
produces to `cards` and replacing the existing `state` with its new one.
Finally, we produce `[cards this]`, where `this` will now contain the modified
`state`. The `[cards this]` is a `(quip card _this)`, which our `on-poke` arm is
expected to produce.
This might seem a little convoluted, but it's a common pattern we do for two
reasons. Firstly, it's not ideal to be passing around the entire `this` agent
core - it's much tidier just passing around the `state`, until you actually want
to return it to Gall. Secondly, It's much easier to read when the poke handling
logic is separated into its own arm. This is a fairly simple example but if your
agent is more complex, handling multiple marks and containing additional logic
before it gets to the actual contents of the `vase`, structuring things this way
can be useful.
You can of course structure your `on-poke` arm differently than we've done
here - we're just demonstrating a typical pattern.
## `mark` files
So far we've just used a `%noun` mark for pokes - we haven't really delved into
what such `mark`s represent, or considered writing custom ones.
Formally, marks are file types in the Clay filesystem. They correspond to mark
files in the `/mar` directory of a desk. The `%noun` mark, for example,
corresponds to the `/mar/noun.hoon` file. Mark files define the actual hoon data
type for the file (e.g. a `*` noun for the `%noun` mark), but they also specify
some extra things:
- Methods for converting between the mark in question and other marks.
- Revision control functions like patching, diffing, merging, etc.
Aside from their use by Clay for storing files in the filesystem, they're also
used extensively for exchanging data with the outside world, and for exchanging
data between Gall agents. When data comes in from a remote ship, destined for a
particular Gall agent, it will be validated by the file in `/mar` that
corresponds to its mark before being delivered to the agent. If the remote data
has no corresponding mark file in `/mar` or it fails validation, it will crash
before it touches the agent.
A mark file is a door with exactly three arms. The door's sample is the data type the
mark will handle. For example, the sample of the `%noun` mark is just `non=*`,
since it handles any noun. The three arms are as follows:
- `grab`: Methods for converting _to_ our mark _from_ other marks.
- `grow`: Methods for converting _from_ our mark _to_ other marks.
- `grad`: Revision control functions.
In the context of Gall agents, you'll likely just use marks for sending and
receiving data, and not for actually storing files in Clay. Therefore, it's
unlikely you'll need to write custom revision control functions in the `grad`
arm. Instead, you can simply delegate `grad` functions to another mark -
typically `%noun`. If you want to learn more about writing such `grad`
functions, you can refer to the [Marks Guide](/reference/arvo/clay/marks/marks) in
the Clay vane documentation, which is much more comprehensive, but it's not
necessary for our purposes here.
#### Example
Here's a very simple mark file for the `action` structure we created in the
[previous section](#sur):
```hoon
/- todo
|_ =action:todo
++ grab
|%
++ noun action:todo
--
++ grow
|%
++ noun action
--
++ grad %noun
--
```
We've imported the `/sur/todo.hoon` structure library from the previous section,
and we've defined the sample of the door as `=action:todo`, since that's what
it will handle. Now let's consider the arms:
- `grab`: This handles conversion methods _to_ our mark. It contains a core with
arm names corresponding to other marks. In this case, it can only convert from
a `noun` mark, so that's the core's only arm. The `noun` arm simply calls the
`action` structure from our structure library. This is called "clamming" or
"molding" - when some noun comes in, it gets called like `(action:todo [some-noun])` - producing data of the `action` type if it nests, and crashing
otherwise.
- `grow`: This handles conversion methods _from_ our mark. Like `grab`, it
contains a core with arm names corresponding to other marks. Here we've also
only added an arm for a `%noun` mark. In this case, `action` data will come in
as the sample of our door, and the `noun` arm simply returns it, since it's
already a noun (as everything is in Hoon).
- `grad`: This is the revision control arm, and as you can see we've simply
delegated it to the `%noun` mark.
This mark file could be saved as `/mar/todo/action.hoon`, and then the `on-poke`
arm in the previous example could test for it instead of `%noun` like so:
```hoon
++ on-poke
|= [=mark =vase]
|^ ^- (quip card _this)
?+ mark (on-poke:def mark vase)
%todo-action
...
```
Note how `%todo-action` will be resolved to `/mar/todo/action.hoon` - the hyphen
will be interpreted as `/` if there's not already a `/mar/todo-action.hoon`.
This simple mark file isn't all that useful. Typically, you'd add `json` arms
to `grow` and `grab`, which allow your data to be converted to and from JSON,
and therefore allow your agent to communicate with a web front-end. Front-ends,
JSON, and Eyre's APIs which facilitate such communications will be covered in
the separate [Full-Stack Walkthrough](/guides/core/app-school-full-stack/1-intro),
which you might like to work through after completing this guide. For now
though, it's still useful to use marks and understand how they work.
One further note on marks - while data from remote ships must have a matching
mark file in `/mar`, it's possible to exchange data between local agents with
"fake" marks - ones that don't exist in `/mar`. Your `on-poke` arm could, for
example, use a made-up mark like `%foobar` for actions initiated locally. This
is because marks come into play only at validation boundries, none of which are
crossed when doing local agent-to-agent communications.
## Permissions
In example agents so far, we haven't bothered to check where events such as
pokes are actually coming from - our example agents would accept data from
anywhere, including random foreign ships. We'll now have a look at how to handle
such permission checks.
Back in [lesson 2](/guides/core/app-school/2-agent#bowl) we discussed the
[bowl](/reference/arvo/gall/data-types#bowl). The `bowl` includes a couple of useful
fields: `our` and `src`. The `our` field just contains the `@p` of the local
ship. The `src` field contains the `@p` of the ship from which the event
originated, and is updated for every new event.
When messages come in over Ames from other ships on the network, they're
[encrypted](/reference/arvo/ames/cryptography) with our ship's public keys and signed by the ship which sent them.
The Ames vane decrypts and verifies the messages using keys in the Jael vane,
which are obtained from the [Azimuth Ethereum contract](/reference/azimuth/azimuth-eth) and [Layer 2 data](/reference/azimuth/l2/layer2) where Urbit ID ownership
and keys are recorded. This means the originating `@p` of all messages are
cryptographically validated before being passed on to Gall, so the `@p`
specified in the `src` field of the `bowl` can be trusted to be correct, which
makes checking permissions very simple.
You're free to use whatever logic you want for this, but the most common way is
to use [wutgar](/reference/hoon/rune/wut#-wutgar) (`?>`) and
[wutgal](/reference/hoon/rune/wut#-wutgal) (`?<`) runes, which are
respectively True and False assertions that crash if they don't evaluate to the
expected truth value. To only allow messages from the local ship, you can just
do the following in the relevant agent arm:
```hoon
?> =(src.bowl our.bowl)
```
A common permission is to allow messages from the local ship, as well as all of
its moons, which can be done with the `team:title` standard library function:
```hoon
?> (team:title our.bowl src.bowl)
```
If we want to only allow messages from a particular set of ships, we could, for
example, have a `(set @p)` in our agent's state called `allowed`. Then, we can
use the `has:in` set function to check:
```hoon
?> (~(has in allowed) src.bowl)
```
If we wanted to check a ship was allowed in a particular group in the Groups
app, we could scry our ship's `%group-store` agent and compare:
```hoon
?> .^(? %gx /(scot %p our.bowl)/group-store/(scot %da now.bowl)/groups/ship/~bitbet-bolbel/urbit-community/join/(scot %p src.bowl)/noun)
```
There are many ways to handle permissions, it all depends on your particular use
case.
## Summary
Type definitions:
- An agent's type definitions live in the `/sur` directory of a desk.
- The `/sur` file is a core, typically containing a number of lusbuc (`+$`)
arms.
- `/sur` files are imported with the fashep (`/-`) Ford rune at the beginning
of a file.
- Agent API types, for pokes and updates to subscribers, are commonly defined as
head-tagged unions such as `[%foo bar=baz]`.
Mark files:
- Mark files live in the `/mar` directory of a desk.
- A mark like `%foo` corresponds to a file in `/mar` like `/mar/foo.hoon`
- Marks are file types in Clay, but are also used for passing data between
agents as well as for external data generally.
- A mark file is a door with a sample of the data type it handles and exactly three
arms: `grab`, `grow` and `grad`.
- `grab` and `grow` each contain a core with arm names corresponding to other marks.
- `grab` and `grow` define functions for converting to and from our mark,
respectively.
- `grad` defines revision control functions for Clay, but you'd typically just
delegate such functions to the `%noun` mark.
- Incoming data from remote ships will have their marks validated by the
corresponding mark file in `/mar`.
- Messages passed between agents on a local ship don't necessarily need mark
files in `/mar`.
- Mark files are most commonly used for converting an agent's native types to
JSON, in order to interact with a web front-end.
Permissions:
- The source of incoming messages from remote ships are cryptographically
validated by Ames and provided to Gall, which then populates the `src` field
of the `bowl` with the `@p`.
- Permissions are most commonly enforced with wutgar (`?>`) and wutgal (`?<`)
assertions in the relevant agent arms.
- Messages can be restricted to the local ship with `?> =(src.bowl our.bowl)` or to
its moons as well with `?> (team:title our.bowl src.bowl)`.
- There are many other ways to handle permissions, it just depends on the needs
of the particular agent.
## Exercises
- Have a quick look at the [tisket
documentation](/reference/hoon/rune/tis#-tisket).
- Try writing a mark file for the `update:todo` type, in a similar fashion to
the `action:todo` one in the [mark file section](#mark-files). You can compare
yours to the one we'll use in the next lesson.

View File

@ -0,0 +1,910 @@
+++
title = "8. Subscriptions"
weight = 40
+++
In this lesson we're going to look at subscriptions. Subscriptions are probably
the most complicated part of writing agents, so there's a fair bit to cover.
Before we get into the nitty-gritty details, we'll give a brief overview of
Gall's subscription mechanics.
The basic unit of subscriptions is the _path_. An agent will typically define a
number of subscription paths in its `on-watch` arm, and other agents (local or
remote) can subscribe to those paths. The agent will then send out updates
called `%fact`s on one or more of its paths, and _all_ subscribers of those
paths will receive them. An agent cannot send out updates to specific
subscribers, it can only target its paths. An agent can kick subscribers from
its paths, and subscribers can unsubscribe from any paths.
The subscription paths an agent defines can be simple and fixed like
`/foo/bar/baz`. They can also be dynamic, containing data of a particular atom
aura encoded in certain elements of the path. These paths can therefore be as
simple or complex as you need for your particular application.
Note it's not strictly necessary to define subscription paths explicitly. As
long as the arm doesn't crash, the subscription will succeed. In practice,
however, it's nearly always appropriate to define them explicitly and crash on
unrecognized paths.
For a deeper explanation of subscription mechanics in Arvo, you can refer to
Arvo's [Subscriptions](/reference/arvo/concepts/subscriptions) section.
## Incoming subscriptions
Subscription requests from other entities arrive in your agent's `on-watch` arm.
The `on-watch` arm takes the `path` to which they're subscribing, and produces a
`(quip card _this)`:
```hoon
++ on-watch
|= =path
^- (quip card _this)
...
```
Your agent's subscription paths would be defined in this arm, typically in a
wutlus (`?+`) expression or similar:
```hoon
?+ path (on-watch:def path)
[%updates ~]
......
......
[%blah %blah ~]
......
......
[%foo @ ~]
=/ when=@da (slav %da i.t.path)
......
......
[%bar %baz *]
?+ t.t.path (on-watch:def path)
~ .....
[%abc %def ~] .....
[%blah ~] .....
==
==
```
Subscription paths can be simple and fixed like the first two examples above:
`/updates` and `/blah/blah`. They can also contain "wildcard" elements, with an
atom of a particular aura encoded in an element of the `path`, as in the `[%foo @ ~]` example. The type pattern matcher is quite limited, so we just specify
such variable elements as `@`, and then decode them with something like `(slav %da i.t.path)` (for a `@da`), as in the example. The incoming `path` in this
example would look like `/foo/~2021.11.14..13.30.39..6b17`. For more information
on decoding atoms in strings, see the [Strings
Guide](/guides/additional/hoon/strings#decoding-from-text).
In the last case of `[%bar baz *]`, we're allowing a variable number of elements
in the path. First we check it's `/foo/bar/...something...`, and then we check
what the "something" is in another wutlus expression and handle it
appropriately. In this case, it could be `/foo/bar`, `/foo/bar/abc/def`, or
`/foo/bar/blah`. You could of course also have "wildcard" elements here too, so
there's not really a limit to the complexity of your subscription paths, or the
data that might be encoded therein.
Permissions can be checked as described in the previous lesson, comparing the
source `@p` of the request in `src.bowl` to `our.bowl` or any other logic you
find appropriate.
If a permission check fails, the path is not valid, or any other reason you want
to reject the subscription request, your agent can simply crash. The behavior
here is the same as with `on-poke` - Gall will send a `%watch-ack` card in
response, which is either an ack (positive acknowledgement) or a nack (negative
acknowledgement). The `(unit tang)` in the `%watch-ack` will be null if
processing succeeded, and non-null if it crashed, with a stack trace in the
`tang`. Like with `poke-ack`s, you don't need to explicitly send a
`%watch-ack` - Gall will do it automatically.
As well as sending a `%watch-ack`, Gall will also record the subscription in the
`sup` field of the `bowl`, if it succeeded. Then, when you send updates out to
subscribers of the `path` in question, the new subscriber will begin receiving
them as well.
Updates to subscribers would usually be sent from other arms, but there's one
special case for `on-watch` which is very useful. Normally updates can only be
sent to all subscribers of a particular path - you can't target a specific
subscriber. There's one exception to this: In `on-watch`, when there's a new
subscription, you can send a `%fact` back with an empty `(list path)`, and it'll
only go to the new subscriber. This is most useful when you want to give the
subscriber some initial state, which you otherwise couldn't do without sending
it to everyone. It might look something like this:
```hoon
:_ this
:~ [%give %fact ~ %todo-update !>(`update:todo`initial+tasks)]
==
```
## Sending updates to subscribers
Once your agent has subscribers, it's easy to send them out updates. All you
need to do is produce `card`s with `%fact`s in them:
```hoon
:_ this
:~ [%give %fact ~[/some/path /another/path] %some-mark !>('some data')]
[%give %fact ~[/some/path] %some-mark !>('more data')]
....
==
```
The `(list path)` in the `%fact` specifies which subscription `path`s the
`%fact` should be sent on. All subscribers of all `path`s specified will receive
the `%fact`. Any agent arm which produces a `(quip card _this)` can send
`%fact`s to subscribers. Most often they will be produced in the `on-poke` arm,
since new data will often be added in `poke`s.
## Kicking subscribers
To kick a subscriber, you just send a `%kick` `card`:
```hoon
[%give %kick ~[/some/path] `~sampel-palnet]
```
The `(list path)` specifies which subscription `path`s the ship should be kicked
from, and the `(unit ship)` specifies which ship to kick. The `(unit ship)` can also be null, like so:
```hoon
[%give %kick ~[/some/path] ~]
```
In this case, all subscribers to the specified `path`s will be kicked.
Note that `%kick`s are not exclusively sent by the agent itself - Gall itself
can also kick subscribers under certain network conditions. Because of this,
`%kick`s are not assumed to be intentional, and the usual behavior is for a
kicked agent to try and resubscribe. Therefore, if you want to disallow a
particular subscriber, your agent's `on-watch` arm should reject further
subscription requests from them - your agent should not just `%kick` them and
call it a day.
## Outgoing subscriptions
Now that we've covered incoming subscriptions, we'll look at the other side of
it: Subscribing to other agents. This is done by `%pass`ing the target agent a
`%watch` task in a `card`:
```hoon
[%pass /some/wire %agent [~some-ship %some-agent] %watch /some/path]
```
If your agent's subscription request is successful, updates will come in to your
agent's `on-agent` arm on the `wire` specified (`/some/wire` in this example).
The `wire` can be anything you like - its purpose is for your agent to figure
out which subscription the updates came from. The `[ship term]` pair specifies the
ship and agent you're trying to subscribe to, and the final `path` (`/some/path`
in this example) is the path you want to subscribe to - a `path` the target
agent has defined in its `on-watch` arm.
Gall will deliver the `card` to the target agent and call that agent's
`on-watch` arm, which will process the request [as described
above](#incoming-subscription-requests), accept or reject it, and send back
either a positive or negative `%watch-ack`. The `%watch-ack` will come back in
to your agent's `on-agent` arm in a `sign`, along with the `wire` you specified
(`/some/wire` in this example). Recall in the lesson on pokes, the `on-agent`
arm starts with:
```hoon
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
.....
```
The `sign` will be of the following format:
```hoon
[%watch-ack p=(unit tang)]
```
How you want to handle the `%watch-ack` really depends on the particular agent.
In the simplest case, you can just pass it to the `on-agent` arm of
`default-agent`, which will just accept it and do nothing apart from printing
the error in the `%watch-ack` `tang` if it's a nack. You shouldn't have your
agent crash on a `%watch-ack` - even if it's a nack your agent should process it
successfully. If you wanted to apply some additional logic on receipt of the
`%watch-ack`, you'd typically first test the `wire`, then test whether it's a
`%watch-ack`, then test whether it's an ack or a nack and do whatever's
appropriate:
```hoon
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
?+ wire (on-agent:def wire sign)
[%expected %wire ~]
?+ -.sign (on-agent:def wire sign)
%watch-ack
?~ p.sign
...(do something if ack)...
...(do something if nack)...
......
```
The `on-agent` arm produces a `(quip card _this)`, so you can produce new
`card`s and update your agent's state, as appropriate.
One further thing to note with subscriptions is that you can subscribe multiple
times to the same `path` on the same ship and agent, as long as the `wire` is
unique. If the ship, agent, `path` and `wire` are all the same as an existing
subscription, Gall will not allow the request to be sent, and instead fail with
an error message fed into the `on-fail` arm of your agent.
## Receiving updates
Assuming the `%watch` succeeded, your agent will now begin receiving any
`%fact`s the other agent publishes on the `path` to which you've subscribed. These
`%fact`s will also come in to your agent's `on-agent` arm in a `sign`, just like
the initial `%watch-ack`. The `%fact` `sign` will have the following format:
```hoon
[%fact =cage]
```
You would typically handle such `%fact`s in the following manner: Test the
`wire`, test whether the `sign` is a `%fact`, test the `mark` in the `cage`,
extract the data from the `vase` in the `cage`, and apply your logic. Again, routing on `wire` before `sign` is one of the [Precepts](/guides/additional/development/precepts#specifics). For example:
```hoon
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
?+ wire (on-agent:def wire sign)
[%expected %wire ~]
?+ -.sign (on-agent:def wire sign)
%fact
?+ p.cage.sign (on-agent:def wire sign)
%expected-mark
=/ foo !<(expected-type q.cage.sign)
.....
......
```
Note that Gall will not allow `sign`s to come into `on-agent` unsolicited, so
you don't necessarily need to include permission logic in this arm.
The `on-agent` arm produces a `(quip card _this)`, so you can produce new
`card`s and update your agent's state, as appropriate.
## Getting kicked
For whatever reason, the agent you're `%watch`ing might want to kick your agent
from a `path` to which it's suscribed, ending your subscription and ceasing to
send your agent `%fact`s. To do this, it will send your agent a `%kick` card [as
described above](#kicking-subscribers). The `%kick` will come in to your agent's
`on-agent` arm in a `sign`, like `%watch-ack`s and `%fact`s do. The `%kick`
`sign` will have the following format:
```hoon
[%kick ~]
```
Since the `%kick` itself contains no information, you'll need to consider the
`wire` it comes in on to know what it pertains to. As explained previously,
`%kick`s aren't always intentional - sometimes Gall will kick subscribers due to
network issues. Your `on-agent` arm therefore has no way to know whether the
other agent actually intended to kick it. This means _your agent should almost
always try to resubscribe if it gets kicked_. Then, if the resubscribe `%watch`
request is rejected with a negative `%watch-ack`, you can conclude that it was
intentional and give up. The logic would look something like this:
```hoon
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
?+ wire (on-agent:def wire sign)
[%some %wire ~]
?+ -.sign (on-agent:def wire sign)
%kick
:_ this
:~ [%pass /some/wire %agent [src.bowl dap.bowl] %watch /some/path]
==
.......
```
## Leaving a subscription
Eventually you may wish to unsubscribe from a `path` in another agent and stop
receiving updates. This is done by `%pass`ing a `%leave` task to the agent in
question:
```hoon
[%pass /some/wire %agent [~some-ship %some-agent] %leave ~]
```
The subcription to be ended is determined by the combination of the `wire`, ship
and agent, so the `%leave` task itself always just has `~` at the end.
## Example
Here we're going to give a pretty well fleshed out example. It will demonstrate
both inbound and outbound subscriptions, most of the concepts we've discussed
here, as well as some from the previous lesson - `/sur` files, `mark` files, and
permission checks.
In previous lessons we've only dealt with things on a local ship - this example
will demonstrate messages being sent over the network.
The example will be composed of two separate agents - a publisher called
`/app/todo.hoon` and a subscriber called `/app/todo-watcher.hoon`, which will
live on separate ships. It will be a very rudimentary To-Do app - to-do tasks
will be poked into the publisher and sent out to the subscriber as `%fact`s,
which will just print them to the dojo. It will have its types defined in
`/sur/todo.hoon`, and it will have a couple of `mark` files for pokes and
updates: `/mar/todo/action.hoon` and `/mar/todo/update.hoon`.
Before we get into trying it out, we'll first walk through the `/sur` file, mark
files, and each agent.
### Types and marks
#### `/sur/todo.hoon`
```hoon
|%
+$ id @
+$ name @t
+$ task [=name done=?]
+$ tasks (map id task)
+$ who @p
+$ friends (set who)
+$ action
$% [%add =name]
[%del =id]
[%toggle =id]
[%rename =id =name]
[%allow =who]
[%kick =who]
==
+$ update
$% [%add =id =name]
[%del =id]
[%toggle =id]
[%rename =id =name]
[%initial =tasks]
==
--
```
This file defines most of the types for the agents. The list of to-do tasks will
be stored in the state of the publisher agent as the `tasks` type, a `(map id task)`, where a `task` is a `[=name done=?]`. The set of ships allowed to
subscribe will be stored in `friends`, a `(set @p)`, also in the publisher's
state. After that, there are the head-tagged unions of accepted poke `action`s
and `update`s for subscribers.
#### `/mar/todo/action.hoon`
```hoon
/- todo
|_ =action:todo
++ grab
|%
++ noun action:todo
--
++ grow
|%
++ noun action
--
++ grad %noun
--
```
This is a very simple mark file for the `action` type.
#### `/mar/todo/update.hoon`
```hoon
/- todo
|_ =update:todo
++ grab
|%
++ noun update:todo
--
++ grow
|%
++ noun update
--
++ grad %noun
--
```
This is a very simple mark file for the `update` type.
### Publisher
#### `/app/todo.hoon`
```hoon
/- todo
/+ default-agent, dbug
|%
+$ versioned-state
$% state-0
==
+$ state-0 [%0 =friends:todo =tasks:todo]
+$ card card:agent:gall
--
%- agent:dbug
=| state-0
=* state -
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
::
++ on-init
^- (quip card _this)
`this
::
++ on-save
^- vase
!>(state)
::
++ on-load
|= old-state=vase
^- (quip card _this)
=/ old !<(versioned-state old-state)
?- -.old
%0 `this(state old)
==
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
|^
?> =(src.bowl our.bowl)
?+ mark (on-poke:def mark vase)
%todo-action
=^ cards state
(handle-poke !<(action:todo vase))
[cards this]
==
++ handle-poke
|= =action:todo
^- (quip card _state)
?- -.action
%add
?: (~(has by tasks) now.bowl)
$(now.bowl (add now.bowl ~s0..0001))
:_ state(tasks (~(put by tasks) now.bowl [name.action %.n]))
:~ :* %give %fact ~[/updates] %todo-update
!>(`update:todo`[%add now.bowl name.action])
==
==
::
%del
:_ state(tasks (~(del by tasks) id.action))
:~ :* %give %fact ~[/updates] %todo-update
!>(`update:todo`action)
==
==
::
%toggle
:- :~ :* %give %fact ~[/updates] %todo-update
!>(`update:todo`action)
== ==
%= state
tasks %+ ~(jab by tasks)
id.action
|=(=task:todo task(done !done.task))
==
::
%rename
:- :~ :* %give %fact ~[/updates] %todo-update
!>(`update:todo`action)
== ==
%= state
tasks %+ ~(jab by tasks)
id.action
|=(=task:todo task(name name.action))
==
%allow
`state(friends (~(put in friends) who.action))
::
%kick
:_ state(friends (~(del in friends) who.action))
:~ [%give %kick ~[/updates] `who.action]
==
==
--
::
++ on-watch
|= =path
^- (quip card _this)
?+ path (on-watch:def path)
[%updates ~]
?> (~(has in friends) src.bowl)
:_ this
:~ [%give %fact ~ %todo-update !>(`update:todo`initial+tasks)]
==
==
::
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--
```
This is the publisher agent, `todo.hoon`. The bulk of its logic is in its
`on-poke` arm, where it handles the various possible actions like `%add`ing a
task, `%toggle`ing its "done" state, `%rename`ing a task, and so on. It also has
a couple of `action`s for `%allow`ing and `%kick`ing subscribers.
Most of these cases both update the state of the agent, as well as producing
`%fact` cards to send out to subscribers with the new data.
You'll notice it only allows these pokes from the local ship, and enforces this
in `on-poke` with:
```hoon
?> =(src.bowl our.bowl)
```
Additionally, you might notice the `%add` case in `handle-poke` begins with the
following:
```hoon
?: (~(has by tasks) now.bowl)
$(now.bowl (add now.bowl ~s0..0001))
```
Back in lesson two, we mentioned that the bowl is only repopulated when there's
a new Arvo event, so simultaneous messages from a local agent or web client
would be processed with the same bowl. Since we're using `now.bowl` for the task
ID, this means multiple `%add` actions could collide. To handle this case, we
check if there's already an entry in the `tasks` map with the current date-time,
and if there is, we increase the time by a fraction of a second and try again.
Let's now look at `on-watch`:
```hoon
++ on-watch
|= =path
^- (quip card _this)
?+ path (on-watch:def path)
[%updates ~]
?> (~(has in friends) src.bowl)
:_ this
:~ [%give %fact ~ %todo-update !>(`update:todo`initial+tasks)]
==
==
```
When `on-watch` gets a subscription request, it checks whether the requesting
ship is in the `friends` set, and crashes if it is not. If they're in `friends`,
it produces a `%fact` card with a null `(list path)`, which means it goes only
to the new subscriber. This `%fact` contains the entire `tasks` map as it
currently exists, getting the new subscriber up to date.
### Subscriber
#### `/app/todo-watcher.hoon`
```hoon
/- todo
/+ default-agent, dbug
|%
+$ versioned-state
$% state-0
==
+$ state-0 [%0 ~]
+$ card card:agent:gall
--
%- agent:dbug
=| state-0
=* state -
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
::
++ on-init
^- (quip card _this)
`this
::
++ on-save
^- vase
!>(state)
::
++ on-load
|= old-state=vase
^- (quip card _this)
=/ old !<(versioned-state old-state)
?- -.old
%0 `this(state old)
==
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?> =(src.bowl our.bowl)
?+ mark (on-poke:def mark vase)
%noun
=/ action !<(?([%sub @p] [%unsub @p]) vase)
?- -.action
%sub
:_ this
:~ [%pass /todos %agent [+.action %todo] %watch /updates]
==
%unsub
:_ this
:~ [%pass /todos %agent [+.action %todo] %leave ~]
==
==
==
::
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
::
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
?+ wire (on-agent:def wire sign)
[%todos ~]
?+ -.sign (on-agent:def wire sign)
%watch-ack
?~ p.sign
((slog '%todo-watcher: Subscribe succeeded!' ~) `this)
((slog '%todo-watcher: Subscribe failed!' ~) `this)
::
%kick
%- (slog '%todo-watcher: Got kick, resubscribing...' ~)
:_ this
:~ [%pass /todos %agent [src.bowl %todo] %watch /updates]
==
::
%fact
?+ p.cage.sign (on-agent:def wire sign)
%todo-update
~& !<(update:todo q.cage.sign)
`this
==
==
==
::
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--
```
This is the subscriber agent. Since it's just for demonstrative purposes, it has
no state and just prints the updates it receives. In practice it would keep the
`tasks` map it receives in its own state, and then update it as it receives new
`%fact`s.
The `on-poke` arm is fairly simple - it accepts two pokes, to either `[%sub ~some-ship]` or `[%unsub ~some-ship]`.
The `on-agent` arm will print whether a subscription request succeeded or
failed, as well as printing a message when it gets kicked. When it receives a
`%fact` from the publisher agent, it will just print it to the terminal with a
`~&` expression.
### Trying it out
We're going to try this between two different ships. The first ship will be the
usual fakezod. We'll add both `mark` files, the `/sur` file, and the `todo.hoon`
agent to the `%base` desk of our fakezod, putting them in the following
directories:
```
base
├── app
│ └── todo.hoon
├── mar
│ └── todo
│ ├── action.hoon
│ └── update.hoon
└── sur
└── todo.hoon
```
In `~zod`'s dojo, we can `|commit %base`, and then start the `%todo` agent:
```
|rein %base [& %todo]
```
Now we need to spin up another fake ship. We'll use `~nut` in this example:
```
urbit -F nut
```
Once it's booted, we can `|mount %base` and then add just the `update.hoon` mark
file, the `/sur` file, and the `todo-watcher.hoon` agent like so:
```
base
├── app
│ └── todo-watcher.hoon
├── mar
│ └── todo
│ └── update.hoon
└── sur
└── todo.hoon
```
On `~nut` we can then `|commit %base`, and start the `%todo-watcher` agent:
```
|rein %base [& %todo-watcher]
```
Now, on `~nut`, let's try subscribing:
```
> :todo-watcher [%sub ~zod]
>=
%todo-watcher: Subscribe failed!
```
Our `%todo-watcher` agent tried, but received a negative `%watch-ack` from
`%todo`, because we haven't yet added `~nut` to the `friends` set of allowed
ships. Let's now remedy that on `~zod`:
```
> :todo &todo-action [%allow ~nut]
>=
```
Let's also add a couple of to-do tasks, on `~zod`:
```
> :todo &todo-action [%add 'foo']
>=
> :todo &todo-action [%add 'bar']
>=
```
If we now check its state with `+dbug`, we'll see they're in the `tasks` map,
and `~nut` will also now be in the `friends` set:
```
> [ %0
friends={~nut}
tasks
{ [ p=170.141.184.505.349.079.206.522.766.950.035.095.552
q=[name='foo' done=%.n]
]
[ p=170.141.184.505.349.079.278.538.984.166.386.565.120
q=[name='bar' done=%.n]
]
}
]
> :todo +dbug
>=
```
Let's now try subscribing again on `~nut`:
```
> :todo-watcher [%sub ~zod]
>=
%todo-watcher: Subscribe succeeded!
[ %initial
tasks
{ [ p=170.141.184.505.349.079.206.522.766.950.035.095.552
q=[name='foo' done=%.n]
]
[ p=170.141.184.505.349.079.278.538.984.166.386.565.120
q=[name='bar' done=%.n]
]
}
]
```
As you can see, this time it's worked, and we've immediately received the
initial `tasks` map.
Now, let's try adding another task on `~zod`:
```
> :todo &todo-action [%add 'baz']
>=
```
On `~nut`, we'll see it has received the `%fact` with the new task in it:
```
[ %add
id=170.141.184.505.349.082.779.030.192.959.445.270.528
name='baz'
]
```
Let's try toggle its done state on `~zod`:
```
> :todo &todo-action [%toggle 170.141.184.505.349.082.779.030.192.959.445.270.528]
>=
```
`~nut` will again get the `%fact`:
```
[ %toggle
id=170.141.184.505.349.082.779.030.192.959.445.270.528
]
```
Recall that incoming subscriptions are stored in `sup.bowl`, and outgoing
subscriptions are stored in `wex.bowl`. Let's have a look at the incoming
subscription on `~zod`:
```
> [ path=/updates
from=~nut
duct=~[/gall/sys/req/~nut/todo /ames/bone/~nut/1 //ames]
]
> :todo +dbug [%incoming %ship ~nut]
>=
```
On `~nut`, let's look at the outgoing subscription:
```
> [wire=/todos agnt=[~zod %todo] path=/updates ackd=%.y]
> :todo-watcher +dbug [%outgoing %ship ~zod]
>=
```
Now on `~zod`, let's try kicking `~nut` and removing it from our `friends` set:
```
> :todo &todo-action [%kick ~nut]
>=
```
On `~nut`, we'll see it got the `%kick`, tried resubscribing automatically, but
was rejected because `~nut` is no longer in `friends`:
```
%todo-watcher: Got kick, resubscribing...
%todo-watcher: Subscribe failed!
```
## Summary
- Incoming subscription requests arrive in an agent's `on-watch` arm.
- An agent will define various subscription `path`s in its `on-watch` arm, which
others can subscribe to.
- Gall will automatically produce a negative `%watch-ack` if `on-watch` crashed,
and a positive one if it was successful.
- Incoming subscribers are recorded in the `sup` field of the `bowl`.
- `on-watch` can produce a `%fact` with a null `(list path)` which will go only
to the new subscriber.
- Updates are sent to subscribers in `%fact` cards, and contain a `cage` with a
`mark` and some data in a `vase`.
- `%fact`s are sent to all subscribers of the paths specified in the `(list path)`.
- A subscriber can be kicked from subscription paths with a `%kick` card
specifying the ship in the `(unit ship)`. All subscribers of the specified
paths will be kicked if the `(unit ship)` is null.
- An outgoing subscription can be initiated with a `%watch` card.
- The `%watch-ack` will come back in to the subscriber's `on-agent` arm as a
`sign`, and may be positive or negative, depending on whether the `(unit tang)` is null.
- `%kick`s will also arrive in the subscriber's `on-agent` arm as a `sign`.
Since kicks may not be intentional, the subscriber should attempt to
resubscribe and only give up if the subsequent `%watch-ack` is negative.
- `%fact`s will also arrive in the subscriber's `on-agent` arm.
- All such `sign`s that arrive in `on-agent` will also have a `wire`.
- The `wire` for subscription updates to arrive on is specified in the initial
`%watch` card.
- A subscriber can unsubscribe by passing a `%leave` card on the original
`wire`.
## Exercises
- Have a look at the [Strings Guide](/guides/additional/hoon/strings) if you're not
already familiar with decoding/encoding atoms in strings.
- Try running through the [example](#example) yourself, if you've not done so
already.
- Try modifying `%todo-watcher` to recording the data it receives in its state,
rather than simply printing it to the terminal.
- If you'd like, try going back to [lesson
6](/guides/core/app-school/6-pokes) (on pokes) and modifying the agents
with an appropriate permission system, and also try running them on separate
ships.

View File

@ -0,0 +1,283 @@
+++
title = "9. Vanes"
weight = 45
+++
In this lesson we're going to look at interacting with vanes (kernel modules).
The API for each vane consists of `task`s it can take, and `gift`s it can
return. The `task`s and `gift`s for each vane are defined in its section of
`lull.hoon`. Here's the `task:iris`s and `gift:iris`s for Iris, the HTTP client
vane, as an example:
```hoon
|%
+$ gift
$% [%request id=@ud request=request:http]
[%cancel-request id=@ud]
[%http-response =client-response]
==
+$ task
$~ [%vega ~]
$% $>(%born vane-task)
$>(%trim vane-task)
$>(%vega vane-task)
[%request =request:http =outbound-config]
[%cancel-request ~]
[%receive id=@ud =http-event:http]
==
```
The API of each vane is documented in its respective section of the [Arvo
documentation](/reference/arvo/overview). Each vane has a detailed API reference and
examples of their usage. There are far too many `task`s and `gift`s across the
vanes to cover here, so in the [`Example`](#example) section of this document,
we'll just look at a single, simple example with a Behn timer. The basic pattern
in the example is broadly applicable to the other vanes as well.
## Sending a vane task
A `task` can be sent to a vane by `%pass`ing it an `%arvo` card. We touched on
these in the [Cards](/guides/core/app-school/5-cards) lesson, but we'll
briefly recap it here. The type of the card is as follows:
```hoon
[%pass path %arvo note-arvo]
```
The `path` will just be the `wire` you want the response to arrive on. The
`note-arvo` is the following union:
```hoon
+$ note-arvo
$~ [%b %wake ~]
$% [%a task:ames]
[%b task:behn]
[%c task:clay]
[%d task:dill]
[%e task:eyre]
[%g task:gall]
[%i task:iris]
[%j task:jael]
[%$ %whiz ~]
[@tas %meta vase]
==
```
The letter tags just specify which vane it goes to, and then follows the `task`
itself. Here are a couple of examples. The first sends a `%wait` `task:behn` to
Behn, setting a timer to go off one minute in the future. The second sends a
`%warp` `task:clay` to Clay, asking whether `sys.kelvin` exists on the `%base`
desk.
```hoon
[%pass /some/wire %arvo %b %wait (add ~m1 now.bowl)]
[%pass /some/wire %arvo %c %warp our.bowl %base ~ %sing %u da+now.bowl /sys/kelvin]
```
## Receiving a vane gift
Once a `task` has been sent to a vane, any `gift`s the vane sends back in
response will arrive in the `on-arvo` arm of your agent. The `on-arvo` arm
exclusively handles such vane `gift`s. The `gift`s will arrive in a `sign-arvo`,
along with the `wire` specified in the original request. The `on-arvo` arm
produces a `(quip card _this)` like usual, so it would look like:
```hoon
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
.....
```
A `sign-arvo` is the following structure, defined in `lull.hoon`:
```hoon
+$ sign-arvo
$% [%ames gift:ames]
$: %behn
$% gift:behn
$>(%wris gift:clay)
$>(%writ gift:clay)
$>(%mere gift:clay)
$>(%unto gift:gall)
==
==
[%clay gift:clay]
[%dill gift:dill]
[%eyre gift:eyre]
[%gall gift:gall]
[%iris gift:iris]
[%jael gift:jael]
==
```
The head of the `sign-arvo` will be the name of the vane like `%behn`, `%clay`,
etc. The tail will be the `gift` itself. Here are a couple of `sign-arvo`
examples, and the responses to the example `task`s in the previous section:
```hoon
[%behn %wake ~]
```
```
[ %clay
[ %writ
p
[ ~
[ p=[p=%u q=[%da p=~2021.11.17..13.55.00..c195] r=%base]
q=/sys/kelvin
r=[p=%flag q=[#t/?(%.y %.n) q=0]]
]
]
]
]
```
The typical pattern is to first test the `wire` with something like a wutlus
(`?+`) expression, and then test the `sign-arvo`. Since most `gift`s are
head-tagged, you can test both the vane and the gift at the same time like:
```hoon
?+ sign-arvo (on-arvo:def wire sign-arvo)
[%behn %wake *]
.....
....
```
## Example
Here's a very simple example that takes a poke of a `@dr` (a relative date-time
value) and sends Behn a `%wait` `task:behn`, setting a timer to go off `@dr` in
the future. When the timer goes off, `on-arvo` will take the `%wake` `gift:behn`
and print "Ding!" to the terminal.
#### `ding.hoon`
```hoon
/+ default-agent, dbug
|%
+$ card card:agent:gall
--
%- agent:dbug
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
++ on-init on-init:def
++ on-save on-save:def
++ on-load on-load:def
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?+ mark (on-poke:def mark vase)
%noun
:_ this
:~ [%pass /timers %arvo %b %wait (add now.bowl !<(@dr vase))]
==
==
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-agent on-agent:def
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?+ wire (on-arvo:def wire sign-arvo)
[%timers ~]
?+ sign-arvo (on-arvo:def wire sign-arvo)
[%behn %wake *]
?~ error.sign-arvo
((slog 'Ding!' ~) `this)
(on-arvo:def wire sign-arvo)
==
==
++ on-fail on-fail:def
--
```
Let's examine the `on-poke` arm:
```hoon
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?+ mark (on-poke:def mark vase)
%noun
:_ this
:~ [%pass /timers %arvo %b %wait (add now.bowl !<(@dr vase))]
==
==
```
A Behn `%wait` task has the format `[%wait @da]` - the `@da` (an absolute
date-time value) is the time the timer should go off. The `vase` of the poke
takes a `@dr`, so we extract it directly into an `add` expression, producing a
date-time `@dr` from now. Behn will receive the `%wait` task and set the timer
in Unix. When it fires, Behn will produce a `%wake` `gift:behn` and deliver it
to `on-arvo`, on the `wire` we specified (`/timers`). Here's the `on-arvo` arm:
```hoon
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?+ wire (on-arvo:def wire sign-arvo)
[%timers ~]
?+ sign-arvo (on-arvo:def wire sign-arvo)
[%behn %wake *]
?~ error.sign-arvo
((slog 'Ding!' ~) `this)
(on-arvo:def wire sign-arvo)
==
==
```
We remark that, just like in the case of agent-agent communication, `gift`s from Arvo are also routed `wire` before `sign-arvo`.
First we check the `wire` is `/timers`, and then we check the `sign-arvo` begins
with `[%behn %wake ....]`. Behn's `%wake` gift has the following format:
```hoon
[%wake error=(unit tang)]
```
The `error` is null if the timer fired successfully, and contains an error in
the `tang` if it did not. We therefore test whether `error.sign-arvo` is `~`,
and if it is, we print `Ding!` to the terminal. If the `wire`, `sign-arvo` or
`error` are something unexpected, we pass it to `%default-agent`, which
will just crash and print an error message.
Let's try it out. Save the agent above as `/app/ding.hoon` on the `%base` desk
and `|commit %base`. Then, start the agent with `|rein %base [& %ding]`.
Next, in the dojo let's try poking our agent, setting a timer for five seconds
from now:
```
> :ding ~s5
>=
```
After approximately five seconds, we see the timer fired successfully:
```
> Ding!
```
## Summary
- Each vane has an API composed of `task`s it takes and `gift`s it produces.
- Each vane's `task`s and `gift`s are defined in `lull.hoon`
- Each vane's section of the [Arvo documentation](/reference/arvo/overview) includes
an API reference that explains its `task`s and `gift`s, as well as an Examples
section demonstrating their usage.
- Vane `task`s can be sent to vanes by `%pass`ing them an `%arvo` `card`.
- Vane `gift`s come back to the `on-arvo` arm of the agent core in a
`sign-arvo`.
## Exercises
- Run through the [Example](#example) yourself if you've not done so already.
- Have a look at some vane sections of `lull.hoon` to familiarize yourself with
its structure.
- Have a quick look at the API reference sections of a couple of vanes in the
[Arvo documentation](/reference/arvo/overview).

View File

@ -0,0 +1,44 @@
+++
title = "App School I"
weight = 6
sort_by = "weight"
insert_anchor_links = "right"
+++
## Table of Contents
- [Introduction](guides/core/app-school/intro)
#### Lessons
1. [Arvo](guides/core/app-school/1-arvo) - This lesson provides an
overview of the Arvo operating system, and some other useful background
information.
2. [The Agent Core](guides/core/app-school/2-agent) - This lesson goes over
the basic structure of a Gall agent.
3. [Imports and Aliases](guides/core/app-school/3-imports-and-aliases) -
This lesson covers some useful libraries, concepts and boilerplate commonly
used when writing Gall agents.
4. [Lifecycle](guides/core/app-school/4-lifecycle) - This lesson introduces
the state management arms of an agent.
5. [Cards](guides/core/app-school/5-cards) - This lesson covers `card`s -
the structure used to pass messages to other agents and vanes.
6. [Pokes](guides/core/app-school/6-pokes) - This lesson covers sending and
receiving one-off messages called "pokes" between agents.
7. [Structures and Marks](guides/core/app-school/7-sur-and-marks) - This
lesson talks about importing type defintions, and writing `mark` files.
8. [Subscriptions](guides/core/app-school/8-subscriptions) - This lesson
goes through the mechanics of subscriptions - both inbound and outbound.
9. [Vanes](guides/core/app-school/9-vanes) - This lesson explains how to
interact with vanes (kernel modules) from an agent.
10. [Scries](guides/core/app-school/10-scry) - This lesson gives an overview
of scrying Gall agents, and how scry endpoints are defined in agents.
11. [Failure](guides/core/app-school/11-fail) - This lesson covers how Gall
handles certain errors and crashes, as well as the concept of a helper core.
12. [Next Steps](guides/core/app-school/12-next-steps) - App School I is
now complete - here are some things you can look at next.
#### Appendix
- [Types](guides/core/app-school/types) - A reference for a few of
the types used in App School.

View File

@ -0,0 +1,82 @@
+++
title = "Introduction"
weight = 1
+++
This guide will walk through everything you need to know to write your own Gall
agents.
App School I is suitable for anyone with an intermediate knowledge of Hoon. If
you've worked through [Hoon School](/guides/core/hoon-school/) or something
equivalent, you should be fine.
## What are Gall agents?
Gall is one of the eight vanes (kernel modules) of Arvo, Urbit's operating
system. Gall's purpose is to manage userspace applications called _agents_.
An agent is a piece of software that is primarily focused on maintaining and
distributing a piece of state with a defined structure. It exposes an interface
that lets programs read, subscribe to, and manipulate the state. Every event
happens in an atomic transaction, so the state is never inconsistent. Since the
state is permanent, when the agent is upgraded with a change to the structure of
the state, the developer provides a migration function from the old state type
to the new state type.
It's not too far off to think of an agent as simply a database with
developer-defined logic. But an agent is significantly less constrained than a
database. Databases are usually tightly constrained in one or more ways because
they need to provide certain guarantees (like atomicity) or optimizations (like
indexes). Arvo is a [single-level store](/reference/arvo/overview#single-level-store), so atomicity comes for free. Many
applications don't use databases because they need relational indices; rather,
they use them for their guarantees around persistence. Some do need the indices,
though, and it's not hard to imagine an agent which provides a SQL-like
interface.
On the other hand, an agent is also a lot like what many systems call a
"service". An agent is permanent and addressable -- a running program can talk
to an agent just by naming it. An agent can perform [IO](/blog/io-in-hoon), unlike most databases.
This is a critical part of an agent: it performs IO along the same transaction
boundaries as changes to its state, so if an effect happens, you know that the
associated state change has happened.
But the best way to think about an agent is as a state machine. Like a state
machine, any input could happen at any time, and it must react coherently to
that input. Output (effects) and the next state of the machine are a pure
function of the previous state and the input event.
## Table of Contents
#### Lessons
1. [Arvo](/guides/core/app-school/1-arvo) - This lesson provides an
overview of the Arvo operating system, and some other useful background
information.
2. [The Agent Core](/guides/core/app-school/2-agent) - This lesson goes over
the basic structure of a Gall agent.
3. [Imports and Aliases](/guides/core/app-school/3-imports-and-aliases) -
This lesson covers some useful libraries, concepts and boilerplate commonly
used when writing Gall agents.
4. [Lifecycle](/guides/core/app-school/4-lifecycle) - This lesson introduces
the state management arms of an agent.
5. [Cards](/guides/core/app-school/5-cards) - This lesson covers `card`s -
the structure used to pass messages to other agents and vanes.
6. [Pokes](/guides/core/app-school/6-pokes) - This lesson covers sending and
receiving one-off messages called "pokes" between agents.
7. [Structures and Marks](/guides/core/app-school/7-sur-and-marks) - This
lesson talks about importing type defintions, and writing `mark` files.
8. [Subscriptions](/guides/core/app-school/8-subscriptions) - This lesson
goes through the mechanics of subscriptions - both inbound and outbound.
9. [Vanes](/guides/core/app-school/9-vanes) - This lesson explains how to
interact with vanes (kernel modules) from an agent.
10. [Scries](/guides/core/app-school/10-scry) - This lesson gives an overview
of scrying Gall agents, and how scry endpoints are defined in agents.
11. [Failure](/guides/core/app-school/11-fail) - This lesson covers how Gall
handles certain errors and crashes, as well as the concept of a helper core.
12. [Next Steps](/guides/core/app-school/12-next-steps) - App School I is
now complete - here are some things you can look at next.
#### Appendix
- [Types](/guides/core/app-school/types) - A reference for a few of
the types used in App School.

View File

@ -0,0 +1,227 @@
+++
title = "Appendix: Types"
weight = 65
+++
This document explains a few of the types commonly used in Gall agents. In
addition to these, the [Data Types](/reference/arvo/gall/data-types) section of the
Gall vane documentation is a useful reference. In particular, the whole
[`agent`](/reference/arvo/gall/data-types#agent) subsection, as well as
[`bowl`](/reference/arvo/gall/data-types#bowl),
[`boat`](/reference/arvo/gall/data-types#boat), and
[`bitt`](/reference/arvo/gall/data-types#bitt).
## `vase`
Vases are used to encapsulate _dynamically typed_ data - they let typed data be
moved around in contexts where you can't know the type ahead of time, and
therefore can't have a _static_ type.
Vases are used extensively - almost all data your agent will send
and received is wrapped in a vase.
A vase is just a cell with data in the tail and the type of the data in the
head. Its formal definition is:
```hoon
+$ vase [p=type q=*]
```
Here's what it looks like if we bunt a vase in the dojo:
```
> *vase
[#t/* q=0]
```
There are two simple runes used to create and unpack vases. We'll look at each
of these next.
### Create a `vase`
The [zapgar](/reference/hoon/rune/zap#-zapgar) rune (`!>`)
takes a single argument of any noun, and wraps it in a vase. For example, in the
dojo:
```
> !>([1 2 3])
[#t/[@ud @ud @ud] q=[1 2 3]]
> !>('foo')
[#t/@t q=7.303.014]
> !>([[0xdead 0xb33f] 'foo'])
[#t/[[@ux @ux] @t] q=[[57.005 45.887] 7.303.014]]
> !>(foo='bar')
[#t/foo=@t q=7.496.034]
```
You would typically use `!>` as part of a [`cage`](#cage) when you're
constructing a `card` like a poke or a `%fact` `gift` to be sent off.
### Extract data from `vase`
The [zapgal](/reference/hoon/rune/zap#-zapgal) rune (`!<`)
takes two arguments: A mold specifying the type to try and extract the data as,
and the vase to be extracted.
Let's look at an example in the dojo. First, let's create a vase of `[@t @ux @ud]`:
```
> =myvase !>(['foo' 0xabcd 123])
> myvase
[#t/[@t @ux @ud] q=[7.303.014 43.981 123]]
```
Next, let's try extracting our vase:
```
> !< [@t @ux @ud] myvase
['foo' 0xabcd 123]
```
Now let's try asking for a `@p` rather than `@t`:
```
> !< [@p @ux @ud] myvase
-need.@p
-have.@t
nest-fail
```
As you can see, it will crash if the type does not nest. Note that
rather than using `!<`, you can also just clam the tail of the vase like:
```
> ((trel @t @ux @ud) +.myvase)
[p='foo' q=0xabcd r=123]
```
The only problem is that you can't tell if the auras were wrong:
```
> ((trel @p @ud @ux) +.myvase)
[p=~sibtel-tallyd q=43.981 r=0x7b]
```
You'd typically use `!<` on the data in `card`s that come in from other ships,
agents, etc.
## `mark`
The `mark` type is just a `@tas` like `%foo`, and specifies the Clay filetype of
some data. The `mark` corresponds to a mark file in the `/mar` directory, so a
`mark` of `%foo` corresponds to `/mar/foo/hoon`. Mark files are used for saving
data in Clay, validating data sent between agents or over the network, and
converting between different data types. For more information about mark files,
you can refer to the [Marks section of the Clay
documentation](/reference/arvo/clay/marks/marks).
## `cage`
A `cage` is a cell of a [`mark`](#mark) and a [`vase`](#vase), like `[%foo !>('bar')]`. The data in the vase should match the data type of the specified
mark.
Most data an agent sends will be in a `cage`, and most data it receives will
arrive in a `cage`. The `mark` may be used to validate or convert the data in
the `vase`, depending on the context.
## `quip`
`quip` is a mold-builder. A `(quip a b)` is equivalent to `[(list a) b]`, it's
just a more convenient way to specify it. Most arms of an agent return a `(quip card _this)`, which is a list of effects and a new state.
## `path`
The `path` type is formally defined as:
```hoon
+$ path (list knot)
```
A knot is a `@ta` text atom (see the [Strings guide](/guides/additional/hoon/strings)
for details), so a `path` is just a list of text. Rather than having to write
`[~.foo ~.bar ~.baz ~]` though, it has its own syntax which looks like
`/foo/bar/baz`.
A `path` is similar to a filesystem path in Unix, giving data a location in a
nested hierarchy. In Arvo though, they're not only used for files, but are a
more general type used for several different purposes. Its elements have no
inherent significance, it depends on the context. In a Gall agent, a `path` is
most commonly a subscription path - you might subscribe for updates to
`/foo/bar` on another agent, or another agent might subscribe to `/baz` on your
agent.
A `path` might just be a series of fixed `@ta` like `/foo/bar`, but some
elements might also be variable and include encoded atoms, or some other datum. For
example, you might like to include a date in the path like
`/updates/~2021.10.31..07.24.27..db68`. Other agents might create the path by
doing something like:
```hoon
/update/(scot %da now.bowl)
```
Then, when you get a subscription request, you might do something like:
```hoon
?+ path !!
[%updates @ ~]
=/ date=@da (slav %da i.t.path)
...(rest of code)...
```
See the [Encoding in text](/guides/additional/hoon/strings#encoding-in-text) and
[Decoding from text](/guides/additional/hoon/strings#decoding-from-text) sections of
the Strings guide for more information on dealing with atoms encoded in strings.
Aside from using function calls when constructing a `path` as demonstrated
above, you can also insert text you're previously stored with `=/` or what have
you, simply by enclosing them in brackets. For example, in the dojo:
```
> =const ~.bar
> `path`/foo/[const]/baz
/foo/bar/baz
```
## `wire`
The type of a wire is formally defined as:
```hoon
+$ wire path
```
So, a `wire` is just a [`path`](#path), type-wise they're exactly the same. The
reason there's a separate `wire` type is just to differentiate their purpose. A
`wire` is a path for responses to requests an agent initiates. If you subscribe
to the `path` `/some/path` on another agent, you also specify `/some/wire`.
Then, when that agent sends out updates to subscribers of `/some/path`, your
agent receives them on `/some/wire`.
More formally, `wire`s are used by Arvo to represent an event cause, and
therefore return path, in a call stack called a
[`duct`](/reference/arvo/overview#duct). Inter-vane communications happen over
`duct`s as [`move`](/reference/arvo/overview#moves)s, and Gall converts the `card`s
produced by agents into such `move`s behind the scenes. A detailed understanding
of this system is not necessary to write Gall agents, but if you're interested
it's comprehensively documented in the [Arvo overview](/reference/arvo/overview) and
[move trace tutorial](/reference/arvo/tutorials/move-trace).
For agents, the `wire` is specified in the second argument of a `%pass` `card`.
It's used for anything you can `%pass`, such as `%poke`s, `%watch`es, and
`%arvo` notes. For example:
```hoon
[%pass /this/is/wire %agent [~zod %foobar] %watch /this/is/path]
::
[%pass /this/is/wire %agent [~zod %foobar] %poke %foo !>('hello')]
::
[%pass /this/is/wire %arvo %b %wait (add now.bowl ~m1)]
```
The `on-agent` and `on-arvo` arms of the agent core include a `wire` in their
respective sample. Responses from agents come in to the former, and responses
from vanes come in to the latter.

View File

@ -0,0 +1,330 @@
+++
title = "Environment Setup"
description = "How to set up an environment for Urbit development."
weight = 11
+++
This guide covers best practices for preparing your environment to develop
within the Urbit ecosystem.
## Text editors
A variety of plugins have been built to provide support for the Hoon language in
different text editors. These are listed below.
**Note:** The hoon compiler expects Unix-style line endings (LF) and will
fail to parse Windows-style line endings (CRLF). Make sure your editor is set
to use LF for line endings, especially if you're developing on Windows.
#### Atom
Atom is free and open-source and runs on all major operating systems. It is
available [here](https://atom.io/). A package for Hoon support is maintained by
Tlon and may be obtained using the package manager within the editor by
searching for `Hoon`.
#### Sublime Text
Sublime Text is closed-source, but may be downloaded for free and there is no
enforced time limit for evaluation. It runs on all major operating systems. It
is available [here](https://www.sublimetext.com/).
#### Visual Studio Code
Visual Studio Code is free and open-source and runs on all major operating
systems. It is available [here](https://code.visualstudio.com/). Hoon support
may be acquired in the Extensions menu within the editor by searching for
`Hoon`.
#### Emacs
Emacs is free and open-source and runs on all major operating systems. It is
available [here](https://www.gnu.org/software/emacs/). Hoon support is available
with [hoon-mode.el](https://github.com/urbit/hoon-mode.el).
#### Vim
Vim is free and open-source and runs on all major operating systems. It is
available [here](https://www.vim.org/). Hoon support is available with
[hoon.vim](https://github.com/urbit/hoon.vim).
## Development ships
### Creating a fake ship
To do work with Hoon, we recommended using a "fake" ship — one that's not
connected to the network.
Because such a ship has no presence on the network, you don't need an Azimuth
identity. You just need to have [installed the Urbit binary](/getting-started/cli).
To create a fake ship named `~zod`, run the command below. You can replace `zod`
with any valid Urbit ship-name.
```
./urbit -F zod
```
This should take a couple of minutes, during which you should see a block of boot
messages, starting with the Urbit version number.
### Fake ship networking
Fake ships on the same machine can automatically talk to one another. Having
created a fakezod, you can create a fake `~bus` the same way:
```
./urbit -F bus
```
Now in the fakezod's dojo, try:
```
> |hi ~bus
>=
hi ~bus successful
```
### Faster fake ship booting
While working with Hoon, you'll often want to delete an old fake ship and
recreate a fresh one. Rather than having to wait a few minutes for the fresh
ship to be initialised, you can instead create a backup copy of a fake ship.
That way you can just delete the current copy, replace it with the backup, and
reboot in a matter of seconds.
To do this, boot a fresh fake ship like usual, but with a different name:
```
./urbit -F zod -c zod.new
```
Once it's finished booting, it's a good idea to mount its desks so you don't
have to do it again each time. In the dojo:
```
> |mount %base
>=
> |mount %garden
>=
> |mount %landscape
>=
> |mount %bitcoin
>=
> |mount %webterm
>=
```
Next, shut the ship down with `ctrl+D`. Then, copy the pier and start using the
copy instead:
```
cp -r zod.new zod
./urbit zod
```
Now whenever you want a fresh fakezod, you can just shut it down and do:
```
rm -r zod
cp -r zod.new zod
./urbit zod
```
## Working with desks
If you're just working in the dojo or experimenting with generators, committing
to the `%base` desk on a fake ship is fine. If you're working on a Gall agent or
developing a desk for distribution, you'll most likely want to work on a
separate desk and it's slightly more complicated.
### Mount a desk
To mount a desk to Unix so you can add files, you just need to run the `|mount`
command in the dojo and specify the name of the desk to mount:
```
|mount %base
```
The desk will now appear in the root of your pier (zod in this case):
```
zod
└── base
```
You can unmount it again by running the `|unmount` command in the dojo:
```
|unmount %base
```
### Create a new desk
To create a new desk, you'll need to merge from an existing one, typically
`%base`. In the dojo, run the following (you can change `%mydesk` to your
preferred name):
```
|merge %mydesk our %base
```
If you now mount it, you'll have `/mydesk` directory in your pier with all the
files of the `%base` desk inside. You can then delete the contents, copy in your
own files and `|commit` it.
Desks must contain all the `mark` files, libraries, etc, that they need. A
`sys.kelvin` file is mandatory, and there are a few `mark` files necessary as
well. In the next couple of sections we'll look at different ways to populate a
new desk with the necessary files.
### Minimal desk
This is the absolute minimal desk you'll be able to commit:
```
skeleton
├── mar
│ ├── hoon.hoon
│ ├── kelvin.hoon
│ ├── mime.hoon
│ ├── noun.hoon
│ ├── txt-diff.hoon
│ └── txt.hoon
└── sys.kelvin
```
`sys.kelvin` specifies the kernel kelvin version with which the desk is
compatible. You can copy it across from the `%base` desk, or just run the
following in the terminal from within the desk directory:
```sh
echo "[%zuse 418]" > sys.kelvin
```
The other `mark` files can just be copied across from the `%base` desk.
### Using dev desks
If you're working on something more complex, for example a desk with agents and
a front-end, there will be a number of `mark` files, libraries, etc, that will
be necessary. Rather than having to manually copy all the files from the
relevant default desks, the [urbit git repo](https://github.com/urbit/urbit)
includes some dev desks which can be used as a base. To get these, make sure you
have git installed and then clone the repo:
```
git clone https://github.com/urbit/urbit ~/git/urbit
```
If you now change to the `~/git/urbit/pkg` directory, you'll see the source for
the default desks, among other things:
```
cd ~/git/urbit/pkg
```
The desks ending in `-dev`, like `base-dev` and `garden-dev`, contain files for
interfacing with those respective desks. If you're creating a new desk that has
a tile and front-end, for example, you might like to use `base-dev` and
`garden-dev` as a base. To create such a base, there's a `symbolic-merge.sh`
script included in the directory. You can use it like so:
```
./symbolic-merge base-dev mydesk
./symbolic-merge garden-dev mydesk
```
After running that, you'll have a `mydesk` desk in the `pkg` directory that
contains the symlinked files from both those dev desks. To copy the files into
your pier, you can create and mount a mydesk desk in the dojo:
```
|merge %mydesk our %base
|mount %mydesk
```
Then, you can go into your pier:
```
cd /path/to/fake/zod
```
Delete the contents of `mydesk`:
```
rm -r mydesk/*
```
And then copy in the contents of the desk you created:
```
cp -rL ~/git/urbit/pkg/mydesk/* mydesk
```
Note you have to use `cp -rL` rather than just `cp -r` because the
`symbolic-merge.sh` script creates symlinks, so the `L` flag is to resolve them
and copy the actual files.
Now you can just add a `sys.kelvin` file:
```
echo "[%zuse 418]" > mydesk/sys.kelvin
```
And you'll be able to mount the desk with `|commit %mydesk`.
## Project organization
When you're developing a desk, it's best to structure your working directory
with the same hierarchy as a real desk. For example, `~/project/mydesk` might
look like:
```
mydesk
├── app
│ └── foo.hoon
├── desk.bill
├── desk.docket-0
├── lib
│ └── foo.hoon
├── mar
│ └── foo
│ ├── action.hoon
│ └── update.hoon
├── sur
│ └── foo.hoon
└── sys.kelvin
```
That way, whenever you want to test your changes, you can just copy it across to
your pier like:
```
cp -ruv mydesk/* /path/to/fake/zod/mydesk
```
And then just commit it in the dojo:
```
|commit %mydesk
```
If you're [using dev desks](#using-dev-desks) as a base, it's best to keep those
files separate from your own code.
### Syncing repos
A useful pattern is to work from a git repo and sync your work with the pier of
a fake ship. An easy way to do this is with the following command:
```
watch rsync -zr --delete /working/repo/desk/* /path/to/fake/zod/mydesk
```
Here `/working/repo/desk` is the folder that has the proper desk structure
outlined above, and `/path/to/fake/zod/mydesk` is the desk you wish to copy the
contents of the working repo to. From here, you can edit from the working repo
and perform git commands there, while testing your changes on the fake ship.

View File

@ -0,0 +1,127 @@
+++
title = "Introduction"
weight = 10
nodes = [100, 103]
objectives = ["Explain what an Urbit ship is.", "Distinguish a fakeship from a liveship.", "Pronounce ASCII characters per standard Hoon developer practice."]
+++
Hoon School is designed to teach you Hoon without assuming you have an extensive programming background. You should be able to following most of it even if you have no programming experience at all yet, though of course experience helps. We strongly encourage you to try out all the examples of each lesson. Hoon School is meant for the beginner, but it's not meant to be skimmed. Each lesson consists of:
- **Explanations**, which are prose-heavy commentary on the Hoon fundamentals.
- **Exercises**, which challenge you to clarify or expand your own understanding in practice.
- **Tutorials**, which are line-by-line commentary on example programs.
There are two flavors of Hoon School: the Hoon School Live cohort class, in which you work through lessons with other students and receive a certification (`%gora`) for completion, and these written Hoon School docs. To sign up for a future cohort of Hoon School Live, please [let us know of your interest here](https://forms.gle/bbW6QtJPMhsjCCML8) and we'll be in touch.
<!-- TODO point to HSL/ASL landing pages -->
## Why Hoon?
The short version is that Hoon uses Urbit's provisions and protocols to enable very fast application development with shared primitives, sensible affordances, and straightforward distribution.
Urbit consists of an identity protocol (“Azimuth”, or “Urbit ID”) and a system protocol (“Arvo”, or “Urbit OS”). These two parts work hand-in-hand to build your hundred-year computer.
1. **Urbit ID (Azimuth)** is a general-purpose public-key infrastructure (PKI) on the Ethereum blockchain, used as a platform for Urbit identities. It provides a system of scarce and immutable identities which are cryptographically secure.
2. **Urbit OS (Arvo)** is an operating system which provides the software for the personal server platform that constitutes the day-to-day usage of Urbit. Arvo works over a [peer-to-peer](https://en.wikipedia.org/wiki/Peer-to-peer) [end-to-end-encrypted](https://en.wikipedia.org/wiki/End-to-end_encryption) network to interact with other Urbit ships (or unique instances).
Arvo is an axiomatic operating system which restricts itself to pure mathematical functions, making it [deterministic](https://en.wikipedia.org/wiki/Deterministic_algorithm) and [functional-as-in-programming](https://en.wikipedia.org/wiki/Functional_programming). Such strong guarantees require an operating protocol, the [Nock virtual machine](/reference/nock/definition), which will be persistent across hardware changes and always provide an upgrade path for necessary changes.
It's hard to write a purely functional operating system on hardware which doesn't make such guarantees, so Urbit OS uses a new language, Hoon, which compiles to Nock and hews to the necessary conceptual models for a platform like Urbit. [The Hoon overview](/reference/hoon/overview) covers more of the high-level design decisions behind the language, as does [developer ~rovnys-ricfer's explanation](https://urbit.org/blog/why-hoon/).
Hoon School introduces and explains the fundamental concepts you need in order to understand Hoon's semantics. It then introduces a number of key examples and higher-order abstractions which will make you a more fluent Hoon programmer.
Once you have completed Hoon School, you should work through [App School](/guides/core/app-school/1-intro) to learn how to build full applications on Urbit.
## Environment Setup
An Urbit ship is a particular realization of an _identity_ and an _event log_ or _state_. Both of these are necessary.
Since live network identities (_liveships_) are finite, scarce, and valuable, most developers prefer to write new code using fake identities (_fakeships_ or _fakezods_). A fakeship is also different from a comet, which is an unkeyed liveship.
Two fakeships can communicate with each other on the same machine, but have no awareness of the broader Urbit network. We won't need to use this capability in Hoon School Live, but it will be helpful later when you start developing networked apps.
Before beginning, you'll need to get a development ship running and configure an appropriate editor. See the [Environment Setup](/guides/additional/development/environment) guide for details.
Once you have a `dojo>` prompt, the system is ready to go and waiting on input.
## Getting started
Once you've created your development ship, let's try a basic command. Type `%- add [2 2]` at the prompt and hit `Return`. (Note the double spaces before and after `add`.) Your screen now shows:
```hoon
fake: ~zod
ames: czar: ~zod on 31337 (localhost only)
http: live (insecure, public) on 80
http: live (insecure, loopback) on 12321
> %- add [2 2]
4
~zod:dojo>
```
You just used a function from the Hoon standard library, `add`, which for reasons that will become clear later is frequently written [`++add`](/reference/hoon/stdlib/1a#add). Next, quit Urbit by entering `|exit`:
```hoon
> %- add [2 2]
4
~zod:dojo> |exit
$
```
Your ship isn't running anymore and you're back at your computer's normal terminal prompt. If your ship is ~zod, then you can restart the ship by typing:
```hoon
urbit zod
```
You've already used a standard library function to produce one value, in the Dojo. Now that your ship is running again, let's try another. Enter the number `17`.
(We won't show the `~zod:dojo>` prompt from here on out. We'll just show the echoed command along with its result.)
You'll see:
```hoon
> 17
17
```
You asked Dojo to evaluate `17` and it echoed the number back at you. This value is a _noun_. We'll talk more about nouns in the next lesson.
Basically, every Hoon expression operates on the values it is given until it reduces to some form that can't evaluate any farther. This is then returned as the result of the evaluation.
One more:
```hoon
> :- 1 2
[1 2]
```
This `:-` rune takes two values and composes them into a _cell_, a pair of values.
## Pronouncing Hoon
Hoon uses _runes_, or two-character ASCII symbols, to describe its structure. (These are analogous to keywords in other programming languages.) Because there has not really been a standard way of pronouncing, say, `#` (hash, pound, number, sharp, hatch) or `!` (exclamation point, bang, shriek, pling), the authors of Urbit decided to adopt a one-syllable mnemonic to uniquely refer to each.
It is highly advisable for you to learn these pronunciations, as the documentation and other developers employ them frequently. For instance, a rune like `|=` is called a “bartis”, and you will find it designated as such in the docs, in the source code, and among the developers.
| Name | Character | Name | Character | Name | Character |
| ]---- | ----- | ---- | ----- | ---- | ----- |
| `ace` | `␣` | `gap` | `␣␣`, `\n` | pat | `@` |
| `bar` | `|` | `gar` | `>` | `sel` | `[` |
| `bas` | `\` | `hax` | `#` | `ser` | `]` |
| `buc` | `$` | `hep` | `-` | `sig` | `~` |
| `cab` | `_` | `kel` | `{` | `soq` | `'` |
| `cen` | `%` | `ker` | `}` | `tar` | `*` |
| `col` | `:` | `ket` | `^` | `tic` | `\`` |
| `com` | `,` | `lus` | `+` | `tis` | `=` |
| `doq` | `"` | `mic` | `;` | `wut` | `?` |
| `dot` | `.` | `pal` | `(` | `zap` | `!` |
| `fas` | `/` | `pam` | `&` | |
| `gal` | `<` | `par` | `)` | |
Note that the list includes two separate whitespace forms: `ace` for a single space `␣`; `gap` is either two or more spaces `␣␣` or a line break `\n`. In Hoon, the only whitespace significance is the distinction between `ace` and `gap`—i.e., the distinction between one space and more than one.

View File

@ -0,0 +1,553 @@
+++
title = "1. Hoon Syntax"
weight = 10
nodes = [110, 113]
objectives = ["Distinguish nouns, cells, and atoms.", "Apply auras to transform an atom.", "Identify common Hoon molds, such as cells, lists, and tapes.", "Pin a face to the subject.", "Make a decision at a branch point.", "Distinguish loobean from boolean operations.", "Slam a gate (call a function)."]
+++
_This module will discuss the fundamental data concepts of Hoon and how programs effect control flow._
The study of Hoon can be divided into two parts: syntax and semantics.
1. The **syntax** of a programming language is the set of rules that determine what counts as admissible code in that language. It determines which characters may be used in the source, and also how these characters may be assembled to constitute a program. Attempting to run a program that doesnt follow these rules will result in a syntax error.
2. The **semantics** of a programming language concerns the meaning of the various parts of that languages code.
In this lesson we will give a general overview of Hoons syntax. By the end of it, you should be familiar with all the basic elements of Hoon code.
## Hoon Elements
An [**expression**](https://en.wikipedia.org/wiki/Expression_%28computer_science%29) is a combination of characters that a language interprets and evaluates to produce a value. All Hoon programs are built of expressions, rather like mathematical equations. Hoon expressions are built along a backbone of _runes_, which are two-character symbols that act like keywords in other programming languages to define the syntax, or grammar, of the expression.
Runes are the building blocks of all Hoon code, represented as a pair of non-alphanumeric ASCII characters. Runes form expressions; runes are used how keywords are used in other languages. In other words, all computations in Hoon ultimately require runes. Runes and other Hoon expressions are all separated from one another by either two spaces or a line break.
All runes take a fixed number of “children” or “daughters”. Children can themselves be runes with children, and Hoon programs work by chaining through these until a value—not another rune—is arrived at. For this reason, we very rarely need to close expressions. Keep this scheme in mind when examining Hoon code.
Hoon expressions can be either basic or complex. Basic expressions of Hoon are fundamental, meaning that they cant be broken down into smaller expressions. Complex expressions are made up of smaller expressions (which are called **subexpressions**).
The Urbit operating system hews to a conceptual model wherein each expression takes place in a certain context (the _subject_). While sharing a lot of practicality with other programming paradigms and platforms, Urbit's model is mathematically well-defined and unambiguously specified. Every expression of Hoon is evaluated relative to its subject, a piece of data that represents the environment, or the context, of an expression.
At its root, Urbit is completely specified by [Nock](/reference/nock/definition), sort of a machine language for the Urbit virtual machine layer and event log. However, Nock code is basically unreadable (and unwriteable) for a human. [One worked example](/reference/nock/example) yields, for decrementing a value by one, the Nock formula:
```hoon
[8 [1 0] 8 [1 6 [5 [0 7] 4 0 6] [0 6] 9 2 [0 2] [4 0 6] 0 7] 9 2 0 1]
```
This is like reading binary machine code: we mortals need a clearer vernacular.
Hoon serves as Urbit's practical programming language. Everything in Urbit OS is written in Hoon, and many of the ancillary tools as well.
Any operation in Urbit ultimately results in a value. Much like machine language designates any value as a command, an address, or a number, a Hoon value is interpreted per the Nock rules and results in a basic data value at the end. So what are our data values in Hoon, and how do they relate to each other?
## Nouns
Think about a child persistently asking you what a thing is made of. At first, you may respond, “plastic”, or “metal”. Eventually, the child may wear you down to a more fundamental level: atoms and molecules (bonded atoms).
In a very similar sense, everything in a Hoon program is an atom or a bond. Metaphorically, a Hoon program is a complex molecule, a digital chemistry that describes one mathematical representation of data.
The most general data category in Hoon is a _noun_. This is just about as broad as saying “thing”, so let's be more specific:
> A noun is an atom or a cell.
Progress? We can say, in plain English, that
- An _atom_ is a nonzero integer number (0+∞), e.g. `42`.
- A _cell_ is a pair of two nouns, written in square brackets, e.g. `[0 1]`.
_Everything_ in Hoon (and Nock, and Urbit) is a noun. The Urbit OS itself is a noun. So given any noun, the Urbit VM simply applies the Nock rules to change the noun in well-defined mechanical ways.
### Atoms
If an atom is a nonzero number, how do we represent anything else? Hoon provides each atom an _aura_, a tag which lets you treat a number as text, time, date, Urbit address, IP address, and much more.
An aura always begins with `@` pat, which denotes an atom (as opposed to a cell, `^` ket, or the general noun, `*` tar). The next letter or letters tells you what kind of representation you want the value to have.
For instance, to change the representation of a regular decimal number like `32` to a binary representation (i.e. for 2⁵), use `@ub`:
```
> `@ub`32
0b10.0000
```
(The tic marks are a shorthand which we'll explain later.)
Aura values are all designed to be [URL-safe](https://developers.google.com/maps/url-encoding), so the European-style thousands separator `.` dot is used instead of the English `,` com. `1.000` is one thousand, not `1.0` one with a fractional part of zero.
While there are dozens of auras for specialized applications, here are the most important ones for you to know:
| Aura | Meaning | Example | Comment |
| ---- | ------- | ------- | ------- |
| `@` | Empty aura | `100` | (displays as `@ud`) |
| `@da` | Date (absolute) | ~2022.2.8..16.48.20..b53a | Epoch calculated from 292 billion B.C. |
| `@p` | Ship name | `~zod` | |
| `@rs` | Number with fractional part | `.3.1415` | Note the preceding `.` dot. |
| `@t` | Text (“cord”) | `'hello'` | One of Urbit's several text types; only UTF-8 values are valid. |
| `@ub` | Binary value | `0b1100.0101` | |
| `@ud` | Decimal value | `100.000` | Note that German-style thousands separator is used, `.` dot. |
| `@ux` | Hexadecimal value | `0x1f.3c4b` | |
Hearkening back to our discussion of interchangeable representations in Lesson -1, you can see that these are all different-but-equivalent ways of representing the same underlying data values.
There's a special value that recurs in many contexts in Hoon: `~` sig is the null or zero value.
The [`^-` kethep](/reference/hoon/rune/ket#-kethep) rune is useful for ensuring that everything in the second child matches the type (aura) of the first, e.g.
```
^- @ux 0x1ab4
```
We will use `^-` kethep extensively to enforce type constraints, a very useful tool in Hoon code.
#### Exercise: Aura Conversions
Convert between some of the given auras at the Dojo prompt, e.g.:
- `100` to `@p`
- `0b1100.0101` to `@p`
- `0b1100.0101` to `@ux`
- `0b1100.0101` to `@ud`
- `~` to any other aura
### Cells
A cell is a pair of two nouns. Cells are traditionally written using square brackets: `[]`. For now, just recall the square brackets and that cells are always _pairs_ of values.
```
[1 2]
[@p @t]
[[1 2] [3 4]]
```
This is actually a shorthand for a rune as well, [`:-` colhep](/reference/hoon/rune/col#-colhep):
```
:- 1 2
```
produces a cell `[1 2]`. You can chain these together:
```
:- 1 :- 2 3
```
to produce `[1 [2 3]]` or `[1 2 3]`.
We deal with cells in more detail below.
> ### Hoon as Noun
>
> We mentioned earlier that everything in Urbit is a noun, including the program itself. This is true, but getting from the rune expression in Hoon to the numeric expression requires a few more tools than we currently are prepared to introduce.
>
> For now, you can preview the structure of the Urbit OS as a noun by typing `.` dot at the Dojo prompt. This displays a summary of the structure of the operating function itself as a noun.
{: .callout}
## Verbs (Runes)
The backbone of any Hoon expression is a scaffolding of _runes_, which are essentially mathematical relationships between daughter components. If nouns are nouns, then runes are verbs: they describe how nouns relate. Runes provide the structural and logical relationship between noun values.
A rune is just a pair of ASCII characters (a digraph). We usually pronounce runes by combining their characters names, e.g.: “kethep” for `^-`, “bartis” for `|=`, and “barcen” for `|%`.
For instance, when we called a function earlier (in Hoon parlance, we _slammed a gate_), we needed to provide the [`%-` cenhep](/reference/hoon/rune/cen#-cenhep) rune with two bits of information, a function name and the values to associate with it:
```hoon
%-
add
[1 2]
```
The operation you just completed is straightforward enough: `1 + 2`, in many languages, or `(+ 1 2)` in a [Lisp dialect](https://en.wikipedia.org/wiki/Lisp_%28programming_language%29) like [Clojure](https://en.wikipedia.org/wiki/Clojure). Literally, we can interpret `%- add [1 2]` as “evaluate the `add` core on the input values `[1 2]`”.
[`++add`](/reference/hoon/stdlib/1a#add) expects precisely two values (or _arguments_), which are provided by `%-` in the neighboring child expression as a cell. There's really no limit to the complexity of Hoon expressions: they can track deep and wide. They also don't care much about layout, which leaves you a lot of latitude. The only hard-and-fast rule is that there are single spaces (`ace`s) and everything else (`gap`s).
```hoon
%-
add
[%-(add [1 2]) 3]
```
(Notice that inside of the `[]` cell notation we are using a slightly different form of the `%-` rune call. In general, there are several ways to use many runes, and we will introduce these gradually. We'll see more expressive ways to write Hoon code after you're comfortable using runes.)
For instance, here are some of the standard library functions which have a similar architecture in common:
- [`++add`](/reference/hoon/stdlib/1a#add) (addition)
- [`++sub`](/reference/hoon/stdlib/1a#sub) (subtraction, positive results only—what happens if you subtract past zero?)
- [`++mul`](/reference/hoon/stdlib/1a#mul) (multiplication)
- [`++div`](/reference/hoon/stdlib/1a#div) (integer division, no remainder)
- [`++pow`](/reference/hoon/stdlib/1a#pow) (power or exponentiation)
- [`++mod`](/reference/hoon/stdlib/1a#add) (modulus, remainder after integer division)
- [`++dvr`](/reference/hoon/stdlib/1a#dvr) (integer division with remainder)
- [`++max`](/reference/hoon/stdlib/1a#max) (maximum of two numbers)
- [`++min`](/reference/hoon/stdlib/1a#min) (minimum of two numbers)
### Rune Expressions
Any Hoon program is architected around runes. If you have used another programming language, you can see these as analogous to keywords, although they also make explicit what most language syntax parsers leave implicit. Hoon aims at a parsimony of representation while leaving latitude for aesthetics. In other words, Hoon strives to give you a unique characteristic way of writing a correct expression, but it leaves you flexibility in how you lay out the components to maximize readability.
We are only going to introduce a handful of runes in this lesson, but by the time we're done with Hoon School, you'll know the twenty-five or so runes that yield 80% of the capability.
#### Exercise: Identifying Unknown Runes
Here is a lightly-edited snippet of Hoon code. Anything written after a `::` colcol is a _comment_ and is ignored by the computer. (Comments are useful for human-language explanations.)
```hoon
%- send
:: forwards compatibility with next-dill
?@ p.kyz [%txt p.kyz ~]
?: ?= %hit -.p.kyz
[%txt ~]
?. ?= %mod -.p.kyz
p.kyz
=/ =@c
?@ key.p.kyz key.p.kyz
?: ?= ?(%bac %del %ret) -.key.p.kyz
`@`-.key.p.kyz
~-
?: ?= %met mod.p.kyz [%met c] [%ctl c]
```
1. Mark each rune.
2. For each rune, find its corresponding children. (You don't need to know what a rune does to identify how things slot together.)
3. Consider these questions:
- Is every pair of punctuation marks a rune?
- How can you tell a rune from other kinds of marks?
One clue: every rune in Hoon (except for one, not in the above code) has _at least one child_.
### Exercise: Inferring Rune Behavior
Here is a snippet of Hoon code:
```hoon
^- list
:~ [hen %lsip %e %init ~]
[hen %lsip %d %init ~]
[hen %lsip %g %init ~]
[hen %lsip %c %init ~]
[hen %lsip %a %init ~]
==
```
Without looking it up first, what does the [`==` tistis](/reference/hoon/rune/terminators#-tistis) do for the [`:~` colsig](/reference/hoon/rune/col#-colsig) rune? Hint: some runes can take any number of arguments.
> Most runes are used at the beginning of a complex expression, but there are exceptions. For example, the runes [`--` hephep](/reference/hoon/rune/terminators#-hephep) and [`==` tistis](/reference/hoon/rune/terminators#-tistis) are used at the end of certain expressions.
#### Aside: Writing Incorrect Code
At the Dojo, you can attempt to operate using the wrong values; for instance, `++add` doesn't know how to add three numbers at the same time.
```hoon
> %- add [1 2 3]
-need.@
-have.[@ud @ud]
nest-fail
dojo: hoon expression failed
```
So this statement above is _syntactically_ correct (for the `%-` rune) but in practice fails because the expected input arguments don't match. Any time you see a `need`/`have` pair, this is what it means.
### Rune Families
Runes are classified by family (with the exceptions of `--` hephep and `==` tistis). The first of the two symbols indicates the family—e.g., the `^-` kethep rune is in the `^` ket family of runes, and the `|=` bartis and `|%` barcen runes are in the `|` bar family. The runes of particular family usually have related meanings. Two simple examples: the runes in the `|` bar family are all used to create cores, and the runes in the `:` col family are all used to create cells.
Rune expressions are usually complex, which means they usually have one or more subexpressions. The appropriate syntax varies from rune to rune; after all, theyre used for different purposes. To see the syntax rules for a particular rune, consult the rune reference. Nevertheless, there are some general principles that hold of all rune expressions.
Runes generally have a fixed number of expected children, and thus do not need to be closed. In other languages youll see an abundance of terminators, such as opening and closing parentheses, and this way of doing this is largely absent from Urbit. Thats because all runes take a fixed number of children. Children of runes can themselves be runes (with more children), and Hoon programs work by chaining through these series of children until a value—not another rune—is arrived at. This makes Hoon code nice and neat to look at.
### Tall and Wide Forms
We call rune expressions separated by `gap`s **tall form** and those using parentheses **wide form**. Tall form is usually used for multi-line expressions, and wide form is used for one-line expressions. Most runes can be used in either tall or wide form. Tall form expressions may contain wide form subexpressions, but wide form expressions may not contain tall form.
The spacing rules differ in the two forms. In tall form, each rune and subexpression must be separated from the others by a `gap`: two or more spaces, or a line break. In wide form the rune is immediately followed by parentheses `( )`, and the various subexpressions inside the parentheses must be separated from the others by an `ace`: a single space.
Seeing an example will help you understand the difference. The `:-` colhep rune is used to produce a cell. Accordingly, it is followed by two subexpressions: the first defines the head of the cell, and the second defines the tail. Here are three different ways to write a `:-` colhep expression in tall form:
```hoon
> :- 11 22
[11 22]
> :- 11
22
[11 22]
> :-
11
22
[11 22]
```
All of these expressions do the same thing. The first example shows that, if you want to, you can write tall form code on a single line. Notice that there are two spaces between the `:-` colhep rune and `11`, and also between `11` and `22`. This is the minimum spacing necessary between the various parts of a tall form expression—any fewer will result in a syntax error.
Usually one or more line breaks are used to break up a tall form expression. This is especially useful when the subexpressions are themselves long stretches of code. The same `:-` colhep expression in wide form is:
```hoon
> :-(11 22)
[11 22]
```
This is the preferred way to write an expression on a single line. The rune itself is followed by a set of parentheses, and the subexpressions inside are separated by a single space. Any more spacing than that results in a syntax error.
Nearly all rune expressions can be written in either form, but there are exceptions. `|%` barcen and `|_` barcab expressions, for example, can only be written in tall form. (Those are a bit too complicated to fit comfortably on one line anyway.)
### Nesting Runes
Since runes take a fixed number of children, one can visualize how Hoon expressions are built by thinking of each rune being followed by a series of boxes to be filled—one for each of its children. Let us illustrate this with the `:-` colhep rune.
![Colhep rune with two empty boxes for children.](https://media.urbit.org/docs/hoon-syntax/cell1.png)
Here we have drawn the `:-` colhep rune followed by a box for each of its two children. We can fill these boxes with either a value or an additional rune. The following figure corresponds to the Hoon expression `:- 2 3`.
![Colhep rune with two boxes for children containing 2 and 3.](https://media.urbit.org/docs/hoon-syntax/cell2.png)
This, of course, evaluates to the cell `[2 3]`.
The next figure corresponds to the Hoon expression `:- :- 2 3 4`.
![Colhep rune with two boxes for children, one containing a colhep rune with two boxes for children containing 2 and 3, and 4.](https://media.urbit.org/docs/hoon-syntax/cell3.png)
This evaluates to `[[2 3] 4]`, and we can think of the second `:-` colhep as being “nested” inside of the first `:-` colhep.
What Hoon expression does the following figure correspond to, and what does it evaluate to?
![Colhep rune with two boxes for children containing 2 and a colhep rune with two boxes for children containing 3 and 4.](https://media.urbit.org/docs/hoon-syntax/cell4.png)
This represents the Hoon expression `:- 2 :- 3 4`, and evaluates to `[2 [3 4]]`. (If you input this into dojo it will print as `[2 3 4]`, which we'll consider later.)
Thinking in terms of such “LEGO brick” diagrams can be a helpful learning and debugging tactic.
## Preserving Values with Faces
A Hoon expression is evaluated against a particular subject, which includes Hoon definitions and the standard library, as well as any cuser-specified values which have been made available. Unlike many procedural programming languages, a Hoon expression only knows what it has been told explicitly. This means that as soon as we calculate a value, it returns and falls back into the ether.
```
%- sub [5 1]
```
Right now, we don't have a way of preserving values for subsequent use in a more complicated Hoon expression.
We are going to store the value as a variable, or in Hoon, “pin a face to the subject”. Hoon faces aren't exactly like variables in other programming languages, but for now we can treat them that way, with the caveat that they are only accessible to daughter or sister expressions.
When we used `++add` or `++sub` previously, we wanted an immediate answer. There's not much more to say than `5 + 1`. In contrast, pinning a face accepts three daughter expressions: a name (or face), a value, and the rest of the expression.
```hoon
=/ perfect-number 28
%- add [perfect-number 10]
```
This yields `38`, but if you attempt to refer to `perfect-number` again on the next line, the Dojo fails to locate the value.
```hoon
> =/ perfect-number 28
%- add [perfect-number 10]
38
> perfect-number
-find.perfect-number
dojo: hoon expression failed
```
This syntax is a little bit strange in the Dojo because subsequent expressions, although it works quite well in long-form code. The Dojo offers a workaround to retain named values:
```
> =perfect-number 28
> %- add [perfect-number 10]
38
> perfect-number
38
```
The difference is that the Dojo “pin” is permanent until deleted:
```
=perfect-number
```
rather than only effective for the daughter expressions of a `=/` tisfas rune. (We also won't be able to use this Dojo-style pin in a regular Hoon program.)
### Exercise: A Large Power of Two
Create two numbers named `two` and `twenty`, with appropriate values, using the `=/` tisfas rune.
Then use these values to calculate 2²⁰ with `++pow` and `%-` cenhep.
## Containers & Basic Data Structures
Atoms are well and fine for relatively simple data, but we already know about cells as pairs of nouns. How else can we think of collections of data?
### Cells
A cell is formally a pair of two objects, but as long as the second (right-hand) object is a cell, these can be written stacked together:
```hoon
> [1 [2 3]]
[1 2 3]
> [1 [2 [3 4]]]
[1 2 3 4]
```
This convention keeps the notation from getting too cluttered. For now, let's call this a “running cell” because it consists of several cells run together.
Since almost all cells branch rightwards, the pretty-printer (the printing routine that the Dojo uses) prefers to omit `[]` brackets marking the rightmost cells in a running cell. These read to the right—that is, `[1 2 3]` is the same as `[1 [2 3]]`.
#### Exercise: Comparing Cells
Enter the following cells:
```hoon
[1 2 3]
[1 [2 3]]
[[1 2] 3]
[[1 2 3]]
[1 [2 [3]]]
[[1 2] [3 4]]
[[[1 2] [3 4]] [[5 6] [7 8]]]
```
Note which are the same as each other, and which are not. We'll look at the deeper structure of cells later when we consider trees.
### Lists
A running cell which terminates in a `~` sig (null) atom is a list.
- What is `~`'s value? Try casting it to another aura.
`~` is the null value, and here acts as a list terminator.
Lists are ubiquitous in Hoon, and many specialized tools exist to work with them. (For instance, to apply a gate to each value in a list, or to sum up the values in a list, etc.) We'll see more of them in a future lesson.
#### Exercise: Making a List from a Null-Terminated Cell
You can apply an aura to explicitly designate a null-terminated running cell as a list containing particular types of data. Sometimes you have to clear the aura using a more general aura (like `@`) before the conversion can work.
```hoon
> `(list @ud)`[1 2 3 ~]
~[1 2 3]
> `(list @ux)`[1 2 3 ~]
mint-nice
-need.?(%~ [i=@ux t=it(@ux)])
-have.[@ud @ud @ud %~]
nest-fail
dojo: hoon expression failed
> `(list @)`[1 2 3 ~]
~[1 2 3]
> `(list @ux)``(list @)`[1 2 3 ~]
~[0x1 0x2 0x3]
```
### Text
There are two ways to represent text in Urbit: cords (`@t` aura atoms) and tapes (lists of individual characters). Both of these are commonly called [“strings”](https://en.wikipedia.org/wiki/String_%28computer_science%29).
Why represent text? What does that mean? We have to have a way of distinguishing words that mean something to Hoon (like `list`) from words that mean something to a human or a process (like `'hello world'`).
Right now, all you need to know is that there are (at least) two valid ways to write text:
- `'with single quotes'` as a cord.
- `"with double quotes"` as text.
We will use these incidentally for now and explain their characteristics in a later lesson. Cords and text both use [UTF-8](https://en.wikipedia.org/wiki/UTF-8) representation, but all actual code is [ASCII](https://en.wikipedia.org/wiki/ASCII).
```hoon
> "You can put ½ in quotes, but not elsewhere!"
"You can put ½ in quotes, but not elsewhere!"
> 'You can put ½ in single quotes, too.'
'You can put ½ in single quotes, too.'
> "Some UTF-8: ἄλφα"
"Some UTF-8: ἄλφα"
```
#### Exercise: ASCII Values in Text
A cord (`@t`) represents text as a sequence of characters. If you know the [ASCII](https://en.wikipedia.org/wiki/ASCII) value for a particular character, you can identify how the text is structured as a number. (This is most easily done using the hexadecimal `@ux` representation due to bit alignment.)
![](https://upload.wikimedia.org/wikipedia/commons/thumb/1/1b/ASCII-Table-wide.svg/1024px-ASCII-Table-wide.svg.png)
If you produce a text string as a cord, you can see the internal structure easily in Hoon:
```hoon
> `@ux`'Mars'
0x7372.614d
```
that is, the character codes `0x73` = `'s'`, `0x72` = `'r'`, `0x61` = `'a'`, and `0x4d` = `'M'`. Thus a cord has its first letter as the smallest (least significant, in computer-science parlance) byte.
## Making a Decision
The final rune we introduce in this lesson will allow us to select between two different Hoon expressions, like picking a fork in a road. Any computational process requires the ability to distinguish options. For this, we first require a basis for discrimination: truthness.
Essentially, we have to be able to decide whether or not some value or expression evaluates as `%.y` _true_ (in which case we will do one thing) or `%.n` _false_ (in which case we do another). At this point, our basic expressions are always mathematical; later on we will check for existence, for equality of two values, etc.
- [`++gth`](/reference/hoon/stdlib/1a#gth) (greater than `>`)
- [`++lth`](/reference/hoon/stdlib/1a#lth) (less than `<`)
- [`++gte`](/reference/hoon/stdlib/1a#gte) (greater than or equal to `≥`)
- [`++lte`](/reference/hoon/stdlib/1a#lte) (less than or equal to `≤`)
If we supply these with a pair of numbers to a `%-` cenhep call, we can see if the expression is considered `%.y` true or `%.n` false.
```
> %- gth [5 6]
%.n
> %- lth [7 6]
%.n
> %- gte [7 6]
%.y
> %- lte [7 7]
%.y
```
Given a test expression like those above, we can use the `?:` wutcol rune to decide between the two possible alternatives. `?:` wutcol accepts three children: a true/false statement, an expression for the `%.y` true case, and an expression for the `%.n` false case.
[Piecewise mathematical functions](https://en.wikipedia.org/wiki/Piecewise) require precisely this functionality. For instance, the Heaviside function is a piecewise mathematical function which is equal to zero for inputs less than zero and one for inputs greater than or equal to zero.
<img src="https://latex.codecogs.com/svg.image?\large&space;H(x)=\begin{cases}&space;1,&space;&&space;x&space;>&space;0&space;\\&space;0,&space;&&space;x&space;\le&space;0&space;\end{cases}" title="https://latex.codecogs.com/svg.image?\large H(x):=\begin{cases} 1, & x > 0 \\ 0, & x \le 0 \end{cases}" />
<!--$$
H(x)
=
\begin{cases} 1, & x > 0 \\ 0, & x \le 0 \end{cases}
$$-->
_However_, we don't yet know how to represent a negative value! All of the decimal values we have used thus far are unsigned (non-negative) values, `@ud`. For now, the easiest solution is to just translate the Heaviside function so it activates at a different value:
<img src="https://latex.codecogs.com/svg.image?\large&space;H_{10}(x)=\begin{cases}&space;1,&space;&&space;x&space;>&space;10&space;\\&space;0,&space;&&space;x&space;\le&space;10&space;\end{cases}" title="https://latex.codecogs.com/svg.image?\large H_{10}(x):=\begin{cases} 1, & x > 10 \\ 0, & x \le 10 \end{cases}" />
<!--$$
H_{10}(x)
=
\begin{cases} 1, & x > 10 \\ 0, & x \le 10 \end{cases}
$$-->
Thus equipped, we can evaluate the Heaviside function for particular values of `x`:
```hoon
=/ x 10
?: %- gte [x 10]
1
0
```
We don't know yet how to store this capability for future use on as-yet-unknown values of `x`, but we'll see how to do so in a future lesson.
Carefully map how the runes in that statement relate to each other, and notice how the taller structure makes it relatively easier to read and understand what's going on.
#### Exercise: “Absolute” Value (Around Ten)
Implement a version of the absolute value function, _|x|_, similar to the Heaviside implementation above. (Translate it to 10 as well since we still can't deal with negative numbers; call this $|x|_{10}$.)
<img src="https://latex.codecogs.com/svg.image?|x|_{10}=\begin{cases}&space;x-10,&space;&&space;x&space;>&space;10&space;\\&space;0,&space;&&space;10-x&space;\le&space;10&space;\end{cases}" title="https://latex.codecogs.com/svg.image?|x|_{10}=\begin{cases} x-10, & x > 10 \\ 0, & 10-x \le 10 \end{cases}" />
<!--$$
|x|_{10}
=
\begin{cases} x-10, & x > 10 \\ 0, & 10-x \le 10 \end{cases}
$$-->
Test it on a few values like 8, 9, 10, 11, and 12.

Some files were not shown because too many files have changed in this diff Show More