# Read the vendor through a shared world

Keep a copy of the account in the team's world: refreshed on demand or on a schedule, read by
every clone without a vendor call, and rebased when the vendor moves.

This page is executed as written by `packages/cli/src/journeys/tutorials.test.ts`; the recording
is made from the same run.

![The page, recorded](../media/read-the-vendor-through-a-shared-world/read-the-vendor-through-a-shared-world.gif)

A root's log is the account's history as observed. A **refresh** asks the vendor what it holds
and appends to that log only what changed; the twin says how often the vendor can be asked, and
the world can say otherwise. Everything downstream reads the held copy: a clone answers from its
tree, so a rate-limited API is read as often as you like. When the vendor moves under a
changeset, `rebase` names the conflict by record and field.

## Three worlds

GitHub itself is a world here, so the whole chain runs on one machine; the commands are the same
when the root is `https://api.github.com`.

```json file=package.json
{ "name": "acme-web", "private": true, "type": "module", "dependencies": { "@octokit/rest": "^21" } }
```

```js file=list-issues.mjs
import { Octokit } from '@octokit/rest';
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const { data: issues } = await octokit.issues.listForRepo({ owner: 'acme', repo: 'web', state: 'all' });
for (const issue of issues) console.log(`#${issue.number} ${issue.title}`);
```

```js file=retitle.mjs
import { Octokit } from '@octokit/rest';
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
await octokit.issues.update({ owner: 'acme', repo: 'web', issue_number: 1, title: process.argv[2] });
console.log('retitled #1');
```

```bash
npm install
npm install -g @volter/world
npm install -D @volter/twin-github
mkdir ../reality && cd ../reality && volter world init --bare acme/reality --twins github
volter world serve --port 4400 &
mkdir ../team && cd ../team && volter world init --bare acme/team --twins github
volter world serve --port 4300 &
cd ../acme-web
```

Wait for both worlds to announce that they are serving before continuing.

```text
serving  acme/reality  http://127.0.0.1:4400/acme/reality
serving  acme/team  http://127.0.0.1:4300/acme/team
```

```bash
for i in $(seq 1 60); do [ -f ../reality/.volter/token ] && [ -f ../team/.volter/token ] && break; sleep 1; done; sleep 3
curl -s -X POST http://127.0.0.1:4400/acme/reality/github/repos/acme/web/issues -H "authorization: Bearer $(cat ../reality/.volter/token)" -H 'content-type: application/json' -d '{"title":"Launch checklist"}' | grep -o '"number":[0-9]*'
```

```text
"number":1
```

## Set the root, and how often it may be asked

`--deploy hold` keeps landed entries waiting until someone deploys; `--at-most 30s` is this
world's word on how often the twin may be refreshed on demand (the twin has its own default).

```bash
cd ../team
volter twin github root http://127.0.0.1:4400/acme/reality/github --scope repos/acme/web --deploy hold --at-most 30s
cat ../reality/.volter/token | volter twin github credential
volter twin github refresh
volter twin github refresh
cd ../acme-web
```

```text
github  refreshed
github  not refreshed: refreshed at ISO; the github twin refreshes at most every 30s (--force to refresh now)
```

## A clone reads the copy

The app's world clones the team's, and its tree holds what the team observed. Stop the vendor,
and the app still reads the issue: nothing here calls the vendor.

```bash
volter world init
volter world up --no-seed
volter world clone http://127.0.0.1:4300/acme/team --token "$(cat ../team/.volter/token)"
kill %1
volter world run -- node list-issues.mjs
```

```text
#1 Launch checklist
```

## When the vendor moves

Bring the vendor back and change the issue there. The team's world observes it on its next
refresh. Meanwhile the app changed the same title, cut a changeset and pushes: the push refuses,
because the team's world moved under it, and says what to do.

```bash
cd ../reality
volter world serve --port 4400 &
```

Wait for the restarted world to announce that it is serving:

```text
serving  acme/reality  http://127.0.0.1:4400/acme/reality
```

```bash
cd ../acme-web
for i in $(seq 1 60); do curl -s http://127.0.0.1:4400/-/ping >/dev/null && break; sleep 1; done; sleep 2
curl -s -X PATCH http://127.0.0.1:4400/acme/reality/github/repos/acme/web/issues/1 -H "authorization: Bearer $(cat ../reality/.volter/token)" -H 'content-type: application/json' -d '{"title":"Launch checklist, final"}' | grep -o '"title":"[^"]*"'
cd ../team
volter twin github refresh --force
cd ../acme-web
volter world run -- node retitle.mjs "Launch checklist, draft"
volter world changeset -m "Retitle the checklist"
volter world push
```

```text
"title":"Launch checklist, final"
github  refreshed  1 changed
retitled #1
changeset  retitle-the-checklist  1 change
origin moved: 1 change on github since your last fetch
```

`fetch` brings the moved base, and `rebase` replays the changeset over it, naming the record and
the field where the two disagree. The rebased changeset has a new hash; a reviewer sees the
conflict on it.

```bash
volter world fetch
volter world rebase retitle-the-checklist
volter world push
```

```text
conflicts:
github issue:acme/web#issue:1 title set
pushed  retitle-the-checklist  1 change → origin
```

The entry waits on the team's world: the root says `hold`. Someone deploys it, and the vendor
holds the app's title.

```bash
cd ../team
volter world deploy
volter world log --receipts
cd ../acme-web
curl -s http://127.0.0.1:4400/acme/reality/github/repos/acme/web/issues/1 -H "authorization: Bearer $(cat ../reality/.volter/token)" | grep -o '"title":"[^"]*"'
```

```text
deployed  github  1 change
"title":"Launch checklist, draft"
```

## Clean up

```bash
volter world down
kill $(jobs -p)
```

<!-- playback:BEGIN — GENERATED by `bun scripts/docs-media.ts`; do not edit between markers -->

## Playback

Each command above, as the recording shows it.

<details><summary><code>npm install</code></summary>

![step 1](../media/read-the-vendor-through-a-shared-world/step-01.png)

</details>
<details><summary><code>npm install -g @volter/world</code></summary>

![step 2](../media/read-the-vendor-through-a-shared-world/step-02.png)

</details>
<details><summary><code>npm install -D @volter/twin-github</code></summary>

![step 3](../media/read-the-vendor-through-a-shared-world/step-03.png)

</details>
<details><summary><code>mkdir ../reality &amp;&amp; cd ../reality &amp;&amp; volter world init --bare acme/reality --twins github</code></summary>

![step 4](../media/read-the-vendor-through-a-shared-world/step-04.png)

</details>
<details><summary><code>volter world serve --port 4400 &amp;</code></summary>

![step 5](../media/read-the-vendor-through-a-shared-world/step-05.png)

</details>
<details><summary><code>mkdir ../team &amp;&amp; cd ../team &amp;&amp; volter world init --bare acme/team --twins github</code></summary>

![step 6](../media/read-the-vendor-through-a-shared-world/step-06.png)

</details>
<details><summary><code>volter world serve --port 4300 &amp;</code></summary>

![step 7](../media/read-the-vendor-through-a-shared-world/step-07.png)

</details>
<details><summary><code>cd ../acme-web</code></summary>

![step 8](../media/read-the-vendor-through-a-shared-world/step-08.png)

</details>
<details><summary><code>for i in $(seq 1 60); do [ -f ../reality/.volter/token ] &amp;&amp; [ -f ../team/.volter/token ] &amp;&amp; break; sleep 1; done; sleep 3</code></summary>

![step 9](../media/read-the-vendor-through-a-shared-world/step-09.png)

</details>
<details><summary><code>curl -s -X POST http://127.0.0.1:4400/acme/reality/github/repos/acme/web/issues -H "authorization: Bearer $(cat ../reality/.volter/token)" -H 'content-type: application/json' -d '{"title":"Launch checklist"}' | grep -o '"number":[0-9]*'</code></summary>

![step 10](../media/read-the-vendor-through-a-shared-world/step-10.png)

</details>
<details><summary><code>cd ../team</code></summary>

![step 11](../media/read-the-vendor-through-a-shared-world/step-11.png)

</details>
<details><summary><code>volter twin github root http://127.0.0.1:4400/acme/reality/github --scope repos/acme/web --deploy hold --at-most 30s</code></summary>

![step 12](../media/read-the-vendor-through-a-shared-world/step-12.png)

</details>
<details><summary><code>cat ../reality/.volter/token | volter twin github credential</code></summary>

![step 13](../media/read-the-vendor-through-a-shared-world/step-13.png)

</details>
<details><summary><code>volter twin github refresh</code></summary>

![step 14](../media/read-the-vendor-through-a-shared-world/step-14.png)

</details>
<details><summary><code>volter twin github refresh</code></summary>

![step 15](../media/read-the-vendor-through-a-shared-world/step-15.png)

</details>
<details><summary><code>cd ../acme-web</code></summary>

![step 16](../media/read-the-vendor-through-a-shared-world/step-16.png)

</details>
<details><summary><code>volter world init</code></summary>

![step 17](../media/read-the-vendor-through-a-shared-world/step-17.png)

</details>
<details><summary><code>volter world up --no-seed</code></summary>

![step 18](../media/read-the-vendor-through-a-shared-world/step-18.png)

</details>
<details><summary><code>volter world clone http://127.0.0.1:4300/acme/team --token "$(cat ../team/.volter/token)"</code></summary>

![step 19](../media/read-the-vendor-through-a-shared-world/step-19.png)

</details>
<details><summary><code>kill %1</code></summary>

![step 20](../media/read-the-vendor-through-a-shared-world/step-20.png)

</details>
<details><summary><code>volter world run -- node list-issues.mjs</code></summary>

![step 21](../media/read-the-vendor-through-a-shared-world/step-21.png)

</details>
<details><summary><code>cd ../reality</code></summary>

![step 22](../media/read-the-vendor-through-a-shared-world/step-22.png)

</details>
<details><summary><code>volter world serve --port 4400 &amp;</code></summary>

![step 23](../media/read-the-vendor-through-a-shared-world/step-23.png)

</details>
<details><summary><code>cd ../acme-web</code></summary>

![step 24](../media/read-the-vendor-through-a-shared-world/step-24.png)

</details>
<details><summary><code>for i in $(seq 1 60); do curl -s http://127.0.0.1:4400/-/ping &gt;/dev/null &amp;&amp; break; sleep 1; done; sleep 2</code></summary>

![step 25](../media/read-the-vendor-through-a-shared-world/step-25.png)

</details>
<details><summary><code>curl -s -X PATCH http://127.0.0.1:4400/acme/reality/github/repos/acme/web/issues/1 -H "authorization: Bearer $(cat ../reality/.volter/token)" -H 'content-type: application/json' -d '{"title":"Launch checklist, final"}' | grep -o '"title":"[^"]*"'</code></summary>

![step 26](../media/read-the-vendor-through-a-shared-world/step-26.png)

</details>
<details><summary><code>cd ../team</code></summary>

![step 27](../media/read-the-vendor-through-a-shared-world/step-27.png)

</details>
<details><summary><code>volter twin github refresh --force</code></summary>

![step 28](../media/read-the-vendor-through-a-shared-world/step-28.png)

</details>
<details><summary><code>cd ../acme-web</code></summary>

![step 29](../media/read-the-vendor-through-a-shared-world/step-29.png)

</details>
<details><summary><code>volter world run -- node retitle.mjs "Launch checklist, draft"</code></summary>

![step 30](../media/read-the-vendor-through-a-shared-world/step-30.png)

</details>
<details><summary><code>volter world changeset -m "Retitle the checklist"</code></summary>

![step 31](../media/read-the-vendor-through-a-shared-world/step-31.png)

</details>
<details><summary><code>volter world push</code></summary>

![step 32](../media/read-the-vendor-through-a-shared-world/step-32.png)

</details>
<details><summary><code>volter world fetch</code></summary>

![step 33](../media/read-the-vendor-through-a-shared-world/step-33.png)

</details>
<details><summary><code>volter world rebase retitle-the-checklist</code></summary>

![step 34](../media/read-the-vendor-through-a-shared-world/step-34.png)

</details>
<details><summary><code>volter world push</code></summary>

![step 35](../media/read-the-vendor-through-a-shared-world/step-35.png)

</details>
<details><summary><code>cd ../team</code></summary>

![step 36](../media/read-the-vendor-through-a-shared-world/step-36.png)

</details>
<details><summary><code>volter world deploy</code></summary>

![step 37](../media/read-the-vendor-through-a-shared-world/step-37.png)

</details>
<details><summary><code>volter world log --receipts</code></summary>

![step 38](../media/read-the-vendor-through-a-shared-world/step-38.png)

</details>
<details><summary><code>cd ../acme-web</code></summary>

![step 39](../media/read-the-vendor-through-a-shared-world/step-39.png)

</details>
<details><summary><code>curl -s http://127.0.0.1:4400/acme/reality/github/repos/acme/web/issues/1 -H "authorization: Bearer $(cat ../reality/.volter/token)" | grep -o '"title":"[^"]*"'</code></summary>

![step 40](../media/read-the-vendor-through-a-shared-world/step-40.png)

</details>
<details><summary><code>volter world down</code></summary>

![step 41](../media/read-the-vendor-through-a-shared-world/step-41.png)

</details>
<details><summary><code>kill $(jobs -p)</code></summary>

![step 42](../media/read-the-vendor-through-a-shared-world/step-42.png)

</details>

<!-- playback:END -->
