LaserData Cloud
Laser SDK

Connect

Connect with a connection string, set TLS and timeouts, and pick a default stream

Create one connected client and share it. It carries authentication, transport, and connection state, and it reaches every stream on the server.

Connection strings

Pass a connection string to connect:

import { Laser } from "@laserdata/laser-sdk"

await using laser = await Laser.connect("iggy:[email protected]:8090")
use laser_sdk::prelude::*;

let laser = Laser::connect("iggy:[email protected]:8090").await?;
import laser_sdk as ls

laser = await ls.Laser.connect("iggy:[email protected]:8090")

Use user:pwd@host:port or token@host:port. The port is optional and defaults to 8090, the Apache Iggy TCP port:

# username and password
iggy:[email protected]:8090

# a token in place of user:pwd
<token>@starter-123.us-west-1.aws.laserdata.cloud:8090

All three SDKs read the string with the same rules, and every broken rule is a configuration error before any connection attempt:

  • The scheme is optional. Only iggy:// and iggy+tcp:// are accepted.
  • Credentials are required. An empty user, password, or token, more than one :, or an @ inside the credentials is refused. The SDK takes them as written, without percent-decoding, so a / in a password works.
  • The address is host or host:port. A present port must be a number from 1 to 65535. IPv6 literals, paths, and fragments are refused.
  • The options are exactly tls, tls_domain, tls_ca_file, reconnection_retries, reconnection_interval, reestablish_after, heartbeat_interval, and nodelay. Names are case-sensitive and each appears at most once as key=value. An unknown, repeated, or malformed option, or an empty ?, is refused.
  • tls and nodelay take only true or false.

To keep the credentials apart from the address, pass them separately:

const laser = await Laser.builder()
  .address("127.0.0.1", 8090)
  .credentials("iggy", "iggy")
  .connect()
let laser = Laser::builder()
    .address("127.0.0.1:8090")
    .credentials("iggy", "iggy")
    .build()
    .await?;
laser = await ls.Laser.connect(
    address="127.0.0.1:8090",
    credentials=("iggy", "iggy"),
)

Pick one form per client. The builder fails without credentials, as the string does. The builder port is optional too and defaults to 8090.

Laser::local() in Rust and Laser.local() in TypeScript and Python connect to iggy:[email protected]:8090. Laser Stack uses iggy:laser by default, so use the connection string it prints:

export LASER_CONNECTION_STRING='iggy:[email protected]:8090'

Environment variables

Laser::connect_env() in Rust, Laser.connectEnv() in TypeScript, and Laser.connect_env() in Python read the environment:

await using laser = await Laser.connectEnv()
let laser = Laser::connect_env().await?;
laser = await ls.Laser.connect_env()
VariableEffect
LASER_CONNECTION_STRINGThe connection string, exactly what connect() takes. Required. The helpers return a configuration error without it
LASER_STREAMOptional default stream
LASER_TLS_CERTPath to a CA certificate file. Turns on TLS with this CA for any host and replaces the bundled LaserData CA
LASER_NO_TLS1, true, yes, or on turns off automatic TLS setup. Use it only for local development
LASER_CONNECT_TIMEOUT_MSThe budget for the first connect. See Connect timeout
LASER_PUBLISH_TIMEOUT_MS, LASER_PUBLISH_MAX_RETRIES, LASER_PUBLISH_RETRY_BACKOFF_MSPublish limits. See Publish timeouts and retries

TypeScript's connectEnv also takes an environment map in place of process.env. The SDK does not read LASER_SERVER, LASER_TOKEN, LASER_USERNAME, or LASER_PASSWORD. Some examples use them to build LASER_CONNECTION_STRING.

Token or password

Use <token>@host:port for a token or user:pwd@host:port for a username and password. Tokens suit services and CI. A username and password suit local development. Create tokens in API keys.

Both sign in an Iggy user with stream and topic permissions. A permission failure is a typed error. Rust has is_permission_denied(), Python sets permission_denied on the exception, and TypeScript has isPermissionDenied(error).

TLS

For *.laserdata.cloud and *.laserdata.com hosts, the SDK turns on TLS with the bundled LaserData root CA. Other hosts keep the TLS settings of their connection string.

  • LASER_TLS_CERT=<path> or the tls_ca_file= parameter selects your own CA instead of the bundled one.
  • tls_ca_file= turns TLS on by itself. tls=true next to it is allowed, and tls=false next to it is a configuration error.
  • An explicit tls=false without a CA file turns off automatic TLS for LaserData hosts and for LASER_TLS_CERT.
  • LASER_NO_TLS=1 turns off automatic TLS setup. 0 and false do not. It never overrides a tls_ca_file= in the connection string, which still turns TLS on.

Connect timeout

The first connect has a 30 second budget. It covers the TCP connection, the TLS handshake, the login, and the managed capability probe. The Iggy client retries a failed connection by itself until the budget runs out.

Rust builderPython Laser.connectTypeScript builderEnvironment variableDefault
connect_timeout(Duration)connect_timeout_ms=connectTimeout(ms)LASER_CONNECT_TIMEOUT_MS30000

An explicit setting wins over the variable. The budget must be positive and at most 2147483647 milliseconds. An invalid value fails before any connection attempt.

When the budget runs out, the connect fails with a timeout error that names the stage that stopped. the Iggy server to accept the connection means the TCP or TLS handshake did not finish. the Iggy login reply means the transport was up but the server did not answer the login. TypeScript drops the leading the, and for a client you supply it can also report Iggy client readiness.

If only the capability probe is still open when the budget runs out, the connect succeeds with the baseline capability set. capabilities() probes again by itself when the set it holds has no managed plane and its last probe is at least one second old. Callers that ask at the same time share one probe. refresh_capabilities() always probes.

const laser = await Laser.builder()
  .connectionString("iggy:[email protected]:8090")
  .connectTimeout(10_000)
  .connect()
use std::time::Duration;

let laser = Laser::builder()
    .connection_string("iggy:[email protected]:8090")
    .connect_timeout(Duration::from_secs(10))
    .build()
    .await?;
laser = await ls.Laser.connect(
    "iggy:[email protected]:8090",
    connect_timeout_ms=10_000,
)

Reconnection

The connect budget applies only to the first connect. After a lost connection, the client reconnects without a limit by default, once a second, so long-running consumers survive a server restart. After it reconnects, it signs in again with the connection string credentials. Change this with connection string parameters:

user:pwd@host:8090?reconnection_retries=<count|unlimited>&reconnection_interval=<duration>

Durations look like 250ms, 1s, or 1m. The other options are tls, tls_domain, tls_ca_file, heartbeat_interval, reestablish_after, and nodelay. TypeScript checks reestablish_after but its Node transport reconnects on its own schedule.

Publish timeouts and retries

Each publish attempt has a 60 second timeout and up to three more attempts. Retry delays start at 250 milliseconds, double after each failure, and stop at 30 seconds. The timeout bounds one attempt. The transport can report a failure sooner.

SettingRust builderPython Laser.connectTypeScript builderEnvironment variableDefault
Attempt timeoutpublish_timeout(Duration)publish_timeout_ms=publishTimeout(ms)LASER_PUBLISH_TIMEOUT_MS60000
More attemptspublish_max_retries(n)publish_max_retries=publishMaxRetries(n)LASER_PUBLISH_MAX_RETRIES3
First retry delaypublish_retry_backoff(Duration)publish_retry_backoff_ms=publishRetryBackoff(ms)LASER_PUBLISH_RETRY_BACKOFF_MS250

Explicit settings win over the variables. Set retries to zero to turn off automatic resends. The timeout and the delay must be positive and at most 2147483647 milliseconds. Invalid settings fail before the connection opens.

const laser = await Laser.builder()
  .connectionString("iggy:[email protected]:8090")
  .publishTimeout(90_000)
  .publishMaxRetries(5)
  .publishRetryBackoff(500)
  .connect()
use std::time::Duration;

let laser = Laser::builder()
    .connection_string("iggy:[email protected]:8090")
    .publish_timeout(Duration::from_secs(90))
    .publish_max_retries(5)
    .publish_retry_backoff(Duration::from_millis(500))
    .build()
    .await?;
laser = await ls.Laser.connect(
    "iggy:[email protected]:8090",
    publish_timeout_ms=90000,
    publish_max_retries=5,
    publish_retry_backoff_ms=500,
)

The variables apply to the builders and the connect helpers, and to TypeScript's Laser.fromClient. Rust's Laser::from_client uses the built-in defaults because it cannot return an error. To configure a client you supply in Rust, use Laser::builder().client(client). That client must already be connected and signed in.

These settings cover fluent, typed, agent, and direct producer publishes. A producer that sets its own retry count or delay keeps it, and one that leaves them unset inherits the connection values. A background producer returns once its queue accepts the records, so the publish timeout does not cover that step. See Producing at volume.

What triggers a retry

A timeout or a temporary connection failure triggers a retry within the limits. An Unauthenticated reply on a connection that had signed in before also triggers recovery, because a reconnect or a leader change can land on a node that holds no session for the client. The SDK reconnects with the connection string credentials, then retries. Permission errors, rejected credentials, invalid requests, and malformed commit confirmations fail at once.

Reconnecting before a retry uses time from that attempt. TypeScript spends at most half of the attempt on reconnecting, so time is left to send. After an attempt times out, Rust can spend one more timeout period closing the old connection and reconnecting. Rust reconnects the shared client in place, so consumers and reply readers keep their connection. TypeScript closes a failed socket and rejoins registered consumer groups on the new one. A client you supply in TypeScript has no connection recipe, so you must replace it after a failure.

Retries keep message IDs, payloads, headers, and routing. Delivery stays at least once. A lost acknowledgement can deliver a record twice, so effects must be safe to repeat.

Longer outages

When retries run out, Rust returns an error, Python raises a typed exception, and TypeScript rejects the promise. Nothing panics or exits. The error reports what was confirmed and what was not:

LanguageErrorConfirmed rangesRecords without a confirmationOriginal error
RustLaserError::PublishFailedcommittedunconfirmedpublish_cause()
TypeScriptPublishFailedErrorcommittedunconfirmedpublishCause(), or the free publishCause(error)
PythonPublishFailedErrorcommittedunconfirmed__cause__

All three also carry stream and topic. The retryable and permission checks answer for the original error: Rust is_retryable() and is_permission_denied(), TypeScript isRetryable(error) and isPermissionDenied(error), and the Python retryable and permission_denied attributes. A permanent error, such as a missing resource or a denied permission, is not retryable. Do not publish the confirmed records again. See When a publish fails.

The SDK has no durable queue for offline publishes. A long-running service keeps failed work in its own durable queue, slows intake during an outage, and retries later. Keep retry limits finite. Consumer and agent tasks can also finish with an error, so watch their result and restart them.

Managed requests

Managed writes, such as a key-value set, a fork write or a control command, carry an operation ID that stays the same on every attempt, so a retry never applies a change twice. When the server answers that an outcome is still pending, or the connection fails after the request left, the SDK sends the same request again with growing, jittered delays. After 30 seconds without an outcome it returns an ambiguous-mutation error that names the operation. The change may still apply, so check the target state before you start new work.

When the node you are connected to cannot reach its managed backend, it answers PlaneUnavailable instead of a generic error. A client that connected from a connection string then moves its managed requests to another node of the cluster, after checking that the node's backend is ready and speaks the same protocol versions. Writes keep their operation ID across the move. The first read of each kind after a move waits until the new node has caught up with the writes made before it, or answers Stale, which is safe to retry. A client built around your own Iggy client cannot open connections to other nodes, so it keeps retrying on its own connection.

Each key-value, fork, graph and session reply also tells the client how far the answering node has applied writes. The client keeps the furthest point it has seen and sends it with its next read of the same kind, so the node that answers waits until it has caught up that far, or answers Stale. A read never misses your own earlier writes and never shows older data than a read before it, whichever node answers. seen_positions() in Rust and Python and seenPositions() in TypeScript show these points.

Managed readiness

A successful connect gives you Iggy access. Queries, key-value state, forks, graph, and other managed features also need a ready backend, and a connected server can still be replaying managed state.

capabilities() returns the capability snapshot. Refresh it after startup or a backend restart, or wait for readiness with a deadline:

const fresh = await laser.refreshCapabilities()
const ready = await laser.waitUntilReady(30_000)
console.log(fresh.kv.available, ready.query.available)
use std::time::Duration;

let fresh = laser.refresh_capabilities().await;
let ready = laser.wait_until_ready(Duration::from_secs(30)).await?;
println!("{} {}", fresh.kv.available, ready.query.available);
fresh = await laser.refresh_capabilities()
ready = await laser.wait_until_ready(30_000)
print(fresh.kv.available, ready.query.available)

The wait takes milliseconds in TypeScript and Python and a Duration in Rust. It reports unsupported when the server has no managed backend. A configured backend stays unavailable until replay finishes and it reports ready.

Capabilities include operation versions, backend descriptions, readiness reasons, and the hello outcome. is_open_only() reports a server with only baseline capabilities, and serves_consistency(level) reports a supported read consistency. TypeScript has these as free functions: isOpenOnly, servesConsistency, isReady, readinessReasons, enabledBackends, and unreadyBackends.

The SDK never repeats a lease acquisition after an unclear disconnect. See Key-value state in depth.

Default stream

Without a default stream, address topics with the full path, laser.stream(name).topic(name). If most calls use one stream, pick it when you connect:

await using laser = await Laser.connectWithStream("iggy:[email protected]:8090", "telemetry")

// shorthand for laser.stream("telemetry").topic("host-metrics")
const metrics = laser.topic("host-metrics")
let laser = Laser::connect_with_stream("iggy:[email protected]:8090", "telemetry").await?;

// shorthand for laser.stream("telemetry").topic("host-metrics")
let metrics = laser.topic("host-metrics");
laser = await ls.Laser.connect("iggy:[email protected]:8090", stream="telemetry")

# shorthand for laser.stream("telemetry").topic("host-metrics")
metrics = laser.topic("host-metrics")

Python also has connect_with_stream. The builders take the default as stream(name) in Rust and TypeScript. Other streams stay reachable through laser.stream(other).topic(name). Without a default stream, laser.topic(name) fails with a no-stream error. TypeScript throws NoStreamError from topic(name). Rust returns LaserError::NoStream and Python raises NoStreamError, a subclass of ConfigError, when you call an operation on the topic.

with_default_stream(name) in Rust and Python, or withDefaultStream(name) in TypeScript, returns a client with another default that shares the same connection.

Stream-scoped resource names

A client with a default stream scopes the managed resources it names to that stream. laser.kv("profiles") on a client whose default stream is telemetry addresses stream:telemetry/profiles. The same rule covers memory namespaces, lease and fence namespaces, the key registry, graph names, projection and index IDs, query indexes, fork IDs, and the change feed index filter. Each stream keeps its own managed data, and grants can name one stream's resources with a stream:<name>/ prefix.

  • A name that already starts with stream: is sent as is, so you can address another stream's resources.
  • Listings of namespaces, projections, and forks return only your stream's names, without the prefix.
  • Writer schemas are registered and looked up in the default stream's registry.
  • resource_name(name) (TypeScript resourceName) returns the name the client sends. A key-value handle reports its scoped namespace as resource_namespace (TypeScript resourceNamespace), and a fork handle reports its scoped ID as resource_id (TypeScript resourceId).
  • A client without a default stream sends bare names.
  • A record's projection selector stays the local projection ID. The backend matches it against the bindings of the record's own stream, so a record cannot select another stream's projection.
  • Consumer filter names are not scoped. A group's own filter is named from the group's identity, which already includes its stream.

Bare naming opts out and sends every name exactly as written. Use it to read data written under bare names:

const bare = laser.withResourceNaming("bare")
const profiles = bare.kv("profiles") // the namespace "profiles"
let bare = laser.with_resource_naming(ResourceNaming::Bare);
let profiles = bare.kv("profiles"); // the namespace "profiles"
bare = laser.with_resource_naming("bare")
profiles = bare.kv("profiles")  # the namespace "profiles"

# or for the whole connection
laser = await ls.Laser.connect("iggy:[email protected]:8090", stream="telemetry", resource_naming="bare")

The builders also take the choice: resource_naming(ResourceNaming::Bare) in Rust and resourceNaming("bare") in TypeScript. with_resource_naming returns a client that shares the connection.

A deployment that enforces stream scoping reports stream_tenancy in its capabilities (TypeScript streamTenancy). It refuses unscoped names and role grants that span streams, and it publishes each stream's change records on that stream's own change topic.

Close

close() ends the shared connection for every clone of the client. Consumers and reply readers on that connection lose it too. It also closes the fenced-lease connection when the client opened one. You can call it more than once.

TypeScript await using calls close() when the scope of the root client ends. Clones made with withDefaultStream, withResourceNaming, and the other with* methods do nothing on dispose. A client you hand to TypeScript with builder().client(c) or fromClient(c) is borrowed by default, so close() leaves it open. Pass { ownership: "owned" } to let close() destroy it. Python async with does not close the connection on exit, because a clone can still be in use. The connection closes when the last handle is dropped or when you call close().

laser.stream(name).delete() removes a stream with all of its topics and messages. It returns false when the stream did not exist. Each partition costs the server open files and memory, so delete streams you no longer need, especially on small tiers.

On this page