Here's a copy/paste of some of my code:
import * as YAML from 'yaml'
import {markdownToHtmlUnwrapped} from './markdown.server'
import {cachified} from 'cachified'
import {downloadDirList, downloadFile} from './github.server'
import {typedBoolean} from './misc'
import type {Workshop} from '~/types'
import {cache, shouldForceFresh} from './cache.server'
type RawWorkshop = {
title?: string
description?: string
meta?: Record<string, unknown>
events?: Array<Omit<Workshop['events'][number], 'type'>>
convertKitTag?: string
categories?: Array<string>
problemStatements?: Workshop['problemStatementHTMLs']
keyTakeaways?: Workshop['keyTakeawayHTMLs']
topics?: Array<string>
prerequisite?: string
}
async function getWorkshops({
request,
forceFresh,
}: {
request?: Request
forceFresh?: boolean
}) {
const key = 'content:workshops'
return cachified({
cache,
key,
ttl: 1000 * 60 * 60 * 24 * 7,
forceFresh: forceFresh ?? (await shouldForceFresh({request, key})),
getFreshValue: async () => {
const dirList = await downloadDirList(`content/workshops`)
const workshopFileList = dirList
.filter(
listing => listing.type === 'file' && listing.name.endsWith('.yml'),
)
.map(listing => listing.name.replace(/\.yml$/, ''))
const workshops = await Promise.all(
workshopFileList.map(slug => getWorkshop(slug)),
)
return workshops.filter(typedBoolean)
},
checkValue: (value: unknown) => Array.isArray(value),
})
}
async function getWorkshop(slug: string): Promise<null | Workshop> {
const {default: pProps} = await import('p-props')
const rawWorkshopString = await downloadFile(
`content/workshops/${slug}.yml`,
).catch(() => null)
if (!rawWorkshopString) return null
let rawWorkshop
try {
rawWorkshop = YAML.parse(rawWorkshopString) as RawWorkshop
} catch (error: unknown) {
console.error(`Error parsing YAML`, error, rawWorkshopString)
return null
}
if (!rawWorkshop.title) {
console.error('Workshop has no title', rawWorkshop)
return null
}
const {
title,
convertKitTag,
description = 'This workshop is... indescribeable',
categories = [],
events = [],
topics,
meta = {},
} = rawWorkshop
if (!convertKitTag) {
throw new Error('All workshops must have a convertKitTag')
}
const [
problemStatementHTMLs,
keyTakeawayHTMLs,
topicHTMLs,
prerequisiteHTML,
] = await Promise.all([
rawWorkshop.problemStatements
? pProps({
part1: markdownToHtmlUnwrapped(rawWorkshop.problemStatements.part1),
part2: markdownToHtmlUnwrapped(rawWorkshop.problemStatements.part2),
part3: markdownToHtmlUnwrapped(rawWorkshop.problemStatements.part3),
part4: markdownToHtmlUnwrapped(rawWorkshop.problemStatements.part4),
})
: {part1: '', part2: '', part3: '', part4: ''},
Promise.all(
rawWorkshop.keyTakeaways?.map(keyTakeaway =>
pProps({
title: markdownToHtmlUnwrapped(keyTakeaway.title),
description: markdownToHtmlUnwrapped(keyTakeaway.description),
}),
) ?? [],
),
Promise.all(topics?.map(r => markdownToHtmlUnwrapped(r)) ?? []),
rawWorkshop.prerequisite
? markdownToHtmlUnwrapped(rawWorkshop.prerequisite)
: '',
])
return {
slug,
title,
events: events.map(e => ({type: 'manual', ...e})),
meta,
description,
convertKitTag,
categories,
problemStatementHTMLs,
keyTakeawayHTMLs,
topicHTMLs,
prerequisiteHTML,
}
}
export {getWorkshops}
With the current implementation of cachified
's types, getWorkshops
returns a Promise<unknown>
because my cache is implemented like so:
export const cache: Cache<unknown> = {
name: 'SQLite cache',
async get(key) {
const result = await prisma.cache.findUnique({
where: {key},
select: {metadata: true, value: true},
})
if (!result) return null
return {
metadata: result.metadata,
value: JSON.parse(result.value),
}
},
async set(key, {value, metadata}) {
await prisma.cache.upsert({
where: {key},
create: {
key,
value: JSON.stringify(value),
metadata: {create: metadata},
},
update: {
key,
value: JSON.stringify(value),
metadata: {
upsert: {
update: metadata,
create: metadata,
},
},
},
})
},
async delete(key) {
await prisma.cache.delete({where: {key}})
},
}
The Cache<unknown>
is required because I want to use this same cache for many different types, so I don't know (or care) what the type is. I think cachified was built assuming each cache would be independent for each type of thing you want to cache, but the original implementation I made was to be a generic function that could use the same cache to cache any number of types of things. So, I think what I'm trying to do should be supported.
I can fix this by changing one thing here:
- cache: Cache<Value>;
+ cache: Cache<unknown>;
If that's the direction we go, then it would probably be even better to just not make Cache
generic instead. I can't think of a situation where the cache needs to know or care about the type that's being cached.