Repository navigation
Description
What version of Effect is running?
4.0.5 (same code on main at 34ec440)
What steps can reproduce the bug?
Seeding an Atom.runtime's layer through registry initialValues also removes the runtime's Reactivity service and every layer added with addGlobalLayer. This is how the existing Atom.test.ts "runtime replacement" and atom-react "can inject test layers" tests inject test services. The seed only has to match the declared type Layer<R, ER>, so it type-checks. After that:
- a
runtime.fn(…, { reactivityKeys })mutation dies; - so does any runtime atom whose effect uses
Reactivity; - services from global layers are missing.
These cases, added to packages/effect/test/reactivity/Atom.test.ts (they use its Counter fixtures), fail on main; the two controls pass:
describe("runtime replacement keeps the runtime's own layers", () => {
const count = counterRuntime.atom(Counter.use((_) => _.get)).pipe(Atom.withReactivity(["counter"]))
const inc = counterRuntime.fn(() => Counter.use((_) => _.inc), { reactivityKeys: ["counter"] })
it("reactivityKeys mutation, no replacement (control)", () => {
const r = AtomRegistry.make()
r.mount(count)
expect(r.get(count)).toEqual(AsyncResult.success(1))
r.set(inc, void 0)
expect(r.get(inc)).toEqual(AsyncResult.success(undefined))
expect(r.get(count)).toEqual(AsyncResult.success(2))
r.dispose()
})
it("reactivityKeys mutation, replaced layer", () => {
const r = AtomRegistry.make({
initialValues: [Atom.initialValue(counterRuntime.layer, CounterTest)]
})
r.mount(count)
expect(r.get(count)).toEqual(AsyncResult.success(10))
r.set(inc, void 0)
expect(r.get(inc)).toEqual(AsyncResult.success(undefined))
expect(r.get(count)).toEqual(AsyncResult.success(11))
r.dispose()
})
const Global = Context.Service<string>("Atom.test/GlobalLayerService")
const factory = Atom.context()
factory.addGlobalLayer(Layer.succeed(Global, "global"))
const runtime = factory(CounterLayer)
const global = runtime.atom(Effect.serviceOption(Global))
it("addGlobalLayer, no replacement (control)", () => {
const r = AtomRegistry.make()
expect(r.get(global)).toEqual(AsyncResult.success(Option.some("global")))
r.dispose()
})
it("addGlobalLayer, replaced layer", () => {
const r = AtomRegistry.make({
initialValues: [Atom.initialValue(runtime.layer, CounterTest)]
})
expect(r.get(global)).toEqual(AsyncResult.success(Option.some("global")))
r.dispose()
})
})What is the expected behavior?
The seed replaces only the layer passed to Atom.runtime, which is what the type of runtime.layer, Atom<Layer<R, ER>>, describes. The runtime keeps providing:
Reactivity, which the types ofruntime.atomandruntime.fnlet their effects require;- the global layers.
The mutation succeeds and count refreshes to 11. The global service is Some("global").
What do you see instead?
reactivityKeys mutation, replaced layer:r.get(inc)is aFailurewithDie(Error: Service not found: effect/reactivity/Reactivity).addGlobalLayer, replaced layer:Effect.serviceOption(Global)isNone.
Putting Reactivity.layer itself into the seed avoids the defect, because the memo map shares that instance with the one withReactivity registers on. A Reactivity built from any other Layer value lets the mutation succeed, but withReactivity atoms then never refresh. The types point to neither requirement.
Additional information
The atom exposed as runtime.layer holds Layer.provideMerge(create, globalLayer), not create (Atom.ts#L801-L806):
globalLayerstarts asReactivity.layer(#L789), andaddGlobalLayermerges into it (#L817-L819).readbuilds whatever the atom holds (#L808-L812), so a seed replaces the merged value. Yetlayeris declaredAtom<Layer.Layer<R, ER>>(#L640).reactivityKeyswraps the function's effect inReactivity.mutation(#L346-L351).
A seed would replace only the user's layer if the layer atom kept create and read applied Layer.provideMerge(…, globalLayer). main has the same code. Its only change to read since 4.0.5 is about logging finalizer failures.
Activity