TypeScript runtime and advanced examples
Build scoped TypeScript workflows with Effect, typed errors, and streams.
The TypeScript SDK uses Effect for operations, streams, typed errors, and scoped resources. SDK calls describe work. They send requests when the Effect runtime executes the workflow.
Use yield* inside Effect.gen to compose SDK operations.
Keep connections and their consumers inside Effect.scoped.
At an async application boundary, run the complete workflow with Effect.runPromise.
See the connection options for supported fields.
Prerequisites
Complete the released SDK installation first.
Use Effect v4 throughout your SDK application.
The portable HTTP modules now live in effect/http; the SDK does not require the Effect v3 @effect/platform package.
Run a managed local server on Node.js
Use @soulfiremc/sdk/node on Node.js 22 or newer.
Start an offline-mode Minecraft server at localhost:25565, then run this file with npx tsx local-effect.ts:
import { NodeRuntime } from "@effect/platform-node";
import { Effect } from "effect";
import { SoulFire } from "@soulfiremc/sdk/node";
const program = Effect.scoped(
Effect.gen(function* () {
const bot = yield* SoulFire.createBot({
server: "localhost:25565",
username: "SoulFireBot",
installation: { version: "2.10.2" },
});
yield* bot.chat.send("Hello from SoulFire");
yield* Effect.logInfo(bot.state.player);
}),
);
NodeRuntime.runMain(program);createBot downloads the backend and Java, provisions the account, starts it, and waits for the initial player snapshot.
Scope exit closes observations and the managed process.
Use SoulFire.install() once and named provisioning when several bots share a backend.
See named instances and bots.
Use @soulfiremc/sdk/bun for managed installation on Bun.
The universal and browser entry points connect to existing backends.
Control managed installation
Downloads and server data stay in .soulfire under the working directory.
Pass these fields through installation in createBot, or directly to install:
directory: choose a persistent data directory.version: pin a released backend instead of selecting the latest release.jarPath: use an existing dedicated JAR, including a source build.javaPath: use an existing Java 25 executable.javaArgs,port,startupTimeoutMs, andonLog: configure the managed process.
Read the installation option reference for all fields.
Connect and send chat
For an existing bot, connect starts it if needed and owns its synchronized observation.
Cleanup stops a bot started by this scope and leaves an already-running bot running.
import { Effect } from "effect";
import { SoulFire } from "@soulfiremc/sdk";
const program = Effect.scoped(
Effect.gen(function* () {
const client = yield* SoulFire.connect({
baseUrl: "https://soulfire.example.com",
token: process.env.SOULFIRE_TOKEN,
});
const bot = client.instance("instance-uuid").bot("bot-uuid");
yield* bot.connect();
yield* bot.chat.send("Hello from SoulFire");
}),
);
await Effect.runPromise(program);The connection handshake checks server compatibility and required capabilities.
For a new account on that backend, use client.createBot or instance.getOrCreateBot.
Provide an Effect v4 HTTP client
Use connectWithHttpClient when your application already supplies an Effect HTTP policy:
import { Effect } from "effect";
import * as FetchHttpClient from "effect/http/FetchHttpClient";
import { SoulFire } from "@soulfiremc/sdk";
const program = Effect.scoped(Effect.gen(function* () {
const client = yield* SoulFire.connectWithHttpClient({
baseUrl: "https://soulfire.example.com",
token: process.env.SOULFIRE_TOKEN,
});
yield* Effect.logInfo(client.server);
}));
await Effect.runPromise(program.pipe(Effect.provide(FetchHttpClient.layer)));The runtime entry points supply their HTTP layer:
@soulfiremc/sdk/nodeuses@effect/platform-node/NodeHttpClient.layerUndiciNoDispatcherwithdispatcherLayerGlobal.@soulfiremc/sdk/browseruseseffect/http/FetchHttpClient.layer.@soulfiremc/sdk/bunuses the same fetch layer over Bun's native implementation.
The universal entry point's connect uses a fetch transport directly.
Its connectWithHttpClient and layerWithHttpClient variants require the application-provided effect/http/HttpClient service.
These variants support tests, workers, Deno, and custom transport policies.
For custom integrations, @soulfiremc/sdk/platform exports makeEffectHttpClientFetch.
Stream bot events
yield* bot.events().pipe(
Stream.runForEach((event) =>
Effect.logInfo("Bot event", { kind: event.event.case })
),
);The stream stays associated with the configured bot across Minecraft
reconnects. Use bot.observe() when the application wants synchronized state
instead of processing every delta itself.
Interact with blocks and beds
Use interactBlock when the intent is to activate a block rather than place
against it. SoulFire checks that the target is loaded and reachable.
yield* bot.interactBlock({
position: buttonPosition,
face: BlockFace.NORTH,
hand: Hand.MAIN,
});
yield* bot.sleep({
bed: bedPosition,
hand: Hand.MAIN,
});
yield* bot.wake();sleep completes after the server confirms the sleeping state. Invalid
targets, daytime restrictions, occupied beds, and rejected interactions
surface as typed action failures.
Run concurrent fleet work
Effect controls concurrency and interruption without losing error types:
yield* Effect.forEach(
botIds,
(botId) =>
soulfire
.instance(instanceId)
.bot(botId)
.chat
.send("Ready"),
{ concurrency: 20, discard: true },
);Integrate with an async application
Convert the complete workflow at the host boundary:
await Effect.runPromise(program, { signal: controller.signal });The signal interrupts the workflow and waits for scoped cleanup.
Use Effect.runPromiseExit to inspect typed failures and defects separately.
Effect provides adapters for host stream APIs:
const readable = Stream.toReadableStream(bot.events());
const events = Stream.toAsyncIterable(bot.events());Consume these adapters while the connection's scope remains open. Closing the scope ends the underlying RPC subscriptions.
Execute production tasks
Recipe inspection and execution are available through bot.recipes:
const recipes = yield* bot.recipes.list({
resultItemId: "minecraft:iron_ingot",
});
const task = yield* bot.recipes.smelt(
{ itemIds: ["minecraft:raw_iron"] },
8,
{
fuel: { tags: ["minecraft:coals"] },
station: furnacePosition,
},
);
yield* task.result();Crafting and smelting run on the SoulFire server as resource-aware tasks. Interrupt the local stream for a call-owned task. Keep the task handle when the task must survive the SDK process.
Provide a shared layer
Define a program dependency
import { Effect } from "effect";
import { SoulFireService } from "@soulfiremc/sdk";
const announce = Effect.gen(function* () {
const soulfire = yield* SoulFireService;
yield* soulfire
.instance(instanceId)
.bot(botId)
.chat
.send("Ready");
});Provide the live client
await Effect.runPromise(
announce.pipe(
Effect.provide(SoulFire.layer({ baseUrl, token })),
),
);Replace that layer in tests to run application logic without a network connection.
Use generated protocol definitions
High-level modules cover normal application work. Generated protobuf services remain available for advanced protocol access:
import {
InstanceService,
} from "@soulfiremc/sdk/generated/soulfire/instance_pb";
const client = soulfire.service(InstanceService);Generated clients expose the wire contract directly. They do not add the validation, task ownership, cleanup, or stable error behavior of the high-level SDK.
Continue with durable tasks or plugin-defined APIs.
How is this page?
Last updated on
