mirror of
https://github.com/hanzomsg/nats.js.git
synced 2026-10-30 20:31:36 +00:00
1455 lines
38 KiB
TypeScript
1455 lines
38 KiB
TypeScript
/*
|
|
* Copyright 2021-2023 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 { connect, NatsServer } from "test_helpers";
|
|
|
|
import { initStream } from "./jstest_util.ts";
|
|
import {
|
|
AckPolicy,
|
|
jetstream,
|
|
type JetStreamClient,
|
|
type JetStreamManager,
|
|
jetstreamManager,
|
|
JsHeaders,
|
|
RepublishHeaders,
|
|
RetentionPolicy,
|
|
StorageType,
|
|
} from "../src/mod.ts";
|
|
|
|
import type { Advisory } from "../src/mod.ts";
|
|
import {
|
|
deferred,
|
|
delay,
|
|
Empty,
|
|
headers,
|
|
nanos,
|
|
NoRespondersError,
|
|
nuid,
|
|
RequestError,
|
|
} from "@nats-io/nats-core";
|
|
import {
|
|
assert,
|
|
assertAlmostEquals,
|
|
assertEquals,
|
|
assertExists,
|
|
assertInstanceOf,
|
|
assertIsError,
|
|
assertRejects,
|
|
assertThrows,
|
|
fail,
|
|
} from "@std/assert";
|
|
|
|
import { JetStreamClientImpl, JetStreamManagerImpl } from "../src/jsclient.ts";
|
|
import {
|
|
type BaseApiClientImpl,
|
|
defaultJsOptions,
|
|
} from "../src/jsbaseclient_api.ts";
|
|
import {
|
|
cleanup,
|
|
flakyTest,
|
|
jetstreamServerConf,
|
|
notCompatible,
|
|
setup,
|
|
} from "test_helpers";
|
|
import { PubHeaders } from "../src/jsapi_types.ts";
|
|
import { JetStreamApiError, JetStreamNotEnabled } from "../src/jserrors.ts";
|
|
import { assertBetween } from "../../test_helpers/mod.ts";
|
|
|
|
Deno.test("jetstream - default options", () => {
|
|
const opts = defaultJsOptions();
|
|
assertEquals(opts, { apiPrefix: "$JS.API", timeout: 5000 });
|
|
});
|
|
|
|
Deno.test("jetstream - default override timeout", () => {
|
|
const opts = defaultJsOptions({ timeout: 1000 });
|
|
assertEquals(opts, { apiPrefix: "$JS.API", timeout: 1000 });
|
|
});
|
|
|
|
Deno.test("jetstream - default override prefix", () => {
|
|
const opts = defaultJsOptions({ apiPrefix: "$XX.API" });
|
|
assertEquals(opts, { apiPrefix: "$XX.API", timeout: 5000 });
|
|
});
|
|
|
|
Deno.test("jetstream - options rejects empty prefix", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
assertThrows(() => {
|
|
jetstream(nc, { apiPrefix: "" });
|
|
});
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - options removes trailing dot", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const js = jetstream(nc, { apiPrefix: "hello." }) as JetStreamClientImpl;
|
|
assertEquals(js.opts.apiPrefix, "hello");
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - find stream throws when not found", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const js = jetstream(nc) as JetStreamClientImpl;
|
|
await assertRejects(
|
|
async () => {
|
|
await js.findStream("hello");
|
|
},
|
|
Error,
|
|
"no stream matches subject",
|
|
undefined,
|
|
);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - publish basic", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { stream, subj } = await initStream(nc);
|
|
|
|
const js = jetstream(nc);
|
|
let pa = await js.publish(subj);
|
|
assertEquals(pa.stream, stream);
|
|
assertEquals(pa.duplicate, false);
|
|
assertEquals(pa.seq, 1);
|
|
|
|
pa = await js.publish(subj);
|
|
assertEquals(pa.stream, stream);
|
|
assertEquals(pa.duplicate, false);
|
|
assertEquals(pa.seq, 2);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - ackAck", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { stream, subj } = await initStream(nc);
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.consumers.add(stream, {
|
|
durable_name: "me",
|
|
ack_policy: AckPolicy.Explicit,
|
|
});
|
|
const js = jetstream(nc);
|
|
await js.publish(subj);
|
|
|
|
const c = await js.consumers.get(stream, "me");
|
|
|
|
const ms = await c.next();
|
|
assertExists(ms);
|
|
assertEquals(await ms.ackAck(), true);
|
|
assertEquals(await ms.ackAck(), false);
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - publish id", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { stream, subj } = await initStream(nc);
|
|
|
|
const js = jetstream(nc);
|
|
const pa = await js.publish(subj, Empty, { msgID: "a" });
|
|
assertEquals(pa.stream, stream);
|
|
assertEquals(pa.duplicate, false);
|
|
assertEquals(pa.seq, 1);
|
|
|
|
const jsm = await jetstreamManager(nc);
|
|
const sm = await jsm.streams.getMessage(stream, { seq: 1 });
|
|
assertEquals(sm?.header.get(PubHeaders.MsgIdHdr), "a");
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - publish require stream", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { stream, subj } = await initStream(nc);
|
|
|
|
const js = jetstream(nc);
|
|
await assertRejects(
|
|
async () => {
|
|
await js.publish(subj, Empty, { expect: { streamName: "xxx" } });
|
|
},
|
|
Error,
|
|
"expected stream does not match",
|
|
undefined,
|
|
);
|
|
|
|
const pa = await js.publish(subj, Empty, { expect: { streamName: stream } });
|
|
assertEquals(pa.stream, stream);
|
|
assertEquals(pa.duplicate, false);
|
|
assertEquals(pa.seq, 1);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - publish require last message id", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { stream, subj } = await initStream(nc);
|
|
|
|
const js = jetstream(nc);
|
|
let pa = await js.publish(subj, Empty, { msgID: "a" });
|
|
assertEquals(pa.stream, stream);
|
|
assertEquals(pa.duplicate, false);
|
|
assertEquals(pa.seq, 1);
|
|
|
|
await assertRejects(
|
|
async () => {
|
|
await js.publish(subj, Empty, { msgID: "b", expect: { lastMsgID: "b" } });
|
|
},
|
|
Error,
|
|
"wrong last msg ID: a",
|
|
undefined,
|
|
);
|
|
|
|
pa = await js.publish(subj, Empty, {
|
|
msgID: "b",
|
|
expect: { lastMsgID: "a" },
|
|
});
|
|
assertEquals(pa.stream, stream);
|
|
assertEquals(pa.duplicate, false);
|
|
assertEquals(pa.seq, 2);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - get message last by subject", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
|
|
const jsm = await jetstreamManager(nc);
|
|
const stream = nuid.next();
|
|
await jsm.streams.add({ name: stream, subjects: [`${stream}.*`] });
|
|
|
|
const js = jetstream(nc);
|
|
await js.publish(`${stream}.A`, "a");
|
|
await js.publish(`${stream}.A`, "aa");
|
|
await js.publish(`${stream}.B`, "b");
|
|
await js.publish(`${stream}.B`, "bb");
|
|
|
|
const sm = await jsm.streams.getMessage(stream, {
|
|
last_by_subj: `${stream}.A`,
|
|
});
|
|
assertEquals(sm?.string(), "aa");
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - publish first sequence", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
const { subj } = await initStream(nc);
|
|
|
|
const js = jetstream(nc);
|
|
await js.publish(subj, Empty, { expect: { lastSequence: 0 } });
|
|
await assertRejects(
|
|
async () => {
|
|
await js.publish(subj, Empty, { expect: { lastSequence: 0 } });
|
|
},
|
|
Error,
|
|
"wrong last sequence",
|
|
);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - publish require last sequence", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { stream, subj } = await initStream(nc);
|
|
|
|
const js = jetstream(nc);
|
|
await js.publish(subj, Empty, { expect: { lastSequence: 0 } });
|
|
|
|
await assertRejects(
|
|
async () => {
|
|
await js.publish(subj, Empty, {
|
|
msgID: "b",
|
|
expect: { lastSequence: 2 },
|
|
});
|
|
},
|
|
Error,
|
|
"wrong last sequence: 1",
|
|
undefined,
|
|
);
|
|
|
|
const pa = await js.publish(subj, Empty, {
|
|
msgID: "b",
|
|
expect: { lastSequence: 1 },
|
|
});
|
|
assertEquals(pa.stream, stream);
|
|
assertEquals(pa.duplicate, false);
|
|
assertEquals(pa.seq, 2);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - publish require last sequence by subject", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const jsm = await jetstreamManager(nc);
|
|
const stream = nuid.next();
|
|
await jsm.streams.add({ name: stream, subjects: [`${stream}.*`] });
|
|
|
|
const js = jetstream(nc);
|
|
|
|
await js.publish(`${stream}.A`, Empty);
|
|
await js.publish(`${stream}.B`, Empty);
|
|
const pa = await js.publish(`${stream}.A`, Empty, {
|
|
expect: { lastSubjectSequence: 1 },
|
|
});
|
|
for (let i = 0; i < 100; i++) {
|
|
await js.publish(`${stream}.B`, Empty);
|
|
}
|
|
// this will only succeed if the last recording sequence for the subject matches
|
|
await js.publish(`${stream}.A`, Empty, {
|
|
expect: { lastSubjectSequence: pa.seq },
|
|
});
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - last subject sequence subject", async () => {
|
|
// https://github.com/nats-io/nats-server/blob/47382b1ee49a0dec1c1d8785d54790b39b7a3289/server/jetstream_test.go#L9095
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.streams.add({
|
|
name: "A",
|
|
subjects: [`a.>`],
|
|
max_msgs_per_subject: 1,
|
|
});
|
|
|
|
const js = jsm.jetstream();
|
|
|
|
const r: Record<string, number> = {};
|
|
|
|
async function pub(subj: string, data: string) {
|
|
return await js.publish(subj, data)
|
|
.then((pa) => {
|
|
r[subj] = pa.seq;
|
|
const chunks = subj.split(".");
|
|
chunks[2] = "*";
|
|
r[chunks.join(".")] = pa.seq;
|
|
});
|
|
}
|
|
|
|
await Promise.all([
|
|
pub("a.1.foo", "1:1"),
|
|
pub("a.1.bar", "1:2"),
|
|
pub("a.2.foo", "2:1"),
|
|
pub("a.3.bar", "3:1"),
|
|
pub("a.1.baz", "1:3"),
|
|
pub("a.1.bar", "1:4"),
|
|
pub("a.2.baz", "2:2"),
|
|
]).then(() => {
|
|
console.table(r);
|
|
});
|
|
|
|
async function pc(subj: string, filter: string, seq: number, ok: boolean) {
|
|
await js.publish(subj, "data", {
|
|
expect: { lastSubjectSequence: seq, lastSubjectSequenceSubject: filter },
|
|
}).then((_) => {
|
|
if (!ok) {
|
|
fail("should have not succeeded");
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
if (ok) {
|
|
fail(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ┌─────────┬────────┐
|
|
// │ (idx) │ Values │
|
|
// ├─────────┼────────┤
|
|
// │ a.1.foo │ 1 │
|
|
// │ a.1.* │ 6 │
|
|
// │ a.1.bar │ 6 │
|
|
// │ a.2.foo │ 3 │
|
|
// │ a.2.* │ 7 │
|
|
// │ a.3.bar │ 4 │
|
|
// │ a.3.* │ 4 │
|
|
// │ a.1.baz │ 5 │
|
|
// │ a.2.baz │ 7 │
|
|
// └─────────┴────────┘
|
|
await pc("a.1.foo", "a.1.*", 0, false);
|
|
await pc("a.1.bar", "a.1.*", 0, false);
|
|
await pc("a.1.xxx", "a.1.*", 0, false);
|
|
await pc("a.1.foo", "a.1.*", 1, false);
|
|
await pc("a.1.bar", "a.1.*", 1, false);
|
|
await pc("a.1.xxx", "a.1.*", 1, false);
|
|
await pc("a.2.foo", "a.2.*", 1, false);
|
|
await pc("a.2.bar", "a.2.*", 1, false);
|
|
await pc("a.2.xxx", "a.2.*", 1, false);
|
|
await pc("a.1.bar", "a.1.*", 3, false);
|
|
await pc("a.1.bar", "a.1.*", 4, false);
|
|
await pc("a.1.bar", "a.1.*", 5, false);
|
|
// this inserts seq 8 because a.1.* is at seq 6 (a.2.baz is at 7)
|
|
await pc("a.1.bar", "a.1.*", 6, true);
|
|
// ┌─────────┬────────┐
|
|
// │ (idx) │ Values │
|
|
// ├─────────┼────────┤
|
|
// │ a.1.foo │ 1 │
|
|
// │ a.1.* │ 8 │
|
|
// │ a.1.bar │ 8 │
|
|
// │ a.2.foo │ 3 │
|
|
// │ a.2.* │ 7 │
|
|
// │ a.3.bar │ 4 │
|
|
// │ a.3.* │ 4 │
|
|
// │ a.1.baz │ 5 │
|
|
// │ a.2.baz │ 7 │
|
|
// └─────────┴────────┘
|
|
await pc("a.1.baz", "a.1.*", 2, false);
|
|
await pc("a.1.bar", "a.1.*", 7, false);
|
|
|
|
// this inserts seq 9, because a.1.* is at 8
|
|
await pc("a.1.xxx", "a.1.*", 8, true);
|
|
// ┌─────────┬────────┐
|
|
// │ (idx) │ Values │
|
|
// ├─────────┼────────┤
|
|
// │ a.1.foo │ 1 │
|
|
// │ a.1.* │ 9 │
|
|
// │ a.1.bar │ 8 │
|
|
// │ a.2.foo │ 3 │
|
|
// │ a.2.* │ 7 │
|
|
// │ a.3.bar │ 4 │
|
|
// │ a.3.* │ 4 │
|
|
// │ a.1.baz │ 5 │
|
|
// │ a.2.baz │ 7 │
|
|
// │ a.1.xxx │ 9 │
|
|
// └─────────┴────────┘
|
|
// and so forth...
|
|
await pc("a.2.foo", "a.2.*", 2, false);
|
|
await pc("a.2.foo", "a.2.*", 7, true);
|
|
await pc("a.xxx", "a.*", 0, true);
|
|
await pc("a.xxx", "a.*.*", 0, false);
|
|
await pc("a.3.xxx", "a.3.*", 4, true);
|
|
await pc("a.3.xyz", "a.3.*", 12, true);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - ephemeral options", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { stream } = await initStream(nc);
|
|
const jsm = await jetstreamManager(nc);
|
|
const v = await jsm.consumers.add(stream, {
|
|
inactive_threshold: nanos(1000),
|
|
ack_policy: AckPolicy.Explicit,
|
|
});
|
|
assertEquals(v.config.inactive_threshold, nanos(1000));
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - publish headers", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { stream, subj } = await initStream(nc);
|
|
const js = jetstream(nc);
|
|
const h = headers();
|
|
h.set("a", "b");
|
|
|
|
await js.publish(subj, Empty, { headers: h });
|
|
|
|
const c = await js.consumers.get(stream);
|
|
|
|
const ms = await c.next();
|
|
assertExists(ms);
|
|
ms.ack();
|
|
assertEquals(ms.headers!.get("a"), "b");
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - JSON", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { stream, subj } = await initStream(nc);
|
|
const js = jetstream(nc);
|
|
const values = [null, true, "", ["hello"], { hello: "world" }];
|
|
for (const v of values) {
|
|
await js.publish(subj, JSON.stringify(v));
|
|
}
|
|
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.consumers.add(stream, {
|
|
durable_name: "me",
|
|
ack_policy: AckPolicy.Explicit,
|
|
});
|
|
|
|
const c = await js.consumers.get(stream, "me");
|
|
for (let v of values) {
|
|
const m = await c.next();
|
|
assertExists(m);
|
|
m.ack();
|
|
// JSON doesn't serialize undefines, but if passed to the encoder
|
|
// it becomes a null
|
|
if (v === undefined) {
|
|
v = null;
|
|
}
|
|
assertEquals(m.json(), v);
|
|
}
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - domain", async () => {
|
|
const { ns, nc } = await setup(
|
|
jetstreamServerConf({
|
|
jetstream: {
|
|
domain: "afton",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const jsm = await jetstreamManager(nc, { domain: "afton" });
|
|
const ai = await jsm.getAccountInfo();
|
|
assert(ai.domain, "afton");
|
|
//@ts-ignore: internal use
|
|
assertEquals(jsm.prefix, `$JS.afton.API`);
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - account domain", async () => {
|
|
const conf = jetstreamServerConf({
|
|
jetstream: {
|
|
domain: "A",
|
|
},
|
|
accounts: {
|
|
A: {
|
|
users: [
|
|
{ user: "a", password: "a" },
|
|
],
|
|
jetstream: { max_memory: 10000, max_file: 10000 },
|
|
},
|
|
},
|
|
});
|
|
|
|
const { ns, nc } = await setup(conf, { user: "a", pass: "a" });
|
|
|
|
const jsm = await jetstreamManager(nc, { domain: "A" });
|
|
const ai = await jsm.getAccountInfo();
|
|
assert(ai.domain, "A");
|
|
//@ts-ignore: internal use
|
|
assertEquals(jsm.prefix, `$JS.A.API`);
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - puback domain", async () => {
|
|
const { ns, nc } = await setup(
|
|
jetstreamServerConf({
|
|
jetstream: {
|
|
domain: "A",
|
|
},
|
|
}),
|
|
);
|
|
|
|
if (await notCompatible(ns, nc, "2.3.5")) {
|
|
return;
|
|
}
|
|
|
|
const { subj } = await initStream(nc);
|
|
const js = jetstream(nc);
|
|
const pa = await js.publish(subj);
|
|
assertEquals(pa.domain, "A");
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - source", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
|
|
const stream = nuid.next();
|
|
const subj = `${stream}.*`;
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.streams.add(
|
|
{ name: stream, subjects: [subj] },
|
|
);
|
|
|
|
const js = jetstream(nc);
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
await js.publish(`${stream}.A`);
|
|
await js.publish(`${stream}.B`);
|
|
}
|
|
|
|
await jsm.streams.add({
|
|
name: "work",
|
|
storage: StorageType.File,
|
|
retention: RetentionPolicy.Workqueue,
|
|
sources: [
|
|
{ name: stream, filter_subject: ">" },
|
|
],
|
|
});
|
|
|
|
// source will not process right away?
|
|
await delay(1000);
|
|
|
|
await jsm.consumers.add("work", {
|
|
ack_policy: AckPolicy.Explicit,
|
|
durable_name: "worker",
|
|
filter_subject: `${stream}.B`,
|
|
});
|
|
|
|
const c = await js.consumers.get("work", "worker");
|
|
const iter = await c.fetch({ max_messages: 10 });
|
|
for await (const m of iter) {
|
|
m.ack();
|
|
}
|
|
await nc.flush();
|
|
|
|
const si = await jsm.streams.info("work");
|
|
// stream still has all the 'A' messages
|
|
assertEquals(si.state.messages, 10);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - seal", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
if (await notCompatible(ns, nc, "2.6.2")) {
|
|
return;
|
|
}
|
|
const { stream, subj } = await initStream(nc);
|
|
const js = jetstream(nc);
|
|
await js.publish(subj, "hello");
|
|
await js.publish(subj, "second");
|
|
|
|
const jsm = await jetstreamManager(nc);
|
|
const si = await jsm.streams.info(stream);
|
|
assertEquals(si.config.sealed, false);
|
|
assertEquals(si.config.deny_purge, false);
|
|
assertEquals(si.config.deny_delete, false);
|
|
|
|
await jsm.streams.deleteMessage(stream, 1);
|
|
|
|
si.config.sealed = true;
|
|
const usi = await jsm.streams.update(stream, si.config);
|
|
assertEquals(usi.config.sealed, true);
|
|
|
|
await assertRejects(
|
|
async () => {
|
|
await jsm.streams.deleteMessage(stream, 2);
|
|
},
|
|
Error,
|
|
"invalid operation on sealed stream",
|
|
undefined,
|
|
);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - deny delete", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
if (await notCompatible(ns, nc, "2.6.2")) {
|
|
return;
|
|
}
|
|
const jsm = await jetstreamManager(nc);
|
|
const stream = nuid.next();
|
|
const subj = `${stream}.*`;
|
|
await jsm.streams.add({
|
|
name: stream,
|
|
subjects: [subj],
|
|
deny_delete: true,
|
|
});
|
|
|
|
const js = jetstream(nc);
|
|
await js.publish(subj, "hello");
|
|
await js.publish(subj, "second");
|
|
|
|
const si = await jsm.streams.info(stream);
|
|
assertEquals(si.config.deny_delete, true);
|
|
|
|
await assertRejects(
|
|
async () => {
|
|
await jsm.streams.deleteMessage(stream, 1);
|
|
},
|
|
Error,
|
|
"message delete not permitted",
|
|
undefined,
|
|
);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - deny purge", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
if (await notCompatible(ns, nc, "2.6.2")) {
|
|
return;
|
|
}
|
|
const jsm = await jetstreamManager(nc);
|
|
const stream = nuid.next();
|
|
const subj = `${stream}.*`;
|
|
await jsm.streams.add({
|
|
name: stream,
|
|
subjects: [subj],
|
|
deny_purge: true,
|
|
});
|
|
|
|
const js = jetstream(nc);
|
|
await js.publish(subj, "hello");
|
|
await js.publish(subj, "second");
|
|
|
|
const si = await jsm.streams.info(stream);
|
|
assertEquals(si.config.deny_purge, true);
|
|
|
|
await assertRejects(
|
|
async () => {
|
|
await jsm.streams.purge(stream);
|
|
},
|
|
Error,
|
|
"stream purge not permitted",
|
|
undefined,
|
|
);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - rollup all", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
if (await notCompatible(ns, nc, "2.6.3")) {
|
|
return;
|
|
}
|
|
const jsm = await jetstreamManager(nc);
|
|
const stream = nuid.next();
|
|
const subj = `${stream}.*`;
|
|
await jsm.streams.add({
|
|
name: stream,
|
|
subjects: [subj],
|
|
allow_rollup_hdrs: true,
|
|
});
|
|
|
|
const js = jetstream(nc);
|
|
const buf = [];
|
|
for (let i = 1; i < 11; i++) {
|
|
buf.push(js.publish(`${stream}.A`, JSON.stringify({ value: i })));
|
|
}
|
|
await Promise.all(buf);
|
|
|
|
const h = headers();
|
|
h.set(JsHeaders.RollupHdr, JsHeaders.RollupValueAll);
|
|
await js.publish(`${stream}.summary`, JSON.stringify({ value: 42 }), {
|
|
headers: h,
|
|
});
|
|
|
|
const si = await jsm.streams.info(stream);
|
|
assertEquals(si.state.messages, 1);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - rollup subject", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
if (await notCompatible(ns, nc, "2.6.3")) {
|
|
return;
|
|
}
|
|
const jsm = await jetstreamManager(nc);
|
|
const stream = "S";
|
|
const subj = `${stream}.*`;
|
|
await jsm.streams.add({
|
|
name: stream,
|
|
subjects: [subj],
|
|
allow_rollup_hdrs: true,
|
|
});
|
|
|
|
const js = jetstream(nc);
|
|
const buf = [];
|
|
for (let i = 1; i < 11; i++) {
|
|
buf.push(js.publish(`${stream}.A`, JSON.stringify({ value: i })));
|
|
buf.push(js.publish(`${stream}.B`, JSON.stringify({ value: i })));
|
|
}
|
|
await Promise.all(buf);
|
|
|
|
let si = await jsm.streams.info(stream);
|
|
assertEquals(si.state.messages, 20);
|
|
|
|
let cia = await jsm.consumers.add(stream, {
|
|
durable_name: "dura",
|
|
filter_subject: `${stream}.A`,
|
|
ack_policy: AckPolicy.Explicit,
|
|
});
|
|
assertEquals(cia.num_pending, 10);
|
|
|
|
const h = headers();
|
|
h.set(JsHeaders.RollupHdr, JsHeaders.RollupValueSubject);
|
|
await js.publish(`${stream}.A`, JSON.stringify({ value: 0 }), {
|
|
headers: h,
|
|
});
|
|
|
|
await delay(5000);
|
|
|
|
cia = await jsm.consumers.info(stream, "dura");
|
|
assertEquals(cia.num_pending, 1);
|
|
|
|
si = await jsm.streams.info(stream);
|
|
assertEquals(si.state.messages, 11);
|
|
|
|
const cib = await jsm.consumers.add(stream, {
|
|
durable_name: "durb",
|
|
filter_subject: `${stream}.B`,
|
|
ack_policy: AckPolicy.Explicit,
|
|
});
|
|
assertEquals(cib.num_pending, 10);
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - no rollup", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
if (await notCompatible(ns, nc, "2.6.3")) {
|
|
return;
|
|
}
|
|
const jsm = await jetstreamManager(nc);
|
|
const stream = "S";
|
|
const subj = `${stream}.*`;
|
|
const si = await jsm.streams.add({
|
|
name: stream,
|
|
subjects: [subj],
|
|
allow_rollup_hdrs: false,
|
|
});
|
|
assertEquals(si.config.allow_rollup_hdrs, false);
|
|
|
|
const js = jetstream(nc);
|
|
const buf = [];
|
|
for (let i = 1; i < 11; i++) {
|
|
buf.push(js.publish(`${stream}.A`, JSON.stringify({ value: i })));
|
|
}
|
|
await Promise.all(buf);
|
|
|
|
const h = headers();
|
|
h.set(JsHeaders.RollupHdr, JsHeaders.RollupValueSubject);
|
|
await assertRejects(
|
|
async () => {
|
|
await js.publish(`${stream}.A`, JSON.stringify({ value: 42 }), {
|
|
headers: h,
|
|
});
|
|
},
|
|
Error,
|
|
"rollup not permitted",
|
|
undefined,
|
|
);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - backoff", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
if (await notCompatible(ns, nc, "2.7.2")) {
|
|
return;
|
|
}
|
|
|
|
const { stream, subj } = await initStream(nc);
|
|
const jsm = await jetstreamManager(nc);
|
|
|
|
const ms = [250, 1000, 3000];
|
|
const backoff = ms.map((n) => nanos(n));
|
|
const ci = await jsm.consumers.add(stream, {
|
|
durable_name: "me",
|
|
ack_policy: AckPolicy.Explicit,
|
|
max_deliver: 4,
|
|
backoff,
|
|
});
|
|
|
|
assert(ci.config.backoff);
|
|
assertEquals(ci.config.backoff[0], backoff[0]);
|
|
assertEquals(ci.config.backoff[1], backoff[1]);
|
|
assertEquals(ci.config.backoff[2], backoff[2]);
|
|
|
|
const js = jetstream(nc);
|
|
await js.publish(subj);
|
|
|
|
const when: number[] = [];
|
|
const c = await js.consumers.get(stream, "me");
|
|
const iter = await c.consume({
|
|
callback: (m) => {
|
|
when.push(Date.now());
|
|
if (m.info.deliveryCount === 4) {
|
|
iter.stop();
|
|
}
|
|
},
|
|
});
|
|
|
|
await iter.closed();
|
|
|
|
const offset = when.map((n, idx) => {
|
|
const p = idx > 0 ? idx - 1 : 0;
|
|
return n - when[p];
|
|
});
|
|
|
|
offset.slice(1).forEach((n, idx) => {
|
|
assertAlmostEquals(n, ms[idx], 50);
|
|
});
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - redelivery", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
if (await notCompatible(ns, nc, "2.7.2")) {
|
|
return;
|
|
}
|
|
|
|
const { stream, subj } = await initStream(nc);
|
|
const jsm = await jetstreamManager(nc);
|
|
|
|
const ci = await jsm.consumers.add(stream, {
|
|
durable_name: "me",
|
|
ack_policy: AckPolicy.Explicit,
|
|
max_deliver: 4,
|
|
ack_wait: nanos(1000),
|
|
});
|
|
|
|
assertEquals(ci.config.max_deliver, 4);
|
|
|
|
const js = jetstream(nc);
|
|
await js.publish(subj);
|
|
|
|
const c = await js.consumers.get(stream, "me");
|
|
|
|
let redeliveries = 0;
|
|
const iter = await c.consume({
|
|
callback: (m) => {
|
|
if (m.redelivered) {
|
|
redeliveries++;
|
|
}
|
|
if (m.info.deliveryCount === 4) {
|
|
setTimeout(() => {
|
|
iter.stop();
|
|
}, 2000);
|
|
}
|
|
},
|
|
});
|
|
|
|
await iter.closed();
|
|
assertEquals(redeliveries, 3);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - detailed errors", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const jsm = await jetstreamManager(nc);
|
|
|
|
const ne = await assertRejects(() => {
|
|
return jsm.streams.add({
|
|
name: "test",
|
|
num_replicas: 3,
|
|
subjects: ["foo"],
|
|
});
|
|
}, JetStreamApiError);
|
|
|
|
assertEquals(ne.message, "replicas > 1 not supported in non-clustered mode");
|
|
assertEquals(ne.code, 10074);
|
|
assertEquals(ne.status, 500);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test(
|
|
"jetstream - repub on 503",
|
|
flakyTest(async () => {
|
|
const servers = await NatsServer.setupDataConnCluster(4);
|
|
const nc = await connect({ port: servers[0].port });
|
|
|
|
const { stream, subj } = await initStream(nc, nuid.next(), {
|
|
num_replicas: 3,
|
|
});
|
|
|
|
const jsm = await jetstreamManager(nc);
|
|
const si = await jsm.streams.info(stream);
|
|
const host = si.cluster!.leader || "";
|
|
const leader = servers.find((s) => {
|
|
return s.config.server_name === host;
|
|
});
|
|
|
|
// publish a message
|
|
const js = jetstream(nc);
|
|
const pa = await js.publish(subj);
|
|
assertEquals(pa.stream, stream);
|
|
|
|
// now stop and wait a bit for the servers
|
|
await leader?.stop();
|
|
await delay(1000);
|
|
|
|
await js.publish(subj, Empty, {
|
|
retries: 15,
|
|
timeout: 15000,
|
|
});
|
|
|
|
await nc.close();
|
|
await NatsServer.stopAll(servers, true);
|
|
}),
|
|
);
|
|
|
|
Deno.test("jetstream - duplicate message pub", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
const { subj } = await initStream(nc);
|
|
const js = jetstream(nc);
|
|
|
|
let ack = await js.publish(subj, Empty, { msgID: "x" });
|
|
assertEquals(ack.duplicate, false);
|
|
|
|
ack = await js.publish(subj, Empty, { msgID: "x" });
|
|
assertEquals(ack.duplicate, true);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - republish", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
if (await notCompatible(ns, nc, "2.9.0")) {
|
|
return;
|
|
}
|
|
const jsm = await jetstreamManager(nc);
|
|
const si = await jsm.streams.add({
|
|
name: nuid.next(),
|
|
subjects: ["foo"],
|
|
republish: {
|
|
src: "foo",
|
|
dest: "bar",
|
|
},
|
|
});
|
|
|
|
assertEquals(si.config.republish?.src, "foo");
|
|
assertEquals(si.config.republish?.dest, "bar");
|
|
|
|
const sub = nc.subscribe("bar", { max: 1 });
|
|
const done = (async () => {
|
|
for await (const m of sub) {
|
|
assertEquals(m.subject, "bar");
|
|
assert(m.headers?.get(RepublishHeaders.Subject), "foo");
|
|
assert(m.headers?.get(RepublishHeaders.Sequence), "1");
|
|
assert(m.headers?.get(RepublishHeaders.Stream), si.config.name);
|
|
assert(m.headers?.get(RepublishHeaders.LastSequence), "0");
|
|
}
|
|
})();
|
|
|
|
nc.publish("foo");
|
|
await done;
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - num_replicas consumer option", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
const jsm = await jetstreamManager(nc);
|
|
|
|
const name = nuid.next();
|
|
|
|
// will reject with a replica error (replica value was properly sent)
|
|
await assertRejects(
|
|
() => {
|
|
return jsm.streams.add({
|
|
name,
|
|
subjects: ["foo"],
|
|
num_replicas: 3,
|
|
});
|
|
},
|
|
Error,
|
|
"replicas > 1 not supported in non-clustered mode",
|
|
);
|
|
|
|
// replica of 1
|
|
const si = await jsm.streams.add({
|
|
name,
|
|
subjects: ["foo"],
|
|
});
|
|
assertEquals(si.config.num_replicas, 1);
|
|
|
|
// will reject since the replicas are not enabled - so it was read
|
|
await assertRejects(
|
|
() => {
|
|
return jsm.consumers.add(name, {
|
|
name,
|
|
ack_policy: AckPolicy.Explicit,
|
|
num_replicas: 3,
|
|
});
|
|
},
|
|
Error,
|
|
"replicas > 1 not supported in non-clustered mode",
|
|
);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - filter_subject consumer update", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
if (await notCompatible(ns, nc, "2.9.0")) {
|
|
return;
|
|
}
|
|
|
|
const jsm = await jetstreamManager(nc);
|
|
const si = await jsm.streams.add({ name: nuid.next(), subjects: ["foo.>"] });
|
|
let ci = await jsm.consumers.add(si.config.name, {
|
|
ack_policy: AckPolicy.Explicit,
|
|
filter_subject: "foo.bar",
|
|
durable_name: "a",
|
|
});
|
|
assertEquals(ci.config.filter_subject, "foo.bar");
|
|
|
|
ci.config.filter_subject = "foo.baz";
|
|
ci = await jsm.consumers.update(si.config.name, "a", ci.config);
|
|
assertEquals(ci.config.filter_subject, "foo.baz");
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - jsmsg decode", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
const name = nuid.next();
|
|
const jsm = await jetstreamManager(nc);
|
|
const js = jetstream(nc);
|
|
await jsm.streams.add({ name, subjects: [`a.>`] });
|
|
|
|
await jsm.consumers.add(name, {
|
|
durable_name: "me",
|
|
ack_policy: AckPolicy.Explicit,
|
|
});
|
|
|
|
await js.publish("a.a", "hello");
|
|
await js.publish("a.a", JSON.stringify({ one: "two", a: [1, 2, 3] }));
|
|
|
|
const c = await js.consumers.get(name, "me");
|
|
assertEquals((await c.next())?.string(), "hello");
|
|
assertEquals((await c.next())?.json(), {
|
|
one: "two",
|
|
a: [1, 2, 3],
|
|
});
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - input transform", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
if (await notCompatible(ns, nc, "2.10.0")) {
|
|
return;
|
|
}
|
|
const name = nuid.next();
|
|
const jsm = await jetstreamManager(nc);
|
|
|
|
const si = await jsm.streams.add({
|
|
name,
|
|
subjects: ["foo"],
|
|
subject_transform: {
|
|
src: ">",
|
|
dest: "transformed.>",
|
|
},
|
|
storage: StorageType.Memory,
|
|
});
|
|
|
|
assertEquals(si.config.subject_transform, {
|
|
src: ">",
|
|
dest: "transformed.>",
|
|
});
|
|
|
|
const js = jetstream(nc);
|
|
const pa = await js.publish("foo", Empty);
|
|
assertEquals(pa.seq, 1);
|
|
|
|
const m = await jsm.streams.getMessage(si.config.name, { seq: 1 });
|
|
assertEquals(m?.subject, "transformed.foo");
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - source transforms", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
if (await notCompatible(ns, nc, "2.10.0")) {
|
|
return;
|
|
}
|
|
const jsm = await jetstreamManager(nc);
|
|
|
|
const proms = ["foo", "bar", "baz"].map((subj) => {
|
|
return jsm.streams.add({
|
|
name: subj,
|
|
subjects: [subj],
|
|
storage: StorageType.Memory,
|
|
});
|
|
});
|
|
await Promise.all(proms);
|
|
|
|
const js = jetstream(nc);
|
|
await Promise.all([
|
|
js.publish("foo", Empty),
|
|
js.publish("bar", Empty),
|
|
js.publish("baz", Empty),
|
|
]);
|
|
|
|
await jsm.streams.add({
|
|
name: "sourced",
|
|
storage: StorageType.Memory,
|
|
sources: [
|
|
{ name: "foo", subject_transforms: [{ src: ">", dest: "foo2.>" }] },
|
|
{ name: "bar" },
|
|
{ name: "baz" },
|
|
],
|
|
});
|
|
|
|
while (true) {
|
|
const si = await jsm.streams.info("sourced");
|
|
if (si.state.messages === 3) {
|
|
break;
|
|
}
|
|
await delay(100);
|
|
}
|
|
|
|
const map = new Map<string, string>();
|
|
const oc = await js.consumers.get("sourced");
|
|
const iter = await oc.fetch({ max_messages: 3 });
|
|
for await (const m of iter) {
|
|
map.set(m.subject, m.subject);
|
|
}
|
|
|
|
assert(map.has("foo2.foo"));
|
|
assert(map.has("bar"));
|
|
assert(map.has("baz"));
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - term reason", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
if (await notCompatible(ns, nc, "2.11.0")) {
|
|
return;
|
|
}
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.streams.add({
|
|
name: "foos",
|
|
subjects: ["foo.*"],
|
|
});
|
|
|
|
const js = jetstream(nc);
|
|
|
|
await Promise.all(
|
|
[
|
|
js.publish("foo.1"),
|
|
js.publish("foo.2"),
|
|
js.publish("foo.term"),
|
|
],
|
|
);
|
|
|
|
await jsm.consumers.add("foos", {
|
|
name: "bar",
|
|
ack_policy: AckPolicy.Explicit,
|
|
});
|
|
|
|
const termed = deferred<Advisory>();
|
|
const advisories = jsm.advisories();
|
|
(async () => {
|
|
for await (const a of advisories) {
|
|
if (a.kind === "terminated") {
|
|
termed.resolve(a);
|
|
break;
|
|
}
|
|
}
|
|
})().catch((err) => {
|
|
console.log(err);
|
|
});
|
|
|
|
const c = await js.consumers.get("foos", "bar");
|
|
const iter = await c.consume();
|
|
await (async () => {
|
|
for await (const m of iter) {
|
|
if (m.subject.endsWith(".term")) {
|
|
m.term("requested termination");
|
|
break;
|
|
} else {
|
|
m.ack();
|
|
}
|
|
}
|
|
})().catch();
|
|
|
|
const s = await termed;
|
|
const d = s.data as Record<string, unknown>;
|
|
assertEquals(d.type, "io.nats.jetstream.advisory.v1.terminated");
|
|
assertEquals(d.reason, "requested termination");
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - publish no responder", async (t) => {
|
|
await t.step("not a jetstream server", async () => {
|
|
const { ns, nc } = await setup();
|
|
const js = jetstream(nc);
|
|
const err = await assertRejects(
|
|
() => {
|
|
return js.publish("hello");
|
|
},
|
|
JetStreamNotEnabled,
|
|
);
|
|
|
|
assertInstanceOf(err.cause, RequestError);
|
|
assertInstanceOf(err.cause?.cause, NoRespondersError);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
await t.step("jetstream not listening for subject", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf());
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.streams.add({ name: "s", subjects: ["a", "b"] });
|
|
const js = jetstream(nc);
|
|
const err = await assertRejects(
|
|
() => {
|
|
return js.publish("c");
|
|
},
|
|
JetStreamNotEnabled,
|
|
);
|
|
|
|
assertInstanceOf(err.cause, RequestError);
|
|
assertInstanceOf(err.cause?.cause, NoRespondersError);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
});
|
|
|
|
Deno.test("jetstream - base client timeout", async () => {
|
|
const { ns, nc } = await setup();
|
|
|
|
nc.subscribe("test", { callback: () => {} });
|
|
|
|
async function pub(c: JetStreamClient): Promise<number> {
|
|
const start = Date.now();
|
|
try {
|
|
await c.publish("test", Empty);
|
|
} catch (err) {
|
|
assertIsError(err, Error, "timeout");
|
|
return Date.now() - start;
|
|
}
|
|
throw new Error("should have failed");
|
|
}
|
|
|
|
async function req(c: JetStreamClient): Promise<number> {
|
|
const start = Date.now();
|
|
try {
|
|
await (c as unknown as BaseApiClientImpl)._request("test", Empty);
|
|
} catch (err) {
|
|
assertIsError(err, Error, "timeout");
|
|
return Date.now() - start;
|
|
}
|
|
throw new Error("should have failed");
|
|
}
|
|
|
|
let c = new JetStreamClientImpl(nc) as JetStreamClient;
|
|
assertBetween(await pub(c), 4900, 5100);
|
|
assertBetween(await req(c), 4900, 5100);
|
|
|
|
c = jetstream(nc);
|
|
assertBetween(await pub(c), 4900, 5100);
|
|
assertBetween(await req(c), 4900, 5100);
|
|
|
|
c = new JetStreamClientImpl(nc, { timeout: 500 }) as JetStreamClient;
|
|
assertBetween(await pub(c), 450, 600);
|
|
assertBetween(await req(c), 450, 600);
|
|
|
|
c = jetstream(nc, { timeout: 500 });
|
|
assertBetween(await pub(c), 450, 600);
|
|
assertBetween(await req(c), 450, 600);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - jsm base timeout", async () => {
|
|
const { ns, nc } = await setup();
|
|
|
|
nc.subscribe("test", { callback: () => {} });
|
|
|
|
async function req(c: JetStreamManager): Promise<number> {
|
|
const start = Date.now();
|
|
try {
|
|
await (c as unknown as BaseApiClientImpl)._request("test", Empty);
|
|
} catch (err) {
|
|
assertIsError(err, Error, "timeout");
|
|
return Date.now() - start;
|
|
}
|
|
throw new Error("should have failed");
|
|
}
|
|
|
|
let c = new JetStreamManagerImpl(nc) as JetStreamManager;
|
|
assertBetween(await req(c), 4900, 5100);
|
|
|
|
c = await jetstreamManager(nc, { checkAPI: false });
|
|
assertBetween(await req(c), 4900, 5100);
|
|
|
|
c = new JetStreamManagerImpl(nc, { timeout: 500 }) as JetStreamManager;
|
|
assertBetween(await req(c), 450, 600);
|
|
|
|
c = await jetstreamManager(nc, { timeout: 500, checkAPI: false });
|
|
assertBetween(await req(c), 450, 600);
|
|
|
|
await cleanup(ns, nc);
|
|
});
|
|
|
|
Deno.test("jetstream - watcherPrefix", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
|
|
async function transitive(js: JetStreamClient): Promise<void> {
|
|
const { watcherPrefix } = js.getOptions();
|
|
const jsm = await js.jetstreamManager();
|
|
assertEquals(watcherPrefix, jsm.getOptions().watcherPrefix);
|
|
|
|
const js2 = jsm.jetstream();
|
|
assertEquals(watcherPrefix, js2.getOptions().watcherPrefix);
|
|
}
|
|
|
|
let js = jetstream(nc);
|
|
assertEquals(js.getOptions().watcherPrefix, undefined);
|
|
|
|
const jsm = await jetstreamManager(nc);
|
|
assertEquals(jsm.getOptions().watcherPrefix, undefined);
|
|
|
|
const nc2 = await connect({ port: ns.port, inboxPrefix: "hello" });
|
|
js = jetstream(nc2);
|
|
assertEquals(js.getOptions().watcherPrefix, "hello");
|
|
await transitive(js);
|
|
|
|
js = jetstream(nc2, { watcherPrefix: "bar" });
|
|
assertEquals(js.getOptions().watcherPrefix, "bar");
|
|
await transitive(js);
|
|
|
|
assertThrows(
|
|
() => {
|
|
jetstream(nc2, { watcherPrefix: "hello.*" });
|
|
},
|
|
Error,
|
|
"'prefix' cannot have wildcards ('hello.*')",
|
|
);
|
|
|
|
await assertRejects(
|
|
() => {
|
|
return jetstreamManager(nc, { watcherPrefix: "hello.*" });
|
|
},
|
|
Error,
|
|
"'prefix' cannot have wildcards ('hello.*')",
|
|
);
|
|
|
|
await cleanup(ns, nc, nc2);
|
|
});
|
|
|
|
Deno.test("jetstream - watcher deliver_subject", async () => {
|
|
const { ns, nc } = await setup(jetstreamServerConf({}));
|
|
|
|
const jsm = await jetstreamManager(nc);
|
|
await jsm.streams.add({ name: "A", subjects: ["a"] });
|
|
|
|
async function assertDeliverTo(
|
|
js: JetStreamClient,
|
|
deliverPrefix: string,
|
|
): Promise<void> {
|
|
const pc = await js.consumers.getPushConsumer("A");
|
|
const { config: { deliver_subject } } = await pc.info(true);
|
|
assertEquals(deliver_subject?.split(".")[0], deliverPrefix);
|
|
await pc.delete();
|
|
}
|
|
|
|
// with no prefix, defaults to inbox
|
|
let js = jetstream(nc);
|
|
await assertDeliverTo(js, "_INBOX");
|
|
|
|
// respects inboxPrefix
|
|
const nc2 = await connect({ port: ns.port, inboxPrefix: "hallo" });
|
|
js = jetstream(nc2);
|
|
await assertDeliverTo(js, "hallo");
|
|
|
|
// override inboxPrefix
|
|
js = jetstream(nc2, { watcherPrefix: "hola" });
|
|
await assertDeliverTo(js, "hola");
|
|
|
|
await cleanup(ns, nc, nc2);
|
|
});
|