Skip to content

Structured plan-limit errors, handshake fix, and PyPI release workflow#1

Merged
jshaw merged 2 commits into
mainfrom
feat/limit-errors-and-release-workflow
Jun 12, 2026
Merged

Structured plan-limit errors, handshake fix, and PyPI release workflow#1
jshaw merged 2 commits into
mainfrom
feat/limit-errors-and-release-workflow

Conversation

@jshaw

@jshaw jshaw commented Jun 11, 2026

Copy link
Copy Markdown
Member

What changed

Bug fix: connect() lied about success, then reconnect-hammered the gateway

The client set its connected state as soon as the WebSocket opened — it never waited for the gateway's {"type": "connected"} handshake. When the gateway rejected a connection pre-handshake (e.g. device_limit_reached) and closed the socket, the client treated it as a normal drop and reconnected in an exponential-backoff loop forever, while connect()/connect_sync() either falsely succeeded or timed out with no explanation.

Now:

  • _await_handshake() waits for connected or an error envelope before reporting success
  • device_limit_reached is fatal: the reconnect loop stops, and connect()/connect_sync() raise the structured DataNetError immediately
  • Transient errors (rate_limited) keep the existing retry behavior

Structured limit errors

  • DataNetError.limit — the plan cap that was hit, populated for device_limit_reached and the gateway's new topic_limit_reached (channel cap, e.g. after a tier downgrade)
  • PROTOCOL.md gains an Error Codes table: every gateway error, its extra fields, and whether it's retryable

PyPI release workflow

v* tags build wheel + sdist and publish via Trusted Publishing (OIDC) — no token stored anywhere. One-time setup before the first tag (documented in the workflow file): register a pending publisher on pypi.org for datanet-sdk and create a pypi environment in repo settings.

Why

Plan tiers (Free 100 msg/s / Artist 2,000 / Studio 8,000) are enforced by the gateway; the SDK needs to surface those rejections as actionable errors rather than silent reconnect loops. Matches the equivalent work in datanet-js, so both SDKs behave identically at the protocol level.

Tests

23 passed (7 new: handshake accept/reject/noise-skip, rate_limited fields, topic_limit_reached fields, mid-session fatal error stops the loop, wait_for_connection raises the fatal error immediately).

🤖 Generated with Claude Code

jshaw and others added 2 commits June 11, 2026 14:58
…ached

The client previously reported itself connected as soon as the WebSocket
opened, without waiting for the gateway handshake. A device-limited client
would therefore reconnect in a backoff loop forever, and connect() would
appear to succeed (or time out) with no explanation.

- Wait for the gateway's {"type": "connected"} handshake (or its error)
  before reporting success; connect()/connect_sync() now raise a structured
  DataNetError for pre-handshake rejections
- Treat device_limit_reached as fatal: stop the reconnect loop instead of
  hammering the gateway with retries that can only fail
- Add DataNetError.limit (plan cap that was hit) for device_limit_reached
  and the gateway's new topic_limit_reached
- Document all gateway error codes in PROTOCOL.md (when they fire, extra
  fields, retryability)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Builds wheel + sdist and publishes to PyPI via OIDC — no token to store or
rotate. Requires a one-time pending-publisher registration on pypi.org and
a "pypi" environment in repo settings (steps documented in the workflow).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jshaw jshaw merged commit 32beb1a into main Jun 12, 2026
3 checks passed
@jshaw jshaw deleted the feat/limit-errors-and-release-workflow branch June 12, 2026 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant