Skip to content

Allow Atom runtime memoization to follow AtomRegistry lifetime #6887

Description

@andrskr

Problem

Atom.context accepts a concrete Layer.MemoMap. That map belongs to the runtime factory, even when the runtime is used by several AtomRegistry instances.

This is fine when the factory and registry have the same lifetime. It becomes a problem when registries are expected to be isolated but their Effect layers are still shared through the factory memo map.

How we hit this

We use module-level AtomRpc queries and mutations with a separate AtomRegistry for each SSR request.

We want to keep module-level atoms. They give us stable atom identity, normal AtomRpc ergonomics, and predictable serialization and hydration.

In our focused reproduction, we mounted the same reactive query in two registries. A mutation in registry A also refreshed the query in registry B. We expected three reads and received four.

Creating another runtime with:

Atom.context({
  memoMap: Layer.makeMemoMapUnsafe()
})

does not solve this. It creates another factory-scoped map, not a registry-scoped one.

Recreating runtimes and atoms for every registry can isolate them, but then consumers must stop using normal module-level atoms. Layer.fresh can also avoid some sharing, but it rebuilds more than necessary and does not model the ownership we need.

We eventually narrowed our application so it does not currently depend on this cross-registry mutation behavior. That is a valid local trade-off, but not a general solution.

Why this is broader than our setup

The same problem can appear anywhere one Atom runtime is used with multiple registries:

  • concurrent SSR requests
  • multiple React roots
  • tenant or session-scoped registries
  • isolated test registries
  • applications using registry-local stateful layers

The registry looks isolated, but the runtime layer memoization may not be.

Expected behavior

We need an opt-in way to make memoization follow the registry:

  • layers are shared inside one registry
  • layers are not shared between different registries
  • disposing one registry releases only its resources
  • existing factory-scoped behavior remains unchanged
  • module-level atoms remain module-level atoms

Possible direction

The cleanest direction we found is allowing Atom.context to accept either a concrete Layer.MemoMap or an Atom<Layer.MemoMap>:

const registryMemoMap = Atom.make(() => Layer.makeMemoMapUnsafe())

const runtime = Atom.context({
  memoMap: registryMemoMap
})

The memo-map atom would be resolved through the active registry, giving each registry its own map while preserving the current behavior for callers passing a concrete map.

I am not sure this is the right public API. An explicit registry scope option, a memo-map resolver, or another composition using the existing Layer APIs may be better.

I plan to open a draft PR with the reproduction, lifecycle tests, and the atom-backed implementation so we have something concrete to discuss. The PR is meant to show the behavior and one possible solution, not to claim that this API is final.

I would especially appreciate feedback on two questions:

  1. Should registry ownership be represented by an atom, an explicit scope option, or something else?
  2. Should registry maps be independent roots or forks of the factory map?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions