Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/client_go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Client (Go)

on:
push:
branches: [main]
paths:
- "client_go/**"
- ".github/workflows/client_go.yml"
pull_request:
branches: [main]
paths:
- "client_go/**"
- ".github/workflows/client_go.yml"

jobs:
test:
name: Test
runs-on: ubuntu-latest

defaults:
run:
working-directory: client_go

steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: client_go/go.mod
cache-dependency-path: client_go/go.sum

- name: Build
run: go build ./...

- name: Vet
run: go vet ./...

- name: Test
run: go test -race ./...
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@
<a href="https://hex.pm/packages/topical"><img src="https://img.shields.io/hexpm/v/topical.svg?color=6e4a7e" /></a>
<a href="https://www.npmjs.com/package/@topical/core"><img src="https://img.shields.io/npm/v/@topical/core.svg?color=3178c6" /></a>
<a href="https://www.npmjs.com/package/@topical/react"><img src="https://img.shields.io/npm/v/@topical/react.svg?color=087ea4" /></a>
<a href="https://pkg.go.dev/github.com/joefreeman/topical/client_go"><img src="https://pkg.go.dev/badge/github.com/joefreeman/topical/client_go.svg" /></a>
</p>

<br />

Topical is an Elixir library for synchronising server-maintained state (_topics_) to connected clients. Topic lifecycle is managed by the server: topics are initialised as needed, shared between subscribing clients, and automatically shut down when not in use.

The accompanying JavaScript library (and React hooks) allow clients to easily connect to topics, and efficiently receive real-time updates. Clients can also send requests (or notifications) upstream to the server.
The accompanying JavaScript library (and React hooks) and Go client allow clients to easily connect to topics, and efficiently receive real-time updates. Clients can also send requests (or notifications) upstream to the server.

<p align="center">
<img src="architecture.png" width="400" alt="Architecture diagram" />
Expand Down Expand Up @@ -137,6 +138,7 @@ This repository is separated into:
- [`server_ex`](server_ex/) - the Elixir library for implementing topic servers, including adapters.
- [`client_js`](client_js/) - the vanilla JavaScript WebSocket client.
- [`client_react`](client_react/) - React hooks built on top of the JavaScript client.
- [`client_go`](client_go/) - Go WebSocket client.

## License

Expand Down
115 changes: 115 additions & 0 deletions client_go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Topical → Client (Go)

A Go client for [Topical](https://github.com/joefreeman/topical), a real-time state synchronization library. Connects to a Topical server over WebSocket and keeps local state in sync.

## Install

```
go get github.com/joefreeman/topical/client_go
```

## Usage

### Connecting

```go
ctx := context.Background()
client, err := topical.Connect(ctx, "ws://localhost:4000/socket")
if err != nil {
log.Fatal(err)
}
defer client.Close()
```

By default the client reconnects automatically with exponential backoff. This can be configured:

```go
client, err := topical.Connect(ctx, url,
topical.WithReconnect(false),
topical.WithBackoff(1*time.Second, 60*time.Second),
)
```

### Subscribing to topics

Subscribe returns a `*Subscription` with channels for receiving values and errors. Multiple subscriptions to the same topic share a single server-side subscription.

```go
sub := client.Subscribe("lists/my-list", nil)
defer sub.Unsubscribe()

for val := range sub.Values() {
fmt.Println("new value:", val)
}
```

Topics can take parameters:

```go
sub := client.Subscribe("lists/my-list", topical.Params{"user_id": "123"})
```

### Typed subscriptions

Use the generic `Subscribe` function to automatically unmarshal values into a struct:

```go
type TodoList struct {
Items map[string]Item `json:"items"`
Order []string `json:"order"`
}

sub := topical.Subscribe[TodoList](client, "lists/my-list", nil)
defer sub.Unsubscribe()

for list := range sub.Values() {
fmt.Printf("got %d items\n", len(list.Items))
}
```

### Execute (RPC)

Send a request and wait for a response. The context controls the timeout:

```go
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

result, err := client.Execute(ctx, "lists/my-list", "add_item", []any{"buy milk"}, nil)
```

### Notify (fire-and-forget)

Send a one-way message with no response:

```go
err := client.Notify("lists/my-list", "mark_done", []any{"item-id"}, nil)
```

### Connection state

```go
fmt.Println(client.State()) // "connected", "connecting", or "disconnected"

stateSub := client.StateChanges()
defer stateSub.Close()

for s := range stateSub.C() {
fmt.Println("state changed:", s)
}
```

### Error handling

Check for subscription errors on the `Err()` channel:

```go
select {
case val := <-sub.Values():
handleValue(val)
case err := <-sub.Err():
handleError(err)
}
```

Operations return `topical.ErrNotConnected` when the client is disconnected.
Loading