Skip to content

Commit 700dd51

Browse files
committed
add util for handling remix headers generally
1 parent 7367c3d commit 700dd51

File tree

6 files changed

+155
-12
lines changed

6 files changed

+155
-12
lines changed

app/root.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { getEnv } from './utils/env.server.ts'
4747
import { honeypot } from './utils/honeypot.server.ts'
4848
import { combineHeaders, getDomainUrl, getUserImgSrc } from './utils/misc.tsx'
4949
import { useNonce } from './utils/nonce-provider.ts'
50+
import { pipeHeaders } from './utils/remix.server.ts'
5051
import { type Theme, getTheme } from './utils/theme.server.ts'
5152
import { makeTimings, time } from './utils/timing.server.ts'
5253
import { getToast } from './utils/toast.server.ts'
@@ -143,12 +144,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
143144
)
144145
}
145146

146-
export const headers: HeadersFunction = ({ loaderHeaders }) => {
147-
const headers = {
148-
'Server-Timing': loaderHeaders.get('Server-Timing') ?? '',
149-
}
150-
return headers
151-
}
147+
export const headers: HeadersFunction = pipeHeaders
152148

153149
function Document({
154150
children,

app/routes/settings+/profile.connections.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
providerNames,
2121
} from '#app/utils/connections.tsx'
2222
import { prisma } from '#app/utils/db.server.ts'
23+
import { pipeHeaders } from '#app/utils/remix.server.js'
2324
import { makeTimings } from '#app/utils/timing.server.ts'
2425
import { createToastHeaders } from '#app/utils/toast.server.ts'
2526
import { type Info, type Route } from './+types/profile.connections.ts'
@@ -84,12 +85,7 @@ export async function loader({ request }: Route.LoaderArgs) {
8485
)
8586
}
8687

87-
export const headers: Route.HeadersFunction = ({ loaderHeaders }) => {
88-
const headers = {
89-
'Server-Timing': loaderHeaders.get('Server-Timing') ?? '',
90-
}
91-
return headers
92-
}
88+
export const headers: Route.HeadersFunction = pipeHeaders
9389

9490
export async function action({ request }: Route.ActionArgs) {
9591
const userId = await requireUserId(request)

app/utils/remix.server.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { format, parse } from '@tusbar/cache-control'
2+
import { describe, expect, test } from 'vitest'
3+
import { getConservativeCacheControl } from './remix.server.ts'
4+
5+
describe('getConservativeCacheControl', () => {
6+
test('works for basic usecase', () => {
7+
const result = getConservativeCacheControl(
8+
'max-age=3600',
9+
'max-age=1800, s-maxage=600',
10+
'private, max-age=86400',
11+
)
12+
13+
expect(result).toEqual(
14+
format({
15+
maxAge: 1800,
16+
sharedMaxAge: 600,
17+
private: true,
18+
}),
19+
)
20+
})
21+
test('retains boolean directive', () => {
22+
const result = parse(
23+
getConservativeCacheControl('private', 'no-cache,no-store'),
24+
)
25+
26+
expect(result.private).toEqual(true)
27+
expect(result.noCache).toEqual(true)
28+
expect(result.noStore).toEqual(true)
29+
})
30+
test('gets smallest number directive', () => {
31+
const result = parse(
32+
getConservativeCacheControl(
33+
'max-age=10, s-maxage=300',
34+
'max-age=300, s-maxage=600',
35+
),
36+
)
37+
38+
expect(result.maxAge).toEqual(10)
39+
expect(result.sharedMaxAge).toEqual(300)
40+
})
41+
})

app/utils/remix.server.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { type CacheControlValue, parse, format } from '@tusbar/cache-control'
2+
import { type HeadersArgs } from 'react-router'
3+
4+
export function pipeHeaders({
5+
parentHeaders,
6+
loaderHeaders,
7+
actionHeaders,
8+
errorHeaders,
9+
}: HeadersArgs) {
10+
const headers = new Headers()
11+
12+
// get the one that's actually in use
13+
let currentHeaders: Headers
14+
if (errorHeaders !== undefined) {
15+
currentHeaders = errorHeaders
16+
} else if (loaderHeaders.entries().next().done) {
17+
currentHeaders = actionHeaders
18+
} else {
19+
currentHeaders = loaderHeaders
20+
}
21+
22+
// take in useful headers route loader/action
23+
// pass this point currentHeaders can be ignored
24+
const forwardHeaders = ['Cache-Control', 'Vary', 'Server-Timing']
25+
for (const headerName of forwardHeaders) {
26+
const header = currentHeaders.get(headerName)
27+
if (header) {
28+
headers.set(headerName, header)
29+
}
30+
}
31+
32+
headers.set(
33+
'Cache-Control',
34+
getConservativeCacheControl(
35+
parentHeaders.get('Cache-Control'),
36+
headers.get('Cache-Control'),
37+
),
38+
)
39+
40+
// append useful parent headers
41+
const inheritHeaders = ['Vary', 'Server-Timing']
42+
for (const headerName of inheritHeaders) {
43+
const header = parentHeaders.get(headerName)
44+
if (header) {
45+
headers.append(headerName, header)
46+
}
47+
}
48+
49+
// fallback to parent headers if loader don't have
50+
const fallbackHeaders = ['Cache-Control', 'Vary']
51+
for (const headerName of fallbackHeaders) {
52+
if (headers.has(headerName)) {
53+
continue
54+
}
55+
const fallbackHeader = parentHeaders.get(headerName)
56+
if (fallbackHeader) {
57+
headers.set(headerName, fallbackHeader)
58+
}
59+
}
60+
61+
return headers
62+
}
63+
64+
export function getConservativeCacheControl(
65+
...cacheControlHeaders: Array<string | null>
66+
): string {
67+
return format(
68+
cacheControlHeaders
69+
.filter(Boolean)
70+
.map((header) => parse(header))
71+
.reduce<CacheControlValue>((acc, current) => {
72+
for (const key in current) {
73+
const directive = key as keyof Required<CacheControlValue> // keyof CacheControl includes functions
74+
75+
const currentValue = current[directive]
76+
77+
switch (typeof currentValue) {
78+
case 'boolean': {
79+
if (currentValue) {
80+
acc[directive] = true as any
81+
}
82+
83+
break
84+
}
85+
case 'number': {
86+
const accValue = acc[directive] as number | undefined
87+
88+
if (accValue === undefined) {
89+
acc[directive] = currentValue as any
90+
} else {
91+
const result = Math.min(accValue, currentValue)
92+
acc[directive] = result as any
93+
}
94+
95+
break
96+
}
97+
}
98+
}
99+
100+
return acc
101+
}, {}),
102+
)
103+
}

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"@sentry/node": "^8.47.0",
7070
"@sentry/profiling-node": "^8.47.0",
7171
"@sentry/react": "^8.47.0",
72+
"@tusbar/cache-control": "1.0.2",
7273
"address": "^2.0.3",
7374
"bcryptjs": "^2.4.3",
7475
"better-sqlite3": "^11.7.0",

0 commit comments

Comments
 (0)