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 source-compatible SDK installation first. The snippets here demonstrate runtime integration, not a separate first-run procedure.
Effect and @effect/platform are peer dependencies.
Use one compatible Effect runtime in the application.
Run a managed local server on Node.js
Local JVM download and process management use explicit Node.js entry points and
require Node.js 22 or newer.
The example creates or reuses an instance and an offline bot.
Run an offline-mode Minecraft server at 127.0.0.1:25565 before the program.
For this source baseline, set SOULFIRE_JAR to a matching dedicated JAR before running the example.
The source installer accepts jarPath, so it can use a source build without a published release.
Without jarPath, install() downloads a release. The registry's latest release can have an incompatible API.
import { Effect, Stream } from "effect";
import { AccountTypeCredentials } from "@soulfiremc/sdk";
import { SoulFire } from "@soulfiremc/sdk/node";
const program = Effect.scoped(
Effect.gen(function* () {
const jarPath = process.env.SOULFIRE_JAR;
if (!jarPath) throw new Error("Set SOULFIRE_JAR to a compatible dedicated JAR");
const soulfire = yield* SoulFire.install({ jarPath });
const name = "Hello World";
const existing = (yield* soulfire.instances()).find(
(item) => item.friendlyName === name,
);
const instance = existing
? soulfire.instance(existing.id)
: yield* soulfire.createInstance(name);
const username = "SoulFireBot";
let botId = (yield* instance.bots()).find(
(item) => item.accountName === username,
)?.profileId;
if (!botId) {
const results = yield* instance.loginCredentials({
service: AccountTypeCredentials.OFFLINE,
payload: [username],
}).pipe(Stream.runCollect);
for (const result of results) {
if (result.data.case === "oneSuccess" && result.data.value.account) {
const account = result.data.value.account;
yield* instance.addAccounts([account]);
botId = account.profileId;
}
}
}
if (!botId) throw new Error("Could not create an offline bot");
const bot = instance.bot(botId);
yield* bot.start();
yield* bot.waitForOnline();
yield* bot.chat.send("Hello from SoulFire");
yield* bot.stop();
}),
);
await Effect.runPromise(program);Use @soulfiremc/sdk/bun for managed installation on Bun.
The universal entry point keeps process and filesystem modules out of browser and worker bundles.
Run bunx tsx local-effect.ts after installation and setting SOULFIRE_JAR.
Expect the greeting in Minecraft chat and the managed backend to stop at scope completion.
Read the source build instructions before packaging a dedicated source JAR.
Connect and send chat
import { Effect } from "effect";
import { SoulFire } from "@soulfiremc/sdk";
const program = Effect.scoped(
Effect.gen(function* () {
const soulfire = yield* SoulFire.connect({
baseUrl: "https://soulfire.example.com",
token: process.env.SOULFIRE_TOKEN,
});
const bot = soulfire.instance("instance-uuid").bot("bot-uuid");
yield* bot.start();
yield* bot.waitForOnline();
yield* bot.chat.send("Hello from SoulFire");
yield* bot.stop();
}),
);
await Effect.runPromise(program);The connection handshake checks server compatibility and required capabilities before the client becomes available.
Use Effect Platform
@effect/platform is the general compatibility layer for Effect. SoulFire can
consume its portable HttpClient service instead of relying on a global
fetch:
const client = yield* SoulFire.connectWithHttpClient({
baseUrl: "https://soulfire.example.com",
token,
});Use the runtime entry point when SoulFire provides the HTTP layer:
@soulfiremc/sdk/nodeuses@effect/platform-node/NodeHttpClient.layerUndiciWithoutDispatcherwithdispatcherLayerGlobal.@soulfiremc/sdk/browseruses@effect/platform/FetchHttpClient.@soulfiremc/sdk/bunuses the same portable fetch layer over Bun's native Fetch implementation.
The universal entry point keeps HttpClient as an application-provided
service. This is useful in tests, workers, Deno, and applications with a
custom client policy. Runtime-specific filesystem, process, socket, and worker
services can come from @effect/platform-browser, @effect/platform-node,
or @effect/platform-bun when the application needs them.
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
