One thing missing: why PokeApiLive
doesn't have a dependency on PokemonCollection
and BuildPokeApiUrl
?
const make = {
getPokemon: Effect.gen(function* () {
const pokemonCollection = yield* PokemonCollection;
const buildPokeApiUrl = yield* BuildPokeApiUrl;
const requestUrl = buildPokeApiUrl({ name: pokemonCollection[0] });
const response = yield* Effect.tryPromise({
try: () => fetch(requestUrl),
catch: () => new FetchError(),
});
if (!response.ok) {
return yield* new FetchError();
}
const json = yield* Effect.tryPromise({
try: () => response.json(),
catch: () => new JsonError(),
});
return yield* Schema.decodeUnknown(Pokemon)(json);
}),
};
export class PokeApi extends Context.Tag("PokeApi")<PokeApi, typeof make>() {
static readonly Live = Layer.succeed(this, make);
}
This comes back to what we discussed previously: the dependency is not on PokeApi
, but instead on the getPokemon
function.
In practice it means that only when we use getPokemon
these dependencies will be required. It's not a requirement for the service itself.
In our example
MainLayer
provides bothPokemonCollection
andBuildPokeApiUrl
, therefore everything works as expected.
const MainLayer = Layer.mergeAll(
PokeApi.Live,
PokemonCollection.Live,
BuildPokeApiUrl.Live,
PokeApiUrl.Live
);
Dependencies on the function level are useful when only 1 function needs them. In practice is common to raise the dependency on the service since multiple functions will use the same dependency:
- Extract
PokemonCollection
andBuildPokeApiUrl
outside ofgetPokemon
- Change the definition of the service
Context
toEffect.Effect.Success<PokeApi>
- Since
make
is now anEffect
we need to useLayer.effect
instead ofLayer.succeed
const make = Effect.gen(function* () {
/// 1️⃣ Extract `PokemonCollection` and `BuildPokeApiUrl` outside of `getPokemon`
const pokemonCollection = yield* PokemonCollection;
const buildPokeApiUrl = yield* BuildPokeApiUrl;
return {
getPokemon: Effect.gen(function* () {
const requestUrl = buildPokeApiUrl({ name: pokemonCollection[0] });
const response = yield* Effect.tryPromise({
try: () => fetch(requestUrl),
catch: () => new FetchError(),
});
if (!response.ok) {
return yield* new FetchError();
}
const json = yield* Effect.tryPromise({
try: () => response.json(),
catch: () => new JsonError(),
});
return yield* Schema.decodeUnknown(Pokemon)(json);
}),
};
});
export class PokeApi extends Context.Tag("PokeApi")<
PokeApi,
/// 2️⃣ Change the definition of the service to `Effect.Effect.Success<typeof make>`
Effect.Effect.Success<typeof make>
>() {
/// 3️⃣ Use `Layer.effect` instead of `Layer.succeed`
static readonly Live = Layer.effect(this, make);
}
Effect.Effect.Success
allows to extract the success type from anyEffect
(the first type parameter).This is needed because we want the service to contain the success methods, not the
Effect
used to create it.
Last step is providing the required dependencies to PokeApiLive
:
export class PokeApi extends Context.Tag("PokeApi")<
PokeApi,
Effect.Effect.Success<typeof make>
>() {
static readonly Live = Layer.effect(this, make).pipe(
// 👇 Remember: provide dependencies directly inside `Live`
Layer.provide(Layer.mergeAll(PokemonCollection.Live, BuildPokeApiUrl.Live))
);
}
Furthermore, PokemonCollection
and BuildPokeApiUrl
are provided from PokeApi
, and PokeApiUrl
is provided from BuildPokeApiUrl
:
export class PokeApi extends Context.Tag("PokeApi")<
PokeApi,
Effect.Effect.Success<typeof make>
>() {
static readonly Live = Layer.effect(this, make).pipe(
// 👇 `PokemonCollection` and `BuildPokeApiUrl` are provided from `PokeApi`
Layer.provide(Layer.mergeAll(PokemonCollection.Live, BuildPokeApiUrl.Live))
);
}
export class BuildPokeApiUrl extends Context.Tag("BuildPokeApiUrl")<
BuildPokeApiUrl,
({ name }: { name: string }) => string
>() {
static readonly Live = Layer.effect(
this,
Effect.gen(function* () {
const pokeApiUrl = yield* PokeApiUrl;
return ({ name }) => `${pokeApiUrl}/${name}`;
})
).pipe(
// 👇 `PokeApiUrl` is provided from `BuildPokeApiUrl`
Layer.provide(PokeApiUrl.Live)
);
}
This means that we don't need to provide them in MainLayer
:
const MainLayer = Layer.mergeAll(
PokeApi.Live,
PokemonCollection.Live,
BuildPokeApiUrl.Live,
PokeApiUrl.Live
);
MainLayer
only requires PokeApi
since it is used directly inside program
. This is the final result:
import { Effect, Layer } from "effect";
import { PokeApi } from "./PokeApi";
const MainLayer = Layer.mergeAll(PokeApi.Live);
export const program = Effect.gen(function* () {
const pokeApi = yield* PokeApi;
return yield* pokeApi.getPokemon;
});
const runnable = program.pipe(Effect.provide(MainLayer));
const main = runnable.pipe(
Effect.catchTags({
FetchError: () => Effect.succeed("Fetch error"),
JsonError: () => Effect.succeed("Json error"),
ParseError: () => Effect.succeed("Parse error"),
})
);
Effect.runPromise(main).then(console.log);
Done! Now we organized all services, layers, and their dependencies.
Every service implementation has its own dependencies provided directly when Layer
is created.
This allows to have a flat MainLayer
with a single mergeAll
. Each dependency is defined and provided in separate files, so that we can focus on one service at the time before composing everything together.
All type safe!