mirror of
https://github.com/urbit/developers.urbit.org.git
synced 2024-09-11 13:55:59 +03:00
commit
b80d5ab9b0
17
.eslintrc.js
17
.eslintrc.js
@ -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
13
.gitignore
vendored
@ -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
|
34
README.md
34
README.md
@ -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
50
components/BasicPage.js
Normal 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
44
components/BlogPreview.js
Normal 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
48
components/Card.js
Normal 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>
|
||||
);
|
||||
}
|
@ -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
86
components/ContentArea.js
Normal 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>
|
||||
);
|
||||
}
|
88
components/EventPreview.js
Normal file
88
components/EventPreview.js
Normal 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>
|
||||
);
|
||||
}
|
@ -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'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'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} />;
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
@ -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
104
components/MenuTray.js
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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
24
components/Pagination.js
Normal 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
50
components/PostPreview.js
Normal 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
389
components/Search.js
Normal 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);
|
@ -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
34
components/Sidebar.js
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
@ -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
107
components/Snippets.js
Normal 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
41
components/TallCard.js
Normal 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
40
components/icons/arvo.js
Normal 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>
|
||||
);
|
||||
}
|
64
components/icons/azimuth.js
Normal file
64
components/icons/azimuth.js
Normal file
File diff suppressed because one or more lines are too long
21
components/icons/comms.js
Normal file
21
components/icons/comms.js
Normal 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>
|
||||
);
|
||||
}
|
72
components/icons/cryptography.js
Normal file
72
components/icons/cryptography.js
Normal 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>
|
||||
);
|
||||
}
|
31
components/icons/database.js
Normal file
31
components/icons/database.js
Normal 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>
|
||||
);
|
||||
}
|
49
components/icons/distribution.js
Normal file
49
components/icons/distribution.js
Normal 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>
|
||||
);
|
||||
}
|
82
components/icons/functional.js
Normal file
82
components/icons/functional.js
Normal 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
28
components/icons/hoon.js
Normal 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>
|
||||
);
|
||||
}
|
94
components/icons/identity.js
Normal file
94
components/icons/identity.js
Normal 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
14
components/icons/index.js
Normal 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";
|
91
components/icons/interface.js
Normal file
91
components/icons/interface.js
Normal 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>
|
||||
);
|
||||
}
|
13
components/icons/mintfiller.js
Normal file
13
components/icons/mintfiller.js
Normal 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
54
components/icons/nock.js
Normal 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
77
components/icons/peer.js
Normal 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
104
components/icons/vere.js
Normal 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>
|
||||
);
|
||||
}
|
50
content/blog/asl-announce.md
Normal file
50
content/blog/asl-announce.md
Normal file
@ -0,0 +1,50 @@
|
||||
+++
|
||||
title = "App School Live"
|
||||
date = "2022-06-29"
|
||||
description = "We’re 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.
|
||||
|
28
content/blog/duo-announce.md
Normal file
28
content/blog/duo-announce.md
Normal file
@ -0,0 +1,28 @@
|
||||
+++
|
||||
title = "(Re)Launching developers.urbit.org"
|
||||
date = "2022-06-29"
|
||||
description = "We’ve 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)._
|
||||
|
52
content/blog/layer-2-guides.md
Normal file
52
content/blog/layer-2-guides.md
Normal file
@ -0,0 +1,52 @@
|
||||
+++
|
||||
title = "Layer 2 Guides"
|
||||
date = "2022-02-14"
|
||||
description = "Urbit’s 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"
|
||||
+++
|
||||
|
||||
Urbit’s [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, **Urbit’s 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 you’ve 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 you’re 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 identity’s 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 ETH’s 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 you’re 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 don’t 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 Tlon’s roller. Planets can make use of Tlon’s 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
|
||||
|
||||
There’s 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) – It’s 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 Tlon’s 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 Tlon’s roller.
|
||||
- Planets on Layer 2 cannot currently interact with Layer 1 tools or contracts like MetaMask or OpenSea.
|
@ -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. It’s 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. They’ve 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. Here’s a list of prominent figures you’ll likely encounter that are open to being contacted:
|
6
content/community/directory/lagrev-nocfep.md
Normal file
6
content/community/directory/lagrev-nocfep.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
name = "N.E. Davis"
|
||||
patp = "~lagrev-nocfep"
|
||||
org = "Urbit Foundation"
|
||||
title = "Technical Director, Urbit Foundation"
|
||||
+++
|
6
content/community/directory/master-morzod.md
Normal file
6
content/community/directory/master-morzod.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
name = "Joe Bryan"
|
||||
patp = "~master-morzod"
|
||||
org = "Tlon"
|
||||
title = "Senior infrastructure engineer, Tlon"
|
||||
+++
|
6
content/community/directory/palfun-foslup.md
Normal file
6
content/community/directory/palfun-foslup.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
name = "~palfun-foslup"
|
||||
patp = "~palfun-foslup"
|
||||
org = "Tlon"
|
||||
title = "Software engineer, Tlon"
|
||||
+++
|
6
content/community/directory/poldec-tonteg.md
Normal file
6
content/community/directory/poldec-tonteg.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
name = "Anthony Arroyo"
|
||||
patp = "~poldec-tonteg"
|
||||
org = "Urbit Foundation"
|
||||
title = "Director of the Combine, Urbit Foundation"
|
||||
+++
|
6
content/community/directory/ravmel-ropdyl.md
Normal file
6
content/community/directory/ravmel-ropdyl.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
name = "Galen Wolfe-Pauly"
|
||||
patp = "~ravmel-ropdyl"
|
||||
org = "Tlon"
|
||||
title = "CEO, Tlon Corporation"
|
||||
+++
|
6
content/community/directory/rovnys-ricfer.md
Normal file
6
content/community/directory/rovnys-ricfer.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
name = "Ted Blackman"
|
||||
patp = "~rovnys-ricfer"
|
||||
org = "Tlon"
|
||||
title = "Senior infrastructure engineer, Tlon"
|
||||
+++
|
6
content/community/directory/sicdev-pilnup.md
Normal file
6
content/community/directory/sicdev-pilnup.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
name = "Kenny Rowe"
|
||||
patp = "~sicdev-pilnup"
|
||||
org = "Additional Community Members"
|
||||
title = "Representative, Dalten Collective"
|
||||
+++
|
6
content/community/directory/taller-ravnut.md
Normal file
6
content/community/directory/taller-ravnut.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
name = "Mike Osbourne"
|
||||
patp = "~taller-ravnut"
|
||||
org = "Urbit Foundation"
|
||||
title = "Developer Relations, Urbit Foundation"
|
||||
+++
|
6
content/community/directory/timluc-miptev.md
Normal file
6
content/community/directory/timluc-miptev.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
name = "~timluc-miptev"
|
||||
patp = "~timluc-miptev"
|
||||
org = "Additional Community Members"
|
||||
title = "CEO of Uqbar"
|
||||
+++
|
6
content/community/directory/wicdev-wisryt.md
Normal file
6
content/community/directory/wicdev-wisryt.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
name = "Philip Monk"
|
||||
patp = "~wicdev-wisryt"
|
||||
org = "Tlon"
|
||||
title = "CTO, Urbit Foundation"
|
||||
+++
|
6
content/community/directory/wolref-podlex.md
Normal file
6
content/community/directory/wolref-podlex.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
name = "Josh Lehman"
|
||||
patp = "~wolref-podlex"
|
||||
org = "Urbit Foundation"
|
||||
title = "Executive Director, Urbit Foundation"
|
||||
+++
|
@ -0,0 +1,23 @@
|
||||
+++
|
||||
title = "Developer Call: Urbit’s 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 Tlon’s ~palfun-foslup will join Neal Davis to discuss Urbit’s 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.
|
@ -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).
|
18
content/community/support.md
Normal file
18
content/community/support.md
Normal file
@ -0,0 +1,18 @@
|
||||
+++
|
||||
title = "Support"
|
||||
+++
|
||||
|
||||
### Email
|
||||
|
||||
Questions? Email [support@urbit.org](mailto:support@urbit.org) and we’ll 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
36
content/courses/asl.md
Normal 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
34
content/courses/hsl.md
Normal 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
3
content/guides/_index.md
Normal file
@ -0,0 +1,3 @@
|
||||
+++
|
||||
title = "Guides"
|
||||
+++
|
5
content/guides/additional/_index.md
Normal file
5
content/guides/additional/_index.md
Normal file
@ -0,0 +1,5 @@
|
||||
+++
|
||||
title = "Additional Guides"
|
||||
weight = 30
|
||||
type = "tab"
|
||||
+++
|
79
content/guides/additional/aqua.md
Normal file
79
content/guides/additional/aqua.md
Normal 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`.
|
439
content/guides/additional/cli-tutorial.md
Normal file
439
content/guides/additional/cli-tutorial.md
Normal 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`.
|
697
content/guides/additional/http-api-guide.md
Normal file
697
content/guides/additional/http-api-guide.md
Normal 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
|
512
content/guides/additional/json-guide.md
Normal file
512
content/guides/additional/json-guide.md
Normal 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`.
|
708
content/guides/additional/parsing.md
Normal file
708
content/guides/additional/parsing.md
Normal 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`.
|
565
content/guides/additional/sail.md
Normal file
565
content/guides/additional/sail.md
Normal 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
|
||||
|
||||
It’s 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 don’t 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!
|
||||
; We’re on the web.
|
||||
==
|
||||
```
|
||||
- ```
|
||||
<body>
|
||||
<h1>Welcome!</h1>
|
||||
Hello, world!
|
||||
We’re 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=""><script src="example.com/xxx.js"><h1>FOO</h1></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
|
281
content/guides/additional/software-distribution.md
Normal file
281
content/guides/additional/software-distribution.md
Normal 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)
|
530
content/guides/additional/strings.md
Normal file
530
content/guides/additional/strings.md
Normal 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{%bar} r={%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.
|
114
content/guides/additional/unit-tests.md
Normal file
114
content/guides/additional/unit-tests.md
Normal 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.
|
5
content/guides/core/_index.md
Normal file
5
content/guides/core/_index.md
Normal file
@ -0,0 +1,5 @@
|
||||
+++
|
||||
title = "Core Curriculum"
|
||||
weight = 2
|
||||
type = "tab"
|
||||
+++
|
87
content/guides/core/app-school-full-stack/1-intro.md
Normal file
87
content/guides/core/app-school-full-stack/1-intro.md
Normal 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.
|
117
content/guides/core/app-school-full-stack/10-final.md
Normal file
117
content/guides/core/app-school-full-stack/10-final.md
Normal 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.
|
214
content/guides/core/app-school-full-stack/2-types.md
Normal file
214
content/guides/core/app-school-full-stack/2-types.md
Normal 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.
|
337
content/guides/core/app-school-full-stack/3-agent.md
Normal file
337
content/guides/core/app-school-full-stack/3-agent.md
Normal 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.
|
356
content/guides/core/app-school-full-stack/4-json.md
Normal file
356
content/guides/core/app-school-full-stack/4-json.md
Normal 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.
|
96
content/guides/core/app-school-full-stack/5-marks.md
Normal file
96
content/guides/core/app-school-full-stack/5-marks.md
Normal 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.
|
78
content/guides/core/app-school-full-stack/6-eyre.md
Normal file
78
content/guides/core/app-school-full-stack/6-eyre.md
Normal 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`).
|
213
content/guides/core/app-school-full-stack/7-react-setup.md
Normal file
213
content/guides/core/app-school-full-stack/7-react-setup.md
Normal 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.
|
405
content/guides/core/app-school-full-stack/8-app-logic.md
Normal file
405
content/guides/core/app-school-full-stack/8-app-logic.md
Normal 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.
|
225
content/guides/core/app-school-full-stack/9-desk.md
Normal file
225
content/guides/core/app-school-full-stack/9-desk.md
Normal 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.
|
51
content/guides/core/app-school-full-stack/_index.md
Normal file
51
content/guides/core/app-school-full-stack/_index.md
Normal 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.
|
304
content/guides/core/app-school/1-arvo.md
Normal file
304
content/guides/core/app-school/1-arvo.md
Normal 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.
|
362
content/guides/core/app-school/10-scry.md
Normal file
362
content/guides/core/app-school/10-scry.md
Normal 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.
|
145
content/guides/core/app-school/11-fail.md
Normal file
145
content/guides/core/app-school/11-fail.md
Normal 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.
|
49
content/guides/core/app-school/12-next-steps.md
Normal file
49
content/guides/core/app-school/12-next-steps.md
Normal 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/).
|
304
content/guides/core/app-school/2-agent.md
Normal file
304
content/guides/core/app-school/2-agent.md
Normal 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.
|
283
content/guides/core/app-school/3-imports-and-aliases.md
Normal file
283
content/guides/core/app-school/3-imports-and-aliases.md
Normal 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.
|
498
content/guides/core/app-school/4-lifecycle.md
Normal file
498
content/guides/core/app-school/4-lifecycle.md
Normal 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.
|
243
content/guides/core/app-school/5-cards.md
Normal file
243
content/guides/core/app-school/5-cards.md
Normal 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.
|
537
content/guides/core/app-school/6-pokes.md
Normal file
537
content/guides/core/app-school/6-pokes.md
Normal 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.
|
407
content/guides/core/app-school/7-sur-and-marks.md
Normal file
407
content/guides/core/app-school/7-sur-and-marks.md
Normal 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.
|
910
content/guides/core/app-school/8-subscriptions.md
Normal file
910
content/guides/core/app-school/8-subscriptions.md
Normal 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.
|
283
content/guides/core/app-school/9-vanes.md
Normal file
283
content/guides/core/app-school/9-vanes.md
Normal 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).
|
44
content/guides/core/app-school/_index.md
Normal file
44
content/guides/core/app-school/_index.md
Normal 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.
|
82
content/guides/core/app-school/intro.md
Normal file
82
content/guides/core/app-school/intro.md
Normal 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.
|
227
content/guides/core/app-school/types.md
Normal file
227
content/guides/core/app-school/types.md
Normal 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.
|
330
content/guides/core/environment.md
Normal file
330
content/guides/core/environment.md
Normal 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.
|
127
content/guides/core/hoon-school/A-intro.md
Normal file
127
content/guides/core/hoon-school/A-intro.md
Normal 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.
|
553
content/guides/core/hoon-school/B-syntax.md
Normal file
553
content/guides/core/hoon-school/B-syntax.md
Normal 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 doesn’t 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 language’s code.
|
||||
|
||||
In this lesson we will give a general overview of Hoon’s 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 can’t 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, they’re 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 you’ll see an abundance of terminators, such as opening and closing parentheses, and this way of doing this is largely absent from Urbit. That’s 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
Loading…
Reference in New Issue
Block a user