@@ -51,6 +51,40 @@ export interface PersistentCacheStore<V = any> extends CacheStore<V> {
5151 close ( ) : void | Promise < void > ;
5252}
5353
54+ /**
55+ * A backing data store wrapper that namespaces all keys using length-prefix framing.
56+ * Prevents key collisions between namespaces regardless of characters (such as colons)
57+ * in the namespace or key.
58+ */
59+ export class NamespacedCacheStore < V > implements CacheStore < V > {
60+ readonly #prefix: string ;
61+
62+ constructor (
63+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64+ private readonly store : CacheStore < any > ,
65+ readonly namespace : string ,
66+ ) {
67+ this . #prefix = `${ namespace . length } :${ namespace } :` ;
68+ }
69+
70+ get ( key : string ) : V | undefined | Promise < V | undefined > {
71+ return this . store . get ( this . #prefix + key ) ;
72+ }
73+
74+ has ( key : string ) : boolean | Promise < boolean > {
75+ return this . store . has ( this . #prefix + key ) ;
76+ }
77+
78+ set ( key : string , value : V ) : this | Promise < this> {
79+ const result = this . store . set ( this . #prefix + key , value ) ;
80+ if ( result instanceof Promise ) {
81+ return result . then ( ( ) => this ) ;
82+ }
83+
84+ return this ;
85+ }
86+ }
87+
5488/**
5589 * A cache object that allows accessing and storing key/value pairs in
5690 * an underlying CacheStore. This class is the primary method for consumers
@@ -64,10 +98,7 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
6498 // Count the number of active, pending getOrCreate operations per key to avoid memory leaks.
6599 readonly #pendingGets = new Map < string , number > ( ) ;
66100
67- constructor (
68- protected readonly store : S ,
69- readonly namespace ?: string ,
70- ) { }
101+ constructor ( protected readonly store : S ) { }
71102
72103 #incrementWrite( key : string ) {
73104 // Only track write counts if there is a pending getOrCreate operation active for the key.
@@ -77,19 +108,6 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
77108 }
78109 }
79110
80- /**
81- * Prefixes a key with the cache namespace if present.
82- * @param key A key string to prefix.
83- * @returns A prefixed key if a namespace is present. Otherwise the provided key.
84- */
85- protected withNamespace ( key : string ) : string {
86- if ( this . namespace ) {
87- return `${ this . namespace } :${ key } ` ;
88- }
89-
90- return key ;
91- }
92-
93111 /**
94112 * Gets the value associated with a provided key if available.
95113 * Otherwise, creates a value using the factory creator function, puts the value
@@ -99,27 +117,25 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
99117 * @returns A value associated with the provided key.
100118 */
101119 async getOrCreate ( key : string , creator : ( ) => V | Promise < V > ) : Promise < V > {
102- const namespacedKey = this . withNamespace ( key ) ;
103-
104120 // 1. If another call is already running the creator for this key, share its promise.
105- let activeRequest = this . #requests. get ( namespacedKey ) ;
121+ let activeRequest = this . #requests. get ( key ) ;
106122 if ( activeRequest !== undefined ) {
107123 return activeRequest ;
108124 }
109125
110126 // Increment pending gets count to enable write-tracking for this key.
111- const currentPending = this . #pendingGets. get ( namespacedKey ) || 0 ;
112- this . #pendingGets. set ( namespacedKey , currentPending + 1 ) ;
127+ const currentPending = this . #pendingGets. get ( key ) || 0 ;
128+ this . #pendingGets. set ( key , currentPending + 1 ) ;
113129
114130 try {
115- const startWriteCount = this . #writeCounts. get ( namespacedKey ) || 0 ;
131+ const startWriteCount = this . #writeCounts. get ( key ) || 0 ;
116132
117133 // 2. Query the backing store. Since store.get can be async, we yield to the event loop.
118- const value = await this . store . get ( namespacedKey ) ;
134+ const value = await this . store . get ( key ) ;
119135
120136 // If a write (e.g. put) occurred during the store.get await gap, we must abort
121137 // the current execution and restart to ensure we return the newly written value.
122- if ( ( this . #writeCounts. get ( namespacedKey ) || 0 ) !== startWriteCount ) {
138+ if ( ( this . #writeCounts. get ( key ) || 0 ) !== startWriteCount ) {
123139 return this . getOrCreate ( key , creator ) ;
124140 }
125141
@@ -129,7 +145,7 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
129145
130146 // 3. Recheck active request after the await gap in case another concurrent call
131147 // initiated a creator during the store.get wait.
132- activeRequest = this . #requests. get ( namespacedKey ) ;
148+ activeRequest = this . #requests. get ( key ) ;
133149 if ( activeRequest !== undefined ) {
134150 return activeRequest ;
135151 }
@@ -139,34 +155,34 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
139155 async ( newValue ) => {
140156 // Ensure this request is still the active one before writing back to the store
141157 // (prevents overwriting newer data if put() was called before resolution).
142- if ( this . #requests. get ( namespacedKey ) === activeRequest ) {
143- this . #incrementWrite( namespacedKey ) ;
144- await this . store . set ( namespacedKey , newValue ) ;
145- this . #requests. delete ( namespacedKey ) ;
158+ if ( this . #requests. get ( key ) === activeRequest ) {
159+ this . #incrementWrite( key ) ;
160+ await this . store . set ( key , newValue ) ;
161+ this . #requests. delete ( key ) ;
146162 }
147163
148164 return newValue ;
149165 } ,
150166 ( error ) => {
151167 // Clean up the active request if the creator fails.
152- if ( this . #requests. get ( namespacedKey ) === activeRequest ) {
153- this . #requests. delete ( namespacedKey ) ;
168+ if ( this . #requests. get ( key ) === activeRequest ) {
169+ this . #requests. delete ( key ) ;
154170 }
155171 throw error ;
156172 } ,
157173 ) ;
158174
159- this . #requests. set ( namespacedKey , activeRequest ) ;
175+ this . #requests. set ( key , activeRequest ) ;
160176
161177 return activeRequest ;
162178 } finally {
163179 // Clean up write counts and pending gets once all concurrent gets for this key finish.
164- const current = this . #pendingGets. get ( namespacedKey ) || 0 ;
180+ const current = this . #pendingGets. get ( key ) || 0 ;
165181 if ( current <= 1 ) {
166- this . #pendingGets. delete ( namespacedKey ) ;
167- this . #writeCounts. delete ( namespacedKey ) ;
182+ this . #pendingGets. delete ( key ) ;
183+ this . #writeCounts. delete ( key ) ;
168184 } else {
169- this . #pendingGets. set ( namespacedKey , current - 1 ) ;
185+ this . #pendingGets. set ( key , current - 1 ) ;
170186 }
171187 }
172188 }
@@ -177,7 +193,7 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
177193 * @returns A value associated with the provided key if present. Otherwise, `undefined`.
178194 */
179195 async get ( key : string ) : Promise < V | undefined > {
180- const value = await this . store . get ( this . withNamespace ( key ) ) ;
196+ const value = await this . store . get ( key ) ;
181197
182198 return value ;
183199 }
@@ -189,19 +205,18 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
189205 * @param value A value to put in the cache.
190206 */
191207 async put ( key : string , value : V ) : Promise < void > {
192- const namespacedKey = this . withNamespace ( key ) ;
193- this . #requests. delete ( namespacedKey ) ;
194- this . #incrementWrite( namespacedKey ) ;
195- await this . store . set ( namespacedKey , value ) ;
208+ this . #requests. delete ( key ) ;
209+ this . #incrementWrite( key ) ;
210+ await this . store . set ( key , value ) ;
196211 }
197212
198213 /**
199- * Clears internal state for a specific namespaced key (requests, write counts, and pending gets).
214+ * Clears internal state for a specific key (requests, write counts, and pending gets).
200215 */
201- protected deleteInternal ( namespacedKey : string ) : void {
202- this . #requests. delete ( namespacedKey ) ;
203- this . #writeCounts. delete ( namespacedKey ) ;
204- this . #pendingGets. delete ( namespacedKey ) ;
216+ protected deleteInternal ( key : string ) : void {
217+ this . #requests. delete ( key ) ;
218+ this . #writeCounts. delete ( key ) ;
219+ this . #pendingGets. delete ( key ) ;
205220 }
206221
207222 /**
@@ -228,10 +243,9 @@ export class MemoryCache<V> extends Cache<V, Map<string, V>> {
228243 * @returns True if an element in the Map existed and has been removed, or false if the element does not exist.
229244 */
230245 delete ( key : string ) : boolean {
231- const namespacedKey = this . withNamespace ( key ) ;
232- this . deleteInternal ( namespacedKey ) ;
246+ this . deleteInternal ( key ) ;
233247
234- return this . store . delete ( namespacedKey ) ;
248+ return this . store . delete ( key ) ;
235249 }
236250
237251 /**
0 commit comments