Compare commits
61 Commits
d737bda473
...
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 | ||
|
|
a27ff5834e | ||
|
|
9a382a80ff |
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,
|
||||
"dependencies": {
|
||||
"@effect/schema": "^0.68.17",
|
||||
"@effect/schema": "^0.68.26",
|
||||
"@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"
|
||||
|
||||
@@ -14,12 +14,12 @@ export module Identifiable {
|
||||
Kind extends string,
|
||||
ID,
|
||||
>(
|
||||
self: Identifiable<Kind, ID>,
|
||||
that: Identifiable<Kind, ID>,
|
||||
to: Identifiable<Kind, ID>,
|
||||
): boolean {
|
||||
// Two elements can only be equal if they both have a defined ID
|
||||
return Option.isSome(that.id) && Option.isSome(to.id)
|
||||
? Equal.equals(that.id.value, to.id.value)
|
||||
return Option.isSome(self.id) && Option.isSome(that.id)
|
||||
? Equal.equals(self.id.value, that.id.value)
|
||||
: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,23 @@
|
||||
".": "./src/router.ts"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development bun --watch src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform": "^0.58.20",
|
||||
"@effect/platform-bun": "^0.38.19",
|
||||
"@effect/schema": "^0.68.17",
|
||||
"@effect/platform": "^0.59.2",
|
||||
"@effect/platform-bun": "^0.39.2",
|
||||
"@effect/schema": "^0.68.26",
|
||||
"@thilawyn/thilalib": "^0.1.5",
|
||||
"@todo-tests/common": "workspace:*",
|
||||
"@trpc/server": "^10.45.2",
|
||||
"effect": "^3.4.7",
|
||||
"effect": "^3.5.6",
|
||||
"express": "^4.19.2",
|
||||
"trpc-playground": "^1.0.4",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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 module Services {
|
||||
export const Live = Layer.mergeAll(
|
||||
export const Live: Layer.Layer<Services, never, never> = Layer.mergeAll(
|
||||
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
|
||||
) 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 { httpPort } from "../config"
|
||||
import { ServerConfig } from "../ServerConfig"
|
||||
import { ExpressApp } from "./ExpressApp"
|
||||
|
||||
|
||||
@@ -9,14 +9,16 @@ export class ExpressHTTPServer extends Context.Tag("ExpressHTTPServer")<ExpressH
|
||||
export module ExpressHTTPServer {
|
||||
export const Live = Layer.effect(ExpressHTTPServer, Effect.acquireRelease(
|
||||
Effect.gen(function*() {
|
||||
const runSync = yield* Effect.runtime().pipe(
|
||||
Effect.map(Runtime.runSync)
|
||||
)
|
||||
|
||||
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*() {
|
||||
|
||||
@@ -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 { 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 { ExpressApp } from "./http/ExpressApp"
|
||||
import { ExpressHTTPServer } from "./http/ExpressHTTPServer"
|
||||
import { WebSocketServer } from "./http/WebSocketServer"
|
||||
import { RPCPlaygroundRoute } from "./rpc/RPCPlaygroundRoute"
|
||||
import { RPCRoute } from "./rpc/RPCRoute"
|
||||
import { RPCRouter } from "./rpc/RPCRouter"
|
||||
import { RPCWebSocketHandler } from "./rpc/RPCWebSocketHandler"
|
||||
import { RPCWebSocketServer } from "./rpc/RPCWebSocketServer"
|
||||
import { RPCProcedureBuilder } from "./rpc/procedures/RPCProcedureBuilder"
|
||||
import { TodoRepository } from "./todo/TodoRepository"
|
||||
import { TRPCBuilder } from "./trpc/TRPCBuilder"
|
||||
@@ -18,22 +18,45 @@ import { TRPCContextCreator } from "./trpc/TRPCContextCreator"
|
||||
const ServerDev = Layer.empty.pipe(
|
||||
Layer.provideMerge(RPCRoute.Live),
|
||||
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(RPCProcedureBuilder.Live),
|
||||
Layer.provideMerge(TRPCBuilder.Live),
|
||||
Layer.provideMerge(TRPCContextCreator.Live),
|
||||
|
||||
Layer.provideMerge(WebSocketServer.Live),
|
||||
Layer.provideMerge(ExpressHTTPServer.Live),
|
||||
Layer.provideMerge(ExpressApp.Live),
|
||||
)
|
||||
|
||||
|
||||
const main = Effect.gen(function*() {
|
||||
const mode = yield* ServerConfig.mode
|
||||
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({
|
||||
id: Option.none(),
|
||||
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(
|
||||
Effect.provide(Services.Dev),
|
||||
Effect.provide(Logger.structured),
|
||||
Effect.scoped,
|
||||
))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Effect, Layer } from "effect"
|
||||
import { expressHandler } from "trpc-playground/handlers/express"
|
||||
import { rpcHTTPPlaygroundRoot, rpcHTTPRoot } from "../config"
|
||||
import { ServerConfig } from "../ServerConfig"
|
||||
import { ExpressApp } from "../http/ExpressApp"
|
||||
import { RPCRouter } from "./RPCRouter"
|
||||
|
||||
@@ -10,10 +10,10 @@ export module RPCPlaygroundRoute {
|
||||
|
||||
export const Dev = Layer.effectDiscard(Effect.gen(function*() {
|
||||
const app = yield* ExpressApp
|
||||
const playgroundEndpoint = yield* rpcHTTPPlaygroundRoot
|
||||
const playgroundEndpoint = yield* ServerConfig.rpcHTTPPlaygroundRoot
|
||||
|
||||
const handler = expressHandler({
|
||||
trpcApiEndpoint: yield* rpcHTTPRoot,
|
||||
trpcApiEndpoint: yield* ServerConfig.rpcHTTPRoot,
|
||||
playgroundEndpoint,
|
||||
router: yield* RPCRouter,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createExpressMiddleware } from "@trpc/server/adapters/express"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { rpcHTTPRoot } from "../config"
|
||||
import { ServerConfig } from "../ServerConfig"
|
||||
import { ExpressApp } from "../http/ExpressApp"
|
||||
import { TRPCContextCreator } from "../trpc/TRPCContextCreator"
|
||||
import { RPCRouter } from "./RPCRouter"
|
||||
@@ -10,10 +10,10 @@ export module RPCRoute {
|
||||
export const Live = Layer.effectDiscard(Effect.gen(function*() {
|
||||
const app = yield* ExpressApp
|
||||
|
||||
app.use(yield* rpcHTTPRoot,
|
||||
app.use(yield* ServerConfig.rpcHTTPRoot,
|
||||
createExpressMiddleware({
|
||||
router: yield* RPCRouter,
|
||||
createContext: yield* TRPCContextCreator,
|
||||
createContext: (yield* TRPCContextCreator).createExpressContext,
|
||||
}),
|
||||
)
|
||||
}))
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { TRPCBuilder } from "../trpc/TRPCBuilder"
|
||||
import { RPCProcedureBuilder } from "./procedures/RPCProcedureBuilder"
|
||||
import { todosRouter } from "./routers/todos"
|
||||
import { indexRouter } from "./routers"
|
||||
|
||||
|
||||
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,
|
||||
Effect.Effect.Success<typeof router>
|
||||
Effect.Effect.Success<typeof indexRouter>
|
||||
>() {}
|
||||
|
||||
export module 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 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 procedure = yield* RPCProcedureBuilder
|
||||
|
||||
@@ -19,7 +20,10 @@ export const todosRouter = Effect.gen(function*() {
|
||||
all: procedure
|
||||
.query(({ ctx }) => ctx.run(Effect.gen(function*() {
|
||||
const todos = yield* TodoRepository
|
||||
return yield* encodeTodos(yield* todos.todos.get)
|
||||
|
||||
return yield* encodeTodos(
|
||||
yield* todos.todos.get
|
||||
)
|
||||
}))),
|
||||
|
||||
changes: procedure
|
||||
@@ -51,27 +55,47 @@ export const todosRouter = Effect.gen(function*() {
|
||||
getByID: procedure
|
||||
.input(S.decodeUnknownPromise(S.String))
|
||||
.query(({ ctx, input }) => ctx.run(Effect.gen(function*() {
|
||||
const todos = yield* TodoRepository
|
||||
|
||||
return yield* encodeOptionalTodo(
|
||||
yield* (yield* TodoRepository).getByID(input)
|
||||
yield* todos.getByID(input)
|
||||
)
|
||||
}))),
|
||||
|
||||
add: procedure
|
||||
.input(S.decodeUnknownPromise(JsonifiableTodo))
|
||||
.input(S.decodeUnknownPromise(
|
||||
S.encodedSchema(JsonifiableTodo)
|
||||
))
|
||||
.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
|
||||
.input(S.decodeUnknownPromise(JsonifiableTodo))
|
||||
.input(S.decodeUnknownPromise(
|
||||
S.encodedSchema(JsonifiableTodo)
|
||||
))
|
||||
.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
|
||||
.input(S.decodeUnknownPromise(JsonifiableTodo))
|
||||
.input(S.decodeUnknownPromise(
|
||||
S.encodedSchema(JsonifiableTodo)
|
||||
))
|
||||
.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) {
|
||||
return Effect.gen(this, function*() {
|
||||
if (Option.isSome(todo.id))
|
||||
return yield* Effect.fail(new TodoHasID({ todo }))
|
||||
return yield* new TodoHasID({ todo })
|
||||
|
||||
const id: string = crypto.randomUUID()
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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 express from "express"
|
||||
import type { IncomingMessage } from "node:http"
|
||||
import { WebSocket } from "ws"
|
||||
import type { Services } from "../Services"
|
||||
|
||||
|
||||
@@ -17,5 +20,20 @@ export interface TRPCContext {
|
||||
options?: Runtime.RunForkOptions,
|
||||
) => 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 type { CreateExpressContextOptions } from "@trpc/server/adapters/express"
|
||||
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 { 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,
|
||||
(opts:
|
||||
| CreateExpressContextOptions
|
||||
| CreateWSSContextFnOptions
|
||||
) => TRPCContext
|
||||
>() {}
|
||||
export class TRPCContextCreator extends Context.Tag("TRPCContextCreator")<TRPCContextCreator, {
|
||||
createContext: (transaction: TRPCContextTransaction) => TRPCContext
|
||||
createExpressContext: (context: CreateExpressContextOptions) => TRPCContext
|
||||
createWebSocketContext: (context: CreateWSSContextFnOptions) => TRPCContext
|
||||
}>() {}
|
||||
|
||||
export module TRPCContextCreator {
|
||||
export const Live = Layer.effect(TRPCContextCreator, Effect.gen(function*() {
|
||||
@@ -24,7 +23,7 @@ export module TRPCContextCreator {
|
||||
effect: Effect.Effect<A, E, Services>,
|
||||
options?: { readonly signal?: AbortSignal },
|
||||
) => Runtime.runPromise(runtime)(
|
||||
effect.pipe(mapErrorsToTRPC),
|
||||
effect.pipe(mapErrors),
|
||||
options,
|
||||
)
|
||||
|
||||
@@ -32,40 +31,82 @@ export module TRPCContextCreator {
|
||||
effect: Effect.Effect<A, E, Services>,
|
||||
options?: Runtime.RunForkOptions,
|
||||
) => Runtime.runFork(runtime)(
|
||||
effect.pipe(mapErrorsToTRPC),
|
||||
effect.pipe(mapErrors),
|
||||
options,
|
||||
)
|
||||
|
||||
return ({ req }) => ({
|
||||
|
||||
const createContext = (transaction: TRPCContextTransaction) => ({
|
||||
runtime,
|
||||
run,
|
||||
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.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(
|
||||
new TRPCError({ code: "INTERNAL_SERVER_ERROR", cause })
|
||||
new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
cause: new TRPCErrorCause(cause),
|
||||
})
|
||||
),
|
||||
Parallel: 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 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',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
"@typescript-eslint/no-namespace": "off",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,34 +9,39 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/schema": "^0.68.17",
|
||||
"@effect/schema": "^0.68.26",
|
||||
"@radix-ui/themes": "^3.1.1",
|
||||
"@tanstack/react-query": "4",
|
||||
"@tanstack/react-router": "^1.44.2",
|
||||
"@tanstack/react-router": "^1.45.6",
|
||||
"@thilawyn/thilalib": "^0.1.5",
|
||||
"@todo-tests/common": "workspace:*",
|
||||
"@trpc/client": "^10.45.2",
|
||||
"@trpc/react-query": "^10.45.2",
|
||||
"effect": "^3.4.7",
|
||||
"mobx": "^6.13.0",
|
||||
"effect": "^3.5.6",
|
||||
"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",
|
||||
"primereact": "^10.7.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"react-dom": "^18.3.1",
|
||||
"remeda": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/router-devtools": "^1.44.2",
|
||||
"@tanstack/router-plugin": "^1.44.3",
|
||||
"@tanstack/router-devtools": "^1.45.6",
|
||||
"@tanstack/router-plugin": "^1.45.3",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
||||
"@typescript-eslint/parser": "^7.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"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",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"vite": "^5.3.3"
|
||||
"tailwindcss": "^3.4.6",
|
||||
"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 { PrimeReactProvider } from "primereact/api"
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { routeTree } from "./routeTree.gen"
|
||||
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 })
|
||||
@@ -17,13 +18,12 @@ declare module "@tanstack/react-router" {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<PrimeReactProvider>
|
||||
<Theme appearance="dark">
|
||||
<TRPCClientProvider>
|
||||
<RouterProvider router={router} />
|
||||
</TRPCClientProvider>
|
||||
</PrimeReactProvider>
|
||||
</Theme>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
||||
@@ -16,10 +16,16 @@ import { Route as rootRoute } from './routes/__root'
|
||||
|
||||
// Create Virtual Routes
|
||||
|
||||
const AboutLazyImport = createFileRoute('/about')()
|
||||
const IndexLazyImport = createFileRoute('/')()
|
||||
|
||||
// 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({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
@@ -36,12 +42,22 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/about': {
|
||||
id: '/about'
|
||||
path: '/about'
|
||||
fullPath: '/about'
|
||||
preLoaderRoute: typeof AboutLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export the route tree
|
||||
|
||||
export const routeTree = rootRoute.addChildren({ IndexLazyRoute })
|
||||
export const routeTree = rootRoute.addChildren({
|
||||
IndexLazyRoute,
|
||||
AboutLazyRoute,
|
||||
})
|
||||
|
||||
/* prettier-ignore-end */
|
||||
|
||||
@@ -51,11 +67,15 @@ export const routeTree = rootRoute.addChildren({ IndexLazyRoute })
|
||||
"__root__": {
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/"
|
||||
"/",
|
||||
"/about"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
"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 { AnimatedOutlet } from "../AnimatedOutlet"
|
||||
|
||||
|
||||
const TanStackRouterDevtools = process.env.NODE_ENV === "production"
|
||||
@@ -8,15 +11,63 @@ const TanStackRouterDevtools = process.env.NODE_ENV === "production"
|
||||
default: res.TanStackRouterDevtools
|
||||
})))
|
||||
|
||||
const ThemePanel = process.env.NODE_ENV === "production"
|
||||
? () => null
|
||||
: lazy(() => import("@radix-ui/themes").then(res => ({
|
||||
default: res.ThemePanel
|
||||
})))
|
||||
|
||||
|
||||
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 })
|
||||
|
||||
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,49 +1,63 @@
|
||||
import { Schema as S } from "@effect/schema"
|
||||
import { Container, Flex, Text } from "@radix-ui/themes"
|
||||
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 { IObservableArray, observable } from "mobx"
|
||||
import { type IObservableArray, observable } from "mobx"
|
||||
import { observer } from "mobx-react-lite"
|
||||
import { useState } from "react"
|
||||
import { Todo } from "../data"
|
||||
import { JsonifiableTodo, Todo } from "../data"
|
||||
import { VTodo } from "../todo/VTodo"
|
||||
import { trpc } from "../trpc/trpc"
|
||||
|
||||
|
||||
const decodeServerTime = S.decodeSync(ServerTime)
|
||||
|
||||
const decodeTodos = flow(
|
||||
S.decodeSync(
|
||||
S.mutable(S.Array(
|
||||
JsonifiableTodo.pipe(S.compose(Todo))
|
||||
))
|
||||
S.mutable(S.Array(JsonifiableTodo))
|
||||
),
|
||||
|
||||
observable.array,
|
||||
)
|
||||
|
||||
|
||||
export const Index = observer(() => {
|
||||
|
||||
const [todos, setTodos] = useState<IObservableArray<Todo>>(observable.array())
|
||||
const [serverTime, setServerTime] = useState(new Date())
|
||||
|
||||
trpc.todos.changes.useSubscription(undefined, {
|
||||
onData: data => {
|
||||
setTodos(decodeTodos(data))
|
||||
}
|
||||
trpc.serverTime.useSubscription(undefined, {
|
||||
onData: flow(decodeServerTime, setServerTime)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 items-stretch">
|
||||
{todos.map(todo => (
|
||||
<VTodo
|
||||
key={Option.match(todo.id, {
|
||||
onSome: identity,
|
||||
onNone: () => todo.order,
|
||||
})}
|
||||
|
||||
todo={todo}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
const [todos, setTodos] = useState<IObservableArray<Todo>>(observable.array())
|
||||
|
||||
trpc.todo.changes.useSubscription(undefined, {
|
||||
onData: flow(decodeTodos, setTodos)
|
||||
})
|
||||
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Flex
|
||||
direction="column"
|
||||
align="stretch"
|
||||
gap="1"
|
||||
>
|
||||
<Text align="center">{serverTime.toString()}</Text>
|
||||
|
||||
{todos.map(todo => (
|
||||
<VTodo
|
||||
key={Option.match(todo.id, {
|
||||
onSome: identity,
|
||||
onNone: () => todo.order,
|
||||
})}
|
||||
|
||||
todo={todo}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
@@ -1,19 +1,83 @@
|
||||
import { Schema as S } from "@effect/schema"
|
||||
import { Card, Flex, IconButton, Skeleton, Text, Tooltip } from "@radix-ui/themes"
|
||||
import { ChevronDown, ChevronUp, X } from "lucide-react"
|
||||
import { observer } from "mobx-react-lite"
|
||||
import { Card } from "primereact/card"
|
||||
import { Todo } from "../data"
|
||||
import { JsonifiableTodo, Todo } from "../data"
|
||||
import { trpc } from "../trpc/trpc"
|
||||
|
||||
|
||||
export interface VTodoProps {
|
||||
todo: Todo
|
||||
const encodeTodo = S.encodeSync(JsonifiableTodo)
|
||||
|
||||
|
||||
export module VTodo {
|
||||
export interface Props {
|
||||
todo: Todo
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const VTodo = observer(({ todo }: VTodoProps) => {
|
||||
export const VTodo = observer(({ todo }: VTodo.Props) => {
|
||||
|
||||
const updateTodo = trpc.todo.update.useMutation()
|
||||
const removeTodo = trpc.todo.remove.useMutation()
|
||||
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<p>{todo.content}</p>
|
||||
</Card>
|
||||
<Skeleton loading={updateTodo.isLoading || removeTodo.isLoading}>
|
||||
<Card>
|
||||
<Flex
|
||||
justify="between"
|
||||
align="center"
|
||||
>
|
||||
<Text>{todo.content}</Text>
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
gap="1"
|
||||
>
|
||||
<Tooltip content="Move down">
|
||||
<IconButton
|
||||
variant="outline"
|
||||
|
||||
onClick={() => updateTodo.mutate(encodeTodo(
|
||||
new Todo({
|
||||
...todo,
|
||||
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 { createWSClient, wsLink } from "@trpc/client"
|
||||
import { ReactNode, useState } from "react"
|
||||
import { createWSClient, httpBatchLink, splitLink, wsLink } from "@trpc/client"
|
||||
import { type ReactNode, useMemo, useState } from "react"
|
||||
import { trpc } from "./trpc"
|
||||
|
||||
|
||||
export interface TRPCClientProviderProps {
|
||||
children?: ReactNode
|
||||
export module TRPCClientProvider {
|
||||
export interface Props {
|
||||
children?: ReactNode
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function TRPCClientProvider({ children }: TRPCClientProviderProps) {
|
||||
export function TRPCClientProvider({ children }: TRPCClientProvider.Props) {
|
||||
|
||||
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: [
|
||||
// httpBatchLink({
|
||||
// url: "http://localhost:8080/rpc",
|
||||
// headers: async () => ({}),
|
||||
// }),
|
||||
splitLink({
|
||||
condition: op => op.type === "subscription",
|
||||
|
||||
wsLink({ client: wsClient }),
|
||||
true: wsLink({ client: wsClient }),
|
||||
|
||||
false: httpBatchLink({
|
||||
url: "/rpc",
|
||||
headers: {},
|
||||
}),
|
||||
})
|
||||
]
|
||||
}))
|
||||
}), [wsClient])
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
|
||||
@@ -5,8 +5,13 @@ import { defineConfig } from "vite"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: true,
|
||||
port: 80,
|
||||
},
|
||||
|
||||
plugins: [
|
||||
TanStackRouterVite(),
|
||||
react(),
|
||||
]
|
||||
],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user