mirror of
https://github.com/hanzomsg/nats.js.git
synced 2026-10-30 20:31:36 +00:00
- Replaced `jsr:@std/*` imports with `@std/*` as per deno linter changes. - Updated `@types/node` to `^24.3.1` and `typescript` to `^5.9.2`. - Synchronized dependency versions across modules for alignment. - Adjusted `deno.json` to reflect resolved import paths as per deno linter Signed-off-by: Alberto Ricart <[email protected]>
581 lines
14 KiB
TypeScript
581 lines
14 KiB
TypeScript
/*
|
|
* Copyright 2022-2025 The NATS Authors
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
import { initStream } from "./jstest_util.ts";
|
|
import {
|
|
assertEquals,
|
|
assertExists,
|
|
assertFalse,
|
|
assertRejects,
|
|
assertStringIncludes,
|
|
} from "@std/assert";
|
|
import {
|
|
AckPolicy,
|
|
DeliverPolicy,
|
|
jetstream,
|
|
JetStreamError,
|
|
jetstreamManager,
|
|
} from "../src/mod.ts";
|
|
import type { Consumer, PullOptions } from "../src/mod.ts";
|
|
import { deferred, nanos } from "@nats-io/nats-core";
|
|
import type { NatsConnectionImpl } from "@nats-io/nats-core/internal";
|
|
import { cleanup, jetstreamServerConf, setup } from "test_helpers";
|
|
import type { PullConsumerMessagesImpl } from "../src/consumer.ts";
|
|
import type {
|
|
ConsumerNotification,
|
|
Discard,
|
|
HeartbeatsMissed,
|
|
} from "../src/types.ts";
|
|
|
|
Deno.test("consumers - min supported server", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { stream } = await initStream(nc);
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.consumers.add(stream, {
|
|
name: "a",
|
|
ack_policy: AckPolicy.Explicit,
|
|
});
|
|
|
|
(nc as NatsConnectionImpl).features.update("2.9.2");
|
|
const js = jetstream(nc);
|
|
await assertRejects(
|
|
async () => {
|
|
await js.consumers.get(stream, "a");
|
|
},
|
|
Error,
|
|
"consumers framework is only supported on servers",
|
|
);
|
|
|
|
await assertRejects(
|
|
async () => {
|
|
await js.consumers.get(stream);
|
|
},
|
|
Error,
|
|
"consumers framework is only supported on servers",
|
|
);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - get", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
const js = jetstream(nc);
|
|
await assertRejects(
|
|
async () => {
|
|
await js.consumers.get("a", "b");
|
|
},
|
|
Error,
|
|
"stream not found",
|
|
);
|
|
|
|
const jsm = await jetstreamManager(nc);
|
|
const { stream } = await initStream(nc);
|
|
|
|
await assertRejects(
|
|
async () => {
|
|
await js.consumers.get(stream, "b");
|
|
},
|
|
Error,
|
|
"consumer not found",
|
|
);
|
|
await jsm.consumers.add(stream, { durable_name: "b" });
|
|
|
|
const consumer = await js.consumers.get(stream, "b");
|
|
assertExists(consumer);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - delete", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
const js = jetstream(nc);
|
|
|
|
const { stream } = await initStream(nc);
|
|
const jsm = await jetstreamManager(nc);
|
|
|
|
await jsm.consumers.add(stream, { durable_name: "b" });
|
|
|
|
const c = await js.consumers.get(stream, "b");
|
|
await c.delete();
|
|
|
|
await assertRejects(
|
|
async () => {
|
|
await js.consumers.get(stream, "b");
|
|
},
|
|
Error,
|
|
"consumer not found",
|
|
);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - info", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
|
|
const js = jetstream(nc);
|
|
|
|
const { stream, subj } = await initStream(nc);
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.consumers.add(stream, { durable_name: "b" });
|
|
const c = await js.consumers.get(stream, "b");
|
|
// retrieve the cached consumer - no messages
|
|
const cached = await c.info(false);
|
|
cached.ts = "";
|
|
assertEquals(cached.num_pending, 0);
|
|
const updated = await jsm.consumers.info(stream, "b");
|
|
updated.ts = "";
|
|
assertEquals(updated, cached);
|
|
|
|
// add a message, retrieve the cached one - still not updated
|
|
await js.publish(subj);
|
|
assertEquals(await c.info(true), cached);
|
|
|
|
// update - info and cached copies are updated
|
|
const ci = await c.info();
|
|
assertEquals(ci.num_pending, 1);
|
|
assertEquals((await c.info(true)).num_pending, 1);
|
|
|
|
await assertRejects(
|
|
async () => {
|
|
await jsm.consumers.delete(stream, "b");
|
|
await c.info();
|
|
},
|
|
Error,
|
|
"consumer not found",
|
|
);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - push consumer on get", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
const js = jetstream(nc);
|
|
|
|
const { stream } = await initStream(nc);
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.consumers.add(stream, {
|
|
durable_name: "b",
|
|
deliver_subject: "foo",
|
|
});
|
|
await assertRejects(
|
|
async () => {
|
|
await js.consumers.get(stream, "b");
|
|
},
|
|
Error,
|
|
"not a pull consumer",
|
|
);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - fetch heartbeats", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
const { stream } = await initStream(nc);
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.consumers.add(stream, {
|
|
durable_name: "a",
|
|
ack_policy: AckPolicy.Explicit,
|
|
});
|
|
|
|
const js = jetstream(nc);
|
|
const c = await js.consumers.get(stream, "a");
|
|
const iter = await c.fetch({
|
|
max_messages: 100,
|
|
idle_heartbeat: 1000,
|
|
expires: 30000,
|
|
}) as PullConsumerMessagesImpl;
|
|
|
|
const d = deferred<ConsumerNotification>();
|
|
|
|
await (async () => {
|
|
const status = iter.status();
|
|
for await (const s of status) {
|
|
d.resolve(s);
|
|
break;
|
|
}
|
|
})();
|
|
|
|
await assertRejects(
|
|
async () => {
|
|
for await (const _ of iter) {
|
|
// ignore
|
|
}
|
|
},
|
|
JetStreamError,
|
|
"heartbeats missed",
|
|
);
|
|
|
|
const nci = nc as NatsConnectionImpl;
|
|
// make it not get anything from the server
|
|
nci._resub(iter.sub, "foo");
|
|
|
|
const cs = await d;
|
|
assertEquals(cs.type, "heartbeats_missed");
|
|
assertEquals((cs as HeartbeatsMissed).count, 2);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - bad options", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { stream } = await initStream(nc);
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.consumers.add(stream, {
|
|
name: "a",
|
|
ack_policy: AckPolicy.Explicit,
|
|
});
|
|
|
|
const js = jetstream(nc);
|
|
const c = await js.consumers.get(stream, "a");
|
|
await assertRejects(
|
|
async () => {
|
|
await c.consume({ max_messages: 100, max_bytes: 100 });
|
|
},
|
|
Error,
|
|
"'max_messages','max_bytes' are mutually exclusive",
|
|
);
|
|
|
|
await assertRejects(
|
|
async () => {
|
|
await c.consume({ expires: 500 });
|
|
},
|
|
Error,
|
|
"'expires' must be at least 1000ms",
|
|
);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - should be able to consume and pull", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { stream } = await initStream(nc);
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.consumers.add(stream, {
|
|
name: "a",
|
|
ack_policy: AckPolicy.Explicit,
|
|
});
|
|
|
|
const js = jetstream(nc);
|
|
const c = await js.consumers.get(stream, "a");
|
|
|
|
async function consume(c: Consumer): Promise<void> {
|
|
const iter = await c.consume({ expires: 1500 });
|
|
setTimeout(() => {
|
|
iter.stop();
|
|
}, 1600);
|
|
return (async () => {
|
|
for await (const _r of iter) {
|
|
// ignore
|
|
}
|
|
})().then();
|
|
}
|
|
|
|
async function fetch(c: Consumer): Promise<void> {
|
|
const iter = await c.fetch({ expires: 1500 });
|
|
return (async () => {
|
|
for await (const _r of iter) {
|
|
// ignore
|
|
}
|
|
})().then();
|
|
}
|
|
|
|
await Promise.all([consume(c), consume(c), fetch(c), fetch(c)]);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - discard notifications", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { stream, subj } = await initStream(nc);
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.consumers.add(stream, {
|
|
name: "a",
|
|
ack_policy: AckPolicy.Explicit,
|
|
});
|
|
const js = jetstream(nc);
|
|
await js.publish(subj);
|
|
const c = await js.consumers.get(stream, "a");
|
|
const iter = await c.consume({ expires: 1000, max_messages: 101 });
|
|
(async () => {
|
|
for await (const _r of iter) {
|
|
// nothing
|
|
}
|
|
})().then();
|
|
for await (const s of iter.status()) {
|
|
if (s.type === "discard") {
|
|
assertEquals(s.messagesLeft, 100);
|
|
break;
|
|
}
|
|
}
|
|
iter.stop();
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - threshold_messages", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
const { stream, subj } = await initStream(nc);
|
|
|
|
const proms = [];
|
|
const js = jetstream(nc);
|
|
for (let i = 0; i < 1000; i++) {
|
|
proms.push(js.publish(subj));
|
|
}
|
|
await Promise.all(proms);
|
|
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.consumers.add(stream, {
|
|
name: "a",
|
|
inactive_threshold: nanos(60_000),
|
|
ack_policy: AckPolicy.None,
|
|
});
|
|
|
|
const c = await js.consumers.get(stream, "a");
|
|
const iter = await c.consume({
|
|
expires: 30000,
|
|
}) as PullConsumerMessagesImpl;
|
|
|
|
let next: PullOptions[] = [];
|
|
const done = (async () => {
|
|
for await (const s of iter.status()) {
|
|
if (s.type === "next") {
|
|
next.push(s.options);
|
|
}
|
|
}
|
|
})().then();
|
|
|
|
for await (const m of iter) {
|
|
if (m.info.pending === 0) {
|
|
iter.stop();
|
|
}
|
|
}
|
|
|
|
await done;
|
|
|
|
// stream has 1000 messages, initial pull is default of 100
|
|
assertEquals(next[0].batch, 100);
|
|
next = next.slice(1);
|
|
// we expect 900 messages retrieved in pulls of 24
|
|
// there will be 36 pulls that yield messages, and 4 that don't.
|
|
assertEquals(next.length, 40);
|
|
next.forEach((po) => {
|
|
assertEquals(po.batch, 25);
|
|
});
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - threshold_messages bytes", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
const { stream, subj } = await initStream(nc);
|
|
|
|
const proms = [];
|
|
const js = jetstream(nc);
|
|
for (let i = 0; i < 1000; i++) {
|
|
proms.push(js.publish(subj));
|
|
}
|
|
await Promise.all(proms);
|
|
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.consumers.add(stream, {
|
|
name: "a",
|
|
inactive_threshold: nanos(60_000),
|
|
ack_policy: AckPolicy.None,
|
|
});
|
|
|
|
const a = new Array(1001).fill(false);
|
|
const c = await js.consumers.get(stream, "a");
|
|
const iter = await c.consume({
|
|
expires: 2_000,
|
|
max_bytes: 1100,
|
|
threshold_bytes: 1,
|
|
}) as PullConsumerMessagesImpl;
|
|
|
|
const next: PullOptions[] = [];
|
|
const discards: Discard[] = [];
|
|
const done = (async () => {
|
|
for await (const s of iter.status()) {
|
|
if (s.type === "next") {
|
|
next.push(s.options);
|
|
}
|
|
if (s.type === "discard") {
|
|
discards.push(s);
|
|
}
|
|
}
|
|
})().then();
|
|
|
|
for await (const m of iter) {
|
|
a[m.seq] = true;
|
|
m.ack();
|
|
if (m.info.pending === 0) {
|
|
setTimeout(() => {
|
|
iter.stop();
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
// verify we got seq 1-1000
|
|
for (let i = 1; i < 1001; i++) {
|
|
assertEquals(a[i], true);
|
|
}
|
|
|
|
await done;
|
|
let received = 0;
|
|
for (let i = 0; i < next.length; i++) {
|
|
if (discards[i] === undefined) {
|
|
continue;
|
|
}
|
|
// pull batch - close responses
|
|
received += next[i].batch - discards[i].messagesLeft;
|
|
}
|
|
// FIXME: this is wrong, making so test passes
|
|
assertEquals(received, 996);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - sub leaks fetch()", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
const { stream } = await initStream(nc);
|
|
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.consumers.add(stream, {
|
|
durable_name: stream,
|
|
ack_policy: AckPolicy.Explicit,
|
|
});
|
|
//@ts-ignore: test
|
|
assertEquals(nc.protocol.subscriptions.size(), 1);
|
|
const js = jetstream(nc);
|
|
const c = await js.consumers.get(stream, stream);
|
|
const iter = await c.fetch({ expires: 1000 });
|
|
const done = (async () => {
|
|
for await (const _m of iter) {
|
|
// nothing
|
|
}
|
|
})().then();
|
|
await done;
|
|
//@ts-ignore: test
|
|
assertEquals(nc.protocol.subscriptions.size(), 1);
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - inboxPrefix is respected", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf(), {
|
|
inboxPrefix: "x",
|
|
});
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.streams.add({ name: "messages", subjects: ["hello"] });
|
|
|
|
const js = jetstream(nc);
|
|
|
|
await jsm.consumers.add("messages", {
|
|
durable_name: "c",
|
|
deliver_policy: DeliverPolicy.All,
|
|
ack_policy: AckPolicy.Explicit,
|
|
ack_wait: nanos(3000),
|
|
max_waiting: 500,
|
|
});
|
|
|
|
const consumer = await js.consumers.get("messages", "c");
|
|
const iter = await consumer.consume() as PullConsumerMessagesImpl;
|
|
const done = (async () => {
|
|
for await (const _m of iter) {
|
|
// nothing
|
|
}
|
|
})().catch();
|
|
assertStringIncludes(iter.inbox, "x.");
|
|
iter.stop();
|
|
await done;
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - processed", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf(), {
|
|
inboxPrefix: "x",
|
|
});
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.streams.add({ name: "messages", subjects: ["hello"] });
|
|
|
|
const js = jetstream(nc);
|
|
await js.publish("hello");
|
|
await js.publish("hello");
|
|
|
|
await jsm.consumers.add("messages", {
|
|
durable_name: "c",
|
|
deliver_policy: DeliverPolicy.All,
|
|
ack_policy: AckPolicy.Explicit,
|
|
ack_wait: nanos(3000),
|
|
max_waiting: 500,
|
|
});
|
|
|
|
const consumer = await js.consumers.get("messages", "c");
|
|
const iter = await consumer.consume({
|
|
callback: (m) => {
|
|
m.ack();
|
|
if (m.info.pending === 0) {
|
|
iter.stop();
|
|
}
|
|
},
|
|
});
|
|
|
|
await iter.closed();
|
|
assertEquals(iter.getProcessed(), 2);
|
|
assertEquals(iter.getReceived(), 2);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("consumers - getConsumerFromInfo doesn't do info", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf(), {
|
|
inboxPrefix: "x",
|
|
});
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.streams.add({ name: "messages", subjects: ["hello"] });
|
|
|
|
const js = jetstream(nc);
|
|
await js.publish("hello");
|
|
await js.publish("hello");
|
|
|
|
const ci = await jsm.consumers.add("messages", {
|
|
durable_name: "c",
|
|
deliver_policy: DeliverPolicy.All,
|
|
ack_policy: AckPolicy.Explicit,
|
|
ack_wait: nanos(3000),
|
|
max_waiting: 500,
|
|
});
|
|
|
|
let heardInfo = false;
|
|
nc.subscribe("$JS.API.CONSUMER.INFO.messages.c", {
|
|
callback: () => {
|
|
heardInfo = true;
|
|
},
|
|
});
|
|
|
|
const consumer = js.consumers.getConsumerFromInfo(ci);
|
|
const iter = await consumer.consume({
|
|
callback: (m) => {
|
|
m.ack();
|
|
if (m.info.pending === 0) {
|
|
iter.stop();
|
|
}
|
|
},
|
|
});
|
|
|
|
await iter.closed();
|
|
assertEquals(iter.getProcessed(), 2);
|
|
assertEquals(iter.getReceived(), 2);
|
|
assertFalse(heardInfo);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|