@truewire/core
v0.1.1
Published
Runtime for Truewire-generated TypeScript clients: HTTP and WebSocket transport, codecs, paging, timestamps, errors.
Maintainers
Readme
@truewire/core
Runtime for Truewire-generated TypeScript clients
Every TypeScript client generated by Truewire depends on this package. It holds the parts of a client that are the same for every API — HTTP and WebSocket transport, response codecs, paging, timestamp conversion and the error hierarchy — so generated code only carries what is specific to one API: envelope extraction, error mapping, request signing, wire quirks.
You do not normally install it directly; a generated client lists it as a dependency. You will import from it when catching errors, converting timestamps, or driving a paginated walk.
Installation
npm install @truewire/coreESM only, Node 22+ (global fetch and WebSocket) or any modern browser. No runtime
dependencies. The generator that emits the clients is the truewire CLI (Python, on PyPI);
truewire.dev and
docs/typescript.md
describe the spec format, the generated code and the core contract.
What it provides
| entry point | contents |
| --- | --- |
| @truewire/core/errors | TruewireError, NetworkError, ValidationError, ApiError (BadRequest, AuthError, RateLimited), LogicError |
| @truewire/core/codec | Codec<T> and the combinators (object, array, tuple, union, literal, record, decimal, epochMillis, ...) |
| @truewire/core/http | HttpClient over fetch, with NetworkError mapping and wire-level recording() |
| @truewire/core/ws | WebSocket base classes: Socket, Streams, Rpc, StreamsRpc, SerialReplies; Stream, Subscription |
| @truewire/core/times | EpochConverter, IsoConverter, DateConverter, the Timestamp* aliases and DateIso |
| @truewire/core/paging | PaginatedResponse and Page |
| @truewire/core/contract | HttpEndpoint<Meta>, CommandEndpoint<Meta>, StreamEndpoint<Meta> and CallOptions: what a generated client asks of its hand-written core |
| @truewire/core | everything above; the codec combinators as the t namespace, the socket classes as ws |
Codecs
A generated type is a plain interface plus a codec: an object with parse (decoded
wire JSON to the typed value) and dump (typed value back to the wire). The codec is built
from a small set of combinators, declared against the interface so tsc proves the two agree:
import { t, type Codec, type Decimal, type TimestampMillis } from '@truewire/core'
interface Order {
id: string
amount: Decimal // "10.50" on the wire, kept as the exact digits, branded
created: TimestampMillis // 1717072496123 on the wire, a Date in the client
note?: string
}
const Order: Codec<Order> = t.object({
id: t.string,
amount: t.decimal,
created: t.epochMillis,
note: t.optional(t.string),
})
const order = Order.parse(JSON.parse(body)) // ValidationError names the path: "expected decimal string, got 10.5 at /amount"
Order.dump(order) // { id, amount: '10.50', created: 1717072496123 }Objects keep keys they were not told about, both ways, so an undocumented field never
breaks a client. union tries variants in order (anyOf); record is a map with arbitrary
keys (additionalProperties); tuple is prefixItems. The wire formats — decimal,
integerString, booleanString, epochSeconds/epochMillis/epochMicros/epochNanos,
dateTime, date — parse to Decimal, number, boolean, Date and DateIso, and
t.wire builds one of your own. There is no schema interpretation and no eval: the
combinators are the whole validator, small enough to ship to a browser.
Errors
Every failure a client throws derives from TruewireError:
import { ApiError, AuthError, NetworkError, RateLimited } from '@truewire/core'
try {
const order = await client.orders.get({ id: 'ord_123' })
} catch (e) {
if (e instanceof AuthError) ... // bad or missing credentials
else if (e instanceof RateLimited) ... // the API told us to slow down
else if (e instanceof ApiError) ... // any other error the API itself returned
else if (e instanceof NetworkError) ...// couldn't reach the server, or the connection dropped
}Each class also carries a string code ('auth', 'rate-limited', ...) so an error that
crossed a duplicate-bundle boundary can still be told apart.
Paging
Every generated <method>Paged returns a PaginatedResponse: thenable (awaiting gives every
row, flattened) and async-iterable (one page of rows at a time, empty pages skipped). Each
page is one pure next(state) call, so a caller can retry or resume a single page rather
than the whole walk:
const paging = client.orders.listPaged({ status: 'open' })
const orders = await paging // every row, flattened
for await (const rows of paging) ... // one page at a time
for await (const page of paging.pages()) ... // { rows, state, next }, for checkpointing
paging.resume(savedState) // restart from a checkpointed state
paging.via(retried) // route every page fetch through a middlewarevia(call) hands each page fetch to call as one zero-argument function, so a retry or
logging layer wraps a page without unrolling the loop by hand. The contract that makes this
safe: next is a pure function of state, state fully determines the request, and the
request is a read.
Timestamps
import { EpochConverter, IsoConverter, DateConverter, timestampMillis } from '@truewire/core'
timestampMillis.parse(1717072496123) // Date
timestampMillis.dump(new Date()) // number
new IsoConverter().parse('2024-05-30T12:34:56.123456789Z') // any fraction length, any offset
new DateConverter({ pattern: '%Y%m%d' }).parse('20260101') // '2026-01-01' as DateIsoTimestamps are Dates behind the TimestampSeconds/TimestampMillis/... aliases, so a
later move to Temporal is one alias change. Epoch arithmetic is integer (BigInt)
throughout: a nanosecond value, even as a numeral string beyond Number.MAX_SAFE_INTEGER,
keeps its millisecond digits exactly; the sub-millisecond digits are lost, since a Date
has none.
WebSocket
Socket opens lazily on first use and closes on close() (or await using). Rpc
correlates replies by id, Streams multiplexes channel subscriptions, StreamsRpc does
both on one connection, and SerialReplies matches uncorrelated replies by order. A
project's hand-written core is a small subclass implementing parseMsg, rpcSend,
requestSubscription and requestUnsubscription:
await using stream = client.streams.ticker({ symbol: 'BTC/USD' })
for await (const tick of stream) ...
// unsubscribed on scope exitDevelopment
The source lives in packages/core-ts of
truewire-dev/truewire; the
CHANGELOG
lists what each version changed.
yarn install
yarn test # vitest
yarn typecheck # tsc --noEmit over src and test
yarn build # tsc to dist/License
MIT — see LICENSE.
