mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 07:01:27 -07:00
Initial commit
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { httpStatus } from 'http-codex/status'
|
||||
|
||||
import type { Context, Next } from 'hono'
|
||||
import type { StatusCode } from 'hono/utils/http-status'
|
||||
import type { HonoApp } from '../types'
|
||||
|
||||
/** Caches status: 200 responses for given ttl */
|
||||
export function withCache<T extends HonoApp>(ttl: number) {
|
||||
return async (ctx: Context<T>, next: Next): Promise<Response | void> => {
|
||||
const c = ctx as unknown as Context<HonoApp>
|
||||
|
||||
const cache = await caches.open('default')
|
||||
const reqMatcher = new Request(c.req.url, { method: c.req.method })
|
||||
const cachedRes = await cache.match(reqMatcher)
|
||||
if (cachedRes) {
|
||||
return c.newResponse(cachedRes.body, cachedRes)
|
||||
}
|
||||
await next()
|
||||
|
||||
if (c.res.status === httpStatus.OK) {
|
||||
const clonedRes = c.res.clone()
|
||||
clonedRes.headers.set('Cloudflare-CDN-Cache-Control', `max-age=${ttl}`)
|
||||
c.executionCtx.waitUntil(cache.put(reqMatcher, clonedRes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Caches using default CF Cache behavior */
|
||||
export function withCacheDefault<T extends HonoApp>(ttl: number) {
|
||||
return async (ctx: Context<T>, next: Next): Promise<Response | void> => {
|
||||
const c = ctx as unknown as Context<HonoApp>
|
||||
|
||||
const cache = await caches.open('default')
|
||||
const cachedRes = await cache.match(c.req.raw)
|
||||
if (cachedRes) {
|
||||
return c.newResponse(cachedRes.body, cachedRes)
|
||||
}
|
||||
await next()
|
||||
|
||||
if (c.res.status === httpStatus.OK) {
|
||||
const clonedRes = c.res.clone()
|
||||
clonedRes.headers.set('Cloudflare-CDN-Cache-Control', `max-age=${ttl}`)
|
||||
c.executionCtx.waitUntil(cache.put(c.req.raw, clonedRes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CacheByStatus {
|
||||
status: StatusCode
|
||||
/** Time in milliseconds to cache this status */
|
||||
ttl: number
|
||||
}
|
||||
|
||||
interface WithCacheByStatusOptions {
|
||||
rules: CacheByStatus[]
|
||||
/** Force caching rather than using default CF cache behavior */
|
||||
force: boolean
|
||||
}
|
||||
/** Caches responses based on status */
|
||||
export function withCacheByStatus<T extends HonoApp>(options: WithCacheByStatusOptions) {
|
||||
return async (ctx: Context<T>, next: Next): Promise<Response | void> => {
|
||||
const c = ctx as unknown as Context<HonoApp>
|
||||
|
||||
const cache = await caches.open('default')
|
||||
const reqMatcher = options.force ? new Request(c.req.url, { method: c.req.method }) : c.req.raw
|
||||
const cachedRes = await cache.match(reqMatcher)
|
||||
if (cachedRes) {
|
||||
return c.newResponse(cachedRes.body, cachedRes)
|
||||
}
|
||||
await next()
|
||||
const opts = options.rules.find((o) => o.status === c.res.status)
|
||||
if (opts) {
|
||||
const clonedRes = c.res.clone()
|
||||
clonedRes.headers.set('Cloudflare-CDN-Cache-Control', `max-age=${opts.ttl}`)
|
||||
c.executionCtx.waitUntil(cache.put(reqMatcher, clonedRes))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { cors } from 'hono/cors'
|
||||
|
||||
/** Default CORS handler */
|
||||
export function withDefaultCors() {
|
||||
return cors({
|
||||
origin: '*',
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { httpStatus } from 'http-codex/status'
|
||||
|
||||
import type { Context } from 'hono'
|
||||
import type { APIError } from '../helpers/errors'
|
||||
import type { HonoApp } from '../types'
|
||||
|
||||
/** Handles typical notFound hooks */
|
||||
export function withNotFound<T extends HonoApp>() {
|
||||
return async (ctx: Context<T>): Promise<Response> => {
|
||||
const c = ctx as unknown as Context<HonoApp>
|
||||
|
||||
return c.json(notFoundResponse, httpStatus.NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
export const notFoundResponse = {
|
||||
success: false,
|
||||
error: { message: 'not found' },
|
||||
} satisfies APIError
|
||||
@@ -0,0 +1,40 @@
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import { httpStatus } from 'http-codex/status'
|
||||
|
||||
import { logger } from '../helpers/logger'
|
||||
|
||||
import type { Context } from 'hono'
|
||||
import type { ContentfulStatusCode } from 'hono/utils/http-status'
|
||||
import type { APIError } from '../helpers/errors'
|
||||
import type { HonoApp } from '../types'
|
||||
|
||||
/** Handles typical onError hooks */
|
||||
export function withOnError<T extends HonoApp>() {
|
||||
return async (err: Error, ctx: Context<T>): Promise<Response> => {
|
||||
const c = ctx as unknown as Context<HonoApp>
|
||||
|
||||
if (err instanceof HTTPException) {
|
||||
const status = err.getResponse().status as ContentfulStatusCode
|
||||
const body: APIError = { success: false, error: { message: err.message } }
|
||||
if (status >= 500) {
|
||||
// TODO: Capture to Sentry
|
||||
// Log to Sentry
|
||||
logger.error(err)
|
||||
} else if (status === httpStatus.Unauthorized) {
|
||||
body.error.message = 'unauthorized'
|
||||
}
|
||||
|
||||
return c.json(body, status)
|
||||
}
|
||||
|
||||
// TODO: Capture to Sentry
|
||||
logger.error(err)
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'internal server error' },
|
||||
} satisfies APIError,
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user