Skip to content

Effect Helpers

withPromise() bridges a promise to actor events. withTimeout() arms a timeout that fires an event. Both check signal.aborted before emitting, so promises resolving after state exit and timers firing after exit produce no side effects. Both use the actor’s clock, so tests stay deterministic on VirtualClock.

withPromise(promise, signal, emit, {
success: (data) => ({ type: "paymentOk", payload: data }),
error: (err) => ({ type: "paymentFail", payload: err }),
});
  • Awaits the promise
  • On resolve: calls success, emits resulting event
  • On reject: calls error, emits resulting event
  • Checks signal.aborted before each emit
  • Mapper return value must be { type, payload? } envelope

Sugar equivalent:

setup: (m) => {
m.effect(submitting, {
name: "chargeCard",
fn: (input) => {
const s = input.context.get();
withPromise(chargeCard(s.paymentInfo!.cardNumber), input.signal, input.emit, {
success: (orderId) => paymentOk.create({ orderId }),
error: (reason) => paymentFail.create({ reason: String(reason) }),
});
},
});
};
withTimeout(ms, input, () => submittingDone.create());
  • Schedules timeout on the actor’s clock
  • On fire: checks signal.aborted, calls eventFn, emits resulting event
  • State exit aborts the effect signal, no emit after exit
  • Warns on invalid ms

Equivalent without sugar:

m.effect(submitting, ({ clock, signal, emit }) => {
clock.setTimeout(800, () => {
if (!signal.aborted) emit(submittingDone.create());
});
});
import { checkout } from "../checkout.ts";
import { VirtualClock } from "@mantaq/core";
const clock = VirtualClock();
checkout.send(submitPayment.create());
clock.advance(800); // triggers timeout synchronously
matches(checkout, "success"); // true

Same clock, same inputs, same trace.