Skip to content
This repository has been archived by the owner on Sep 14, 2023. It is now read-only.

Commit

Permalink
feat: capi.dev (#764)
Browse files Browse the repository at this point in the history
  • Loading branch information
tjjfvi authored Mar 21, 2023
1 parent f2d7d4a commit b34fd3a
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 42 deletions.
2 changes: 1 addition & 1 deletion main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
WssProvider,
ZombienetProvider,
} from "./providers/frame/mod.ts"
import { handler } from "./server/local.ts"
import { handler } from "./server/handler.ts"
import { Env } from "./server/mod.ts"
import { FsCache } from "./util/cache/mod.ts"

Expand Down
26 changes: 26 additions & 0 deletions server/capi.dev/delegatee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { serve } from "../../deps/std/http.ts"
import { WssProvider } from "../../providers/frame/wss.ts"
import { S3Cache } from "../../util/cache/mod.ts"
import { handler } from "../handler.ts"
import { Env } from "../mod.ts"

// this is only used by the dev providers, which should target the local server
const href = "http://localhost:4646/"

const controller = new AbortController()
const { signal } = controller

const cache = new S3Cache(Deno.env.get("DENO_DEPLOYMENT_ID")!, {
accessKeyID: Deno.env.get("S3_ACCESS_KEY")!,
secretKey: Deno.env.get("S3_SECRET_KEY")!,
region: Deno.env.get("S3_REGION")!,
bucket: Deno.env.get("S3_BUCKET")!,
}, signal)

const env = new Env(href, cache, signal, (env) => ({
frame: {
wss: new WssProvider(env),
},
}))

serve(handler(env))
97 changes: 97 additions & 0 deletions server/capi.dev/delegator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { serve } from "../../deps/std/http.ts"
import { TimedMemo } from "../../util/memo.ts"
import { f, handleErrors } from "../mod.ts"

serve(handleErrors(handler))

const ttl = 60_000
const shaAbbrevLength = 8

const rTagVersion = /^v(\d+\.\d+\.\d+(?:-.+)?)$/
const rPrVersion = /^pr:(\d+)$/
const rVersionedUrl = /^(?:\/@(.+?))?(\/.*)?$/

const delegateeProjectId = "70eddd08-c9b0-4cb3-b100-8c6facf52f1e"
const githubApiBase = "https://api.github.com/repos/paritytech/capi/"

const { signal } = new AbortController()

async function handler(request: Request) {
const url = new URL(request.url)
const [, version, path = "/"] = rVersionedUrl.exec(url.pathname)!
if (!version) return f.redirect(`/@${await defaultVersion()}${path}`)
const sha = await getSha(version)
let normalizedVersion
if (rTagVersion.test(version)) {
normalizedVersion = version.replace(rTagVersion, "v$1")
} else {
normalizedVersion = sha.slice(0, shaAbbrevLength)
}
if (version !== normalizedVersion) {
return f.redirect(`/@${normalizedVersion}${path}`)
}
const deploymentUrl = await getDeployment(sha)
return await fetch(new URL(path, deploymentUrl), {
method: request.method,
headers: request.headers,
body: request.body,
})
}

const defaultVersionMemo = new TimedMemo<void, string>(ttl, signal)
async function defaultVersion() {
return await defaultVersionMemo.run(undefined, async () => {
const release = await github<GithubRelease>(`releases/latest`).catch(async () =>
(await github<GithubRelease[]>(`releases`))[0]!
)
return release.tag_name
})
}

const shaMemo = new TimedMemo<string, string>(ttl, signal)
async function getSha(version: string): Promise<string> {
const ref = version
.replace(rPrVersion, "pull/$1/head")
.replace(rTagVersion, "v$1")
.replace(/:/g, "/")
return await shaMemo.run(
ref,
async () => (await github<GithubCommit>(`commits/${ref}`)).sha,
)
}

const deploymentMemo = new TimedMemo<string, string>(ttl, signal)
async function getDeployment(sha: string) {
return await deploymentMemo.run(sha, async () => {
const deployments = await github<GithubDeployment[]>(`deployments?sha=${sha}`)
const deployment = deployments.find((x) => x.payload.project_id === delegateeProjectId)
if (!deployment) throw f.notFound()
const statuses = await github<GithubStatus[]>(deployment.statuses_url)
const url = statuses.map((x) => x.environment_url).find((x) => x)
if (!url) throw f.notFound()
return url
})
}

async function github<T>(url: string): Promise<T> {
const response = await fetch(new URL(url, githubApiBase))
if (!response.ok) throw new Error(`${url}: invalid response`)
return await response.json()
}

interface GithubDeployment {
statuses_url: string
payload: { project_id: string }
}

interface GithubStatus {
environment_url?: string
}

interface GithubRelease {
tag_name: string
}

interface GithubCommit {
sha: string
}
2 changes: 1 addition & 1 deletion server/factories.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as shiki from "npm:shiki"
import * as shiki from "https://esm.sh/shiki@0.11.1?bundle"
import { escapeHtml } from "../deps/escape.ts"
import { Status } from "../deps/std/http.ts"
import { CacheBase } from "../util/cache/base.ts"
Expand Down
24 changes: 15 additions & 9 deletions server/local.ts → server/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as f from "./factories.ts"
import { parsePathInfo } from "./PathInfo.ts"

export function handler(env: Env): Handler {
return async (request) => {
return handleErrors(async (request) => {
const url = new URL(request.url)
const { pathname } = url
if (pathname === "/") return new Response("capi dev server active")
Expand All @@ -19,13 +19,7 @@ export function handler(env: Env): Handler {
}
const provider = env.providers[generatorId]?.[providerId]
if (provider) {
try {
return await provider.handle(request, pathInfo)
} catch (e) {
if (e instanceof Response) return e.clone()
console.error(e)
return f.serverError(Deno.inspect(e))
}
return await provider.handle(request, pathInfo)
}
}
for (const dir of staticDirs) {
Expand All @@ -40,7 +34,19 @@ export function handler(env: Env): Handler {
} catch (_e) {}
}
return f.notFound()
}
})
}

const staticDirs = ["../", "./static/"].map((p) => import.meta.resolve(p))

export function handleErrors(handler: (request: Request) => Promise<Response>) {
return async (request: Request) => {
try {
return await handler(request)
} catch (e) {
if (e instanceof Response) return e.clone()
console.error(e)
return f.serverError(Deno.inspect(e))
}
}
}
1 change: 1 addition & 0 deletions server/mod.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./Env.ts"
export * as f from "./factories.ts"
export * from "./handler.ts"
export * from "./PathInfo.ts"
export * from "./Provider.ts"
7 changes: 0 additions & 7 deletions util/cache/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,4 @@ export abstract class CacheBase {
return value
}, ttl)
}

abstract _list(prefix: string): Promise<string[]>

listMemo = new WeakMemo<string, string[]>()
list(prefix: string) {
return this.listMemo.run(prefix, () => this._list(prefix))
}
}
15 changes: 0 additions & 15 deletions util/cache/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,4 @@ export class FsCache extends CacheBase {
return content
}
}

async _list(prefix: string): Promise<string[]> {
try {
const result = []
for await (const entry of Deno.readDir(path.join(this.location, prefix))) {
result.push(entry.name)
}
return result
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return []
}
throw e
}
}
}
3 changes: 0 additions & 3 deletions util/cache/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,4 @@ export class InMemoryCache extends CacheBase {
_getRaw(key: string, init: () => Promise<Uint8Array>): Promise<Uint8Array> {
return Promise.resolve(this.memo.run(key, init))
}
_list(): Promise<string[]> {
throw new Error("unimplemented")
}
}
8 changes: 2 additions & 6 deletions util/cache/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { CacheBase } from "./base.ts"
export class S3Cache extends CacheBase {
bucket

constructor(config: S3BucketConfig, signal: AbortSignal) {
constructor(readonly prefix: string, config: S3BucketConfig, signal: AbortSignal) {
super(signal)
this.bucket = new S3Bucket(config)
}

async _getRaw(key: string, init: () => Promise<Uint8Array>) {
key = this.prefix + key
const result = await this.bucket.getObject(key)
if (result) {
return new Uint8Array(await new Response(result.body).arrayBuffer())
Expand All @@ -18,9 +19,4 @@ export class S3Cache extends CacheBase {
await this.bucket.putObject(key, value)
return value
}

async _list(prefix: string): Promise<string[]> {
const result = await this.bucket.listObjects({ prefix })
return result?.contents?.map((object) => object.key!.slice(prefix.length)) ?? []
}
}

0 comments on commit b34fd3a

Please sign in to comment.