Files

1817 lines
43 KiB
TypeScript

/*
* Copyright 2020-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 {
assert,
assertEquals,
assertExists,
assertInstanceOf,
assertRejects,
assertThrows,
fail,
} from "@std/assert";
import {
collect,
createInbox,
deferred,
delay,
Empty,
Feature,
headers,
isIP,
nuid,
syncIterator,
} from "../src/internal_mod.ts";
import type {
ConnectionClosedListener,
Deferred,
Msg,
MsgHdrs,
MsgImpl,
NatsConnectionImpl,
Payload,
Publisher,
PublishOptions,
SubscriptionImpl,
} from "../src/internal_mod.ts";
import { cleanup, Lock, NatsServer, setup } from "test_helpers";
import { connect } from "./connect.ts";
import { errors } from "../src/errors.ts";
Deno.test("basics - connect port", async () => {
const ns = await NatsServer.start();
const nc = await connect({ port: ns.port });
await cleanup(ns, nc);
});
Deno.test("basics - connect default", async () => {
const ns = await NatsServer.start({ port: 4222 });
const nc = await connect({});
await cleanup(ns, nc);
});
Deno.test("basics - connect host", async () => {
const nc = await connect({ servers: "demo.nats.io" });
await nc.close();
});
Deno.test("basics - connect hostport", async () => {
const nc = await connect({ servers: "demo.nats.io:4222" });
await nc.close();
});
Deno.test("basics - scott", async () => {
const nc = await connect({ servers: "demo.nats.io:4222", debug: true });
await nc.close();
});
Deno.test("basics - connect servers", async () => {
const ns = await NatsServer.start();
const nc = await connect({ servers: [`${ns.hostname}:${ns.port}`] });
await cleanup(ns, nc);
});
Deno.test("basics - fail connect", async () => {
await assertRejects(
() => {
return connect({ servers: `127.0.0.1:32001` });
},
errors.ConnectionError,
"connection refused",
);
});
Deno.test("basics - publish", async () => {
const { ns, nc } = await setup();
nc.publish(createInbox());
await nc.flush();
await cleanup(ns, nc);
});
Deno.test("basics - no publish without subject", async () => {
const { ns, nc } = await setup();
assertThrows(
() => {
nc.publish("");
},
errors.InvalidSubjectError,
"illegal subject: ''",
);
await cleanup(ns, nc);
});
Deno.test("basics - pubsub", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj);
const iter = (async () => {
for await (const _m of sub) {
break;
}
})();
nc.publish(subj);
await iter;
assertEquals(sub.getProcessed(), 1);
await cleanup(ns, nc);
});
Deno.test("basics - subscribe and unsubscribe", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
const sub = nc.subscribe(subj, { max: 1000, queue: "aaa" });
// check the subscription
assertEquals(nci.protocol.subscriptions.size(), 1);
let s = nci.protocol.subscriptions.get(1);
assert(s);
assertEquals(s.getReceived(), 0);
assertEquals(s.subject, subj);
assert(s.callback);
assertEquals(s.max, 1000);
assertEquals(s.queue, "aaa");
// modify the subscription
sub.unsubscribe(10);
s = nci.protocol.subscriptions.get(1);
assert(s);
assertEquals(s.max, 10);
// verify subscription updates on message
nc.publish(subj);
await nc.flush();
s = nci.protocol.subscriptions.get(1);
assert(s);
assertEquals(s.getReceived(), 1);
// verify cleanup
sub.unsubscribe();
assertEquals(nci.protocol.subscriptions.size(), 0);
await cleanup(ns, nc);
});
Deno.test("basics - subscriptions iterate", async () => {
const { ns, nc } = await setup();
const lock = Lock();
const subj = createInbox();
const sub = nc.subscribe(subj);
(async () => {
for await (const _m of sub) {
lock.unlock();
}
})().then();
nc.publish(subj);
await nc.flush();
await lock;
await cleanup(ns, nc);
});
Deno.test("basics - subscriptions pass exact subject to cb", async () => {
const { ns, nc } = await setup();
const s = createInbox();
const subj = `${s}.foo.bar.baz`;
const sub = nc.subscribe(`${s}.*.*.*`);
const sp = deferred<string>();
(async () => {
for await (const m of sub) {
sp.resolve(m.subject);
break;
}
})().then();
nc.publish(subj);
assertEquals(await sp, subj);
await cleanup(ns, nc);
});
Deno.test("basics - subscribe returns Subscription", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj);
assertEquals(sub.getID(), 1);
await cleanup(ns, nc);
});
Deno.test("basics - wildcard subscriptions", async () => {
const { ns, nc } = await setup();
const single = 3;
const partial = 2;
const full = 5;
const s = createInbox();
const sub = nc.subscribe(`${s}.*`);
const sub2 = nc.subscribe(`${s}.foo.bar.*`);
const sub3 = nc.subscribe(`${s}.foo.>`);
nc.publish(`${s}.bar`);
nc.publish(`${s}.baz`);
nc.publish(`${s}.foo.bar.1`);
nc.publish(`${s}.foo.bar.2`);
nc.publish(`${s}.foo.baz.3`);
nc.publish(`${s}.foo.baz.foo`);
nc.publish(`${s}.foo.baz`);
nc.publish(`${s}.foo`);
await nc.drain();
assertEquals(sub.getReceived(), single, "single");
assertEquals(sub2.getReceived(), partial, "partial");
assertEquals(sub3.getReceived(), full, "full");
await ns.stop();
});
Deno.test("basics - correct data in message", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const mp = deferred<Msg>();
const sub = nc.subscribe(subj);
(async () => {
for await (const m of sub) {
mp.resolve(m);
break;
}
})().then();
nc.publish(subj, subj);
const m = await mp;
assertEquals(m.subject, subj);
assertEquals(m.string(), subj);
assertEquals(m.reply, "");
await cleanup(ns, nc);
});
Deno.test("basics - correct reply in message", async () => {
const { ns, nc } = await setup();
const s = createInbox();
const r = createInbox();
const rp = deferred<string>();
const sub = nc.subscribe(s);
(async () => {
for await (const m of sub) {
rp.resolve(m.reply);
break;
}
})().then();
nc.publish(s, Empty, { reply: r });
assertEquals(await rp, r);
await cleanup(ns, nc);
});
Deno.test("basics - respond returns false if no reply subject set", async () => {
const { ns, nc } = await setup();
const s = createInbox();
const dr = deferred<boolean>();
const sub = nc.subscribe(s);
(async () => {
for await (const m of sub) {
dr.resolve(m.respond());
break;
}
})().then();
nc.publish(s);
const failed = await dr;
assert(!failed);
await cleanup(ns, nc);
});
Deno.test("basics - closed cannot subscribe", async () => {
const { ns, nc } = await setup();
await nc.close();
let failed = false;
try {
nc.subscribe(createInbox());
fail("should have not been able to subscribe");
} catch (_err) {
failed = true;
}
assert(failed);
await ns.stop();
});
Deno.test("basics - close cannot request", async () => {
const { ns, nc } = await setup();
await nc.close();
let failed = false;
try {
await nc.request(createInbox());
fail("should have not been able to request");
} catch (_err) {
failed = true;
}
assert(failed);
await ns.stop();
});
Deno.test("basics - flush returns promise", async () => {
const { ns, nc } = await setup();
const p = nc.flush();
if (!p) {
fail("should have returned a promise");
}
await p;
await cleanup(ns, nc);
});
Deno.test("basics - unsubscribe after close", async () => {
const { ns, nc } = await setup();
const sub = nc.subscribe(createInbox());
await nc.close();
sub.unsubscribe();
await ns.stop();
});
Deno.test("basics - unsubscribe stops messages", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
// in this case we use a callback otherwise messages are buffered.
const sub = nc.subscribe(subj, {
callback: () => {
sub.unsubscribe();
},
});
nc.publish(subj);
nc.publish(subj);
nc.publish(subj);
nc.publish(subj);
await nc.flush();
assertEquals(sub.getReceived(), 1);
await cleanup(ns, nc);
});
Deno.test("basics - request", async () => {
const { ns, nc } = await setup();
const s = createInbox();
const sub = nc.subscribe(s);
(async () => {
for await (const m of sub) {
m.respond("foo");
}
})().then();
const msg = await nc.request(s);
assertEquals(msg.string(), "foo");
await cleanup(ns, nc);
});
Deno.test("basics - request no responders", async () => {
const { ns, nc } = await setup();
await assertRejects(
() => {
return nc.request("q", Empty, { timeout: 100 });
},
errors.RequestError,
"no responders: 'q'",
);
await cleanup(ns, nc);
});
Deno.test("basics - request no responders noMux", async () => {
const { ns, nc } = await setup();
await assertRejects(
() => {
return nc.request("q", Empty, { timeout: 100, noMux: true });
},
errors.RequestError,
"no responders: 'q'",
);
await cleanup(ns, nc);
});
Deno.test("basics - request timeout", async () => {
const { ns, nc } = await setup();
const s = createInbox();
nc.subscribe(s, { callback: () => {} });
await assertRejects(() => {
return nc.request(s, Empty, { timeout: 100 });
}, errors.TimeoutError);
await cleanup(ns, nc);
});
Deno.test("basics - request timeout noMux", async () => {
const { ns, nc } = await setup();
const s = createInbox();
nc.subscribe(s, { callback: () => {} });
await assertRejects(() => {
return nc.request(s, Empty, { timeout: 100, noMux: true });
}, errors.TimeoutError);
await cleanup(ns, nc);
});
Deno.test("basics - request cancel rejects", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const s = createInbox();
const check = assertRejects(
() => {
return nc.request(s, Empty, { timeout: 1000 });
},
errors.RequestError,
"cancelled",
);
nci.protocol.muxSubscriptions.reqs.forEach((v) => {
v.cancel();
});
await check;
await cleanup(ns, nc);
});
Deno.test("basics - old style requests", async () => {
const { ns, nc } = await setup();
nc.subscribe("q", {
callback: (_err, msg) => {
msg.respond("hello");
},
});
const m = await nc.request(
"q",
Empty,
{ reply: "bar", noMux: true, timeout: 1000 },
);
assertEquals("hello", m.string());
assertEquals("bar", m.subject);
await cleanup(ns, nc);
});
Deno.test("basics - reply can only be used with noMux", async () => {
const { ns, nc } = await setup();
nc.subscribe("q", {
callback: (_err, msg) => {
msg.respond("hello");
},
});
await assertRejects(
() => {
return nc.request("q", Empty, { reply: "bar", timeout: 1000 });
},
errors.InvalidArgumentError,
"'reply','noMux' are mutually exclusive",
);
await cleanup(ns, nc);
});
Deno.test("basics - request with headers", async () => {
const { ns, nc } = await setup();
const s = createInbox();
const sub = nc.subscribe(s);
(async () => {
for await (const m of sub) {
const headerContent = m.headers?.get("test-header");
m.respond(`header content: ${headerContent}`);
}
})().then();
const requestHeaders = headers();
requestHeaders.append("test-header", "Hello, world!");
const msg = await nc.request(s, Empty, {
headers: requestHeaders,
timeout: 5000,
});
assertEquals(msg.string(), "header content: Hello, world!");
await cleanup(ns, nc);
});
Deno.test("basics - request with headers and custom subject", async () => {
const { ns, nc } = await setup();
const s = createInbox();
const sub = nc.subscribe(s);
(async () => {
for await (const m of sub) {
const headerContent = m.headers?.get("test-header");
m.respond(`header content: ${headerContent}`);
}
})().then();
const requestHeaders = headers();
requestHeaders.append("test-header", "Hello, world!");
const msg = await nc.request(s, Empty, {
headers: requestHeaders,
timeout: 5000,
reply: "reply-subject",
noMux: true,
});
assertEquals(msg.string(), "header content: Hello, world!");
await cleanup(ns, nc);
});
Deno.test("basics - request requires a subject", async () => {
const { ns, nc } = await setup();
await assertRejects(
() => {
//@ts-ignore: testing
return nc.request();
},
errors.InvalidSubjectError,
"illegal subject: ''",
);
await cleanup(ns, nc);
});
Deno.test("basics - closed returns error", async () => {
const { ns, nc } = await setup({}, { reconnect: false });
setTimeout(() => {
(nc as NatsConnectionImpl).protocol.sendCommand("Y\r\n");
}, 100);
const done = await nc.closed();
assertInstanceOf(done, errors.ProtocolError);
await cleanup(ns, nc);
});
Deno.test("basics - subscription with timeout", async () => {
const { ns, nc } = await setup();
const sub = nc.subscribe(createInbox(), { max: 1, timeout: 250 });
await assertRejects(
async () => {
for await (const _m of sub) {
// ignored
}
},
errors.TimeoutError,
"timeout",
);
await cleanup(ns, nc);
});
Deno.test("basics - subscription expecting 2 doesn't fire timeout", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj, { max: 2, timeout: 500 });
(async () => {
for await (const _m of sub) {
// ignored
}
})().catch((err) => {
fail(err);
});
nc.publish(subj);
await nc.flush();
await delay(1000);
assertEquals(sub.getReceived(), 1);
await cleanup(ns, nc);
});
Deno.test("basics - subscription timeout auto cancels", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
let c = 0;
const sub = nc.subscribe(subj, { max: 2, timeout: 300 });
(async () => {
for await (const _m of sub) {
c++;
}
})().catch((err) => {
fail(err);
});
nc.publish(subj);
nc.publish(subj);
await delay(500);
assertEquals(c, 2);
await cleanup(ns, nc);
});
Deno.test("basics - no mux requests create normal subs", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
nc.request(createInbox(), Empty, { timeout: 1000, noMux: true }).then();
assertEquals(nci.protocol.subscriptions.size(), 1);
assertEquals(nci.protocol.muxSubscriptions.size(), 0);
const sub = nci.protocol.subscriptions.get(1);
assert(sub);
assertEquals(sub.max, 1);
sub.unsubscribe();
assertEquals(nci.protocol.subscriptions.size(), 0);
await cleanup(ns, nc);
});
Deno.test("basics - no mux requests timeout", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
nc.subscribe(subj, { callback: () => {} });
await assertRejects(
() => {
return nc.request(subj, Empty, { timeout: 500, noMux: true });
},
errors.TimeoutError,
);
await cleanup(ns, nc);
});
Deno.test("basics - no mux requests", async () => {
const { ns, nc } = await setup({ max_payload: 2048 });
const subj = createInbox();
const sub = nc.subscribe(subj);
const data = Uint8Array.from([1234]);
(async () => {
for await (const m of sub) {
m.respond(data);
}
})().then();
const m = await nc.request(subj, Empty, { timeout: 1000, noMux: true });
assertEquals(m.data, data);
await cleanup(ns, nc);
});
Deno.test("basics - no mux request timeout doesn't leak subs", async () => {
const { ns, nc } = await setup();
nc.subscribe("q", { callback: () => {} });
const nci = nc as NatsConnectionImpl;
assertEquals(nci.protocol.subscriptions.size(), 1);
await assertRejects(
() => {
return nc.request("q", Empty, { noMux: true, timeout: 1000 });
},
errors.TimeoutError,
);
assertEquals(nci.protocol.subscriptions.size(), 1);
await cleanup(ns, nc);
});
Deno.test("basics - no mux request no responders doesn't leak subs", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
assertEquals(nci.protocol.subscriptions.size(), 0);
await assertRejects(() => {
return nc.request("q", Empty, { noMux: true, timeout: 500 });
});
assertEquals(nci.protocol.subscriptions.size(), 0);
await cleanup(ns, nc);
});
Deno.test("basics - no mux request no perms doesn't leak subs", async () => {
const { ns, nc } = await setup({
authorization: {
users: [{
user: "s",
password: "s",
permission: {
publish: "q",
subscribe: "response",
allow_responses: true,
},
}],
},
}, { user: "s", pass: "s" });
const nci = nc as NatsConnectionImpl;
assertEquals(nci.protocol.subscriptions.size(), 0);
await assertRejects(
async () => {
await nc.request("qq", Empty, {
noMux: true,
reply: "response",
timeout: 1000,
});
},
Error,
"Permissions Violation for Publish",
);
await assertRejects(
async () => {
await nc.request("q", Empty, { noMux: true, reply: "r", timeout: 1000 });
},
Error,
"Permissions Violation for Subscription",
);
assertEquals(nci.protocol.subscriptions.size(), 0);
await cleanup(ns, nc);
});
Deno.test("basics - max_payload errors", async () => {
const { ns, nc } = await setup({ max_payload: 2048 });
const nci = nc as NatsConnectionImpl;
assert(nci.protocol.info);
const big = new Uint8Array(nci.protocol.info.max_payload + 1);
assertThrows(
() => {
nc.publish("foo", big);
},
errors.InvalidArgumentError,
`payload size exceeded`,
);
assertRejects(
() => {
return nc.request("foo", big);
},
errors.InvalidArgumentError,
`payload size exceeded`,
);
const d = deferred();
setTimeout(() => {
nc.request("foo").catch((err) => {
d.reject(err);
});
});
const sub = nc.subscribe("foo");
for await (const m of sub) {
assertThrows(
() => {
m.respond(big);
},
errors.InvalidArgumentError,
`payload size exceeded`,
);
break;
}
await assertRejects(
() => {
return d;
},
errors.TimeoutError,
"timeout",
);
await cleanup(ns, nc);
});
Deno.test("basics - close cancels requests", async () => {
const { ns, nc } = await setup();
nc.subscribe("q", { callback: () => {} });
const done = assertRejects(
() => {
return nc.request("q");
},
errors.RequestError,
"connection closed",
);
await nc.close();
await done;
await cleanup(ns, nc);
});
Deno.test("basics - empty message", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const mp = deferred<Msg>();
const sub = nc.subscribe(subj);
(async () => {
for await (const m of sub) {
mp.resolve(m);
break;
}
})().then();
nc.publish(subj);
const m = await mp;
assertEquals(m.subject, subj);
assertEquals(m.data.length, 0);
await cleanup(ns, nc);
});
Deno.test("basics - msg buffers dont overwrite", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const N = 100;
const sub = nc.subscribe(">");
const msgs: Msg[] = [];
(async () => {
for await (const m of sub) {
msgs.push(m);
}
})().then();
const a = "a".charCodeAt(0);
const fill = (n: number, b: Uint8Array) => {
const v = n % 26 + a;
for (let i = 0; i < b.length; i++) {
b[i] = v;
}
};
const td = new TextDecoder();
assert(nci.protocol.info);
const buf = new Uint8Array(nci.protocol.info.max_payload);
for (let i = 0; i < N; i++) {
fill(i, buf);
const subj = td.decode(buf.subarray(0, 26));
nc.publish(subj, buf, { reply: subj });
await nc.flush();
}
await nc.drain();
await ns.stop();
const check = (n: number, m: Msg) => {
const v = n % 26 + a;
assert(nci.protocol.info);
assertEquals(m.data.length, nci.protocol.info.max_payload);
for (let i = 0; i < m.data.length; i++) {
if (m.data[i] !== v) {
fail(
`failed on iteration ${i} - expected ${String.fromCharCode(v)} got ${
String.fromCharCode(m.data[i])
}`,
);
}
}
assertEquals(m.subject, td.decode(m.data.subarray(0, 26)), "subject check");
assertEquals(m.reply, td.decode(m.data.subarray(0, 26)), "reply check");
};
assertEquals(msgs.length, N);
for (let i = 0; i < N; i++) {
check(i, msgs[i]);
}
});
Deno.test("basics - get client ip", async () => {
const ns = await NatsServer.start();
const nc = await connect({ servers: `localhost:${ns.port}` });
const ip = nc.info?.client_ip || "";
assertEquals(isIP(ip), true);
await nc.close();
assert(nc.info === undefined);
await ns.stop();
});
Deno.test("basics - subs pending count", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj, { max: 10 });
const done = (async () => {
let count = 0;
for await (const _m of sub) {
count++;
assertEquals(count, sub.getProcessed());
console.log({ processed: sub.getProcessed(), pending: sub.getPending() });
assertEquals(sub.getProcessed() + sub.getPending(), 10);
}
})();
for (let i = 0; i < 10; i++) {
nc.publish(subj);
}
await nc.flush();
await done;
await cleanup(ns, nc);
});
Deno.test("basics - create inbox", () => {
type inout = [string, string, boolean?];
const t: inout[] = [];
t.push(["", "_INBOX."]);
//@ts-ignore testing
t.push([undefined, "_INBOX."]);
//@ts-ignore testing
t.push([null, "_INBOX."]);
//@ts-ignore testing
t.push([5, "5.", true]);
t.push(["hello", "hello."]);
t.forEach((v, index) => {
if (v[2]) {
assertThrows(() => {
createInbox(v[0]);
});
} else {
const out = createInbox(v[0]);
assert(out.startsWith(v[1]), `test ${index}`);
}
});
});
Deno.test("basics - custom prefix", async () => {
const { ns, nc } = await setup({}, { inboxPrefix: "_x" });
const subj = createInbox();
nc.subscribe(subj, {
max: 1,
callback: (_err, msg) => {
msg.respond();
},
});
const v = await nc.request(subj);
assert(v.subject.startsWith("_x."));
await cleanup(ns, nc);
});
Deno.test("basics - custom prefix noMux", async () => {
const { ns, nc } = await setup({}, { inboxPrefix: "_y" });
const subj = createInbox();
nc.subscribe(subj, {
max: 1,
callback: (_err, msg) => {
msg.respond();
},
});
const v = await nc.request(subj);
assert(v.subject.startsWith("_y."));
await cleanup(ns, nc);
});
Deno.test("basics - debug", async () => {
const { ns, nc } = await setup({}, { debug: true });
await nc.flush();
await cleanup(ns, nc);
assertEquals(nc.isClosed(), true);
});
Deno.test("basics - subscription with timeout cancels on message", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const sub = nc.subscribe(subj, { max: 1, timeout: 500 }) as SubscriptionImpl;
assert(sub.timer !== undefined);
const done = (async () => {
for await (const _m of sub) {
assertEquals(sub.timer, undefined);
}
})();
nc.publish(subj);
await done;
await cleanup(ns, nc);
});
Deno.test("basics - subscription cb with timeout cancels on message", async () => {
const { ns, nc } = await setup();
const subj = createInbox();
const done = Lock();
const sub = nc.subscribe(subj, {
max: 1,
timeout: 500,
callback: () => {
done.unlock();
},
}) as SubscriptionImpl;
assert(sub.timer !== undefined);
nc.publish(subj);
await done;
assertEquals(sub.timer, undefined);
await cleanup(ns, nc);
});
Deno.test("basics - resolve", async () => {
const nci = await connect({
servers: "demo.nats.io",
}) as NatsConnectionImpl;
await nci.flush();
const srv = nci.protocol.servers.getCurrentServer();
assert(srv.resolves && srv.resolves.length > 1);
await nci.close();
});
Deno.test("basics - port and server are mutually exclusive", async () => {
await assertRejects(
async () => {
await connect({ servers: "localhost", port: 4222 });
},
errors.InvalidArgumentError,
"'servers','port' are mutually exclusive",
undefined,
);
});
Deno.test("basics - rtt", async () => {
const { ns, nc } = await setup({}, {
maxReconnectAttempts: 1,
reconnectTimeWait: 750,
});
const rtt = await nc.rtt();
assert(rtt >= 0);
await ns.stop();
await assertRejects(
() => {
return nc.rtt();
},
errors.RequestError,
"disconnected",
);
await nc.closed();
await assertRejects(() => {
return nc.rtt();
}, errors.ClosedConnectionError);
});
Deno.test("basics - request many count", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
nc.subscribe(subj, {
callback: (_err, msg) => {
for (let i = 0; i < 5; i++) {
msg.respond();
}
},
});
const lock = Lock(5, 2000);
const iter = await nci.requestMany(subj, Empty, {
strategy: "count",
maxWait: 2000,
maxMessages: 5,
});
for await (const mer of iter) {
if (mer instanceof Error) {
fail(mer.message);
}
lock.unlock();
}
await lock;
await cleanup(ns, nc);
});
Deno.test("basics - request many jitter", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
nc.subscribe(subj, {
callback: (_err, msg) => {
for (let i = 0; i < 10; i++) {
msg.respond();
}
},
});
let count = 0;
const start = Date.now();
const iter = await nci.requestMany(subj, Empty, {
strategy: "stall",
maxWait: 5000,
});
for await (const mer of iter) {
if (mer instanceof Error) {
fail(mer.message);
}
count++;
}
const time = Date.now() - start;
assert(500 > time);
assertEquals(count, 10);
await cleanup(ns, nc);
});
Deno.test("basics - request many sentinel", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
nc.subscribe(subj, {
callback: (_err, msg) => {
for (let i = 0; i < 10; i++) {
msg.respond("hello");
}
msg.respond();
},
});
let count = 0;
const start = Date.now();
const iter = await nci.requestMany(subj, Empty, {
strategy: "sentinel",
maxWait: 2000,
});
for await (const mer of iter) {
if (mer instanceof Error) {
fail(mer.message);
}
count++;
}
const time = Date.now() - start;
assert(500 > time);
assertEquals(count, 11);
await cleanup(ns, nc);
});
Deno.test("basics - request many sentinel - partial response", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
nc.subscribe(subj, {
callback: (_err, msg) => {
for (let i = 0; i < 10; i++) {
msg.respond("hello");
}
},
});
let count = 0;
const start = Date.now();
const iter = await nci.requestMany(subj, Empty, {
strategy: "sentinel",
maxWait: 2000,
});
for await (const mer of iter) {
if (mer instanceof Error) {
fail(mer.message);
}
count++;
}
const time = Date.now() - start;
assert(time >= 2000);
assertEquals(count, 10);
await cleanup(ns, nc);
});
Deno.test("basics - request many wait for timer - no respone", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
nc.subscribe(subj, {
callback: () => {
// ignore it
},
});
let count = 0;
const start = Date.now();
const iter = await nci.requestMany(subj, Empty, {
strategy: "timer",
maxWait: 2000,
});
for await (const mer of iter) {
if (mer instanceof Error) {
fail(mer.message);
}
count++;
}
const time = Date.now() - start;
assert(time >= 2000);
assertEquals(count, 0);
await cleanup(ns, nc);
});
Deno.test("basics - request many waits for timer late response", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
const subj = createInbox();
nc.subscribe(subj, {
callback: (_err, msg) => {
delay(1759).then(() => msg.respond());
},
});
let count = 0;
const start = Date.now();
const iter = await nci.requestMany(subj, Empty, {
strategy: "timer",
maxWait: 2000,
});
for await (const mer of iter) {
if (mer instanceof Error) {
fail(mer.message);
}
count++;
}
const time = Date.now() - start;
assert(time >= 2000);
assertEquals(count, 1);
await cleanup(ns, nc);
});
Deno.test("basics - server version", async () => {
const { ns, nc } = await setup();
const nci = nc as NatsConnectionImpl;
assertEquals(nci.protocol.features.require("3.0.0"), false);
assertEquals(nci.protocol.features.require("2.8.2"), true);
const ok = nci.features.require("2.8.3");
const bytes = nci.features.get(Feature.JS_PULL_MAX_BYTES);
assertEquals(ok, bytes.ok);
assertEquals(bytes.min, "2.8.3");
assertEquals(ok, nci.protocol.features.supports(Feature.JS_PULL_MAX_BYTES));
await cleanup(ns, nc);
});
Deno.test("basics - info", async () => {
const { ns, nc } = await setup();
assertExists(nc.info);
await cleanup(ns, nc);
});
Deno.test("basics - initial connect error", async () => {
const listener = Deno.listen({ port: 0 });
const port = (listener.addr as Deno.NetAddr).port;
const INFO = new TextEncoder().encode(
`INFO {"server_id":"FAKE","server_name":"FAKE","version":"2.9.4","proto":1,"go":"go1.19.2","host":"127.0.0.1","port":${port},"headers":true,"max_payload":1048576,"jetstream":true,"client_id":4,"client_ip":"127.0.0.1"}\r\n`,
);
const done = (async () => {
for await (const conn of listener) {
await conn.write(INFO);
setTimeout(() => {
conn.close();
});
}
})();
const err = await assertRejects(() => {
return connect({ port, reconnect: false });
});
assert(
err instanceof errors.ConnectionError ||
err instanceof Deno.errors.ConnectionReset,
);
listener.close();
await done;
});
Deno.test("basics - close promise resolves", async () => {
const ns = await NatsServer.start();
const nc = await connect({ port: ns.port, reconnect: false });
const results = await Promise.all([nc.closed(), nc.close()]);
assertEquals(results[0], undefined);
await ns.stop();
});
Deno.test("basics - inbox prefixes cannot have wildcards", async () => {
await assertRejects(
async () => {
await connect({ inboxPrefix: "_inbox.foo.>" });
},
errors.InvalidArgumentError,
"'prefix' cannot have wildcards",
);
assertThrows(
() => {
createInbox("_inbox.foo.*");
},
errors.InvalidArgumentError,
"'prefix' cannot have wildcards",
);
});
Deno.test("basics - msg typed payload", async () => {
const ns = await NatsServer.start();
const nc = await connect({ port: ns.port });
nc.subscribe("echo", {
callback: (_err: Error | null, msg: Msg) => {
msg.respond(msg.data);
},
});
assertEquals((await nc.request("echo", Empty)).string(), "");
assertEquals((await nc.request("echo", "hello")).string(), "hello");
assertEquals((await nc.request("echo", "5")).string(), "5");
await assertRejects(
async () => {
const r = await nc.request("echo", Empty);
r.json<number>();
},
Error,
"Unexpected end of JSON input",
);
assertEquals((await nc.request("echo", JSON.stringify(null))).json(), null);
assertEquals((await nc.request("echo", JSON.stringify(5))).json(), 5);
assertEquals(
(await nc.request("echo", JSON.stringify("hello"))).json(),
"hello",
);
assertEquals((await nc.request("echo", JSON.stringify(["hello"]))).json(), [
"hello",
]);
assertEquals(
(await nc.request("echo", JSON.stringify({ one: "two" }))).json(),
{ one: "two" },
);
assertEquals(
(await nc.request("echo", JSON.stringify([{ one: "two" }]))).json(),
[{ one: "two" }],
);
await cleanup(ns, nc);
});
Deno.test("basics - ipv4 mapped to ipv6", async () => {
const ns = await NatsServer.start({ port: 4222 });
const nc = await connect({ servers: [`::ffff:127.0.0.1`] });
const nc2 = await connect({ servers: [`[::ffff:127.0.0.1]:${ns.port}`] });
await cleanup(ns, nc, nc2);
});
Deno.test("basics - data types empty", async () => {
const { ns, nc } = await setup();
const subj = nuid.next();
nc.subscribe(subj, {
callback: (_err, msg) => {
assertEquals(msg.data.length, 0);
msg.respond();
},
});
nc.publish(subj);
nc.publish(subj, Empty);
let r = await nc.request(subj);
assertEquals(r.data.length, 0);
r = await nc.request(subj, Empty);
assertEquals(r.data.length, 0);
let iter = await collect(
await nc.requestMany(subj, undefined, { maxMessages: 1 }),
);
assertEquals(iter[0].data.length, 0);
iter = await collect(
await nc.requestMany(subj, undefined, { maxMessages: 1 }),
);
assertEquals(iter[0].data.length, 0);
await cleanup(ns, nc);
});
Deno.test("basics - data types string", async () => {
const { ns, nc } = await setup();
const subj = nuid.next();
nc.subscribe(subj, {
callback: (_err, msg) => {
const s = msg.string();
if (s.length > 0) {
assertEquals(s, "hello");
}
msg.respond(s);
},
});
nc.publish(subj, "");
nc.publish(subj, "hello");
let r = await nc.request(subj);
assertEquals(r.string(), "");
r = await nc.request(subj, "hello");
assertEquals(r.string(), "hello");
let iter = await collect(
await nc.requestMany(subj, "", { maxMessages: 1 }),
);
assertEquals(iter[0].string(), "");
iter = await collect(
await nc.requestMany(subj, "hello", { maxMessages: 1 }),
);
assertEquals(iter[0].string(), "hello");
await cleanup(ns, nc);
});
Deno.test("basics - json reviver", async () => {
const { ns, nc } = await setup();
const subj = nuid.next();
nc.subscribe(subj, {
callback: (_err, msg) => {
msg.respond(JSON.stringify({ date: Date.now(), auth: true }));
},
});
const m = await nc.request(subj);
const d = m.json<{ date: Date; auth: string }>((key, value) => {
if (typeof value === "boolean") {
return value ? "yes" : "no";
}
switch (key) {
case "date":
return new Date(value);
default:
return value;
}
});
assert(d.date instanceof Date);
assert(typeof d.auth === "string");
await cleanup(ns, nc);
});
Deno.test("basics - sync subscription", async () => {
const { ns, nc } = await setup();
const subj = nuid.next();
const sub = nc.subscribe(subj);
const sync = syncIterator(sub);
nc.publish(subj);
let m = await sync.next();
assertExists(m);
sub.unsubscribe();
m = await sync.next();
assertEquals(m, null);
await cleanup(ns, nc);
});
Deno.test("basics - publish message", async () => {
const { ns, nc } = await setup();
const sub = nc.subscribe("q");
const nis = new MM(nc);
nis.data = new TextEncoder().encode("not in service");
(async () => {
for await (const m of sub) {
if (m.reply) {
nis.subject = m.reply;
nc.publishMessage(nis);
}
}
})().then();
const r = await nc.request("q");
assertEquals(r.string(), "not in service");
await cleanup(ns, nc);
});
Deno.test("basics - respond message", async () => {
const { ns, nc } = await setup();
const sub = nc.subscribe("q");
const nis = new MM(nc);
nis.data = new TextEncoder().encode("not in service");
(async () => {
for await (const m of sub) {
if (m.reply) {
nis.reply = m.reply;
nc.respondMessage(nis);
}
}
})().then();
const r = await nc.request("q");
assertEquals(r.string(), "not in service");
await cleanup(ns, nc);
});
Deno.test("basics - resolve false", async () => {
const nci = await connect({
servers: "demo.nats.io",
resolve: false,
}) as NatsConnectionImpl;
const srv = nci.protocol.servers.getCurrentServer();
assertEquals(srv.resolves, undefined);
await nci.close();
});
Deno.test("basics - stats", async () => {
const { ns, nc } = await setup();
const cid = nc.info?.client_id || -1;
if (cid === -1) {
fail("client_id not found");
}
async function check(m = ""): Promise<void> {
await nc.flush();
const client = nc.stats();
const { in_msgs, out_msgs, in_bytes, out_bytes } =
(await ns.connz(cid, "detail")).connections[0];
const server = { in_msgs, out_msgs, in_bytes, out_bytes };
console.log(m, client, server);
assertEquals(client.outBytes, in_bytes);
assertEquals(client.inBytes, out_bytes);
assertEquals(client.outMsgs, in_msgs);
assertEquals(client.inMsgs, out_msgs);
}
await check("start");
// publish
nc.publish("hello", "world");
await check("simple publish");
nc.subscribe("hello", { callback: () => {} });
nc.publish("hello", "hi");
await check("subscribe");
const h = headers();
h.set("hello", "very long value that we want to add here");
nc.publish("hello", "hello", { headers: h });
await check("headers");
await cleanup(ns, nc);
});
Deno.test("basics - slow", async () => {
const { ns, nc } = await setup();
let slow = 0;
(async () => {
for await (const m of nc.status()) {
//@ts-ignore: test
if (m.type === "slowConsumer") {
console.log(`sub: ${m.sub.getID()}`);
slow++;
}
}
})().catch();
const sub = nc.subscribe("test", { slow: 10 });
const s = syncIterator(sub);
// we go over, should have a notification
for (let i = 0; i < 11; i++) {
nc.publish("test", "");
}
await delay(100);
assertEquals(sub.getPending(), 11);
assertEquals(slow, 1);
slow = 0;
// send one more, no more notifications until we drop below 10
nc.publish("test", "");
await nc.flush(); // 12
await delay(100);
assertEquals(sub.getPending(), 12);
assertEquals(slow, 0);
await s.next(); // 11
await s.next(); // 10
await s.next(); // 9
nc.publish("test", ""); // 10
await nc.flush();
await delay(100);
assertEquals(sub.getPending(), 10);
assertEquals(slow, 0);
// now this will notify
await s.next(); // 9
await s.next(); // 8
await nc.flush();
await delay(100);
assertEquals(sub.getPending(), 8);
await s.next(); // 7
nc.publish("test", ""); // 8
await nc.flush();
await delay(100);
assertEquals(sub.getPending(), 8);
assertEquals(slow, 0);
nc.publish("test", ""); // 9
nc.publish("test", ""); // 10
await nc.flush();
await delay(100);
assertEquals(sub.getPending(), 10);
assertEquals(slow, 0);
nc.publish("test", ""); // 11
await nc.flush();
await delay(100);
assertEquals(sub.getPending(), 11);
assertEquals(slow, 1);
await cleanup(ns, nc);
});
Deno.test("basics - msg sids", async () => {
const { ns, nc } = await setup();
const lock = Lock(2);
let first: Msg;
let second: Msg;
const sub = nc.subscribe("test", {
callback: (_, msg) => {
first = msg;
lock.unlock();
},
});
const sub2 = nc.subscribe(">", {
callback: (_, msg) => {
second = msg;
lock.unlock();
},
});
nc.publish("test", "hello", { reply: "foo" });
await lock;
assertEquals(first!.sid, sub.getID());
assertEquals(
(first! as MsgImpl).size(),
"hello".length + "test".length + "foo".length,
);
assertEquals(second!.sid, sub2.getID());
await cleanup(ns, nc);
});
class MM implements Msg {
data!: Uint8Array;
sid: number;
subject!: string;
reply?: string;
headers?: MsgHdrs;
publisher: Publisher;
constructor(p: Publisher) {
this.publisher = p;
this.sid = -1;
}
json<T>(): T {
throw new Error("not implemented");
}
respond(payload?: Payload, opts?: PublishOptions): boolean {
if (!this.reply) {
return false;
}
payload = payload || Empty;
this.publisher.publish(this.reply, payload, opts);
return true;
}
respondMessage(m: Msg): boolean {
return this.respond(m.data, { headers: m.headers, reply: m.reply });
}
string(): string {
return "";
}
}
Deno.test("basics - internal close listener", async () => {
const ns = await NatsServer.start();
const port = ns.port;
// nothing bad should happen if none registered
let nc = await connect({ port }) as NatsConnectionImpl;
await nc.close();
function makeListener(d: Deferred<unknown>): ConnectionClosedListener {
return {
connectionClosedCallback: () => {
d.resolve();
},
};
}
// can add and remove
nc = await connect({ port }) as NatsConnectionImpl;
let done = deferred();
let listener = makeListener(done);
(nc as NatsConnectionImpl).addCloseListener(listener);
// @ts-ignore: internal
assertEquals((nc as NatsConnectionImpl).closeListeners.listeners.length, 1);
(nc as NatsConnectionImpl).removeCloseListener(listener);
// @ts-ignore: internal
assertEquals((nc as NatsConnectionImpl).closeListeners.listeners.length, 0);
await nc.close();
done.resolve();
await done;
// closed called
nc = await connect({ port }) as NatsConnectionImpl;
done = deferred();
listener = makeListener(done);
(nc as NatsConnectionImpl).addCloseListener(listener);
await nc.close();
await done;
// @ts-ignore: internal
assertEquals((nc as NatsConnectionImpl).closeListeners.listeners.length, 0);
await ns.stop();
});
Deno.test("basics - publish tracing", async () => {
const { ns, nc } = await setup();
const sub = nc.subscribe("foo", { callback: () => {} });
const traces = nc.subscribe("traces", {
callback: () => {},
max: 2,
});
nc.flush();
nc.publish("foo", Empty, { traceDestination: "traces" });
nc.publish("foo", Empty, { traceDestination: "traces", traceOnly: true });
await traces.closed;
assertEquals(sub.getReceived(), 1);
assertEquals(traces.getReceived(), 2);
await cleanup(ns, nc);
});
Deno.test("basics - request tracing", async () => {
const { ns, nc } = await setup();
const sub = nc.subscribe("foo", {
callback: (_, m) => {
m.respond();
},
});
const traces = nc.subscribe("traces", {
callback: () => {},
max: 2,
});
nc.flush();
await nc.request("foo", Empty, {
timeout: 2_000,
traceDestination: "traces",
});
await assertRejects(() => {
return nc.request("foo", Empty, {
timeout: 2_000,
traceDestination: "traces",
traceOnly: true,
});
});
await traces.closed;
assertEquals(sub.getReceived(), 1);
assertEquals(traces.getReceived(), 2);
await cleanup(ns, nc);
});
Deno.test("basics - close status", async () => {
const { ns, nc } = await setup();
setTimeout(() => {
nc.close();
}, 500);
const d = deferred();
for await (const s of nc.status()) {
if (s.type === "close") {
d.resolve();
}
}
await d;
await cleanup(ns, nc);
});
Deno.test("basics - pub subject verified", async () => {
const { ns, nc } = await setup();
assertThrows(
() => {
// subject that encodes a protocol pub...
nc.publish("foo 6\r\npwntus\r\nPUB bar");
},
Error,
"illegal subject",
);
await cleanup(ns, nc);
});