@@ -51,6 +51,39 @@ 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+ private readonly store : CacheStore < V > ,
64+ readonly namespace : string ,
65+ ) {
66+ this . #prefix = `${ namespace . length } :${ namespace } :` ;
67+ }
68+
69+ get ( key : string ) : V | undefined | Promise < V | undefined > {
70+ return this . store . get ( this . #prefix + key ) ;
71+ }
72+
73+ has ( key : string ) : boolean | Promise < boolean > {
74+ return this . store . has ( this . #prefix + key ) ;
75+ }
76+
77+ set ( key : string , value : V ) : this | Promise < this> {
78+ const result = this . store . set ( this . #prefix + key , value ) ;
79+ if ( result instanceof Promise ) {
80+ return result . then ( ( ) => this ) ;
81+ }
82+
83+ return this ;
84+ }
85+ }
86+
5487/**
5588 * A cache object that allows accessing and storing key/value pairs in
5689 * an underlying CacheStore. This class is the primary method for consumers
@@ -64,10 +97,7 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
6497 // Count the number of active, pending getOrCreate operations per key to avoid memory leaks.
6598 readonly #pendingGets = new Map < string , number > ( ) ;
6699
67- constructor (
68- protected readonly store : S ,
69- readonly namespace ?: string ,
70- ) { }
100+ constructor ( protected readonly store : S ) { }
71101
72102 #incrementWrite( key : string ) {
73103 // Only track write counts if there is a pending getOrCreate operation active for the key.
@@ -77,19 +107,6 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
77107 }
78108 }
79109
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-
93110 /**
94111 * Gets the value associated with a provided key if available.
95112 * Otherwise, creates a value using the factory creator function, puts the value
@@ -99,27 +116,25 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
99116 * @returns A value associated with the provided key.
100117 */
101118 async getOrCreate ( key : string , creator : ( ) => V | Promise < V > ) : Promise < V > {
102- const namespacedKey = this . withNamespace ( key ) ;
103-
104119 // 1. If another call is already running the creator for this key, share its promise.
105- let activeRequest = this . #requests. get ( namespacedKey ) ;
120+ let activeRequest = this . #requests. get ( key ) ;
106121 if ( activeRequest !== undefined ) {
107122 return activeRequest ;
108123 }
109124
110125 // 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 ) ;
126+ const currentPending = this . #pendingGets. get ( key ) || 0 ;
127+ this . #pendingGets. set ( key , currentPending + 1 ) ;
113128
114129 try {
115- const startWriteCount = this . #writeCounts. get ( namespacedKey ) || 0 ;
130+ const startWriteCount = this . #writeCounts. get ( key ) || 0 ;
116131
117132 // 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 ) ;
133+ const value = await this . store . get ( key ) ;
119134
120135 // If a write (e.g. put) occurred during the store.get await gap, we must abort
121136 // the current execution and restart to ensure we return the newly written value.
122- if ( ( this . #writeCounts. get ( namespacedKey ) || 0 ) !== startWriteCount ) {
137+ if ( ( this . #writeCounts. get ( key ) || 0 ) !== startWriteCount ) {
123138 return this . getOrCreate ( key , creator ) ;
124139 }
125140
@@ -129,7 +144,7 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
129144
130145 // 3. Recheck active request after the await gap in case another concurrent call
131146 // initiated a creator during the store.get wait.
132- activeRequest = this . #requests. get ( namespacedKey ) ;
147+ activeRequest = this . #requests. get ( key ) ;
133148 if ( activeRequest !== undefined ) {
134149 return activeRequest ;
135150 }
@@ -139,34 +154,34 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
139154 async ( newValue ) => {
140155 // Ensure this request is still the active one before writing back to the store
141156 // (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 ) ;
157+ if ( this . #requests. get ( key ) === activeRequest ) {
158+ this . #incrementWrite( key ) ;
159+ await this . store . set ( key , newValue ) ;
160+ this . #requests. delete ( key ) ;
146161 }
147162
148163 return newValue ;
149164 } ,
150165 ( error ) => {
151166 // Clean up the active request if the creator fails.
152- if ( this . #requests. get ( namespacedKey ) === activeRequest ) {
153- this . #requests. delete ( namespacedKey ) ;
167+ if ( this . #requests. get ( key ) === activeRequest ) {
168+ this . #requests. delete ( key ) ;
154169 }
155170 throw error ;
156171 } ,
157172 ) ;
158173
159- this . #requests. set ( namespacedKey , activeRequest ) ;
174+ this . #requests. set ( key , activeRequest ) ;
160175
161176 return activeRequest ;
162177 } finally {
163178 // Clean up write counts and pending gets once all concurrent gets for this key finish.
164- const current = this . #pendingGets. get ( namespacedKey ) || 0 ;
179+ const current = this . #pendingGets. get ( key ) || 0 ;
165180 if ( current <= 1 ) {
166- this . #pendingGets. delete ( namespacedKey ) ;
167- this . #writeCounts. delete ( namespacedKey ) ;
181+ this . #pendingGets. delete ( key ) ;
182+ this . #writeCounts. delete ( key ) ;
168183 } else {
169- this . #pendingGets. set ( namespacedKey , current - 1 ) ;
184+ this . #pendingGets. set ( key , current - 1 ) ;
170185 }
171186 }
172187 }
@@ -177,7 +192,7 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
177192 * @returns A value associated with the provided key if present. Otherwise, `undefined`.
178193 */
179194 async get ( key : string ) : Promise < V | undefined > {
180- const value = await this . store . get ( this . withNamespace ( key ) ) ;
195+ const value = await this . store . get ( key ) ;
181196
182197 return value ;
183198 }
@@ -189,19 +204,18 @@ export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
189204 * @param value A value to put in the cache.
190205 */
191206 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 ) ;
207+ this . #requests. delete ( key ) ;
208+ this . #incrementWrite( key ) ;
209+ await this . store . set ( key , value ) ;
196210 }
197211
198212 /**
199- * Clears internal state for a specific namespaced key (requests, write counts, and pending gets).
213+ * Clears internal state for a specific key (requests, write counts, and pending gets).
200214 */
201- protected deleteInternal ( namespacedKey : string ) : void {
202- this . #requests. delete ( namespacedKey ) ;
203- this . #writeCounts. delete ( namespacedKey ) ;
204- this . #pendingGets. delete ( namespacedKey ) ;
215+ protected deleteInternal ( key : string ) : void {
216+ this . #requests. delete ( key ) ;
217+ this . #writeCounts. delete ( key ) ;
218+ this . #pendingGets. delete ( key ) ;
205219 }
206220
207221 /**
@@ -228,10 +242,9 @@ export class MemoryCache<V> extends Cache<V, Map<string, V>> {
228242 * @returns True if an element in the Map existed and has been removed, or false if the element does not exist.
229243 */
230244 delete ( key : string ) : boolean {
231- const namespacedKey = this . withNamespace ( key ) ;
232- this . deleteInternal ( namespacedKey ) ;
245+ this . deleteInternal ( key ) ;
233246
234- return this . store . delete ( namespacedKey ) ;
247+ return this . store . delete ( key ) ;
235248 }
236249
237250 /**
0 commit comments