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
MainLayerprovides bothPokemonCollectionandBuildPokeApiUrl, 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
PokemonCollectionandBuildPokeApiUrloutside ofgetPokemon - Change the definition of the service
ContexttoEffect.Effect.Success<PokeApi> - Since
makeis now anEffectwe need to useLayer.effectinstead 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.Successallows 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
Effectused 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!
