Reference Go client for Fitz.
The supported public API is the canonical github.com/cntryl/fitz-go/v2/fitz
package: token-provider auth, Connect/Close, State, and
spec-facing domain verbs.
Import github.com/cntryl/fitz-go/v2/fitz for the public API.
package main
import (
"context"
"time"
"github.com/cntryl/fitz-go/v2/fitz"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client := fitz.NewClient("ws://localhost:4090/ws", func(context.Context) (string, error) {
return "your-jwt-token", nil
})
if err := client.Connect(ctx); err != nil {
panic(err)
}
defer client.Close()
tx, err := client.KV().Begin(ctx, "kv://realm/area/users", fitz.KVDurabilitySync)
if err != nil {
panic(err)
}
defer tx.Rollback(ctx)
if err := tx.Put(ctx, []byte("user-1"), []byte(`{"name":"Alice"}`)); err != nil {
panic(err)
}
value, err := tx.Get(ctx, []byte("user-1"))
if err != nil {
panic(err)
}
if value.Found {
println(string(value.Value))
}
if err := tx.Commit(ctx); err != nil {
panic(err)
}
}Use one control plane for request lifetime: context.Context.
- RPC calls use context deadlines/cancellation only.
- KV/Schedule/Notice/Queue/Lease/Stream subscription handlers return
error. - Streaming iterators should be closed when no longer needed.
- Clients validate route shape locally: scheme, segment count, empty segments, and method-specific wildcard placement. Route existence, permissions, authorization, realm semantics, and resource names remain broker-owned.
Connection lifecycle is part of the stable public API:
Disconnected: beforeConnectand after an unrecoverable disconnectConnecting: transport dial/handshake in progressConnected: transport established, auth not yet settledAuthenticating: CONNECT/auth exchange in progressAuthenticated: ready for domain trafficReconnecting: automatic reconnect loop is actively retryingClosed: client has been closed and will not reconnect
Reconnect guarantees:
- Automatic reconnect is enabled by default after the first successful
Connect;fitz.WithReconnect(false, ..., ...)disables it. fitz.WithReconnect(..., maxAttempts=0)means unlimited reconnect attempts.- KV, notice, stream, lease, queue, and schedule subscriptions are restored after reconnect.
- RPC worker registrations are restored after reconnect.
Close()is idempotent and permanently ends reconnect activity.
Production defaults also include heartbeat (10s interval, 30s timeout),
safe automatic retries for replayable reads, and a bounded outbound request
queue of 1024. See docs/PUBLIC_CONTRACT.md.
The broker-backed test suite verifies those guarantees through a live disconnect proxy rather than by closing one client and creating another.
RPC timeout pattern:
callCtx, cancelCall := context.WithTimeout(ctx, 2*time.Second)
defer cancelCall()
iter, err := client.RPC().Call(callCtx, "rpc://realm/area/echo", []byte("ping"))
if err != nil {
panic(err)
}
defer iter.Close()
for iter.Next() {
_ = iter.Value()
}
if err := iter.Err(); err != nil {
panic(err)
}Schedule subscription handler pattern:
sub, err := client.Schedule().Subscribe(ctx, "schedule://realm/area/resource/run", func(ctx context.Context, n fitz.ScheduleNotification) error {
_ = n
return nil
})
if err != nil {
panic(err)
}
defer sub.Unsubscribe()Stream replay pattern:
filter := fitz.StreamFilterSet{Clauses: []fitz.StreamFilterClause{{Kind: fitz.StreamFilterEquals, Value: "proj.alpha"}}}
records, err := client.Stream().Read(ctx, "stream://realm/area/events", 0, 100, fitz.WithStreamFilter(filter))
if err != nil {
panic(err)
}
defer records.Close()
page, err := client.Stream().ReadPage(ctx, "stream://realm/area/events", 0, 100, fitz.WithStreamFilter(filter))
if err != nil {
panic(err)
}
// Read is the event-only projection of ReadPage.
// ReadPage exposes filtered markers plus cursor progression across hidden offsets.
_ = page.Cursor.LastResourceOffsetfitz/: public client, public domain wrappers, public typesinternal/core/client: top-level client implementationinternal/core/connection: CONNECT lifecycle, request correlation, notify dispatchinternal/core/transport: TCP and WebSocket transportsinternal/protocol: frame encoding and message type constantsinternal/domains/*: spec-aligned domain clientstest/: broker-backed integration coverage
Integration tests target a running Fitz broker and are opt-in via the
integration build tag.
Use the local compose stack in compose.yml:
docker compose up -dThat starts:
fitz-authon loopback-onlylocalhost:4091andws://localhost:4090/wsfitz-anonon loopback-onlylocalhost:4191andws://localhost:4190/ws
Both services use ghcr.io/cntryl/fitz:latest and local storage volumes. Host
ports are configurable through the FITZ_AUTH_HOST_* and
FITZ_ANON_HOST_* variables.
Anonymous broker example:
export FITZ_BROKER_TCP_ADDR=localhost:4191
export FITZ_BROKER_WS_ADDR=ws://localhost:4190/ws
go test ./...Run the broker-backed acceptance suite explicitly when you want the full end-to-end matrix:
go test -tags=integration ./test ./test/conformance/...Auth-required broker example:
export FITZ_BROKER_TCP_ADDR=localhost:4091
export FITZ_BROKER_WS_ADDR=ws://localhost:4090/ws
export FITZ_BROKER_AUTH_REQUIRED=true
export FITZ_BROKER_JWT_HMAC_SECRET=dev-test-secret
export FITZ_BROKER_JWT_AUDIENCE=fitz
go test -tags=integration ./testError-path coverage in the broker-backed suite includes unauthorized operations across all 7 domains, plus invalid KV range and invalid cron cases.
Focused reconnect validation is usually more useful than a blanket go test -tags=integration ./test run in this repo. The high-signal reconnect slices are:
go test -tags=integration ./test -run "TestShould(RestoreNoticeSubscriptionGivenLiveDisconnectWhenReconnectEnabled|RestoreWorkerRegistrationGivenLiveDisconnectWhenReconnectEnabled|RestoreAvailabilitySubscriptionGivenLiveDisconnectWhenReconnectEnabled|RestoreCommitSubscriptionGivenLiveDisconnectWhenReconnectEnabled|RestoreLeaseSubscriptionGivenLiveDisconnectWhenReconnectEnabled)"
go test -tags=integration ./test -run "TestShouldRestoreScheduleSubscriptionGivenLiveDisconnectWhenReconnectEnabled"
go test -tags=integration ./test/conformance/... -run "TestConformanceSuite/(CS-009_disconnect_during_request|CS-010_reconnect_behavior)"Those tests use the shared live-disconnect seam in test/fixture/proxy.go to exercise real disconnect, reconnect, and restore behavior against a running broker.
Benchmark thresholds and evidence policy are documented in docs/PERF_RESULTS.md; treat that report as the source of truth for hot-path gates.
Run the full suite with:
go test ./...
go test -tags=integration ./test ./test/conformance/...Run pedantic lint and style checks directly with golangci-lint v2:
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
$(go env GOPATH)/bin/golangci-lint version
$(go env GOPATH)/bin/golangci-lint run --config .golangci.ymlRun the repo-local spec-compliance conformance suite with:
go test -v -timeout 120s ./test/conformance/... -run TestConformanceSuiteUse direct go test and go tool pprof commands while optimizing hot paths.
The benchmark gates and evidence policy are summarized in docs/PERF_RESULTS.md.
Run hotpath micro-benchmarks:
go test -run=^$ -bench='Benchmark(EncodeFrame|DecodeFrame|EncodeFrameWithPayloadWriter|MuxDispatchResponse|RegisterRequest|ConcurrentDispatch|WriteFrame|ReadFrame|WriteWSFrame|ReadWSFrame|WriteU64|WriteU32|WriteString|WriteBytes)' -benchmem -count=5 -benchtime=2s ./internal/protocol ./internal/core/encoding ./internal/core/connection ./internal/core/transportRun domain-level benchmarks:
go test -run=^$ -bench=Benchmark -benchmem -count=5 -benchtime=2s ./internal/domains/...Run the public hotpath suite:
go test -run=^$ -bench='Benchmark(HandleRPCResponseHotPath|QueueReserveHotPath|QueueCompleteHotPath|StreamBeginHotPath|StreamAppendHotPath|ScheduleCreateHotPath|ScheduleCancelHotPath|SubscriptionRegistryRestore|KVTransactionLoopback|NoticePublishHotPath|FrameEncode|RPCCorrelation1KInFlight)' -benchmem -count=5 -benchtime=2s ./bench ./internal/domains/rpcbench/hotpath_bench_test.go holds the cross-domain hotpaths, and internal/domains/rpc/rpc_test.go keeps the RPC dispatch benchmark next to the implementation.
Collect CPU and memory profiles for one benchmark target:
go test -run=^$ -bench=BenchmarkMuxDispatchResponse -benchmem -count=1 -cpuprofile=cpu.prof -memprofile=mem.prof ./internal/core/connection
go tool pprof -top cpu.prof
go tool pprof -top mem.profWhen you need a regression diff, capture two local benchmark runs and compare them with benchstat:
go test -run=^$ -bench=. -benchmem -count=3 \
github.com/cntryl/fitz-go/v2/bench \
github.com/cntryl/fitz-go/v2/internal/domains/rpc \
github.com/cntryl/fitz-go/v2/internal/protocol \
github.com/cntryl/fitz-go/v2/internal/core/connection \
github.com/cntryl/fitz-go/v2/internal/core/encoding \
github.com/cntryl/fitz-go/v2/internal/core/transport \
github.com/cntryl/fitz-go/v2/internal/domains/stream \
github.com/cntryl/fitz-go/v2/internal/domains/kv \
github.com/cntryl/fitz-go/v2/internal/domains/notice \
github.com/cntryl/fitz-go/v2/internal/domains/schedule > before.txt
go test -run=^$ -bench=. -benchmem -count=3 \
github.com/cntryl/fitz-go/v2/bench \
github.com/cntryl/fitz-go/v2/internal/domains/rpc \
github.com/cntryl/fitz-go/v2/internal/protocol \
github.com/cntryl/fitz-go/v2/internal/core/connection \
github.com/cntryl/fitz-go/v2/internal/core/encoding \
github.com/cntryl/fitz-go/v2/internal/core/transport \
github.com/cntryl/fitz-go/v2/internal/domains/stream \
github.com/cntryl/fitz-go/v2/internal/domains/kv \
github.com/cntryl/fitz-go/v2/internal/domains/notice \
github.com/cntryl/fitz-go/v2/internal/domains/schedule > after.txt
benchstat before.txt after.txtInstall benchstat once if needed with go install golang.org/x/perf/cmd/benchstat@latest.
This repo does not maintain an independent copy of the protocol specification. Use the canonical server-owned docs referenced from:
This is a lightweight map from implemented message-type ranges to conformance
scenario coverage in test/conformance.
| Domain | Message type range | Key message types (examples) | Conformance scenarios |
|---|---|---|---|
| Control | 1 | CONNECT | CS-001, CS-002 |
| KV | 100-108 | BEGIN, COMMIT, GET, PUT, INSERT, SCAN | CS-001, CS-003, CS-005, CS-006, CS-014, CS-015 |
| Queue | 200, 202-204, 207-209 | ENQUEUE, RESERVE, EXTEND, COMPLETE, SUBSCRIBE | CS-018, CS-022 |
| RPC | 300-303 | SUBSCRIBE_WORKER, UNSUBSCRIBE_WORKER, REQUEST, RESPONSE | CS-004, CS-006, CS-007, CS-008, CS-009, CS-017 |
| Lease | 400-403, 407-409 | ACQUIRE, RENEW, RELEASE, QUERY, NOTIFY | CS-019 |
| Notice | 500-504 | PUBLISH, SUBSCRIBE, UNSUBSCRIBE, NOTIFY | CS-010, CS-020 |
| Stream | 600-609 | BEGIN, APPEND, COMMIT, READ, SUBSCRIBE | CS-011, CS-012, CS-013, CS-016 |
| Schedule | 700-705 | CREATE, CANCEL, LIST, SUBSCRIBE, NOTIFY | CS-021 |
Notes:
- Queue
201(ENQUEUE_BATCH) is reserved by spec and intentionally not implemented. - Delayed queue visibility is available through
EnqueueWithOptionsandWithQueueEnqueueDelaySeconds;Enqueueremains the immediate shorthand. - CS-001–CS-017 are the shared cross-language suite. CS-018–CS-022 are Go additions for Queue, Lease, Notice, Schedule, and queue reconnect coverage.
- Schedule fire delivery (actual cron trigger) is covered in the integration suite
(
TestShouldDeliverScheduleNotificationGivenLiveBrokerWhenScheduleFires), which requires up to 90 s for the next* * * * *tick. - Conformance scenarios focus on cross-language semantic parity; integration tests provide additional domain-specific operation coverage.
client.Lease().WithLease(ctx, route, ttlSecs, callback) owns acquisition, renewal,
callback cancellation, and release. Add fitz.WithLeaseWaitSeconds(30) to use the
broker's bounded FIFO acquisition queue.
The callback must honor its context promptly; context.Cause reports caller cancellation
or fitz.ErrLeaseLost. Low-level lease methods remain available and serialize token rotation.
Use fitz.LeaseAuthorityFromContext inside the callback to obtain the broker-issued
admission fence from the successful ACQUIRE that started that invocation:
err := client.Lease().WithLease(ctx, route, 30, func(callbackCtx context.Context) error {
if authority, ok := fitz.LeaseAuthorityFromContext(callbackCtx); ok {
return runRole(callbackCtx, authority.FencingToken)
}
return errors.New("managed lease authority is missing")
}, fitz.WithLeaseOwnerID("worker-1"))The authority is an immutable value snapshot. A queued acquisition exposes the final granted token, and managed renewal does not update the callback snapshot even when it rotates the live lease credential. Treat it as an admission fencing epoch, not a live renewal credential. Tokens are ordered only across successive ownership of the same lease route. An external store enforces fencing by atomically retaining its greatest accepted value and rejecting lower values; do not compare the admission snapshot for equality with a later live broker token.