chrome / app.mjs
aripbae's picture
Update app.mjs
518ff8a verified
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { prettyJSON } from 'hono/pretty-json'
import os from 'node:os'
import { format } from 'node:util'
import { isMainThread, parentPort, Worker } from 'node:worker_threads'
import playwright from 'playwright-extra'
import prettyBytes from 'pretty-bytes'
import prettyMs from 'pretty-ms'
import pluginStealth from 'puppeteer-extra-plugin-stealth'
const TIMEOUT_MS = 6e4
const MAX_CODE_LENGTH = 6e4
if (!isMainThread) {
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor
parentPort.on('message', async (code) => {
try {
const fn = new AsyncFunction(
'playwright',
'pluginStealth',
'console',
code
)
parentPort.postMessage({
result: await fn(playwright, pluginStealth, console)
})
} catch (e) {
parentPort.postMessage({ error: format(e) })
}
})
} else {
const isNumber = (v) => typeof v === 'number' && !isNaN(v)
const transformObj = (obj, cb) =>
JSON.parse(
JSON.stringify(obj, (key, val) =>
cb(key, val) ? prettyBytes(val) : val
).replace(/_(\w)/g, (_, g) => g.toUpperCase())
)
const getServerStats = () => {
const stats = {}
stats.uptime = prettyMs(process.uptime() * 1e3)
stats.osUptime = prettyMs(os.uptime() * 1e3)
const report = process.report?.getReport?.()
if (report)
Object.assign(stats, {
header: report.header,
javascriptHeap: transformObj(
report.javascriptHeap,
(k, v) => !/(ContextCount|Garbage)/i.test(k) && isNumber(v)
),
resourceUsage: transformObj(
report.resourceUsage,
(k, v) =>
!/(Percent|IO|reads|write)/i.test(k) && isNumber(v)
),
uvthreadResourceUsage: transformObj(
report.uvthreadResourceUsage,
(k, v) => isNumber(v)
)
})
stats.memoryUsage = transformObj(process.memoryUsage(), (k, v) =>
isNumber(v)
)
return stats
}
const runBrowserScript = (code) =>
new Promise((resolve, reject) => {
const worker = new Worker(new URL(import.meta.url), {
resourceLimits: { maxOldGenerationSizeMb: 512 }
})
const timer = setTimeout(() => {
worker.terminate()
reject(new Error('Execution timeout'))
}, TIMEOUT_MS)
const cleanup = () => {
clearTimeout(timer)
worker.terminate()
}
worker.postMessage(code)
worker.on('message', ({ result, error }) => {
cleanup()
error ? reject(new Error(error)) : resolve(format(result))
})
worker.on('error', (e) => {
cleanup()
reject(e)
})
worker.on('exit', (exitCode) => {
clearTimeout(timer)
exitCode !== 0 &&
reject(new Error(`Worker exited with code ${exitCode}`))
})
})
const app = new Hono()
app.use(logger())
app.use(prettyJSON({ force: true }))
app.use(
'/file/*',
serveStatic({
root: os.tmpdir(),
rewriteRequestPath: (path) => path.replace(/^\/file/, '')
})
)
app.get('/', (c) => c.json(getServerStats()))
app.post('/run', async (c) => {
const body = await c.req.json().catch(() => ({}))
const { code } = body
if (!code) return c.json({ error: 'Code is required' }, 400)
if (typeof code !== 'string')
return c.json({ error: 'Code must be a string' }, 400)
if (code.length > MAX_CODE_LENGTH)
return c.json({ error: 'Code too long' }, 400)
try {
return c.json({ result: await runBrowserScript(code) })
} catch (e) {
return c.json({ error: format(e) }, 500)
}
})
const port = process.env.SPACE_ID ? 7860 : +(process.env.PORT || 3000)
serve({ fetch: app.fetch, port }, console.log)
}