Skip to content

TypeScript SDK: strict TypeScript consumers, Expo codegen, short-lived tokens, and HTTP/2 for procedure HTTP - #6006

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

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

Conversation

@bradleyshep

Copy link
Copy Markdown
Contributor

Description of Changes

Changes needed to use the TypeScript SDK from a strict Node/TypeScript codebase with Clerk auth, and to send iOS pushes from a module:

  • Types that work for NodeNext and exactOptionalPropertyTypes consumers. The published type files re-export with extensionless paths such as './lib/identity', so under NodeNext resolution spacetimedb appears to have no exports. Every relative import now has an explicit extension, a lint rule keeps it that way, and codegen and generate-client-api emit them too. Separately, under exactOptionalPropertyTypes every generated table schema failed the SDK's own constraint (name?: string versus string | undefined), so conn.reducers became never. Those two option types now allow undefined. tests/consumer typechecks a strict app against the built package.
  • Fail the connection instead of crashing Node when a server message can't be applied. Those errors escaped the WebSocket listener, and Node rethrows them and exits.
  • Type DbConnection.db with a merged interface instead of a declare field. Babel rejects declare fields unless allowDeclareFields is set, which babel-preset-expo doesn't do, so Expo apps couldn't bundle generated bindings.
  • Let short-lived auth tokens survive reconnects. withToken also accepts a function, called before every attempt, and useSpacetimeDB() gains reconnect(builder).
  • Negotiate HTTP/2 for procedure HTTPS requests (native-tls-alpn), for HTTP/2-only APIs such as APNs.
  • Stop re-signed websocket tokens from outliving the original. /v1/identity/websocket-token re-signed any accepted token with a fresh iat and an exp 60 s out, so a token could be renewed forever. The copy now keeps the original iat and never expires after the original.

A smaller alternative that keeps only the crash fix, the token function and HTTP/2 is bradley/ts-sdk-bundler. The history includes a CommonJS-types commit and its revert; squash on merge.

API and ABI breaking changes

None intended. The import-extension change affects only relative paths inside the package. withToken accepts a function in addition to a string.

Rollback safety impact

n/a

Expected complexity level and risk

  1. The extension change touches about 105 files but is mechanical, and it is covered by the lint rule and the consumer typecheck test. The re-signed token change narrows token lifetimes; a client that relied on renewing a re-signed token indefinitely would now have to fetch a fresh one.

Testing

  • pnpm test in crates/bindings-typescript (329 passing), including the strict consumer typecheck
  • eslint src
  • Used by a Node server (NodeNext, exactOptionalPropertyTypes), a web client and an Expo app against a local 2.11 standalone
  • Reviewer: check codegen output for other languages is unaffected

…opertyTypes consumers

The published .d.ts files re-export with extensionless relative paths
(`export * from './lib/connection_id'`). The package is `"type": "module"`,
so TypeScript projects using `moduleResolution: node16/nodenext` cannot
resolve them and see no exports from `spacetimedb` at all.

Separately, `ConstraintOpts` and `IndexOpts` declare `name?: string` while
the SDK derives `{ name: string | undefined }` for them. Under
`exactOptionalPropertyTypes` every generated schema then fails
`UntypedSchemaDef`, and `conn.reducers` collapses to `never`.

- Give every relative import in the SDK source an explicit extension; tsc
  copies specifiers into the emitted .d.ts files verbatim. The moduledef and
  client-api generators now emit the extension too.
- Widen the optional `name` fields to `string | undefined`.
- Lint rejects extensionless relative imports in the SDK source, and a
  test typechecks generated bindings against the built package with
  `exactOptionalPropertyTypes` on.
…y points

The package is `"type": "module"`, so its .d.ts files are ES module types,
yet `require` resolves to the .cjs builds. Under node16 resolution
CommonJS consumers were told they were importing an ES module
("Masquerading as ESM" in arethetypeswrong) and could not require it.

build:types now mirrors dist/**/*.d.ts as .d.cts, and each entry that ships
a .cjs build points its `require` types condition at them. The consumer
types test also typechecks a Node ESM and a CommonJS consumer.
…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.
…ire entry points"

This reverts commit b2e4ae6. T3 Code, the consumer these SDK changes
were made for, only imports the SDK as an ES module, and typechecks
without the .d.cts declarations.

The TokenProvider check a later commit added to the removed Node
consumer tests moves to the strict consumer app.
…ct-types

# Conflicts:
#	crates/bindings-typescript/src/sdk/reducers.ts
#	crates/bindings-typescript/src/server/index.ts
@bradleyshep

Copy link
Copy Markdown
Contributor Author

Split into separate drafts: #6009 (strict TypeScript consumers), #6010 (Node crash), #6011 (token provider), #6012 (HTTP/2), #6013 (codegen db interface). The re-signed websocket token fix isn't carried over; it can come back as its own PR if wanted.

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