diff --git a/bun.lockb b/bun.lockb index 2d6c81a..5bb451d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 23e5299..255ffcb 100644 --- a/package.json +++ b/package.json @@ -81,10 +81,11 @@ }, "devDependencies": { "@effect/schema": "^0.72.2", - "@prisma/studio-server": "^0.502.0", "@tanstack/form-core": "^0.30.0", + "@trpc/server": "^10.45.2", "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.6", + "@types/ws": "^8.5.12", "bun-types": "^1.1.26", "effect": "^3.7.2", "express": "^4.19.2", @@ -95,6 +96,7 @@ "openai": "^4.57.3", "tsup": "^8.2.4", "tsx": "^4.19.0", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "ws": "^8.18.0" } } diff --git a/src/Layers/index.ts b/src/Layers/index.ts index 2f143bb..3784801 100644 --- a/src/Layers/index.ts +++ b/src/Layers/index.ts @@ -1,3 +1,4 @@ export * from "./express" export * as JSONWebToken from "./JSONWebToken" export * as OpenAIClient from "./OpenAIClient" +export * from "./trpc" diff --git a/src/Layers/trpc/TRPCContext.ts b/src/Layers/trpc/TRPCContext.ts new file mode 100644 index 0000000..94089d8 --- /dev/null +++ b/src/Layers/trpc/TRPCContext.ts @@ -0,0 +1,38 @@ +import type { TRPCError } from "@trpc/server" +import { Data, type Effect, type Runtime } from "effect" +import type { RuntimeFiber } from "effect/Fiber" +import type express from "express" +import type { IncomingMessage } from "node:http" +import type { WebSocket } from "ws" + + +export interface TRPCContext { + readonly runtime: Runtime.Runtime + + readonly run: ( + effect: Effect.Effect, + options?: { readonly signal?: AbortSignal }, + ) => Promise + + readonly fork: ( + effect: Effect.Effect, + options?: Runtime.RunForkOptions, + ) => RuntimeFiber + + readonly transaction: TRPCContextTransaction +} + + +export type TRPCContextTransaction = Data.TaggedEnum<{ + readonly Express: { + readonly req: express.Request + readonly res: express.Response + } + + readonly WebSocket: { + readonly req: IncomingMessage + readonly res: WebSocket + } +}> + +export const TRPCContextTransactionEnum = Data.taggedEnum() diff --git a/src/Layers/trpc/TRPCContextCreator.ts b/src/Layers/trpc/TRPCContextCreator.ts new file mode 100644 index 0000000..06278f8 --- /dev/null +++ b/src/Layers/trpc/TRPCContextCreator.ts @@ -0,0 +1,55 @@ +import type { CreateExpressContextOptions } from "@trpc/server/adapters/express" +import type { CreateWSSContextFnOptions } from "@trpc/server/adapters/ws" +import { Context, Effect, Layer, Runtime } from "effect" +import { createTRCPErrorMapper } from "./createTRCPErrorMapper" +import { TRPCContextTransactionEnum, type TRPCContext, type TRPCContextTransaction } from "./TRPCContext" + + +export { TRPCErrorCause } from "./createTRCPErrorMapper" +export { TRPCContextTransactionEnum } from "./TRPCContext" +export type { TRPCContext, TRPCContextTransaction } from "./TRPCContext" + + +export const makeService = () => { + class TRPCContextCreator extends Context.Tag("TRPCContextCreator") TRPCContext + readonly createExpressContext: (context: CreateExpressContextOptions) => TRPCContext + readonly createWebSocketContext: (context: CreateWSSContextFnOptions) => TRPCContext + }>() {} + + const TRPCContextCreatorLive = Layer.effect(TRPCContextCreator, Effect.gen(function*() { + const runtime = yield* Effect.runtime() + const mapErrors = yield* createTRCPErrorMapper + + const run = ( + effect: Effect.Effect, + options?: { readonly signal?: AbortSignal }, + ) => Runtime.runPromise(runtime)( + effect.pipe(mapErrors), + options, + ) + + const fork = ( + effect: Effect.Effect, + options?: Runtime.RunForkOptions, + ) => Runtime.runFork(runtime)( + effect.pipe(mapErrors), + options, + ) + + + const createContext = (transaction: TRPCContextTransaction) => ({ + runtime, + run, + fork, + transaction, + }) + + const createExpressContext = (context: CreateExpressContextOptions) => createContext(TRPCContextTransactionEnum.Express(context)) + const createWebSocketContext = (context: CreateWSSContextFnOptions) => createContext(TRPCContextTransactionEnum.WebSocket(context)) + + return { createContext, createExpressContext, createWebSocketContext } + })) + + return { TRPCContextCreator, TRPCContextCreatorLive } +} diff --git a/src/Layers/trpc/createTRCPErrorMapper.ts b/src/Layers/trpc/createTRCPErrorMapper.ts new file mode 100644 index 0000000..405d636 --- /dev/null +++ b/src/Layers/trpc/createTRCPErrorMapper.ts @@ -0,0 +1,67 @@ +import { Effect, type Cause } from "effect" + + +const importTRPCServer = Effect.tryPromise({ + try: () => import("@trpc/server"), + catch: cause => new Error("Could not import '@trpc/server'. Make sure it is installed.", { cause }), +}) + +export const createTRCPErrorMapper = importTRPCServer.pipe(Effect.map(({ TRPCError }) => + (effect: Effect.Effect) => Effect.sandbox(effect).pipe( + Effect.catchTags({ + Empty: cause => Effect.fail( + new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + cause: new TRPCErrorCause(cause), + }) + ), + + Fail: cause => Effect.fail( + cause.error instanceof TRPCError + ? cause.error + : new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + cause: new TRPCErrorCause(cause), + }) + ), + + Die: cause => Effect.fail( + cause.defect instanceof TRPCError + ? cause.defect + : new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + cause: new TRPCErrorCause(cause), + }) + ), + + Interrupt: cause => Effect.fail( + new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + cause: new TRPCErrorCause(cause), + }) + ), + + Sequential: cause => Effect.fail( + new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + cause: new TRPCErrorCause(cause), + }) + ), + + Parallel: cause => Effect.fail( + new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + cause: new TRPCErrorCause(cause), + }) + ), + }), + + Effect.tapError(Effect.logError), + ) +)) + +export class TRPCErrorCause extends Error { + constructor(readonly cause: Cause.Cause) { + super() + } +} diff --git a/src/Layers/trpc/index.ts b/src/Layers/trpc/index.ts new file mode 100644 index 0000000..b34aba4 --- /dev/null +++ b/src/Layers/trpc/index.ts @@ -0,0 +1 @@ +export * as TRPCContextCreator from "./TRPCContextCreator"