File tree Expand file tree Collapse file tree
packages/devframe/src/rpc Expand file tree Collapse file tree Original file line number Diff line number Diff line change 1+ import { describe , expect , it } from 'vitest'
2+ import { defineRpcFunction } from './define'
3+ import { getRpcResolvedSetupResult } from './handler'
4+
5+ describe ( 'getRpcResolvedSetupResult' , ( ) => {
6+ it ( 'retries setup after a rejection instead of caching it (object context)' , async ( ) => {
7+ let calls = 0
8+ const def = defineRpcFunction ( {
9+ name : 't:retry' ,
10+ type : 'query' ,
11+ setup : ( ) => {
12+ calls ++
13+ if ( calls === 1 )
14+ throw new Error ( 'transient' )
15+ return { handler : ( ) => 'ok' }
16+ } ,
17+ } )
18+ const ctx = { }
19+ await expect ( getRpcResolvedSetupResult ( def as any , ctx ) ) . rejects . toThrow ( 'transient' )
20+ const result = await getRpcResolvedSetupResult ( def as any , ctx )
21+ expect ( result . handler ?.( ) ) . toBe ( 'ok' )
22+ expect ( calls ) . toBe ( 2 )
23+ } )
24+
25+ it ( 'caches a successful setup result per context (no re-run)' , async ( ) => {
26+ let calls = 0
27+ const def = defineRpcFunction ( {
28+ name : 't:cache' ,
29+ type : 'query' ,
30+ setup : ( ) => {
31+ calls ++
32+ return { handler : ( ) => calls }
33+ } ,
34+ } )
35+ const ctx = { }
36+ await getRpcResolvedSetupResult ( def as any , ctx )
37+ await getRpcResolvedSetupResult ( def as any , ctx )
38+ expect ( calls ) . toBe ( 1 )
39+ } )
40+ } )
Original file line number Diff line number Diff line change @@ -21,16 +21,30 @@ export async function getRpcResolvedSetupResult<
2121 // closed over a prior context's state.
2222 if ( typeof context === 'object' && context !== null ) {
2323 definition . __cache ??= new WeakMap ( )
24- let promise = definition . __cache . get ( context as object )
24+ const cache = definition . __cache
25+ let promise = cache . get ( context as object )
2526 if ( ! promise ) {
2627 promise = Promise . resolve ( definition . setup ( context ) )
27- definition . __cache . set ( context as object , promise )
28+ // If setup rejects, evict so a later call can retry instead of
29+ // re-awaiting the cached rejection permanently.
30+ promise . catch ( ( ) => {
31+ if ( cache . get ( context as object ) === promise )
32+ cache . delete ( context as object )
33+ } )
34+ cache . set ( context as object , promise )
2835 }
2936 return await promise
3037 }
3138
3239 // Primitive / undefined context — fall back to a single-slot cache.
33- definition . __promise ??= Promise . resolve ( definition . setup ( context ) )
40+ if ( ! definition . __promise ) {
41+ const promise = Promise . resolve ( definition . setup ( context ) )
42+ promise . catch ( ( ) => {
43+ if ( definition . __promise === promise )
44+ definition . __promise = undefined
45+ } )
46+ definition . __promise = promise
47+ }
3448 return await definition . __promise
3549}
3650
Load diff This file was deleted.
Original file line number Diff line number Diff line change @@ -17,7 +17,7 @@ changes are allowed as long as they're marked).
1717| 002 | Clear the critical ` shell-quote ` advisory in the bundle | security/deps | P1 | S | — | DONE |
1818| 003 | Reject cross-origin WebSocket upgrades ⚠️ | security | P1 | S | — | DONE |
1919| 004 | Stop git argument injection via ` ref ` /` hash ` | security/bug | P1 | S | — | TODO |
20- | 005 | Don't cache a rejected RPC ` setup() ` promise | bug | P1 | S | — | TODO |
20+ | 005 | Don't cache a rejected RPC ` setup() ` promise | bug | P1 | S | — | DONE |
2121| 006 | Bound ` SharedState.syncIds ` (memory leak) | bug | P1 | S | — | DONE |
2222| 007 | Behavioral tests for the auth/OTP trust boundary | tests | P1 | S | — | DONE |
2323| 008 | Cap hub terminal buffer + reject restart-after-terminate | bug | P2 | S | — | TODO |
You can’t perform that action at this time.
0 commit comments