From 122f122d4a3bf577a41a226c1602968e12e0959a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Re=C3=A9?= Date: Tue, 30 Jun 2026 21:58:09 +0200 Subject: [PATCH] Add CacheConfig: typed cache-tag/lifetime functor over Cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A functor that wraps the Cache 'use cache' tag + lifetime bindings with typed variants, so each app supplies its own cache-tag type while sharing one implementation. The tag's string coercion lives at the call site (the app's tagToString) since an abstract functor type can't be coerced to string; Defaults carries the common lifetime enum (Seconds … Max). --- src/CacheConfig.res | 59 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/CacheConfig.res diff --git a/src/CacheConfig.res b/src/CacheConfig.res new file mode 100644 index 0000000..f4aea58 --- /dev/null +++ b/src/CacheConfig.res @@ -0,0 +1,59 @@ +// Typed wrappers around the Next.js `'use cache'` tag + lifetime APIs (Cache), +// as a functor so each app supplies its own cache-tag type while reusing one +// implementation. Tags + lifetimes are typed variants rather than raw strings, +// so cache keys stay typo-proof. +// +// The string coercion of a tag lives at the *call site* (the app's +// `tagToString`): the library can't coerce an abstract functor type to string, +// and the app's concrete `@as`-string variant can (`(t :> string)`). +// `DefaultsLife` carries the common lifetime enum so apps rarely define their own. +// +// `cacheTag` / `cacheLife` are called INSIDE a `'use cache'` component; +// `updateTag` / `revalidateTag` bust a tag from a mutation server action. + +module type CacheConfig = { + type tag + type life + let tagToString: tag => string + let lifeToString: life => string +} + +module DefaultsLife = { + type life = + | @as("seconds") Seconds + | @as("minutes") Minutes + | @as("hours") Hours + | @as("days") Days + | @as("max") Max + + let lifeToString = (life: life) => (life :> string) +} + +module Make = (Config: CacheConfig) => { + type tag = Config.tag + type life = Config.life + let tagToString = Config.tagToString + let lifeToString = Config.lifeToString + let cacheTag = tag => tag->tagToString->Cache.cacheTag + let cacheLife = life => life->lifeToString->Cache.cacheLife + let updateTag = tag => tag->tagToString->Cache.updateTag + let revalidateTag = (tag, ~profile: Cache.revalidateProfile) => + tag->tagToString->Cache.revalidateTag(~profile) +} + +// Example usage — an app supplies its own cache-tag type, then applies `Make`. +// The config must be a *named* module (the functor result mentions its `tag` / +// `life`, so an inline struct gives "parameter cannot be eliminated"); +// `include DefaultsLife` takes the standard lifetime enum as the default. +// +// module MyCacheConfigBase = { +// include DefaultsLife +// type tag = | @as("public-content") PublicContent +// let tagToString = (tag: tag) => (tag :> string) +// } +// module MyCacheConfig = Make(MyCacheConfigBase) +// +// // tag + lifetime values come from the Base module: +// MyCacheConfig.cacheTag(MyCacheConfigBase.PublicContent) +// MyCacheConfig.cacheLife(MyCacheConfigBase.Max) +// MyCacheConfig.updateTag(MyCacheConfigBase.PublicContent)