Skip to main content

TypeScript

The TypeScript client comes in two flavors: a Node.js library and a browser library. They share the same conceptual surface (IngestClient, OutletClient, ApiClient, LineageClient) but are built on different stacks.

  • Node.js@dataspine/typescript-node-client-library. gRPC over HTTP/2 via @connectrpc/connect-node and REST via the platform fetch.
  • Browser@dataspine/typescript-browser-client-library. gRPC-Web via grpc-web.

Per-data-product clients are generated by the Dataspine code generator from your published manifest and depend on the runtime library above. The placeholder used throughout this page is a data product com.acme.orders published by an organization acme, which produces the per-data-product npm package @acme/orders-node-client (or @acme/orders-browser-client).

Installation

Node.js:

npm install @dataspine/typescript-node-client-library @acme/orders-node-client

Browser:

npm install @dataspine/typescript-browser-client-library @acme/orders-browser-client

A single application typically depends on the runtime library transitively (it is a peer dependency of the per-data-product client) and pins compatible versions of both.

Configuration

The runtime library uses a ConfigLoader that resolves configuration from node-config, environment variables (prefixed DATASPINE_), and an optional inline overrides object. The minimum input is:

  • clientName — a free-form identifier for your application, used in logs and tracing.
  • region — the Dataspine region the data product is deployed in.
  • auth — one of:
    • { type: 'static-token', token }
    • { type: 'token-file', tokenFile }
    • { type: 'token-provider', tokenProvider } — your own TokenProvider implementation.
    • { type: 'exchange-token', tokenExchangeEndpoint, refreshInterval, inputToken } — STS-style token exchange.
    • { type: 'aws-sigv4', tokenExchangeEndpoint, principalId, refreshInterval, credentialsProvider? }
    • { type: 'ssh-ed25519', tokenExchangeEndpoint, principalId, privateKeyPath, publicKeyPath, refreshInterval }

The endpoint URLs (ingestEndpointUrl, outletEndpointUrl, apiGrpcEndpointUrl, apiRestEndpointUrl) are usually provided by the surrounding configuration so callers do not have to know them.

import { ConfigLoader } from '@dataspine/typescript-node-client-library';
import { UUID } from '@dataspine/typescript-node-client-library';

const config = await new ConfigLoader().load(
{ dataProductId: UUID.parse('00000000-0000-0000-0000-000000000000') },
{
clientName: 'orders-app',
region: 'eu-central-1',
auth: { type: 'token-file', tokenFile: '/var/run/secrets/dataspine/token' },
},
);

config is reusable across the typed clients you instantiate from it.

Per-data-product typed client

The generated package wires each declared API operation, ingest, and outlet to a typed method. The shape:

import { OrdersClient } from '@acme/orders-node-client';

const ordersClient = await OrdersClient.create(config);

OrdersClient.create instantiates the underlying IngestClient, OutletClient, and ApiClient for that data product and sets up the operation IDs.

API call

import { PlaceOrder, Order } from '@acme/orders-node-client';

const result = await ordersClient.placeOrder(
PlaceOrder.create({
customerId: '00000000-0000-0000-0000-000000000000',
items: [{ sku: 'BOOK-001', quantity: 1 }],
}),
);

if (result.isError) {
console.error('placeOrder failed:', result.errorMessage);
} else {
const order: Order = result.payload;
console.log('placed order', order.id);
}

The typed client wraps ApiResponse<T> (a ApiSuccessResponse<T> | ApiErrorResponse) with helpers for the ApiStatusCode mapping documented under gRPC (wire reference).

Ingest

const ingest = await ordersClient.openOrderIngest();
const batch = await ingest.beginBatch();

await batch.append({ customerId: '...', items: [...] });
await batch.append({ customerId: '...', items: [...] });

await batch.end();
await ingest.close();

For transactional producers the typed client also exposes beginRootTransaction / commit / abort, which map to the equivalent AppendMessageCommand envelopes documented in gRPC (wire reference).

Outlet

const subscription = ordersClient.subscribeToOrderEvents({
start: { type: 'Latest' },
});

for await (const event of subscription.poll(1000)) {
if (event.type === 'message') {
console.log('order event', event.offset, event.message);
} else if (event.type === 'errorCode') {
console.error('outlet error', event.errorCode);
}
}

subscription.close();

The typed wrapper handles message chunking and the pollCompleted envelope; consumers see one event.message per logical message.

Browser specifics

The browser client has the same conceptual API but:

  • OutletClient uses grpc-web streaming and exposes the same poll(durationMillis) async iterator.
  • ApiClient uses grpc-web unary RPCs.
  • RestApiClient (Node-only today) is replaced by fetch calls in the browser bundle.
  • Token providers in the browser typically come from your auth context (e.g. an OIDC PKCE flow). The static-token and token-provider auth types are the most common in browsers.
import { OrdersClient } from '@acme/orders-browser-client';
import { ConfigLoader } from '@dataspine/typescript-browser-client-library';

const config = new ConfigLoader().load({
clientName: 'orders-spa',
region: 'eu-central-1',
endpointUrl: 'https://orders.acme.example',
auth: { type: 'token-provider', tokenProvider: oidcTokenProvider },
});

const ordersClient = await OrdersClient.create(config);

Reauthentication

The runtime library subscribes to the TokenProvider and pushes a Reauthenticate envelope on every active ingest and outlet stream when the token rotates. You generally do not need to do anything; if your token provider blocks (e.g. your AWS SigV4 exchange momentarily fails), the client logs a warning and continues using the last valid token.

REST escape hatch

For raw HTTP calls (e.g. an API operation marked supports_raw_response, where the response is a non-Dataspine media type like a PDF), the Node runtime exposes RestApiClient:

import { RestApiClient } from '@dataspine/typescript-node-client-library';

const rest = new RestApiClient(tokenProvider, 'https://orders.acme.example', logger);

const stream = await rest.rawCall(
PlaceOrder.serialize,
'/api/v1/com.acme.orders.GetReceiptPdf@v1',
{ orderId: '00000000-0000-0000-0000-000000000000' },
);

stream is a ReadableStream<Uint8Array> so you can pipe the response into a file or another HTTP response without buffering it. This matches the raw HTTP response flow described in the wire reference.