1+ import * as NodeV8 from "node:v8" ;
2+ import * as NodeVM from "node:vm" ;
13import { assert , it } from "@effect/vitest" ;
24import * as NodeServices from "@effect/platform-node/NodeServices" ;
35import { PullRequestOperationError } from "@t3tools/contracts" ;
@@ -8,6 +10,9 @@ import * as FileSystem from "effect/FileSystem";
810import * as KeyValueStore from "effect/persistence/KeyValueStore" ;
911import * as PullRequestReadCache from "./PullRequestReadCache.ts" ;
1012
13+ NodeV8 . setFlagsFromString ( "--expose-gc" ) ;
14+ const collectGarbage = NodeVM . runInNewContext ( "gc" ) as ( ) => void ;
15+
1116const cacheLayer = ( directory : string ) =>
1217 PullRequestReadCache . make . pipe ( Effect . provide ( KeyValueStore . layerFileSystem ( directory ) ) ) ;
1318
@@ -147,6 +152,32 @@ it.layer(NodeServices.layer)("PR filesystem cache", (it) => {
147152 } ) . pipe ( Effect . provide ( KeyValueStore . layerMemory ) ) ,
148153 ) ;
149154
155+ it . effect ( "does not keep a caller's data alive after its reads and invalidations" , ( ) =>
156+ Effect . gen ( function * ( ) {
157+ const cache = yield * PullRequestReadCache . make ;
158+ const sweeps : Array < WeakRef < object > > = [ ] ;
159+ // Like a sync sweep: an Effect.fn made per run that closes over that run's data.
160+ const sweep = Effect . fn ( "sweep" ) ( function * ( index : number ) {
161+ const groups = new Map < string , number > (
162+ Array . from ( { length : 1_000 } , ( _ , id ) => [ `group-${ id } ` , id ] ) ,
163+ ) ;
164+ sweeps . push ( new WeakRef ( groups ) ) ;
165+ const read = Effect . fn ( "read" ) ( function * ( pr : string ) {
166+ const size = groups . size ;
167+ yield * cache . invalidate ( pr ) ;
168+ const answer = yield * cache . get ( `${ pr } -${ index } ` , Effect . succeed ( "answer" ) , [ pr ] ) ;
169+ return size + answer . length ;
170+ } ) ;
171+ yield * Effect . forEach ( [ "pr-1" , "pr-2" ] , read , { discard : true } ) ;
172+ } ) ;
173+ for ( let index = 0 ; index < 5 ; index ++ ) yield * sweep ( index ) ;
174+ // WeakRef targets stay alive until the current job ends, so yield before collecting.
175+ yield * Effect . yieldNow ;
176+ collectGarbage ( ) ;
177+ assert . strictEqual ( sweeps . filter ( ( sweepData ) => sweepData . deref ( ) !== undefined ) . length , 0 ) ;
178+ } ) . pipe ( Effect . provide ( KeyValueStore . layerMemory ) ) ,
179+ ) ;
180+
150181 it . effect ( "cancels abandoned reads without blocking invalidation" , ( ) =>
151182 Effect . gen ( function * ( ) {
152183 const cache = yield * PullRequestReadCache . make ;
0 commit comments