TRPCContextCreator
All checks were successful
Lint / lint (push) Successful in 12s

This commit is contained in:
Julien Valverdé
2024-09-05 06:07:29 +02:00
parent ca10286e1f
commit 85b6340b97
7 changed files with 166 additions and 2 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -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"
}
}

View File

@@ -1,3 +1,4 @@
export * from "./express"
export * as JSONWebToken from "./JSONWebToken"
export * as OpenAIClient from "./OpenAIClient"
export * from "./trpc"

View File

@@ -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<R> {
readonly runtime: Runtime.Runtime<R>
readonly run: <A, E>(
effect: Effect.Effect<A, E, R>,
options?: { readonly signal?: AbortSignal },
) => Promise<A>
readonly fork: <A, E>(
effect: Effect.Effect<A, E, R>,
options?: Runtime.RunForkOptions,
) => RuntimeFiber<A, TRPCError>
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<TRPCContextTransaction>()

View File

@@ -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 = <R>() => {
class TRPCContextCreator extends Context.Tag("TRPCContextCreator")<TRPCContextCreator, {
readonly createContext: (transaction: TRPCContextTransaction) => TRPCContext<R>
readonly createExpressContext: (context: CreateExpressContextOptions) => TRPCContext<R>
readonly createWebSocketContext: (context: CreateWSSContextFnOptions) => TRPCContext<R>
}>() {}
const TRPCContextCreatorLive = Layer.effect(TRPCContextCreator, Effect.gen(function*() {
const runtime = yield* Effect.runtime<R>()
const mapErrors = yield* createTRCPErrorMapper
const run = <A, E>(
effect: Effect.Effect<A, E, R>,
options?: { readonly signal?: AbortSignal },
) => Runtime.runPromise(runtime)(
effect.pipe(mapErrors),
options,
)
const fork = <A, E>(
effect: Effect.Effect<A, E, R>,
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 }
}

View File

@@ -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 }) =>
<A, E, R>(effect: Effect.Effect<A, E, R>) => 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<E> extends Error {
constructor(readonly cause: Cause.Cause<E>) {
super()
}
}

1
src/Layers/trpc/index.ts Normal file
View File

@@ -0,0 +1 @@
export * as TRPCContextCreator from "./TRPCContextCreator"