Setup example package
Some checks failed
Lint / lint (push) Failing after 40s

This commit is contained in:
Julien Valverdé
2026-02-13 05:09:23 +01:00
parent 2c95e13aab
commit 8d8d73b3d0
12 changed files with 215 additions and 98 deletions

View File

@@ -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=="],
}
}

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"include": ["./scripts"]
}

View File

@@ -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<ContainerSummary, DockerClientContainerError>
listContainers(params?: Docker153Client.listContainers.Params): Effect.Effect<readonly ContainerSummary[], DockerClientError>
}
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<readonly ContainerSummary[], ResponseError, never>)
)
}
}
export const make: Effect.Effect<Docker153Client, never, HttpClient.HttpClient> = 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)),
)

View File

@@ -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<DockerClientError>
readonly httpContainerClient: HttpClient.HttpClient.With<DockerClientContainerError>
}
export class DockerBaseClientImpl implements DockerBaseClient {
readonly [DockerBaseClientTypeId]: DockerBaseClientTypeId = DockerBaseClientTypeId
readonly httpClient: HttpClient.HttpClient.With<DockerClientError>
readonly httpContainerClient: HttpClient.HttpClient.With<DockerClientContainerError>
constructor(httpClient: HttpClient.HttpClient) {
this.httpClient = httpClient.pipe(
@@ -33,18 +29,6 @@ export class DockerBaseClientImpl implements DockerBaseClient {
Match.orElse(Effect.fail),
) as Effect.Effect<never, DockerClientError>),
)
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<HttpErrorResponse>,
({ message }) => new NoSuchContainer({ message }, true),
)),
Match.orElse(Effect.fail),
) as Effect.Effect<never, DockerClientContainerError>),
)
}
}
@@ -66,5 +50,3 @@ export type DockerClientError = HttpClientError.HttpClientError | BadParameter |
export class NoSuchContainer extends Schema.TaggedError<NoSuchContainer>("@effect-docker/DockerBaseClient/NoSuchContainer")("NoSuchContainer", {
message: Schema.String,
}) {}
export type DockerClientContainerError = DockerClientError | NoSuchContainer

View File

@@ -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"

View File

@@ -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")("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
)

View File

@@ -34,5 +34,5 @@
]
},
"include": ["./src", "./scripts"]
"include": ["./src"]
}

View File

@@ -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 (
<Container>
<Heading align="center">Todos</Heading>
<Flex direction="column" align="stretch" gap="2" mt="2">
<TodoFC _tag="new" />
{Chunk.map(todos, todo =>
<TodoFC key={todo.id} _tag="edit" id={todo.id} />
)}
</Flex>
</Container>
)
}) {}
const TodosStateLive = TodosState.Default("todos")
const Index = Component.make("Index")(function*() {
const context = yield* useContext(TodosStateLive)
const TodosFC = yield* Effect.provide(Todos, context)
return <TodosFC />
}).pipe(
Component.withRuntime(runtime.context)
)
export const Route = createFileRoute("/")({
component: Index
})
```

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
"root": false,
"extends": "//",
"files": {
"includes": ["./src/**"]
}
}

View File

@@ -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"
}
}

View File

@@ -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<DockerClient>()("@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,
)

View File

@@ -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"]
}