Skip to content

Quickstart

This program asks H3 for one clip, follows it from acceptance to its end, and reads its video as decoded frames in your own process. Without an API key it runs on ReactorTest, Reactor simulated in memory at the timing paid runs measured, so it costs nothing. With a key, only the layer changes: the program is written to run on hosted H3, and its pieces ran in paid checks under Bun, though it has not run there itself.

  1. Start an ES module project. npm 11’s npm init writes "type": "commonjs", under which Node cannot run this program’s import statements, so set it to module:

    Terminal window
    mkdir reactor-quickstart && cd reactor-quickstart
    npm init -y && npm pkg set type=module
  2. Install the client, the native host and Effect, pinned exactly:

    Terminal window
    npm install --save-exact reactor-effect-client reactor-effect-native effect@4.0.0-rc.117 @effect/platform-node@4.0.0-rc.117

    Effect 4 is still in release candidates, and a later candidate can move modules, so every package pins it exactly. Under Bun or pnpm, first add an override to package.json that pins @effect/platform-node-shared to 4.0.0-rc.117 ("overrides" for Bun, "pnpm": { "overrides" } for pnpm); npm needs none here, since reactor-effect-native pins it. Installation covers other hosts.

  3. Save this as main.ts. It is examples/quickstart/src/main.ts in the repository, which CI typechecks against every change.

    main.ts
    import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
    import * as NodeServices from "@effect/platform-node/NodeServices";
    import { Config, Console, Effect, Layer, Option, Redacted, Stream } from "effect";
    import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
    import { CoordinatorClient, H3, Reactor, ReactorTest } from "reactor-effect-client";
    import { NativePeer } from "reactor-effect-native";
    /** One H3 clip from prompt to its end, with its frames decoded in this process. */
    const firstClip = Effect.gen(function* () {
    const coordinator = yield* CoordinatorClient.CoordinatorClient;
    const reactor = yield* Reactor.Reactor;
    const session = yield* reactor.create({
    model: H3.modelName,
    // The session runs on a token of its own, capped so it never bills past two minutes.
    tokens: coordinator.tokens({ modelName: H3.modelName, maxSessionDuration: "2 minutes" }),
    });
    yield* Console.log(`session ${session.id} connected`);
    const h3 = yield* H3.make(session);
    // H3 plays nothing on its own: play each clip as soon as it is ready.
    yield* h3.setAutoplay(true);
    const media = yield* session.decoded;
    yield* media.video("main_video").pipe(
    Stream.filter((frame) => frame.sequence % 24n === 0n),
    Stream.runForEach((frame) =>
    Console.log(`frame ${frame.sequence}: ${frame.width}x${frame.height} ${frame.format}`),
    ),
    Effect.forkScoped,
    );
    const submission = yield* h3.prepare({
    prompt: "A paper boat drifting down a rain-soaked street at dusk, neon reflections",
    seconds: 5,
    });
    const accepted = yield* submission.submit;
    yield* Console.log(`clip ${accepted.clip.clip_id} accepted`);
    const clip = yield* h3.operation(submission);
    yield* clip.reached("started");
    yield* Console.log("playing");
    yield* clip.ended;
    yield* Console.log("ended");
    const report = yield* session.close;
    yield* Console.log(`session closed, termination confirmed: ${report.remote.confirmed}`);
    }).pipe(Effect.scoped);
    /** Reactor simulated in memory, at the timing paid runs measured: no key, nothing billed. */
    const Simulated = Reactor.layer().pipe(
    Layer.provideMerge(CoordinatorClient.layer({ apiKey: Redacted.make("demo") })),
    Layer.provideMerge(
    ReactorTest.layer({
    timing: ReactorTest.Timing.hosted,
    apiKey: "demo",
    width: 320,
    height: 180,
    }),
    ),
    );
    /** Hosted Reactor with the key in REACTOR_API_KEY, frames decoded by libwebrtc in this process. */
    const Hosted = Reactor.layer().pipe(
    Layer.provideMerge(Layer.mergeAll(CoordinatorClient.layerConfig, NativePeer.layer())),
    Layer.provide(FetchHttpClient.layer),
    );
    /** The program is the same either way; only this choice differs. */
    const Reactors = Layer.unwrap(
    Config.option(Config.Redacted("REACTOR_API_KEY")).pipe(
    Effect.map((key) => (Option.isSome(key) ? Hosted : Simulated)),
    ),
    );
    firstClip.pipe(
    // The program's entry point, the one place a layer is provided.
    // @effect-diagnostics-next-line strictEffectProvide:off
    Effect.provide(Layer.mergeAll(Reactors, NodeServices.layer)),
    NodeRuntime.runMain,
    );
  4. Run it offline. Node 22.18 and later run TypeScript directly, and so does Bun:

    Terminal window
    node main.ts
    session sess_reactor_test_1 connected
    clip 00000000-0000-4000-8000-000000000001 accepted
    frame 0: 320x180 BGRA
    playing
    frame 24: 320x180 BGRA
    ...
    ended
    session closed, termination confirmed: true

    About 12 seconds pass: the simulated build takes as long as H3’s did on hosted runs, then the 5-second clip plays.

  5. Run it on hosted H3. Create a key in Reactor’s dashboard and:

    Terminal window
    REACTOR_API_KEY=rk_... node main.ts

    The frames now arrive at 1344x768 from H3, decoded in your process by the native addon over Reactor’s reactor-webrtc crate (libwebrtc).

  • Reactor.create allocated a session and connected it. The session runs on a token minted for it by CoordinatorClient.tokens, never on the API key, and the token caps the session’s length.
  • H3.make read the model’s schema, state and queue. H3 plays nothing until told to, so the program turned autoplay on.
  • prepare and submit enqueued the clip. submit returns H3’s correlated acceptance; if the reply were lost, the SDK would never send the clip again, so a lost reply can never play it twice.
  • h3.operation followed the clip through its facts: generated, started, ended.
  • session.decoded streamed owned BGRA frames into JavaScript. On hosted Reactor they come from reactor-effect-native’s addon over Reactor’s reactor-webrtc crate, with no browser involved.
  • session.close terminated the session and confirmed the termination with an independent read. Effect.scoped guarantees it runs however the program ends, including Ctrl-C.
  • Only the layer differs between offline and hosted. The program itself names no host.