diff --git a/bun.lock b/bun.lock index 399f4cc..1fd9b37 100644 --- a/bun.lock +++ b/bun.lock @@ -30,6 +30,20 @@ "effect": "^3.19.0", }, }, + "packages/example": { + "name": "@effect-docker/example", + "dependencies": { + "@effect/platform": "^0.94.4", + "@effect/platform-bun": "^0.87.1", + "@effect/platform-node": "^0.104.1", + "effect": "^3.19.16", + "effect-docker": "workspace:*", + "undici": "^7.19.0", + }, + "devDependencies": { + "tsx": "^4.21.0", + }, + }, }, "packages": { "@biomejs/biome": ["@biomejs/biome@2.3.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.11", "@biomejs/cli-darwin-x64": "2.3.11", "@biomejs/cli-linux-arm64": "2.3.11", "@biomejs/cli-linux-arm64-musl": "2.3.11", "@biomejs/cli-linux-x64": "2.3.11", "@biomejs/cli-linux-x64-musl": "2.3.11", "@biomejs/cli-win32-arm64": "2.3.11", "@biomejs/cli-win32-x64": "2.3.11" }, "bin": { "biome": "bin/biome" } }, "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ=="], @@ -50,13 +64,15 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="], + "@effect-docker/example": ["@effect-docker/example@workspace:packages/example"], + "@effect/cluster": ["@effect/cluster@0.56.1", "", { "dependencies": { "kubernetes-types": "^1.30.0" }, "peerDependencies": { "@effect/platform": "^0.94.1", "@effect/rpc": "^0.73.0", "@effect/sql": "^0.49.0", "@effect/workflow": "^0.16.0", "effect": "^3.19.14" } }, "sha512-gnrsH6kfrUjn+82j/bw1IR4yFqJqV8tc7xZvrbJPRgzANycc6K1hu3LMg548uYbUkTzD8YYyqrSatMO1mkQpzw=="], "@effect/experimental": ["@effect/experimental@0.58.0", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.94.0", "effect": "^3.19.13", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-IEP9sapjF6rFy5TkoqDPc86st/fnqUfjT7Xa3pWJrFGr1hzaMXHo+mWsYOZS9LAOVKnpHuVziDK97EP5qsCHVA=="], "@effect/language-service": ["@effect/language-service@0.72.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-MWkyTPCXSs5Q3OIBWR3q24SA+ipkdWW7EBJBt6EPUzlzZxjJLXtLBhXpMoCFheSEM0FTWOHT4BRLh5lufsmjVw=="], - "@effect/platform": ["@effect/platform@0.94.2", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.15" } }, "sha512-85vdwpnK4oH/rJ3EuX/Gi2Hkt+K4HvXWr9bxCuqvty9hxyEcRxkJcqTesYrcVoQB6aULb1Za2B0MKoTbvffB3Q=="], + "@effect/platform": ["@effect/platform@0.94.4", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.16" } }, "sha512-mK8pbskFAcBRA5Ooyt02kCBuWltZakyaDcM4ByTY0jXQMFC3NUveNK62JVH7XB+f3XZ8OoBBxUnlLben4plEKQ=="], "@effect/platform-bun": ["@effect/platform-bun@0.87.1", "", { "dependencies": { "@effect/platform-node-shared": "^0.57.1", "multipasta": "^0.2.7" }, "peerDependencies": { "@effect/cluster": "^0.56.1", "@effect/platform": "^0.94.2", "@effect/rpc": "^0.73.0", "@effect/sql": "^0.49.0", "effect": "^3.19.15" } }, "sha512-I88d0YqWbvLY2GGeIxK3r5k0l/MoUCCnxiHJG+X6gqaHu+pIs0djDtJ+ORhw/3qha9ojcVu6pyaBmnUjgzQHWQ=="], @@ -194,7 +210,7 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "effect": ["effect@3.19.15", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-vzMmgfZKLcojmUjBdlQx+uaKryO7yULlRxjpDnHdnvcp1NPHxJyoM6IOXBLlzz2I/uPtZpGKavt5hBv7IvGZkA=="], + "effect": ["effect@3.19.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-7+XC3vGrbAhCHd8LTFHvnZjRpZKZ8YHRZqJTkpNoxcJ2mCyNs2SwI+6VkV/ij8Y3YW7wfBN4EbU06/F5+m/wkQ=="], "effect-docker": ["effect-docker@workspace:packages/effect-docker"], @@ -323,5 +339,7 @@ "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "openapi-to-effect/effect": ["effect@3.19.15", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-vzMmgfZKLcojmUjBdlQx+uaKryO7yULlRxjpDnHdnvcp1NPHxJyoM6IOXBLlzz2I/uPtZpGKavt5hBv7IvGZkA=="], } } diff --git a/packages/effect-docker/scripts/tsconfig.json b/packages/effect-docker/scripts/tsconfig.json new file mode 100644 index 0000000..d09a76e --- /dev/null +++ b/packages/effect-docker/scripts/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["./scripts"] +} diff --git a/packages/effect-docker/src/Docker153Client.ts b/packages/effect-docker/src/Docker153Client.ts index e62d972..bb6d4dd 100644 --- a/packages/effect-docker/src/Docker153Client.ts +++ b/packages/effect-docker/src/Docker153Client.ts @@ -1,6 +1,7 @@ import { HttpClient, HttpClientRequest } from "@effect/platform" +import type { ResponseError } from "@effect/platform/HttpClientError" import { Effect } from "effect" -import { type DockerBaseClient, DockerBaseClientImpl, type DockerClientContainerError } from "./DockerBaseClient.js" +import { type DockerBaseClient, DockerBaseClientImpl, type DockerClientError } from "./DockerBaseClient.js" import type { ContainerSummary } from "./gen/v1.53/index.js" @@ -12,7 +13,18 @@ export type Docker153ClientTypeId = typeof Docker153ClientTypeId export interface Docker153Client extends DockerBaseClient { readonly [Docker153ClientTypeId]: Docker153ClientTypeId - listContainers(): Effect.Effect + listContainers(params?: Docker153Client.listContainers.Params): Effect.Effect +} + +export declare namespace Docker153Client { + export namespace listContainers { + export interface Params { + readonly all?: boolean + readonly limit?: number + readonly size?: boolean + readonly filters?: string + } + } } export class Docker153ClientImpl @@ -20,14 +32,21 @@ extends DockerBaseClientImpl implements Docker153Client { readonly [Docker153ClientTypeId]: Docker153ClientTypeId = Docker153ClientTypeId - listContainers() { - return this.httpContainerClient.get("/json").pipe( - + listContainers(params?: Docker153Client.listContainers.Params) { + return HttpClientRequest.get("/containers/json").pipe( + HttpClientRequest.appendUrlParams({ + all: params?.all ?? false, + limit: params?.limit, + size: params?.size ?? false, + filters: params?.filters, + }), + req => this.httpClient.execute(req), + Effect.andThen(res => res.json as Effect.Effect) ) } } export const make: Effect.Effect = HttpClient.HttpClient.pipe( - Effect.map(HttpClient.mapRequest(HttpClientRequest.prependUrl("/v1.53"))), + Effect.map(HttpClient.mapRequest(HttpClientRequest.appendUrl("/v1.53"))), Effect.map(client => new Docker153ClientImpl(client)), ) diff --git a/packages/effect-docker/src/DockerBaseClient.ts b/packages/effect-docker/src/DockerBaseClient.ts index 1d87dd3..019ad61 100644 --- a/packages/effect-docker/src/DockerBaseClient.ts +++ b/packages/effect-docker/src/DockerBaseClient.ts @@ -1,4 +1,4 @@ -import { HttpClient, type HttpClientError, HttpClientRequest } from "@effect/platform" +import { HttpClient, type HttpClientError } from "@effect/platform" import { Effect, Match, Schema } from "effect" @@ -7,16 +7,12 @@ export type DockerBaseClientTypeId = typeof DockerBaseClientTypeId export interface DockerBaseClient { readonly [DockerBaseClientTypeId]: DockerBaseClientTypeId - readonly httpClient: HttpClient.HttpClient.With - readonly httpContainerClient: HttpClient.HttpClient.With } export class DockerBaseClientImpl implements DockerBaseClient { readonly [DockerBaseClientTypeId]: DockerBaseClientTypeId = DockerBaseClientTypeId - readonly httpClient: HttpClient.HttpClient.With - readonly httpContainerClient: HttpClient.HttpClient.With constructor(httpClient: HttpClient.HttpClient) { this.httpClient = httpClient.pipe( @@ -33,18 +29,6 @@ export class DockerBaseClientImpl implements DockerBaseClient { Match.orElse(Effect.fail), ) as Effect.Effect), ) - - this.httpContainerClient = this.httpClient.pipe( - HttpClient.mapRequest(HttpClientRequest.appendUrl("/containers")), - HttpClient.filterStatus(status => status !== 404), - HttpClient.catchTag("ResponseError", e => Match.value(e.response.status).pipe( - Match.when(404, () => Effect.andThen( - e.response.json as Effect.Effect, - ({ message }) => new NoSuchContainer({ message }, true), - )), - Match.orElse(Effect.fail), - ) as Effect.Effect), - ) } } @@ -66,5 +50,3 @@ export type DockerClientError = HttpClientError.HttpClientError | BadParameter | export class NoSuchContainer extends Schema.TaggedError("@effect-docker/DockerBaseClient/NoSuchContainer")("NoSuchContainer", { message: Schema.String, }) {} - -export type DockerClientContainerError = DockerClientError | NoSuchContainer diff --git a/packages/effect-docker/src/index.ts b/packages/effect-docker/src/index.ts index 45c8a10..e0e594a 100644 --- a/packages/effect-docker/src/index.ts +++ b/packages/effect-docker/src/index.ts @@ -1,40 +1,2 @@ -import net from "node:net" -import { HttpClient } from "@effect/platform" -import { NodeHttpClient, NodeRuntime } from "@effect/platform-node" -import { Effect, Layer } from "effect" -import { Agent } from "undici" - - -const DockerNodeSocketAgent = Effect.acquireUseRelease( - Effect.sync(() => { - console.log("acquire") - return net.createConnection({ path: "/var/run/docker.sock" }) - }), - socket => Effect.sync(() => { - socket.on("connect", console.log) - socket.on("data", console.log) - return socket - }), - socket => Effect.sync(() => { - console.log("release") - socket.end() - }), -).pipe( - Effect.andThen(socket => NodeHttpClient.makeAgentLayer({ socket })), - Layer.unwrapScoped, -) -const DockerNodeHttpClient = Layer.provide(NodeHttpClient.layerWithoutAgent, DockerNodeSocketAgent) - -const DockerUndiciDispatcher = Layer.succeed(NodeHttpClient.Dispatcher, new Agent({ - socketPath: "/var/run/docker.sock" -})) -const DockerUndiciHttpClient = Layer.provide(NodeHttpClient.layerUndiciWithoutDispatcher, DockerUndiciDispatcher) - -Effect.gen(function*() { - const client = yield* HttpClient.HttpClient - const response = yield* client.get("http://localhost/images/json") - console.log(yield* response.json) -}).pipe( - Effect.provide(DockerUndiciHttpClient), - NodeRuntime.runMain, -) +export * as Docker153Client from "./Docker153Client.js" +export * as DockerBaseClient from "./DockerBaseClient.js" diff --git a/packages/effect-docker/src/test.ts b/packages/effect-docker/src/test.ts deleted file mode 100644 index 8dd9d3e..0000000 --- a/packages/effect-docker/src/test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { HttpClient } from "@effect/platform" -import { NodeHttpClient, NodeRuntime } from "@effect/platform-node" -import { Effect, Schema } from "effect" - - -export class NotFound extends Schema.TaggedError("NotFound")("NotFound", { - message: Schema.String, -}) {} - -const program = Effect.gen(function*() { - const client = yield* HttpClient.HttpClient.pipe( - Effect.map(HttpClient.filterStatus(status => status !== 404)), - Effect.map(HttpClient.catchTag("ResponseError", e => - e.response.status === 404 - ? Effect.andThen(e.response.text, message => Effect.fail(new NotFound({ message: "" }))) - : Effect.fail(e) - )), - ) - const res = yield* client.get("https://docs.docker.com/adolf") - console.log(res.status) -// console.log(yield* res.text) - -}).pipe(Effect.withSpan("program", { - attributes: { source: "Playground" } -})) - -program.pipe( - Effect.provide(NodeHttpClient.layer), - NodeRuntime.runMain -) diff --git a/packages/effect-docker/tsconfig.json b/packages/effect-docker/tsconfig.json index 2d1d821..3a9e67c 100644 --- a/packages/effect-docker/tsconfig.json +++ b/packages/effect-docker/tsconfig.json @@ -34,5 +34,5 @@ ] }, - "include": ["./src", "./scripts"] + "include": ["./src"] } diff --git a/packages/example/README.md b/packages/example/README.md new file mode 100644 index 0000000..b3c7ecd --- /dev/null +++ b/packages/example/README.md @@ -0,0 +1,58 @@ +# Effect FC + +[Effect-TS](https://effect.website/) integration for React 19.2+ that allows you to write function components using Effect generators. + +This library is in early development. While it is (almost) feature complete and mostly usable, expect bugs and quirks. Things are still being ironed out, so ideas and criticisms are more than welcome. + +Documentation is currently being written. In the meantime, you can take a look at the `packages/example` directory. + +## Peer dependencies +- `effect` 3.19+ +- `react` & `@types/react` 19.2+ + +## Known issues +- React Refresh doesn't work for Effect FC's yet. Page reload is required to view changes. Regular React components are unaffected. + +## What writing components looks like +```typescript +export class Todos extends Component.make("Todos")(function*() { + const state = yield* TodosState + const [todos] = yield* useSubscribables(state.ref) + + yield* useOnMount(() => Effect.andThen( + Console.log("Todos mounted"), + Effect.addFinalizer(() => Console.log("Todos unmounted")), + )) + + const TodoFC = yield* Todo + + return ( + + Todos + + + + + {Chunk.map(todos, todo => + + )} + + + ) +}) {} + +const TodosStateLive = TodosState.Default("todos") + +const Index = Component.make("Index")(function*() { + const context = yield* useContext(TodosStateLive) + const TodosFC = yield* Effect.provide(Todos, context) + + return +}).pipe( + Component.withRuntime(runtime.context) +) + +export const Route = createFileRoute("/")({ + component: Index +}) +``` diff --git a/packages/example/biome.json b/packages/example/biome.json new file mode 100644 index 0000000..41d707b --- /dev/null +++ b/packages/example/biome.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://biomejs.dev/schemas/latest/schema.json", + "root": false, + "extends": "//", + "files": { + "includes": ["./src/**"] + } +} diff --git a/packages/example/package.json b/packages/example/package.json new file mode 100644 index 0000000..c07cb50 --- /dev/null +++ b/packages/example/package.json @@ -0,0 +1,23 @@ +{ + "name": "@effect-docker/example", + "type": "module", + "license": "MIT", + "scripts": { + "lint:tsc": "tsc --noEmit", + "lint:biome": "biome lint", + "clean:cache": "rm -rf .turbo", + "clean:dist": "rm -rf dist", + "clean:modules": "rm -rf node_modules" + }, + "devDependencies": { + "tsx": "^4.21.0" + }, + "dependencies": { + "@effect/platform": "^0.94.4", + "@effect/platform-bun": "^0.87.1", + "@effect/platform-node": "^0.104.1", + "effect": "^3.19.16", + "effect-docker": "workspace:*", + "undici": "^7.19.0" + } +} diff --git a/packages/example/src/socket.ts b/packages/example/src/socket.ts new file mode 100644 index 0000000..f7aec2e --- /dev/null +++ b/packages/example/src/socket.ts @@ -0,0 +1,35 @@ +import { HttpClient, HttpClientRequest } from "@effect/platform" +import { NodeHttpClient, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer } from "effect" +import { Docker153Client } from "effect-docker" +import { Agent } from "undici" + + +const DockerSocketHttpClient = Layer.effect( + HttpClient.HttpClient, + Effect.map(HttpClient.HttpClient, HttpClient.mapRequest(HttpClientRequest.prependUrl("http://localhost"))), +).pipe( + Layer.provide(NodeHttpClient.layerUndiciWithoutDispatcher), + Layer.provide( + Layer.succeed(NodeHttpClient.Dispatcher, new Agent({ + socketPath: "/var/run/docker.sock" + })) + ), +) + + +export class DockerClient extends Effect.Service()("@effect-docker/example/socket/DockerClient", { + effect: Docker153Client.make, + dependencies: [DockerSocketHttpClient], +}) {} + +const withService = Effect.gen(function*() { + const docker = yield* DockerClient + const containers = yield* docker.listContainers({ all: true }) + console.log(containers) +}) + +withService.pipe( + Effect.provide(DockerClient.Default), + NodeRuntime.runMain, +) diff --git a/packages/example/tsconfig.json b/packages/example/tsconfig.json new file mode 100644 index 0000000..3a9e67c --- /dev/null +++ b/packages/example/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "NodeNext", + "moduleDetection": "force", + "jsx": "react-jsx", + // "allowJs": true, + + // Bundler mode + "moduleResolution": "NodeNext", + // "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + // "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + + // Build + "outDir": "./dist", + "declaration": true, + "sourceMap": true, + + "plugins": [ + { "name": "@effect/language-service" } + ] + }, + + "include": ["./src"] +}