|
| 1 | +import { Context, Effect } from "effect" |
| 2 | + |
| 3 | +type EffectMethod = (...args: ReadonlyArray<never>) => Effect.Effect<unknown, unknown, unknown> |
| 4 | + |
| 5 | +type ServiceUse<Identifier, Shape> = { |
| 6 | + readonly [Key in keyof Shape as Shape[Key] extends EffectMethod ? Key : never]: Shape[Key] extends ( |
| 7 | + ...args: infer Args |
| 8 | + ) => infer Return |
| 9 | + ? Args extends ReadonlyArray<unknown> |
| 10 | + ? Return extends Effect.Effect<infer A, infer E, infer R> |
| 11 | + ? (...args: Args) => Effect.Effect<A, E, R | Identifier> |
| 12 | + : never |
| 13 | + : never |
| 14 | + : never |
| 15 | +} |
| 16 | + |
| 17 | +export const serviceUse = <Identifier, Shape>(tag: Context.Service<Identifier, Shape>) => { |
| 18 | + // This is the only dynamic boundary: TypeScript knows the accessor shape, |
| 19 | + // but Proxy property names are runtime values. |
| 20 | + const access = new Proxy( |
| 21 | + {}, |
| 22 | + { |
| 23 | + get: (_, key) => { |
| 24 | + if (typeof key !== "string") return undefined |
| 25 | + return (...args: unknown[]) => |
| 26 | + tag.use((service) => { |
| 27 | + // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy keys are checked at runtime. |
| 28 | + const method = service[key as keyof Shape] |
| 29 | + if (typeof method !== "function") return Effect.die(new Error(`Service method not found: ${key}`)) |
| 30 | + // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- ServiceUse exposes only Effect-returning methods. |
| 31 | + return (method as (...args: unknown[]) => Effect.Effect<unknown, unknown, unknown>)(...args) |
| 32 | + }) |
| 33 | + }, |
| 34 | + }, |
| 35 | + ) |
| 36 | + // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy implements the mapped accessor surface lazily. |
| 37 | + return access as ServiceUse<Identifier, Shape> |
| 38 | +} |
0 commit comments