SDKs
Effect-first TypeScript SDK with generated operations, authoring DSL, and XState FSMs.
The TypeScript SDK (@strait/ts) is an Effect-first SDK with full API coverage, a code-first authoring DSL, and XState state machines for run lifecycle management.
Installation
npm install @strait/ts
# or
bun add @strait/tsClient Creation
From strait.json (recommended)
import { createClientFromConfigFile } from "@strait/ts/node";
const client = await createClientFromConfigFile();See Configuration for strait.json schema and options.
From environment variables
import { createClientFromEnv } from "@strait/ts/node";
// Reads STRAIT_BASE_URL, STRAIT_API_KEY, STRAIT_AUTH_TYPE, STRAIT_TIMEOUT_MS
const client = createClientFromEnv();Inline
import { createClient } from "@strait/ts";
const client = createClient({
baseUrl: "https://api.strait.dev",
auth: { type: "bearer", token: process.env.STRAIT_API_KEY! },
timeoutMs: 30_000,
});Calling Operations
High-level methods
const job = await client.createJob({
body: {
project_id: "proj_1",
name: "Sync inventory",
slug: "sync-inventory",
endpoint_url: "https://worker.example/jobs/sync",
},
});Namespaced methods
const jobs = await client.jobs.list({ query: { project_id: "proj_1" } });Result variants
Non-GET operations have *Result variants that return { ok, data, error } instead of throwing:
const result = await client.createJobResult({
body: { project_id: "proj_1", name: "...", slug: "...", endpoint_url: "..." },
});
if (!result.ok) {
console.error(result.error);
}Authoring DSL
Defining jobs
import { defineJob, zodSchema } from "@strait/ts";
import { z } from "zod";
const syncInventory = defineJob({
name: "Sync Inventory",
slug: "sync-inventory",
endpointUrl: "https://worker.dev/jobs/sync",
projectId: "proj_1",
schema: zodSchema(z.object({ sku: z.string() })),
maxConcurrency: 5,
maxAttempts: 5,
retryStrategy: "exponential",
timeoutSecs: 300,
run: async (payload, ctx) => {
ctx.logger.info("Starting sync", { sku: payload.sku });
await ctx.reportProgress(0.5);
const result = await fetchInventory(payload.sku);
return { synced: true, count: result.items.length };
},
onSuccess: async ({ output }) => console.log("Synced", output.count, "items"),
onFailure: async ({ error }) => alertOncall(error),
});
// Register, trigger, wait
await syncInventory.register(client);
const run = await syncInventory.trigger(client, { payload: { sku: "ABC-123" } });
const completed = await syncInventory.triggerAndWait(client, { payload: { sku: "ABC-123" } });Defining workflows
import { defineWorkflow, step } from "@strait/ts";
const pipeline = defineWorkflow({
name: "Order Pipeline",
slug: "order-pipeline",
projectId: "proj_1",
steps: [
step.job("validate", "job_validate"),
step.job("charge", "job_charge", { dependsOn: ["validate"] }),
step.approval("review", { dependsOn: ["charge"], approvalTimeoutSecs: 3600 }),
step.waitForEvent("shipping", "shipping.confirmed", { dependsOn: ["review"] }),
step.sleep("cooldown", 60, { dependsOn: ["shipping"] }),
],
});
// DAG is validated at definition timeComposition Helpers
import { withRetry, waitForRun, paginate, collectAll } from "@strait/ts";
// Retry with jitter
const run = await withRetry(
() => client.triggerJob({ pathParams: { jobID: "job_1" }, body: { payload: { sku: "A" } } }),
{ attempts: 5, delayMs: 250, jitter: "full" }
);
// Wait for run completion
await waitForRun(client.getRun, run.id, { timeoutMs: 120_000 });
// Paginate
for await (const r of paginate((q) => client.listRuns({ query: q }))) {
console.log(r.id, r.status);
}
const all = await collectAll(paginate((q) => client.listRuns({ query: q })));FSM State Machines
XState v5 machines for run lifecycle validation and UI state management:
import { canTransitionRun, isTerminalRunStatus, runMachine } from "@strait/ts";
import { createActor } from "xstate";
canTransitionRun("executing", "COMPLETE"); // true
isTerminalRunStatus("completed"); // true
const actor = createActor(runMachine);
actor.start();
actor.send({ type: "ENQUEUE" });
actor.send({ type: "DEQUEUE" });
actor.send({ type: "EXECUTE" });
actor.getSnapshot().value; // "executing"Middleware
const client = createClient({
baseUrl: "https://api.strait.dev",
auth: { type: "bearer", token: "..." },
}, {
middleware: [{
onRequest: ({ method, url }) => console.log(`-> ${method} ${url}`),
onResponse: ({ status, durationMs }) => console.log(`<- ${status} (${durationMs}ms)`),
onError: ({ error }) => console.error(error),
}],
});Error Handling
All errors extend Effect's Data.TaggedError:
import { NotFoundError, UnauthorizedError, RateLimitedError } from "@strait/ts";
try {
await client.getJob({ pathParams: { jobID: "nonexistent" } });
} catch (e) {
if (e instanceof NotFoundError) console.log("Not found:", e.message);
if (e instanceof RateLimitedError) console.log("Rate limited:", e.message);
}| Error | HTTP Status | Description |
|---|---|---|
TransportError | — | Network failure |
DecodeError | — | JSON decode failure |
ValidationError | — | Config/input validation |
UnauthorizedError | 401, 403 | Auth failure |
NotFoundError | 404 | Resource not found |
ConflictError | 409 | Duplicate/conflict |
RateLimitedError | 429 | Rate limit exceeded |
ApiError | other | Generic HTTP error |
TimeoutError | — | Polling timeout |
DagValidationError | — | Invalid workflow DAG |
Was this page helpful?