Docs / Clients

Clients

TypeScript client

The @signalweave/woven-client TypeScript client is Online for the managed browser path. It carries WVN1 over WebTransport and HTTP/3/QUIC to the same managed Woven runtime used by native QUIC clients; WebTransport is a separate browser-facing endpoint, not the native QUIC socket.

Online · browser WebTransport

Supported managed surface

  • WHATWG WebTransport connections through the Host descriptor's webTransport.url
  • Explicit Bearer authentication through WovenClient.connect
  • Optional SHA-256 certificate pinning through webTransport.certificateSha256 and serverCertificateHashes
  • Managed admission with requestAdmission and the queue status, heartbeat, claim, and cancel operations
  • Bounded admission scheduling through admitWithCancellation and an AbortController stop path
  • Space subscription, server-assigned entities, reliable events, receive deadlines, and explicit shutdown
  • Message-only logger.info/warn/error and log(), with session/connection metadata in Host Logs

Install the client

Install the published @signalweave/woven-client 0.3.0 SDK from npm with npm install @signalweave/woven-client@0.3.0. For source-path development instead, use the sibling-checkout commands below and pin a reviewed source revision. The managed browser transport remains WebTransport.

npm --prefix ../woven/crates/woven-client-ts ci
npm --prefix ../woven/crates/woven-client-ts run build
npm install ../woven/crates/woven-client-ts

Host descriptor to client transport

  1. After recent account reauthentication, request POST /v1/servers/:id/connection with the server's current generation. Keep the returned token out of source control, URLs, and logs. It is one shared product-generation possession credential for trusted/internal clients, not end-user identity.
  2. Pass webTransport.url to WovenClient.connect exactly as returned. It is the managed browser endpoint for WVN1 over HTTP/3/QUIC; it reaches the same managed runtime as native QUIC but is not the same socket.
  3. If webTransport.certificateSha256 is present, require exactly 64 lowercase hexadecimal characters, convert each pair to one byte, and pass the resulting 32-byte value as a sha-256 serverCertificateHashes entry. Without a hash, the browser uses its normal Web PKI validation.
  4. Browser JavaScript cannot install an arbitrary caPem trust root. caPem is for native clients; do not convert it into a browser certificate override or disable verification.
  5. Keep namespaceId, sessionId, spaceId, epoch, and channelIds as canonical nonzero decimal u64 strings in JSON, then parse them with BigInt. Never convert protocol scope IDs through Number.

Managed flow

Connect from a Host browser descriptor

import {
  AdmissionStatus,
  AuthenticationScheme,
  MessageKind,
  QueueState,
  WovenClient,
} from "@signalweave/woven-client";

type HostConnectionDescriptor = {
  token: string;
  namespaceId: string;
  sessionId: string;
  webTransport: {
    url: string;
    certificateSha256?: string;
  };
  spaces: Array<
    | {
        kind: "logical";
        spaceId: "1" | "2";
        epoch: "1";
        channelIds: ["1", "4"];
      }
    | {
        kind: "spatialGrid3D";
        slug: string;
        displayName: string;
        spaceId: string;
        epoch: "1";
        channelIds: ["1", "4"];
        metersPerUnit: number;
        cellSize: number;
        interestRadius: number;
        exactDistance: boolean;
        bounds: {
          min: { x: number; y: number; z: number };
          max: { x: number; y: number; z: number };
        };
      }
  >;
};

const MAX_U64 = 18_446_744_073_709_551_615n;

function scopeId(value: string, name: string): bigint {
  if (!/^[1-9][0-9]*$/.test(value)) throw new Error(`invalid ${name}`);
  const parsed = BigInt(value);
  if (parsed > MAX_U64) throw new Error(`${name} exceeds u64`);
  return parsed;
}

function sha256Bytes(hex: string): Uint8Array {
  if (!/^[0-9a-f]{64}$/.test(hex)) {
    throw new Error("certificateSha256 must be 64 lowercase hex characters");
  }
  return Uint8Array.from(
    { length: 32 },
    (_, index) => Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16),
  );
}

const descriptor: HostConnectionDescriptor = await connectionResponse.json();
const certificateSha256 = descriptor.webTransport.certificateSha256;
const client = await WovenClient.connect({
  url: descriptor.webTransport.url,
  token: descriptor.token,
  authenticationScheme: AuthenticationScheme.Bearer,
  webTransportOptions: certificateSha256
    ? {
        serverCertificateHashes: [
          { algorithm: "sha-256", value: sha256Bytes(certificateSha256) },
        ],
      }
    : undefined,
});

const namespaceId = scopeId(descriptor.namespaceId, "namespaceId");
const sessionId = scopeId(descriptor.sessionId, "sessionId");

Wait for managed admission with a stop path

const cancellation = new AbortController();
// Call cancellation.abort() from your application's explicit stop control.
const outcome = await client.admitWithCancellation(
  namespaceId,
  sessionId,
  crypto.randomUUID(),
  60_000,
  cancellation.signal,
);

const admitted =
  outcome.kind === "admission"
    ? outcome.result.status === AdmissionStatus.Admitted
    : outcome.update.state === QueueState.Admitted;
if (!admitted) {
  client.close(1, "not admitted");
  throw new Error("managed admission did not admit this connection");
}

Subscribe to channel 1, receive entity assignment, then publish a reliable event

const space = descriptor.spaces[0];
if (!space) throw new Error("descriptor has no spaces");
const channel = "1";
if (!space.channelIds.includes(channel)) {
  throw new Error("channel 1 is not allowed for this space");
}

const spaceId = scopeId(space.spaceId, "spaceId");
const spaceEpoch = scopeId(space.epoch, "spaceEpoch");
const channelId = scopeId(channel, "channelId");

// Managed admission has already returned Admitted before this subscription.
await client.subscribeSpace(namespaceId, sessionId, spaceId, spaceEpoch, channelId);

// Treat SubscriptionRejected as fatal. Publish only after SubscriptionAccepted
// is followed by EntityEntered with the server-assigned entity ID.
const subscriptionReply = await client.recvTimeout(2_000);
if (subscriptionReply?.messageKind !== MessageKind.SubscriptionAccepted) {
  throw new Error("subscription was not accepted");
}
const entityAssignment = await client.recvTimeout(2_000);
if (entityAssignment?.messageKind !== MessageKind.EntityEntered) {
  throw new Error("server did not assign an entity");
}
const entityId = entityAssignment.entityId;
if (entityId === null) throw new Error("entity assignment omitted its entity ID");

await client.publishEvent(
  namespaceId,
  sessionId,
  spaceId,
  spaceEpoch,
  channelId,
  entityId,
  1n, // sender sequence; increment for each event in this scope
  1n, // application-defined payload type ID
  new TextEncoder().encode("hello from Woven Host"),
);

client.close();

Send a message to Host Logs

// After managed admission returns Admitted; no space subscription is needed.
await client.logger.info("Scene loaded");
await client.logger.warn("Optional asset unavailable");
await client.logger.error("Failed to save progress");
await client.log("Info-level alias");

// Await sequentially: these share the existing ordered control stream.
// Sending is not database acknowledgement; keep receiving server rejections.

Cancellation and bounds

  • WovenClient.connect bounds WebTransport readiness to 10 seconds by default and completes the WVN1 Hello, Capabilities, Authenticate, and Authenticated handshake with explicit Bearer credentials.
  • admitWithCancellation requires a positive timeout of at most 15 minutes, uses bounded exchanges and polling, and closes the connection on abort, deadline, transport failure, or protocol mismatch. Create a fresh client before retrying after cancellation.
  • Admission and queue rejections are returned as semantic data; the helper performs no transport retries. Do not silently replay a partial stream exchange.
  • Subscribe only after an Admitted outcome. Publish only after SubscriptionAccepted and EntityEntered provide the server-assigned entity ID, and keep sender sequences strictly increasing.
  • The optional certificateSha256 pin must decode to exactly 32 bytes. Browser code cannot add arbitrary CA PEM trust roots; use the descriptor's browser URL and normal Web PKI when no certificate hash is supplied.
  • Bound application concurrency, publish rate, payload size, receive waits, and shutdown behavior. Keep all protocol IDs as bigint so u64 values remain exact.
  • Credential rotation and short-lived browser grants are not implemented. If this shared token may be compromised, delete and recreate the product before reconnecting trusted clients.
  • After admission, await client.logger.info/warn/error(message) sequentially. Messages are limited to 1,024 UTF-8 bytes and 10 per connection per second; no tokens or sensitive data. Host keeps the latest 200 events per managed server, including connections/disconnections, not noisy state updates. Sending is not database acknowledgement, and collection is eventual and best-effort.

Availability

Online for managed browser WebTransport. Use only the endpoint, certificate metadata, logical spaces, and managed spatial definitions returned by Host; persistence beyond Ephemeral and Inference remain Preview.