1- // Server -side in-memory cache for WorkOS organization details ONLY
1+ // Aggressive server -side caching for WorkOS data
22//
3- // ⚠️ IMPORTANT: This cache is ONLY for WorkOS organization metadata (name, id)
3+ // ⚠️ IMPORTANT: This cache is ONLY for WorkOS metadata
44// DO NOT cache core application data like:
55// - Units, unit versions, unit status
66// - Projects, project status
@@ -16,12 +16,27 @@ interface CacheEntry<T> {
1616 expiresAt : number ;
1717}
1818
19- class OrgCache {
19+ interface UserMembership {
20+ userId : string ;
21+ memberships : any [ ] ; // Store full membership objects from WorkOS
22+ }
23+
24+ class AggressiveWorkOSCache {
25+ // Organization cache - 30 minutes (org names rarely change)
2026 private readonly orgCache = new Map < string , CacheEntry < Organization > > ( ) ;
21-
22- // Cache organization details for 5 minutes
23- // Org names/details rarely change, so this is safe
24- private readonly ORG_TTL = 5 * 60 * 1000 ;
27+ private readonly ORG_TTL = 30 * 60 * 1000 ; // 30 minutes
28+
29+ // User memberships cache - 15 minutes (users don't switch orgs often)
30+ private readonly membershipCache = new Map < string , CacheEntry < UserMembership > > ( ) ;
31+ private readonly MEMBERSHIP_TTL = 15 * 60 * 1000 ; // 15 minutes
32+
33+ // Widget tokens cache - 5 minutes (tokens expire quickly)
34+ private readonly widgetTokenCache = new Map < string , CacheEntry < string > > ( ) ;
35+ private readonly WIDGET_TOKEN_TTL = 5 * 60 * 1000 ; // 5 minutes
36+
37+ // ============================================================================
38+ // ORGANIZATION CACHE
39+ // ============================================================================
2540
2641 getOrg ( orgId : string ) : Organization | null {
2742 const entry = this . orgCache . get ( orgId ) ;
@@ -46,26 +61,125 @@ class OrgCache {
4661 this . orgCache . delete ( orgId ) ;
4762 }
4863
49- // Clear expired entries periodically
64+ // ============================================================================
65+ // USER MEMBERSHIPS CACHE
66+ // ============================================================================
67+
68+ getUserMemberships ( userId : string ) : any [ ] | null {
69+ const entry = this . membershipCache . get ( userId ) ;
70+ if ( ! entry ) return null ;
71+
72+ if ( Date . now ( ) > entry . expiresAt ) {
73+ this . membershipCache . delete ( userId ) ;
74+ return null ;
75+ }
76+
77+ return entry . data . memberships ;
78+ }
79+
80+ setUserMemberships ( userId : string , memberships : any [ ] ) : void {
81+ this . membershipCache . set ( userId , {
82+ data : { userId, memberships } ,
83+ expiresAt : Date . now ( ) + this . MEMBERSHIP_TTL ,
84+ } ) ;
85+ }
86+
87+ clearUserMemberships ( userId : string ) : void {
88+ this . membershipCache . delete ( userId ) ;
89+ }
90+
91+ // ============================================================================
92+ // WIDGET TOKEN CACHE
93+ // ============================================================================
94+
95+ getWidgetToken ( userId : string , orgId : string ) : string | null {
96+ const key = `${ userId } :${ orgId } ` ;
97+ const entry = this . widgetTokenCache . get ( key ) ;
98+ if ( ! entry ) return null ;
99+
100+ if ( Date . now ( ) > entry . expiresAt ) {
101+ this . widgetTokenCache . delete ( key ) ;
102+ return null ;
103+ }
104+
105+ return entry . data ;
106+ }
107+
108+ setWidgetToken ( userId : string , orgId : string , token : string ) : void {
109+ const key = `${ userId } :${ orgId } ` ;
110+ this . widgetTokenCache . set ( key , {
111+ data : token ,
112+ expiresAt : Date . now ( ) + this . WIDGET_TOKEN_TTL ,
113+ } ) ;
114+ }
115+
116+ clearWidgetToken ( userId : string , orgId : string ) : void {
117+ const key = `${ userId } :${ orgId } ` ;
118+ this . widgetTokenCache . delete ( key ) ;
119+ }
120+
121+ // ============================================================================
122+ // CACHE MANAGEMENT
123+ // ============================================================================
124+
50125 cleanExpired ( ) : void {
51126 const now = Date . now ( ) ;
127+
128+ // Clean org cache
52129 for ( const [ key , entry ] of this . orgCache . entries ( ) ) {
53130 if ( now > entry . expiresAt ) {
54131 this . orgCache . delete ( key ) ;
55132 }
56133 }
134+
135+ // Clean membership cache
136+ for ( const [ key , entry ] of this . membershipCache . entries ( ) ) {
137+ if ( now > entry . expiresAt ) {
138+ this . membershipCache . delete ( key ) ;
139+ }
140+ }
141+
142+ // Clean widget token cache
143+ for ( const [ key , entry ] of this . widgetTokenCache . entries ( ) ) {
144+ if ( now > entry . expiresAt ) {
145+ this . widgetTokenCache . delete ( key ) ;
146+ }
147+ }
57148 }
58149
59- // Get cache stats for monitoring
60150 getStats ( ) {
61151 return {
62152 orgCacheSize : this . orgCache . size ,
153+ membershipCacheSize : this . membershipCache . size ,
154+ widgetTokenCacheSize : this . widgetTokenCache . size ,
63155 } ;
64156 }
157+
158+ // Invalidate all caches for a user (when they switch orgs, etc.)
159+ invalidateUser ( userId : string ) : void {
160+ this . clearUserMemberships ( userId ) ;
161+ // Clear all widget tokens for this user
162+ for ( const key of this . widgetTokenCache . keys ( ) ) {
163+ if ( key . startsWith ( `${ userId } :` ) ) {
164+ this . widgetTokenCache . delete ( key ) ;
165+ }
166+ }
167+ }
168+
169+ // Invalidate all caches for an org (when org details change)
170+ invalidateOrg ( orgId : string ) : void {
171+ this . clearOrg ( orgId ) ;
172+ // Clear all widget tokens for this org
173+ for ( const key of this . widgetTokenCache . keys ( ) ) {
174+ if ( key . endsWith ( `:${ orgId } ` ) ) {
175+ this . widgetTokenCache . delete ( key ) ;
176+ }
177+ }
178+ }
65179}
66180
67181// Single instance per server process
68- export const serverCache = new OrgCache ( ) ;
182+ export const serverCache = new AggressiveWorkOSCache ( ) ;
69183
70184// Clean up expired entries every minute
71185setInterval ( ( ) => serverCache . cleanExpired ( ) , 60 * 1000 ) ;
0 commit comments