API surface explorations #25

Merged
decentral1se merged 2 commits from api-surface-explorations into main 2026-11-08 16:13:24 +00:00
36 changed files with 368 additions and 3 deletions
+1
View File
@@ -14,4 +14,5 @@ examples/http-dialer/http-dialer
examples/pairchat/pairchat
examples/simple-pipe/simple-pipe
examples/timeserve/timeserve
examples/timeserve-close/timeserve
iroh-go/target/
+12
View File
@@ -0,0 +1,12 @@
# Contributing
Please read the [README](./README.md) before diving in. Please [raise an issue
before starting on large
changes](https://git.coopcloud.tech/decentral1se/iroh-go/issues/new).
## Conventions
* API wrappers (such as `endpoint.go`) should return upstream errors even
though it would be more convenient to use `iroh.As` internally. This is to
maintain consistency in error handling across the entire API boundary
(bindings or custom). The API consumer uses `iroh.As`, not the producer.
+4
View File
@@ -115,6 +115,10 @@ in [`libs`](./libs).
Apache 2.0 & MIT, following the upstream licensing choices in
[`iroh-ffi`](https://github.com/n0-computer/iroh-ffi).
## Contributing
[`CONTRIBUTING.md`](./CONTRIBUTING.md).
## LLM Policy
We adopt the [Strict No LLM / No AI
+13
View File
@@ -0,0 +1,13 @@
package iroh_ffi
type Alpn []byte
func Alpns(alpns ...Alpn) [][]byte {
var a [][]byte
for _, alpn := range alpns {
a = append(a, []byte(alpn))
}
return a
}
+55
View File
@@ -0,0 +1,55 @@
package iroh_ffi
import (
"context"
"golang.org/x/sync/errgroup"
)
func (e *Endpoint) Accept(ctx context.Context, a Accepter) error {
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
for {
incomingConn := *e.AcceptNext()
if incomingConn == nil {
return nil
}
acceptedConn, err := incomingConn.Accept()
if err != nil {
return err
}
conn, err := acceptedConn.Connect()
if err != nil {
return err
}
Log.Debug("incoming connection established")
if err := a.Accept(gCtx, conn); err != nil {
return err
}
return nil
}
})
return g.Wait()
}
func (e *Endpoint) Open(ctx context.Context, addr *EndpointAddr, alpn Alpn, o Opener) error {
conn, err := e.Connect(addr, alpn)
if err != nil {
return err
}
Log.Debug("remote connection established")
if err := o.Open(ctx, conn); err != nil {
return err
}
return nil
}
+78
View File
@@ -0,0 +1,78 @@
package endpoint
import (
"errors"
"log/slog"
"os"
iroh "git.coopcloud.tech/decentral1se/iroh-go"
)
type Option func(*iroh.EndpointBuilder) error
func New(options ...Option) (*iroh.Endpoint, error) {
decentral1se marked this conversation as resolved Outdated
Outdated
Review

since this is following the "options" pattern, i think it's better here if you do endpointBuilder.ApplyMinimal. a later option can then set a more maximal preset. the reason is some situations i do not want to use N0's defaults. this is not just a preference for my own projects but important in other situations, like testing strictly local connectivity :)

since this is following the "options" pattern, i think it's better here if you do [`endpointBuilder.ApplyMinimal`](https://godocs.io/git.coopcloud.tech/decentral1se/iroh-go#EndpointBuilder.ApplyMinimal). a later option can then set a more maximal preset. the reason is some situations i do not want to use N0's defaults. this is not just a preference for my own projects but important in other situations, like testing strictly local connectivity :)
var err error
endpointBuilder := iroh.NewEndpointBuilder()
for _, opt := range options {
if optErr := opt(endpointBuilder); optErr != nil {
decentral1se marked this conversation as resolved Outdated
Outdated
Review

beware beware, this endpoint.Online bit can, depending on relay connectivity (I THINK) hang indefinitely

beware beware, this `endpoint.Online` bit can, depending on relay connectivity (I THINK) hang indefinitely
Outdated
Review

some more thinking, maybe the online wait could be an option?

func WithUntilOnline(){}
some more thinking, maybe the online wait could be an option? ```golang func WithUntilOnline(){} ```
Outdated
Review

I think I'll have to leave this to the option API consumer as the options that get passed in are for constructing the endpoint, not running anything on top of it. I guess it gives more control since we don't really know what is coming back out of that function call 😆

I think I'll have to leave this to the option API consumer as the options that get passed in are for constructing the endpoint, not running anything on top of it. I guess it gives more control since we don't really know what is coming back out of that function call 😆
err = errors.Join(err, optErr)
}
}
// TODO(d1): implement DNS customisation when it lands
// https://docs.iroh.computer/connecting/dns-address-lookup#use-your-own-server
// https://github.com/n0-computer/iroh-ffi/issues/289
endpoint, err := endpointBuilder.Bind()
if bindErr := iroh.As(err); err != nil {
err = errors.Join(err, bindErr)
}
return endpoint, err
}
func WithAlpns(alpn ...iroh.Alpn) Option {
return func(endpointBuilder *iroh.EndpointBuilder) error {
endpointBuilder.Alpns(iroh.Alpns(alpn...))
return nil
}
}
func WithApplyN0() Option {
return func(endpointBuilder *iroh.EndpointBuilder) error {
endpointBuilder.ApplyN0()
return nil
}
}
func WithApplyN0DisableRelay() Option {
return func(endpointBuilder *iroh.EndpointBuilder) error {
endpointBuilder.ApplyN0DisableRelay()
return nil
}
}
func WithApplyMinimal() Option {
return func(endpointBuilder *iroh.EndpointBuilder) error {
endpointBuilder.ApplyMinimal()
return nil
}
}
func WithRelayMode(rMode *iroh.RelayMode) Option {
return func(endpointBuilder *iroh.EndpointBuilder) error {
endpointBuilder.RelayMode(rMode)
return nil
}
}
func WithDebug() Option {
return func(endpointBuilder *iroh.EndpointBuilder) error {
lvl := new(slog.LevelVar)
lvl.Set(slog.LevelDebug)
iroh.Log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl}))
return nil
}
}
+2
View File
@@ -4,4 +4,6 @@ go 1.26.1
require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25
require golang.org/x/sync v0.24.0 // indirect
replace git.coopcloud.tech/decentral1se/iroh-go => ../../
+2
View File
@@ -0,0 +1,2 @@
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
+1
View File
@@ -33,6 +33,7 @@ require (
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.24.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
)
+2
View File
@@ -64,6 +64,8 @@ golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+2
View File
@@ -4,4 +4,6 @@ go 1.26.1
require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25
require golang.org/x/sync v0.24.0 // indirect
replace git.coopcloud.tech/decentral1se/iroh-go => ../../
+2
View File
@@ -0,0 +1,2 @@
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
+4 -1
View File
@@ -4,6 +4,9 @@ go 1.26.1
require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25
require gomod.cblgh.org/cerca v0.3.3 // indirect
require (
golang.org/x/sync v0.24.0 // indirect
gomod.cblgh.org/cerca v0.3.3
)
replace git.coopcloud.tech/decentral1se/iroh-go => ../../
+2
View File
@@ -1,2 +1,4 @@
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
gomod.cblgh.org/cerca v0.3.3 h1:NaDO+J/cHn5nIsmLhjBUfWb/4RkNg6Dn3frrbX7TvHc=
gomod.cblgh.org/cerca v0.3.3/go.mod h1:ZLFyHtI0Dd9GohYRn2Y+HpY3vpHQHa21efOTeSXmWvo=
+4 -1
View File
@@ -4,6 +4,9 @@ go 1.26.1
require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25
require gomod.cblgh.org/cerca v0.3.3 // indirect
require (
golang.org/x/sync v0.24.0 // indirect
gomod.cblgh.org/cerca v0.3.3
)
replace git.coopcloud.tech/decentral1se/iroh-go => ../../
+2
View File
@@ -1,2 +1,4 @@
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
gomod.cblgh.org/cerca v0.3.3 h1:NaDO+J/cHn5nIsmLhjBUfWb/4RkNg6Dn3frrbX7TvHc=
gomod.cblgh.org/cerca v0.3.3/go.mod h1:ZLFyHtI0Dd9GohYRn2Y+HpY3vpHQHa21efOTeSXmWvo=
+2
View File
@@ -4,4 +4,6 @@ go 1.26.1
require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25
require golang.org/x/sync v0.24.0 // indirect
replace git.coopcloud.tech/decentral1se/iroh-go => ../../
+2
View File
@@ -0,0 +1,2 @@
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
+4 -1
View File
@@ -4,6 +4,9 @@ go 1.26.1
require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25
require gomod.cblgh.org/cerca v0.3.3 // indirect
require (
golang.org/x/sync v0.24.0 // indirect
gomod.cblgh.org/cerca v0.3.3
)
replace git.coopcloud.tech/decentral1se/iroh-go => ../../
+2
View File
@@ -1,2 +1,4 @@
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
gomod.cblgh.org/cerca v0.3.3 h1:NaDO+J/cHn5nIsmLhjBUfWb/4RkNg6Dn3frrbX7TvHc=
gomod.cblgh.org/cerca v0.3.3/go.mod h1:ZLFyHtI0Dd9GohYRn2Y+HpY3vpHQHa21efOTeSXmWvo=
+2
View File
@@ -4,4 +4,6 @@ go 1.26.1
require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25
require golang.org/x/sync v0.24.0 // indirect
replace git.coopcloud.tech/decentral1se/iroh-go => ../../
+2
View File
@@ -0,0 +1,2 @@
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
+2
View File
@@ -4,4 +4,6 @@ go 1.26.1
require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25
require golang.org/x/sync v0.24.0 // indirect
replace git.coopcloud.tech/decentral1se/iroh-go => ../../
+2
View File
@@ -0,0 +1,2 @@
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
+3
View File
@@ -0,0 +1,3 @@
# timeserve-close
> **WIP**
+9
View File
@@ -0,0 +1,9 @@
module git.coopcloud.tech/decentral1se/iroh-go/examples/timeserve
go 1.26.1
require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25
require golang.org/x/sync v0.24.0 // indirect
replace git.coopcloud.tech/decentral1se/iroh-go => ../../
+2
View File
@@ -0,0 +1,2 @@
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
+2
View File
@@ -0,0 +1,2 @@
default:
@go build -v ./...
+108
View File
@@ -0,0 +1,108 @@
package main
import (
"context"
"flag"
"fmt"
"time"
iroh "git.coopcloud.tech/decentral1se/iroh-go"
"git.coopcloud.tech/decentral1se/iroh-go/endpoint"
)
var (
alpn = iroh.Alpn("iroh-go-timeserve/0")
frameSize uint32 = 256
endpointPeer *string
numMessages = 5
)
type Timeserve struct{}
func (t Timeserve) Open(ctx context.Context, conn *iroh.Connection) error {
stream, err := conn.OpenBi()
if err := iroh.As(err); err != nil {
return err
}
err = stream.Ready()
if err := iroh.As(err); err != nil {
panic(err)
}
recv := stream.Recv()
for range numMessages {
frame, err := recv.Read(frameSize)
if err := iroh.As(err); err != nil {
return err
}
fmt.Printf("recv: %s: %s\n", recv.Id(), string(frame))
}
return nil
}
func (t Timeserve) Accept(ctx context.Context, conn *iroh.Connection) error {
stream, err := conn.AcceptBi()
if err := iroh.As(err); err != nil {
return err
}
send := stream.Send()
for range numMessages {
now := time.Now().Format(time.DateTime)
fmt.Printf("send: %s: %s\n", send.Id(), now)
err := send.WriteAll([]byte(now))
if err := iroh.As(err); err != nil {
return err
}
<-time.After(1 * time.Second)
}
return nil
}
func main() {
endpointPeer = flag.String("endpoint", "", "endpoint id of peer")
flag.Parse()
endpoint, err := endpoint.New(
endpoint.WithAlpns(alpn),
endpoint.WithApplyN0(),
endpoint.WithDebug(),
)
if err := iroh.As(err); err != nil {
panic(err)
}
endpoint.Online()
timeserve := Timeserve{}
ctx := context.Background()
if *endpointPeer == "" {
fmt.Println("endpoint online! use id to connect peer:")
fmt.Printf("timeserve -endpoint %s\n", endpoint.Id())
err := endpoint.Accept(ctx, timeserve)
if err := iroh.As(err); err != nil {
panic(err)
}
return
}
remote, err := iroh.EndpointIdFromString(*endpointPeer)
if err := iroh.As(err); err != nil {
panic(err)
}
addr := iroh.NewEndpointAddr(remote, nil, nil)
err = endpoint.Open(ctx, addr, alpn, timeserve)
if err := iroh.As(err); err != nil {
panic(err)
}
}
+2
View File
@@ -4,4 +4,6 @@ go 1.26.1
require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25
require golang.org/x/sync v0.24.0 // indirect
replace git.coopcloud.tech/decentral1se/iroh-go => ../../
+2
View File
@@ -0,0 +1,2 @@
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
+2
View File
@@ -1,3 +1,5 @@
module git.coopcloud.tech/decentral1se/iroh-go
go 1.26.1
require golang.org/x/sync v0.24.0
+2
View File
@@ -0,0 +1,2 @@
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
+16
View File
@@ -0,0 +1,16 @@
package iroh_ffi
import "context"
type Accepter interface {
Accept(context.Context, *Connection) error
}
type Opener interface {
Open(context.Context, *Connection) error
}
type AcceptOpener interface {
Accepter
Opener
}
+8
View File
@@ -0,0 +1,8 @@
package iroh_ffi
import (
"log/slog"
"os"
)
var Log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{}))
+8
View File
@@ -0,0 +1,8 @@
package iroh_ffi
func (s *BiStream) Ready() error {
if err := s.Send().WriteAll([]byte{1}); err != nil {
return err
}
return nil
}