flotilla/e2e/harness/seed/scenario.ts

155 lines
5 KiB
TypeScript
Raw Normal View History

2026-08-06 15:43:57 +00:00
import {int, now} from "@welshman/lib"
import type {MaybeAsync} from "@welshman/lib"
import {ROOMS, makeEvent} from "@welshman/util"
import type {SignedEvent} from "@welshman/util"
import type {Zooid} from "../zooid/relay"
2026-09-10 03:28:02 +00:00
import type {OpenRelayName, SpaceName} from "../zooid/config"
2026-08-06 15:43:57 +00:00
import {users} from "../keys"
import type {TestUser} from "../keys"
import {seedSpace} from "./space"
import type {SeededSpace} from "./space"
2026-09-10 03:28:02 +00:00
import {seedOpenRelay} from "./openRelay"
import type {SeededOpenRelay} from "./openRelay"
2026-08-06 15:43:57 +00:00
// A fixture timestamp, as an offset from the moment the scenario started. `at(2, HOUR)` is two
// hours before the test began, count-first like int and ago.
2026-08-06 15:43:57 +00:00
export type At = (count: number, unit: number) => number
export type SeedTools = {
2026-09-10 03:28:02 +00:00
// Names a space the container already serves. Its policy is its toml in zooid/docker/config.
relay: (name: SpaceName) => SeededSpace
// Names one of the public relays, which is where the follow graph lives: `indexer` is what a
// pubkey's own lists are resolved from, `outbox` is a followed pubkey's write relay. See
// ARCHITECTURE.md, "The follow graph".
open: (name: OpenRelayName) => SeededOpenRelay
2026-08-06 15:43:57 +00:00
user: typeof users
at: At
}
export type Scenario = {
readonly startedAt: number
readonly at: At
2026-09-10 03:28:02 +00:00
// The spaces this scenario seeded, which are the relays the app is handed as its own.
2026-08-06 15:43:57 +00:00
readonly urls: string[]
2026-09-10 03:28:02 +00:00
// What a pubkey's own lists are resolved from: the open indexer when a scenario declared one,
// and the spaces otherwise, which is what a scenario that knows nothing about open relays gets.
readonly indexerUrls: string[]
space(name: SpaceName): SeededSpace
open(name: OpenRelayName): SeededOpenRelay
// The events a returning user's client would already have on disk: their room list, and their
// relay list when the scenario seeded one. A relay won't serve the list that would tell
// authPolicy it may identify to it. See ARCHITECTURE.md, "Users and sessions".
2026-08-06 15:43:57 +00:00
cache(user: TestUser): SignedEvent[]
}
export const seed = async (
zooid: Zooid,
build: (tools: SeedTools) => MaybeAsync<void>,
): Promise<Scenario> => {
const startedAt = now()
const at: At = (count, unit) => startedAt - int(count, unit)
const spaces = new Map<string, SeededSpace>()
2026-09-10 03:28:02 +00:00
const opened: SeededOpenRelay[] = []
2026-08-06 15:43:57 +00:00
const writes: (() => Promise<void>)[] = []
const roomLists = new Map<string, SignedEvent>()
2026-09-10 03:28:02 +00:00
const relayLists = new Map<string, SignedEvent>()
2026-08-06 15:43:57 +00:00
2026-09-10 03:28:02 +00:00
const enqueue = (write: () => Promise<void>) => writes.push(write)
const relay = (name: SpaceName) => {
const space = seedSpace({zooid, startedAt, name, enqueue})
2026-08-06 15:43:57 +00:00
spaces.set(name, space)
return space
}
2026-09-10 03:28:02 +00:00
const open = (name: OpenRelayName) => {
const relay = seedOpenRelay({zooid, startedAt, name, enqueue})
opened.push(relay)
return relay
}
await build({relay, open, user: users, at})
2026-08-06 15:43:57 +00:00
// Seeding is async and fixtures depend on one another, so the builder only records what to
// write. Draining the queue here publishes each fixture in the order it was declared.
for (const write of writes) {
await write()
}
const seeded = Array.from(spaces.values())
// A room list is replaceable and covers every space at once, so it can only be written after
// all of them have been seeded.
const membershipsByPubkey = new Map<string, {user: TestUser; urls: string[]; tags: string[][]}>()
for (const space of seeded) {
for (const {user, rooms} of space.memberships) {
const membership = membershipsByPubkey.get(user.pubkey) ?? {user, urls: [], tags: []}
membership.urls.push(space.url)
membership.tags.push(["r", space.url], ...rooms.map(h => ["group", h, space.url]))
membershipsByPubkey.set(user.pubkey, membership)
}
}
for (const {user, urls, tags} of membershipsByPubkey.values()) {
const event = await user.signer.sign(makeEvent(ROOMS, {tags, created_at: startedAt}))
for (const url of urls) {
await zooid.publish(url, event)
}
roomLists.set(user.pubkey, event)
}
2026-09-10 03:28:02 +00:00
for (const relay of opened) {
for (const {event} of relay.relayLists) {
relayLists.set(event.pubkey, event)
}
}
const getSpace = (name: SpaceName) => {
2026-08-06 15:43:57 +00:00
const space = spaces.get(name)
if (space) return space
throw new Error(`No space named "${name}" was seeded`)
}
2026-09-10 03:28:02 +00:00
const getOpenRelay = (name: OpenRelayName) => {
const relay = opened.find(candidate => candidate.name === name)
if (relay) return relay
throw new Error(`No open relay named "${name}" was seeded`)
}
2026-08-06 15:43:57 +00:00
const cache = (user: TestUser) => {
2026-09-10 03:28:02 +00:00
const events: SignedEvent[] = []
2026-08-06 15:43:57 +00:00
const roomList = roomLists.get(user.pubkey)
2026-09-10 03:28:02 +00:00
const relayList = relayLists.get(user.pubkey)
if (roomList) events.push(roomList)
if (relayList) events.push(relayList)
2026-08-06 15:43:57 +00:00
2026-09-10 03:28:02 +00:00
return events
2026-08-06 15:43:57 +00:00
}
2026-09-10 03:28:02 +00:00
const urls = seeded.map(space => space.url)
const indexers = opened.filter(relay => relay.name === "indexer").map(relay => relay.url)
2026-08-06 15:43:57 +00:00
return {
startedAt,
at,
cache,
2026-09-10 03:28:02 +00:00
urls,
indexerUrls: indexers.length > 0 ? indexers : urls,
2026-08-06 15:43:57 +00:00
space: getSpace,
2026-09-10 03:28:02 +00:00
open: getOpenRelay,
2026-08-06 15:43:57 +00:00
}
}