chadtmiller.com/knotless

Git hosted in your atproto account, on any stock PDS, fast enough for agents.

git clone knotless://chadtmiller.com/knotless

needs the knotless git helper. set it up

main
108 lines · 4.6 KB Raw History
// The screenshots without a PDS: what knotless-serve answered for each page,
// recorded once in fixtures.json, and played back by a stand-in for it in
// front of the pages. The pages are drawn as they are by the real app, from
// web/build; only their data is fixed.
//
//   node harness/shots/fixtures.mjs    record them again, from the world
//                                      `just shots-up` keeps up, or a new
//                                      one: `just shots-record`

import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { createServer, request } from 'node:http'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { base, create, pages, port, root, saved, serve, until } from './world.mjs'

export const fixtures = join(root, 'harness/shots/fixtures.json')

/// What knotless-serve answers itself, rather than handing to the pages.
const answered = (path) => /^\/(_data|xrpc)\//.test(path) || /^\/[^/]+\/[^/]+\/raw\//.test(path)

/// The headers of an answer worth keeping.
const KEPT = ['content-type', 'cache-control', 'location', 'content-security-policy']

/// knotless-serve's place on `port`, in front of the pages: answering from
/// `recorded`, or, given `from`, asking knotless-serve there and recording
/// what it says. Returns the recording, and how to stop.
export async function front({ recorded = {}, from } = {}) {
  const drawn = pages(port + 1, base)
  const answer = async (path) => {
    if (!from) return recorded[path]
    const res = await fetch(from + path, { redirect: 'manual' })
    const body = Buffer.from(await res.arrayBuffer())
    const text = body.toString('utf8')
    const binary = !Buffer.from(text, 'utf8').equals(body)
    const headers = Object.fromEntries(KEPT.filter((h) => res.headers.has(h)).map((h) => [h, res.headers.get(h)]))
    return (recorded[path] = { status: res.status, headers, ...(binary ? { base64: body.toString('base64') } : { body: text }) })
  }
  const server = createServer(async (req, res) => {
    if (answered(req.url)) {
      const a = await answer(req.url).catch(() => null)
      if (!a) {
        res.writeHead(404, { 'content-type': 'application/json' })
        res.end(JSON.stringify({ message: `nothing recorded for ${req.url}` }))
        return
      }
      res.writeHead(a.status, a.headers)
      res.end(a.base64 === undefined ? a.body : Buffer.from(a.base64, 'base64'))
      return
    }
    const headers = { ...req.headers, 'x-forwarded-host': req.headers.host, 'x-forwarded-proto': 'http' }
    const on = request({ host: '127.0.0.1', port: port + 1, path: req.url, method: req.method, headers }, (up) => {
      res.writeHead(up.statusCode, up.headers)
      up.pipe(res)
    })
    on.on('error', (e) => {
      res.writeHead(502)
      res.end(`The pages are not being drawn: ${e.message}`)
    })
    req.pipe(on)
  })
  await new Promise((r) => server.listen(port, '127.0.0.1', r))
  const kill = () => {
    server.close()
    drawn.kill()
  }
  try {
    await until('the pages', async () => (await fetch(`${base}/start`)).ok)
  } catch (e) {
    kill()
    throw e
  }
  return { recorded, kill }
}

/// The recording, if there is one.
export const recording = () => (existsSync(fixtures) ? JSON.parse(readFileSync(fixtures, 'utf8')) : null)

/// Records every page's answers from `world`, and writes them with the pages.
async function record(world) {
  const at = port + 2
  const server = await serve(world, { at, alone: true })
  const stand = await front({ from: `http://127.0.0.1:${at}` })
  try {
    for (const path of Object.values(world.views)) {
      const html = await (await fetch(base + path)).text()
      // What the browser asks for itself: images, from the raw files.
      for (const [, src] of html.matchAll(/src="(\/[^"]+\/raw\/[^"]+)"/g)) await fetch(base + src)
    }
  } finally {
    stand.kill()
    server.kill()
  }
  const sorted = Object.fromEntries(Object.entries(stand.recorded).sort(([a], [b]) => (a < b ? -1 : 1)))
  writeFileSync(fixtures, JSON.stringify({ views: world.views, answers: sorted }, null, 1) + '\n')
  return Object.keys(sorted).length
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const kept = existsSync(saved) ? JSON.parse(readFileSync(saved, 'utf8')) : null
  const up = kept && (await fetch(`${kept.net.pds}/xrpc/com.atproto.server.describeServer`).then((r) => r.ok, () => false))
  const world = up ? kept : await create()
  // A new world's server is stopped first: the recording starts its own.
  if (!up) world.server.kill()
  const n = await record(world)
  if (!up) world.stop()
  console.log(`recorded ${n} answers for ${Object.keys(world.views).length} pages in ${fixtures}`)
  process.exit(0)
}