fly-node
fly-node copied to clipboard
Remix replayable
function getReplayResponse(request: Request) {
if (isPrimaryRegion) return null
const pathname = new URL(request.url).pathname
const logInfo = {
pathname,
method: request.method,
PRIMARY_REGION,
FLY_REGION,
}
console.info(`Replaying:`, logInfo)
return redirect(pathname, {
status: 302,
headers: {'fly-replay': `region=${PRIMARY_REGION}`},
})
}
function getReplayResponseForError(request: Request, errorMessage?: string) {
// depending on how the error is serialized, there may be quotes and escape
// characters in the error message, so we'll use a regex instead of a regular includes.
const isReadOnlyError = /SqlState\(.*?25006.*?\)/.test(errorMessage ?? '')
return isReadOnlyError ? getReplayResponse(request) : null
}
async function getDocumentReplayResponse(
request: Request,
remixContext: EntryContext,
) {
return getReplayResponseForError(
request,
remixContext.componentDidCatchEmulator.error?.message,
)
}
async function getDataReplayResponse(request: Request, response: Response) {
if (response.status > 199 && response.status < 300) return null
const textClone = response.clone()
const text = await textClone.text().catch(() => null)
return getReplayResponseForError(request, text ?? '')
}
Patch on remix to make this work:
diff --git a/node_modules/@remix-run/server-runtime/server.js b/node_modules/@remix-run/server-runtime/server.js
index 7289bcc..76ba6ca 100644
--- a/node_modules/@remix-run/server-runtime/server.js
+++ b/node_modules/@remix-run/server-runtime/server.js
@@ -66,7 +66,7 @@ async function handleDataRequest(request, loadContext, build, platform, routes)
var _platform$formatServe;
let formattedError = (await ((_platform$formatServe = platform.formatServerError) === null || _platform$formatServe === void 0 ? void 0 : _platform$formatServe.call(platform, error))) || error;
- return responses.json(await errors.serializeError(formattedError), {
+ response = responses.json(await errors.serializeError(formattedError), {
status: 500,
headers: {
"X-Remix-Error": "unfortunately, yes"
@@ -74,6 +74,19 @@ async function handleDataRequest(request, loadContext, build, platform, routes)
});
}
+ if (build.entry.module.handleDataRequest) {
+ try {
+ response = await build.entry.module.handleDataRequest(clonedRequest, response, loadContext, routeMatch.params);
+ } catch (error) {
+ return responses.json(errors.serializeError(error), {
+ status: 500,
+ headers: {
+ "X-Data-Request-Handler-Error": "Handle your errors, yo."
+ }
+ });
+ }
+ }
+
if (isRedirectResponse(response)) {
// We don't have any way to prevent a fetch request from following
// redirects. So we use the `X-Remix-Redirect` header to indicate the
This is entry.server.ts:
// This isn't supported yet. We're using patch-package to make this work:
// https://github.com/remix-run/remix/issues/217
export async function handleDataRequest(
request: Request,
dataResponse: Response,
) {
const replayResponse = await getDataReplayResponse(request, dataResponse)
if (replayResponse) {
return replayResponse
}
// TODO: remove this when we go to production
dataResponse.headers.set('X-Robots-Tag', 'none')
dataResponse.headers.set('X-Powered-By', 'Kody the Koala')
dataResponse.headers.set('X-Fly-Region', process.env.FLY_REGION ?? 'unknown')
return dataResponse
}
https://npm.im/patch-package
This is my redis client stuff:
import redis from 'redis'
import {getRequiredServerEnvVar} from './misc'
declare global {
// This prevents us from making multiple connections to the db when the
// require cache is cleared.
// eslint-disable-next-line
var replicaClient: redis.RedisClient | undefined,
primaryClient: redis.RedisClient | undefined
}
const REDIS_URL = getRequiredServerEnvVar('REDIS_URL')
const replica = new URL(REDIS_URL)
const isLocalHost = replica.hostname === 'localhost'
const isInternal = replica.hostname.includes('.internal')
const isMultiRegion = !isLocalHost && isInternal
const PRIMARY_REGION = isMultiRegion
? getRequiredServerEnvVar('PRIMARY_REGION')
: null
const FLY_REGION = isMultiRegion ? getRequiredServerEnvVar('FLY_REGION') : null
if (FLY_REGION) {
replica.host = `${FLY_REGION}.${replica.host}`
}
const replicaClient = createClient('replicaClient', {
url: replica.toString(),
family: isInternal ? 'IPv6' : 'IPv4',
})
let primaryClient: redis.RedisClient | null = null
if (FLY_REGION !== PRIMARY_REGION) {
const primary = new URL(REDIS_URL)
if (!isLocalHost) {
primary.host = `${PRIMARY_REGION}.${primary.host}`
}
primaryClient = createClient('primaryClient', {
url: primary.toString(),
family: isInternal ? 'IPv6' : 'IPv4',
})
}
function createClient(
name: 'replicaClient' | 'primaryClient',
options: redis.ClientOpts,
): redis.RedisClient {
let client = global[name]
if (!client) {
const url = new URL(options.url ?? 'http://no-redis-url.example.com?weird')
// eslint-disable-next-line no-multi-assign
client = global[name] = redis.createClient(options)
client.on('error', (error: string) => {
console.error(`REDIS ${name} (${url.host}) ERROR:`, error)
})
}
return client
}
// NOTE: Caching should never crash the app, so instead of rejecting all these
// promises, we'll just resolve things with null and log the error.
function get<Value = unknown>(key: string): Promise<Value | null> {
return new Promise(resolve => {
replicaClient.get(key, (err: Error | null, result: string | null) => {
if (err) {
console.error(
`REDIS replicaClient (${FLY_REGION}) ERROR with .get:`,
err,
)
}
resolve(result ? (JSON.parse(result) as Value) : null)
})
})
}
function set<Value>(key: string, value: Value): Promise<'OK'> {
return new Promise(resolve => {
replicaClient.set(
key,
JSON.stringify(value),
(err: Error | null, reply: 'OK') => {
if (err)
console.error(
`REDIS replicaClient (${FLY_REGION}) ERROR with .set:`,
err,
)
resolve(reply)
},
)
})
}
function del(key: string): Promise<string> {
return new Promise(resolve => {
// fire and forget on primary, we only care about replica
primaryClient?.del(key, (err: Error | null) => {
if (err) {
console.error('Primary delete error', err)
}
})
replicaClient.del(key, (err: Error | null, result: number | null) => {
if (err) {
console.error(
`REDIS replicaClient (${FLY_REGION}) ERROR with .del:`,
err,
)
resolve('error')
} else {
resolve(`${key} deleted: ${result}`)
}
})
})
}
const redisCache = {get, set, del}
export {get, set, del, redisCache}
All of it is here: https://github.com/kentcdodds/remix-kentcdodds
Here's what I have at the top of my prisma file that might be useful:
import {PrismaClient} from '@prisma/client'
import type {EntryContext} from 'remix'
import {redirect} from 'remix'
import chalk from 'chalk'
import type {User, Session} from '~/types'
import {encrypt, decrypt} from './encryption.server'
import {getRequiredServerEnvVar} from './misc'
declare global {
// This prevents us from making multiple connections to the db when the
// require cache is cleared.
// eslint-disable-next-line
var prisma: ReturnType<typeof getClient> | undefined
}
const DATABASE_URL = getRequiredServerEnvVar('DATABASE_URL')
const regionalDB = new URL(DATABASE_URL)
const isLocalHost = regionalDB.hostname === 'localhost'
const PRIMARY_REGION = isLocalHost
? null
: getRequiredServerEnvVar('PRIMARY_REGION')
const FLY_REGION = isLocalHost ? null : getRequiredServerEnvVar('FLY_REGION')
const isPrimaryRegion = PRIMARY_REGION === FLY_REGION
if (!isLocalHost) {
regionalDB.host = `${FLY_REGION}.${regionalDB.host}`
if (!isPrimaryRegion) {
// 5433 is the read-replica port
regionalDB.port = '5433'
}
}
const logThreshold = 15
const prisma = getClient(() => {
// NOTE: during development if you change anything in this function, remember
// that this only runs once per server restart and won't automatically be
// re-run per request like everything else is.
console.log(`Connecting to ${regionalDB.host}`)
const client = new PrismaClient({
log: [
{level: 'query', emit: 'event'},
{level: 'error', emit: 'stdout'},
{level: 'info', emit: 'stdout'},
{level: 'warn', emit: 'stdout'},
],
datasources: {
db: {
url: regionalDB.toString(),
},
},
})
client.$on('query', e => {
if (e.duration < logThreshold) return
const color =
e.duration < 15
? 'green'
: e.duration < 20
? 'blue'
: e.duration < 35
? 'yellow'
: e.duration < 50
? 'redBright'
: 'red'
const dur = chalk[color](`${e.duration}ms`)
console.log(`prisma:query - ${dur} - ${e.query}`)
})
return client
})
function getClient(createClient: () => PrismaClient): PrismaClient {
let client = global.prisma
if (!client) {
// eslint-disable-next-line no-multi-assign
client = global.prisma = createClient()
}
return client
}
const isProd = process.env.NODE_ENV === 'production'
if (!isProd && DATABASE_URL && !DATABASE_URL.includes('localhost')) {
// if we're connected to a non-localhost db, let's make
// sure we know it.
const domain = new URL(DATABASE_URL)
if (domain.password) {
domain.password = '**************'
}
console.warn(
`
⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
Connected to non-localhost DB in dev mode:
${domain.toString()}
⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
`.trim(),
)
}