Skip to main content

The Effect surface

The same namespaced route tree returning Effect values instead of promises, with a tagged failure channel and interruption wired to the request.

Overview

The Effect entry has the same route tree and transport as the promise client. Calls return Effect<A, DalpRequestError<Code>> instead of Promise<A>, where Code is the union of error codes that operation can return. Interrupting a fibre aborts a request that uses the client-provided signal.

import { createDalpEffectClient } from "@settlemint/dalp-sdk/effect";
import * as Effect from "effect/Effect";

const dalp = createDalpEffectClient({
  baseUrl: process.env.DALP_API_URL!,
  apiKey: process.env.DALP_API_KEY!,
  runtime: "server",
});

const program = Effect.gen(function* () {
  const tokens = yield* dalp.api.token.list({ page: { limit: 50 } });
  return tokens.data;
});

const rows = await Effect.runPromise(program);

effect is an optional peer dependency, and this entry supports Effect 4. Install the latest Effect 4 release only if you use this entry.

npm install effect@4

Why it is a separate entry

The root package does not import Effect. Import @settlemint/dalp-sdk/effect when your application uses Effect.

The failure channel

Every call fails with one type, DalpRequestError<Code>, carrying the platform's error contract. Let the compiler infer the type from the call so Code stays the operation's own codes:

const program = dalp.api.token
  .list({ page: { limit: 50 } })
  .pipe(Effect.retry({ times: 3, while: (error) => error.retryable }));
Field
What it holds
_tag
"DalpRequestError", so Effect.catchTag matches it by tag.
code
The machine-readable code. A failure with no structured response carries sdk.transport.REQUEST_FAILED.
id
The stable DALP identifier when the platform supplied one, otherwise the SDK's own code.
data
The error's detail map when the platform or the SDK supplied one, for example approvalId on a held write.
status
The HTTP status, when there was a response.
retryable
Whether retrying can succeed. A platform or SDK error carries its own flag, and a 5xx response without one is true. A network failure before any response is true. A missing path parameter, an unknown route id and an aborted request are false.
retryAfterSeconds
How long the platform asked you to wait, from Retry-After, when it named a delay.
routeId
Which route failed, which matters when several are composed in one program.

A bare DalpRequestError types code as the union of every published API and SDK error code (DalpApiErrorCode), never plain string. Each route's own call narrows it further to the error codes that route declares. A failure that never reaches a declared response, such as a network error, carries an SDK code like sdk.transport.REQUEST_FAILED that the route's type does not list, so branch on error.retryable for those and keep the operation's inferred error type instead of annotating a bare DalpRequestError.

Branch on error.id for domain handling and on error.retryable for retry policy. Success responses are checked at runtime as well as typed: a body that does not match the route's schema fails with sdk.response.MALFORMED_SUCCESS instead of arriving as a value the type promises.

Interruption

Interrupting a fibre aborts the request. The abort controller is wired to the fibre rather than handed to you, so a cancelled fibre does not leave a socket open:

import * as Fiber from "effect/Fiber";

const program = Effect.gen(function* () {
  const fiber = yield* Effect.forkChild(dalp.api.token.list({ page: { limit: 50 } }));
  yield* Effect.sleep("2 seconds");
  yield* Fiber.interrupt(fiber); // the HTTP request is aborted
});

If you pass your own signal, manage its cancellation yourself.

Composition

Concurrency, retry, and timeout are Effect's, not the SDK's:

const program = Effect.gen(function* () {
  const [tokens, users] = yield* Effect.all(
    [dalp.api.token.list({ page: { limit: 50 } }), dalp.api.party.users.list({ page: { limit: 50 } })],
    {
      concurrency: 2,
    }
  );
  return { tokens: tokens.data, users: users.data };
});
dalp.api.token
  .list({ page: { limit: 50 } })
  .pipe(
    Effect.timeout("5 seconds"),
    Effect.retry({ times: 3, while: (error) => error._tag === "TimeoutError" || error.retryable })
  );

An Effect.timeout around a call interrupts it, which aborts the request. That composes correctly with settlement: a queue-backed write interrupted mid-wait stops waiting, and the instruction it submitted stays live on the platform exactly as it would if the platform's own wait had run out and answered with a 202 handle. Interrupting the wait is not cancelling the transaction.

Route ids

Dispatch by id, for code that builds its own command surface:

const program = dalp.execute("token/list", { page: { limit: 50 } });

An unknown id fails with a DalpRequestError in the error channel rather than throwing, so the failure stays in the type system with every other failure.

What is the same

The Effect client matches the promise client in the flat argument split, the response envelope, settlement, the acting-context headers and the per-call options, and the pages on the promise surface document each of them once.

The Effect client differs in what the call hands back and in what it leaves out: it exposes only api and execute, with no conditionalRequests handle and no sendParts, streamParts or downloadParts. An Effect program that switches organisation or participant therefore builds a new client rather than clearing held tags.

A streaming route returns an effect that succeeds at once with the async iterable of frames. The request opens on the first iteration, so a refusal or a stream failure is thrown as a DalpApiError while you iterate, not in the effect's error channel. Break out of the loop, or abort a signal you pass, to close the stream.

Next

Command-line access to the same routes: CLI.

Was this page helpful?