Skip to content

TypeScript SDK: survive short-lived tokens and bad messages in Node; HTTP/2 for procedure HTTP - #6005

Closed
bradleyshep wants to merge 9 commits into
masterfrom
bradley/ts-sdk-bundler
Closed

bradleyshep wants to merge 9 commits into
masterfrom
bradley/ts-sdk-bundler

Conversation

@bradleyshep

Copy link
Copy Markdown
Contributor

Description of Changes

Three fixes needed to run a TypeScript SDK client in Node against Clerk auth, and to send iOS pushes from a module:

  • Fail the connection instead of crashing Node when a server message can't be applied, for example a row the client's bindings can't decode, or a user callback that throws. Those errors escaped the WebSocket message listener. Browsers only report them; Node rethrows them and exits. The inbound drain now catches them, closes the socket, and reports the error through onDisconnect.
  • Let short-lived auth tokens survive reconnects. withToken also accepts a sync or async function, called before every connection attempt. Before this, ConnectionManager rebuilt connections with the original token, so a Clerk session JWT (about a minute) had expired by the first reconnect and every retry got a 401. useSpacetimeDB() also gains reconnect(builder) to switch identity while the provider is mounted.
  • Negotiate HTTP/2 for procedure HTTPS requests. This enables reqwest's native-tls-alpn feature so ctx.http.fetch can reach HTTP/2-only APIs such as APNs.

This is the smaller of two alternatives. The other, which also changes the SDK's types for NodeNext and exactOptionalPropertyTypes consumers and changes codegen, is bradley/ts-sdk-strict-types. The branch history adds and then reverts those changes; squash on merge.

API and ABI breaking changes

None. withToken accepts a function in addition to a string, and reconnect is new.

Rollback safety impact

n/a

Expected complexity level and risk

  1. The token function touches ConnectionManager's reconnect path. A builder with a token function never has its token overwritten by the session's last token.

Testing

  • pnpm test in crates/bindings-typescript (322 passing), including new reconnect and token-function tests
  • Used by a Node server and a React/React Native client against a local 2.11 standalone, with Clerk tokens across reconnects
  • Module procedures reaching an HTTP/2-only mock of APNs
  • Reviewer: sanity-check the ConnectionManager change for other auth providers

…nnot be applied

Errors while processing an inbound message, whether an undecodable row
(e.g. client bindings out of step with the module's schema) or a user
callback that throws, escaped the WebSocket message listener. Browsers
only report such errors, but Node rethrows them on the next tick, which
killed the host process.

The inbound drain now catches them, logs, drops the queued messages, and
closes the socket, so `onDisconnect` receives the error the same way it
does for a websocket error on an established connection.
Generated TypeScript bindings narrowed `db` with `declare db: DbView;`.
Babel rejects `declare` class fields unless `allowDeclareFields` is set,
and babel-preset-expo does not set it, so React Native and Expo apps
could not bundle generated bindings for any module with snake_case table
accessor aliases.

Declare the narrowed type on an interface merged into the class instead.
It is type-only like before and emits nothing in any transpiler. Updated
the codegen snapshot and the checked-in bindings to match.
`withToken` only took a string, and the ConnectionManager re-applies the
session's token on every automatic rebuild. For third-party OIDC tokens
such as Clerk session JWTs (about a minute of lifetime) that token is
the original JWT, so once it expired every reconnect failed with 401 and
retried forever. React apps also had no way to switch identity while
mounted: `retain()` ignores a new builder once a connection is live.

`withToken` now also accepts a `TokenProvider`, a sync or async function
that is called on every connection attempt: the initial build, automatic
reconnects, resume and zombie revival, and `rebuild()`. Its value is that
attempt's token (`undefined` connects anonymously) and `conn.token`
reflects it. A throw or rejection reaches `onConnectError`, so the
manager retries it with backoff. The manager no longer resumes the
session token over a builder with a provider; string tokens resume as
before. `TokenProvider` is exported from the package root.

The React context gains `reconnect(builder)`, calling
`ConnectionManager.rebuild()` like the Svelte provider does. The Clerk
guide now passes a provider instead of a token fetched once.
Enable reqwest's `native-tls-alpn` feature so the native-tls connector
offers ALPN `h2`. Without it every outbound `ctx.http.fetch` over HTTPS
was pinned to HTTP/1.1, and HTTP/2-only APIs such as Apple's APNs
(iOS push notifications) could not be reached from modules.
/v1/identity/websocket-token re-signed any token it accepted, its own
included, with a fresh iat and an exp 60 s out. Re-signing each copy before
it expired renewed a token forever, and modules could never tell when a
session token was issued. The copy now keeps the original iat and expires
within 60 s but never after the original.
…exactOptionalPropertyTypes consumers"

This reverts commit f574b84.
@bradleyshep

Copy link
Copy Markdown
Contributor Author

Split into separate drafts: #6010 (Node crash), #6011 (token provider), #6012 (HTTP/2).

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