This commit is contained in:
@@ -81,10 +81,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@effect/schema": "^0.72.2",
|
"@effect/schema": "^0.72.2",
|
||||||
"@prisma/studio-server": "^0.502.0",
|
|
||||||
"@tanstack/form-core": "^0.30.0",
|
"@tanstack/form-core": "^0.30.0",
|
||||||
|
"@trpc/server": "^10.45.2",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
|
"@types/ws": "^8.5.12",
|
||||||
"bun-types": "^1.1.26",
|
"bun-types": "^1.1.26",
|
||||||
"effect": "^3.7.2",
|
"effect": "^3.7.2",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
@@ -95,6 +96,7 @@
|
|||||||
"openai": "^4.57.3",
|
"openai": "^4.57.3",
|
||||||
"tsup": "^8.2.4",
|
"tsup": "^8.2.4",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4",
|
||||||
|
"ws": "^8.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./express"
|
export * from "./express"
|
||||||
export * as JSONWebToken from "./JSONWebToken"
|
export * as JSONWebToken from "./JSONWebToken"
|
||||||
export * as OpenAIClient from "./OpenAIClient"
|
export * as OpenAIClient from "./OpenAIClient"
|
||||||
|
export * from "./trpc"
|
||||||
|
|||||||
38
src/Layers/trpc/TRPCContext.ts
Normal file
38
src/Layers/trpc/TRPCContext.ts
Normal 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>()
|
||||||
55
src/Layers/trpc/TRPCContextCreator.ts
Normal file
55
src/Layers/trpc/TRPCContextCreator.ts
Normal 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 }
|
||||||
|
}
|
||||||
67
src/Layers/trpc/createTRCPErrorMapper.ts
Normal file
67
src/Layers/trpc/createTRCPErrorMapper.ts
Normal 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
1
src/Layers/trpc/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * as TRPCContextCreator from "./TRPCContextCreator"
|
||||||
Reference in New Issue
Block a user