Skip to content

Instantly share code, notes, and snippets.

@xesrevinu
Created April 17, 2024 04:16
Show Gist options
  • Select an option

  • Save xesrevinu/ec6264c05f69d4e39a20301e555d03a1 to your computer and use it in GitHub Desktop.

Select an option

Save xesrevinu/ec6264c05f69d4e39a20301e555d03a1 to your computer and use it in GitHub Desktop.
effect-worker.ts
import { WorkerEntrypoint } from "cloudflare:workers"
import type { D1Database, ExecutionContext, KVNamespace, R2Bucket } from "@cloudflare/workers-types"
import { Context, Data, Effect, Layer, pipe } from "effect"
interface Env {
KV: KVNamespace
D1: D1Database
BUCKET: R2Bucket
AUTH_SERVICE: Service<AuthServiceWorker>
}
// AuthService.ts
export class UserNotFound extends Data.TaggedError("UserNotFound") {}
export interface UserInfo { username: string }
export interface AuthService_ { // define service api
readonly getUser: (token: string) => Promise<UserInfo | null>
}
export interface AuthService extends Effectify<AuthService_, RPCError> {}
export const AuthService = Context.GenericTag<AuthService>("AuthService") // Effect based on Tag dependency injection
export class AuthServiceWorker extends WorkerEntrypoint<Env> implements AuthService_ { // a remote rpc implementation
async getUser(token: string): Promise<UserInfo | null> {
// this.env.BUCKET
// this.env.D1
return await this.env.KV.get<UserInfo>(token)
}
}
export const AuthServiceTesting = Layer.succeed(AuthService, { // a testing implementation
getUser: (token: string) => Effect.succeed({ username: "test" } satisfies UserInfo),
})
// Your business logic ⬇️
// you don't need to care about the implementation details
// and your can check the type of the effect
// Success, Error Requirements
// Effect.Effect<UserInfo, RPCError | BadRequest | UserNotFound, Request | AuthService>
export const getUserInfo = Effect.gen(function* (_) {
const request = yield* _(Request)
const authService = yield* _(AuthService)
const d1 = yield* _(D1)
// recommend using @effect/schema to validate the request
const { token } = yield* _(
Effect.tryPromise({
try: () => request.json<{ token: string }>(),
catch: () => new BadRequest(),
})
)
yield* _(d1.exec(
`INSERT INTO access_log ${token} getUserInfo`
), Effect.ignoreLogged)
const user = yield* _(authService.getUser(token))
if (!user) {
return yield* _(Effect.fail(new UserNotFound()))
}
return user
})
// more ...
// test.spec.ts ⬇️
it('get user info', async () => {
const request = new Request("http://localhost/user", {
body: JSON.stringify({ token: "test" }),
});
const Testing = Layer.mergeAll(
Layer.succeed(Request, request),
// provide a testing implementation
AuthServiceTesting,
// local d1
D1Wrangler
)
const result = await pipe(getUserInfo, Effect.provide(Testing), Effect.runPromise)
expect(result).toEqual({ username: "test" })
})
// worker.ts ⬇️
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
if (request.url === "/") {
return new Response("Hello, world!")
}
if (request.url === "/user") {
const Live = Layer.mergeAll(
Layer.succeed(Request, request),
rpcService(AuthService, () => env.AUTH_SERVICE),
D1FromEnv(() => env.D1)
)
return pipe(
getUserInfo,
Effect.map((_) => new Response(JSON.stringify(_))),
Effect.catchTags({
BadRequest: () => Effect.succeed(new Response("Bad request", { status: 400 })),
UserNotFound: () => Effect.succeed(new Response("User not found", { status: 404 })),
RPCError: () => Effect.succeed(new Response("Internal server error", { status: 500 })),
}),
// unexpected error
Effect.catchAllCause((cause) => Effect.succeed(new Response(cause.toString(), { status: 500 }))),
Effect.provide(Live),
Effect.runPromise
)
}
},
}
export const Request = Context.GenericTag<Request>("Request")
export class RPCError extends Data.TaggedError("RPCError")<{ readonly reason: unknown }> {}
export class D1Error extends Data.TaggedError("D1Error")<{ readonly reason: unknown }> {}
export class BadRequest extends Data.TaggedError("BadRequest")<{}> {}
export type Effectify<T, E> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R ? (...args: A) => Effect.Effect<Awaited<R>, E> : never
}
export const wrapService = <T extends object, E>(service: T): Effectify<T, E> =>
new Proxy(service, {
get(target, prop) {
// can do better
return (...args: any) =>
Effect.mapError(
Effect.promise(() => target[prop](...args)),
(err) => new RPCError({ reason: err }),
)
},
}) as Effectify<T, E>
export const rpcService = <S extends Service, T extends Context.Tag<any, any>>(tag: T , service: () => S) => {
let wrap = wrapService<S,RPCError>(service())
return Layer.succeed(tag, wrap as any)
}
export interface D1 {
exec: (query: string) => Effect.Effect<D1ExecResult, D1Error>
}
export const D1 = Context.GenericTag<D1>("D1")
export const D1FromEnv = (d1: () => D1Database) => {
const database = d1();
return Layer.succeed(D1, {
exec: (sql) => Effect.promise(() => database.exec(sql))
})
}
export const D1Wrangler = Layer.effect(D1, Effect.gen(function* (_) {
const miniflare = new Miniflare({
// ...
})
const env = Effect.promise(() => miniflare.getBindings())
return {
exec: (query: string) => Effect.promise(() => env.D1.exec(query))
}
}))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment