Skip to content

ts-sdk: coerce numeric inputs to bigint in id-like constructors#5041

Open
Ludv1gL wants to merge 2 commits into
clockworklabs:masterfrom
Ludv1gL:ludvig/coerce-bigint-in-id-ctors
Open

ts-sdk: coerce numeric inputs to bigint in id-like constructors#5041
Ludv1gL wants to merge 2 commits into
clockworklabs:masterfrom
Ludv1gL:ludvig/coerce-bigint-in-id-ctors

Conversation

@Ludv1gL
Copy link
Copy Markdown
Contributor

@Ludv1gL Ludv1gL commented May 15, 2026

Description of Changes

The wrapper constructors for Identity, ConnectionId, Timestamp, TimeDuration, and Uuid all claim to take bigint (or string | bigint for Identity), but real-world callers can end up passing a number whenever data round-trips through JSON in caller-side code — JSON.parse has no way to produce a bigint, so a u128/u256 that goes through any JSON pipeline (custom state caches, hand-rolled HTTP clients, codegen drift, etc.) comes back as a lossy number.

The TS SDK itself does not do this — its normal flow is WebSocket BSATN, which is bigint-safe end-to-end. The fix is a defensive guard at the type boundary so that if something hands the SDK a number, the failure is graceful rather than a cryptic late crash.

Before this patch the constructors stored that number verbatim into a field typed as bigint. The failure then manifested several layers later in BSATN serialization, e.g. inside BinaryWriter.writeU256:

TypeError: Cannot mix BigInt and other types, use explicit conversions
    at BinaryWriter.writeU256 (binary_writer.ts:171:21)

with no indication of which call site was at fault.

Fix

Wrap the non-string branch of each constructor in BigInt(...):

Class File Behavior on number input — before after
Identity lib/identity.ts silent corruption → late crash in writeU256 coerced to bigint
ConnectionId lib/connection_id.ts silent corruption → late crash in writeU128 coerced to bigint
Timestamp lib/timestamp.ts silent corruption → mix error in arithmetic / toDate() coerced to bigint
TimeDuration lib/time_duration.ts silent corruption → mix error in arithmetic coerced to bigint
Uuid lib/uuid.ts cryptic Cannot mix BigInt and other types from range check range check passes / fails cleanly
  • bigint inputs are returned unchanged (BigInt(123n) === 123n).
  • number inputs are coerced cleanly. Precision was already lost upstream by whatever produced the number — this just stops the secondary crash.
  • undefined / objects throw a clear TypeError immediately at the constructor, rather than several frames deep in serialization.

No public type signatures change; strict-TS callers compile-check exactly as before.

Tests

Added crates/bindings-typescript/tests/id_ctor_coercion.test.ts pinning the new behavior across all five classes (9 cases): bigint pass-through, number coercion, hex-string parsing for Identity, range check preservation for Uuid, and clear errors on undefined.

Full test suite (pnpm run test) — 191 passed, 0 failed.
pnpm run lint — clean.
pnpm run build:types — clean.

API and ABI breaking changes

None. Public type signatures unchanged. Behavior is strictly more permissive (previously-failing inputs now succeed or throw earlier with a clearer error).

Expected complexity level and risk

1 — narrow defensive change, six small constructors, isolated tests.

Ludv1gL added 2 commits May 15, 2026 23:25
The wrapper constructors for `Identity`, `ConnectionId`, `Timestamp`,
`TimeDuration`, and `Uuid` all claim to take `bigint` (or `string |
bigint` for `Identity`), but real-world callers can end up passing a
`number` whenever data round-trips through JSON — `JSON.parse` has no
way to produce a bigint, so HTTP responses, custom state caches, and
JSON-format SDK endpoints can quietly downgrade a u128/u256 to a lossy
number. Before this patch the constructors stored that number verbatim
into a field typed as `bigint`, and the failure manifested several
layers later in BSATN serialization as `TypeError: Cannot mix BigInt
and other types`, with no indication of which call site was at fault.

Wrap the non-string branch of each constructor in `BigInt(...)`:

* `bigint` inputs are returned unchanged.
* `number` inputs are coerced cleanly (and lose precision the same way
  `JSON.parse` already lost it — this just stops the crash).
* `undefined` / objects throw a clear `TypeError` immediately, pointing
  at the constructor call instead of a deep serialization frame.

No public type changes; strict-TS callers see no difference. Adds a
small test file pinning the new behavior across all five classes.
The previous comment on Identity's constructor cited the HTTP `/sql`
endpoint as a JSON path, but the TS SDK doesn't call `/sql` anywhere.
The real number-input source for this constructor is caller-side code
outside the SDK (custom state caches, hand-rolled HTTP clients, codegen
drift, etc.). Update the comment to be honest about that.

No code change.
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