What version of Effect is running?
effect@4.0.0-rc.117, tested with Bun 1.2.21 and Node.js v24.7.0 on macOS arm64.
The preload retention behavior was also compared against effect@3.22.2.
What steps can reproduce the bug?
Create a resource-owning LayerMap with preloadKeys and the default idleTimeToLive. The resource is acquired and immediately released before LayerMap.make returns. The first actual use has to build it again.
In a new directory:
npm init -y
npm install --save-exact effect@4.0.0-rc.117
Save as repro.ts and run bun repro.ts (or node repro.ts with Node.js v24.7.0):
import { Effect, Layer, LayerMap, RcMap } from "effect"
const program = Effect.gen(function* () {
const events: Array<string> = []
const map = yield* LayerMap.make(
(key: string) => Layer.effectDiscard(Effect.acquireRelease(
Effect.sync(() => { events.push(`open ${key}`) }),
() => Effect.sync(() => { events.push(`close ${key}`) }),
)),
{ preloadKeys: ["tenant-a"] },
)
console.log("after make:", JSON.stringify(events))
console.log("cached keys:", JSON.stringify(Array.from(yield* RcMap.keys(map.rcMap))))
yield* Effect.scoped(map.contextEffect("tenant-a"))
console.log("after first use:", JSON.stringify(events))
})
Effect.runPromise(Effect.scoped(program))
What is the expected behavior?
Preloaded resources remain available for subsequent use rather than being immediately discarded. In particular, the v3 behavior retained a reference in the LayerMap's owning scope, keeping preloaded resources alive until that scope closed (unless explicitly invalidated).
With that behavior, the output before the owning scope closes would be:
after make: ["open tenant-a"]
cached keys: ["tenant-a"]
after first use: ["open tenant-a"]
The resource would be released when the owning scope closes.
What do you see instead?
Both Bun and Node print:
after make: ["open tenant-a","close tenant-a"]
cached keys: []
after first use: ["open tenant-a","close tenant-a","open tenant-a","close tenant-a"]
Default preloading therefore adds an extra acquire/release cycle without keeping the service ready for its first use. It still detects acquisition failures during construction, but does not retain the resource.
Additional information
Cause and v3 comparison
The rc.117 preload loop is:
for (const key of options.preloadKeys) {
yield* Effect.scoped(RcMap.get(rcMap, key))
}
Each temporary scope immediately releases its reference. RcMap defaults idleTimeToLive to zero, so its last-reference release removes and closes the entry.
In v3.22.2, the loop yields RcMap.get(rcMap, key) without the temporary Effect.scoped; the reference is held by the owning scope instead. I verified that v3 acquires once, retains the cached key across the first use, and releases only at owner-scope shutdown.
Coverage
Deterministic tests against rc.117 also confirmed:
LayerMap.fromRecord({ ... }, { preload: true }) has the same default-TTL behavior.
LayerMap.Service with preloadKeys has the same behavior.
- With a finite idle TTL, a preload is retained only until idle expiry unless another borrower retains/reuses it.
- With infinite idle TTL, a preload remains cached until invalidation or map shutdown.
- A failed preload still fails construction, so construction-time failure checking is not the broken part.
As a temporary workaround, an infinite idle TTL retains preloads, but it also changes the retention policy for other entries; it is not equivalent to retaining only preloaded keys in the owning scope.
Related existing test
PR #6871 addressed preload options having no runtime effect. The current make preloads the requested keys test checks that acquisition occurred, but does not check whether those resources remain retained afterward. This report concerns that retention gap, not the already-covered absence of acquisition.
If changing preload lifetime from v3 was intentional, clarification of that policy would help. With the current defaults, however, the selected resources have already been closed when construction returns.
What version of Effect is running?
effect@4.0.0-rc.117, tested with Bun 1.2.21 and Node.js v24.7.0 on macOS arm64.The preload retention behavior was also compared against
effect@3.22.2.What steps can reproduce the bug?
Create a resource-owning
LayerMapwithpreloadKeysand the defaultidleTimeToLive. The resource is acquired and immediately released beforeLayerMap.makereturns. The first actual use has to build it again.In a new directory:
Save as
repro.tsand runbun repro.ts(ornode repro.tswith Node.js v24.7.0):What is the expected behavior?
Preloaded resources remain available for subsequent use rather than being immediately discarded. In particular, the v3 behavior retained a reference in the LayerMap's owning scope, keeping preloaded resources alive until that scope closed (unless explicitly invalidated).
With that behavior, the output before the owning scope closes would be:
The resource would be released when the owning scope closes.
What do you see instead?
Both Bun and Node print:
Default preloading therefore adds an extra acquire/release cycle without keeping the service ready for its first use. It still detects acquisition failures during construction, but does not retain the resource.
Additional information
Cause and v3 comparison
The rc.117 preload loop is:
Each temporary scope immediately releases its reference.
RcMapdefaultsidleTimeToLiveto zero, so its last-reference release removes and closes the entry.In v3.22.2, the loop yields
RcMap.get(rcMap, key)without the temporaryEffect.scoped; the reference is held by the owning scope instead. I verified that v3 acquires once, retains the cached key across the first use, and releases only at owner-scope shutdown.Coverage
Deterministic tests against rc.117 also confirmed:
LayerMap.fromRecord({ ... }, { preload: true })has the same default-TTL behavior.LayerMap.ServicewithpreloadKeyshas the same behavior.As a temporary workaround, an infinite idle TTL retains preloads, but it also changes the retention policy for other entries; it is not equivalent to retaining only preloaded keys in the owning scope.
Related existing test
PR #6871 addressed preload options having no runtime effect. The current
make preloads the requested keystest checks that acquisition occurred, but does not check whether those resources remain retained afterward. This report concerns that retention gap, not the already-covered absence of acquisition.If changing preload lifetime from v3 was intentional, clarification of that policy would help. With the current defaults, however, the selected resources have already been closed when construction returns.