Skip to content

Commit d59aacd

Browse files
committed
add util for handling remix headers generally
1 parent 95982ee commit d59aacd

File tree

6 files changed

+154
-12
lines changed

6 files changed

+154
-12
lines changed

app/root.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { getEnv } from './utils/env.server.ts'
4949
import { honeypot } from './utils/honeypot.server.ts'
5050
import { combineHeaders, getDomainUrl, getUserImgSrc } from './utils/misc.tsx'
5151
import { useNonce } from './utils/nonce-provider.ts'
52+
import { pipeHeaders } from './utils/remix.server.ts'
5253
import { type Theme, getTheme } from './utils/theme.server.ts'
5354
import { makeTimings, time } from './utils/timing.server.ts'
5455
import { getToast } from './utils/toast.server.ts'
@@ -145,12 +146,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
145146
)
146147
}
147148

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

155151
function Document({
156152
children,

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { prisma } from '#app/utils/db.server.ts'
3030
import { makeTimings } from '#app/utils/timing.server.ts'
3131
import { createToastHeaders } from '#app/utils/toast.server.ts'
3232
import { type BreadcrumbHandle } from './profile.tsx'
33+
import { pipeHeaders } from '#app/utils/remix.server.js'
3334

3435
export const handle: BreadcrumbHandle & SEOHandle = {
3536
breadcrumb: <Icon name="link-2">Connections</Icon>,
@@ -90,12 +91,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
9091
)
9192
}
9293

93-
export const headers: HeadersFunction = ({ loaderHeaders }) => {
94-
const headers = {
95-
'Server-Timing': loaderHeaders.get('Server-Timing') ?? '',
96-
}
97-
return headers
98-
}
94+
export const headers: HeadersFunction = pipeHeaders
9995

10096
export async function action({ request }: ActionFunctionArgs) {
10197
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: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { type HeadersArgs } from '@remix-run/node'
2+
import { type CacheControlValue, parse, format } from '@tusbar/cache-control'
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 (let directive in current) {
73+
const currentValue = current[directive]
74+
75+
// ts-expect-error because typescript doesn't know it's the same directive.
76+
switch (typeof currentValue) {
77+
case 'boolean': {
78+
if (currentValue) {
79+
acc[directive] = true
80+
}
81+
82+
break
83+
}
84+
case 'number': {
85+
const accValue = acc[directive] as number | undefined
86+
87+
if (accValue === undefined) {
88+
acc[directive] = currentValue
89+
} else {
90+
const result = Math.min(accValue, currentValue)
91+
acc[directive] = result
92+
}
93+
94+
break
95+
}
96+
}
97+
}
98+
99+
return acc
100+
}, {}),
101+
)
102+
}

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
@@ -66,6 +66,7 @@
6666
"@remix-run/react": "2.13.1",
6767
"@sentry/profiling-node": "^8.35.0",
6868
"@sentry/remix": "^8.35.0",
69+
"@tusbar/cache-control": "1.0.2",
6970
"address": "^2.0.3",
7071
"bcryptjs": "^2.4.3",
7172
"better-sqlite3": "^11.4.0",

0 commit comments

Comments
 (0)