From 02d8e38f4dddae942f7eb0fe1b3a3ea1fa4af46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 7 Sep 2024 20:56:30 +0200 Subject: [PATCH] 0.1.17 (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julien Valverdé Reviewed-on: https://git.valverde.cloud/Thilawyn/thilalib/pulls/18 --- bun.lockb | Bin 104113 -> 107036 bytes package.json | 107 +++++++++++--------------- src/Express/ExpressApp.ts | 22 ++++++ src/Express/ExpressNodeHTTPServer.ts | 70 +++++++++++++++++ src/Express/example.ts | 17 ++++ src/Express/index.ts | 2 + src/ImportError.ts | 11 +++ src/{Layers => }/JSONWebToken.ts | 13 ++-- src/Layers/index.ts | 5 -- src/OpenAIClient.ts | 69 +++++++++++++++++ src/{Layers => }/PrismaStudioRoute.ts | 0 src/TRPC/TRPCBuilder.ts | 20 +++++ src/TRPC/TRPCContext.ts | 38 +++++++++ src/TRPC/TRPCContextCreator.ts | 57 ++++++++++++++ src/TRPC/TRPCExpressRoute.ts | 27 +++++++ src/TRPC/TRPCRouter.ts | 18 +++++ src/TRPC/TRPCWebSocketServer.ts | 66 ++++++++++++++++ src/TRPC/createTRCPErrorMapper.ts | 63 +++++++++++++++ src/TRPC/example.ts | 51 ++++++++++++ src/TRPC/importTRPCServer.ts | 11 +++ src/TRPC/index.ts | 7 ++ src/TRPC/middlewares.ts | 44 +++++++++++ src/index.ts | 5 +- tsconfig.json | 16 ++-- tsup.config.ts | 9 ++- 25 files changed, 666 insertions(+), 82 deletions(-) create mode 100644 src/Express/ExpressApp.ts create mode 100644 src/Express/ExpressNodeHTTPServer.ts create mode 100644 src/Express/example.ts create mode 100644 src/Express/index.ts create mode 100644 src/ImportError.ts rename src/{Layers => }/JSONWebToken.ts (88%) delete mode 100644 src/Layers/index.ts create mode 100644 src/OpenAIClient.ts rename src/{Layers => }/PrismaStudioRoute.ts (100%) create mode 100644 src/TRPC/TRPCBuilder.ts create mode 100644 src/TRPC/TRPCContext.ts create mode 100644 src/TRPC/TRPCContextCreator.ts create mode 100644 src/TRPC/TRPCExpressRoute.ts create mode 100644 src/TRPC/TRPCRouter.ts create mode 100644 src/TRPC/TRPCWebSocketServer.ts create mode 100644 src/TRPC/createTRCPErrorMapper.ts create mode 100644 src/TRPC/example.ts create mode 100644 src/TRPC/importTRPCServer.ts create mode 100644 src/TRPC/index.ts create mode 100644 src/TRPC/middlewares.ts diff --git a/bun.lockb b/bun.lockb index 22706bc996ac2568b292fd7a9aaeffed48e21e27..9ff58ea702e4394881280da8bd5aa6670db3a7f0 100755 GIT binary patch delta 27726 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

&HSkZ;q7Di2Fy^RFzBD_tF>Bnq^s71`u!p9|J z;D`{4vzqQYEZvYuPKnbCp;MTP&}kCh3@$8Um4xMs*id1~GG-!lUWWGtHz9EuOUnMc zF}Z60b&OiWN14=9W^x7sn)>5~V>C+pK?c7Q%3 zc92%el=BAaGqkCR(HR3$(vr&ZlJy8>o|2{=q)!}_s!x-;(R$z`)mUFYFTJOSo0nI# zU$mRMPn^HMm#|sbZ8~g{6s;W=o&K#8Ll%WNPS+-; zM8^rC?V0sJx#C}n^=UK}Qafdp^l9a6@IM%z5FO2|qa@S*b&TlgubGC9{wG!tI`w1q zyyc@cT9uPp>2OUGH;_Vh56Dc^4oJ^Tj>jrUk4}V33HcqErFHonR4>nJJ%oHcYan1D z7@3kmP%fXkwLXITNYyRiCA430BUUvAPd=TzZOSz2!)bq;H2 zF3*wZYmgJEq0n9~EbmxiTV;M`5Dk?@%ljon%d_QXSC%$7wHBrllA~C?K7Uh?9-oqo zmdc4z&i`r#lbCH?gNY2ym1|fsh`r%$5Cw*mR(u z$~L7j`>NsrANfRu(CN&f3f@4$;|`PAj$;j+WnB&XrnGTTQ_?;v@zNe8yD)18YhdwBjGB}+LY0aJdnltslYQOJ#8Ru=z~m4TYlc4=@AVe@oWu?BLbHw(BdePlpB zYb)H!W7U|K5Hg1~=dMyW*TL zEbG9m#9vjR9Xj`G{iTIgeLVY3cs_wOsVzq^hk%^adI?*x;>^D_j*gK5N8zH2?OjLFu=&p}w$tCacYSg^M$pYhmSCYyw_wgxvX< z{q39`s06E#-a_@I_-)agIjoQ1v4GhLHRrObi)7u diff --git a/package.json b/package.json index 2545760..52f0e92 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@thilawyn/thilalib", - "version": "0.1.16", + "version": "0.1.17", "type": "module", "files": [ "./dist" @@ -8,68 +8,44 @@ "types": "./dist/index.d.ts", "exports": { ".": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "require": { - "types": "./dist/index.d.cts", - "default": "./dist/index.cjs" - } + "types": "./dist/index.d.ts", + "default": "./dist/index.js" }, - "./Layers": { - "import": { - "types": "./dist/Layers/index.d.ts", - "default": "./dist/Layers/index.js" - }, - "require": { - "types": "./dist/Layers/index.d.cts", - "default": "./dist/Layers/index.cjs" - } + "./Express": { + "types": "./dist/Express/index.d.ts", + "default": "./dist/Express/index.js" }, - "./Schema": { - "import": { - "types": "./dist/Schema/index.d.ts", - "default": "./dist/Schema/index.js" - }, - "require": { - "types": "./dist/Schema/index.d.cts", - "default": "./dist/Schema/index.cjs" - } - }, - "./Schema/MobX": { - "import": { - "types": "./dist/Schema/MobX/index.d.ts", - "default": "./dist/Schema/MobX/index.js" - }, - "require": { - "types": "./dist/Schema/MobX/index.d.cts", - "default": "./dist/Schema/MobX/index.cjs" - } - }, - "./Schema/TanStackForm": { - "import": { - "types": "./dist/Schema/TanStackForm/index.d.ts", - "default": "./dist/Schema/TanStackForm/index.js" - }, - "require": { - "types": "./dist/Schema/TanStackForm/index.d.cts", - "default": "./dist/Schema/TanStackForm/index.cjs" - } + "./TRPC": { + "types": "./dist/TRPC/index.d.ts", + "default": "./dist/TRPC/index.js" }, "./Types": { - "import": { - "types": "./dist/Types/index.d.ts", - "default": "./dist/Types/index.js" - }, - "require": { - "types": "./dist/Types/index.d.cts", - "default": "./dist/Types/index.cjs" - } + "types": "./dist/Types/index.d.ts", + "default": "./dist/Types/index.js" + }, + "./Schema": { + "types": "./dist/Schema/index.d.ts", + "default": "./dist/Schema/index.js" + }, + "./Schema/MobX": { + "types": "./dist/Schema/MobX/index.d.ts", + "default": "./dist/Schema/MobX/index.js" + }, + "./Schema/TanStackForm": { + "types": "./dist/Schema/TanStackForm/index.d.ts", + "default": "./dist/Schema/TanStackForm/index.js" + }, + "./JSONWebToken": { + "types": "./dist/JSONWebToken.d.ts", + "default": "./dist/JSONWebToken.js" + }, + "./OpenAIClient": { + "types": "./dist/OpenAIClient.d.ts", + "default": "./dist/OpenAIClient.js" } }, "scripts": { - "build": "tsup", + "build": "tsc", "lint:tsc": "tsc --noEmit", "clean:cache": "rm -f tsconfig.tsbuildinfo", "clean:dist": "rm -rf dist", @@ -80,18 +56,23 @@ "type-fest": "^4.26.0" }, "devDependencies": { - "@effect/schema": "^0.72.0", - "@prisma/studio-server": "^0.502.0", - "@tanstack/form-core": "^0.30.0", + "@effect/schema": "^0.72.3", + "@tanstack/form-core": "^0.32.0", + "@trpc/server": "^10.45.2", + "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.6", - "bun-types": "^1.1.26", - "effect": "^3.7.0", + "@types/ws": "^8.5.12", + "bun-types": "^1.1.27", + "effect": "^3.7.2", + "express": "^4.19.2", "jsonwebtoken": "^9.0.2", - "mobx": "^6.13.1", + "mobx": "^6.13.2", "npm-check-updates": "^17.1.1", "npm-sort": "^0.0.4", + "openai": "^4.58.1", "tsup": "^8.2.4", "tsx": "^4.19.0", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "ws": "^8.18.0" } } diff --git a/src/Express/ExpressApp.ts b/src/Express/ExpressApp.ts new file mode 100644 index 0000000..af91481 --- /dev/null +++ b/src/Express/ExpressApp.ts @@ -0,0 +1,22 @@ +import { Config, Context, Effect, Layer } from "effect" +import type { Express } from "express" +import { ImportError } from "../ImportError" + + +export class ExpressApp extends Context.Tag("ExpressApp")() {} + + +const importExpress = Effect.tryPromise({ + try: () => import("express"), + catch: cause => new ImportError({ path: "express", cause }), +}) + +export const ExpressAppLive = ( + config: { + readonly trustProxy?: Config.Config + } = {} +) => Layer.effect(ExpressApp, Effect.gen(function*() { + const app = (yield* importExpress).default() + app.set("trust proxy", (yield* config.trustProxy ?? Config.succeed(undefined)) ?? false) + return app +})) diff --git a/src/Express/ExpressNodeHTTPServer.ts b/src/Express/ExpressNodeHTTPServer.ts new file mode 100644 index 0000000..25ff95f --- /dev/null +++ b/src/Express/ExpressNodeHTTPServer.ts @@ -0,0 +1,70 @@ +import { Config, Context, Effect, Layer, Match } from "effect" +import type { Server } from "node:http" +import type { AddressInfo } from "node:net" +import { ImportError } from "../ImportError" +import { ExpressApp } from "./ExpressApp" + + +export class ExpressNodeHTTPServer extends Context.Tag("ExpressNodeHTTPServer")() {} + + +const importNodeHTTP = Effect.tryPromise({ + try: () => import("node:http"), + catch: cause => new ImportError({ path: "node:http", cause }), +}) + +const serverListeningMessage = Match.type().pipe( + Match.when(Match.null, () => "HTTP server listening"), + Match.when(Match.string, v => `HTTP server listening on ${ v }`), + Match.orElse(v => `HTTP server listening on ${ v.address === "::" ? "*" : v.address }:${ v.port }`), +) + +export const ExpressNodeHTTPServerLive = ( + config: { + readonly backlog?: Config.Config + readonly exclusive?: Config.Config + readonly host?: Config.Config + readonly ipv6Only?: Config.Config + readonly path?: Config.Config + readonly port?: Config.Config + readonly readableAll?: Config.Config + readonly signal?: AbortSignal + readonly writableAll?: Config.Config + } = {} +) => Layer.effect(ExpressNodeHTTPServer, Effect.acquireRelease( + Effect.gen(function*() { + const app = yield* ExpressApp + const http = yield* importNodeHTTP + + const options = { + backlog: yield* config.backlog ?? Config.succeed(undefined), + exclusive: yield* config.exclusive ?? Config.succeed(undefined), + host: yield* config.host ?? Config.succeed(undefined), + ipv6Only: yield* config.ipv6Only ?? Config.succeed(undefined), + path: yield* config.path ?? Config.succeed(undefined), + port: yield* config.port ?? Config.succeed(undefined), + readableAll: yield* config.readableAll ?? Config.succeed(undefined), + signal: config.signal, + writableAll: yield* config.writableAll ?? Config.succeed(undefined), + } as const + + return yield* Effect.async(resume => { + const server = http.createServer(app).listen(options, + () => resume( + Effect.succeed(server).pipe( + Effect.tap(Effect.logInfo( + serverListeningMessage(server.address()) + )) + ) + ) + ) + }) + }), + + server => Effect.gen(function*() { + yield* Effect.logInfo("HTTP server is stopping. Waiting for existing connections to end...") + yield* Effect.async(resume => { + server.close(() => resume(Effect.logInfo("HTTP server closed"))) + }) + }), +)) diff --git a/src/Express/example.ts b/src/Express/example.ts new file mode 100644 index 0000000..11fd4f1 --- /dev/null +++ b/src/Express/example.ts @@ -0,0 +1,17 @@ +import { Effect, Layer } from "effect" +import { ExpressAppLive } from "./ExpressApp" +import { ExpressNodeHTTPServerLive } from "./ExpressNodeHTTPServer" + + +const AppLive = ExpressAppLive() +const HTTPServerLive = ExpressNodeHTTPServerLive() + +const ServerLive = Layer.empty.pipe( + Layer.provideMerge(HTTPServerLive), + Layer.provideMerge(AppLive), +) + +Layer.launch(ServerLive).pipe( + Effect.scoped, + Effect.runPromise, +) diff --git a/src/Express/index.ts b/src/Express/index.ts new file mode 100644 index 0000000..c301111 --- /dev/null +++ b/src/Express/index.ts @@ -0,0 +1,2 @@ +export * as ExpressApp from "./ExpressApp" +export * as ExpressNodeHTTPServer from "./ExpressNodeHTTPServer" diff --git a/src/ImportError.ts b/src/ImportError.ts new file mode 100644 index 0000000..04b654e --- /dev/null +++ b/src/ImportError.ts @@ -0,0 +1,11 @@ +import { Data } from "effect" + + +export class ImportError extends Data.TaggedError("ImportError")<{ + path: string + cause: unknown +}> { + toString(): string { + return `Could not import '${ this.path }'` + } +} diff --git a/src/Layers/JSONWebToken.ts b/src/JSONWebToken.ts similarity index 88% rename from src/Layers/JSONWebToken.ts rename to src/JSONWebToken.ts index 24bf772..401ee6f 100644 --- a/src/Layers/JSONWebToken.ts +++ b/src/JSONWebToken.ts @@ -1,8 +1,11 @@ import { Context, Effect, Layer } from "effect" import type * as JWT from "jsonwebtoken" +import { ImportError } from "./ImportError" -export class JSONWebToken extends Context.Tag("JSONWebToken")() {} + +export interface JSONWebTokenService { sign: ( payload: string | object | Buffer, secretOrPrivateKey: JWT.Secret, @@ -11,7 +14,7 @@ export class JSONWebToken extends Context.Tag("JSONWebToken"), + > verify: ( token: string, @@ -21,13 +24,13 @@ export class JSONWebToken extends Context.Tag("JSONWebToken"), -}>() {} + > +} const importJWT = Effect.tryPromise({ try: () => import("jsonwebtoken"), - catch: cause => new Error("Could not import 'jsonwebtoken'. Make sure it is installed.", { cause }), + catch: cause => new ImportError({ path: "jsonwebtoken", cause }), }) export const JSONWebTokenLive = Layer.effect(JSONWebToken, importJWT.pipe( diff --git a/src/Layers/index.ts b/src/Layers/index.ts deleted file mode 100644 index 77d7071..0000000 --- a/src/Layers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * A wrapper around the jsonwebtoken library for Effect - * Requires `effect`, `jsonwebtoken` and `@types/jsonwebtoken` to be installed - */ -export * as JSONWebToken from "./JSONWebToken" diff --git a/src/OpenAIClient.ts b/src/OpenAIClient.ts new file mode 100644 index 0000000..f6c7867 --- /dev/null +++ b/src/OpenAIClient.ts @@ -0,0 +1,69 @@ +import { Config, Context, Effect, Layer } from "effect" +import type { OpenAI } from "openai" +import { ImportError } from "./ImportError" + + +export class OpenAIClient extends Context.Tag("OpenAIClient")() {} + +export class OpenAIClientService { + constructor( + readonly openai: Effect.Effect.Success, + readonly client: OpenAI, + ) {} + + try( + try_: ( + client: OpenAI, + signal: AbortSignal, + ) => Promise + ) { + return Effect.tryPromise({ + try: signal => try_(this.client, signal), + catch: e => e instanceof this.openai.OpenAIError + ? e + : new Error(`Unknown OpenAIClient error: ${ e }`), + }) + } +} + + +const importOpenAI = Effect.tryPromise({ + try: () => import("openai"), + catch: cause => new ImportError({ path: "openai", cause }), +}) + +export const OpenAIClientLive = ( + config: { + readonly apiKey: Config.Config + readonly organization?: Config.Config + readonly project?: Config.Config + readonly baseURL?: Config.Config + readonly timeout?: Config.Config + readonly maxRetries?: Config.Config + + readonly httpAgent?: any + readonly fetch?: any + readonly defaultHeaders?: { [x: string]: string } + readonly defaultQuery?: { [x: string]: string } + } +) => Layer.effect(OpenAIClient, Effect.gen(function*() { + const openai = yield* importOpenAI + + return new OpenAIClientService( + openai, + + new openai.OpenAI({ + apiKey: yield* config.apiKey, + organization: (yield* config.organization ?? Config.succeed(undefined)) ?? null, + project: (yield* config.project ?? Config.succeed(undefined)) ?? null, + baseURL: (yield* config.baseURL ?? Config.succeed(undefined)) ?? "https://api.openai.com/v1", + timeout: yield* config.timeout ?? Config.succeed(undefined), + maxRetries: yield* config.maxRetries ?? Config.succeed(undefined), + + httpAgent: config.httpAgent, + fetch: config.fetch, + defaultHeaders: config.defaultHeaders, + defaultQuery: config.defaultQuery, + }), + ) +})) diff --git a/src/Layers/PrismaStudioRoute.ts b/src/PrismaStudioRoute.ts similarity index 100% rename from src/Layers/PrismaStudioRoute.ts rename to src/PrismaStudioRoute.ts diff --git a/src/TRPC/TRPCBuilder.ts b/src/TRPC/TRPCBuilder.ts new file mode 100644 index 0000000..5edd653 --- /dev/null +++ b/src/TRPC/TRPCBuilder.ts @@ -0,0 +1,20 @@ +import { Context, Effect, Layer } from "effect" +import { type TRPCContext } from "./TRPCContext" +import { importTRPCServer } from "./importTRPCServer" + + +const createTRPC = () => importTRPCServer.pipe(Effect.map(({ initTRPC }) => + initTRPC.context>().create() +)) + +export const Identifier = "@thilalib/TRPC/TRPCBuilder" +export interface TRPCBuilder extends Context.Tag> {} +export interface TRPCBuilderService extends Effect.Effect.Success>> {} + + +export const make = () => { + const TRPCBuilder = Context.GenericTag>(Identifier) + const TRPCBuilderLive = Layer.effect(TRPCBuilder, createTRPC()) + + return { TRPCBuilder, TRPCBuilderLive } as const +} diff --git a/src/TRPC/TRPCContext.ts b/src/TRPC/TRPCContext.ts new file mode 100644 index 0000000..94089d8 --- /dev/null +++ b/src/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/TRPC/TRPCContextCreator.ts b/src/TRPC/TRPCContextCreator.ts new file mode 100644 index 0000000..9087b1e --- /dev/null +++ b/src/TRPC/TRPCContextCreator.ts @@ -0,0 +1,57 @@ +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 const Identifier = "@thilalib/TRPC/TRPCContextCreator" + +export interface TRPCContextCreator extends Context.Tag> {} + +export interface TRPCContextCreatorService { + readonly createContext: (transaction: TRPCContextTransaction) => TRPCContext + readonly createExpressContext: (context: CreateExpressContextOptions) => TRPCContext + readonly createWebSocketContext: (context: CreateWSSContextFnOptions) => TRPCContext +} + +export const TRPCUnknownContextCreator = Context.GenericTag>(Identifier) + + +export const make = () => { + const TRPCContextCreator = Context.GenericTag>(Identifier) + + 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 } as const +} diff --git a/src/TRPC/TRPCExpressRoute.ts b/src/TRPC/TRPCExpressRoute.ts new file mode 100644 index 0000000..22333dd --- /dev/null +++ b/src/TRPC/TRPCExpressRoute.ts @@ -0,0 +1,27 @@ +import { Config, Effect, Layer } from "effect" +import { ExpressApp } from "../Express" +import { ImportError } from "../ImportError" +import { TRPCUnknownContextCreator } from "./TRPCContextCreator" +import { TRPCAnyRouter } from "./TRPCRouter" + + +const importTRPCServerExpressAdapter = Effect.tryPromise({ + try: () => import("@trpc/server/adapters/express"), + catch: cause => new ImportError({ path: "@trpc/server/adapters/express", cause }), +}) + +export const TRPCExpressRouteLive = ( + config: { + readonly root: Config.Config + } +) => Layer.effectDiscard(Effect.gen(function*() { + const { createExpressMiddleware } = yield* importTRPCServerExpressAdapter + const app = yield* ExpressApp.ExpressApp + + app.use(yield* config.root, + createExpressMiddleware({ + router: yield* TRPCAnyRouter, + createContext: (yield* TRPCUnknownContextCreator).createExpressContext, + }) + ) +})) diff --git a/src/TRPC/TRPCRouter.ts b/src/TRPC/TRPCRouter.ts new file mode 100644 index 0000000..55226bb --- /dev/null +++ b/src/TRPC/TRPCRouter.ts @@ -0,0 +1,18 @@ +import type { AnyRouter } from "@trpc/server" +import { Context, Effect, Layer } from "effect" + + +export const Identifier = "@thilalib/TRPC/TRPCRouter" +export interface TRPCRouter extends Context.Tag {} + +export const TRPCAnyRouter = Context.GenericTag(Identifier) + + +export const make = ( + router: Effect.Effect +) => { + const TRPCRouter = Context.GenericTag(Identifier) + const TRPCRouterLive = Layer.effect(TRPCRouter, router) + + return { TRPCRouter, TRPCRouterLive } as const +} diff --git a/src/TRPC/TRPCWebSocketServer.ts b/src/TRPC/TRPCWebSocketServer.ts new file mode 100644 index 0000000..22238fe --- /dev/null +++ b/src/TRPC/TRPCWebSocketServer.ts @@ -0,0 +1,66 @@ +import type { applyWSSHandler } from "@trpc/server/adapters/ws" +import { Config, Context, Effect, Layer } from "effect" +import type ws from "ws" +import { ExpressNodeHTTPServer } from "../Express" +import { ImportError } from "../ImportError" +import { TRPCUnknownContextCreator } from "./TRPCContextCreator" +import { TRPCAnyRouter } from "./TRPCRouter" + + +export class TRPCWebSocketServer extends Context.Tag("@thilalib/TRPC/TRPCWebSocketServer")() {} + +export interface TRPCWebSocketServerService { + wss: ws.Server + handler: ReturnType +} + + +const importWS = Effect.tryPromise({ + try: () => import("ws"), + catch: cause => new ImportError({ path: "ws", cause }), +}) + +const importTRPCServerWSAdapter = Effect.tryPromise({ + try: () => import("@trpc/server/adapters/ws"), + catch: cause => new ImportError({ path: "@trpc/server/adapters/ws", cause }), +}) + +export const TRPCWebSocketServerLive = ( + config: { + readonly host: Config.Config + } +) => Layer.effect(TRPCWebSocketServer, Effect.gen(function*() { + const { WebSocketServer } = yield* importWS + const { applyWSSHandler } = yield* importTRPCServerWSAdapter + + const host = yield* config.host + + return yield* Effect.acquireRelease( + Effect.gen(function*() { + yield* Effect.logInfo(`WebSocket server started on ${ host }`) + + const wss = new WebSocketServer({ + server: yield* ExpressNodeHTTPServer.ExpressNodeHTTPServer, + host, + }) + + return { + wss, + handler: applyWSSHandler({ + wss, + router: yield* TRPCAnyRouter, + createContext: (yield* TRPCUnknownContextCreator).createWebSocketContext, + }), + } + }), + + ({ wss, handler }) => Effect.gen(function*() { + yield* Effect.logInfo(`WebSocket server on ${ host } is stopping. Waiting for existing connections to end...`) + + handler.broadcastReconnectNotification() + yield* Effect.async(resume => { + wss.close(() => resume(Effect.logInfo("WebSocket server closed"))) + }) + }), + ) +})) diff --git a/src/TRPC/createTRCPErrorMapper.ts b/src/TRPC/createTRCPErrorMapper.ts new file mode 100644 index 0000000..d59ac86 --- /dev/null +++ b/src/TRPC/createTRCPErrorMapper.ts @@ -0,0 +1,63 @@ +import { Effect, type Cause } from "effect" +import { importTRPCServer } from "./importTRPCServer" + + +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), + ) +)) + +class TRPCErrorCause extends Error { + constructor(readonly cause: Cause.Cause) { + super() + } +} diff --git a/src/TRPC/example.ts b/src/TRPC/example.ts new file mode 100644 index 0000000..1a3bd14 --- /dev/null +++ b/src/TRPC/example.ts @@ -0,0 +1,51 @@ +import { Config, Effect, Layer } from "effect" +import * as TRPC from "." +import { Express, JSONWebToken } from ".." + + +// Context available to the router procedures +type Services = + | JSONWebToken.JSONWebToken + +const ServicesLive = Layer.empty.pipe( + Layer.provideMerge(JSONWebToken.JSONWebTokenLive) +) + + +const { TRPCContextCreator, TRPCContextCreatorLive } = TRPC.TRPCContextCreator.make() +const { TRPCBuilder, TRPCBuilderLive } = TRPC.TRPCBuilder.make() + + +const router = TRPCBuilder.pipe(Effect.map(t => t.router({ + ping: t.procedure.query(({ ctx }) => ctx.run( + Effect.succeed("pong") + )), +}))) +const { TRPCRouter, TRPCRouterLive } = TRPC.TRPCRouter.make(router) + + +const ServerLive = Layer.empty.pipe( + Layer.provideMerge(TRPC.TRPCExpressRoute.TRPCExpressRouteLive({ + root: Config.succeed("/rpc") + })), + Layer.provideMerge(TRPC.TRPCWebSocketServer.TRPCWebSocketServerLive({ + host: Config.succeed("/rpc") + })), + + Layer.provideMerge(TRPCRouterLive), + Layer.provideMerge(TRPCBuilderLive), + Layer.provideMerge(TRPCContextCreatorLive), + + Layer.provideMerge(Express.ExpressNodeHTTPServer.ExpressNodeHTTPServerLive({ + port: Config.succeed(3000) + })), + Layer.provideMerge(Express.ExpressApp.ExpressAppLive()) +) + +await Effect.gen(function*() { + return yield* Layer.launch(ServerLive) +}).pipe( + Effect.provide(ServicesLive), + Effect.scoped, + Effect.runPromise, +) diff --git a/src/TRPC/importTRPCServer.ts b/src/TRPC/importTRPCServer.ts new file mode 100644 index 0000000..43db62b --- /dev/null +++ b/src/TRPC/importTRPCServer.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import { ImportError } from "../ImportError" + + +export const importTRPCServer: Effect.Effect< + typeof import("@trpc/server"), + ImportError +> = Effect.tryPromise({ + try: () => import("@trpc/server"), + catch: cause => new ImportError({ path: "@trpc/server", cause }), +}) diff --git a/src/TRPC/index.ts b/src/TRPC/index.ts new file mode 100644 index 0000000..71403b6 --- /dev/null +++ b/src/TRPC/index.ts @@ -0,0 +1,7 @@ +export * from "./middlewares" +export * as TRPCBuilder from "./TRPCBuilder" +export * as TRPCContext from "./TRPCContext" +export * as TRPCContextCreator from "./TRPCContextCreator" +export * as TRPCExpressRoute from "./TRPCExpressRoute" +export * as TRPCRouter from "./TRPCRouter" +export * as TRPCWebSocketServer from "./TRPCWebSocketServer" diff --git a/src/TRPC/middlewares.ts b/src/TRPC/middlewares.ts new file mode 100644 index 0000000..fcd57e3 --- /dev/null +++ b/src/TRPC/middlewares.ts @@ -0,0 +1,44 @@ +import { Effect, Match } from "effect" +import type { TRPCContextTransaction } from "./TRPCContext" +import { importTRPCServer } from "./importTRPCServer" + + +export const ExpressOnly = importTRPCServer.pipe(Effect.map(({ + experimental_standaloneMiddleware, + TRPCError, +}) => experimental_standaloneMiddleware<{ + ctx: { readonly transaction: TRPCContextTransaction } +}>().create(opts => + Match.value(opts.ctx.transaction).pipe( + Match.tag("Express", transaction => + opts.next({ ctx: { transaction } }) + ), + + Match.orElse(() => { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Only Express transport is supported by this procedure", + }) + }), + ) +))) + +export const WebSocketOnly = importTRPCServer.pipe(Effect.map(({ + experimental_standaloneMiddleware, + TRPCError, +}) => experimental_standaloneMiddleware<{ + ctx: { readonly transaction: TRPCContextTransaction } +}>().create(opts => + Match.value(opts.ctx.transaction).pipe( + Match.tag("WebSocket", transaction => + opts.next({ ctx: { transaction } }) + ), + + Match.orElse(() => { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Only WebSocket transport is supported by this procedure", + }) + }), + ) +))) diff --git a/src/index.ts b/src/index.ts index 1129b8d..f729f79 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,6 @@ -export * as Layers from "./Layers" +export * as Express from "./Express" +export * from "./ImportError" +export * as JSONWebToken from "./JSONWebToken" +export * as OpenAIClient from "./OpenAIClient" export * as Schema from "./Schema" export * as Types from "./Types" diff --git a/tsconfig.json b/tsconfig.json index 238655f..e46d674 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,13 +6,13 @@ "module": "ESNext", "moduleDetection": "force", "jsx": "react-jsx", - "allowJs": true, + // "allowJs": true, // Bundler mode "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + // "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, - "noEmit": true, + // "noEmit": true, // Best practices "strict": true, @@ -22,6 +22,12 @@ // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } + "noPropertyAccessFromIndexSignature": false, + + // Build + "outDir": "./dist", + "declaration": true + }, + + "include": ["./src"] } diff --git a/tsup.config.ts b/tsup.config.ts index 3ea8c21..c96c727 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -4,13 +4,16 @@ import { defineConfig } from "tsup" export default defineConfig({ entry: [ "./src/index.ts", - "./src/Layers/index.ts", + "./src/Express/index.ts", + "./src/TRPC/index.ts", + "./src/Types/index.ts", "./src/Schema/index.ts", "./src/Schema/MobX/index.ts", "./src/Schema/TanStackForm/index.ts", - "./src/Types/index.ts", + "./src/JSONWebToken.ts", + "./src/OpenAIClient.ts", ], - format: ["esm", "cjs"], + format: ["esm"], skipNodeModulesBundle: true, dts: true, splitting: true,