NATS

To define a stream & consumer

import { connect, nanos, AckPolicy } from "nats";


const nc = await connect({ servers: "localhost:4222" })
const jsm = await nc.jetstreamManager();


await jsm.streams.add({ name: "DEPOSITS", subjects: ["deposits.>"] });

await jsm.consumers.add("DEPOSITS", {
  durable_name: "worker",
  ack_policy: AckPolicy.Explicit,
  ack_wait: nanos(10_000),
  max_deliver: 5
})

console.log("Done");
await nc.drain();

nc.drain() here is a graceful shutdown, stop taking new message and stuff.

AckPolicy.Explicit = ack each message individually.
AckPolicy.All = once ack a message, previous messages also acked.

deposits.> means everything after this, says subject could be deposits.something.something_else
To have only 1 thing, we can have deposits.* which match deposits.something but not deposits.something.something_else

This also means our consumer will consume all deposits related event. If we want a specific subject, we can do like so

await jsm.consumers.add("DEPOSITS", {
  durable_name: "bonus-worker",
  filter_subject: "deposits.created",   // only this subject
  ack_policy: AckPolicy.Explicit,
  ack_wait: nanos(10_000),
  max_deliver: 5,
});

To create a worker and consume the stream

import { connect, JSONCodec } from "nats";

import { applyDepositEvent, type DepositEvent } from "./db.js";

const nc = await connect({ servers: "localhost:4222" });
const js = nc.jetstream();
const jc = JSONCodec<DepositEvent>();

const consumer = await js.consumers.get("DEPOSITS", "worker");
const messages = await consumer.consume();

console.log("worker waiting for message...");

for await (const m of messages) {
  if (m.subject !== "deposits.created") { m.term(); continue; }
  
  try {
    const event = jc.decode(m.data);
    if (process.env.FAIL) throw new Error("boom");

    const result = await applyDepositEvent(event);

    console.log(event.eventId, result, `delivery #${m.info.deliveryCount}`);
    m.ack();
  } catch (err) {
    console.error("failed with retry in 5s", err);
    m.nak(5000);
  }
}

We use JSONCodec here since NATs only understand byte, JSONCodec helps to encode JSON → Byte → JSON

m.term() means never send this message to me again

To create a producer produce to stream

import { connect, JSONCodec } from "nats";

const nc = await connect({ servers: "localhost:4222" });
const js = nc.jetstream();
const jc = JSONCodec();

const eventId = process.argv[2] ?? crypto.randomUUID();

const ack = await js.publish(
  "deposits.created",
  jc.encode({ eventId, playerId: "p1", amountCents: 5000 }),
  { msgID: eventId }
)

console.log({ eventId, seq: ack.seq, duplicate: ack.duplicate })

await nc.drain()

Manage and view the stream

To manage and view the stream, use nats cli