Jev Events

Questions and events

How choice, noul and score questions become typed events, and how thresholds decide when they fire.

A monitor's questions is an object of questions. Jev answers all of them about each item, in one request. Jev Events re-exports TypeSafe's three question builders, so you don't need a second import.

BuilderAnswersEvents it creates
choice(instructions, labels)Which one label fits, with a probability for every label"<id>:<label>" for each label
noul(instructions, criteria?)The probability that the answer is yes"<id>"
score(instructions, levels)A position on your ordered scale, from 0 to levels − 1"<id>"
questions.ts
import { choice, monitor, noul, score } from "jev-events";
import { twitch } from "@jev-events/twitch";

const chat = monitor({
  source: twitch.chat(),
  questions: {
    // choice: exactly one label wins.
    // Events: "kind:question", "kind:hype" and "kind:other".
    kind: choice("What is this chat message mainly doing?", {
      question: "Asks the streamer a genuine question",
      hype: "Cheers, celebrates or reacts with excitement",
      other: null,
    }),
    // noul: the probability that the answer is yes. Event: "spoiler"
    spoiler: noul("Does this message reveal the ending of Elden Ring?"),
    // score: a position on your own scale, from 0 to 3 here. Event: "toxicity"
    toxicity: score("How toxic is this message toward other people?", [
      "Friendly or neutral",
      "Rude, but not aimed at anyone",
      "Insulting or mean toward someone",
      "Harassment, slurs or threats",
    ]),
  },
});

// Every answer rides along, such as the probability of each kind label.
chat.on("kind:question", (e) => console.log(e.answers.kind.probabilities));
chat.on("spoiler", { min: 0.8 }, (e) => console.log(e.trigger.probability));
chat.on("toxicity", { atLeast: 2 }, (e) => console.log(e.trigger.score));

// @ts-expect-error there is no "kind:spoiler": typos are compile errors
chat.on("kind:spoiler", () => {});

Event names and payloads are inferred from the questions. e.answers.kind.probabilities is typed with your labels, and handling an event that doesn't exist is a compile error, as the last line above shows.

When an event fires

Pass a policy between the event name and the handler:

QuestionDefaultPolicy
choiceFires for the label Jev chose{ min: 0.8 } fires when that label's probability reaches 0.8, even if it wasn't the top choice
noulFires at p ≥ 0.5{ min: 0.9 }
scoreFires at the middle of the scale{ atLeast: 2 }

Pick thresholds by what a mistake costs. A wrong highlight is cheap, so fire early. A wrong timeout is public, so demand more certainty and send the uncertain middle to a person.

The review band

Add review to any policy. When the answer lands between review and min (or atLeast), the handler doesn't run. The monitor emits a review event instead, so a person can decide:

chat.on("hateful", { min: 0.9, review: 0.6 }, twitch.timeout({ seconds: 600 }));
chat.on("review", (e) => modQueue.add(e)); // 0.6 ≤ p < 0.9: ask a human

Special events

Besides outcome events, every monitor emits these:

EventWhenPayload
judgedEvery item Jev answereditem, answers, connection, monitor, latencyMs, usage, cached, dryRun, protected
reviewAn answer fell in a review bandThe same, plus trigger and the handler that didn't run
actionA native action ran, would have run, or was skippedaction, description, status, reason, and the event that triggered it
droppedAn item was skipped before judgingitem, connection, reason: filtered, duplicate, stale, overflow, budget or stopped
errorSomething failed; the monitor keeps goingerror, phase: source, judge, handler or action, and the connection it happened for

Outcome events carry connection too: the account the item came from, so one handler can serve every user. It's undefined for streams that aren't read as anyone, such as a public chat.

An error with fatal: true ended one connection's run, which starts again after a delay. One with needsSignIn: true means the platform signed the user out: the connection is marked needs-sign-in and isn't read again until they reconnect. See managing connections.

Writing good questions

These follow TypeSafe's own guidance, which is worth reading in full.

  • Ask one narrow judgment per question. "Is this hateful?" and "Is this spam?" are two questions, not one choice between them, when a message can be both.
  • Describe every outcome. Labels and criteria are shown to Jev. "Tells the streamer how to play, without being asked" beats "backseat".
  • Leave a way out. Give a choice an other label, so Jev isn't forced into a wrong one.
  • Put the meaning in the question. Question ids like kind are for your code and aren't sent to Jev.
  • Keep math and dates in code. Compute "starts in 20 minutes" or "5th message this minute" yourself and pass it as a fact.
  • Ask independent questions together. They are answered in parallel in the same request and can't see each other's answers.

Or start from a recipe, which already does all of this.

On this page