Volter World

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 supplies an executable walkthrough for packages/cli/src/journeys/tutorials.test.ts.

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.

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

The app declares the credential it reads. The World issues a throwaway token for its GitHub account, world; that account is separate from the platform org acme. The seed creates world/web through Octokit before any issue is written. It can run again without creating a second repository.

.env.example
GITHUB_TOKEN=
.volter/seed.ts
import { Octokit } from '@octokit/rest';
const github = new Octokit({ auth: process.env.GITHUB_TOKEN });
const { data: { login: owner } } = await github.users.getAuthenticated();
try {
  await github.repos.get({ owner, repo: 'web' });
} catch (error) {
  if (error.status !== 404) throw error;
  await github.repos.createForAuthenticatedUser({ name: 'web' });
}
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: 'world', repo: 'web', state: 'all' });
for (const issue of issues) console.log(`#${issue.number} ${issue.title}`);
retitle.mjs
import { Octokit } from '@octokit/rest';
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
await octokit.issues.update({ owner: 'world', repo: 'web', issue_number: 1, title: process.argv[2] });
console.log('retitled #1');
../reality/.env.example
GITHUB_TOKEN=
../reality/.volter/seed.ts
import '../../acme-web/.volter/seed.ts';
../team/.env.example
GITHUB_TOKEN=
../team/.volter/seed.ts
import '../../acme-web/.volter/seed.ts';
root-credential.mjs
import { readFileSync } from 'node:fs';
const worldToken = readFileSync('../reality/.volter/token', 'utf8').trim();
const response = await fetch('http://127.0.0.1:4400/acme/reality/github/_twin/app-credentials', {
  method: 'POST', headers: { 'x-twins-key': worldToken, 'content-type': 'application/json' }, body: '{}',
});
if (!response.ok) throw new Error(`The local GitHub account did not issue a token (${response.status})`);
const { token } = await response.json();
if (!token) throw new Error('The local GitHub account returned no token');
console.log(process.argv.includes('--token') ? token : JSON.stringify({ headers: {
  'x-twins-key': worldToken, authorization: `Bearer ${token}`,
} }));
npm install
npm install -g @volter/world
npm install -D @volter/twin-github
cd ../reality && volter world init --bare acme/reality --twins github
volter world up
volter world down
volter world serve --port 4400 &
cd ../team && volter world init --bare acme/team --twins github
volter world up
volter world down
volter world serve --port 4300 &
cd ../acme-web

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

serving  acme/reality  http://127.0.0.1:4400/acme/reality
serving  acme/team  http://127.0.0.1:4300/acme/team
for i in $(seq 1 60); do [ -f ../reality/.volter/token ] && [ -f ../team/.volter/token ] && break; sleep 1; done; sleep 3
cd ../reality
export ROOT_GITHUB_TOKEN=$(volter world run -- node ../acme-web/root-credential.mjs --token)
cd ../acme-web
curl -s -X POST http://127.0.0.1:4400/acme/reality/github/repos/world/web/issues -H "x-twins-key: $(cat ../reality/.volter/token)" -H "authorization: Bearer $ROOT_GITHUB_TOKEN" -H 'content-type: application/json' -d '{"title":"Launch checklist"}' | grep -o '"number":[0-9]*'
"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).

cd ../team
volter twin github root http://127.0.0.1:4400/acme/reality/github --scope repos/world/web --deploy hold --at-most 30s
volter world run -- node ../acme-web/root-credential.mjs | volter twin github credential
volter twin github refresh
volter twin github refresh
cd ../acme-web
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.

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
#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.

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

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

serving  acme/reality  http://127.0.0.1:4400/acme/reality
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/world/web/issues/1 -H "x-twins-key: $(cat ../reality/.volter/token)" -H "authorization: Bearer $ROOT_GITHUB_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
"title":"Launch checklist, final"
github  refreshed  1 changed
retitled #1
changeset  retitle-the-checklist  1 change
refused  retitle-the-checklist  origin moved on github; fetch and rebase before pushing

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.

volter world fetch
volter world rebase retitle-the-checklist
volter world push
conflicts:
github 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.

cd ../team
volter world deploy
volter world log --receipts
cd ../acme-web
curl -s http://127.0.0.1:4400/acme/reality/github/repos/world/web/issues/1 -H "x-twins-key: $(cat ../reality/.volter/token)" -H "authorization: Bearer $ROOT_GITHUB_TOKEN" | grep -o '"title":"[^"]*"'
deployed  github  1 change
"title":"Launch checklist, draft"

Clean up

volter world down
kill $(jobs -p)
View Markdown source

On this page