layer-map.example.ts 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. export * as LayerMapExample from "./layer-map.example"
  2. import { Context, Effect, Layer, LayerMap } from "effect"
  3. import { Npm } from "@opencode-ai/util/npm"
  4. /**
  5. * Tutorial: split global services from context-specific services.
  6. *
  7. * Use this pattern when part of the app should be constructed once at the app edge,
  8. * while another part should be cached per request/project/workspace key.
  9. *
  10. * In this example:
  11. * - Npm.Service is the global service. It is not keyed by request context and should
  12. * be provided once by the application runtime.
  13. * - ConfigService is context-specific. It is built from a RequestContext key and is
  14. * cached by LayerMap for that key.
  15. * - ConfigServiceMap.layer owns the cache. Provide it once globally, then each
  16. * request can provide ConfigServiceMap.get(context) to select the right instance.
  17. *
  18. * Lifetime model:
  19. * - ConfigServiceMap.layer has the app/global lifetime and depends on Npm.Service.
  20. * - ConfigServiceMap.get(context) has the request/context lifetime and provides
  21. * ConfigService for exactly that context key.
  22. * - The cached ConfigService entry stays alive while something is using it. Once idle,
  23. * it remains cached for idleTimeToLive, then its scope is finalized.
  24. * - invalidate(context) removes the cache entry for future lookups. Active users keep
  25. * running on the old instance; the next lookup can create a fresh instance.
  26. *
  27. * Key model:
  28. * - Keys can be strings, structs, classes, arrays, etc.
  29. * - Prefer primitive or immutable keys. Effect uses Hash / Equal semantics for cache
  30. * lookup, so mutating an object after it has been used as a key is a bug.
  31. */
  32. export type RequestContext = {
  33. readonly directory: string
  34. readonly workspace: string
  35. }
  36. export class RequestContextRef extends Context.Service<RequestContextRef, RequestContext>()(
  37. "@opencode/example/RequestContextRef",
  38. ) {}
  39. export interface ConfigServiceShape {
  40. readonly directory: string
  41. readonly workspace: string
  42. readonly nextUse: () => Effect.Effect<number>
  43. readonly which: Npm.Interface["which"]
  44. }
  45. export class ConfigService extends Context.Service<ConfigService, ConfigServiceShape>()(
  46. "@opencode/example/ConfigService",
  47. ) {}
  48. const configServiceLayer = Layer.effect(
  49. ConfigService,
  50. Effect.gen(function* () {
  51. const context = yield* RequestContextRef
  52. const npm = yield* Npm.Service
  53. let useCount = 0
  54. return ConfigService.of({
  55. directory: context.directory,
  56. workspace: context.workspace,
  57. nextUse: () => Effect.succeed(++useCount),
  58. which: npm.which,
  59. })
  60. }),
  61. )
  62. export class ConfigServiceMap extends LayerMap.Service<ConfigServiceMap>()("@opencode/example/ConfigServiceMap", {
  63. lookup: (context: RequestContext) =>
  64. configServiceLayer.pipe(Layer.provide(Layer.succeed(RequestContextRef, RequestContextRef.of(context)))),
  65. idleTimeToLive: "5 minutes",
  66. }) {}
  67. export const appLayer = ConfigServiceMap.layer
  68. export const readConfig = Effect.fn("LayerMapExample.readConfig")(function* () {
  69. const config = yield* ConfigService
  70. return {
  71. directory: config.directory,
  72. workspace: config.workspace,
  73. useCount: yield* config.nextUse(),
  74. }
  75. })
  76. export const handleRequest = Effect.fn("LayerMapExample.handleRequest")(function* (context: RequestContext) {
  77. return yield* readConfig().pipe(Effect.provide(ConfigServiceMap.get(context)))
  78. })
  79. export const invalidateContext = (context: RequestContext) => ConfigServiceMap.invalidate(context)