feat(sdk): Effect v4 + @effect/openapi-generator; typed pf.api & example ladder

Drop Orval for the first-party @effect/openapi-generator (OpenAPI 3.1 ->
Effect Schema + a typed HttpClient client) and bump effect 3.19 ->
4.0.0-beta.98. Port the hand-written surfaces to the v4 API (Result over
Either, Context.Service, Codec, Literals/Union arrays, Stream/Schedule/
Effect renames). Transport (CA-pinning fetch) and the reconnecting SSE
source are kept intact.

Make the SDK approachable for non-Effect users:
- Add pf.api.* on the Promise facade: the generated client surfaced as
  typed, Promise-native methods (await pf.api.listPairedClients()), so REST
  calls are autocompleted and checked instead of stringly-typed
  pf.request(method, path, body) + `as` casts. Zero-drift veneer over
  make(httpClient), backed by the same pinning fetch. pf.request stays as
  the untyped escape hatch.
- Re-tier examples into a 1-4 complexity ladder, rewritten onto pf.api.*
  (the typed payloads caught a wrong `launch` shape in provider-sync);
  the Effect example is labelled advanced. Add examples/ to tsconfig so
  they are typechecked (stops rot).

typecheck + 19 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 12:40:04 +02:00
parent 27a5d8daac
commit f012ebbcba
19 changed files with 1645 additions and 1974 deletions
+27 -4
View File
@@ -6,7 +6,8 @@ stream start/stop, pairing, displays, library) — built on [Effect](https://eff
Two surfaces, one core:
- **`@punktfunk/host`** — the Promise facade, the front door. `connect()`, `await`, `.on()`.
- **`@punktfunk/host`** — the Promise facade, the front door. `connect()`, then `pf.api.*` (the
typed management API — every endpoint autocompletes, every response is typed) and `pf.events.on()`.
You never need to know Effect exists.
- **`@punktfunk/host/effect`** — the Effect-native surface for plugins and composed programs:
the `PunktfunkHost` service + layer, `Stream`-based events, typed errors
@@ -21,15 +22,25 @@ import { connect } from "@punktfunk/host";
const pf = await connect(); // zero config on the host box
// Typed API — autocomplete every endpoint, typed responses, no hand-written paths or casts.
const clients = await pf.api.listPairedClients();
console.log(`${clients.length} paired clients`);
// Live events:
pf.events.on("stream.started", (e) => {
console.log(`${e.stream.client} started ${e.stream.mode}${e.stream.hdr ? " HDR" : ""}`);
});
pf.events.on("pairing.pending", async (e) => {
// notify your phone, then decide through the API:
// await pf.request("POST", `/native/pending/${id}/approve`);
// notify your phone, then decide through the typed API:
const pending = await pf.api.listPendingDevices();
const match = pending.find((d) => d.fingerprint === e.device.fingerprint);
if (match) await pf.api.approvePendingDevice(String(match.id), { payload: {} });
});
```
Need something the generated client doesn't cover? `pf.request(method, path, body)` is the untyped
escape hatch (returns `unknown`).
The same, Effect-native:
```ts
@@ -43,6 +54,18 @@ const program = events().pipe(
Effect.runPromise(program.pipe(Effect.provide(PunktfunkHostLive())));
```
## Examples
A complexity ladder in [`examples/`](./examples) — start at the top:
1. [`tail-events.ts`](./examples/tail-events.ts) — **hello world**: connect, one typed call, tail events.
2. [`notify-pairing.ts`](./examples/notify-pairing.ts) — **event → decision**: approve/deny pairing through the typed API.
3. [`provider-sync.ts`](./examples/provider-sync.ts) — **typed bulk REST**: declaratively reconcile a game-library provider.
4. [`couch-preset.effect.ts`](./examples/couch-preset.effect.ts) — **advanced, Effect-native**: only if you're composing Effect programs.
Examples 13 are the plain Promise facade and cover most automation; you only need example 4's
Effect surface for composed, interruptible programs. Run any with `bun examples/<file>.ts`.
## Connection resolution
`connect()` / `PunktfunkHostLive()` resolve, in order:
@@ -166,7 +189,7 @@ Windows Task Scheduler: a task triggered *At log on* running
```sh
bun install
bun run gen # regenerate src/gen/schemas.ts from ../api/openapi.json
bun run gen # regenerate src/gen/punktfunk.ts from ../api/openapi.json (@effect/openapi-generator)
bun run typecheck
bun test
```