This is a polyglot SDK monorepo. A single Rust core (rust/sdk/) implements gRPC streaming, OAuth, recovery, and ingestion logic. Five wrapper SDKs expose it to other languages:
| SDK | Directory | Binding mechanism | Build system |
|---|---|---|---|
| Rust | rust/ |
Native | Cargo workspace |
| Python | python/ |
PyO3 / maturin | maturin + pip |
| TypeScript | typescript/ |
NAPI-RS | npm + napi-rs |
| Java | java/ |
JNI (rust/jni/) |
Maven |
| Go | go/ |
cgo + static FFI lib | Go modules |
| C++ | cpp/ |
C FFI (rust/ffi/) |
CMake |
The data flow is: User code → Wrapper API → FFI boundary → Rust core → gRPC → Zerobus service.
Go and C++ use the C FFI layer (rust/ffi/, header auto-generated by cbindgen). Java has its own JNI crate (rust/jni/). Python and TypeScript bind Rust directly via PyO3 and NAPI-RS respectively.
These rules apply to every example, snippet, doc comment, and test you write across all SDKs, and to any client code you generate for a user. Ingestion is asynchronous and pipelined in all SDKs — getting this wrong is the single most common performance bug.
The cardinal rule: never wait for an acknowledgment after every ingest call.
ingest(ingest_record_offset/ingestRecordOffset/IngestRecordOffset) returns as soon as the record is queued; sending and acknowledgment happen on background tasks. The returned offset is a handle you can wait on later — not a signal to wait now.- Calling the wait method (
wait_for_offset/waitForOffset/WaitForOffset, or.join()on a per-record future) inside the ingest loop forces one full server round-trip per record. Throughput collapses to ~1 record per round-trip — orders of magnitude below the SDK's capability.
The correct pattern (use this in all examples by default):
for record in records:
stream.ingest(record) # queue only — do NOT wait here
stream.flush() # wait once for all pending acks at the end
- For continuous/unbounded streams, call
flush()periodically (every N records) rather than per record, or register an ack callback for async notification. - Reserve per-record
wait_for_offsetfor genuinely low-volume cases where each record must be confirmed durable before continuing — and label it as such. - Prefer the batch API (
ingest_records_offset/ingestRecordsOffset/IngestRecordsOffset) in hot paths; it amortizes the per-call FFI crossing.
When writing or reviewing a README, doc comment, or example, the first ingestion code a reader sees must demonstrate the loop-then-flush() pattern, not per-record waiting.
Strict semver. Breaking changes are only allowed in major version bumps.
- Removing or renaming a public API element is a breaking change. Mark it
#[deprecated](or language equivalent) in a minor release first, then remove in the next major. - Changing the FFI C header (
rust/ffi/zerobus.h) in a non-additive way breaks Go and Java. Treat FFI signature changes with the same rigor as public API changes. - Each SDK is versioned independently. A Rust core bump does not automatically require wrapper version bumps unless the wrapper's public API changes.
- Release tags follow
<sdk>/v<semver>(e.g.,rust/v1.1.0,python/v1.0.0).
Because every non-Rust SDK crosses a foreign-function boundary, keep these in mind:
- Every call across FFI serializes/deserializes data. Prefer batch APIs (
ingest_records) over single-record APIs in hot paths. - Proto descriptors and Arrow IPC schemas are serialized as byte arrays across the boundary. These are typically one-time costs at stream creation.
- Arrow Flight uses IPC format across FFI for near-zero-copy transfer; avoid unnecessary re-encoding.
- Rust owns the memory for SDK and stream handles. Wrappers hold opaque pointers and must call the corresponding free/close/destroy function.
- C-allocated error strings (
CResult.error_message) must be freed by the caller viazerobus_free_error_message(). - Go uses
runtime.Pinnerto prevent GC from relocating memory while Rust holds a pointer. Never remove pinner calls. - Go's
cgo.Handleregistry keepsHeadersProvidercallbacks alive; leaking these handles leaks Go objects. - Java has no finalizers on streams — users must call
close()or use try-with-resources. Forgetting this leaks JNI resources. - Python and TypeScript rely on GC-triggered cleanup via PyO3/NAPI-RS reference counting.
- Rust core is async (tokio). The FFI layer manages its own tokio runtime.
- Go: safe for concurrent goroutines.
- Python: async streams are safe; sync streams are single-threaded per instance.
- TypeScript: async-safe via Node.js event loop.
- Java: not thread-safe — external synchronization required for concurrent access.
Each SDK has its own build/test commands. Run from the SDK directory:
| SDK | Build | Test | Lint | Format |
|---|---|---|---|---|
| Rust | make build |
make test |
make lint |
make fmt |
| Python | make build-rust |
make test |
make lint |
make fmt |
| TypeScript | npm run build |
npm test |
cargo clippy |
cargo fmt |
| Java | mvn compile |
mvn test |
mvn spotless:check |
mvn spotless:apply |
| Go | make build |
make test |
make lint |
make fmt |
| C++ | make build |
make test |
make lint |
make fmt |
push.ymlis the main gate — uses path filtering to run only affected SDK CI jobs.- Cross-SDK CI (non-blocking): when
rust/**changes, wrapper SDK tests also run against local Rust source to catch FFI breakage early. - Release workflows are tag-triggered (see Release Process below).
Every SDK has two changelog files:
NEXT_CHANGELOG.md— Accumulates entries for the upcoming release. Every PR that changes user-facing behavior must add an entry here under the appropriate section (New Features, Bug Fixes, Breaking Changes, Deprecations, API Changes, Documentation, Internal Changes).CHANGELOG.md— Finalized release history. When a version-bump PR is merged, the contents ofNEXT_CHANGELOG.mdare prepended toCHANGELOG.mdandNEXT_CHANGELOG.mdis reset to an empty template with the next version header.
Rules:
- If a PR adds a feature, fixes a bug, deprecates an API, or changes behavior — it must update
NEXT_CHANGELOG.md. No exceptions. - Breaking changes must be called out under
### Breaking Changeswith migration instructions. - Deprecations must include what to use instead and the planned removal timeline.
Every PR that changes user-facing behavior must also update:
- README — If the change affects usage, setup, or API surface, update the SDK's
README.md. - Examples — If the change adds a new API or modifies an existing one, add or update examples in the SDK's
examples/directory. - API docs — Rust: doc comments. Python: docstrings + type stubs. TypeScript: JSDoc. Java: Javadoc. Go: godoc comments.
Releases are per-SDK and tag-triggered:
- Version bump PR: Update the version in the SDK's config file (
Cargo.toml,package.json,pom.xml, etc.). MoveNEXT_CHANGELOG.mdcontents intoCHANGELOG.md. ResetNEXT_CHANGELOG.mdwith the next planned version header. - Merge to main.
- Tag: Push a tag matching
<sdk>/v<semver>(e.g.,rust/v1.2.0,python/v1.1.0). This triggers the release workflow. - Release workflow:
- Builds platform-specific artifacts (5 platforms: Linux x86_64/aarch64, macOS x86_64/arm64, Windows x86_64).
- Publishes to the SDK's package registry (crates.io, PyPI, npm, Maven Central, or git tag for Go).
- Creates a GitHub Release with release notes extracted from
CHANGELOG.md.
SDK-specific release details:
| SDK | Version source | Registry | Tag pattern |
|---|---|---|---|
| Rust | rust/sdk/Cargo.toml |
crates.io | rust/v* |
| Python | Derived from Cargo.toml via maturin | PyPI | python/v* |
| TypeScript | typescript/package.json |
npm | typescript/v* |
| Java | java/pom.xml |
Maven Central | java/v* |
| Go | Git tag (no version file) | pkg.go.dev | go/v* |
| C++ | cpp/include/zerobus/version.hpp + cpp/CMakeLists.txt |
Source / CMake (GitHub Release planned) | cpp/v* (planned) |
| FFI | rust/ffi/Cargo.toml |
GitHub Release | ffi/v* |
The FFI library has its own release cycle because Go and Java depend on pre-built static/dynamic libraries. An FFI release must happen before Go/Java can pick up Rust core changes. (C++ builds the FFI from local Rust source by default, so it needs the FFI source present rather than a pre-built library; a C++ release still bundles a built FFI archive.)
- Prefix commit messages with
[SDK]when changes are scoped to one SDK (e.g.,[Rust],[Python],[All]). - Signed commits required (DCO). Use
git commit -s. - Present tense, imperative mood. Keep subject line under 50 chars.