-
Notifications
You must be signed in to change notification settings - Fork 169
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(tooling): add API proxy code and move worker to PL account (#1237)
- Loading branch information
1 parent
4ae1e90
commit 1a7c4c7
Showing
7 changed files
with
318 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,4 +7,6 @@ package-lock.json | |
node_modules | ||
resources | ||
static/_gen | ||
data/toc.json | ||
data/toc.json | ||
api/dist | ||
api/worker |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
## Usage | ||
|
||
Install Cloudflare Wrangler | ||
|
||
```bash | ||
wrangler login # to get the token | ||
|
||
wrangler publish # to publish the worker to specs-api.protocol-labs.workers.dev | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
const Router = require('./router') | ||
const dlv = require('dlv') | ||
const merge = require('merge-options') | ||
const nanoid = require('nanoid/non-secure') | ||
|
||
const cacheBust = nanoid.nanoid() | ||
/** | ||
* Example of how router can be used in an application | ||
* */ | ||
addEventListener('fetch', (event) => { | ||
event.respondWith(handleRequest(event)) | ||
}) | ||
|
||
async function handleRequest(event) { | ||
const r = new Router() | ||
// Replace with the appropriate paths and handlers | ||
r.get('.*/cov', () => cov(event)) | ||
r.get('.*/github', () => github(event)) | ||
r.get('/', () => new Response('Hello worker!')) // return a default message for the root route | ||
|
||
try { | ||
return await r.route(event.request) | ||
} catch (err) { | ||
console.log('handleRequest -> err', err.stack) | ||
return new Response(err.message, { | ||
status: 500, | ||
statusText: 'internal server error', | ||
headers: { | ||
'content-type': 'text/plain', | ||
}, | ||
}) | ||
} | ||
} | ||
|
||
async function cov(event) { | ||
const url = new URL(event.request.url) | ||
// https://github.com/filecoin-project/lotus | ||
const repo = url.searchParams.get('repo').split('/').slice(3).join('/') | ||
const data = await get(event, { | ||
url: `https://codecov.io/api/gh/${repo}`, | ||
transform: (data) => { | ||
const out = { | ||
cov: dlv(data, 'commit.totals.c', 0), | ||
ci: dlv(data, 'commit.ci_passed', false), | ||
repo: dlv(data, 'repo.name', 'N/A'), | ||
org: dlv(data, 'owner.username', 'N/A'), | ||
lang: dlv(data, 'repo.language', 'N/A'), | ||
} | ||
return out | ||
}, | ||
}) | ||
return data | ||
} | ||
|
||
async function github(event) { | ||
const url = new URL(event.request.url) | ||
const file = url.searchParams.get('file').split('/') | ||
// https://github.com/filecoin-project/lotus/blob/master/paychmgr/paych.go | ||
const repo = file.slice(3, 5).join('/') | ||
const path = file.slice(7).join('/') | ||
const ref = file[6] | ||
const headers = { | ||
'User-Agent': 'hugomrdias', | ||
// Authorization: `token ${GITHUB_TOKEN}` | ||
} | ||
|
||
const treeUrlRsp = await get(event, { | ||
url: `https://api.github.com/repos/${repo}/commits?sha=${ref}&per_page=1&page=1`, | ||
headers, | ||
}) | ||
const treeUrl = await treeUrlRsp.json() | ||
|
||
const data = await get(event, { | ||
url: `https://api.github.com/repos/${repo}/contents/${path}?ref=${ref}`, | ||
transform: (data) => { | ||
return { | ||
content: data.content, | ||
size: data.size, | ||
url: `https://github.com/${repo}/tree/${treeUrl[0].sha}/${path}`, | ||
} | ||
}, | ||
headers, | ||
}) | ||
return data | ||
} | ||
|
||
async function get(event, options) { | ||
const { url, transform, force, headers } = merge( | ||
{ | ||
url: '', | ||
transform: (d) => d, | ||
force: false, | ||
headers: {}, | ||
}, | ||
options | ||
) | ||
|
||
const cache = caches.default | ||
const cacheKey = url + cacheBust | ||
const cacheTTL = 86400 * 2 // 2 days | ||
const cacheRevalidateTTL = 3600 * 2 // 2 hours | ||
const cachedResponse = await cache.match(cacheKey) | ||
|
||
if (force || !cachedResponse) { | ||
console.log('Cache miss for ', cacheKey) | ||
// if not in cache get from the origin | ||
const response = await fetch(url, { | ||
headers: { | ||
...headers, | ||
'If-None-Match': cachedResponse | ||
? cachedResponse.headers.get('ETag') | ||
: null, | ||
}, | ||
}) | ||
|
||
if (response.ok) { | ||
const { headers } = response | ||
const contentType = headers.get('content-type') || '' | ||
|
||
if (contentType.includes('application/json')) { | ||
// transform the data | ||
const data = transform(await response.json()) | ||
|
||
// build new response with the transformed body | ||
const transformedResponse = new Response(JSON.stringify(data), { | ||
headers: { | ||
'Content-Type': 'application/json;charset=UTF-8', | ||
'Cache-Control': `max-age=${cacheTTL}`, | ||
'X-RateLimit-Limit': headers.get('X-RateLimit-Limit'), | ||
'X-RateLimit-Remaining': headers.get('X-RateLimit-Remaining'), | ||
'X-RateLimit-Reset': headers.get('X-RateLimit-Reset'), | ||
ETag: headers.get('ETag'), | ||
}, | ||
}) | ||
|
||
// save response to cache | ||
event.waitUntil(cache.put(cacheKey, transformedResponse.clone())) | ||
|
||
return transformedResponse | ||
} else { | ||
throw new Error( | ||
`Request error content type not supported. ${contentType}` | ||
) | ||
} | ||
} else if (response.status === 304) { | ||
// renew cache response | ||
event.waitUntil(cache.put(cacheKey, cachedResponse.clone())) | ||
return cachedResponse.clone() | ||
} else { | ||
return response | ||
} | ||
} else { | ||
console.log('Cache hit for ', cacheKey, cachedResponse.headers.get('age')) | ||
const cacheAge = cachedResponse.headers.get('age') | ||
|
||
if (cacheAge > cacheRevalidateTTL) { | ||
console.log('Cache is too old, revalidating...') | ||
event.waitUntil(get(event, { url, transform, force: true })) | ||
} | ||
return cachedResponse | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"name": "specs-api", | ||
"version": "1.0.0", | ||
"description": "spec proxy api", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
}, | ||
"author": "Hugo Dias <hugomrdias@gmail.com>", | ||
"license": "MIT", | ||
"dependencies": { | ||
"dlv": "^1.1.3", | ||
"merge-options": "^3.0.3", | ||
"nanoid": "^3.1.16" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
/** | ||
* Helper functions that when passed a request will return a | ||
* boolean indicating if the request uses that HTTP method, | ||
* header, host or referrer. | ||
*/ | ||
const Method = (method) => (req) => | ||
req.method.toLowerCase() === method.toLowerCase() | ||
const Connect = Method('connect') | ||
const Delete = Method('delete') | ||
const Get = Method('get') | ||
const Head = Method('head') | ||
const Options = Method('options') | ||
const Patch = Method('patch') | ||
const Post = Method('post') | ||
const Put = Method('put') | ||
const Trace = Method('trace') | ||
|
||
const Header = (header, val) => (req) => req.headers.get(header) === val | ||
const Host = (host) => Header('host', host.toLowerCase()) | ||
const Referrer = (host) => Header('referrer', host.toLowerCase()) | ||
|
||
const Path = (regExp) => (req) => { | ||
const url = new URL(req.url) | ||
const path = url.pathname | ||
const match = path.match(regExp) || [] | ||
return match[0] === path | ||
} | ||
|
||
/** | ||
* The Router handles determines which handler is matched given the | ||
* conditions present for each request. | ||
*/ | ||
class Router { | ||
constructor() { | ||
this.routes = [] | ||
} | ||
|
||
handle(conditions, handler) { | ||
this.routes.push({ | ||
conditions, | ||
handler, | ||
}) | ||
return this | ||
} | ||
|
||
connect(url, handler) { | ||
return this.handle([Connect, Path(url)], handler) | ||
} | ||
|
||
delete(url, handler) { | ||
return this.handle([Delete, Path(url)], handler) | ||
} | ||
|
||
get(url, handler) { | ||
return this.handle([Get, Path(url)], handler) | ||
} | ||
|
||
head(url, handler) { | ||
return this.handle([Head, Path(url)], handler) | ||
} | ||
|
||
options(url, handler) { | ||
return this.handle([Options, Path(url)], handler) | ||
} | ||
|
||
patch(url, handler) { | ||
return this.handle([Patch, Path(url)], handler) | ||
} | ||
|
||
post(url, handler) { | ||
return this.handle([Post, Path(url)], handler) | ||
} | ||
|
||
put(url, handler) { | ||
return this.handle([Put, Path(url)], handler) | ||
} | ||
|
||
trace(url, handler) { | ||
return this.handle([Trace, Path(url)], handler) | ||
} | ||
|
||
all(handler) { | ||
return this.handle([], handler) | ||
} | ||
|
||
route(req) { | ||
const route = this.resolve(req) | ||
|
||
if (route) { | ||
return route.handler(req) | ||
} | ||
|
||
return new Response('resource not found', { | ||
status: 404, | ||
statusText: 'not found', | ||
headers: { | ||
'content-type': 'text/plain', | ||
}, | ||
}) | ||
} | ||
|
||
/** | ||
* resolve returns the matching route for a request that returns | ||
* true for all conditions (if any). | ||
*/ | ||
resolve(req) { | ||
return this.routes.find((r) => { | ||
if (!r.conditions || (Array.isArray(r) && !r.conditions.length)) { | ||
return true | ||
} | ||
|
||
if (typeof r.conditions === 'function') { | ||
return r.conditions(req) | ||
} | ||
|
||
return r.conditions.every((c) => c(req)) | ||
}) | ||
} | ||
} | ||
|
||
module.exports = Router |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
name = "specs-api" | ||
type = "webpack" | ||
account_id = "fffa4b4363a7e5250af8357087263b3a" | ||
workers_dev = true | ||
route = "" | ||
zone_id = "" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters