Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Holochain TypeScript Client

Package Versions

@holochain/client   ^0.20.x   (compatible with hdk 0.6.x / hdi 0.7.x)

Connection Setup

import { AppWebsocket, AppAgentWebsocket } from "@holochain/client";

// Basic connection (for simple apps)
const appWs = await AppWebsocket.connect(
  new URL(`ws://localhost:${process.env.HC_PORT}`),
  30000  // timeout ms
);

// Agent-aware connection (recommended — wraps calls with cell context)
const client = await AppAgentWebsocket.connect(
  new URL(`ws://localhost:${process.env.HC_PORT}`),
  "my-app-id"  // Installed app ID
);

callZome Pattern

// Direct AppWebsocket (requires explicit cell_id)
const record = await appWs.callZome({
  cell_id: [dnaHash, agentPubKey],
  zome_name: "my_zome",
  fn_name: "create_my_entry",
  payload: {
    title: "New Entry",
    description: "Created from TypeScript",
    status: "Active",
  },
  cap_secret: null,
  provenance: agentPubKey,
});

// AppAgentWebsocket (cleaner — cell resolved by role name)
const record = await client.callZome({
  role_name: "my_dna",
  zome_name: "my_zome",
  fn_name: "create_my_entry",
  payload: { title: "New Entry", status: "Active" },
});

Signal Subscription

// Subscribe to all signals from the app
appWs.on("signal", (signal) => {
  if (signal.type !== "App") return;  // Filter system signals

  const { zome_name, payload } = signal.data.payload;

  // Discriminate by zome
  if (zome_name === "my_zome") {
    handleMyZomeSignal(payload);
  }
});

// Signal payload matches Rust enum (serde tag = "type")
type MySignal =
  | { type: "EntryCreated"; action: SignedActionHashed }
  | { type: "EntryUpdated"; action: SignedActionHashed; original_action_hash: HoloHash }
  | { type: "EntryDeleted"; action: SignedActionHashed; original_action_hash: HoloHash };

function handleMyZomeSignal(payload: MySignal) {
  switch (payload.type) {
    case "EntryCreated":
      // Refresh entry list
      break;
    case "EntryUpdated":
      // Update specific entry in store
      break;
  }
}

Effect Library Pattern

The Effect library provides typed error handling and timeouts for zome calls:

import * as E from "effect";
import { Effect, pipe } from "effect";

// Typed error
class ZomeCallError {
  readonly _tag = "ZomeCallError";
  constructor(readonly message: string, readonly cause?: unknown) {}
}

// Wrapped zome call with timeout and error handling
function callZomeEffect<T>(params: CallZomeRequest) {
  return pipe(
    E.tryPromise({
      try: () => client.callZome(params) as Promise<T>,
      catch: (cause) => new ZomeCallError(`Zome call failed: ${params.fn_name}`, cause),
    }),
    E.timeout("10 seconds"),
    E.mapError((e) =>
      e._tag === "TimeoutException"
        ? new ZomeCallError(`Zome call timed out: ${params.fn_name}`)
        : e
    )
  );
}

// Usage
const result = await E.runPromise(
  callZomeEffect<MyEntry>({
    role_name: "my_dna",
    zome_name: "my_zome",
    fn_name: "get_my_entry",
    payload: actionHash,
  })
);

Svelte 5 Reactive Store Integration

// stores/myEntry.svelte.ts
import { AppAgentWebsocket } from "@holochain/client";

export class MyEntryStore {
  entries = $state<MyEntry[]>([]);
  loading = $state(false);
  error = $state<string | null>(null);

  private client: AppAgentWebsocket;

  constructor(client: AppAgentWebsocket) {
    this.client = client;

    // Subscribe to signals for real-time updates
    client.on("signal", (signal) => {
      if (signal.type !== "App") return;
      const { zome_name, payload } = signal.data.payload;
      if (zome_name === "my_zome") this.handleSignal(payload);
    });
  }

  async loadAll() {
    this.loading = true;
    try {
      const records = await this.client.callZome({
        role_name: "my_dna",
        zome_name: "my_zome",
        fn_name: "get_all_my_entries",
        payload: null,
      });
      this.entries = records.map(decodeEntry);
    } catch (e) {
      this.error = String(e);
    } finally {
      this.loading = false;
    }
  }

  private handleSignal(signal: MySignal) {
    switch (signal.type) {
      case "EntryCreated":
        this.loadAll();
        break;
      case "EntryDeleted":
        this.entries = this.entries.filter(
          (e) => e.originalHash !== signal.original_action_hash
        );
        break;
    }
  }
}

Type Utilities

import { decodeHashFromBase64, encodeHashToBase64, HoloHash } from "@holochain/client";

// Hash serialization (for URLs, localStorage)
const hashString = encodeHashToBase64(actionHash);
const hashBack = decodeHashFromBase64(hashString);

// Decode entry from record
function decodeEntry<T>(record: Record): T {
  if (!("Present" in record.entry)) {
    throw new Error("Expected Present entry");
  }
  return decode(record.entry.Present.entry) as T;
}

// Extract action hash from record
function getActionHash(record: Record): HoloHash {
  return record.signed_action.hashed.hash;
}

Connection Context (SvelteKit)

// src/lib/holochainClient.ts
import { AppAgentWebsocket } from "@holochain/client";
import { getContext, setContext } from "svelte";

const CLIENT_KEY = Symbol("holochain-client");

export function setHolochainClient(client: AppAgentWebsocket) {
  setContext(CLIENT_KEY, client);
}

export function getHolochainClient(): AppAgentWebsocket {
  const client = getContext<AppAgentWebsocket>(CLIENT_KEY);
  if (!client) throw new Error("Holochain client not initialized");
  return client;
}

// In +layout.svelte:
// const client = await AppAgentWebsocket.connect(...);
// setHolochainClient(client);

Environment Variables

HC_PORT=8888           # Holochain conductor WebSocket port
HC_ADMIN_PORT=9000     # Admin port (for conductor management)
VITE_HC_PORT=8888      # Vite prefix for browser access