├── app ├── globals.css ├── favicon.ico ├── layout.tsx └── page.tsx ├── .envrc ├── .eslintrc.json ├── .env ├── database ├── db.sqlite ├── db.sqlite-shm ├── db.sqlite-wal └── migrate.ts ├── .vscode └── settings.json ├── postcss.config.js ├── next.config.js ├── migrations ├── 00002_add_todos.ts └── 00001_create_todos.ts ├── services ├── Sql.ts ├── Runtime.ts ├── Tracing.ts └── TodoRepo.ts ├── actions ├── deleteTodo.tsx ├── updateTodo.tsx └── createTodo.tsx ├── .gitignore ├── tailwind.config.ts ├── public ├── vercel.svg └── next.svg ├── lib ├── otel.ts └── effect.ts ├── components ├── AddTodoForm.tsx └── TodoRow.tsx ├── flake.nix ├── tsconfig.json ├── server.js ├── README.md ├── flake.lock ├── package.json └── pnpm-lock.yaml /app/globals.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake; 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | HONEYCOMB_API_KEY=JbN1lUcQCJB1LPsgd1DfTH 2 | HONEYCOMB_SERVICE_NAME=next-effect -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikearnaldi/next-effect/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /database/db.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikearnaldi/next-effect/HEAD/database/db.sqlite -------------------------------------------------------------------------------- /database/db.sqlite-shm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikearnaldi/next-effect/HEAD/database/db.sqlite-shm -------------------------------------------------------------------------------- /database/db.sqlite-wal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikearnaldi/next-effect/HEAD/database/db.sqlite-wal -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "typescript.tsdk": "node_modules/typescript/lib" 4 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | //output: "standalone", 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /database/migrate.ts: -------------------------------------------------------------------------------- 1 | import { SqlLive } from "@/services/Sql"; 2 | import * as Migrator from "@sqlfx/sqlite/Migrator/Node"; 3 | import { Effect, Layer } from "effect"; 4 | 5 | Effect.log("Migrations Complete").pipe( 6 | Effect.provide( 7 | Migrator.makeLayer({ 8 | loader: Migrator.fromDisk(`${__dirname}/../migrations`), 9 | }).pipe(Layer.provide(SqlLive)) 10 | ), 11 | Effect.runFork 12 | ); 13 | -------------------------------------------------------------------------------- /migrations/00002_add_todos.ts: -------------------------------------------------------------------------------- 1 | import * as Effect from "effect/Effect"; 2 | import * as Sql from "@sqlfx/sqlite/Client"; 3 | 4 | export default Effect.gen(function* ($) { 5 | const sql = yield* $(Sql.tag); 6 | 7 | yield* $(sql`INSERT INTO todos (title) VALUES ('Try Next.js with RSC')`); 8 | yield* $(sql`INSERT INTO todos (title) VALUES ('Integrate Effect')`); 9 | yield* $(sql`INSERT INTO todos (title) VALUES ('Integrate OpenTelemetry')`); 10 | }); 11 | -------------------------------------------------------------------------------- /services/Sql.ts: -------------------------------------------------------------------------------- 1 | import * as Sqlfx from "@sqlfx/sqlite/node"; 2 | import { Config } from "effect"; 3 | 4 | export const Sql = Sqlfx.tag; 5 | 6 | export const SqlLive = Sqlfx.makeLayer({ 7 | filename: Config.succeed( 8 | process.cwd().replace(".next/standalone", "") + "/database/db.sqlite" 9 | ), 10 | transformQueryNames: Config.succeed(Sqlfx.transform.camelToSnake), 11 | transformResultNames: Config.succeed(Sqlfx.transform.snakeToCamel), 12 | }); 13 | -------------------------------------------------------------------------------- /actions/deleteTodo.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { effectAction } from "@/services/Runtime"; 4 | import { TodoRepo } from "@/services/TodoRepo"; 5 | import { Schema } from "@effect/schema"; 6 | import { Effect } from "effect"; 7 | import { revalidatePath } from "next/cache"; 8 | 9 | export const deleteTodo = effectAction(Schema.number)((id) => 10 | Effect.gen(function* ($) { 11 | const todos = yield* $(TodoRepo); 12 | yield* $(todos.deleteTodo(id)); 13 | revalidatePath("/"); 14 | }) 15 | ); 16 | -------------------------------------------------------------------------------- /actions/updateTodo.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { effectAction } from "@/services/Runtime"; 4 | import { TodoRepo, TodoStatus } from "@/services/TodoRepo"; 5 | import { Schema } from "@effect/schema"; 6 | import { Effect } from "effect"; 7 | import { revalidatePath } from "next/cache"; 8 | 9 | export const updateTodo = effectAction( 10 | Schema.number, 11 | TodoStatus 12 | )((id, status) => 13 | Effect.gen(function* ($) { 14 | const todos = yield* $(TodoRepo); 15 | yield* $(todos.updateTodo(id, status)); 16 | revalidatePath("/"); 17 | }) 18 | ); 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | ## direnv 39 | .direnv 40 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /migrations/00001_create_todos.ts: -------------------------------------------------------------------------------- 1 | import * as Effect from "effect/Effect"; 2 | import * as Sql from "@sqlfx/sqlite/Client"; 3 | 4 | export default Effect.gen(function* ($) { 5 | const sql = yield* $(Sql.tag); 6 | 7 | yield* $(sql` 8 | CREATE TABLE todos ( 9 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 10 | title VARCHAR(255) NOT NULL, 11 | status TEXT CHECK( status IN ('COMPLETED','CREATED') ) DEFAULT 'CREATED', 12 | created_at datetime NOT NULL DEFAULT current_timestamp, 13 | updated_at datetime NOT NULL DEFAULT current_timestamp 14 | )`); 15 | }); 16 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | 4 | import "todomvc-common/base.css"; 5 | import "todomvc-app-css/index.css"; 6 | import "./globals.css"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "Create Next App", 12 | description: "Generated by create next app", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 |
{children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /lib/otel.ts: -------------------------------------------------------------------------------- 1 | import Module from "node:module"; 2 | 3 | const require = Module.createRequire(import.meta.url); 4 | 5 | export const { OTLPTraceExporter } = 6 | require("@opentelemetry/exporter-trace-otlp-proto") as typeof import("@opentelemetry/exporter-trace-otlp-proto"); 7 | 8 | export const { OTLPMetricExporter } = 9 | require("@opentelemetry/exporter-metrics-otlp-proto") as typeof import("@opentelemetry/exporter-metrics-otlp-proto"); 10 | 11 | export const { BatchSpanProcessor } = 12 | require("@opentelemetry/sdk-trace-base") as typeof import("@opentelemetry/sdk-trace-base"); 13 | 14 | export const { PeriodicExportingMetricReader } = 15 | require("@opentelemetry/sdk-metrics") as typeof import("@opentelemetry/sdk-metrics"); 16 | -------------------------------------------------------------------------------- /actions/createTodo.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { formData } from "@/lib/effect"; 4 | import { effectAction } from "@/services/Runtime"; 5 | import { TodoRepo } from "@/services/TodoRepo"; 6 | import { Schema } from "@effect/schema"; 7 | import { Effect } from "effect"; 8 | import { revalidatePath } from "next/cache"; 9 | 10 | export const createTodo = effectAction( 11 | Schema.string, 12 | formData(Schema.struct({ title: Schema.string })) 13 | )((_state, { title }) => 14 | Effect.gen(function* ($) { 15 | const todos = yield* $(TodoRepo); 16 | if (title.length === 0) { 17 | return "invalid title"; 18 | } 19 | yield* $(todos.addTodo(title)); 20 | revalidatePath("/"); 21 | return "ok"; 22 | }) 23 | ); 24 | -------------------------------------------------------------------------------- /components/AddTodoForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createTodo } from "@/actions/createTodo"; 4 | import { useEffect, useRef } from "react"; 5 | import { useFormState, useFormStatus } from "react-dom"; 6 | 7 | export const AddTodoForm = () => { 8 | const ref = useRef