Compare commits
59 Commits
a27ff5834e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4366c89fd | ||
|
|
58d307de14 | ||
|
|
8ab38af883 | ||
|
|
15f89974cf | ||
|
|
2e867f9da5 | ||
|
|
8f7617b5a3 | ||
|
|
35dc1b2b44 | ||
|
|
0e4950a3cd | ||
|
|
1170083e49 | ||
|
|
ad9951947a | ||
|
|
7c1d6c4e83 | ||
|
|
95cbd2a20c | ||
|
|
e1cbe27c56 | ||
|
|
ea2fc9f23f | ||
|
|
061272ee0c | ||
|
|
c033aa0bed | ||
|
|
7729bff903 | ||
|
|
e154d4a88e | ||
|
|
6bc20bd455 | ||
|
|
2c2a867c80 | ||
|
|
00aa9fe10f | ||
|
|
d07ba927f9 | ||
|
|
0b695506cf | ||
|
|
99d192bc43 | ||
|
|
5ebf7f4a6e | ||
|
|
4315d0dc52 | ||
|
|
ddb531a521 | ||
|
|
af7d944116 | ||
|
|
0d792ad8be | ||
|
|
d364eba805 | ||
|
|
8579f3b7cd | ||
|
|
c3560c41bc | ||
|
|
a0bf11466d | ||
|
|
ec5cc51f82 | ||
|
|
552483d915 | ||
|
|
77c0141154 | ||
|
|
23ccc1da28 | ||
|
|
52f7c5cc4b | ||
|
|
af9baa3e9a | ||
|
|
2daf60faf3 | ||
|
|
2497aaa236 | ||
|
|
af2329105d | ||
|
|
0be4e0d8ce | ||
|
|
f4eeb66459 | ||
|
|
000c5bda35 | ||
|
|
19f852fba5 | ||
|
|
d2734fa71b | ||
|
|
3ae5f31f96 | ||
|
|
63f636f231 | ||
|
|
5a3400f6dd | ||
|
|
c001738f46 | ||
|
|
69d9ffb3e0 | ||
|
|
50b525cfd2 | ||
|
|
d053c61eab | ||
|
|
2a909a34bc | ||
|
|
329b9e7c41 | ||
|
|
e5641924f8 | ||
|
|
44909f256e | ||
|
|
0b740d6c7f |
33
Caddyfile
Normal file
33
Caddyfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
(reverse_proxy_headers) {
|
||||||
|
header_up Host {upstream_hostport}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# App entrypoint
|
||||||
|
http://* {
|
||||||
|
# Redirect RPC calls (both HTTP and WebSockets) to the backend
|
||||||
|
handle /rpc* {
|
||||||
|
reverse_proxy http://server {
|
||||||
|
import reverse_proxy_headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Or serve the UI
|
||||||
|
handle {
|
||||||
|
reverse_proxy http://webui {
|
||||||
|
import reverse_proxy_headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http://server.* {
|
||||||
|
reverse_proxy http://server {
|
||||||
|
import reverse_proxy_headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http://webui.* {
|
||||||
|
reverse_proxy http://webui {
|
||||||
|
import reverse_proxy_headers
|
||||||
|
}
|
||||||
|
}
|
||||||
49
docker-compose.yml
Normal file
49
docker-compose.yml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
x-service-base: &service-base
|
||||||
|
user: ${UID:?UID missing}:${GID:?GID missing}
|
||||||
|
tty: true
|
||||||
|
|
||||||
|
x-volume-app: &volume-app ./:/app/
|
||||||
|
|
||||||
|
x-env-base: &env-base
|
||||||
|
TZ: ${TZ:?TZ missing}
|
||||||
|
|
||||||
|
|
||||||
|
services:
|
||||||
|
reverse-proxy:
|
||||||
|
container_name: reverse-proxy
|
||||||
|
image: caddy:latest
|
||||||
|
ports:
|
||||||
|
- ${PORT:?PORT missing}:80
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
environment:
|
||||||
|
<<: *env-base
|
||||||
|
|
||||||
|
server:
|
||||||
|
<<: *service-base
|
||||||
|
container_name: server
|
||||||
|
image: oven/bun:latest
|
||||||
|
volumes:
|
||||||
|
- *volume-app
|
||||||
|
working_dir: /app/packages/server
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
<<: *env-base
|
||||||
|
NODE_ENV: development
|
||||||
|
entrypoint: ["bun", "--watch", "src/index.ts"]
|
||||||
|
depends_on:
|
||||||
|
- reverse-proxy
|
||||||
|
|
||||||
|
webui:
|
||||||
|
<<: *service-base
|
||||||
|
container_name: webui
|
||||||
|
image: oven/bun:latest
|
||||||
|
volumes:
|
||||||
|
- *volume-app
|
||||||
|
working_dir: /app/packages/webui
|
||||||
|
environment:
|
||||||
|
<<: *env-base
|
||||||
|
entrypoint: ["bun", "vite"]
|
||||||
|
stop_signal: SIGKILL
|
||||||
|
depends_on:
|
||||||
|
- reverse-proxy
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/schema": "^0.68.17",
|
"@effect/schema": "^0.68.26",
|
||||||
"@thilawyn/thilalib": "^0.1.5",
|
"@thilawyn/thilalib": "^0.1.5",
|
||||||
"effect": "^3.4.7"
|
"effect": "^3.5.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
packages/common/src/data/ServerTime.type.ts
Normal file
4
packages/common/src/data/ServerTime.type.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Schema as S } from "@effect/schema"
|
||||||
|
|
||||||
|
|
||||||
|
export const ServerTime = S.DateFromString
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
export * from "./ServerTime.type"
|
||||||
export * from "./Todo.class"
|
export * from "./Todo.class"
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ export module Identifiable {
|
|||||||
Kind extends string,
|
Kind extends string,
|
||||||
ID,
|
ID,
|
||||||
>(
|
>(
|
||||||
|
self: Identifiable<Kind, ID>,
|
||||||
that: Identifiable<Kind, ID>,
|
that: Identifiable<Kind, ID>,
|
||||||
to: Identifiable<Kind, ID>,
|
|
||||||
): boolean {
|
): boolean {
|
||||||
// Two elements can only be equal if they both have a defined ID
|
// Two elements can only be equal if they both have a defined ID
|
||||||
return Option.isSome(that.id) && Option.isSome(to.id)
|
return Option.isSome(self.id) && Option.isSome(that.id)
|
||||||
? Equal.equals(that.id.value, to.id.value)
|
? Equal.equals(self.id.value, that.id.value)
|
||||||
: false
|
: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,23 @@
|
|||||||
".": "./src/router.ts"
|
".": "./src/router.ts"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "NODE_ENV=development bun --watch src/index.ts"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform": "^0.58.20",
|
"@effect/platform": "^0.59.2",
|
||||||
"@effect/platform-bun": "^0.38.19",
|
"@effect/platform-bun": "^0.39.2",
|
||||||
"@effect/schema": "^0.68.17",
|
"@effect/schema": "^0.68.26",
|
||||||
"@thilawyn/thilalib": "^0.1.5",
|
"@thilawyn/thilalib": "^0.1.5",
|
||||||
"@todo-tests/common": "workspace:*",
|
"@todo-tests/common": "workspace:*",
|
||||||
"@trpc/server": "^10.45.2",
|
"@trpc/server": "^10.45.2",
|
||||||
"effect": "^3.4.7",
|
"effect": "^3.5.6",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"trpc-playground": "^1.0.4",
|
"trpc-playground": "^1.0.4",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"bun-types": "^1.1.18"
|
"bun-types": "^1.1.20"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
packages/server/src/ServerConfig.ts
Normal file
20
packages/server/src/ServerConfig.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Schema as S } from "@effect/schema"
|
||||||
|
import { Config } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export module ServerConfig {
|
||||||
|
export const mode = Config.string("NODE_ENV").pipe(
|
||||||
|
Config.validate({
|
||||||
|
validation: S.is(S.Union(
|
||||||
|
S.Literal("development"),
|
||||||
|
S.Literal("production"),
|
||||||
|
)),
|
||||||
|
message: "Expected 'development' or 'production'",
|
||||||
|
}),
|
||||||
|
Config.withDefault("development"),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const httpPort = Config.number("HTTP_PORT").pipe(Config.withDefault(80))
|
||||||
|
export const rpcHTTPRoot = Config.string("RPC_HTTP_ROOT").pipe(Config.withDefault("/rpc"))
|
||||||
|
export const rpcHTTPPlaygroundRoot = Config.string("RPC_HTTP_PLAYGROUND_ROOT").pipe(Config.withDefault("/rpc/playground"))
|
||||||
|
}
|
||||||
@@ -8,11 +8,11 @@ type TServices =
|
|||||||
export interface Services extends TServices {}
|
export interface Services extends TServices {}
|
||||||
|
|
||||||
export module Services {
|
export module Services {
|
||||||
export const Live = Layer.mergeAll(
|
export const Live: Layer.Layer<Services, never, never> = Layer.mergeAll(
|
||||||
TodoRepository.Live
|
TodoRepository.Live
|
||||||
) satisfies Layer.Layer<Services, never, never>
|
)
|
||||||
|
|
||||||
export const Dev = Layer.mergeAll(
|
export const Dev: Layer.Layer<Services, never, never> = Layer.mergeAll(
|
||||||
TodoRepository.Live
|
TodoRepository.Live
|
||||||
) satisfies Layer.Layer<Services, never, never>
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { Config } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
export const httpPort = Config.number("HTTP_PORT").pipe(Config.withDefault(8080))
|
|
||||||
export const rpcHTTPRoot = Config.string("RPC_HTTP_ROOT").pipe(Config.withDefault("/rpc"))
|
|
||||||
export const rpcHTTPPlaygroundRoot = Config.string("RPC_HTTP_PLAYGROUND_ROOT").pipe(Config.withDefault("/rpc/playground"))
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Context, Effect, Layer, Runtime } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { Server } from "node:http"
|
import { Server } from "node:http"
|
||||||
import { httpPort } from "../config"
|
import { ServerConfig } from "../ServerConfig"
|
||||||
import { ExpressApp } from "./ExpressApp"
|
import { ExpressApp } from "./ExpressApp"
|
||||||
|
|
||||||
|
|
||||||
@@ -9,14 +9,16 @@ export class ExpressHTTPServer extends Context.Tag("ExpressHTTPServer")<ExpressH
|
|||||||
export module ExpressHTTPServer {
|
export module ExpressHTTPServer {
|
||||||
export const Live = Layer.effect(ExpressHTTPServer, Effect.acquireRelease(
|
export const Live = Layer.effect(ExpressHTTPServer, Effect.acquireRelease(
|
||||||
Effect.gen(function*() {
|
Effect.gen(function*() {
|
||||||
const runSync = yield* Effect.runtime().pipe(
|
|
||||||
Effect.map(Runtime.runSync)
|
|
||||||
)
|
|
||||||
|
|
||||||
const app = yield* ExpressApp
|
const app = yield* ExpressApp
|
||||||
const port = yield* httpPort
|
const port = yield* ServerConfig.httpPort
|
||||||
|
|
||||||
return app.listen(port, () => runSync(Effect.logInfo(`HTTP server listening on ${ port }`)))
|
return yield* Effect.async<Server>(resume => {
|
||||||
|
const server = app.listen(port, () => resume(
|
||||||
|
Effect.succeed(server).pipe(
|
||||||
|
Effect.tap(Effect.logInfo(`HTTP server listening on ${ port }`))
|
||||||
|
)
|
||||||
|
))
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
|
|
||||||
server => Effect.gen(function*() {
|
server => Effect.gen(function*() {
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Context, Effect, Layer } from "effect"
|
|
||||||
import ws from "ws"
|
|
||||||
import { ExpressHTTPServer } from "../http/ExpressHTTPServer"
|
|
||||||
|
|
||||||
|
|
||||||
export class WebSocketServer extends Context.Tag("WebSocketServer")<WebSocketServer, ws.Server>() {}
|
|
||||||
|
|
||||||
export module WebSocketServer {
|
|
||||||
export const Live = Layer.effect(WebSocketServer, Effect.acquireRelease(
|
|
||||||
Effect.gen(function*() {
|
|
||||||
yield* Effect.logInfo("WebSocket server started")
|
|
||||||
return new ws.WebSocketServer({ server: yield* ExpressHTTPServer })
|
|
||||||
}),
|
|
||||||
|
|
||||||
wss => Effect.gen(function*() {
|
|
||||||
yield* Effect.logInfo("WebSocket server is stopping. Waiting for existing connections to end...")
|
|
||||||
yield* Effect.async(resume => {
|
|
||||||
wss.close(() => resume(Effect.logInfo("WebSocket server closed")))
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { BunRuntime } from "@effect/platform-bun"
|
import { BunRuntime } from "@effect/platform-bun"
|
||||||
import { Todo } from "@todo-tests/common/data"
|
import { Todo } from "@todo-tests/common/data"
|
||||||
import { Duration, Effect, Layer, Option } from "effect"
|
import { Duration, Effect, Layer, Logger, Match, Option } from "effect"
|
||||||
|
import { ServerConfig } from "./ServerConfig"
|
||||||
import { Services } from "./Services"
|
import { Services } from "./Services"
|
||||||
import { ExpressApp } from "./http/ExpressApp"
|
import { ExpressApp } from "./http/ExpressApp"
|
||||||
import { ExpressHTTPServer } from "./http/ExpressHTTPServer"
|
import { ExpressHTTPServer } from "./http/ExpressHTTPServer"
|
||||||
import { WebSocketServer } from "./http/WebSocketServer"
|
|
||||||
import { RPCPlaygroundRoute } from "./rpc/RPCPlaygroundRoute"
|
import { RPCPlaygroundRoute } from "./rpc/RPCPlaygroundRoute"
|
||||||
import { RPCRoute } from "./rpc/RPCRoute"
|
import { RPCRoute } from "./rpc/RPCRoute"
|
||||||
import { RPCRouter } from "./rpc/RPCRouter"
|
import { RPCRouter } from "./rpc/RPCRouter"
|
||||||
import { RPCWebSocketHandler } from "./rpc/RPCWebSocketHandler"
|
import { RPCWebSocketServer } from "./rpc/RPCWebSocketServer"
|
||||||
import { RPCProcedureBuilder } from "./rpc/procedures/RPCProcedureBuilder"
|
import { RPCProcedureBuilder } from "./rpc/procedures/RPCProcedureBuilder"
|
||||||
import { TodoRepository } from "./todo/TodoRepository"
|
import { TodoRepository } from "./todo/TodoRepository"
|
||||||
import { TRPCBuilder } from "./trpc/TRPCBuilder"
|
import { TRPCBuilder } from "./trpc/TRPCBuilder"
|
||||||
@@ -18,22 +18,45 @@ import { TRPCContextCreator } from "./trpc/TRPCContextCreator"
|
|||||||
const ServerDev = Layer.empty.pipe(
|
const ServerDev = Layer.empty.pipe(
|
||||||
Layer.provideMerge(RPCRoute.Live),
|
Layer.provideMerge(RPCRoute.Live),
|
||||||
Layer.provideMerge(RPCPlaygroundRoute.Dev),
|
Layer.provideMerge(RPCPlaygroundRoute.Dev),
|
||||||
Layer.provideMerge(RPCWebSocketHandler.Live),
|
Layer.provideMerge(RPCWebSocketServer.Live),
|
||||||
|
|
||||||
|
Layer.provideMerge(RPCRouter.Live),
|
||||||
|
Layer.provideMerge(RPCProcedureBuilder.Live),
|
||||||
|
Layer.provideMerge(TRPCBuilder.Live),
|
||||||
|
Layer.provideMerge(TRPCContextCreator.Live),
|
||||||
|
|
||||||
|
Layer.provideMerge(ExpressHTTPServer.Live),
|
||||||
|
Layer.provideMerge(ExpressApp.Live),
|
||||||
|
)
|
||||||
|
|
||||||
|
const ServerLive = Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(RPCRoute.Live),
|
||||||
|
Layer.provideMerge(RPCPlaygroundRoute.Live),
|
||||||
|
Layer.provideMerge(RPCWebSocketServer.Live),
|
||||||
|
|
||||||
Layer.provideMerge(RPCRouter.Live),
|
Layer.provideMerge(RPCRouter.Live),
|
||||||
Layer.provideMerge(RPCProcedureBuilder.Live),
|
Layer.provideMerge(RPCProcedureBuilder.Live),
|
||||||
Layer.provideMerge(TRPCBuilder.Live),
|
Layer.provideMerge(TRPCBuilder.Live),
|
||||||
Layer.provideMerge(TRPCContextCreator.Live),
|
Layer.provideMerge(TRPCContextCreator.Live),
|
||||||
|
|
||||||
Layer.provideMerge(WebSocketServer.Live),
|
|
||||||
Layer.provideMerge(ExpressHTTPServer.Live),
|
Layer.provideMerge(ExpressHTTPServer.Live),
|
||||||
Layer.provideMerge(ExpressApp.Live),
|
Layer.provideMerge(ExpressApp.Live),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
const main = Effect.gen(function*() {
|
const main = Effect.gen(function*() {
|
||||||
|
const mode = yield* ServerConfig.mode
|
||||||
const todos = yield* TodoRepository
|
const todos = yield* TodoRepository
|
||||||
|
|
||||||
|
// yield* Effect.fork(
|
||||||
|
// todos.todos.changes.pipe(
|
||||||
|
// Stream.runForEach(values => Effect.gen(function*() {
|
||||||
|
// yield* Console.log("Todos updated:")
|
||||||
|
// yield* Console.log(values)
|
||||||
|
// }))
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
|
||||||
yield* todos.add(new Todo({
|
yield* todos.add(new Todo({
|
||||||
id: Option.none(),
|
id: Option.none(),
|
||||||
order: 0,
|
order: 0,
|
||||||
@@ -79,10 +102,15 @@ const main = Effect.gen(function*() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
yield* Layer.launch(ServerDev)
|
return yield* Layer.launch(Match.value(mode).pipe(
|
||||||
|
Match.when("development", () => ServerDev),
|
||||||
|
Match.when("production", () => ServerLive),
|
||||||
|
Match.exhaustive,
|
||||||
|
))
|
||||||
})
|
})
|
||||||
|
|
||||||
BunRuntime.runMain(main.pipe(
|
BunRuntime.runMain(main.pipe(
|
||||||
Effect.provide(Services.Dev),
|
Effect.provide(Services.Dev),
|
||||||
|
Effect.provide(Logger.structured),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { expressHandler } from "trpc-playground/handlers/express"
|
import { expressHandler } from "trpc-playground/handlers/express"
|
||||||
import { rpcHTTPPlaygroundRoot, rpcHTTPRoot } from "../config"
|
import { ServerConfig } from "../ServerConfig"
|
||||||
import { ExpressApp } from "../http/ExpressApp"
|
import { ExpressApp } from "../http/ExpressApp"
|
||||||
import { RPCRouter } from "./RPCRouter"
|
import { RPCRouter } from "./RPCRouter"
|
||||||
|
|
||||||
@@ -10,10 +10,10 @@ export module RPCPlaygroundRoute {
|
|||||||
|
|
||||||
export const Dev = Layer.effectDiscard(Effect.gen(function*() {
|
export const Dev = Layer.effectDiscard(Effect.gen(function*() {
|
||||||
const app = yield* ExpressApp
|
const app = yield* ExpressApp
|
||||||
const playgroundEndpoint = yield* rpcHTTPPlaygroundRoot
|
const playgroundEndpoint = yield* ServerConfig.rpcHTTPPlaygroundRoot
|
||||||
|
|
||||||
const handler = expressHandler({
|
const handler = expressHandler({
|
||||||
trpcApiEndpoint: yield* rpcHTTPRoot,
|
trpcApiEndpoint: yield* ServerConfig.rpcHTTPRoot,
|
||||||
playgroundEndpoint,
|
playgroundEndpoint,
|
||||||
router: yield* RPCRouter,
|
router: yield* RPCRouter,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createExpressMiddleware } from "@trpc/server/adapters/express"
|
import { createExpressMiddleware } from "@trpc/server/adapters/express"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { rpcHTTPRoot } from "../config"
|
import { ServerConfig } from "../ServerConfig"
|
||||||
import { ExpressApp } from "../http/ExpressApp"
|
import { ExpressApp } from "../http/ExpressApp"
|
||||||
import { TRPCContextCreator } from "../trpc/TRPCContextCreator"
|
import { TRPCContextCreator } from "../trpc/TRPCContextCreator"
|
||||||
import { RPCRouter } from "./RPCRouter"
|
import { RPCRouter } from "./RPCRouter"
|
||||||
@@ -10,10 +10,10 @@ export module RPCRoute {
|
|||||||
export const Live = Layer.effectDiscard(Effect.gen(function*() {
|
export const Live = Layer.effectDiscard(Effect.gen(function*() {
|
||||||
const app = yield* ExpressApp
|
const app = yield* ExpressApp
|
||||||
|
|
||||||
app.use(yield* rpcHTTPRoot,
|
app.use(yield* ServerConfig.rpcHTTPRoot,
|
||||||
createExpressMiddleware({
|
createExpressMiddleware({
|
||||||
router: yield* RPCRouter,
|
router: yield* RPCRouter,
|
||||||
createContext: yield* TRPCContextCreator,
|
createContext: (yield* TRPCContextCreator).createExpressContext,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,27 +1,12 @@
|
|||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { TRPCBuilder } from "../trpc/TRPCBuilder"
|
import { indexRouter } from "./routers"
|
||||||
import { RPCProcedureBuilder } from "./procedures/RPCProcedureBuilder"
|
|
||||||
import { todosRouter } from "./routers/todos"
|
|
||||||
|
|
||||||
|
|
||||||
export const router = Effect.gen(function*() {
|
|
||||||
const t = yield* TRPCBuilder
|
|
||||||
const procedure = yield* RPCProcedureBuilder
|
|
||||||
|
|
||||||
return t.router({
|
|
||||||
ping: procedure.query(({ ctx }) =>
|
|
||||||
ctx.run(Effect.succeed("pong"))
|
|
||||||
),
|
|
||||||
|
|
||||||
todos: yield* todosRouter,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
export class RPCRouter extends Context.Tag("RPCRouter")<RPCRouter,
|
export class RPCRouter extends Context.Tag("RPCRouter")<RPCRouter,
|
||||||
Effect.Effect.Success<typeof router>
|
Effect.Effect.Success<typeof indexRouter>
|
||||||
>() {}
|
>() {}
|
||||||
|
|
||||||
export module RPCRouter {
|
export module RPCRouter {
|
||||||
export type Router = Context.Tag.Service<RPCRouter>
|
export type Router = Context.Tag.Service<RPCRouter>
|
||||||
export const Live = Layer.effect(RPCRouter, router)
|
export const Live = Layer.effect(RPCRouter, indexRouter)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { applyWSSHandler } from "@trpc/server/adapters/ws"
|
|
||||||
import { Effect, Layer } from "effect"
|
|
||||||
import { WebSocketServer } from "../http/WebSocketServer"
|
|
||||||
import { TRPCContextCreator } from "../trpc/TRPCContextCreator"
|
|
||||||
import { RPCRouter } from "./RPCRouter"
|
|
||||||
|
|
||||||
|
|
||||||
export module RPCWebSocketHandler {
|
|
||||||
export const Live = Layer.effectDiscard(Effect.acquireRelease(
|
|
||||||
Effect.gen(function*() {
|
|
||||||
return applyWSSHandler({
|
|
||||||
wss: yield* WebSocketServer,
|
|
||||||
router: yield* RPCRouter,
|
|
||||||
createContext: yield* TRPCContextCreator,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
|
|
||||||
handler => Effect.sync(() =>
|
|
||||||
handler.broadcastReconnectNotification()
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
46
packages/server/src/rpc/RPCWebSocketServer.ts
Normal file
46
packages/server/src/rpc/RPCWebSocketServer.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { applyWSSHandler } from "@trpc/server/adapters/ws"
|
||||||
|
import { Context, Effect, Layer } from "effect"
|
||||||
|
import ws from "ws"
|
||||||
|
import { ExpressHTTPServer } from "../http/ExpressHTTPServer"
|
||||||
|
import { ServerConfig } from "../ServerConfig"
|
||||||
|
import { TRPCContextCreator } from "../trpc/TRPCContextCreator"
|
||||||
|
import { RPCRouter } from "./RPCRouter"
|
||||||
|
|
||||||
|
|
||||||
|
export class RPCWebSocketServer extends Context.Tag("RPCWebSocketServer")<RPCWebSocketServer, {
|
||||||
|
wss: ws.Server
|
||||||
|
handler: ReturnType<typeof applyWSSHandler>
|
||||||
|
}>() {}
|
||||||
|
|
||||||
|
export module RPCWebSocketServer {
|
||||||
|
export const Live = Layer.effect(RPCWebSocketServer, ServerConfig.rpcHTTPRoot.pipe(
|
||||||
|
Effect.flatMap(rpcHTTPRoot => Effect.acquireRelease(
|
||||||
|
Effect.gen(function*() {
|
||||||
|
yield* Effect.logInfo(`WebSocket server started on ${ rpcHTTPRoot }`)
|
||||||
|
|
||||||
|
const wss = new ws.WebSocketServer({
|
||||||
|
server: yield* ExpressHTTPServer,
|
||||||
|
host: rpcHTTPRoot,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
wss,
|
||||||
|
handler: applyWSSHandler({
|
||||||
|
wss,
|
||||||
|
router: yield* RPCRouter,
|
||||||
|
createContext: (yield* TRPCContextCreator).createWebSocketContext,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
({ wss, handler }) => Effect.gen(function*() {
|
||||||
|
yield* Effect.logInfo(`WebSocket server on ${ rpcHTTPRoot } is stopping. Waiting for existing connections to end...`)
|
||||||
|
|
||||||
|
handler.broadcastReconnectNotification()
|
||||||
|
yield* Effect.async(resume => {
|
||||||
|
wss.close(() => resume(Effect.logInfo("WebSocket server closed")))
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
))
|
||||||
|
}
|
||||||
54
packages/server/src/rpc/routers/index.ts
Normal file
54
packages/server/src/rpc/routers/index.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { Schema as S } from "@effect/schema"
|
||||||
|
import { ServerTime } from "@todo-tests/common/data"
|
||||||
|
import { TRPCError } from "@trpc/server"
|
||||||
|
import { observable } from "@trpc/server/observable"
|
||||||
|
import { Data, Effect, Fiber, Schedule } from "effect"
|
||||||
|
import { TRPCBuilder } from "../../trpc/TRPCBuilder"
|
||||||
|
import { RPCProcedureBuilder } from "../procedures/RPCProcedureBuilder"
|
||||||
|
import { todoRouter } from "./todo"
|
||||||
|
|
||||||
|
|
||||||
|
const encodeServerTime = S.encode(ServerTime)
|
||||||
|
|
||||||
|
|
||||||
|
export const indexRouter = Effect.gen(function*() {
|
||||||
|
const t = yield* TRPCBuilder
|
||||||
|
const procedure = yield* RPCProcedureBuilder
|
||||||
|
|
||||||
|
return t.router({
|
||||||
|
ping: procedure.query(({ ctx }) => ctx.run(
|
||||||
|
Effect.succeed("pong")
|
||||||
|
)),
|
||||||
|
|
||||||
|
serverTime: procedure
|
||||||
|
.subscription(({ ctx }) =>
|
||||||
|
observable<typeof ServerTime.Encoded>(emit => {
|
||||||
|
const emitter = ctx.fork(
|
||||||
|
Effect.sync(() => new Date()).pipe(
|
||||||
|
Effect.flatMap(encodeServerTime),
|
||||||
|
Effect.map(emit.next),
|
||||||
|
Effect.repeat(Schedule.fixed("1 second")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => ctx.fork(
|
||||||
|
Fiber.interrupt(emitter)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
fail1: procedure.query(({ ctx }) => ctx.run(
|
||||||
|
Effect.fail(new AnError({ aValue: "A value" }))
|
||||||
|
)),
|
||||||
|
fail2: procedure.query(({ ctx }) => ctx.run(
|
||||||
|
Effect.fail(new TRPCError({ code: "NOT_IMPLEMENTED" }))
|
||||||
|
)),
|
||||||
|
|
||||||
|
todo: yield* todoRouter,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
class AnError extends Data.TaggedError("AnError")<{
|
||||||
|
aValue: string
|
||||||
|
}> {}
|
||||||
@@ -9,9 +9,10 @@ import { RPCProcedureBuilder } from "../procedures/RPCProcedureBuilder"
|
|||||||
|
|
||||||
const encodeTodos = S.encode(S.Chunk(JsonifiableTodo))
|
const encodeTodos = S.encode(S.Chunk(JsonifiableTodo))
|
||||||
const encodeOptionalTodo = S.encode(S.OptionFromNullOr(JsonifiableTodo))
|
const encodeOptionalTodo = S.encode(S.OptionFromNullOr(JsonifiableTodo))
|
||||||
|
const decodeTodo = S.decode(JsonifiableTodo)
|
||||||
|
|
||||||
|
|
||||||
export const todosRouter = Effect.gen(function*() {
|
export const todoRouter = Effect.gen(function*() {
|
||||||
const t = yield* TRPCBuilder
|
const t = yield* TRPCBuilder
|
||||||
const procedure = yield* RPCProcedureBuilder
|
const procedure = yield* RPCProcedureBuilder
|
||||||
|
|
||||||
@@ -19,7 +20,10 @@ export const todosRouter = Effect.gen(function*() {
|
|||||||
all: procedure
|
all: procedure
|
||||||
.query(({ ctx }) => ctx.run(Effect.gen(function*() {
|
.query(({ ctx }) => ctx.run(Effect.gen(function*() {
|
||||||
const todos = yield* TodoRepository
|
const todos = yield* TodoRepository
|
||||||
return yield* encodeTodos(yield* todos.todos.get)
|
|
||||||
|
return yield* encodeTodos(
|
||||||
|
yield* todos.todos.get
|
||||||
|
)
|
||||||
}))),
|
}))),
|
||||||
|
|
||||||
changes: procedure
|
changes: procedure
|
||||||
@@ -51,27 +55,47 @@ export const todosRouter = Effect.gen(function*() {
|
|||||||
getByID: procedure
|
getByID: procedure
|
||||||
.input(S.decodeUnknownPromise(S.String))
|
.input(S.decodeUnknownPromise(S.String))
|
||||||
.query(({ ctx, input }) => ctx.run(Effect.gen(function*() {
|
.query(({ ctx, input }) => ctx.run(Effect.gen(function*() {
|
||||||
|
const todos = yield* TodoRepository
|
||||||
|
|
||||||
return yield* encodeOptionalTodo(
|
return yield* encodeOptionalTodo(
|
||||||
yield* (yield* TodoRepository).getByID(input)
|
yield* todos.getByID(input)
|
||||||
)
|
)
|
||||||
}))),
|
}))),
|
||||||
|
|
||||||
add: procedure
|
add: procedure
|
||||||
.input(S.decodeUnknownPromise(JsonifiableTodo))
|
.input(S.decodeUnknownPromise(
|
||||||
|
S.encodedSchema(JsonifiableTodo)
|
||||||
|
))
|
||||||
.mutation(({ ctx, input }) => ctx.run(Effect.gen(function*() {
|
.mutation(({ ctx, input }) => ctx.run(Effect.gen(function*() {
|
||||||
return yield* (yield* TodoRepository).add(input)
|
const todos = yield* TodoRepository
|
||||||
|
|
||||||
|
return yield* todos.add(
|
||||||
|
yield* decodeTodo(input)
|
||||||
|
)
|
||||||
}))),
|
}))),
|
||||||
|
|
||||||
update: procedure
|
update: procedure
|
||||||
.input(S.decodeUnknownPromise(JsonifiableTodo))
|
.input(S.decodeUnknownPromise(
|
||||||
|
S.encodedSchema(JsonifiableTodo)
|
||||||
|
))
|
||||||
.mutation(({ ctx, input }) => ctx.run(Effect.gen(function*() {
|
.mutation(({ ctx, input }) => ctx.run(Effect.gen(function*() {
|
||||||
return yield* (yield* TodoRepository).update(input)
|
const todos = yield* TodoRepository
|
||||||
|
|
||||||
|
return yield* todos.update(
|
||||||
|
yield* decodeTodo(input)
|
||||||
|
)
|
||||||
}))),
|
}))),
|
||||||
|
|
||||||
remove: procedure
|
remove: procedure
|
||||||
.input(S.decodeUnknownPromise(JsonifiableTodo))
|
.input(S.decodeUnknownPromise(
|
||||||
|
S.encodedSchema(JsonifiableTodo)
|
||||||
|
))
|
||||||
.mutation(({ ctx, input }) => ctx.run(Effect.gen(function*() {
|
.mutation(({ ctx, input }) => ctx.run(Effect.gen(function*() {
|
||||||
return yield* (yield* TodoRepository).remove(input)
|
const todos = yield* TodoRepository
|
||||||
|
|
||||||
|
return yield* todos.remove(
|
||||||
|
yield* decodeTodo(input)
|
||||||
|
)
|
||||||
}))),
|
}))),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -37,7 +37,7 @@ export class TodoRepositoryService {
|
|||||||
add(todo: Todo) {
|
add(todo: Todo) {
|
||||||
return Effect.gen(this, function*() {
|
return Effect.gen(this, function*() {
|
||||||
if (Option.isSome(todo.id))
|
if (Option.isSome(todo.id))
|
||||||
return yield* Effect.fail(new TodoHasID({ todo }))
|
return yield* new TodoHasID({ todo })
|
||||||
|
|
||||||
const id: string = crypto.randomUUID()
|
const id: string = crypto.randomUUID()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { TRPCError } from "@trpc/server"
|
import type { TRPCError } from "@trpc/server"
|
||||||
import type { Effect, Runtime } from "effect"
|
import { Data, type Effect, type Runtime } from "effect"
|
||||||
import type { RuntimeFiber } from "effect/Fiber"
|
import type { RuntimeFiber } from "effect/Fiber"
|
||||||
|
import express from "express"
|
||||||
|
import type { IncomingMessage } from "node:http"
|
||||||
|
import { WebSocket } from "ws"
|
||||||
import type { Services } from "../Services"
|
import type { Services } from "../Services"
|
||||||
|
|
||||||
|
|
||||||
@@ -17,5 +20,20 @@ export interface TRPCContext {
|
|||||||
options?: Runtime.RunForkOptions,
|
options?: Runtime.RunForkOptions,
|
||||||
) => RuntimeFiber<A, TRPCError>
|
) => RuntimeFiber<A, TRPCError>
|
||||||
|
|
||||||
// req: Request
|
transaction: TRPCContextTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type TRPCContextTransaction = Data.TaggedEnum<{
|
||||||
|
Express: {
|
||||||
|
readonly req: express.Request
|
||||||
|
readonly res: express.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocket: {
|
||||||
|
readonly req: IncomingMessage
|
||||||
|
readonly res: WebSocket
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
|
||||||
|
export const TRPCContextTransactionEnum = Data.taggedEnum<TRPCContextTransaction>()
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { TRPCError } from "@trpc/server"
|
import { TRPCError } from "@trpc/server"
|
||||||
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express"
|
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express"
|
||||||
import type { CreateWSSContextFnOptions } from "@trpc/server/adapters/ws"
|
import type { CreateWSSContextFnOptions } from "@trpc/server/adapters/ws"
|
||||||
import { Context, Effect, Layer, Runtime } from "effect"
|
import { Cause, Context, Effect, Layer, Runtime } from "effect"
|
||||||
import type { Services } from "../Services"
|
import type { Services } from "../Services"
|
||||||
import type { TRPCContext } from "./TRPCContext"
|
import { TRPCContextTransactionEnum, type TRPCContext, type TRPCContextTransaction } from "./TRPCContext"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a function that instantiates a fresh context for each tRPC procedure call
|
* Provides functions that instantiate a fresh context for each tRPC procedure call
|
||||||
*/
|
*/
|
||||||
export class TRPCContextCreator extends Context.Tag("TRPCContextCreator")<TRPCContextCreator,
|
export class TRPCContextCreator extends Context.Tag("TRPCContextCreator")<TRPCContextCreator, {
|
||||||
(opts:
|
createContext: (transaction: TRPCContextTransaction) => TRPCContext
|
||||||
| CreateExpressContextOptions
|
createExpressContext: (context: CreateExpressContextOptions) => TRPCContext
|
||||||
| CreateWSSContextFnOptions
|
createWebSocketContext: (context: CreateWSSContextFnOptions) => TRPCContext
|
||||||
) => TRPCContext
|
}>() {}
|
||||||
>() {}
|
|
||||||
|
|
||||||
export module TRPCContextCreator {
|
export module TRPCContextCreator {
|
||||||
export const Live = Layer.effect(TRPCContextCreator, Effect.gen(function*() {
|
export const Live = Layer.effect(TRPCContextCreator, Effect.gen(function*() {
|
||||||
@@ -24,7 +23,7 @@ export module TRPCContextCreator {
|
|||||||
effect: Effect.Effect<A, E, Services>,
|
effect: Effect.Effect<A, E, Services>,
|
||||||
options?: { readonly signal?: AbortSignal },
|
options?: { readonly signal?: AbortSignal },
|
||||||
) => Runtime.runPromise(runtime)(
|
) => Runtime.runPromise(runtime)(
|
||||||
effect.pipe(mapErrorsToTRPC),
|
effect.pipe(mapErrors),
|
||||||
options,
|
options,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,40 +31,82 @@ export module TRPCContextCreator {
|
|||||||
effect: Effect.Effect<A, E, Services>,
|
effect: Effect.Effect<A, E, Services>,
|
||||||
options?: Runtime.RunForkOptions,
|
options?: Runtime.RunForkOptions,
|
||||||
) => Runtime.runFork(runtime)(
|
) => Runtime.runFork(runtime)(
|
||||||
effect.pipe(mapErrorsToTRPC),
|
effect.pipe(mapErrors),
|
||||||
options,
|
options,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ({ req }) => ({
|
|
||||||
|
const createContext = (transaction: TRPCContextTransaction) => ({
|
||||||
runtime,
|
runtime,
|
||||||
run,
|
run,
|
||||||
fork,
|
fork,
|
||||||
// req,
|
transaction,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
createContext,
|
||||||
|
createExpressContext: context => createContext(TRPCContextTransactionEnum.Express(context)),
|
||||||
|
createWebSocketContext: context => createContext(TRPCContextTransactionEnum.WebSocket(context)),
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const mapErrorsToTRPC = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
const mapErrors = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||||
Effect.sandbox(effect).pipe(
|
Effect.sandbox(effect).pipe(
|
||||||
Effect.catchTags({
|
Effect.catchTags({
|
||||||
Die: cause => Effect.fail(
|
|
||||||
new TRPCError({ code: "INTERNAL_SERVER_ERROR", cause })
|
|
||||||
),
|
|
||||||
Interrupt: cause => Effect.fail(
|
|
||||||
new TRPCError({ code: "INTERNAL_SERVER_ERROR", cause })
|
|
||||||
),
|
|
||||||
Fail: cause => Effect.fail(
|
|
||||||
new TRPCError({ code: "INTERNAL_SERVER_ERROR", cause })
|
|
||||||
),
|
|
||||||
Empty: cause => Effect.fail(
|
Empty: cause => Effect.fail(
|
||||||
new TRPCError({ code: "INTERNAL_SERVER_ERROR", cause })
|
new TRPCError({
|
||||||
),
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
Parallel: cause => Effect.fail(
|
cause: new TRPCErrorCause(cause),
|
||||||
new TRPCError({ code: "INTERNAL_SERVER_ERROR", cause })
|
|
||||||
),
|
|
||||||
Sequential: cause => Effect.fail(
|
|
||||||
new TRPCError({ code: "INTERNAL_SERVER_ERROR", 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ module.exports = {
|
|||||||
'warn',
|
'warn',
|
||||||
{ allowConstantExport: true },
|
{ allowConstantExport: true },
|
||||||
],
|
],
|
||||||
|
"@typescript-eslint/no-namespace": "off",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,38 +9,39 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/schema": "^0.68.17",
|
"@effect/schema": "^0.68.26",
|
||||||
"@emotion/react": "^11.11.4",
|
"@radix-ui/themes": "^3.1.1",
|
||||||
"@emotion/styled": "^11.11.5",
|
|
||||||
"@mui/icons-material": "^5.16.1",
|
|
||||||
"@mui/material": "^5.16.1",
|
|
||||||
"@tanstack/react-query": "4",
|
"@tanstack/react-query": "4",
|
||||||
"@tanstack/react-router": "^1.44.2",
|
"@tanstack/react-router": "^1.45.6",
|
||||||
"@thilawyn/thilalib": "^0.1.5",
|
"@thilawyn/thilalib": "^0.1.5",
|
||||||
"@todo-tests/common": "workspace:*",
|
"@todo-tests/common": "workspace:*",
|
||||||
"@trpc/client": "^10.45.2",
|
"@trpc/client": "^10.45.2",
|
||||||
"@trpc/react-query": "^10.45.2",
|
"@trpc/react-query": "^10.45.2",
|
||||||
"effect": "^3.4.7",
|
"effect": "^3.5.6",
|
||||||
"mobx": "^6.13.0",
|
"framer-motion": "^11.3.8",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"lucide-react": "^0.412.0",
|
||||||
|
"mobx": "^6.13.1",
|
||||||
"mobx-react-lite": "^4.0.7",
|
"mobx-react-lite": "^4.0.7",
|
||||||
"primereact": "^10.7.0",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1",
|
||||||
|
"remeda": "^2.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/router-devtools": "^1.44.2",
|
"@tanstack/router-devtools": "^1.45.6",
|
||||||
"@tanstack/router-plugin": "^1.44.3",
|
"@tanstack/router-plugin": "^1.45.3",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||||
"@typescript-eslint/parser": "^7.15.0",
|
"@typescript-eslint/parser": "^7.16.1",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"eslint-plugin-react-refresh": "^0.4.7",
|
"eslint-plugin-react-refresh": "^0.4.8",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.6",
|
||||||
"vite": "^5.3.3"
|
"vite": "^5.3.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
34
packages/webui/src/AnimatedOutlet.tsx
Normal file
34
packages/webui/src/AnimatedOutlet.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { getRouterContext, Outlet } from "@tanstack/react-router"
|
||||||
|
import { motion, useIsPresent, type HTMLMotionProps } from "framer-motion"
|
||||||
|
import { cloneDeep } from "lodash-es"
|
||||||
|
import { forwardRef, useContext, useRef } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export module AnimatedOutlet {
|
||||||
|
export interface Props extends HTMLMotionProps<"div"> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const AnimatedOutlet = forwardRef<HTMLDivElement, AnimatedOutlet.Props>((props, ref) => {
|
||||||
|
|
||||||
|
const RouterContext = getRouterContext()
|
||||||
|
const routerContext = useContext(RouterContext)
|
||||||
|
const renderedContext = useRef(routerContext)
|
||||||
|
|
||||||
|
const isPresent = useIsPresent()
|
||||||
|
if (isPresent)
|
||||||
|
renderedContext.current = cloneDeep(routerContext)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RouterContext.Provider value={renderedContext.current}>
|
||||||
|
<Outlet />
|
||||||
|
</RouterContext.Provider>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
|
||||||
|
})
|
||||||
13
packages/webui/src/data/Todo.class.ts
Normal file
13
packages/webui/src/data/Todo.class.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Schema as S } from "@effect/schema"
|
||||||
|
import { MobXObservable, MutableClass } from "@thilawyn/thilalib/effect/schema/class"
|
||||||
|
import { JsonifiableTodo as CommonJsonifiableTodo, Todo as CommonTodo } from "@todo-tests/common/data"
|
||||||
|
|
||||||
|
|
||||||
|
export class Todo
|
||||||
|
extends MutableClass<Todo>("Todo")(CommonTodo.fields).pipe(
|
||||||
|
MobXObservable
|
||||||
|
)
|
||||||
|
{}
|
||||||
|
|
||||||
|
|
||||||
|
export const JsonifiableTodo = CommonJsonifiableTodo.pipe(S.compose(Todo))
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { MobXObservable, MutableClass } from "@thilawyn/thilalib/effect/schema/class"
|
|
||||||
import { Todo as CommonTodo } from "@todo-tests/common/data"
|
|
||||||
|
|
||||||
|
|
||||||
export class Todo
|
|
||||||
extends MutableClass<Todo>("Todo")(CommonTodo.fields).pipe(
|
|
||||||
MobXObservable
|
|
||||||
)
|
|
||||||
{}
|
|
||||||
@@ -1 +1 @@
|
|||||||
export * from "./Todo"
|
export * from "./Todo.class"
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import { Theme } from "@radix-ui/themes"
|
||||||
import { RouterProvider, createRouter } from "@tanstack/react-router"
|
import { RouterProvider, createRouter } from "@tanstack/react-router"
|
||||||
import { PrimeReactProvider } from "primereact/api"
|
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import ReactDOM from "react-dom/client"
|
import ReactDOM from "react-dom/client"
|
||||||
import { routeTree } from "./routeTree.gen"
|
import { routeTree } from "./routeTree.gen"
|
||||||
import { TRPCClientProvider } from "./trpc/TRPCClientProvider"
|
import { TRPCClientProvider } from "./trpc/TRPCClientProvider"
|
||||||
|
|
||||||
import "primereact/resources/themes/lara-light-cyan/theme.css"
|
|
||||||
import "./index.css"
|
import "@radix-ui/themes/styles.css"
|
||||||
|
import "./tailwind.css"
|
||||||
|
|
||||||
|
|
||||||
const router = createRouter({ routeTree })
|
const router = createRouter({ routeTree })
|
||||||
@@ -17,13 +18,12 @@ declare module "@tanstack/react-router" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<PrimeReactProvider>
|
<Theme appearance="dark">
|
||||||
<TRPCClientProvider>
|
<TRPCClientProvider>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</TRPCClientProvider>
|
</TRPCClientProvider>
|
||||||
</PrimeReactProvider>
|
</Theme>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,10 +16,16 @@ import { Route as rootRoute } from './routes/__root'
|
|||||||
|
|
||||||
// Create Virtual Routes
|
// Create Virtual Routes
|
||||||
|
|
||||||
|
const AboutLazyImport = createFileRoute('/about')()
|
||||||
const IndexLazyImport = createFileRoute('/')()
|
const IndexLazyImport = createFileRoute('/')()
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
|
const AboutLazyRoute = AboutLazyImport.update({
|
||||||
|
path: '/about',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any).lazy(() => import('./routes/about.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const IndexLazyRoute = IndexLazyImport.update({
|
const IndexLazyRoute = IndexLazyImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@@ -36,12 +42,22 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexLazyImport
|
preLoaderRoute: typeof IndexLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/about': {
|
||||||
|
id: '/about'
|
||||||
|
path: '/about'
|
||||||
|
fullPath: '/about'
|
||||||
|
preLoaderRoute: typeof AboutLazyImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and export the route tree
|
// Create and export the route tree
|
||||||
|
|
||||||
export const routeTree = rootRoute.addChildren({ IndexLazyRoute })
|
export const routeTree = rootRoute.addChildren({
|
||||||
|
IndexLazyRoute,
|
||||||
|
AboutLazyRoute,
|
||||||
|
})
|
||||||
|
|
||||||
/* prettier-ignore-end */
|
/* prettier-ignore-end */
|
||||||
|
|
||||||
@@ -51,11 +67,15 @@ export const routeTree = rootRoute.addChildren({ IndexLazyRoute })
|
|||||||
"__root__": {
|
"__root__": {
|
||||||
"filePath": "__root.tsx",
|
"filePath": "__root.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/"
|
"/",
|
||||||
|
"/about"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
"filePath": "index.lazy.tsx"
|
"filePath": "index.lazy.tsx"
|
||||||
|
},
|
||||||
|
"/about": {
|
||||||
|
"filePath": "about.lazy.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Outlet, createRootRoute } from "@tanstack/react-router"
|
import { Button, Container, Flex } from "@radix-ui/themes"
|
||||||
|
import { createRootRoute, useMatch, useMatches, useNavigate } from "@tanstack/react-router"
|
||||||
|
import { AnimatePresence } from "framer-motion"
|
||||||
import { Suspense, lazy } from "react"
|
import { Suspense, lazy } from "react"
|
||||||
|
import { AnimatedOutlet } from "../AnimatedOutlet"
|
||||||
|
|
||||||
|
|
||||||
const TanStackRouterDevtools = process.env.NODE_ENV === "production"
|
const TanStackRouterDevtools = process.env.NODE_ENV === "production"
|
||||||
@@ -8,15 +11,63 @@ const TanStackRouterDevtools = process.env.NODE_ENV === "production"
|
|||||||
default: res.TanStackRouterDevtools
|
default: res.TanStackRouterDevtools
|
||||||
})))
|
})))
|
||||||
|
|
||||||
|
const ThemePanel = process.env.NODE_ENV === "production"
|
||||||
|
? () => null
|
||||||
|
: lazy(() => import("@radix-ui/themes").then(res => ({
|
||||||
|
default: res.ThemePanel
|
||||||
|
})))
|
||||||
|
|
||||||
|
|
||||||
export function Root() {
|
export function Root() {
|
||||||
return <>
|
|
||||||
<div className="container mx-auto mt-8">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Suspense><TanStackRouterDevtools /></Suspense>
|
const matches = useMatches()
|
||||||
|
const match = useMatch({ strict: false })
|
||||||
|
const nextMatch = matches[ matches.findIndex(d => d.id === match.id) + 1 ]
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Container pt="8" pb="4">
|
||||||
|
<Flex
|
||||||
|
gap="4"
|
||||||
|
justify="center"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
onClick={() => navigate({ to: "/" })}
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
onClick={() => navigate({ to: "/about" })}
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
<AnimatedOutlet
|
||||||
|
key={nextMatch.id}
|
||||||
|
|
||||||
|
transition={{ x: { type: "spring", stiffness: 300, damping: 30 } }}
|
||||||
|
|
||||||
|
initial={{ x: "100vw", opacity: 0 }}
|
||||||
|
animate={{ x: "0", opacity: 1 }}
|
||||||
|
exit={{ x: "-100vw", opacity: 0 }}
|
||||||
|
/>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<Suspense>
|
||||||
|
<TanStackRouterDevtools />
|
||||||
|
<ThemePanel />
|
||||||
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createRootRoute({ component: Root })
|
export const Route = createRootRoute({ component: Root })
|
||||||
|
|||||||
16
packages/webui/src/routes/about.lazy.tsx
Normal file
16
packages/webui/src/routes/about.lazy.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Container, Heading } from "@radix-ui/themes"
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router"
|
||||||
|
import { observer } from "mobx-react-lite"
|
||||||
|
|
||||||
|
|
||||||
|
export const About = observer(() => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Heading align="center">About</Heading>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/about")({ component: About })
|
||||||
@@ -1,39 +1,51 @@
|
|||||||
import { Schema as S } from "@effect/schema"
|
import { Schema as S } from "@effect/schema"
|
||||||
|
import { Container, Flex, Text } from "@radix-ui/themes"
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router"
|
import { createLazyFileRoute } from "@tanstack/react-router"
|
||||||
import { JsonifiableTodo } from "@todo-tests/common/data"
|
import { ServerTime } from "@todo-tests/common/data"
|
||||||
import { Option, flow, identity } from "effect"
|
import { Option, flow, identity } from "effect"
|
||||||
import { IObservableArray, observable } from "mobx"
|
import { type IObservableArray, observable } from "mobx"
|
||||||
import { observer } from "mobx-react-lite"
|
import { observer } from "mobx-react-lite"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Todo } from "../data"
|
import { JsonifiableTodo, Todo } from "../data"
|
||||||
import { VTodo } from "../todo/VTodo"
|
import { VTodo } from "../todo/VTodo"
|
||||||
import { trpc } from "../trpc/trpc"
|
import { trpc } from "../trpc/trpc"
|
||||||
|
|
||||||
|
|
||||||
|
const decodeServerTime = S.decodeSync(ServerTime)
|
||||||
|
|
||||||
const decodeTodos = flow(
|
const decodeTodos = flow(
|
||||||
S.decodeSync(
|
S.decodeSync(
|
||||||
S.mutable(S.Array(
|
S.mutable(S.Array(JsonifiableTodo))
|
||||||
JsonifiableTodo.pipe(S.compose(Todo))
|
|
||||||
))
|
|
||||||
),
|
),
|
||||||
|
|
||||||
observable.array,
|
observable.array,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
export const Index = observer(() => {
|
export const Index = observer(() => {
|
||||||
|
|
||||||
|
const [serverTime, setServerTime] = useState(new Date())
|
||||||
|
|
||||||
|
trpc.serverTime.useSubscription(undefined, {
|
||||||
|
onData: flow(decodeServerTime, setServerTime)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
const [todos, setTodos] = useState<IObservableArray<Todo>>(observable.array())
|
const [todos, setTodos] = useState<IObservableArray<Todo>>(observable.array())
|
||||||
|
|
||||||
trpc.todos.changes.useSubscription(undefined, {
|
trpc.todo.changes.useSubscription(undefined, {
|
||||||
onData: data => {
|
onData: flow(decodeTodos, setTodos)
|
||||||
setTodos(decodeTodos(data))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1 items-stretch">
|
<Container>
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
align="stretch"
|
||||||
|
gap="1"
|
||||||
|
>
|
||||||
|
<Text align="center">{serverTime.toString()}</Text>
|
||||||
|
|
||||||
{todos.map(todo => (
|
{todos.map(todo => (
|
||||||
<VTodo
|
<VTodo
|
||||||
key={Option.match(todo.id, {
|
key={Option.match(todo.id, {
|
||||||
@@ -44,7 +56,8 @@ export const Index = observer(() => {
|
|||||||
todo={todo}
|
todo={todo}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Flex>
|
||||||
|
</Container>
|
||||||
)
|
)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,42 +1,83 @@
|
|||||||
import { Schema as S } from "@effect/schema"
|
import { Schema as S } from "@effect/schema"
|
||||||
import { ArrowUpward } from "@mui/icons-material"
|
import { Card, Flex, IconButton, Skeleton, Text, Tooltip } from "@radix-ui/themes"
|
||||||
import { JsonifiableTodo } from "@todo-tests/common/data"
|
import { ChevronDown, ChevronUp, X } from "lucide-react"
|
||||||
import { observer } from "mobx-react-lite"
|
import { observer } from "mobx-react-lite"
|
||||||
import { Button } from "primereact/button"
|
import { JsonifiableTodo, Todo } from "../data"
|
||||||
import { Todo } from "../data"
|
|
||||||
import { trpc } from "../trpc/trpc"
|
import { trpc } from "../trpc/trpc"
|
||||||
|
|
||||||
|
|
||||||
const encodeTodo = S.encodeSync(
|
const encodeTodo = S.encodeSync(JsonifiableTodo)
|
||||||
JsonifiableTodo.pipe(S.compose(Todo))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
export interface VTodoProps {
|
export module VTodo {
|
||||||
|
export interface Props {
|
||||||
todo: Todo
|
todo: Todo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const VTodo = observer(({ todo }: VTodoProps) => {
|
export const VTodo = observer(({ todo }: VTodo.Props) => {
|
||||||
|
|
||||||
const updateTodo = trpc.todos.update.useMutation()
|
const updateTodo = trpc.todo.update.useMutation()
|
||||||
|
const removeTodo = trpc.todo.remove.useMutation()
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row justify-between content-center p-4 rounded-lg border-2 border-black">
|
<Skeleton loading={updateTodo.isLoading || removeTodo.isLoading}>
|
||||||
<p>{todo.content}</p>
|
<Card>
|
||||||
|
<Flex
|
||||||
|
justify="between"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<Text>{todo.content}</Text>
|
||||||
|
|
||||||
<div className="flex flex-row gap-1 content-center">
|
<Flex
|
||||||
<Button
|
align="center"
|
||||||
severity="secondary"
|
gap="1"
|
||||||
rounded
|
>
|
||||||
size="small"
|
<Tooltip content="Move down">
|
||||||
icon={<ArrowUpward />}
|
<IconButton
|
||||||
|
variant="outline"
|
||||||
|
|
||||||
onClick={() => updateTodo.mutate(encodeTodo(todo))}
|
onClick={() => updateTodo.mutate(encodeTodo(
|
||||||
/>
|
new Todo({
|
||||||
</div>
|
...todo,
|
||||||
</div>
|
order: todo.order + 2,
|
||||||
|
}, { disableValidation: true })
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
<ChevronDown />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content="Move up">
|
||||||
|
<IconButton
|
||||||
|
variant="outline"
|
||||||
|
|
||||||
|
onClick={() => updateTodo.mutate(encodeTodo(
|
||||||
|
new Todo({
|
||||||
|
...todo,
|
||||||
|
order: todo.order - 2,
|
||||||
|
}, { disableValidation: true })
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
<ChevronUp />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content="Delete">
|
||||||
|
<IconButton
|
||||||
|
variant="outline"
|
||||||
|
|
||||||
|
onClick={() => removeTodo.mutate(encodeTodo(todo))}
|
||||||
|
>
|
||||||
|
<X />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Skeleton>
|
||||||
)
|
)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,29 +1,35 @@
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||||
import { createWSClient, wsLink } from "@trpc/client"
|
import { createWSClient, httpBatchLink, splitLink, wsLink } from "@trpc/client"
|
||||||
import { ReactNode, useState } from "react"
|
import { type ReactNode, useMemo, useState } from "react"
|
||||||
import { trpc } from "./trpc"
|
import { trpc } from "./trpc"
|
||||||
|
|
||||||
|
|
||||||
export interface TRPCClientProviderProps {
|
export module TRPCClientProvider {
|
||||||
|
export interface Props {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function TRPCClientProvider({ children }: TRPCClientProviderProps) {
|
export function TRPCClientProvider({ children }: TRPCClientProvider.Props) {
|
||||||
|
|
||||||
const [queryClient] = useState(new QueryClient())
|
const [queryClient] = useState(new QueryClient())
|
||||||
const [wsClient] = useState(createWSClient({ url: "ws://localhost:8080" }))
|
const [wsClient] = useState(createWSClient({ url: "/rpc" }))
|
||||||
|
|
||||||
const [trpcClient] = useState(trpc.createClient({
|
const trpcClient = useMemo(() => trpc.createClient({
|
||||||
links: [
|
links: [
|
||||||
// httpBatchLink({
|
splitLink({
|
||||||
// url: "http://localhost:8080/rpc",
|
condition: op => op.type === "subscription",
|
||||||
// headers: async () => ({}),
|
|
||||||
// }),
|
|
||||||
|
|
||||||
wsLink({ client: wsClient }),
|
true: wsLink({ client: wsClient }),
|
||||||
|
|
||||||
|
false: httpBatchLink({
|
||||||
|
url: "/rpc",
|
||||||
|
headers: {},
|
||||||
|
}),
|
||||||
|
})
|
||||||
]
|
]
|
||||||
}))
|
}), [wsClient])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
|
|||||||
@@ -5,8 +5,13 @@ import { defineConfig } from "vite"
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
host: true,
|
||||||
|
port: 80,
|
||||||
|
},
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
TanStackRouterVite(),
|
TanStackRouterVite(),
|
||||||
react(),
|
react(),
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user