cache-semantics.constants.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. /**
  2. * Redis cache key semantic types and constants.
  3. *
  4. * This file provides TypeScript types and constants to enforce semantic correctness
  5. * when working with Redis cache keys. Use these to ensure:
  6. * - Video ID keys always contain video IDs (strings), never JSON
  7. * - Tag keys always contain Tag JSON, not video IDs
  8. * - Operations match the key type (LRANGE for LIST, ZREVRANGE for ZSET, etc.)
  9. *
  10. * SEE: libs/common/src/cache/CACHE_SEMANTICS.md for detailed documentation
  11. */
  12. /**
  13. * Marker type for video ID strings (Mongo ObjectId format).
  14. * Use this to distinguish video IDs from other string types.
  15. *
  16. * Example:
  17. * const videoIds: VideoId[] = ['64a2b3c4d5e6f7g8h9i0j1k2', 'abc123...']
  18. */
  19. export type VideoId = string & { readonly __videoId: true };
  20. /**
  21. * Marker type for tag ID strings.
  22. */
  23. export type TagId = string & { readonly __tagId: true };
  24. /**
  25. * Marker type for category ID strings.
  26. */
  27. export type CategoryId = string & { readonly __categoryId: true };
  28. /**
  29. * Marker type for channel ID strings.
  30. */
  31. export type ChannelId = string & { readonly __channelId: true };
  32. /**
  33. * Helper to create a VideoId (for cases where you have a validated string).
  34. *
  35. * Example:
  36. * const videoId = asVideoId('64a2b3c4d5e6f7g8h9i0j1k2');
  37. * const listKey = tsCacheKeys.video.categoryList('ch-1');
  38. * const videoIds = await redis.lrange(listKey, 0, -1) as VideoId[];
  39. */
  40. export function asVideoId(value: string): VideoId {
  41. return value as VideoId;
  42. }
  43. export function asTagId(value: string): TagId {
  44. return value as TagId;
  45. }
  46. export function asCategoryId(value: string): CategoryId {
  47. return value as CategoryId;
  48. }
  49. export function asChannelId(value: string): ChannelId {
  50. return value as ChannelId;
  51. }
  52. /**
  53. * Redis cache key semantic classification.
  54. *
  55. * Use these constants to document the expected type and operations for a key.
  56. */
  57. export enum RedisKeySemanticType {
  58. /**
  59. * LIST of video IDs (strings only, no JSON).
  60. * Operations: LRANGE, RPUSH, LPUSH
  61. * Example: box:app:video:category:list:cat-1 → ['vid-1', 'vid-2', ...]
  62. */
  63. VideoIdList = 'VideoIdList',
  64. /**
  65. * LIST of video IDs with tag filter (strings only, no JSON).
  66. * Operations: LRANGE, RPUSH, LPUSH
  67. * Example: box:app:video:tag:list:cat-1:tag-1 → ['vid-1', 'vid-3', ...]
  68. */
  69. FilteredVideoIdList = 'FilteredVideoIdList',
  70. /**
  71. * ZSET of video IDs with timestamp scores for pagination.
  72. * Operations: ZREVRANGE, ZADD
  73. * Example: box:app:video:list:category:ch-1:cat-1:latest
  74. * Members: video IDs, Scores: timestamps
  75. */
  76. VideoIdZSet = 'VideoIdZSet',
  77. /**
  78. * LIST of Tag JSON objects.
  79. * Operations: LRANGE, RPUSH, LPUSH (parse each element as JSON)
  80. * Example: box:app:tag:list:cat-1 → ['{"id":"tag-1","name":"Sports",...}', ...]
  81. */
  82. TagJsonList = 'TagJsonList',
  83. /**
  84. * STRING containing JSON array of Tag objects.
  85. * Operations: GET, SET (parse result as JSON)
  86. * Example: box:app:tag:all → '[{"id":"tag-1","name":"Sports",...}, ...]'
  87. */
  88. TagJsonString = 'TagJsonString',
  89. /**
  90. * STRING containing JSON object (video detail, category, etc.)
  91. * Operations: GET, SET (parse result as JSON)
  92. * Example: box:app:video:detail:vid-1 → '{"id":"vid-1","title":"Video 1",...}'
  93. */
  94. DetailJson = 'DetailJson',
  95. /**
  96. * LIST of generic items (various uses).
  97. * Operations: LRANGE, RPUSH, LPUSH
  98. * Example: box:app:video:list:home:ch-1:latest → ['vid-1', 'vid-2', ...]
  99. */
  100. ItemList = 'ItemList',
  101. }
  102. /**
  103. * Redis command type for a given semantic type.
  104. *
  105. * Maps semantic types to their primary read/write operations.
  106. */
  107. export const RedisCommandsForSemanticType: Record<
  108. RedisKeySemanticType,
  109. { read: string[]; write: string[] }
  110. > = {
  111. [RedisKeySemanticType.VideoIdList]: {
  112. read: ['LRANGE'],
  113. write: ['DEL', 'RPUSH', 'LPUSH'],
  114. },
  115. [RedisKeySemanticType.FilteredVideoIdList]: {
  116. read: ['LRANGE'],
  117. write: ['DEL', 'RPUSH', 'LPUSH'],
  118. },
  119. [RedisKeySemanticType.VideoIdZSet]: {
  120. read: ['ZREVRANGE', 'ZRANGE'],
  121. write: ['DEL', 'ZADD'],
  122. },
  123. [RedisKeySemanticType.TagJsonList]: {
  124. read: ['LRANGE'],
  125. write: ['DEL', 'RPUSH', 'LPUSH'],
  126. },
  127. [RedisKeySemanticType.TagJsonString]: {
  128. read: ['GET'],
  129. write: ['SET', 'DEL'],
  130. },
  131. [RedisKeySemanticType.DetailJson]: {
  132. read: ['GET'],
  133. write: ['SET', 'DEL'],
  134. },
  135. [RedisKeySemanticType.ItemList]: {
  136. read: ['LRANGE'],
  137. write: ['DEL', 'RPUSH', 'LPUSH'],
  138. },
  139. };
  140. /**
  141. * Cache key metadata for documentation and validation.
  142. *
  143. * Define the semantics of each cache key pattern to enforce type safety
  144. * and prevent common mistakes.
  145. */
  146. export interface CacheKeyMetadata {
  147. /**
  148. * Human-readable description of the key.
  149. */
  150. description: string;
  151. /**
  152. * Semantic type (determines Redis type and operations).
  153. */
  154. semanticType: RedisKeySemanticType;
  155. /**
  156. * Example key (with placeholders like {categoryId}).
  157. */
  158. exampleKey: string;
  159. /**
  160. * Which builder creates/updates this key.
  161. */
  162. builtBy: string;
  163. /**
  164. * Which service/component reads this key.
  165. */
  166. readBy: string[];
  167. /**
  168. * Notes or special considerations.
  169. */
  170. notes?: string;
  171. }
  172. /**
  173. * Metadata registry for all major cache keys.
  174. *
  175. * Use this for validation, documentation generation, or runtime checks.
  176. */
  177. export const CacheKeyMetadataRegistry: Record<string, CacheKeyMetadata> = {
  178. 'app:video:category:list': {
  179. description: 'All video IDs in a category, in business order',
  180. semanticType: RedisKeySemanticType.VideoIdList,
  181. exampleKey: 'box:app:video:category:list:{categoryId}',
  182. builtBy: 'VideoCategoryCacheBuilder.buildCategoryListForChannel()',
  183. readBy: ['VideoService.getCategoryListForChannel()'],
  184. notes:
  185. 'LIST of video IDs only (strings), never JSON objects. Used by category detail API.',
  186. },
  187. 'app:video:tag:list': {
  188. description: 'Video IDs in category filtered by tag, in business order',
  189. semanticType: RedisKeySemanticType.FilteredVideoIdList,
  190. exampleKey: 'box:app:video:tag:list:{categoryId}:{tagId}',
  191. builtBy: 'VideoCategoryCacheBuilder.buildTagListForCategory()',
  192. readBy: ['VideoService.getTagListForCategory()'],
  193. notes:
  194. 'LIST of video IDs only (strings), subset of category videos. Distinct from tag.all().',
  195. },
  196. 'app:video:list:category': {
  197. description:
  198. 'Scored video IDs for a category (for pagination with consistent ordering)',
  199. semanticType: RedisKeySemanticType.VideoIdZSet,
  200. exampleKey: 'box:app:video:list:category:{channelId}:{categoryId}:{sort}',
  201. builtBy: 'VideoListCacheBuilder.buildCategoryPoolsForChannel()',
  202. readBy: ['VideoService.getVideosByCategoryWithPaging()'],
  203. notes:
  204. 'ZSET with video IDs as members and timestamp scores. Use ZREVRANGE for pagination.',
  205. },
  206. 'app:video:list:tag': {
  207. description: 'Scored video IDs for a tag (for pagination with ordering)',
  208. semanticType: RedisKeySemanticType.VideoIdZSet,
  209. exampleKey: 'box:app:video:list:tag:{channelId}:{tagId}:{sort}',
  210. builtBy: 'VideoListCacheBuilder.buildTagPoolsForChannel()',
  211. readBy: ['VideoService.getVideosByTagWithPaging()'],
  212. notes:
  213. 'ZSET with video IDs as members and timestamp scores. Use ZREVRANGE for pagination.',
  214. },
  215. 'app:video:list:home': {
  216. description: 'Home page section videos',
  217. semanticType: RedisKeySemanticType.ItemList,
  218. exampleKey: 'box:app:video:list:home:{channelId}:{section}',
  219. builtBy: 'VideoListCacheBuilder.buildHomeSectionsForChannel()',
  220. readBy: ['VideoService.getHomeSectionVideos()'],
  221. notes: 'LIST of video IDs, top N most recent across all categories.',
  222. },
  223. 'app:tag:all': {
  224. description: 'Global pool of all tags (for tag filter suggestions)',
  225. semanticType: RedisKeySemanticType.TagJsonString,
  226. exampleKey: 'box:app:tag:all',
  227. builtBy: 'TagCacheBuilder.buildAll()',
  228. readBy: ['TagCacheService.getAllTags()'],
  229. notes:
  230. 'JSON array of Tag objects. ✅ Contains TAG JSON, NOT video IDs. Distinct from video.tagList().',
  231. },
  232. 'app:video:detail': {
  233. description: 'Single video metadata/detail',
  234. semanticType: RedisKeySemanticType.DetailJson,
  235. exampleKey: 'box:app:video:detail:{videoId}',
  236. builtBy: 'VideoDetailCacheBuilder (if applicable)',
  237. readBy: ['VideoService.getVideoDetail()'],
  238. },
  239. };
  240. /**
  241. * Validation helper: Check if a key is expected to contain video IDs.
  242. *
  243. * @param keyPattern - The cache key pattern (e.g., 'app:video:category:list')
  244. * @returns true if the key contains video IDs (not JSON)
  245. */
  246. export function isVideoIdKey(keyPattern: string): boolean {
  247. const metadata = CacheKeyMetadataRegistry[keyPattern];
  248. if (!metadata) return false;
  249. return (
  250. metadata.semanticType === RedisKeySemanticType.VideoIdList ||
  251. metadata.semanticType === RedisKeySemanticType.FilteredVideoIdList ||
  252. metadata.semanticType === RedisKeySemanticType.VideoIdZSet
  253. );
  254. }
  255. /**
  256. * Validation helper: Check if a key is expected to contain JSON objects.
  257. *
  258. * @param keyPattern - The cache key pattern
  259. * @returns true if the key contains JSON (not raw strings)
  260. */
  261. export function isJsonKey(keyPattern: string): boolean {
  262. const metadata = CacheKeyMetadataRegistry[keyPattern];
  263. if (!metadata) return false;
  264. return (
  265. metadata.semanticType === RedisKeySemanticType.TagJsonList ||
  266. metadata.semanticType === RedisKeySemanticType.TagJsonString ||
  267. metadata.semanticType === RedisKeySemanticType.DetailJson
  268. );
  269. }
  270. /**
  271. * Validation helper: Get Redis operation types for a key pattern.
  272. *
  273. * @param keyPattern - The cache key pattern
  274. * @returns Object with read and write operations
  275. */
  276. export function getOperationsForKey(
  277. keyPattern: string,
  278. ): { read: string[]; write: string[] } | null {
  279. const metadata = CacheKeyMetadataRegistry[keyPattern];
  280. if (!metadata) return null;
  281. return RedisCommandsForSemanticType[metadata.semanticType];
  282. }