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.
-
Start an ES module project. npm 11’s
npm initwrites"type": "commonjs", under which Node cannot run this program’simportstatements, so set it tomodule:Terminal window mkdir reactor-quickstart && cd reactor-quickstartnpm init -y && npm pkg set type=module -
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.117Terminal window bun add --exact reactor-effect-client reactor-effect-native effect@4.0.0-rc.117 @effect/platform-node@4.0.0-rc.117Terminal window pnpm add --save-exact reactor-effect-client reactor-effect-native effect@4.0.0-rc.117 @effect/platform-node@4.0.0-rc.117Effect 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.jsonthat pins@effect/platform-node-sharedto4.0.0-rc.117("overrides"for Bun,"pnpm": { "overrides" }for pnpm); npm needs none here, sincereactor-effect-nativepins it. Installation covers other hosts. -
Save this as
main.ts. It isexamples/quickstart/src/main.tsin 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:offEffect.provide(Layer.mergeAll(Reactors, NodeServices.layer)),NodeRuntime.runMain,); -
Run it offline. Node 22.18 and later run TypeScript directly, and so does Bun:
Terminal window node main.tssession sess_reactor_test_1 connectedclip 00000000-0000-4000-8000-000000000001 acceptedframe 0: 320x180 BGRAplayingframe 24: 320x180 BGRA...endedsession closed, termination confirmed: trueAbout 12 seconds pass: the simulated build takes as long as H3’s did on hosted runs, then the 5-second clip plays.
-
Run it on hosted H3. Create a key in Reactor’s dashboard and:
Terminal window REACTOR_API_KEY=rk_... node main.tsThe frames now arrive at 1344x768 from H3, decoded in your process by the native addon over Reactor’s
reactor-webrtccrate (libwebrtc).
What just happened
Section titled “What just happened”Reactor.createallocated a session and connected it. The session runs on a token minted for it byCoordinatorClient.tokens, never on the API key, and the token caps the session’s length.H3.makeread the model’s schema, state and queue. H3 plays nothing until told to, so the program turned autoplay on.prepareandsubmitenqueued the clip.submitreturns 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.operationfollowed the clip through its facts: generated, started, ended.session.decodedstreamed owned BGRA frames into JavaScript. On hosted Reactor they come fromreactor-effect-native’s addon over Reactor’sreactor-webrtccrate, with no browser involved.session.closeterminated the session and confirmed the termination with an independent read.Effect.scopedguarantees it runs however the program ends, including Ctrl-C.- Only the layer differs between offline and hosted. The program itself names no host.
- Watch the frames instead of counting them: the terminal viewer draws H3 in your terminal.
- Keep H3 on air across session caps: Playout and the 24/7 channel guide.
- Test without paying: Test offline with ReactorTest.