~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs-socialweb-computer-app

app.socialweb.computer: socialweb.computer in one page, one atlibs composition

0
0
main
114 lines · 6.1 KB Raw History
// The page's logic, DOM-free: load the staged composition, run an instance's entry as the user (each
// instance held to its own module's OAuth scope), the webrtc transport's signals, and what the views show.
import { bindParameters, loadComposition, runCompose, staged } from '@publicdomainrelay/atlibs/compose/index.js'
import { policyCall } from '@publicdomainrelay/atlibs/oauth/index.js'
import { hostCall, sessionXrpc } from '@publicdomainrelay/atlibs/run-module/host-call.js'

// a run ends when it is done: vm_create waits minutes for the bid, the receipt, the guest
const BUDGET = { calls: Infinity, events: Infinity, wallMs: 1_800_000 }

/** stage.json (sessions, scopes) and the composition `atlibs stage --target spa` staged beside the page. */
export async function loadApp(ctx, base, fetchImpl = globalThis.fetch) {
	const json = async (name) => (await fetchImpl(new URL(name, base))).json()
	const [stage, manifest] = await Promise.all([json('stage.json'), json('composition.json')])
	const bound = bindParameters(manifest)
	const composition = await loadComposition(ctx, bound, { load: staged(base, fetchImpl) })
	return { stage, manifest: bound, composition }
}

/** A composition parameter's bound value (bindParameters wrote defaults into instance inputs). */
export const paramDefault = (manifest, key) => manifest.parameters?.find((p) => p.key === key)?.values?.default

/**
 * run(instance, entry, input) -> { result, error }: runCompose as the user. Each instance's calls go through
 * policyCall (its module's session); signed out, public reads only. vm: { attest, calls, fetchers }: the
 * attestation key and the webrtc transport (webrtc.connect, and webrtc: URLs for process-mcp's http.fetch and the
 * bidders' market endpoints).
 */
export function runner(ctx, { stage, composition }, session, vm = {}) {
	const host = hostCall({ ctx, canHeader: false, fetchers: vm.fetchers })
	const calls = session && Object.fromEntries(Object.keys(stage.sessions).map((name) => [name, hostCall({ ctx, xrpc: sessionXrpc(session), canHeader: false, attest: vm.attest, calls: vm.calls, fetchers: vm.fetchers })]))
	const callFor = (inst) => (session ? policyCall(stage, inst.module, calls) : host)
	return (instance, entry, input = {}, { signal, onEvent } = {}) =>
		runCompose(ctx, composition, instance, entry, { ...input, ...(session && { did: session.did }) }, { callFor, signal, onEvent, budget: BUDGET, http: { 'webrtc://*': {} }, fetchers: vm.fetchers })
}

/** As run, but the result or a thrown error. */
export const must = (run) => async (instance, entry, input, options) => {
	const ran = await run(instance, entry, input, options)
	if (ran.error !== undefined) throw new Error(`${instance} ${entry}: ${ran.error}`)
	return ran.result
}

/**
 * atlibs-webrtc recordSignals' repo for a session: reads from each repo's own PDS; the offer (a fresh TID
 * rkey) and its delete as the user.
 */
export function sessionRepo(ctx, session) {
	const read = hostCall({ ctx, canHeader: false })
	const xrpc = sessionXrpc(session)
	return {
		did: session.did,
		getRecord: ({ repo, collection, rkey }) => read('com.atproto.repo.getRecord', { params: { repo, collection, rkey }, service: repo }).catch((e) => (/RecordNotFound|not found|404/i.test(e.message) ? null : Promise.reject(e))),
		putRecord: ({ collection, rkey, record }) => xrpc('com.atproto.repo.createRecord', { input: { repo: session.did, collection, rkey, record } }),
		deleteRecord: ({ collection, rkey }) => xrpc('com.atproto.repo.deleteRecord', { input: { repo: session.did, collection, rkey } }),
	}
}

/** WebrtcHost's events and signal as requester (instance machine) runs: the guest's reports, the sealed offer. */
export const vmSignals = (run) => ({
	events: async ({ receipt, tag }) => {
		const bidder = receipt.bidder ?? receipt.uri.split('/')[2]
		return (await run('machine', 'watch', { bidder, receipt: receipt.uri, tags: [tag], max_polls: 1 })).events
	},
	signal: (input) => run('machine', 'signal', input),
})

/** atirc's channel for a handle or subhandle. */
export const channelOf = (name) => `#${name.slice(0, 63).replace(/[.,\s]/g, '_').toLowerCase()}`

/**
 * atlibs-atirc's guest for a VM you keep: its contract receipt (from the shell URL's ref, base64url JSON
 * {receipt: {uri, cid}, bidder}) and the requester's watch and signal (vmSignals) for its "atirc" tunnel.
 */
export function guestOf(shell, signals) {
	const ref = shell.split('/')[3] ?? ''
	const { receipt, bidder } = JSON.parse(atob(ref.replace(/-/g, '+').replace(/_/g, '/')))
	if (!receipt?.uri || !receipt?.cid) throw new Error(`no receipt in ${shell.slice(0, 40)}`)
	return { receipt: { ...receipt, ...(bidder && { bidder }) }, ...signals }
}

/** A heartbeat's age: live within 5 minutes. */
export function liveness(beatAt, now = Date.now()) {
	const t = Date.parse(beatAt ?? '')
	if (Number.isNaN(t)) return { live: false, age: 'no heartbeat' }
	const s = Math.max(0, Math.round((now - t) / 1000))
	const age = s < 120 ? `${s}s ago` : s < 7200 ? `${Math.round(s / 60)}m ago` : s < 172800 ? `${Math.round(s / 3600)}h ago` : `${Math.round(s / 86400)}d ago`
	return { live: s < 300, age }
}

/**
 * The Running view's bidder groups from market-status rows: yours (you, or an operator confirmed to be you)
 * and those of accounts you vouch for (the bidder, or its confirmed operator, vouched).
 */
export function groupBidders(rows, me, vouched) {
	const who = (r) => r.operator ?? r.did
	const has = (r) => r.offering || r.heartbeat
	return {
		mine: rows.filter((r) => has(r) && who(r) === me),
		vouched: rows.filter((r) => has(r) && who(r) !== me && vouched.has(who(r))),
	}
}

/** The DIDs whose bidders the Running view reads: the composition's, yours followed, you, and those you vouch for. */
export const bidderDids = (defaults, known, me, vouched) => [...new Set([...(defaults ?? []), ...known, me, ...vouched].filter((d) => d?.startsWith('did:')))].slice(0, 50)

/** Quick shell commands (the Shell view's buttons). */
export const QUICK = [
	['uname', 'uname -a'],
	['uptime', 'uptime'],
	['disk', 'df -h /'],
	['memory', 'free -m'],
	['agents', 'systemctl --no-pager --type=service list-units "atirc-*"'],
	['agent log', 'journalctl --no-pager -n 40 -u "atirc-agent-*"'],
]