Warning: Effeck is v0. The language will change in major, breaking ways. It is not suitable for production use.

Layer Memoization

Effect shares repeated uses of the same layer value within one build graph. Effeck keeps this behavior, so reuse a layer value when consumers should share its initialization and resources.

Upstream page

Sharing acquisition across a graph

service Prefix {
  value: string
}
service Heading {
  value: string
}
service Footer {
  value: string
}

const PrefixLive = Layer.effect(Prefix) {
  log("Prefix initialized")
  return {
    value: "Monthly"
  }
}
const HeadingLive = Layer.effect(Heading) {
  const prefix = Prefix()
  return {
    value: `${prefix.value} report`
  }
}
const FooterLive = Layer.effect(Footer) {
  const prefix = Prefix()
  return {
    value: `${prefix.value} archive`
  }
}

const Shared = Layer.merge(
  Layer.provide(HeadingLive, PrefixLive),
  Layer.provide(FooterLive, PrefixLive)
)

function documentLabels() {
  provide Shared { return [Heading().value, Footer().value] }
}

Both graph branches reference PrefixLive, so its initialization logs once per documentLabels call. Memoization uses layer object identity, not the output service type or structural equivalence. If a factory builds a layer, call it once and reuse the resulting value when sharing matters.

Sharing lasts for the graph's managed lifetime; it is not a process-wide singleton. Scoped acquisitions remain open while the graph uses them and release when the owning lifetime ends.

Requesting fresh acquisition

const Separate = Layer.merge(
  Layer.provide(HeadingLive, Layer.fresh(PrefixLive)),
  Layer.provide(FooterLive, Layer.fresh(PrefixLive))
)

function isolatedLabels() {
  provide Separate { return [Heading().value, Footer().value] }
}

Each fresh branch acquires its own prefix implementation, producing two initialization messages. Layer.fresh isolates acquisition from the surrounding graph's memoization. Use it when separate state or resource ownership is part of the intended behavior.

Local provision boundaries

function twoLocalReads() {
  const first = Effect.provide(() => Prefix().value, PrefixLive)
  const second = Effect.provide(() => Prefix().value, PrefixLive)
  return [first, second]
}

function oneSharedReadBoundary() {
  provide PrefixLive {
    const first = Prefix().value
    const second = Prefix().value
    return [first, second]
  }
}

twoLocalReads builds twice because each Effect.provide starts a separate build boundary. oneSharedReadBoundary builds once and shares within it. Provide the application layer around the entry point; use ManagedRuntime for multiple entry points that need a common runtime lifetime.

Controlling memoization explicitly

function manualSharing() {
  return Effect.scoped {
    const memoMap = Layer.makeMemoMap()
    const scope = Effect.scope()
    const first = Layer.buildWithMemoMap(PrefixLive, memoMap, scope)
    const second = Layer.buildWithMemoMap(PrefixLive, memoMap, scope)
    return [
      Effect.provide(() => Prefix().value, first),
      Effect.provide(() => Prefix().value, second)
    ]
  }
}

Layer.makeMemoMap is an Effect value, so call it to obtain the map. Building the same layer with the same map reuses its acquisition; providing the resulting contexts does not rebuild it. The explicit scope owns resource release, and both contexts are consumed before that scope closes.

A memo map controls sharing, while a scope controls lifetime. Keep both aligned with the intended application or test boundary, and avoid retaining resource-owning implementations after their scopes close.